diff --git a/Libs/Analyze/MeshGenerator.cpp b/Libs/Analyze/MeshGenerator.cpp index 06566ff98e..85a0b084dd 100644 --- a/Libs/Analyze/MeshGenerator.cpp +++ b/Libs/Analyze/MeshGenerator.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -16,7 +17,6 @@ #include #include -#include namespace shapeworks { @@ -99,12 +99,16 @@ MeshHandle MeshGenerator::build_mesh_from_image(ImageType::Pointer image, float try { // only interested in 1's and 0's - Image itk_image = Image(image); - if (!itk_image.isDistanceTransform()) { - itk_image.binarize(0, 1); - image = itk_image.getITKImage(); + Image sw_image = Image(image); + if (!sw_image.isDistanceTransform()) { + sw_image.binarize(); + image = sw_image.getITKImage(); } + // pad the image in case the segmentation is on the edge + sw_image.pad(1); + image = sw_image.getITKImage(); + // connect to VTK vtkSmartPointer vtk_image = vtkSmartPointer::New(); itk::VTKImageExport::Pointer itk_exporter = itk::VTKImageExport::New(); @@ -165,6 +169,8 @@ MeshHandle MeshGenerator::build_mesh_from_file(std::string filename, float iso_v } } else if (is_image) { try { + ImageUtils::register_itk_factories(); + // read file using ITK using ReaderType = itk::ImageFileReader; ReaderType::Pointer reader = ReaderType::New(); diff --git a/Libs/Analyze/MeshWorker.cpp b/Libs/Analyze/MeshWorker.cpp index 5832e2a7fa..83c98e1cac 100644 --- a/Libs/Analyze/MeshWorker.cpp +++ b/Libs/Analyze/MeshWorker.cpp @@ -19,6 +19,9 @@ void MeshWorker::run() { // build the mesh using our MeshGenerator auto item = this->queue_->get_next_work_item(); + if (!item) { + return; + } MeshHandle mesh = this->mesh_generator_->build_mesh(*item); diff --git a/Libs/Analyze/Shape.cpp b/Libs/Analyze/Shape.cpp index 452646a854..08a49f17b5 100644 --- a/Libs/Analyze/Shape.cpp +++ b/Libs/Analyze/Shape.cpp @@ -125,10 +125,8 @@ void Shape::recompute_original_surface() { original_meshes_.set_mesh(0, mesh_handle); } - //--------------------------------------------------------------------------- -void Shape::ensure_segmentation() -{ +void Shape::ensure_segmentation() { if (get_segmentation()) { return; } @@ -159,8 +157,7 @@ void Shape::ensure_segmentation() //--------------------------------------------------------------------------- MeshGroup Shape::get_groomed_meshes(bool wait) { if (!subject_) { - std::cerr << "Error: asked for groomed meshes when none are present!\n"; - assert(0); + return {}; } if (!groomed_meshes_.valid() || groomed_meshes_.meshes().size() != subject_->get_number_of_domains()) { diff --git a/Libs/Common/Logging.cpp b/Libs/Common/Logging.cpp index 4868020765..9290819d4a 100644 --- a/Libs/Common/Logging.cpp +++ b/Libs/Common/Logging.cpp @@ -13,11 +13,11 @@ namespace shapeworks { static std::string create_header(const int line, const char* filename, const char* function = "") { const char* name = (strrchr(filename, '/') ? strrchr(filename, '/') + 1 : filename); const char* name2 = (strrchr(name, '\\') ? strrchr(name, '\\') + 1 : name); - const char* function_name = (strrchr(function, ':') ? strrchr(function, ':') + 1 : function); if (!function) { std::string header = "[" + std::string(name2) + "|" + std::to_string(line) + "]"; return header; } else { + const char* function_name = (strrchr(function, ':') ? strrchr(function, ':') + 1 : function); std::string header = "[" + std::string(name2) + "|" + std::string(function_name) + "|" + std::to_string(line) + "]"; return header; } @@ -69,6 +69,14 @@ void Logging::log_message(const std::string& message, const int line, const char } } +//----------------------------------------------------------------------------- +void Logging::log_only(const std::string& message, const int line, const char* file) const { + spd::info(message); + if (log_open_) { + spd::get("file")->info(message); + } +} + //----------------------------------------------------------------------------- void Logging::log_stack(const std::string& message) const { spd::error(message); diff --git a/Libs/Common/Logging.h b/Libs/Common/Logging.h index 08c541a93c..1e3961097f 100644 --- a/Libs/Common/Logging.h +++ b/Libs/Common/Logging.h @@ -102,6 +102,9 @@ class Logging { //! Log a message, use SW_LOG macro void log_message(const std::string& message, const int line, const char* file) const; + //! Log a message, use SW_LOG_ONLY macro + void log_only(const std::string& message, const int line, const char* file) const; + //! Log a stack trace message, use SW_LOG_STACK macro void log_stack(const std::string& message) const; @@ -171,6 +174,10 @@ class Logging { #define SW_LOG(message, ...) \ shapeworks::Logging::Instance().log_message(safe_format(message, ##__VA_ARGS__), __LINE__, __FILE__); +//! Log only macro +#define SW_LOG_ONLY(message, ...) \ + shapeworks::Logging::Instance().log_only(safe_format(message, ##__VA_ARGS__), __LINE__, __FILE__); + //! Log warning macro #define SW_WARN(message, ...) \ shapeworks::Logging::Instance().log_warning(safe_format(message, ##__VA_ARGS__), __LINE__, __FILE__) diff --git a/Libs/Groom/GroomParameters.h b/Libs/Groom/GroomParameters.h index a1eed6498a..6161840de3 100644 --- a/Libs/Groom/GroomParameters.h +++ b/Libs/Groom/GroomParameters.h @@ -137,7 +137,6 @@ class GroomParameters { bool get_skip_grooming(); void set_skip_grooming(bool skip); - bool get_shared_boundary(); void set_shared_boundary(bool shared_boundary); @@ -152,6 +151,8 @@ class GroomParameters { void restore_defaults(); + Parameters get_parameters() const { return params_; } + // constants const static std::string GROOM_SMOOTH_VTK_LAPLACIAN_C; const static std::string GROOM_SMOOTH_VTK_WINDOWED_SINC_C; diff --git a/Libs/Image/Image.cpp b/Libs/Image/Image.cpp index 2560d83731..5d54699acf 100644 --- a/Libs/Image/Image.cpp +++ b/Libs/Image/Image.cpp @@ -44,26 +44,8 @@ #include "ShapeworksUtils.h" #include "itkTPGACLevelSetImageFilter.h" // actually a shapeworks class, not itk -// ITK image factories -#include -#include -#include - namespace shapeworks { -namespace { -void register_factories() { - static bool registered = false; - if (!registered) { - // register all the factories - itk::NrrdImageIOFactory::RegisterOneFactory(); - itk::NiftiImageIOFactory::RegisterOneFactory(); - itk::MetaImageIOFactory::RegisterOneFactory(); - registered = true; - } -} -} // namespace - Image::Image(const Dims dims) : itk_image_(ImageType::New()) { ImageType::RegionType region; region.SetSize(dims); @@ -119,7 +101,7 @@ Image& Image::operator=(Image&& img) { } Image::ImageType::Pointer Image::read(const std::string& pathname) { - register_factories(); + ImageUtils::register_itk_factories(); if (pathname.empty()) { throw std::invalid_argument("Empty pathname"); diff --git a/Libs/Image/ImageUtils.cpp b/Libs/Image/ImageUtils.cpp index c272c1fe60..09c23e4d9d 100644 --- a/Libs/Image/ImageUtils.cpp +++ b/Libs/Image/ImageUtils.cpp @@ -2,6 +2,11 @@ #include +// ITK image factories +#include +#include +#include + namespace shapeworks { PhysicalRegion ImageUtils::boundingBox(const std::vector& filenames, Image::PixelType isoValue) { @@ -39,6 +44,18 @@ PhysicalRegion ImageUtils::boundingBox(const std::vector; static TPSTransform::Pointer createWarpTransform(const std::string& source_landmarks_file, const std::string& target_landmarks_file, const int stride = 1); + + static void register_itk_factories(); + }; } // namespace shapeworks diff --git a/Libs/Mesh/MeshWarper.cpp b/Libs/Mesh/MeshWarper.cpp index dac711b0b2..f6eed42654 100644 --- a/Libs/Mesh/MeshWarper.cpp +++ b/Libs/Mesh/MeshWarper.cpp @@ -56,6 +56,9 @@ vtkSmartPointer MeshWarper::build_mesh(const Eigen::MatrixXd& parti //--------------------------------------------------------------------------- void MeshWarper::set_reference_mesh(vtkSmartPointer reference_mesh, const Eigen::MatrixXd& reference_particles, const Eigen::MatrixXd& landmarks) { + // lock so that we don't swap out the reference mesh while we are using it + std::scoped_lock lock(mutex); + if (this->incoming_reference_mesh_ == reference_mesh) { if (this->reference_particles_.size() == reference_particles.size()) { if (this->reference_particles_ == reference_particles && landmarks_points_ == landmarks) { diff --git a/Libs/Optimize/OptimizeParameters.cpp b/Libs/Optimize/OptimizeParameters.cpp index d7f8786002..3c1a70d28c 100644 --- a/Libs/Optimize/OptimizeParameters.cpp +++ b/Libs/Optimize/OptimizeParameters.cpp @@ -864,3 +864,6 @@ double OptimizeParameters::get_shared_boundary_weight() { return params_.get(Key //--------------------------------------------------------------------------- void OptimizeParameters::set_shared_boundary_weight(double value) { params_.set(Keys::shared_boundary_weight, value); } + +//--------------------------------------------------------------------------- +Parameters OptimizeParameters::get_parameters() const { return params_; } diff --git a/Libs/Optimize/OptimizeParameters.h b/Libs/Optimize/OptimizeParameters.h index 6e37f063b7..ffc85bbf9f 100644 --- a/Libs/Optimize/OptimizeParameters.h +++ b/Libs/Optimize/OptimizeParameters.h @@ -138,6 +138,8 @@ class OptimizeParameters { double get_shared_boundary_weight(); void set_shared_boundary_weight(double value); + Parameters get_parameters() const; + private: std::string get_output_prefix(); diff --git a/Python/shapeworks/shapeworks/network_analysis.py b/Python/shapeworks/shapeworks/network_analysis.py index fc61d7af4f..d5d926ba3d 100644 --- a/Python/shapeworks/shapeworks/network_analysis.py +++ b/Python/shapeworks/shapeworks/network_analysis.py @@ -297,7 +297,12 @@ def run(self): snpm = spm1d.stats.nonparam.ttest2( np.transpose(all_data[:, :, np.where(grouprs == 0)[0], :].reshape(num_pts, len(np.where(grouprs == 0)[0]))), np.transpose(all_data[:, :, np.where(grouprs == 1)[0], :].reshape(num_pts, len(np.where(grouprs == 1)[0])))) - snpmi = snpm.inference(0.05, two_tailed=True, iterations=n_iter, force_iterations=True) # assume one-sided + + alpha = 0.05 + # Specified alpha must be greater than or equal to (1/nPermTotal)=0.33333 + if (alpha < 1/n_iter): + alpha = 1/n_iter + snpmi = snpm.inference(alpha, two_tailed=True, iterations=n_iter, force_iterations=True) # assume one-sided Z = snpmi.z # flattened test statistic (i.e., t value) over only non-zero-variance nodes tradzstar = snpmi.zstar # critical test statistic (i.e., critical t value) diff --git a/Studio/Analysis/AnalysisTool.cpp b/Studio/Analysis/AnalysisTool.cpp index 059759021e..90aa126b18 100644 --- a/Studio/Analysis/AnalysisTool.cpp +++ b/Studio/Analysis/AnalysisTool.cpp @@ -345,6 +345,8 @@ void AnalysisTool::set_session(QSharedPointer session) { ui_->group1_button->setChecked(false); ui_->group2_button->setChecked(false); update_difference_particles(); + ui_->pcaSlider->setValue(0); + ui_->group_slider->setValue(10); ui_->show_predicted_scalar->setChecked(false); ui_->show_difference_to_predicted_scalar->setChecked(false); @@ -380,6 +382,7 @@ void AnalysisTool::handle_analysis_options() { ui_->pcaAnimateCheckBox->setEnabled(false); ui_->pcaModeSpinBox->setEnabled(false); pca_animate_timer_.stop(); + group_animate_timer_.stop(); ui_->pcaSlider->setEnabled(false); if (ui_->singleSamplesRadio->isChecked()) { // one sample mode @@ -406,6 +409,7 @@ void AnalysisTool::handle_analysis_options() { ui_->pcaSlider->setEnabled(true); ui_->pcaAnimateCheckBox->setEnabled(true); ui_->pcaModeSpinBox->setEnabled(true); + group_animate_timer_.stop(); auto domain_names = session_->get_project()->get_domain_names(); bool multiple_domains = domain_names.size() > 1; if (multiple_domains) { @@ -422,6 +426,7 @@ void AnalysisTool::handle_analysis_options() { ui_->pcaAnimateCheckBox->setEnabled(false); ui_->pcaModeSpinBox->setEnabled(false); pca_animate_timer_.stop(); + group_animate_timer_.stop(); } update_difference_particles(); @@ -519,6 +524,11 @@ void AnalysisTool::group_p_values_clicked() { //--------------------------------------------------------------------------- void AnalysisTool::network_analysis_clicked() { + if (ui_->network_feature->currentText().isEmpty()) { + QMessageBox::warning(this, "Network Analysis", "Project must have a scalar feature for network analysis."); + // SW_WARN("Project must have a scalar features for network analysis"); + return; + } network_analysis_job_ = QSharedPointer::create(session_->get_project(), ui_->group_combo->currentText().toStdString(), ui_->network_feature->currentText().toStdString()); @@ -602,6 +612,7 @@ bool AnalysisTool::compute_stats() { if (particles.size() == 0) { continue; // skip any that don't have particles } + if (groups_active()) { auto group = shape->get_subject()->get_group_value(group_set); if (group == left_group) { @@ -718,7 +729,6 @@ bool AnalysisTool::compute_stats() { //----------------------------------------------------------------------------- Particles AnalysisTool::get_mean_shape_points() { if (!compute_stats()) { - std::cerr << "Non buenas, returning empty particles\n"; return Particles(); } @@ -926,6 +936,7 @@ void AnalysisTool::store_settings() { //--------------------------------------------------------------------------- void AnalysisTool::shutdown() { pca_animate_timer_.stop(); + group_animate_timer_.stop(); for (const auto& worker : workers_) { if (worker) { @@ -1453,6 +1464,8 @@ void AnalysisTool::update_group_boxes() { //--------------------------------------------------------------------------- void AnalysisTool::update_group_values() { block_group_change_ = true; + stats_ready_ = false; + auto values = session_->get_project()->get_group_values(ui_->group_combo->currentText().toStdString()); if (values != current_group_values_) { @@ -2107,7 +2120,9 @@ void AnalysisTool::reconstruction_method_changed() { void AnalysisTool::set_active(bool active) { if (!active) { ui_->pcaAnimateCheckBox->setChecked(false); + ui_->group_animate_checkbox->setChecked(false); pca_animate_timer_.stop(); + group_animate_timer_.stop(); } else { auto features = session_->get_project()->get_feature_names(); ui_->network_feature->clear(); @@ -2137,6 +2152,12 @@ Particles AnalysisTool::convert_from_combined(const Eigen::VectorXd& points) { int idx = 0; for (int d = 0; d < worlds.size(); d++) { Eigen::VectorXd new_world(worlds[d].size()); + + if (idx + new_world.size() > points.size()) { + SW_WARN("Inconsistent number of values in particle vector"); + return {}; + } + for (int i = 0; i < worlds[d].size(); i++) { new_world[i] = points[idx++]; } diff --git a/Studio/Groom/GroomTool.cpp b/Studio/Groom/GroomTool.cpp index 9396a7d89b..aca6c9f2fd 100644 --- a/Studio/Groom/GroomTool.cpp +++ b/Studio/Groom/GroomTool.cpp @@ -591,6 +591,18 @@ void GroomTool::on_run_groom_button_clicked() { timer_.start(); SW_LOG("Please wait: running groom step..."); + + // log parameters + SW_LOG_ONLY("Grooming parameters:"); + + for (const auto& domain_name : session_->get_project()->get_domain_names()) { + auto params = GroomParameters(session_->get_project(), domain_name); + SW_LOG_ONLY("Domain: " + domain_name); + for (const auto& pair : params.get_parameters().get_map()) { + SW_LOG_ONLY(pair.first + ": " + pair.second); + } + } + Q_EMIT progress(0); groom_ = QSharedPointer(new Groom(session_->get_project())); diff --git a/Studio/Interface/ShapeWorksStudioApp.cpp b/Studio/Interface/ShapeWorksStudioApp.cpp index 9919b34536..771c390d3d 100644 --- a/Studio/Interface/ShapeWorksStudioApp.cpp +++ b/Studio/Interface/ShapeWorksStudioApp.cpp @@ -442,6 +442,9 @@ void ShapeWorksStudioApp::import_files(QStringList file_names) { } catch (std::runtime_error& e) { handle_error(e.what()); } + + visualizer_->reset_camera(); + handle_message("Files loaded"); handle_progress(100); } diff --git a/Studio/Interface/UpdateChecker.cpp b/Studio/Interface/UpdateChecker.cpp index 449af0a2dd..ccc3bf982e 100644 --- a/Studio/Interface/UpdateChecker.cpp +++ b/Studio/Interface/UpdateChecker.cpp @@ -50,7 +50,7 @@ void UpdateChecker::run_auto_update_check() { void UpdateChecker::run_update_check() { // check if a new version is available - QNetworkRequest request(QUrl("http://www.sci.utah.edu/~shapeworks/version.json")); + QNetworkRequest request(QUrl("https://www.sci.utah.edu/~shapeworks/version.json")); network_.get(request); } diff --git a/Studio/Optimize/OptimizeTool.cpp b/Studio/Optimize/OptimizeTool.cpp index c5225f3639..19df1af12e 100644 --- a/Studio/Optimize/OptimizeTool.cpp +++ b/Studio/Optimize/OptimizeTool.cpp @@ -222,6 +222,12 @@ void OptimizeTool::on_run_optimize_button_clicked() { optimize_parameters_->set_load_callback(std::bind(&OptimizeTool::handle_load_progress, this, std::placeholders::_1)); optimize_->SetFileOutputEnabled(false); + // log parameters + SW_LOG_ONLY("Optimization parameters:"); + for (const auto& pair : optimize_parameters_->get_parameters().get_map()) { + SW_LOG_ONLY(pair.first + ": " + pair.second); + } + ShapeworksWorker* worker = new ShapeworksWorker(ShapeworksWorker::OptimizeType, NULL, optimize_, optimize_parameters_, session_); QThread* thread = new QThread; diff --git a/Studio/Visualization/ColorMap.cpp b/Studio/Visualization/ColorMap.cpp index bf0b41de74..d3421d7de6 100644 --- a/Studio/Visualization/ColorMap.cpp +++ b/Studio/Visualization/ColorMap.cpp @@ -59,7 +59,7 @@ ColorMaps::ColorMaps() { add_custom_series("Rainbow", {Qt::blue, Qt::cyan, Qt::green, Qt::yellow, Qt::red}); add_custom_series("Grayscale", {Qt::black, Qt::darkGray, Qt::lightGray, Qt::white}); - add_custom_series("Blue to Red", {Qt::blue, Qt::white, Qt::red}); + add_custom_series("Blue White Red", {Qt::blue, Qt::white, Qt::red}); add_custom_series("Magenta to Green", {QColor(191, 53, 136), QColor(208, 121, 178), Qt::white, QColor(155, 196, 128), QColor(102, 167, 61)}); add_custom_series("Black-Body Radiation", {Qt::black, Qt::red, Qt::yellow, Qt::white}); @@ -78,6 +78,7 @@ ColorMaps::ColorMaps() { for (int i = vtkColorSeries::SPECTRUM; i < vtkColorSeries::CUSTOM; i++) { add_vtk_series(i); } + add_custom_series("Blue and Red", {Qt::blue, Qt::red}); } } // namespace shapeworks diff --git a/Studio/main.cpp b/Studio/main.cpp index f391712c7c..d11ed62e82 100644 --- a/Studio/main.cpp +++ b/Studio/main.cpp @@ -26,6 +26,7 @@ using namespace shapeworks; +//--------------------------------------------------------------------------- class OverrideQApplication : public QApplication { public: OverrideQApplication(int& argc, char** argv) : QApplication(argc, argv) {} @@ -47,6 +48,7 @@ class OverrideQApplication : public QApplication { QString stored_filename_; }; +//--------------------------------------------------------------------------- static void new_log() { QDateTime date_time = QDateTime::currentDateTime(); QString session_name = date_time.toString("yyyy-MM-dd_HH_mm_ss"); @@ -71,10 +73,17 @@ static void new_log() { Logging::Instance().open_file_log(logfile.toStdString()); } +//--------------------------------------------------------------------------- int main(int argc, char** argv) { // tbb::task_scheduler_init init(1); try { + +#ifdef Q_OS_MACOS + // Prevent cursor crashes on Apple Silicon + qputenv("QT_MAC_DISABLE_NATIVE_CURSORS", "1"); +#endif + // needed to ensure appropriate OpenGL context is created for VTK rendering. QSurfaceFormat format = QVTKOpenGLNativeWidget::defaultFormat(); #ifdef _WIN32 diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 4283db6015..7005c46f8f 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -1,6 +1,25 @@ # Release Notes -## ShapeWorks 6.6.0 - 2025-03 + +## ShapeWorks 6.6.1 - 2025-05 + +### Fixes + * Fix loading of non-binary segmentations + * Add padding for visualization of segmentation surfaces + * Add Blue/Red color map for scalar visualization + * Fix PCA/Group sliders when switching projects + * Fix stats being reloaded when groups are changed + * Fix a crash that can happen when changing projects while mesh warping + * Turn off group animate checkbox when deactivating analysis module + * Disallow network analysis when there are no scalar features + * Auto adjust network analysis alpha when necessary + * Add logging of groom and optimize parameters to file log + * Fix itk image factory registration for network analysis with image based features + * Added warning about inconsistent number of values in particle vector + * Fix libomp.dylib install problem on mac arm64 + * Update URL for UpdateChecker + +## ShapeWorks 6.6.0 - 2025-05 ![](../img/about/release6.6.png) diff --git a/install_shapeworks.sh b/install_shapeworks.sh index 6be6e0823b..ab5c964210 100644 --- a/install_shapeworks.sh +++ b/install_shapeworks.sh @@ -141,11 +141,14 @@ function install_conda() { if ! pip install open3d-cpu==0.17.0; then return 1; fi elif [[ "$(uname)" == "Darwin" ]]; then if ! pip install open3d==0.17.0; then return 1; fi - # fix hard-coded homebrew libomp.dylib - pushd $CONDA_PREFIX/lib/python3.9/site-packages/open3d/cpu - install_name_tool -change /opt/homebrew/opt/libomp/lib/libomp.dylib @rpath/libomp.dylib pybind.cpython-39-darwin.so - install_name_tool -add_rpath @loader_path/../../../ pybind.cpython-39-darwin.so - popd + + if [[ "$(uname -m)" == "arm64" ]]; then + pushd $CONDA_PREFIX/lib/python3.9/site-packages/open3d/cpu + install_name_tool -change /opt/homebrew/opt/libomp/lib/libomp.dylib @rpath/libomp.dylib pybind.cpython-39-darwin.so + install_name_tool -add_rpath @loader_path/../../../ pybind.cpython-39-darwin.so + popd + ln -sf "$CONDA_PREFIX/lib/libomp.dylib" "$CONDA_PREFIX/lib/python3.9/site-packages/open3d/cpu/../../../libomp.dylib" + fi else if ! pip install open3d==0.17.0; then return 1; fi fi