From 86b50345d4632cd3ceabb5b740d0b5abf2532959 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 1 Jun 2026 23:24:18 +0200 Subject: [PATCH 01/37] update quality control for alignment --- src/automated_registration.cpp | 838 +++++++++------------------------ src/auxiliary.cpp | 136 +++--- src/auxiliary.h | 33 +- 3 files changed, 289 insertions(+), 718 deletions(-) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 7bbb1305..416f74bf 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -15,6 +15,10 @@ using namespace Rcpp; using namespace std; using namespace cv; +//// +// Parameters +//// + // SIFT Parameters struct SIFTParameters { @@ -27,25 +31,12 @@ struct SIFTParameters const int ransac_maxIters=2000; }; -// check if keypoints are degenerate -bool check_degenerate(std::vector &points1, std::vector &points2) { - - // get sd - double points1_sd = cppSD(points1); - double points2_sd = cppSD(points2); - - // get warning message - bool is_degenerate = FALSE; - if(points1_sd < 1.0 | points2_sd < 1.0){ - is_degenerate = TRUE; - Rcout << "WARNING: points may be in a degenerate configuration." << endl; - } - - return is_degenerate; -} +//// +// Quality Control +//// // check distribution of registered points -std::string check_transformation_by_point_distribution(Mat &im, Mat &h){ +double checkMappedGridDistribution(Mat &im, Mat &h){ // message std::string message; @@ -70,26 +61,20 @@ std::string check_transformation_by_point_distribution(Mat &im, Mat &h){ cv::transform(gridpoints, gridpoints_reg, h); } else if(h.rows == 3) { cv::perspectiveTransform(gridpoints, gridpoints_reg, h); - } else { - message = "no distribution"; - return message; - } + } // Compute the standard deviation of the transformed points double gridpoints_reg_sd = cppSD(gridpoints_reg); // get warning message if(gridpoints_reg_sd < 1.0 | gridpoints_reg_sd > max(height, width)){ - message = "large distribution"; - Rcout << "WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; - } else { - message = "small distribution"; + Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; } - return message; + return gridpoints_reg_sd; } -bool check_matches(Mat &mask){ +bool checkMaskAbundance(Mat &mask){ int j=0; for (int i = 0; i < mask.rows; i++) { if (mask.at(i)) { @@ -99,24 +84,124 @@ bool check_matches(Mat &mask){ return j > 6; } -// do overall checks on keypoints and images -bool check_transformation_metrics(std::vector &points1, std::vector &points2, Mat &im2, Mat &h, Mat &mask) { +// compare the distance between two sets of match points +double medianMappingDistance(std::vector &keypoints1, std::vector &keypoints2, Mat &h) { + std::vector keypoints1_warped; + if(keypoints1.size() > 0){ + if (h.rows == 2){ + cv::transform(keypoints1, keypoints1_warped, h); + } else { + cv::perspectiveTransform(keypoints1, keypoints1_warped, h); + } + } - // check keypoint standard deviation - bool is_degenerate = check_degenerate(points1, points2); + return medianDistances(keypoints1_warped, keypoints2); +} - // TODO: check transformation - // make keypoints from points - // std::string transformation; - // transformation = check_transformation_by_pts_mean_sqrt(keypoints1, keypoints2, h, mask); +// calculate inlier percentage +int checkInlierPercentage(Mat &mask){ + int j=0; + for (int i = 0; i < mask.rows; i++) { + if (mask.at(i)) { + j++; + } + } + double ratio = (double) j/mask.rows; + double perc = round(100.0 * ratio); + return (int) perc; +} + +void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, + std::vector &keypoints1_masked, std::vector &keypoints2_masked, + std::vector &top_matches, Mat &mask) +{ + int j=0; + for (int i = 0; i < mask.rows; i++) { + if (mask.at(i)) { + keypoints1_masked.push_back(keypoints1_good[i]); + keypoints2_masked.push_back(keypoints2_good[i]); + top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); + j++; + } + } +} - // check distribution - std::string distribution; - distribution = check_transformation_by_point_distribution(im2, h); +// void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, +// std::vector &keypoints1_masked, std::vector &keypoints2_masked, +// Mat &mask) +// { +// int j=0; +// for (int i = 0; i < mask.rows; i++) { +// if (mask.at(i)) { +// keypoints1_masked.push_back(keypoints1_good[i]); +// keypoints2_masked.push_back(keypoints2_good[i]); +// j++; +// } +// } +// } +// check if keypoints are degenerate +bool checkDegenerate(double pts1, double pts2) { + + // get warning message + bool is_degenerate = FALSE; + if(pts1 < 1.0 | pts2 < 1.0){ + is_degenerate = TRUE; + Rcout << "WARNING: points may be in a degenerate configuration." << endl; + } + return is_degenerate; } +// do overall checks on keypoints and images +std::vector getTransformationMetrics(std::vector &points1, + std::vector &points2, + Mat &im2, Mat &h, Mat &mask) { + + // metrics list + std::vector metrics_list; + + // Alignment report + Rcout << "Alignment Report: " << endl; + + // Report final keypoints + Rcout << " Calculated transformation matrix with " << points1.size() << " keypoints" << endl; + + // points stand. dev. + double points1_sd = cppSD(points1); + double points2_sd = cppSD(points2); + if(points1_sd < 1.0 | points2_sd < 1.0){ + Rcout << " WARNING: points may be in a degenerate configuration." << endl; + } + Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; + metrics_list.push_back(checkDegenerate(points1_sd, points2_sd)); + metrics_list.push_back(points1_sd); + metrics_list.push_back(points2_sd); + + // check distribution of points + double stddev = checkMappedGridDistribution(im2, h); + Rcout << " Std dev of registered points: " << stddev << endl; + metrics_list.push_back(stddev); + + // warp keypoints and compare + double md = medianMappingDistance(points1, points2, h); + Rcout << " Median distance between points: " << md << endl; + if(md > 3){ + Rcout << " WARNING: Transformation may be poor - mean euclidean distance of mapped source and destination key points is high!" << endl; + } + + // get inlier percentages + double ratio = checkInlierPercentage(mask); + Rcout << " Inlier Percentage: " << ratio << endl; + + // return is_degenerate; + return metrics_list; +} + +//// +// Manage Keypoints and Matches +//// + // get good matching keypoints void getGoodMatches(std::vector> &matches12,std::vector> &matches21, std::vector &good_matches, const float lowe_ratio = 0.8) @@ -171,15 +256,6 @@ void getGoodMatches(std::vector> &matches12,std::vector, std::vector>().swap(matches21_map); } -void getGoodMatches_temp(std::vector> matches, std::vector &good_matches, const float lowe_ratio = 0.8) -{ - for (size_t i = 0; i < matches.size(); i++) { - if (matches[i][0].distance < lowe_ratio * matches[i][1].distance) { - good_matches.push_back(matches[i][0]); - } - } -} - // remove duplicate keypoints for TPS void removeCloseMatches(std::vector& points1, std::vector& points2, float threshold = std::numeric_limits::epsilon()) { @@ -293,6 +369,10 @@ void keepTopKeypoints(std::vector &keypoints, Mat &descriptors, SIFTPa } } +//// +// Compute SIFT/ORB and transformations +//// + void computeSIFTTiles(Mat &im, std::vector &keypoints, Mat &descriptors, Ptr &sift, SIFTParameters params){ // profiler @@ -352,6 +432,7 @@ void computeSIFTTiles(Mat &im, std::vector &keypoints, Mat &descriptor } } + bool getSIFTTransformationMatrixSingle( Mat &im1Proc, Mat &im2Proc, Mat &h, Mat &mask, Mat &imMatches, @@ -370,8 +451,8 @@ bool getSIFTTransformationMatrixSingle( Ptr sift = cv::SIFT::create(params.sift_nfeatures); computeSIFTTiles(im1Proc, keypoints1, descriptors1, sift, params); computeSIFTTiles(im2Proc, keypoints2, descriptors2, sift, params); - Rcout << "MESSAGE: Generated " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; - Rcout << "DONE: SIFT based key-points detection and descriptors computation" << endl; + // Rcout << "Generated " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; + // Rcout << "DONE: SIFT based key-points detection and descriptors computation" << endl; // filter duplicates filterDuplicateKeypoints(keypoints1, descriptors1); @@ -380,7 +461,7 @@ bool getSIFTTransformationMatrixSingle( // get top key points keepTopKeypoints(keypoints1, descriptors1, params); keepTopKeypoints(keypoints2, descriptors2, params); - Rcout << "MESSAGE: Filtered other than " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; + // Rcout << "Filtered other than " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; /////////////////////// /// Compute FLANN ///// @@ -389,7 +470,7 @@ bool getSIFTTransformationMatrixSingle( // Match features using FLANN matching std::vector> matches12, matches21; getFLANNMatches(descriptors1, descriptors2, matches12, matches21); - Rcout << "DONE: FLANN - Fast Library for Approximate Nearest Neighbors - descriptor matching" << endl; + // Rcout << "DONE: FLANN - Fast Library for Approximate Nearest Neighbors - descriptor matching" << endl; // TODO: can I release there now ? descriptors1.release(); @@ -398,11 +479,7 @@ bool getSIFTTransformationMatrixSingle( // Find good matches std::vector good_matches; getGoodMatches(matches12, matches21, good_matches); - Rcout << "DONE: get good mutual matches by distance thresholding" << endl; - - // TODO: can I release there now ? - std::vector>().swap(matches12); - std::vector>().swap(matches21); + // Rcout << "DONE: get good mutual matches by distance thresholding" << endl; /////////////////////// /// Find Homography /// @@ -414,10 +491,7 @@ bool getSIFTTransformationMatrixSingle( points1.push_back(keypoints1[good_matches[i].queryIdx].pt); points2.push_back(keypoints2[good_matches[i].trainIdx].pt); } - - // check variable - Rcout << "MESSAGE: Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; - + // Find transformation matrix if(points1.size() > 0){ if(run_Affine){ @@ -444,35 +518,31 @@ bool getSIFTTransformationMatrixSingle( return false; } - // Draw top matches and good ones only - std::vector top_matches; - std::vector keypoints1_best, keypoints2_best; + // filter keypoints by good matches + std::vector keypoints1_good, keypoints2_good; for(size_t i = 0; i < good_matches.size(); i++ ) { - keypoints1_best.push_back(keypoints1[good_matches[i].queryIdx]); - keypoints2_best.push_back(keypoints2[good_matches[i].trainIdx]); - } - std::vector keypoints1_best2, keypoints2_best2; - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - keypoints1_best2.push_back(keypoints1_best[i]); - keypoints2_best2.push_back(keypoints2_best[i]); - top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); - j++; - } + keypoints1_good.push_back(keypoints1[good_matches[i].queryIdx]); + keypoints2_good.push_back(keypoints2[good_matches[i].trainIdx]); } - scaledDrawMatches(im1Proc, keypoints1_best2, im2Proc, keypoints2_best2, top_matches, imMatches); - - // TODO: can I release there now ? - // std::vector().swap(keypoints1_best); - // std::vector().swap(keypoints2_best); - // std::vector().swap(keypoints1_best2); - // std::vector().swap(keypoints2_best2); - // std::vector().swap(top_matches); + + // filter keypoints using mask + std::vector keypoints1_masked, keypoints2_masked; + std::vector top_matches; + maskKeypoints(keypoints1_good, keypoints2_good, + keypoints1_masked, keypoints2_masked, + top_matches, mask); + + // convert keypoints to points + points1 = KeyPointToPoint2f(keypoints1_masked); + points2 = KeyPointToPoint2f(keypoints2_masked); + + // draw matches + scaledDrawMatches(im1Proc, keypoints1_masked, im2Proc, keypoints2_masked, + top_matches, imMatches); // check number of matches - return check_matches(mask); + return checkMaskAbundance(mask); } void getSIFTTransformationMatrix( @@ -493,67 +563,73 @@ void getSIFTTransformationMatrix( // check variable bool check; - Rcout << "MESSAGE: Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; + Rcout << "Calculating" << (run_Affine ? " Affine " : " Homography ") << "Transformation Matrix" << endl; + + Rcout << "Round 1: No histogram equalization" << endl; // find matches and points check = getSIFTTransformationMatrixSingle(im1Proc, im2Proc, h, mask, imMatches, points1, points2, run_Affine, params, is_faulty); - Rcout << "DONE: calculated homography matrix with " << points1.size() << " points" << endl; - + // equalize first image if fails if(!check){ + + // clear points + points1.clear(); + points2.clear(); Mat im1Proc_eq; cv::equalizeHist(im1Proc, im1Proc_eq); - Rcout << "MESSAGE: Calculating Transformation Matrix with histogram equalization (1)" << endl; + Rcout << "Round 2: Histogram equalization of Image 1" << endl; check = getSIFTTransformationMatrixSingle(im1Proc_eq, im2Proc, h, mask, imMatches, points1, points2, run_Affine, params, is_faulty); - Rcout << "DONE: calculated homography matrix with " << points1.size() << " points" << endl; } else { return; } // equalize second image if fails if(!check){ + + // clear points + points1.clear(); + points2.clear(); cv::equalizeHist(im2Proc, im2Proc_eq); - Rcout << "MESSAGE: Calculating Transformation Matrix with histogram equalization (2)" << endl; + Rcout << "Round 3: Histogram equalization of Image 2" << endl; check = getSIFTTransformationMatrixSingle(im1Proc, im2Proc_eq, h, mask, imMatches, points1, points2, run_Affine, params, is_faulty); - Rcout << "DONE: calculated homography matrix with " << points1.size() << " points" << endl; } else { return; } // last try with both equalized images if(!check){ + + // clear points + points1.clear(); + points2.clear(); cv::equalizeHist(im1Proc, im1Proc_eq2); cv::equalizeHist(im2Proc, im2Proc_eq2); - Rcout << "MESSAGE: Calculating Transformation Matrix with histogram equalization (3)" << endl; + Rcout << "Round 4: Histogram equalization of Image 1 and 2" << endl; check = getSIFTTransformationMatrixSingle(im1Proc_eq2, im2Proc_eq2, h, mask, imMatches, points1, points2, run_Affine, params, is_faulty); - Rcout << "DONE: calculated homography matrix with " << points1.size() << " points" << endl; } else { return; } - // TODO: release ? - // im1Proc_eq.release(); - // im1Proc_eq2.release(); - // im2Proc_eq.release(); - // im2Proc_eq2.release(); + } bool getORBTransformationMatrix( @@ -573,13 +649,13 @@ bool getORBTransformationMatrix( Ptr orb = ORB::create(MAX_FEATURES); orb->detectAndCompute(im1Proc, Mat(), keypoints1, descriptors1); orb->detectAndCompute(im2Proc, Mat(), keypoints2, descriptors2); - Rcout << "DONE: orb based key-points detection and descriptors computation" << endl; + // Rcout << "DONE: orb based key-points detection and descriptors computation" << endl; // Match features. std::vector matches; Ptr matcher = DescriptorMatcher::create("BruteForce-Hamming"); matcher->match(descriptors1, descriptors2, matches, Mat()); - Rcout << "DONE: BruteForce-Hamming - descriptor matching" << endl; + // Rcout << "DONE: BruteForce-Hamming - descriptor matching" << endl; // Sort matches by score std::sort(matches.begin(), matches.end()); @@ -587,7 +663,7 @@ bool getORBTransformationMatrix( // Remove not so good matches const int numGoodMatches = matches.size() * GOOD_MATCH_PERCENT; matches.erase(matches.begin()+numGoodMatches, matches.end()); - Rcout << "DONE: get good matches by distance thresholding" << endl; + //Rcout << "DONE: get good matches by distance thresholding" << endl; // Extract location of good matches for( size_t i = 0; i < matches.size(); i++ ) @@ -597,7 +673,7 @@ bool getORBTransformationMatrix( } // check variable - Rcout << "MESSAGE: Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; + Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; // Find transformation matrix if(points1.size() > 0){ @@ -616,34 +692,41 @@ bool getORBTransformationMatrix( mask); } } else { - Rcout << "Found no matches!" << endl; + Rcout << "WARNING: Found no matches!" << endl; return false; } - - // Draw top matches and good ones only - std::vector top_matches; - std::vector keypoints1_best, keypoints2_best; + + // filter keypoints by good matches + std::vector keypoints1_good, keypoints2_good; for(size_t i = 0; i < matches.size(); i++ ) { - keypoints1_best.push_back(keypoints1[matches[i].queryIdx]); - keypoints2_best.push_back(keypoints2[matches[i].trainIdx]); - } - std::vector keypoints1_best2, keypoints2_best2; - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - keypoints1_best2.push_back(keypoints1_best[i]); - keypoints2_best2.push_back(keypoints2_best[i]); - top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); - j++; - } + keypoints1_good.push_back(keypoints1[matches[i].queryIdx]); + keypoints2_good.push_back(keypoints2[matches[i].trainIdx]); } - scaledDrawMatches(im1Proc, keypoints1_best2, im2Proc, keypoints2_best2, top_matches, imMatches); - + + // filter keypoints using mask + std::vector keypoints1_masked, keypoints2_masked; + std::vector top_matches; + maskKeypoints(keypoints1_good, keypoints2_good, + keypoints1_masked, keypoints2_masked, + top_matches, mask); + + // convert keypoints to points + points1 = KeyPointToPoint2f(keypoints1_masked); + points2 = KeyPointToPoint2f(keypoints2_masked); + + // draw matches + scaledDrawMatches(im1Proc, keypoints1_masked, im2Proc, keypoints2_masked, + top_matches, imMatches); + // check number of matches - return check_matches(mask); + return checkMaskAbundance(mask); } +//// +// Align Images +//// + // align images with FLANN algorithm void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, Mat &imMatches, Mat &h, Rcpp::List &keypoints, @@ -683,7 +766,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, if(strcmp(matcher.get_cstring(), "BRUTE-FORCE") == 0){ // message - Rcout << "MESSAGE: Running BRUTE-FORCE Alignment" << endl; + Rcout << "Running BRUTE-FORCE Alignment" << endl; // run ORB bool check; @@ -694,17 +777,18 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } else { // message - Rcout << "MESSAGE: Running SIFT+FLANN Alignment" << ((run_TPS) ? " with TPS" : "") << endl; + Rcout << "Running SIFT+FLANN Alignment" << ((run_TPS) ? " with TPS" : "") << endl; // run SIFT getSIFTTransformationMatrix(im1Proc, im2Proc, h, mask, imMatches, points1, points2, run_Affine, is_faulty); } - - // check result - is_faulty = check_transformation_metrics(points1, points2, im2, h, mask); - Rcout << "MESSAGE: Registration is " << (is_faulty ? "degenerate!" : "not degenerate!") << endl; + + // get metrics + std::vector metrics; + metrics = getTransformationMetrics(points1, points2, im2, h, mask); + Rcout << "Registration is " << (metrics[0] ? "degenerate!" : "not degenerate!") << endl; // Use homography to warp image if(h.rows == 2){ @@ -718,7 +802,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, return; } - Rcout << "DONE: warped query image" << endl; + // Rcout << "DONE: warped query image" << endl; /////////////////////// /// Find Homography /// @@ -736,7 +820,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // TPS is requested (only if FLANN succeeded) } else { - Rcout << "MESSAGE: Running Thin-Plate-Spline Alignment" << endl; + Rcout << "Calculating Thin-Plate-Spline Interpolation" << endl; // Filtered points (inliers) based on the mask std::vector filtered_points1; @@ -872,498 +956,4 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R // return return out; -} - -///////////////// -/// scratch ///// -///////////////// - -// align images with BRUTE FORCE algorithm -void alignImagesBRUTE(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, Mat &imMatches, Mat &h, - const float GOOD_MATCH_PERCENT, const int MAX_FEATURES, - const bool invert_query, const bool invert_ref, - const char* flipflop_query, const char* flipflop_ref, - const char* rotate_query, const char* rotate_ref, - const bool run_Affine) -{ - - // Convert images to grayscale - Mat im1Gray, im2Gray; - cvtColor(im1, im1Gray, cv::COLOR_BGR2GRAY); - cvtColor(im2, im2Gray, cv::COLOR_BGR2GRAY); - - // Variables to store keypoints and descriptors - std::vector keypoints1, keypoints2; - Mat descriptors1, descriptors2; - - // Process images - Mat im1Proc, im2Proc, im1NormalProc; - im1Proc = preprocessImage(im1Gray, invert_query, flipflop_query, rotate_query); - im1NormalProc = preprocessImage(im1, FALSE, flipflop_query, rotate_query); - im2Proc = preprocessImage(im2Gray, invert_ref, flipflop_ref, rotate_ref); - - // Detect ORB features and compute descriptors. - Ptr orb = ORB::create(MAX_FEATURES); - orb->detectAndCompute(im1Proc, Mat(), keypoints1, descriptors1); - orb->detectAndCompute(im2Proc, Mat(), keypoints2, descriptors2); - Rcout << "DONE: orb based key-points detection and descriptors computation" << endl; - - // Match features. - std::vector matches; - Ptr matcher = DescriptorMatcher::create("BruteForce-Hamming"); - matcher->match(descriptors1, descriptors2, matches, Mat()); - Rcout << "DONE: BruteForce-Hamming - descriptor matching" << endl; - - // Sort matches by score - std::sort(matches.begin(), matches.end()); - - // Remove not so good matches - const int numGoodMatches = matches.size() * GOOD_MATCH_PERCENT; - matches.erase(matches.begin()+numGoodMatches, matches.end()); - Rcout << "DONE: get good matches by distance thresholding" << endl; - - // Extract location of good matches - std::vector points1, points2; - for( size_t i = 0; i < matches.size(); i++ ) - { - points1.push_back( keypoints1[ matches[i].queryIdx ].pt ); - points2.push_back( keypoints2[ matches[i].trainIdx ].pt ); - } - - // check variable - Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; - - // Find transformation matrix - cv::Mat mask; - if(run_Affine){ - std::vector match_mask; - h = estimateAffine2D(points1, - points2, - match_mask, - cv::RANSAC); - mask = IntVectorToMat(match_mask); - } else { - h = findHomography(points1, - points2, - cv::RANSAC, - 5, - mask); - } - - // Draw top matches and good ones only - std::vector top_matches; - std::vector keypoints1_best, keypoints2_best; - for(size_t i = 0; i < matches.size(); i++ ) - { - keypoints1_best.push_back(keypoints1[matches[i].queryIdx]); - keypoints2_best.push_back(keypoints2[matches[i].trainIdx]); - } - std::vector keypoints1_best2, keypoints2_best2; - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - keypoints1_best2.push_back(keypoints1_best[i]); - keypoints2_best2.push_back(keypoints2_best[i]); - top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); - j++; - } - } - scaledDrawMatches(im1Proc, keypoints1_best2, im2Proc, keypoints2_best2, top_matches, imMatches); - - // Use homography to warp image - Mat im1Warp, im1NormalWarp; - if(h.rows == 2){ - warpAffine(im1Proc, im1Warp, h, im2Proc.size()); - warpAffine(im1NormalProc, im1NormalWarp, h, im2Proc.size()); - } else { - warpPerspective(im1Proc, im1Warp, h, im2Proc.size()); - warpPerspective(im1NormalProc, im1NormalWarp, h, im2Proc.size()); - } - - // Reverse process - im1Reg = reversepreprocessImage(im1NormalWarp, flipflop_ref, rotate_ref); - - // return as rgb - cvtColor(im2Proc, im2, cv::COLOR_GRAY2BGR); - - // resize image to visualize faster later in Shiny - im2 = resize_image(im2, 500); - im1Overlay = resize_image(im1Reg, 500); -} - -// align images with FLANN algorithm -void alignImagesFLANN(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, - Mat &imMatches, Mat &h, Rcpp::List &keypoints, - const bool invert_query, const bool invert_ref, - const char* flipflop_query, const char* flipflop_ref, - const char* rotate_query, const char* rotate_ref, - const bool run_Affine, const bool run_TPS) -{ - - // parameters - cv::setRNGSeed(0); - SIFTParameters params; - - ////////////////////// - /// Process Images /// - ////////////////////// - - // Convert images to grayscale - Mat im1Gray, im2Gray; - cvtColor(im1, im1Gray, cv::COLOR_BGR2GRAY); - cvtColor(im2, im2Gray, cv::COLOR_BGR2GRAY); - - // Process images - Mat im1Proc, im2Proc, im1NormalProc; - im1Proc = preprocessImage(im1Gray, invert_query, flipflop_query, rotate_query); - im1NormalProc = preprocessImage(im1, FALSE, flipflop_query, rotate_query); - im2Proc = preprocessImage(im2Gray, invert_ref, flipflop_ref, rotate_ref); - - // //////////////////////////////////// - // /// Compute SIFT+FLANN+Homograpy /// - // //////////////////////////////////// - - // RUN SIFT+FLANN+Homography with retry - bool is_faulty = FALSE; - cv::Mat mask; - std::vector points1, points2; - // getSIFTTransformationMatrix(im1Proc, im2Proc, im1, im2, h, mask, imMatches, - // points1, points2, run_Affine, params, is_faulty); - - // check result - is_faulty = check_transformation_metrics(points1, points2, im2, h, mask); - Rcout << "MESSAGE: Registration is " << (is_faulty ? "degenerate!" : "not degenerate!") << endl; - - // Use homography to warp image - Mat im1Warp, im1NormalWarp; - if(h.rows == 2){ - warpAffine(im1Proc, im1Warp, h, im2Proc.size()); - warpAffine(im1NormalProc, im1NormalWarp, h, im2Proc.size()); - } else { - warpPerspective(im1Proc, im1Warp, h, im2Proc.size()); - warpPerspective(im1NormalProc, im1NormalWarp, h, im2Proc.size()); - } - - Rcout << "DONE: warped query image" << endl; - - /////////////////////// - /// Find Homography /// - /////////////////////// - - // continue with TPS or do FLANN only - Mat im1Reg_Warp_nonrigid; - Mat im1Reg_NormalWarp_nonrigid; - Mat im1Combine; - if(is_faulty || !run_TPS){ - - // change color map - cv::addWeighted(im2Proc, 0.7, im1Warp, 0.3, 0, im1Combine); - - // Reverse process - im1Reg = reversepreprocessImage(im1NormalWarp, flipflop_ref, rotate_ref); - - // return as rgb - cvtColor(im1Combine, im1Overlay, cv::COLOR_GRAY2BGR); - cvtColor(im2Proc, im2, cv::COLOR_GRAY2BGR); - - // TPS is requested (only if FLANN succeeded) - } else { - - Rcout << "MESSAGE: Running Thin-Plate-Spline Alignment" << endl; - - // Filtered points (inliers) based on the mask - std::vector filtered_points1; - std::vector filtered_points2; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - filtered_points1.push_back(points1[i]); - filtered_points2.push_back(points2[i]); - } - } - removeCloseMatches(filtered_points1, filtered_points2); - - // transform query - std::vector filtered_points1_reg; - if (h.rows == 2){ - cv::transform(filtered_points1, filtered_points1_reg, h); - } else { - cv::perspectiveTransform(filtered_points1, filtered_points1_reg, h); - } - - // get TPS matches - std::vector matches; - for (unsigned int i = 0; i < filtered_points2.size(); i++) - matches.push_back(cv::DMatch(i, i, 0)); - - // calculate TPS transformation - Ptr tps = cv::createThinPlateSplineShapeTransformer(0); - tps->estimateTransformation(filtered_points2, filtered_points1_reg, matches); - - // save keypoints - keypoints[0] = point2fToNumericMatrix(filtered_points2); - keypoints[1] = point2fToNumericMatrix(filtered_points1_reg); - - // determine extension limits for both images - int y_max = max(im1Warp.rows, im2.rows); - int x_max = max(im1Warp.cols, im2.cols); - - // extend images - cv::copyMakeBorder(im1Warp, im1Warp, 0.0, (int) (y_max - im1Warp.rows), 0.0, (x_max - im1Warp.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - cv::copyMakeBorder(im1NormalWarp, im1NormalWarp, 0.0, (int) (y_max - im1NormalWarp.rows), 0.0, (x_max - im1NormalWarp.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - - // transform image - Mat im1Reg_Warp_nonrigid; - Mat im1Reg_NormalWarp_nonrigid; - tps->warpImage(im1Warp, im1Reg_Warp_nonrigid); - tps->warpImage(im1NormalWarp, im1Reg_NormalWarp_nonrigid); - - // resize image - cv::Mat im1Reg_NormalWarp_nonrigid_cropped = im1Reg_NormalWarp_nonrigid(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - im1Reg_NormalWarp_nonrigid = im1Reg_NormalWarp_nonrigid_cropped.clone(); - - cv::Mat im1Reg_Warp_nonrigid_cropped = im1Reg_Warp_nonrigid(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - im1Reg_Warp_nonrigid = im1Reg_Warp_nonrigid_cropped.clone(); - - // change color map - cv::addWeighted(im2Proc, 0.7, im1Reg_Warp_nonrigid, 0.3, 0, im1Combine); - - // Reverse process - im1Reg = reversepreprocessImage(im1Reg_NormalWarp_nonrigid, flipflop_ref, rotate_ref); - - // return as rgb - cvtColor(im1Combine, im1Overlay, cv::COLOR_GRAY2BGR); - cvtColor(im2Proc, im2, cv::COLOR_GRAY2BGR); - } - - // resize image to visualize faster later in Shiny - im2 = resize_image(im2, 500); - im1Overlay = resize_image(im1Overlay, 500); -} - -// align images with FLANN algorithm -void alignImagesFLANN2(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, - Mat &imMatches, Mat &h, Rcpp::List &keypoints, - const bool invert_query, const bool invert_ref, - const char* flipflop_query, const char* flipflop_ref, - const char* rotate_query, const char* rotate_ref, - const bool run_Affine, const bool run_TPS) -{ - - // parameters - cv::setRNGSeed(0); - SIFTParameters params; - - ////////////////////// - /// Process Images /// - ////////////////////// - - // Convert images to grayscale - Mat im1Gray, im2Gray; - cvtColor(im1, im1Gray, cv::COLOR_BGR2GRAY); - cvtColor(im2, im2Gray, cv::COLOR_BGR2GRAY); - - // Process images - Mat im1Proc, im2Proc, im1NormalProc; - im1Proc = preprocessImage(im1Gray, invert_query, flipflop_query, rotate_query); - im1NormalProc = preprocessImage(im1, FALSE, flipflop_query, rotate_query); - im2Proc = preprocessImage(im2Gray, invert_ref, flipflop_ref, rotate_ref); - - // //////////////////////////////////// - // /// Compute SIFT+FLANN+Homograpy /// - // //////////////////////////////////// - - // RUN SIFT+FLANN+Homography with retry - bool is_faulty = FALSE; - cv::Mat mask; - std::vector points1, points2; - - - // Variables to store keypoints and descriptors - std::vector keypoints1, keypoints2; - Mat descriptors1, descriptors2; - - // Detect SIFT features - // Ptr sift = cv::SIFT::create(params.sift_nfeatures); - Ptr sift = cv::SIFT::create(); - // computeSIFTTiles(im1Proc, keypoints1, descriptors1, sift, params); - // computeSIFTTiles(im2Proc, keypoints2, descriptors2, sift, params); - sift->detectAndCompute(im1Proc, Mat(), keypoints1, descriptors1); - sift->detectAndCompute(im2Proc, Mat(), keypoints2, descriptors2); - - Rcout << "MESSAGE: Generated " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; - Rcout << "DONE: SIFT based key-points detection and descriptors computation" << endl; - - /////////////////////// - /// Compute FLANN ///// - /////////////////////// - - // Match features using FLANN matching - std::vector> matches; - cv::FlannBasedMatcher custom_matcher = cv::FlannBasedMatcher(cv::makePtr(5), cv::makePtr(50, 0, TRUE)); - cv::Ptr matcher = custom_matcher.create(); - matcher->knnMatch(descriptors1, descriptors2, matches, 2); - Rcout << "DONE: FLANN - Fast Library for Approximate Nearest Neighbors - descriptor matching" << endl; - - // Find good matches - // goodMatches = get_good_matches(matches) - std::vector good_matches; - getGoodMatches_temp(matches, good_matches); - Rcout << "DONE: get good matches by distance thresholding" << endl; - - /////////////////////// - /// Find Homography /// - /////////////////////// - - // Extract location of good matches - for( size_t i = 0; i < good_matches.size(); i++ ) - { - points1.push_back(keypoints1[good_matches[i].queryIdx].pt); - points2.push_back(keypoints2[good_matches[i].trainIdx].pt); - } - - // check variable - Rcout << "MESSAGE: Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; - - // Find transformation matrix - Rcout << "MESSAGE: Matching " << points1.size() << " keypoints" << endl; - if(run_Affine){ - std::vector match_mask; - h = estimateAffine2D(points1, - points2, - match_mask, - cv::RANSAC); - mask = IntVectorToMat(match_mask); - } else { - h = findHomography(points1, points2, RANSAC); - } - - // Draw top matches and good ones only - std::vector top_matches; - std::vector keypoints1_best, keypoints2_best; - for(size_t i = 0; i < good_matches.size(); i++ ) - { - keypoints1_best.push_back(keypoints1[good_matches[i].queryIdx]); - keypoints2_best.push_back(keypoints2[good_matches[i].trainIdx]); - } - std::vector keypoints1_best2, keypoints2_best2; - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - keypoints1_best2.push_back(keypoints1_best[i]); - keypoints2_best2.push_back(keypoints2_best[i]); - top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); - j++; - } - } - // scaledDrawMatches(im1Proc, keypoints1_best2, im2Proc, keypoints2_best2, top_matches, imMatches); - drawMatches(im1Proc, keypoints1_best2, im2Proc, keypoints2_best2, top_matches, imMatches); - - // check result - is_faulty = check_transformation_metrics(points1, points2, im2, h, mask); - Rcout << "MESSAGE: Registration is " << (is_faulty ? "degenerate!" : "not degenerate!") << endl; - - // Use homography to warp image - Mat im1Warp, im1NormalWarp; - if(h.rows == 2){ - warpAffine(im1Proc, im1Warp, h, im2Proc.size()); - warpAffine(im1NormalProc, im1NormalWarp, h, im2Proc.size()); - } else { - warpPerspective(im1Proc, im1Warp, h, im2Proc.size()); - warpPerspective(im1NormalProc, im1NormalWarp, h, im2Proc.size()); - } - - Rcout << "DONE: warped query image" << endl; - - /////////////////////// - /// Find Homography /// - /////////////////////// - - // continue with TPS or do FLANN only - Mat im1Reg_Warp_nonrigid; - Mat im1Reg_NormalWarp_nonrigid; - Mat im1Combine; - if(is_faulty || !run_TPS){ - - // change color map - cv::addWeighted(im2Proc, 0.7, im1Warp, 0.3, 0, im1Combine); - - // Reverse process - im1Reg = reversepreprocessImage(im1NormalWarp, flipflop_ref, rotate_ref); - - // return as rgb - cvtColor(im1Combine, im1Overlay, cv::COLOR_GRAY2BGR); - cvtColor(im2Proc, im2, cv::COLOR_GRAY2BGR); - - // TPS is requested (only if FLANN succeeded) - } else { - - Rcout << "MESSAGE: Running Thin-Plate-Spline Alignment" << endl; - - // Filtered points (inliers) based on the mask - std::vector filtered_points1; - std::vector filtered_points2; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - filtered_points1.push_back(points1[i]); - filtered_points2.push_back(points2[i]); - } - } - removeCloseMatches(filtered_points1, filtered_points2); - - // transform query - std::vector filtered_points1_reg; - if (h.rows == 2){ - cv::transform(filtered_points1, filtered_points1_reg, h); - } else { - cv::perspectiveTransform(filtered_points1, filtered_points1_reg, h); - } - - // get TPS matches - std::vector matches; - for (unsigned int i = 0; i < filtered_points2.size(); i++) - matches.push_back(cv::DMatch(i, i, 0)); - - // calculate TPS transformation - Ptr tps = cv::createThinPlateSplineShapeTransformer(0); - tps->estimateTransformation(filtered_points2, filtered_points1_reg, matches); - - // save keypoints - keypoints[0] = point2fToNumericMatrix(filtered_points2); - keypoints[1] = point2fToNumericMatrix(filtered_points1_reg); - - // determine extension limits for both images - int y_max = max(im1Warp.rows, im2.rows); - int x_max = max(im1Warp.cols, im2.cols); - - // extend images - cv::copyMakeBorder(im1Warp, im1Warp, 0.0, (int) (y_max - im1Warp.rows), 0.0, (x_max - im1Warp.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - cv::copyMakeBorder(im1NormalWarp, im1NormalWarp, 0.0, (int) (y_max - im1NormalWarp.rows), 0.0, (x_max - im1NormalWarp.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - - // transform image - Mat im1Reg_Warp_nonrigid; - Mat im1Reg_NormalWarp_nonrigid; - tps->warpImage(im1Warp, im1Reg_Warp_nonrigid); - tps->warpImage(im1NormalWarp, im1Reg_NormalWarp_nonrigid); - - // resize image - cv::Mat im1Reg_NormalWarp_nonrigid_cropped = im1Reg_NormalWarp_nonrigid(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - im1Reg_NormalWarp_nonrigid = im1Reg_NormalWarp_nonrigid_cropped.clone(); - - cv::Mat im1Reg_Warp_nonrigid_cropped = im1Reg_Warp_nonrigid(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - im1Reg_Warp_nonrigid = im1Reg_Warp_nonrigid_cropped.clone(); - - // change color map - cv::addWeighted(im2Proc, 0.7, im1Reg_Warp_nonrigid, 0.3, 0, im1Combine); - - // Reverse process - im1Reg = reversepreprocessImage(im1Reg_NormalWarp_nonrigid, flipflop_ref, rotate_ref); - - // return as rgb - cvtColor(im1Combine, im1Overlay, cv::COLOR_GRAY2BGR); - cvtColor(im2Proc, im2, cv::COLOR_GRAY2BGR); - } - - // resize image to visualize faster later in Shiny - im2 = resize_image(im2, 500); - im1Overlay = resize_image(im1Overlay, 500); } \ No newline at end of file diff --git a/src/auxiliary.cpp b/src/auxiliary.cpp index ba4a455f..9e8c9cb3 100644 --- a/src/auxiliary.cpp +++ b/src/auxiliary.cpp @@ -35,73 +35,6 @@ Rcpp::NumericMatrix replaceNaMatrix(Rcpp::NumericMatrix mat, int replace) { return mat; } -//// -// memory -//// - -// // memory check -// void log_mem_usage(const std::string& label = "") { -// struct rusage usage; -// getrusage(RUSAGE_SELF, &usage); -// long rss_b = usage.ru_maxrss; -// -// double rss_kb = rss_b / 1024.0; -// double rss_mb = rss_kb / 1024.0; -// double rss_gb = rss_mb / 1024.0; -// -// Rcpp::Rcout << "Used Memory [" << label << "]: " << rss_gb << " GB" << std::endl; -// } -// -// void log_mem_macos(const std::string& label = "") { -// mach_task_basic_info info; -// mach_msg_type_number_t size = MACH_TASK_BASIC_INFO_COUNT; -// kern_return_t kr = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, -// (task_info_t)&info, &size); -// -// if (kr != KERN_SUCCESS) { -// Rcpp::Rcerr << "[MEM " << label << "] Failed to get memory info.\n"; -// return; -// } -// -// double rss_gb = static_cast(info.resident_size) / (1024.0 * 1024.0 * 1024.0); -// double virt_gb = static_cast(info.virtual_size) / (1024.0 * 1024.0 * 1024.0); -// -// Rcpp::Rcout << "[MEM " << label << "] Resident (RSS): " -// << rss_gb << " GB, Virtual: " << virt_gb << " GB\n"; -// } -// -// double object_size_long(long bsize) { -// -// double rss_kb = bsize / 1024.0; -// double rss_mb = rss_kb / 1024.0; -// double rss_gb = rss_mb / 1024.0; -// -// return rss_gb; -// } -// -// double object_size_double(double bsize) { -// -// double rss_kb = bsize / 1024; -// double rss_mb = rss_kb / 1024; -// double rss_gb = rss_mb / 1024; -// -// return rss_gb; -// } -// -// double get_resident_bytes() { -// mach_task_basic_info info; -// mach_msg_type_number_t size = MACH_TASK_BASIC_INFO_COUNT; -// if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, -// (task_info_t)&info, &size) != KERN_SUCCESS) { -// return 0; -// } -// return static_cast(info.resident_size); -// } -// -// double bytes_to_gb(double bytes) { -// return bytes / (1024.0 * 1024.0 * 1024.0); -// } - //// // Conversion //// @@ -197,6 +130,17 @@ std::vector Point2fToDoubleVector(std::vector &points) { return vec; } +// Function to convert a cv::Keypoint object to a std::vector +std::vector KeyPointToPoint2f(std::vector &keypoints) { + int n = keypoints.size(); + std::vector points; + + for (int i = 0; i < n; i++) { + points.push_back(keypoints[i].pt); + } + return points; +} + // Function to convert a cv::Point2f object to a cv::Mat std::vector matToPoint2f(cv::Mat &mat) { std::vector points; @@ -289,3 +233,61 @@ double cppSD(std::vector &points) std::vector().swap(inVec); return std::sqrt( sd / (n-1) ); } + +double meanDistances(std::vector& pts1, + std::vector& pts2) +{ + if (pts1.size() != pts2.size() || pts1.empty()) + return 0.0; + + double sumDist = 0.0; + for (size_t i = 0; i < pts1.size(); ++i) + { + const double dx = pts1[i].x - pts2[i].x; + const double dy = pts1[i].y - pts2[i].y; + sumDist += std::sqrt(dx * dx + dy * dy); + } + + return sumDist / pts1.size(); +} + +double medianDistances(std::vector& pts1, + std::vector& pts2) +{ + if (pts1.size() != pts2.size() || pts1.empty()) + return 0.0; + + std::vector distances; + distances.reserve(pts1.size()); + + for (size_t i = 0; i < pts1.size(); ++i) + { + const double dx = pts1[i].x - pts2[i].x; + const double dy = pts1[i].y - pts2[i].y; + distances.push_back(std::sqrt(dx * dx + dy * dy)); + } + + const size_t n = distances.size(); + const size_t mid = n / 2; + + std::nth_element(distances.begin(), + distances.begin() + mid, + distances.end()); + + if (n % 2 == 1) + { + return distances[mid]; + } + else + { + double upper = distances[mid]; + + std::nth_element(distances.begin(), + distances.begin() + mid - 1, + distances.end()); + + double lower = distances[mid - 1]; + + return (lower + upper) / 2.0; + } +} \ No newline at end of file diff --git a/src/auxiliary.h b/src/auxiliary.h index 0985e819..6deb4419 100644 --- a/src/auxiliary.h +++ b/src/auxiliary.h @@ -37,6 +37,9 @@ cv::Mat IntVectorToMat(std::vector &points); // std::vector vs std::vector std::vector KeyPointToDoubleVector(std::vector &points); std::vector Point2fToDoubleVector(std::vector &points); + +// std::vector vs std::vector +std::vector KeyPointToPoint2f(std::vector &keypoints); //// // stats @@ -46,32 +49,8 @@ std::vector Point2fToDoubleVector(std::vector &points); double cppSD(std::vector &points); double cppSD(std::vector &points); -//// -// memory -//// - -// void log_mem_usage(const std::string& label); -// void log_mem_macos(const std::string& label); -// double object_size_long(long bsize); -// double object_size_double(double bsize); -// double get_resident_bytes(); -// double bytes_to_gb(double bytes); -// -// struct MemProfiler { -// size_t start; -// std::string label; -// -// MemProfiler(const std::string& lbl) : label(lbl) { -// start = get_resident_bytes(); -// } -// -// ~MemProfiler() { -// size_t end = get_resident_bytes(); -// double diff = (double) end - (double) start; -// if(diff < 0.0) diff = 0.0; -// double diff_gb = bytes_to_gb(diff); -// Rcpp::Rcout << "[MEM] " << label << ": +" << diff_gb << " GB" << std::endl; -// } -// }; +// mean distance between points +double meanDistances(std::vector &pts1, std::vector &pts2); +double medianDistances(std::vector &pts1, std::vector &pts2); #endif \ No newline at end of file From 2701661555d39a9d01987d1349f868e5f8df444e Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 3 Jun 2026 16:23:10 +0200 Subject: [PATCH 02/37] separate some functions to metrics.cpp --- src/automated_registration.cpp | 180 +---------------- src/manual_registration.cpp | 7 +- src/metrics.cpp | 349 +++++++++++++++++++++++++++++++++ src/metrics.h | 35 ++++ 4 files changed, 397 insertions(+), 174 deletions(-) create mode 100644 src/metrics.cpp create mode 100644 src/metrics.h diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 416f74bf..24da4e48 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -6,9 +6,10 @@ #include "opencv2/shape/shape_transformer.hpp" // #include -// Internal functions +// Library #include "auxiliary.h" #include "image.h" +#include "metrics.h" // Namespaces using namespace Rcpp; @@ -31,173 +32,6 @@ struct SIFTParameters const int ransac_maxIters=2000; }; -//// -// Quality Control -//// - -// check distribution of registered points -double checkMappedGridDistribution(Mat &im, Mat &h){ - - // message - std::string message; - - // get image shape - int height = im.rows; - int width = im.cols; - int height_interval = height > 50 ? (double) height/50.0 : 1; - int width_interval = width > 50 ? (double) width/50.0 : 1; - - // perspective transformation of grid points - std::vector gridpoints; - for (double i = 0.0; i <= height; i += height_interval) { - for (double j = 0.0; j <= width; j += width_interval) { - gridpoints.push_back(cv::Point2f(j,i)); - } - } - - // register grid points - std::vector gridpoints_reg; - if (h.rows == 2){ - cv::transform(gridpoints, gridpoints_reg, h); - } else if(h.rows == 3) { - cv::perspectiveTransform(gridpoints, gridpoints_reg, h); - } - - // Compute the standard deviation of the transformed points - double gridpoints_reg_sd = cppSD(gridpoints_reg); - - // get warning message - if(gridpoints_reg_sd < 1.0 | gridpoints_reg_sd > max(height, width)){ - Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; - } - - return gridpoints_reg_sd; -} - -bool checkMaskAbundance(Mat &mask){ - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - j++; - } - } - return j > 6; -} - -// compare the distance between two sets of match points -double medianMappingDistance(std::vector &keypoints1, std::vector &keypoints2, Mat &h) { - std::vector keypoints1_warped; - if(keypoints1.size() > 0){ - if (h.rows == 2){ - cv::transform(keypoints1, keypoints1_warped, h); - } else { - cv::perspectiveTransform(keypoints1, keypoints1_warped, h); - } - } - - return medianDistances(keypoints1_warped, keypoints2); -} - -// calculate inlier percentage -int checkInlierPercentage(Mat &mask){ - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - j++; - } - } - double ratio = (double) j/mask.rows; - double perc = round(100.0 * ratio); - return (int) perc; -} - -void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, - std::vector &keypoints1_masked, std::vector &keypoints2_masked, - std::vector &top_matches, Mat &mask) -{ - int j=0; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - keypoints1_masked.push_back(keypoints1_good[i]); - keypoints2_masked.push_back(keypoints2_good[i]); - top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); - j++; - } - } -} - -// void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, -// std::vector &keypoints1_masked, std::vector &keypoints2_masked, -// Mat &mask) -// { -// int j=0; -// for (int i = 0; i < mask.rows; i++) { -// if (mask.at(i)) { -// keypoints1_masked.push_back(keypoints1_good[i]); -// keypoints2_masked.push_back(keypoints2_good[i]); -// j++; -// } -// } -// } - -// check if keypoints are degenerate -bool checkDegenerate(double pts1, double pts2) { - - // get warning message - bool is_degenerate = FALSE; - if(pts1 < 1.0 | pts2 < 1.0){ - is_degenerate = TRUE; - Rcout << "WARNING: points may be in a degenerate configuration." << endl; - } - - return is_degenerate; -} - -// do overall checks on keypoints and images -std::vector getTransformationMetrics(std::vector &points1, - std::vector &points2, - Mat &im2, Mat &h, Mat &mask) { - - // metrics list - std::vector metrics_list; - - // Alignment report - Rcout << "Alignment Report: " << endl; - - // Report final keypoints - Rcout << " Calculated transformation matrix with " << points1.size() << " keypoints" << endl; - - // points stand. dev. - double points1_sd = cppSD(points1); - double points2_sd = cppSD(points2); - if(points1_sd < 1.0 | points2_sd < 1.0){ - Rcout << " WARNING: points may be in a degenerate configuration." << endl; - } - Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; - metrics_list.push_back(checkDegenerate(points1_sd, points2_sd)); - metrics_list.push_back(points1_sd); - metrics_list.push_back(points2_sd); - - // check distribution of points - double stddev = checkMappedGridDistribution(im2, h); - Rcout << " Std dev of registered points: " << stddev << endl; - metrics_list.push_back(stddev); - - // warp keypoints and compare - double md = medianMappingDistance(points1, points2, h); - Rcout << " Median distance between points: " << md << endl; - if(md > 3){ - Rcout << " WARNING: Transformation may be poor - mean euclidean distance of mapped source and destination key points is high!" << endl; - } - - // get inlier percentages - double ratio = checkInlierPercentage(mask); - Rcout << " Inlier Percentage: " << ratio << endl; - - // return is_degenerate; - return metrics_list; -} - //// // Manage Keypoints and Matches //// @@ -785,11 +619,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } - // get metrics - std::vector metrics; - metrics = getTransformationMetrics(points1, points2, im2, h, mask); - Rcout << "Registration is " << (metrics[0] ? "degenerate!" : "not degenerate!") << endl; - // Use homography to warp image if(h.rows == 2){ warpAffine(im1Proc, im1Proc, h, im2Proc.size()); @@ -802,6 +631,11 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, return; } + // get metrics + std::vector keypoint_metrics, image_metrics; + keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); + image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h); + // Rcout << "DONE: warped query image" << endl; /////////////////////// diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index a1614b5e..984d9abb 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -4,8 +4,9 @@ #include #include "opencv2/shape/shape_transformer.hpp" -// Auxiliary +// Library #include "auxiliary.h" +#include "metrics.h" // Namespaces using namespace Rcpp; @@ -87,6 +88,10 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cv::perspectiveTransform(query_mat, query_reg, h); } + // get metrics + std::vector image_metrics; + image_metrics = getAlignmentMetrics(im1Affine, im2, h); + if(!run_TPS){ // clone and exit diff --git a/src/metrics.cpp b/src/metrics.cpp new file mode 100644 index 00000000..d9d5c088 --- /dev/null +++ b/src/metrics.cpp @@ -0,0 +1,349 @@ +#include + +// OpenCV +#include +#include "opencv2/features2d.hpp" +#include "opencv2/shape/shape_transformer.hpp" +// #include + +// Internal functions +#include "auxiliary.h" +#include "image.h" + +// Namespaces +using namespace Rcpp; +using namespace std; +using namespace cv; + +//// +// Quality Control +//// + +// check distribution of registered points +double checkMappedGridDistribution(Mat &im, Mat &h){ + + // message + std::string message; + + // get image shape + int height = im.rows; + int width = im.cols; + int height_interval = height > 50 ? (double) height/50.0 : 1; + int width_interval = width > 50 ? (double) width/50.0 : 1; + + // perspective transformation of grid points + std::vector gridpoints; + for (double i = 0.0; i <= height; i += height_interval) { + for (double j = 0.0; j <= width; j += width_interval) { + gridpoints.push_back(cv::Point2f(j,i)); + } + } + + // register grid points + std::vector gridpoints_reg; + if (h.rows == 2){ + cv::transform(gridpoints, gridpoints_reg, h); + } else if(h.rows == 3) { + cv::perspectiveTransform(gridpoints, gridpoints_reg, h); + } + + // Compute the standard deviation of the transformed points + double gridpoints_reg_sd = cppSD(gridpoints_reg); + + // get warning message + if(gridpoints_reg_sd < 1.0 | gridpoints_reg_sd > max(height, width)){ + Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; + } + + return gridpoints_reg_sd; +} + +bool checkMaskAbundance(Mat &mask){ + int j=0; + for (int i = 0; i < mask.rows; i++) { + if (mask.at(i)) { + j++; + } + } + return j > 6; +} + +// compare the distance between two sets of match points +double medianMappingDistance(std::vector &keypoints1, std::vector &keypoints2, Mat &h) { + std::vector keypoints1_warped; + if(keypoints1.size() > 0){ + if (h.rows == 2){ + cv::transform(keypoints1, keypoints1_warped, h); + } else { + cv::perspectiveTransform(keypoints1, keypoints1_warped, h); + } + } + + return medianDistances(keypoints1_warped, keypoints2); +} + +// calculate inlier percentage +int checkInlierPercentage(Mat &mask){ + int j=0; + for (int i = 0; i < mask.rows; i++) { + if (mask.at(i)) { + j++; + } + } + double ratio = (double) j/mask.rows; + double perc = round(100.0 * ratio); + return (int) perc; +} + +void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, + std::vector &keypoints1_masked, std::vector &keypoints2_masked, + std::vector &top_matches, Mat &mask) +{ + int j=0; + for (int i = 0; i < mask.rows; i++) { + if (mask.at(i)) { + keypoints1_masked.push_back(keypoints1_good[i]); + keypoints2_masked.push_back(keypoints2_good[i]); + top_matches.push_back(cv::DMatch(static_cast(j), static_cast(j), 0)); + j++; + } + } +} + +// void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, +// std::vector &keypoints1_masked, std::vector &keypoints2_masked, +// Mat &mask) +// { +// int j=0; +// for (int i = 0; i < mask.rows; i++) { +// if (mask.at(i)) { +// keypoints1_masked.push_back(keypoints1_good[i]); +// keypoints2_masked.push_back(keypoints2_good[i]); +// j++; +// } +// } +// } + +// check if keypoints are degenerate +bool checkDegenerate(double pts1, double pts2) { + + // get warning message + bool is_degenerate = FALSE; + if(pts1 < 1.0 | pts2 < 1.0){ + is_degenerate = TRUE; + Rcout << "WARNING: points may be in a degenerate configuration." << endl; + } + + return is_degenerate; +} + +cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize) +{ + // generate mask + cv::Mat mask = cv::Mat::ones(im.size(), CV_8UC1) * 255; + cv::Mat warped; + + // Keep masks crisp: nearest-neighbor only. + const int interp = cv::INTER_NEAREST; + const int borderMode = cv::BORDER_CONSTANT; + const cv::Scalar borderValue(0); + + // warp mask + if (h.rows == 2){ + cv::warpAffine(mask, warped, h, dsize, + interp, borderMode, borderValue); + } else { + cv::warpPerspective(mask, warped, h, dsize, + interp, borderMode, borderValue); + } + + // Force binary mask again. + cv::threshold(warped, warped, 0, 255, cv::THRESH_BINARY); + return warped; +} + +double Entropy(cv::Mat& im1, cv::Mat& overlapMask, int bins = 256) { + + // Histogram settings + int histSize = 256; + float range[] = {0.0, 256.0}; + const float* histRange = {range}; + int channels[] = {0}; + + // Compute histograms + cv::Mat hist; + cv::calcHist(&im1, 1, channels, overlapMask, + hist, 1, &histSize, &histRange); + + // Normalize histograms + cv::normalize(hist, hist, 0, 1, cv::NORM_MINMAX); + + // Convert counts to probabilities + hist /= cv::sum(hist)[0]; + + double entropy = 0.0; + for (int r = 0; r < hist.rows; ++r) + { + const float* ptr = hist.ptr(r); + + for (int c = 0; c < hist.cols; ++c) + { + double p = ptr[c]; + + if (p > 0.0) + entropy -= p * std::log(p); + } + } + + return entropy; +} + +double jointEntropy(cv::Mat& im1, cv::Mat& im2, + cv::Mat& overlapMask, int bins = 256) { + + // 2D histogram parameters + int histSize[] = {bins, bins}; + float range[] = {0.f, 256.f}; + const float* ranges[] = {range, range}; + int channels[] = {0, 1}; + + // calculate histogram + cv::Mat images[] = {im1, im2}; + cv::Mat hist; + cv::calcHist(images, + 2, + channels, + overlapMask, + hist, + 2, + histSize, + ranges, + true, + false); + cv::normalize(hist, hist, 0, 1, cv::NORM_MINMAX); + + // Convert counts to probabilities + hist /= cv::sum(hist)[0]; + + double entropy = 0.0; + for (int r = 0; r < hist.rows; ++r) + { + const float* ptr = hist.ptr(r); + + for (int c = 0; c < hist.cols; ++c) + { + double p = ptr[c]; + + if (p > 0.0) + entropy -= p * std::log(p); + } + } + + return entropy; +} + +double MutualInfo(cv::Mat& im1, cv::Mat& im2, + cv::Mat& overlapMask, int bins = 256) { + double ent1=Entropy(im1, overlapMask, bins); + double ent2=Entropy(im2, overlapMask, bins); + double ent12=jointEntropy(im1, im2, overlapMask, bins); + return ent1+ent2-ent12; +} + +double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, + cv::Mat& overlapMask, int bins = 256) { + double ent1=Entropy(im1, overlapMask, bins); + double ent2=Entropy(im2, overlapMask, bins); + double ent12=jointEntropy(im1, im2, overlapMask, bins); + return (ent1+ent2)/ent12; +} + +std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h){ + + // Histogram settings + int histSize = 256; + float range[] = {0.0, 256.0}; + const float* histRange = {range}; + int channels[] = {0}; + + // get overlap mask + cv::Mat alignmentMask = generateOverlapMask(im1, h, im2.size()); + + // Compute histograms + cv::Mat hist1, hist2; + cv::calcHist(&im1, 1, channels, alignmentMask, + hist1, 1, &histSize, &histRange); + cv::calcHist(&im2, 1, channels, alignmentMask, + hist2, 1, &histSize, &histRange); + + // Normalize histograms + cv::normalize(hist1, hist1, 0, 1, cv::NORM_MINMAX); + cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX); + + // Summary + Rcout << "Alignment Report: " << endl; + std::vector metrics; + metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_CHISQR)); + metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT)); + metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA)); + metrics.push_back(jointEntropy(im1, im2, alignmentMask, histSize)); + metrics.push_back(MutualInfo(im1, im2, alignmentMask, histSize)); + metrics.push_back(NormalizedMutualInfo(im1, im2, alignmentMask, histSize)); + + Rcout << " Chi-Square: " << metrics[0] << std::endl; + Rcout << " Intersection: " << metrics[1] << std::endl; + Rcout << " Bhattacharyya: " << metrics[2] << std::endl; + Rcout << " Joint Entropy: " << metrics[3] << std::endl; + Rcout << " MutualInfo: " << metrics[4] << std::endl; + Rcout << " NormalizedMutualInfo: " << metrics[5] << std::endl; + return metrics; +} + +// do overall checks on keypoints and images +std::vector getKeypointMetrics(std::vector &points1, + std::vector &points2, + Mat &im1, Mat &im2, + Mat &h, Mat &mask) { + + // metrics list + std::vector metrics_list; + + // Alignment report + Rcout << "Keypoint Report: " << endl; + + // Report final keypoints + Rcout << " Calculated transformation matrix with " << points1.size() << " keypoints" << endl; + + // points stand. dev. + double points1_sd = cppSD(points1); + double points2_sd = cppSD(points2); + if(points1_sd < 1.0 | points2_sd < 1.0){ + Rcout << " WARNING: points may be in a degenerate configuration." << endl; + } + Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; + metrics_list.push_back(checkDegenerate(points1_sd, points2_sd)); + metrics_list.push_back(points1_sd); + metrics_list.push_back(points2_sd); + + // check distribution of points + double stddev = checkMappedGridDistribution(im2, h); + Rcout << " Std dev of registered points: " << stddev << endl; + metrics_list.push_back(stddev); + + // warp keypoints and compare + double md = medianMappingDistance(points1, points2, h); + Rcout << " Median distance between points: " << md << endl; + if(md > 3){ + Rcout << " WARNING: Transformation may be poor - mean euclidean distance of mapped source and destination key points is high!" << endl; + } + + // get inlier percentages + double ratio = checkInlierPercentage(mask); + Rcout << " Inlier Percentage: " << ratio << endl; + + // degenerate ? + Rcout << "Registration is " << (metrics_list[0] ? "degenerate!" : "not degenerate!") << endl; + + // return is_degenerate; + return metrics_list; +} \ No newline at end of file diff --git a/src/metrics.h b/src/metrics.h new file mode 100644 index 00000000..7c821767 --- /dev/null +++ b/src/metrics.h @@ -0,0 +1,35 @@ +#include "Rcpp.h" +#include + +#ifndef METRICS_H +#define METRICS_H + +// check distribution of registered points +double checkMappedGridDistribution(cv::Mat &im, cv::Mat &h); + +bool checkMaskAbundance(cv::Mat &mask); + +// compare the distance between two sets of match points +double medianMappingDistance(std::vector &keypoints1, std::vector &keypoints2, cv::Mat &h); + +// calculate inlier percentage +int checkInlierPercentage(cv::Mat &mask); + +void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints2_good, + std::vector &keypoints1_masked, std::vector &keypoints2_masked, + std::vector &top_matches, cv::Mat &mask); + +// check if keypoints are degenerate +bool checkDegenerate(double pts1, double pts2); + +cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize); + +std::vector getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &h); + +// do overall checks on keypoints and images +std::vector getKeypointMetrics(std::vector &points1, + std::vector &points2, + cv::Mat &im1, cv::Mat &im2, + cv::Mat &h, cv::Mat &mask); + +#endif \ No newline at end of file From ff4f895d2abac3dcea2528a07def55128cccaa63 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 18 Jun 2026 11:42:24 +0200 Subject: [PATCH 03/37] small modifs to metrics --- src/automated_registration.cpp | 6 +++++- src/manual_registration.cpp | 2 +- src/metrics.cpp | 29 ++++++++++++++++------------- src/metrics.h | 6 ++++-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 24da4e48..5de11c2d 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -619,6 +619,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } + // imwrite("img1_before.tif", im1Proc); + // Use homography to warp image if(h.rows == 2){ warpAffine(im1Proc, im1Proc, h, im2Proc.size()); @@ -634,7 +636,9 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get metrics std::vector keypoint_metrics, image_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); - image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h); + image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, im1.size()); + // imwrite("img1.tif", im1Proc); + // imwrite("img2.tif", im2Proc); // Rcout << "DONE: warped query image" << endl; diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 984d9abb..49743ea5 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -90,7 +90,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // get metrics std::vector image_metrics; - image_metrics = getAlignmentMetrics(im1Affine, im2, h); + image_metrics = getAlignmentMetrics(im1Affine, im2, h, im1.size()); if(!run_TPS){ diff --git a/src/metrics.cpp b/src/metrics.cpp index d9d5c088..042cb70b 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -137,17 +137,18 @@ bool checkDegenerate(double pts1, double pts2) { return is_degenerate; } -cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize) +cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, + cv::Size dsize, cv::Size ssize) { // generate mask - cv::Mat mask = cv::Mat::ones(im.size(), CV_8UC1) * 255; + cv::Mat mask = cv::Mat::ones(ssize, CV_8UC1) * 255; cv::Mat warped; // Keep masks crisp: nearest-neighbor only. const int interp = cv::INTER_NEAREST; const int borderMode = cv::BORDER_CONSTANT; const cv::Scalar borderValue(0); - + // warp mask if (h.rows == 2){ cv::warpAffine(mask, warped, h, dsize, @@ -156,7 +157,7 @@ cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize) cv::warpPerspective(mask, warped, h, dsize, interp, borderMode, borderValue); } - + // Force binary mask again. cv::threshold(warped, warped, 0, 255, cv::THRESH_BINARY); return warped; @@ -258,7 +259,8 @@ double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, return (ent1+ent2)/ent12; } -std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h){ +std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, + cv::Size ssize){ // Histogram settings int histSize = 256; @@ -267,8 +269,9 @@ std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h){ int channels[] = {0}; // get overlap mask - cv::Mat alignmentMask = generateOverlapMask(im1, h, im2.size()); - + cv::Mat alignmentMask = generateOverlapMask(im1, h, im2.size(), ssize); + // imwrite("mask.tif", alignmentMask); + // Compute histograms cv::Mat hist1, hist2; cv::calcHist(&im1, 1, channels, alignmentMask, @@ -286,16 +289,16 @@ std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h){ metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_CHISQR)); metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT)); metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA)); - metrics.push_back(jointEntropy(im1, im2, alignmentMask, histSize)); - metrics.push_back(MutualInfo(im1, im2, alignmentMask, histSize)); - metrics.push_back(NormalizedMutualInfo(im1, im2, alignmentMask, histSize)); + // metrics.push_back(jointEntropy(im1, im2, alignmentMask, histSize)); + // metrics.push_back(MutualInfo(im1, im2, alignmentMask, histSize)); + // metrics.push_back(NormalizedMutualInfo(im1, im2, alignmentMask, histSize)); Rcout << " Chi-Square: " << metrics[0] << std::endl; Rcout << " Intersection: " << metrics[1] << std::endl; Rcout << " Bhattacharyya: " << metrics[2] << std::endl; - Rcout << " Joint Entropy: " << metrics[3] << std::endl; - Rcout << " MutualInfo: " << metrics[4] << std::endl; - Rcout << " NormalizedMutualInfo: " << metrics[5] << std::endl; + // Rcout << " Joint Entropy: " << metrics[3] << std::endl; + // Rcout << " MutualInfo: " << metrics[4] << std::endl; + // Rcout << " NormalizedMutualInfo: " << metrics[5] << std::endl; return metrics; } diff --git a/src/metrics.h b/src/metrics.h index 7c821767..550774b7 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -22,9 +22,11 @@ void maskKeypoints(std::vector &keypoints1_good, std::vector getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &h); +std::vector getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &h, + cv::Size dsize); // do overall checks on keypoints and images std::vector getKeypointMetrics(std::vector &points1, From 9decb56f53cacc2c9ce6a10d920da2ea250129c1 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Tue, 7 Jul 2026 16:29:37 +0200 Subject: [PATCH 04/37] use intersection and bhattacharyya measures for accuracy --- src/metrics.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/metrics.cpp b/src/metrics.cpp index 042cb70b..0fada758 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -286,16 +286,16 @@ std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, // Summary Rcout << "Alignment Report: " << endl; std::vector metrics; - metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_CHISQR)); metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT)); metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA)); + //metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_CHISQR)); // metrics.push_back(jointEntropy(im1, im2, alignmentMask, histSize)); // metrics.push_back(MutualInfo(im1, im2, alignmentMask, histSize)); // metrics.push_back(NormalizedMutualInfo(im1, im2, alignmentMask, histSize)); - Rcout << " Chi-Square: " << metrics[0] << std::endl; - Rcout << " Intersection: " << metrics[1] << std::endl; - Rcout << " Bhattacharyya: " << metrics[2] << std::endl; + Rcout << " Intersection: " << metrics[0] << std::endl; + Rcout << " Bhattacharyya: " << metrics[1] << std::endl; + // Rcout << " Chi-Square: " << metrics[0] << std::endl; // Rcout << " Joint Entropy: " << metrics[3] << std::endl; // Rcout << " MutualInfo: " << metrics[4] << std::endl; // Rcout << " NormalizedMutualInfo: " << metrics[5] << std::endl; From 5cfefa7c4a34f19bc5a67a66a455e96173fdc77d Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 12 Jul 2026 15:48:12 +0200 Subject: [PATCH 05/37] compilable matte mi code --- src/manual_registration.cpp | 4 + src/matte_mi.cpp | 588 ++++++++++++++++++++++++++++++++++++ src/matte_mi.h | 28 ++ src/metrics.cpp | 1 + 4 files changed, 621 insertions(+) create mode 100644 src/matte_mi.cpp create mode 100644 src/matte_mi.h diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 44ca9316..41bbdf9d 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -128,6 +128,10 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cv::perspectiveTransform(query_mat, query_reg, h); } + // TODO: remove later + // imwrite("img1.tif", im1Affine); + // imwrite("img2.tif", im2); + // get metrics std::vector image_metrics; image_metrics = getAlignmentMetrics(im1Affine, im2, h, im1.size()); diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp new file mode 100644 index 00000000..f2d2a87b --- /dev/null +++ b/src/matte_mi.cpp @@ -0,0 +1,588 @@ +#include + +// OpenCV +#include + +// Internal functions +#include "auxiliary.h" +#include "image.h" + +// Namespaces +using namespace Rcpp; +using namespace std; +using namespace cv; + +struct IntensityRange { + double min; + double max; +}; + +//// +// Chunk wise Matte MI +//// + +double cubicBSpline(double u) { + u = std::abs(u); + + if (u < 1.0) { + const double u2 = u * u; + const double u3 = u2 * u; + + return (4.0 - 6.0 * u2 + 3.0 * u3) / 6.0; + } + + if (u < 2.0) { + const double t = 2.0 - u; + return (t * t * t) / 6.0; + } + + return 0.0; +} + +double scaleToBinPosition( + double value, + IntensityRange range, + double low, + double high) { + + if (!std::isfinite(range.min) || + !std::isfinite(range.max) || + !(range.max > range.min)) { + throw std::invalid_argument("Invalid intensity range."); + } + + value = std::clamp(value, range.min, range.max); + + return low + + (value - range.min) * (high - low) / + (range.max - range.min); +} + +std::size_t roundToNearestEvenNonnegative(double x) { + const double lowerAsDouble = std::floor(x); + const double fraction = x - lowerAsDouble; + const auto lower = static_cast(lowerAsDouble); + + if (fraction < 0.5) { + return lower; + } + + if (fraction > 0.5) { + return lower + 1U; + } + + return (lower % 2U == 0U) ? lower : lower + 1U; +} + +bool isValidRange(IntensityRange range) { + return std::isfinite(range.min) && + std::isfinite(range.max) && + range.max > range.min; +} + +/** + * Compute Mattes-style mutual information from paired sample values. + * + * Fixed samples use nearest-bin assignment. + * Moving samples use cubic B-spline Parzen smoothing. + * + * @param fixedValues Pointer to fixed-image sample values. + * @param movingValues Pointer to corresponding moving-image values. + * @param countPair Number of paired values. + * @param bins Number of histogram bins; must be >= 4. + * @param fixedRange Optional fixed intensity range. + * @param movingRange Optional moving intensity range. + * + * @return Mutual information in nats, or quiet NaN for degenerate data. + */ +double mattesMiFromValues( + const double* fixedValues, + const double* movingValues, + std::size_t countPair, + std::size_t bins = 64, + std::optional fixedRange = std::nullopt, + std::optional movingRange = std::nullopt) { + + const double nan = + std::numeric_limits::quiet_NaN(); + + if (countPair != 0U && + (fixedValues == nullptr || movingValues == nullptr)) { + throw std::invalid_argument("Input value pointer is null."); + } + + /* + * First pass: + * - count finite sample pairs + * - calculate automatic intensity ranges + */ + std::size_t validCount = 0; + + double fixedMin = std::numeric_limits::infinity(); + double fixedMax = -std::numeric_limits::infinity(); + + double movingMin = std::numeric_limits::infinity(); + double movingMax = -std::numeric_limits::infinity(); + + for (std::size_t i = 0; i < countPair; ++i) { + + if (!std::isfinite(fixedValues[i]) || + !std::isfinite(movingValues[i])) { + continue; + } + + ++validCount; + + fixedMin = std::min(fixedMin, fixedValues[i]); + fixedMax = std::max(fixedMax, fixedValues[i]); + + movingMin = std::min(movingMin, movingValues[i]); + movingMax = std::max(movingMax, movingValues[i]); + } + + // Preserve the Python function's validation order. + if (validCount < 2U) { + return nan; + } + + if (bins < 4U) { + throw std::invalid_argument( + "bins must be >= 4 for cubic B-spline smoothing."); + } + + if (bins > + std::numeric_limits::max() / bins || + bins > + static_cast( + std::numeric_limits::max())) { + throw std::length_error( + "Histogram dimensions are too large."); + } + + /* + * Row: fixed-image bin + * Column: moving-image bin + */ + std::vector jointHistogram( + bins * bins, + 0.0); + + /* + * Second pass: construct the joint histogram. + */ + for (std::size_t i = 0; i < countPair; ++i) { + const double fixedValue = static_cast(fixedValues[i]); + const double movingValue = static_cast(movingValues[i]); + + if (!std::isfinite(fixedValue) || + !std::isfinite(movingValue)) { + continue; + } + + /* + * Fixed image: + * map to [0, bins - 1] and use nearest-bin assignment. + */ + const double fixedPosition = + scaleToBinPosition( + fixedValue, + *fixedRange, + 0.0, + static_cast(bins - 1U)); + + std::size_t fixedBin = + roundToNearestEvenNonnegative( + fixedPosition); + + fixedBin = std::min( + fixedBin, + bins - 1U); + + /* + * Moving image: + * map to [1, bins - 2] so the cubic kernel has + * room at both ends of the histogram. + */ + const double movingPosition = + scaleToBinPosition( + movingValue, + *movingRange, + 1.0, + static_cast(bins - 2U)); + + const auto baseBin = + static_cast( + std::floor(movingPosition)); + + /* + * A cubic B-spline contributes to at most four bins. + */ + for (int offset = -1; offset <= 2; ++offset) { + const std::ptrdiff_t movingBin = + baseBin + offset; + + if (movingBin < 0 || + movingBin >= + static_cast(bins)) { + continue; + } + + const double weight = + cubicBSpline(movingPosition - static_cast(movingBin)); + + if (weight <= 0.0) { + continue; + } + + jointHistogram[ + fixedBin * bins + + static_cast(movingBin) + ] += weight; + } + } + + const double total = + std::accumulate( + jointHistogram.begin(), + jointHistogram.end(), + 0.0); + + if (!(total > 0.0)) { + return nan; + } + + /* + * Marginal distributions. + */ + std::vector px(bins, 0.0); + std::vector py(bins, 0.0); + + for (std::size_t fixedBin = 0; + fixedBin < bins; + ++fixedBin) { + + for (std::size_t movingBin = 0; + movingBin < bins; + ++movingBin) { + + const double pxy = + jointHistogram[ + fixedBin * bins + movingBin + ] / total; + + px[fixedBin] += pxy; + py[movingBin] += pxy; + } + } + + /* + * MI = sum p(x,y) log(p(x,y) / (p(x)p(y))) + */ + double mi = 0.0; + + for (std::size_t fixedBin = 0; + fixedBin < bins; + ++fixedBin) { + + for (std::size_t movingBin = 0; + movingBin < bins; + ++movingBin) { + + const double pxy = + jointHistogram[ + fixedBin * bins + movingBin + ] / total; + + const double pxPy = + px[fixedBin] * py[movingBin]; + + if (pxy > 0.0 && pxPy > 0.0) { + mi += + pxy * std::log(pxy / pxPy); + } + } + } + + // std::log is the natural logarithm, so MI is in nats. + return mi; +} + +//// +// Matte MI +//// + +/** + * Spatial chunk dimensions. + * + * The field order intentionally follows the Python API: + * ChunkSize{height, width} + * + * This avoids cv::Size's opposite (width, height) ordering. + */ +struct ChunkSize { + int height = 256; + int width = 256; +}; + +/** + * Output of chunkedNmiMap(). + * + * Despite the legacy nmiMap name, the values are Mattes-style mutual + * information values, not normalized mutual information values. + * + * Matrix layouts: + * nmiMap : CV_64FC1, shape [chunk rows, chunk columns] + * bounds : CV_32SC4, each element is [y0, y1, x0, x1] + * centers : CV_64FC2, each element is [y center, x center] + */ +struct ChunkedNmiMapResult { + cv::Mat1d nmiMap; + cv::Mat_ bounds; + cv::Mat_ centers; +}; + +int ceilDividePositive(int value, int divisor) noexcept { + return value / divisor + ((value % divisor) != 0 ? 1 : 0); +} + +double linearPercentileFromSorted( + const std::vector& sortedValues, + double percentile) { + if (sortedValues.empty()) { + throw std::invalid_argument( + "Cannot calculate a percentile of an empty array."); + } + + if (!std::isfinite(percentile) || + percentile < 0.0 || percentile > 100.0) { + throw std::invalid_argument( + "Percentile must be finite and in [0, 100]."); + } + + if (sortedValues.size() == 1U) { + return sortedValues.front(); + } + + // Matches NumPy's default linear percentile interpolation: + // index = (N - 1) * percentile / 100. + const double index = + (static_cast(sortedValues.size() - 1U) * percentile) / + 100.0; + + const auto lowerIndex = + static_cast(std::floor(index)); + const auto upperIndex = + static_cast(std::ceil(index)); + const double fraction = index - static_cast(lowerIndex); + + const double lower = sortedValues[lowerIndex]; + const double upper = sortedValues[upperIndex]; + + return lower + fraction * (upper - lower); +} + +void validateChunkedMatteMIInputs( + cv::Mat1b& validGlobal, + std::size_t& validGlobalCount, + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + ChunkSize chunkSize, + int bins) { + + const int height = fixed.rows; + const int width = fixed.cols; + + for (int y = 0; y < height; ++y) { + const double* fixedRow = fixed.ptr(y); + const double* movingRow = moving.ptr(y); + const double* maskRow = + mask.empty() ? nullptr : mask.ptr(y); + unsigned char* validRow = validGlobal.ptr(y); + + for (int x = 0; x < width; ++x) { + const bool insideMask = + maskRow == nullptr || static_cast(maskRow[x]); + + const bool valid = + insideMask && + std::isfinite(fixedRow[x]) && + std::isfinite(movingRow[x]); + + if (valid) { + validRow[x] = 1U; + ++validGlobalCount; + } + } + } + + // stop if no pixels are valid + if (validGlobalCount == 0U) { + throw std::invalid_argument( + "The mask contains no valid pixels."); + } + +} + +ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + ChunkSize chunkSize = ChunkSize{}, + int bins = 50) { + + // validate chunks and return validation map of pixels + const int height = fixed.rows; + const int width = fixed.cols; + cv::Mat1b validGlobal(height, width, static_cast(0)); + std::size_t validGlobalCount = 0U; + validateChunkedMatteMIInputs(validGlobal, + validGlobalCount, + fixed, + moving, + mask, + chunkSize, + bins); + + // Do I need these to be cv_64f ? + cv::Mat fixed64; + cv::Mat moving64; + fixed.convertTo(fixed64, CV_64F); + moving.convertTo(moving64, CV_64F); + + // Do I need these to be cv_64f ? + cv::Mat mask64; + if (!mask.empty()) { + mask.convertTo(mask64, CV_64F); + } + + // Calculate one fixed-image range and one moving-image range globally, + // then reuse those ranges in every chunk. This makes chunk values + // comparable across the image. + std::vector fixedGlobalValues; + std::vector movingGlobalValues; + fixedGlobalValues.reserve(validGlobalCount); + movingGlobalValues.reserve(validGlobalCount); + + for (int y = 0; y < height; ++y) { + const double* fixedRow = fixed64.ptr(y); + const double* movingRow = moving64.ptr(y); + const unsigned char* validRow = + validGlobal.ptr(y); + + for (int x = 0; x < width; ++x) { + if (validRow[x] != 0U) { + fixedGlobalValues.push_back(fixedRow[x]); + movingGlobalValues.push_back(movingRow[x]); + } + } + } + + std::sort(fixedGlobalValues.begin(), fixedGlobalValues.end()); + std::sort(movingGlobalValues.begin(), movingGlobalValues.end()); + + constexpr double lowerPercentile = 0.5; + constexpr double upperPercentile = 99.5; + + const IntensityRange fixedRange{ + linearPercentileFromSorted(fixedGlobalValues, lowerPercentile), + linearPercentileFromSorted(fixedGlobalValues, upperPercentile) + }; + + // Intentional correction from the pasted Python: movingRange is derived + // from moving-image values, not from fixed-image values. + const IntensityRange movingRange{ + linearPercentileFromSorted(movingGlobalValues, lowerPercentile), + linearPercentileFromSorted(movingGlobalValues, upperPercentile) + }; + + if (!isValidRange(fixedRange)) { + throw std::invalid_argument( + "Invalid fixed intensity range."); + } + + if (!isValidRange(movingRange)) { + throw std::invalid_argument( + "Invalid moving intensity range."); + } + + const int nRows = ceilDividePositive(height, chunkSize.height); + const int nCols = ceilDividePositive(width, chunkSize.width); + + ChunkedNmiMapResult result{ + cv::Mat1d(nRows, nCols), + cv::Mat_(nRows, nCols), + cv::Mat_(nRows, nCols)}; + + const double nan = + std::numeric_limits::quiet_NaN(); + + // only set nmimap, why need bounds and centers + result.nmiMap.setTo(cv::Scalar(nan)); + // result.bounds.setTo(cv::Scalar::all(0)); + // result.centers.setTo(cv::Scalar::all(0)); + + constexpr std::size_t minValidPixels = 100U; + constexpr double minValidFraction = 0.10; + + std::vector fixedChunkValues; + std::vector movingChunkValues; + + for (int row = 0; row < nRows; ++row) { + for (int col = 0; col < nCols; ++col) { + const int y0 = row * chunkSize.height; + const int x0 = col * chunkSize.width; + + // Written this way instead of y0 + chunkSize.height to avoid + // signed integer overflow for extreme dimensions. + const int y1 = y0 + std::min(chunkSize.height, height - y0); + const int x1 = x0 + std::min(chunkSize.width, width - x0); + + const std::size_t totalPixels = + static_cast(y1 - y0) * + static_cast(x1 - x0); + + fixedChunkValues.clear(); + movingChunkValues.clear(); + fixedChunkValues.reserve(totalPixels); + movingChunkValues.reserve(totalPixels); + + for (int y = y0; y < y1; ++y) { + const double* fixedRow = fixed64.ptr(y); + const double* movingRow = moving64.ptr(y); + const unsigned char* validRow = + validGlobal.ptr(y); + + for (int x = x0; x < x1; ++x) { + if (validRow[x] != 0U) { + fixedChunkValues.push_back(fixedRow[x]); + movingChunkValues.push_back(movingRow[x]); + } + } + } + + const std::size_t validPixels = fixedChunkValues.size(); + + if (validPixels < minValidPixels) { + continue; + } + + const double validFraction = + static_cast(validPixels) / + static_cast(totalPixels); + + if (validFraction < minValidFraction) { + continue; + } + + result.nmiMap(row, col) = mattesMiFromValues( + fixedChunkValues.data(), + movingChunkValues.data(), + validPixels, + static_cast(bins), + std::optional{fixedRange}, + std::optional{movingRange}); + } + } + + return result; +} \ No newline at end of file diff --git a/src/matte_mi.h b/src/matte_mi.h new file mode 100644 index 00000000..9694ee16 --- /dev/null +++ b/src/matte_mi.h @@ -0,0 +1,28 @@ +#include "Rcpp.h" +#include + +#ifndef METRICS_H +#define METRICS_H + +struct IntensityRange { + double min; + double max; +}; + +double cubicBSpline(double u); + +double scaleToBinPosition(double value, IntensityRange range, + double low, double high); + +std::size_t roundToNearestEvenNonnegative(double x); + +bool isValidRange(IntensityRange range); + +double mattesMiFromValues(const double* fixedValues, + const double* movingValues, + std::size_t count, + std::size_t bins = 64, + std::optional fixedRange = std::nullopt, + std::optional movingRange = std::nullopt); + +#endif \ No newline at end of file diff --git a/src/metrics.cpp b/src/metrics.cpp index 0fada758..edf6657c 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -9,6 +9,7 @@ // Internal functions #include "auxiliary.h" #include "image.h" +#include "matte_mi.h" // Namespaces using namespace Rcpp; From b9c2c342936f0c5d8aae49737fd906535c0a02b7 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 12 Jul 2026 20:05:09 +0200 Subject: [PATCH 06/37] implement matte map for shiny app --- R/registration.R | 46 ++++++++++++++--- src/automated_registration.cpp | 26 +++++++--- src/manual_registration.cpp | 10 +++- src/matte_mi.cpp | 92 +++++++++++++++++++++++----------- src/matte_mi.h | 11 ++-- src/metrics.cpp | 17 +++---- src/metrics.h | 4 +- 7 files changed, 150 insertions(+), 56 deletions(-) diff --git a/R/registration.R b/R/registration.R index e5b0fac5..a07fd876 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3195,6 +3195,7 @@ getAutomatedRegisteration <- function( overlayed_image_list <- list() aligned_image_list <- list() alignment_image_list <- list() + matte_map_list <- list() for (i in register_ind) { # Increment the progress bar, and update the detail text. incProgress( @@ -3225,6 +3226,9 @@ getAutomatedRegisteration <- function( # save matches alignment_image_list[[i]] <- results$alignment_image + + # save matte map + matte_map_list[[i]] <- results$matte_map } } ) @@ -3255,11 +3259,30 @@ getAutomatedRegisteration <- function( }) # Plot Alignment + # lapply(register_ind, function(i) { + # cur_alignment_image <- alignment_image_list[[i]] + # output[[paste0("plot_alignment", i)]] <- renderPlot({ + # if (!suppressWarnings(is.na(cur_alignment_image))) { + # magick::image_ggplot(cur_alignment_image) + # } + # }) + # }) + + # Plot Matte lapply(register_ind, function(i) { - cur_alignment_image <- alignment_image_list[[i]] + cur_alignment_image <- matte_map_list[[i]] output[[paste0("plot_alignment", i)]] <- renderPlot({ - if (!suppressWarnings(is.na(cur_alignment_image))) { - magick::image_ggplot(cur_alignment_image) + if (!suppressWarnings(!is.matrix(cur_alignment_image))) { + cur_alignment_image <- + cur_alignment_image[nrow(cur_alignment_image):1,] + ggplot(reshape2::melt(cur_alignment_image), + aes(Var2, Var1, fill= value)) + + ggplot2::geom_tile() + + ggplot2::theme_void() + + ggplot2::coord_fixed(expand = FALSE) + + ggplot2::scale_fill_gradient(low = "#440154FF", + high = "#FDE725FF", + name = "Matte's MI") } }) }) @@ -3463,6 +3486,7 @@ computeAutomatedPairwiseTransform <- function( aligned_image <- reg$aligned_image alignment_image <- reg$alignment_image overlay_image <- reg$overlay_image + matte_map <- reg$matte_map } return(list( @@ -3470,7 +3494,8 @@ computeAutomatedPairwiseTransform <- function( dest_image = dest_image, aligned_image = aligned_image, alignment_image = alignment_image, - overlay_image = overlay_image + overlay_image = overlay_image, + matte_map = matte_map )) } @@ -3537,14 +3562,22 @@ getRcppAutomatedRegistration <- function( if (suppressWarnings(all(lapply(reg[[1]][[2]], is.null)))) { reg[[1]] <- list(reg[[1]][[1]], NULL) } + + # adjust matte mi map + tmp <- reg[[6]] + tmp[is.na(tmp)] <- 0 + tmp[tmp < 0] <- 0 + reg[[6]] <- tmp - # check for failed registeration + # check for failed registration aligned_image <- if (!is.null(reg[[3]])) magick::image_read(reg[[3]]) else NA alignment_image <- if (!is.null(reg[[4]])) magick::image_read(reg[[4]]) else NA overlay_image <- if (!is.null(reg[[5]])) magick::image_read(reg[[5]]) else NA + matte_map <- + if (!is.null(reg[[6]])) reg[[6]] else NA # return return(list( @@ -3552,7 +3585,8 @@ getRcppAutomatedRegistration <- function( dest_image = magick::image_read(reg[[2]]), aligned_image = aligned_image, alignment_image = alignment_image, - overlay_image = overlay_image + overlay_image = overlay_image, + matte_map = matte_map )) } diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 5de11c2d..761fb04c 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -10,6 +10,7 @@ #include "auxiliary.h" #include "image.h" #include "metrics.h" +#include "matte_mi.h" // Namespaces using namespace Rcpp; @@ -569,7 +570,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, const bool invert_query, const bool invert_ref, const char* flipflop_query, const char* flipflop_ref, const char* rotate_query, const char* rotate_ref, - const bool run_Affine, const bool run_TPS) + const bool run_Affine, const bool run_TPS, + Mat1d &accuracyMatte) { // parameters @@ -633,10 +635,18 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, return; } - // get metrics - std::vector keypoint_metrics, image_metrics; + // get keypoint metrics + std::vector keypoint_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); - image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, im1.size()); + + // get alignment metrics + std::vector image_metrics; + cv::Mat alignmentMask = generateOverlapMask(im1Proc, h, im2Proc.size(), im1.size()); + image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); + + // get matte metric + accuracyMatte = chunkedMatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + // imwrite("img1.tif", im1Proc); // imwrite("img2.tif", im2Proc); @@ -739,10 +749,11 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid) { // Return data - Rcpp::List out(5); + Rcpp::List out(6); Rcpp::List out_trans(2); Rcpp::List keypoints(2); Mat imOverlay, imReg, h, imMatches; + Mat1d accuracyMatte; // Read reference image cv::Mat imReference = imageToMat(ref_image, width1, height1); @@ -763,7 +774,8 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R invert_query, invert_ref, flipflop_query.get_cstring(), flipflop_ref.get_cstring(), rotate_query.get_cstring(), rotate_ref.get_cstring(), - run_Affine, run_TPS); + run_Affine, run_TPS, + accuracyMatte); // transformation matrix, can be either a matrix, set of keypoints or both out_trans[0] = matToNumericMatrix(h.clone()); @@ -779,10 +791,12 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R out[2] = matToImage(imReg); // registered image out[3] = matToImage(imMatches); // keypoint matching image out[4] = matToImage(imOverlay); // overlay image + out[5] = matToNumericMatrix(accuracyMatte); // Matte MI metric } else { out[2] = R_NilValue; out[3] = R_NilValue; out[4] = R_NilValue; + out[5] = R_NilValue; } // release diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 41bbdf9d..bbf71809 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -132,9 +132,15 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // imwrite("img1.tif", im1Affine); // imwrite("img2.tif", im2); - // get metrics + // // get metrics + // std::vector image_metrics; + // image_metrics = getAlignmentMetrics(im1Affine, im2, h, im1.size()); + + // get alignment metrics std::vector image_metrics; - image_metrics = getAlignmentMetrics(im1Affine, im2, h, im1.size()); + cv::Mat alignmentMask = generateOverlapMask(im1Affine, h, + im2.size(), im1Affine.size()); + image_metrics = getAlignmentMetrics(im1Affine, im2, h, alignmentMask); if(!run_TPS){ diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index f2d2a87b..7008e514 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -320,8 +320,8 @@ double mattesMiFromValues( * This avoids cv::Size's opposite (width, height) ordering. */ struct ChunkSize { - int height = 256; - int width = 256; + int height = 50; + int width = 50; }; /** @@ -423,25 +423,27 @@ void validateChunkedMatteMIInputs( } } - -ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, - const cv::Mat& moving, - const cv::Mat& mask, - ChunkSize chunkSize = ChunkSize{}, - int bins = 50) { - + +// ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, +cv::Mat1d chunkedMatteMIMap(const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins = 50) { + ChunkSize chunkSize = ChunkSize{}; + // validate chunks and return validation map of pixels - const int height = fixed.rows; - const int width = fixed.cols; - cv::Mat1b validGlobal(height, width, static_cast(0)); - std::size_t validGlobalCount = 0U; - validateChunkedMatteMIInputs(validGlobal, - validGlobalCount, - fixed, - moving, - mask, - chunkSize, - bins); + // const int height = fixed.rows; + // const int width = fixed.cols; + // cv::Mat1b validGlobal(height, width, static_cast(0)); + // std::size_t validGlobalCount = 0U; + // validateChunkedMatteMIInputs(validGlobal, + // validGlobalCount, + // fixed, + // moving, + // mask, + // chunkSize, + // bins); + // Do I need these to be cv_64f ? cv::Mat fixed64; @@ -455,6 +457,41 @@ ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, mask.convertTo(mask64, CV_64F); } + // validate + const int height = fixed.rows; + const int width = fixed.cols; + + cv::Mat1b validGlobal(height, width, static_cast(0)); + std::size_t validGlobalCount = 0U; + + for (int y = 0; y < height; ++y) { + const double* fixedRow = fixed64.ptr(y); + const double* movingRow = moving64.ptr(y); + const double* maskRow = + mask64.empty() ? nullptr : mask64.ptr(y); + unsigned char* validRow = validGlobal.ptr(y); + + for (int x = 0; x < width; ++x) { + const bool insideMask = + maskRow == nullptr || static_cast(maskRow[x]); + + const bool valid = + insideMask && + std::isfinite(fixedRow[x]) && + std::isfinite(movingRow[x]); + + if (valid) { + validRow[x] = 1U; + ++validGlobalCount; + } + } + } + + if (validGlobalCount == 0U) { + throw std::invalid_argument( + "The mask contains no valid pixels."); + } + // Calculate one fixed-image range and one moving-image range globally, // then reuse those ranges in every chunk. This makes chunk values // comparable across the image. @@ -466,6 +503,8 @@ ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, for (int y = 0; y < height; ++y) { const double* fixedRow = fixed64.ptr(y); const double* movingRow = moving64.ptr(y); + const double* maskRow = + mask64.empty() ? nullptr : mask64.ptr(y); const unsigned char* validRow = validGlobal.ptr(y); @@ -508,16 +547,13 @@ ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, const int nRows = ceilDividePositive(height, chunkSize.height); const int nCols = ceilDividePositive(width, chunkSize.width); - ChunkedNmiMapResult result{ - cv::Mat1d(nRows, nCols), - cv::Mat_(nRows, nCols), - cv::Mat_(nRows, nCols)}; - const double nan = std::numeric_limits::quiet_NaN(); // only set nmimap, why need bounds and centers - result.nmiMap.setTo(cv::Scalar(nan)); + cv::Mat1d NmiMap(nRows, nCols); + NmiMap.setTo(cv::Scalar(nan)); + // result.nmiMap.setTo(cv::Scalar(nan)); // result.bounds.setTo(cv::Scalar::all(0)); // result.centers.setTo(cv::Scalar::all(0)); @@ -574,7 +610,7 @@ ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, continue; } - result.nmiMap(row, col) = mattesMiFromValues( + NmiMap(row, col) = mattesMiFromValues( fixedChunkValues.data(), movingChunkValues.data(), validPixels, @@ -584,5 +620,5 @@ ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, } } - return result; + return NmiMap; } \ No newline at end of file diff --git a/src/matte_mi.h b/src/matte_mi.h index 9694ee16..b2ea9333 100644 --- a/src/matte_mi.h +++ b/src/matte_mi.h @@ -1,8 +1,8 @@ #include "Rcpp.h" #include -#ifndef METRICS_H -#define METRICS_H +#ifndef MATTE_MI_H +#define MATTE_MI_H struct IntensityRange { double min; @@ -21,8 +21,13 @@ bool isValidRange(IntensityRange range); double mattesMiFromValues(const double* fixedValues, const double* movingValues, std::size_t count, - std::size_t bins = 64, + std::size_t bins, std::optional fixedRange = std::nullopt, std::optional movingRange = std::nullopt); + +cv::Mat1d chunkedMatteMIMap(const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins); #endif \ No newline at end of file diff --git a/src/metrics.cpp b/src/metrics.cpp index edf6657c..d7723809 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -260,8 +260,7 @@ double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, return (ent1+ent2)/ent12; } -std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, - cv::Size ssize){ +std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ // Histogram settings int histSize = 256; @@ -270,14 +269,14 @@ std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, int channels[] = {0}; // get overlap mask - cv::Mat alignmentMask = generateOverlapMask(im1, h, im2.size(), ssize); - // imwrite("mask.tif", alignmentMask); + // cv::Mat mask = generateOverlapMask(im1, h, im2.size(), ssize); + // imwrite("mask.tif", mask); // Compute histograms cv::Mat hist1, hist2; - cv::calcHist(&im1, 1, channels, alignmentMask, + cv::calcHist(&im1, 1, channels, mask, hist1, 1, &histSize, &histRange); - cv::calcHist(&im2, 1, channels, alignmentMask, + cv::calcHist(&im2, 1, channels, mask, hist2, 1, &histSize, &histRange); // Normalize histograms @@ -290,9 +289,9 @@ std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT)); metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA)); //metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_CHISQR)); - // metrics.push_back(jointEntropy(im1, im2, alignmentMask, histSize)); - // metrics.push_back(MutualInfo(im1, im2, alignmentMask, histSize)); - // metrics.push_back(NormalizedMutualInfo(im1, im2, alignmentMask, histSize)); + // metrics.push_back(jointEntropy(im1, im2, mask, histSize)); + // metrics.push_back(MutualInfo(im1, im2, mask, histSize)); + // metrics.push_back(NormalizedMutualInfo(im1, im2, mask, histSize)); Rcout << " Intersection: " << metrics[0] << std::endl; Rcout << " Bhattacharyya: " << metrics[1] << std::endl; diff --git a/src/metrics.h b/src/metrics.h index 550774b7..a28dbff3 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -25,8 +25,8 @@ bool checkDegenerate(double pts1, double pts2); cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize, cv::Size ssize); -std::vector getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &h, - cv::Size dsize); +std::vector getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, + cv::Mat &h, cv::Mat &mask); // do overall checks on keypoints and images std::vector getKeypointMetrics(std::vector &points1, From 38ff84ace476771babc1aa843b6d34e19b7dbc61 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 13 Jul 2026 21:44:00 +0200 Subject: [PATCH 07/37] implement Alignment stats interface --- R/registration.R | 129 +++++++++--- man/VoltRon-methods.Rd | 8 +- man/vrLayer-methods.Rd | 4 +- man/vrSample-methods.Rd | 8 +- src/automated_registration.cpp | 27 ++- src/manual_registration.cpp | 25 ++- src/matte_mi.cpp | 350 ++++++++++++++++++++++++++++++++- src/matte_mi.h | 9 +- src/metrics.cpp | 58 +++--- src/metrics.h | 4 +- 10 files changed, 535 insertions(+), 87 deletions(-) diff --git a/R/registration.R b/R/registration.R index a07fd876..c4a590f0 100644 --- a/R/registration.R +++ b/R/registration.R @@ -619,12 +619,23 @@ getAlignmentTabPanel <- function(len_images, centre, register_ind) { do.call( tabsetPanel, c( + # alignment tab panel id = 'image_tab_panel_alignment', lapply(register_ind, function(i) { tabPanel( paste0("Ali. ", i, "->", centre), br(), - fluidRow(imageOutput(paste0("plot_alignment", i))) + + tabsetPanel( + id = "inner_tabs", + tabPanel("Matte's MI Map", + imageOutput(paste0("plot_matte_map", i))), + tabPanel("Alignment Stat.", + tableOutput(paste0("alignment_stats", i))), + tabPanel("Matching Keypoints", + imageOutput(paste0("plot_keypoint_match", i))), + + ) ) }) ) @@ -2862,6 +2873,8 @@ getManualRegisteration <- function( { # Register keypoints aligned_image_list <- list() + matte_map_list <- list() + alignment_stats_list <- list() for (i in register_ind) { # Increment the progress bar, and update the detail text. incProgress( @@ -2883,6 +2896,12 @@ getManualRegisteration <- function( # save matches aligned_image_list[[i]] <- results$aligned_image + + # save matte map + matte_map_list[[i]] <- results$matte_map + + # save alignment stats + alignment_stats_list[[i]] <- results$alignment_stats } } ) @@ -2930,7 +2949,35 @@ getManualRegisteration <- function( deleteFile = TRUE ) }) + + # Plot Matte + lapply(register_ind, function(i) { + cur_alignment_image <- matte_map_list[[i]] + output[[paste0("plot_matte_map", i)]] <- renderPlot({ + if (!suppressWarnings(!is.matrix(cur_alignment_image))) { + cur_alignment_image <- + cur_alignment_image[nrow(cur_alignment_image):1,] + ggplot(reshape2::melt(cur_alignment_image), + aes(Var2, Var1, fill= value)) + + ggplot2::geom_tile() + + ggplot2::theme_void() + + ggplot2::coord_fixed(expand = FALSE) + + ggplot2::scale_fill_gradient(low = "#440154FF", + high = "#FDE725FF", + name = "Matte's MI") + } + }) + }) + # Plot Alignment Stats + lapply(register_ind, function(i) { + cur_align_stats <- alignment_stats_list[[i]] + output[[paste0("alignment_stats", i)]] <- renderTable({ + data.frame(Metrics = names(cur_align_stats), + `Stats.` = cur_align_stats) + }) + }) + # Output summary output[["summary"]] <- renderUI({ str1 <- paste0(" Registration Summary:") @@ -3030,10 +3077,14 @@ computeManualPairwiseTransform <- function( # return transformation matrix and images mapping[[kk]] <- reg[[1]] aligned_image <- reg$aligned_image + matte_map <- reg$matte_map + alignment_stats <- reg$alignment_stats } return(list(mapping = mapping, - aligned_image = aligned_image)) + aligned_image = aligned_image, + matte_map = matte_map, + alignment_stats = alignment_stats)) } #' getRcppManualRegistration @@ -3119,6 +3170,12 @@ getRcppManualRegistration <- function( reg[[1]] <- list(reg[[1]][[1]], NULL) } + # adjust matte mi map + tmp <- reg[[3]] + tmp[is.na(tmp)] <- 0 + tmp[tmp < 0] <- 0 + reg[[3]] <- tmp + # check for null images aligned_image <- if(ncol(reg[[2]]) == 2){ rownames(reg[[2]]) <- rownames(query_image) @@ -3133,9 +3190,17 @@ getRcppManualRegistration <- function( magick::image_read(reg[[2]]) } + # check for null data + matte_map <- + if (!is.null(reg[[3]])) reg[[3]] else NA + alignment_stats <- + if (!is.null(reg[[4]])) reg[[4]] else NA + return(list( transmat = reg[[1]], - aligned_image = aligned_image + aligned_image = aligned_image, + matte_map = matte_map, + alignment_stats = alignment_stats )) } @@ -3196,6 +3261,7 @@ getAutomatedRegisteration <- function( aligned_image_list <- list() alignment_image_list <- list() matte_map_list <- list() + alignment_stats_list <- list() for (i in register_ind) { # Increment the progress bar, and update the detail text. incProgress( @@ -3229,6 +3295,9 @@ getAutomatedRegisteration <- function( # save matte map matte_map_list[[i]] <- results$matte_map + + # save alignment stats + alignment_stats_list[[i]] <- results$alignment_stats } } ) @@ -3259,33 +3328,42 @@ getAutomatedRegisteration <- function( }) # Plot Alignment - # lapply(register_ind, function(i) { - # cur_alignment_image <- alignment_image_list[[i]] - # output[[paste0("plot_alignment", i)]] <- renderPlot({ - # if (!suppressWarnings(is.na(cur_alignment_image))) { - # magick::image_ggplot(cur_alignment_image) - # } - # }) - # }) + lapply(register_ind, function(i) { + cur_alignment_image <- alignment_image_list[[i]] + output[[paste0("plot_keypoint_match", i)]] <- renderPlot({ + if (!suppressWarnings(is.na(cur_alignment_image))) { + magick::image_ggplot(cur_alignment_image) + } + }) + }) # Plot Matte lapply(register_ind, function(i) { cur_alignment_image <- matte_map_list[[i]] - output[[paste0("plot_alignment", i)]] <- renderPlot({ + output[[paste0("plot_matte_map", i)]] <- renderPlot({ if (!suppressWarnings(!is.matrix(cur_alignment_image))) { - cur_alignment_image <- + cur_alignment_image <- cur_alignment_image[nrow(cur_alignment_image):1,] - ggplot(reshape2::melt(cur_alignment_image), - aes(Var2, Var1, fill= value)) + - ggplot2::geom_tile() + - ggplot2::theme_void() + - ggplot2::coord_fixed(expand = FALSE) + - ggplot2::scale_fill_gradient(low = "#440154FF", - high = "#FDE725FF", + ggplot(reshape2::melt(cur_alignment_image), + aes(Var2, Var1, fill= value)) + + ggplot2::geom_tile() + + ggplot2::theme_void() + + ggplot2::coord_fixed(expand = FALSE) + + ggplot2::scale_fill_gradient(low = "#440154FF", + high = "#FDE725FF", name = "Matte's MI") } }) }) + + # Plot Alignment Stats + lapply(register_ind, function(i) { + cur_align_stats <- alignment_stats_list[[i]] + output[[paste0("alignment_stats", i)]] <- renderTable({ + data.frame(Metrics = names(cur_align_stats), + `Stats.` = cur_align_stats) + }) + }) # Output summary output[["summary"]] <- renderUI({ @@ -3487,6 +3565,7 @@ computeAutomatedPairwiseTransform <- function( alignment_image <- reg$alignment_image overlay_image <- reg$overlay_image matte_map <- reg$matte_map + alignment_stats <- reg$alignment_stats } return(list( @@ -3495,7 +3574,8 @@ computeAutomatedPairwiseTransform <- function( aligned_image = aligned_image, alignment_image = alignment_image, overlay_image = overlay_image, - matte_map = matte_map + matte_map = matte_map, + alignment_stats = alignment_stats )) } @@ -3578,7 +3658,9 @@ getRcppAutomatedRegistration <- function( if (!is.null(reg[[5]])) magick::image_read(reg[[5]]) else NA matte_map <- if (!is.null(reg[[6]])) reg[[6]] else NA - + alignment_stats <- + if (!is.null(reg[[7]])) reg[[7]] else NA + # return return(list( transmat = reg[[1]], @@ -3586,7 +3668,8 @@ getRcppAutomatedRegistration <- function( aligned_image = aligned_image, alignment_image = alignment_image, overlay_image = overlay_image, - matte_map = matte_map + matte_map = matte_map, + alignment_stats = alignment_stats )) } diff --git a/man/VoltRon-methods.Rd b/man/VoltRon-methods.Rd index 452d01bb..96bdf224 100644 --- a/man/VoltRon-methods.Rd +++ b/man/VoltRon-methods.Rd @@ -8,10 +8,10 @@ \alias{.DollarNames.VoltRon} \alias{[[,VoltRon,character,missing-method} \alias{[[,VoltRon-methods} -\alias{[[<-,VoltRon,character,missing,ANY-method} +\alias{[[<-,VoltRon,character,missing-method} \alias{[[<-,VoltRon-methods} \alias{[[,VoltRon,character,character-method} -\alias{[[<-,VoltRon,character,character,ANY-method} +\alias{[[<-,VoltRon,character,character-method} \title{Methods for VoltRon} \usage{ \method{$}{VoltRon}(x, i, ...) @@ -22,11 +22,11 @@ \S4method{[[}{VoltRon,character,missing}(x, i, j, ...) -\S4method{[[}{VoltRon,character,missing,ANY}(x, i, j, ...) <- value +\S4method{[[}{VoltRon,character,missing}(x, i, j, ...) <- value \S4method{[[}{VoltRon,character,character}(x, i, j, ...) -\S4method{[[}{VoltRon,character,character,ANY}(x, i, j, ...) <- value +\S4method{[[}{VoltRon,character,character}(x, i, j, ...) <- value } \arguments{ \item{x}{A VoltRon object} diff --git a/man/vrLayer-methods.Rd b/man/vrLayer-methods.Rd index 11bdbf9b..90baad9d 100644 --- a/man/vrLayer-methods.Rd +++ b/man/vrLayer-methods.Rd @@ -3,12 +3,12 @@ \name{vrLayer-methods} \alias{vrLayer-methods} \alias{[[,vrLayer,character,ANY-method} -\alias{[[<-,vrLayer,character,ANY,ANY-method} +\alias{[[<-,vrLayer,character,ANY-method} \title{Methods for vrLayer objects} \usage{ \S4method{[[}{vrLayer,character,ANY}(x, i) -\S4method{[[}{vrLayer,character,ANY,ANY}(x, i) <- value +\S4method{[[}{vrLayer,character,ANY}(x, i) <- value } \arguments{ \item{x}{A vrLayer object} diff --git a/man/vrSample-methods.Rd b/man/vrSample-methods.Rd index f9bba972..7ffb9b88 100644 --- a/man/vrSample-methods.Rd +++ b/man/vrSample-methods.Rd @@ -3,18 +3,18 @@ \name{vrSample-methods} \alias{vrSample-methods} \alias{[[,vrSample,character,ANY-method} -\alias{[[<-,vrSample,character,ANY,ANY-method} +\alias{[[<-,vrSample,character,ANY-method} \alias{[[,vrBlock,character,ANY-method} -\alias{[[<-,vrBlock,character,ANY,ANY-method} +\alias{[[<-,vrBlock,character,ANY-method} \title{Methods for vrSample objects} \usage{ \S4method{[[}{vrSample,character,ANY}(x, i) -\S4method{[[}{vrSample,character,ANY,ANY}(x, i) <- value +\S4method{[[}{vrSample,character,ANY}(x, i) <- value \S4method{[[}{vrBlock,character,ANY}(x, i) -\S4method{[[}{vrBlock,character,ANY,ANY}(x, i) <- value +\S4method{[[}{vrBlock,character,ANY}(x, i) <- value } \arguments{ \item{x}{A vrSample object} diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 761fb04c..c3455c3e 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -571,7 +571,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, const char* flipflop_query, const char* flipflop_ref, const char* rotate_query, const char* rotate_ref, const bool run_Affine, const bool run_TPS, - Mat1d &accuracyMatte) + Mat1d &accuracyMatte, + std::unordered_map &accuracy) { // parameters @@ -636,16 +637,26 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } // get keypoint metrics - std::vector keypoint_metrics; + std::unordered_map keypoint_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); // get alignment metrics - std::vector image_metrics; + std::unordered_map image_metrics; cv::Mat alignmentMask = generateOverlapMask(im1Proc, h, im2Proc.size(), im1.size()); image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); + + // combine metrics + std::unordered_map temp_map(keypoint_metrics); + temp_map.insert(image_metrics.begin(), image_metrics.end()); + accuracy = temp_map; + // + // accuracy.reserve(keypoint_metrics.size() + image_metrics.size()); + // accuracy.insert(accuracy.end(), keypoint_metrics.begin(), keypoint_metrics.end()); + // accuracy.insert(accuracy.end(), image_metrics.begin(), image_metrics.end()); + // Rcout << accuracy.size() << std::endl; // get matte metric - accuracyMatte = chunkedMatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); // imwrite("img1.tif", im1Proc); // imwrite("img2.tif", im2Proc); @@ -749,11 +760,12 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid) { // Return data - Rcpp::List out(6); + Rcpp::List out(7); Rcpp::List out_trans(2); Rcpp::List keypoints(2); Mat imOverlay, imReg, h, imMatches; Mat1d accuracyMatte; + std::unordered_map accuracy; // Read reference image cv::Mat imReference = imageToMat(ref_image, width1, height1); @@ -775,7 +787,8 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R flipflop_query.get_cstring(), flipflop_ref.get_cstring(), rotate_query.get_cstring(), rotate_ref.get_cstring(), run_Affine, run_TPS, - accuracyMatte); + accuracyMatte, + accuracy); // transformation matrix, can be either a matrix, set of keypoints or both out_trans[0] = matToNumericMatrix(h.clone()); @@ -792,11 +805,13 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R out[3] = matToImage(imMatches); // keypoint matching image out[4] = matToImage(imOverlay); // overlay image out[5] = matToNumericMatrix(accuracyMatte); // Matte MI metric + out[6] = accuracy; // accuracy scores } else { out[2] = R_NilValue; out[3] = R_NilValue; out[4] = R_NilValue; out[5] = R_NilValue; + out[6] = R_NilValue; } // release diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index bbf71809..b477a9e9 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -7,6 +7,7 @@ // Library #include "auxiliary.h" #include "metrics.h" +#include "matte_mi.h" // Namespaces using namespace Rcpp; @@ -98,7 +99,9 @@ void alignImagesTPS_points(Rcpp::NumericMatrix &query_data, // align images with FLANN algorithm void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &keypoints, Rcpp::NumericMatrix query_landmark, Rcpp::NumericMatrix reference_landmark, - const bool run_Affine, const bool run_TPS) + const bool run_Affine, const bool run_TPS, + Mat1d &accuracyMatte, + std::unordered_map &accuracy) { // seed cv::setRNGSeed(0); @@ -132,15 +135,13 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // imwrite("img1.tif", im1Affine); // imwrite("img2.tif", im2); - // // get metrics - // std::vector image_metrics; - // image_metrics = getAlignmentMetrics(im1Affine, im2, h, im1.size()); - // get alignment metrics - std::vector image_metrics; cv::Mat alignmentMask = generateOverlapMask(im1Affine, h, im2.size(), im1Affine.size()); - image_metrics = getAlignmentMetrics(im1Affine, im2, h, alignmentMask); + accuracy = getAlignmentMetrics(im1Affine, im2, h, alignmentMask); + + // get matte metric + accuracyMatte = MatteMIMap(im2, im1Affine, alignmentMask, 50); if(!run_TPS){ @@ -251,10 +252,12 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::String nonrigid) { // Return data - Rcpp::List out(2); + Rcpp::List out(4); Rcpp::List out_trans(2); Rcpp::List keypoints(2); Mat imReg, h; + Mat1d accuracyMatte; + std::unordered_map accuracy; // get params const bool run_TPS = (strcmp(method.get_cstring(), "Homography + Non-Rigid") == 0 || @@ -272,7 +275,9 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, alignImagesAffineTPS(im, imReference, imReg, h, keypoints, query_landmark, reference_landmark, - run_Affine, run_TPS); + run_Affine, run_TPS, + accuracyMatte, + accuracy); } // Non-rigid (TPS) only @@ -289,6 +294,8 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, // registered image if exists out[1] = matToImage(imReg.clone()); + out[2] = matToNumericMatrix(accuracyMatte); // Matte MI metric + out[3] = accuracy; return out; } diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index 7008e514..ea402be1 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -425,10 +425,11 @@ void validateChunkedMatteMIInputs( } // ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, -cv::Mat1d chunkedMatteMIMap(const cv::Mat& fixed, - const cv::Mat& moving, - const cv::Mat& mask, - int bins = 50) { +cv::Mat1d MatteMIMap(const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins = 50) { + ChunkSize chunkSize = ChunkSize{}; // validate chunks and return validation map of pixels @@ -534,11 +535,13 @@ cv::Mat1d chunkedMatteMIMap(const cv::Mat& fixed, linearPercentileFromSorted(movingGlobalValues, upperPercentile) }; + Rcout << fixedRange.min << " " << fixedRange.max << endl; if (!isValidRange(fixedRange)) { throw std::invalid_argument( "Invalid fixed intensity range."); } + Rcout << movingRange.min << " " << movingRange.max << endl; if (!isValidRange(movingRange)) { throw std::invalid_argument( "Invalid moving intensity range."); @@ -621,4 +624,343 @@ cv::Mat1d chunkedMatteMIMap(const cv::Mat& fixed, } return NmiMap; +} + +const double* getRowAsDouble( + const cv::Mat& image, + int row, + cv::Mat& scratch) { + + if (image.depth() == CV_64F) { + return image.ptr(row); + } + + image.row(row).convertTo(scratch, CV_64F); + return scratch.ptr(0); +} + +double MatteMI( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins) { + + const double nan = + std::numeric_limits::quiet_NaN(); + + if (bins < 4U) { + throw std::invalid_argument( + "bins must be >= 4 for cubic B-spline smoothing."); + } + + const std::size_t binCount = + static_cast(bins); + + if (binCount > + std::numeric_limits::max() / binCount) { + throw std::length_error( + "Histogram dimensions are too large."); + } + + /* + * Convert the mask to a conventional CV_8U binary mask. + * + * Every nonzero mask value becomes 255. + */ + cv::Mat1b mask8; + + if (!mask.empty()) { + cv::compare( + mask, + cv::Scalar::all(0), + mask8, + cv::CMP_NE); + } + + const int height = fixed.rows; + const int width = fixed.cols; + + /* + * Only row-sized double buffers are needed. This avoids converting + * both complete images to CV_64F and avoids full-image value vectors. + */ + cv::Mat fixedRowScratch; + cv::Mat movingRowScratch; + + /* + * Utility that visits every valid pixel pair. + * + * This function performs no sampling. Every valid pair is delivered + * to the supplied visitor. + */ + auto visitValidPairs = [&](auto&& visitor) { + for (int y = 0; y < height; ++y) { + const double* fixedRow = + getRowAsDouble( + fixed, + y, + fixedRowScratch); + + const double* movingRow = + getRowAsDouble( + moving, + y, + movingRowScratch); + + const unsigned char* maskRow = + mask.empty() + ? nullptr + : mask8.ptr(y); + + for (int x = 0; x < width; ++x) { + if (maskRow != nullptr && + maskRow[x] == 0U) { + continue; + } + + const double fixedValue = fixedRow[x]; + const double movingValue = movingRow[x]; + + if (!std::isfinite(fixedValue) || + !std::isfinite(movingValue)) { + continue; + } + + visitor(fixedValue, movingValue); + } + } + }; + + /* + * First pass: + * determine full-image intensity ranges from every valid pixel pair. + * + * This matches the default behavior of your original Python + * _mattes_mi_from_values function when no explicit ranges are supplied. + */ + std::size_t validCount = 0U; + + double fixedMin = + std::numeric_limits::infinity(); + + double fixedMax = + -std::numeric_limits::infinity(); + + double movingMin = + std::numeric_limits::infinity(); + + double movingMax = + -std::numeric_limits::infinity(); + + visitValidPairs( + [&](double fixedValue, double movingValue) { + ++validCount; + + fixedMin = + std::min(fixedMin, fixedValue); + + fixedMax = + std::max(fixedMax, fixedValue); + + movingMin = + std::min(movingMin, movingValue); + + movingMax = + std::max(movingMax, movingValue); + }); + + if (validCount < 2U) { + return nan; + } + + const IntensityRange fixedRange{ + fixedMin, + fixedMax + }; + + const IntensityRange movingRange{ + movingMin, + movingMax + }; + + /* + * This follows the behavior of the supplied Python function: + * a constant fixed or moving image has an invalid range and returns NaN. + */ + if (!isValidRange(fixedRange) || + !isValidRange(movingRange)) { + return nan; + } + + /* + * Joint histogram: + * + * rows = fixed-image bins + * columns = moving-image bins + */ + std::vector jointHistogram( + binCount * binCount, + 0.0); + + /* + * Second pass: + * add every valid pixel pair to the Mattes joint histogram. + */ + visitValidPairs( + [&](double fixedValue, double movingValue) { + /* + * Fixed image: + * nearest-bin assignment over [0, bins - 1]. + */ + const double fixedPosition = + scaleToBinPosition( + fixedValue, + fixedRange, + 0.0, + static_cast(binCount - 1U)); + + std::size_t fixedBin = + roundToNearestEvenNonnegative( + fixedPosition); + + fixedBin = + std::min( + fixedBin, + binCount - 1U); + + /* + * Moving image: + * continuous coordinate over [1, bins - 2]. + * + * The one-bin margin leaves room for the cubic B-spline + * support at both histogram boundaries. + */ + const double movingPosition = + scaleToBinPosition( + movingValue, + movingRange, + 1.0, + static_cast(binCount - 2U)); + + const std::ptrdiff_t baseBin = + static_cast( + std::floor(movingPosition)); + + /* + * Cubic B-spline support covers at most four bins. + */ + for (int offset = -1; + offset <= 2; + ++offset) { + + const std::ptrdiff_t movingBin = + baseBin + + static_cast(offset); + + if (movingBin < 0 || + movingBin >= + static_cast( + binCount)) { + continue; + } + + const double weight = + cubicBSpline( + movingPosition - + static_cast(movingBin)); + + if (weight <= 0.0) { + continue; + } + + const std::size_t histogramIndex = + fixedBin * binCount + + static_cast( + movingBin); + + jointHistogram[histogramIndex] += + weight; + } + }); + + const double total = + std::accumulate( + jointHistogram.begin(), + jointHistogram.end(), + 0.0); + + if (!(total > 0.0) || + !std::isfinite(total)) { + return nan; + } + + /* + * Marginal probability distributions. + */ + std::vector px( + binCount, + 0.0); + + std::vector py( + binCount, + 0.0); + + for (std::size_t fixedBin = 0; + fixedBin < binCount; + ++fixedBin) { + + for (std::size_t movingBin = 0; + movingBin < binCount; + ++movingBin) { + + const std::size_t index = + fixedBin * binCount + + movingBin; + + const double pxy = + jointHistogram[index] / total; + + px[fixedBin] += pxy; + py[movingBin] += pxy; + } + } + + /* + * MI = sum p(x,y) log(p(x,y) / (p(x)p(y))) + */ + double mi = 0.0; + + for (std::size_t fixedBin = 0; + fixedBin < binCount; + ++fixedBin) { + + for (std::size_t movingBin = 0; + movingBin < binCount; + ++movingBin) { + + const std::size_t index = + fixedBin * binCount + + movingBin; + + const double pxy = + jointHistogram[index] / total; + + const double productOfMarginals = + px[fixedBin] * + py[movingBin]; + + if (pxy > 0.0 && + productOfMarginals > 0.0) { + + mi += + pxy * + std::log( + pxy / + productOfMarginals); + } + } + } + + // Natural logarithm: the result is in nats. + return mi; } \ No newline at end of file diff --git a/src/matte_mi.h b/src/matte_mi.h index b2ea9333..1618484b 100644 --- a/src/matte_mi.h +++ b/src/matte_mi.h @@ -25,9 +25,10 @@ double mattesMiFromValues(const double* fixedValues, std::optional fixedRange = std::nullopt, std::optional movingRange = std::nullopt); -cv::Mat1d chunkedMatteMIMap(const cv::Mat& fixed, - const cv::Mat& moving, - const cv::Mat& mask, - int bins); +cv::Mat1d MatteMIMap(const cv::Mat& fixed, const cv::Mat& moving, + const cv::Mat& mask, int bins); + +double MatteMI(const cv::Mat& fixed, const cv::Mat& moving, + const cv::Mat& mask, int bins); #endif \ No newline at end of file diff --git a/src/metrics.cpp b/src/metrics.cpp index d7723809..d4da0a9b 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -260,19 +260,16 @@ double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, return (ent1+ent2)/ent12; } -std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ +std::unordered_map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ - // Histogram settings + // Metrics + std::unordered_map metrics; + + // Compute histograms int histSize = 256; float range[] = {0.0, 256.0}; const float* histRange = {range}; int channels[] = {0}; - - // get overlap mask - // cv::Mat mask = generateOverlapMask(im1, h, im2.size(), ssize); - // imwrite("mask.tif", mask); - - // Compute histograms cv::Mat hist1, hist2; cv::calcHist(&im1, 1, channels, mask, hist1, 1, &histSize, &histRange); @@ -284,38 +281,39 @@ std::vector getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX); // Summary - Rcout << "Alignment Report: " << endl; - std::vector metrics; - metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT)); - metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA)); + Rcout << "Alignment Accuracy (Course): " << endl; + metrics["Intersection"] = cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT); + metrics["Bhattacharyya"] = cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA); + metrics["Matte's MI"] = MatteMI(im2, im1, mask, 50); + + Rcout << " Intersection: " << metrics["Intersection"] << std::endl; + Rcout << " Bhattacharyya: " << metrics["Bhattacharyya"] << std::endl; + Rcout << " Matte's MI: " << metrics["Matte's MI"] << std::endl; + + // old metrics, keep for comparison //metrics.push_back(cv::compareHist(hist1, hist2, cv::HISTCMP_CHISQR)); // metrics.push_back(jointEntropy(im1, im2, mask, histSize)); // metrics.push_back(MutualInfo(im1, im2, mask, histSize)); // metrics.push_back(NormalizedMutualInfo(im1, im2, mask, histSize)); - Rcout << " Intersection: " << metrics[0] << std::endl; - Rcout << " Bhattacharyya: " << metrics[1] << std::endl; - // Rcout << " Chi-Square: " << metrics[0] << std::endl; - // Rcout << " Joint Entropy: " << metrics[3] << std::endl; - // Rcout << " MutualInfo: " << metrics[4] << std::endl; - // Rcout << " NormalizedMutualInfo: " << metrics[5] << std::endl; return metrics; } // do overall checks on keypoints and images -std::vector getKeypointMetrics(std::vector &points1, +std::unordered_map getKeypointMetrics(std::vector &points1, std::vector &points2, Mat &im1, Mat &im2, Mat &h, Mat &mask) { // metrics list - std::vector metrics_list; + std::unordered_map metrics; // Alignment report Rcout << "Keypoint Report: " << endl; // Report final keypoints Rcout << " Calculated transformation matrix with " << points1.size() << " keypoints" << endl; + metrics["#Keypoints"] = points1.size(); // points stand. dev. double points1_sd = cppSD(points1); @@ -324,14 +322,17 @@ std::vector getKeypointMetrics(std::vector &points1, Rcout << " WARNING: points may be in a degenerate configuration." << endl; } Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; - metrics_list.push_back(checkDegenerate(points1_sd, points2_sd)); - metrics_list.push_back(points1_sd); - metrics_list.push_back(points2_sd); + metrics["SD Keypoints (ref.)"] = points1_sd; + metrics["SD Keypoints (query)"] = points2_sd; + + // degenerate ? + bool degenerate = checkDegenerate(points1_sd, points2_sd); + Rcout << "Registration is " << (degenerate ? "degenerate!" : "not degenerate!") << endl; // check distribution of points double stddev = checkMappedGridDistribution(im2, h); Rcout << " Std dev of registered points: " << stddev << endl; - metrics_list.push_back(stddev); + metrics["SD Grid points"] = stddev; // warp keypoints and compare double md = medianMappingDistance(points1, points2, h); @@ -339,14 +340,13 @@ std::vector getKeypointMetrics(std::vector &points1, if(md > 3){ Rcout << " WARNING: Transformation may be poor - mean euclidean distance of mapped source and destination key points is high!" << endl; } + metrics["Median distance"] = md; // get inlier percentages double ratio = checkInlierPercentage(mask); Rcout << " Inlier Percentage: " << ratio << endl; - - // degenerate ? - Rcout << "Registration is " << (metrics_list[0] ? "degenerate!" : "not degenerate!") << endl; - + metrics["Inlier Ratio"] = ratio; + // return is_degenerate; - return metrics_list; + return metrics; } \ No newline at end of file diff --git a/src/metrics.h b/src/metrics.h index a28dbff3..75ea2ced 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -25,11 +25,11 @@ bool checkDegenerate(double pts1, double pts2); cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize, cv::Size ssize); -std::vector getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, +std::unordered_map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &h, cv::Mat &mask); // do overall checks on keypoints and images -std::vector getKeypointMetrics(std::vector &points1, +std::unordered_map getKeypointMetrics(std::vector &points1, std::vector &points2, cv::Mat &im1, cv::Mat &im2, cv::Mat &h, cv::Mat &mask); From 48e5786b9fde32a5d424fcf330d55faea48d1188 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 15 Jul 2026 15:18:37 +0200 Subject: [PATCH 08/37] replace unordered_map with map --- src/automated_registration.cpp | 29 ++++++++++++++++++++++------- src/manual_registration.cpp | 4 ++-- src/metrics.cpp | 8 ++++---- src/metrics.h | 4 ++-- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index c3455c3e..810664ab 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -572,7 +572,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, const char* rotate_query, const char* rotate_ref, const bool run_Affine, const bool run_TPS, Mat1d &accuracyMatte, - std::unordered_map &accuracy) + std::map &accuracy) { // parameters @@ -637,18 +637,33 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } // get keypoint metrics - std::unordered_map keypoint_metrics; + std::map keypoint_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); // get alignment metrics - std::unordered_map image_metrics; + std::map image_metrics; cv::Mat alignmentMask = generateOverlapMask(im1Proc, h, im2Proc.size(), im1.size()); image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); // combine metrics - std::unordered_map temp_map(keypoint_metrics); - temp_map.insert(image_metrics.begin(), image_metrics.end()); - accuracy = temp_map; + // std::map temp_map = keypoint_metrics; + // temp_map.insert(image_metrics.begin(), image_metrics.end()); + // accuracy = temp_map; + + // combine metrics + std::vector> temp_map; + temp_map.reserve(keypoint_metrics.size() + image_metrics.size()); + std::copy(keypoint_metrics.begin(), keypoint_metrics.end(), std::back_inserter(temp_map)); + std::copy(image_metrics.begin(), image_metrics.end(), std::back_inserter(temp_map)); + std::map final_map(temp_map.begin(), temp_map.end()); + accuracy = final_map; + + // std::vector> temp_map; + // temp_map.reserve(keypoint_metrics.size() + image_metrics.size()); + // temp_map.insert(temp_map.end(), keypoint_metrics.begin(), keypoint_metrics.end()); + // temp_map.insert(temp_map.end(), image_metrics.begin(), image_metrics.end()); + // std::map accuracy(temp_map.begin(), temp_map.end()); + // // accuracy.reserve(keypoint_metrics.size() + image_metrics.size()); // accuracy.insert(accuracy.end(), keypoint_metrics.begin(), keypoint_metrics.end()); @@ -765,7 +780,7 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R Rcpp::List keypoints(2); Mat imOverlay, imReg, h, imMatches; Mat1d accuracyMatte; - std::unordered_map accuracy; + std::map accuracy; // Read reference image cv::Mat imReference = imageToMat(ref_image, width1, height1); diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index b477a9e9..d8a676c9 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -101,7 +101,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k Rcpp::NumericMatrix query_landmark, Rcpp::NumericMatrix reference_landmark, const bool run_Affine, const bool run_TPS, Mat1d &accuracyMatte, - std::unordered_map &accuracy) + std::map &accuracy) { // seed cv::setRNGSeed(0); @@ -257,7 +257,7 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::List keypoints(2); Mat imReg, h; Mat1d accuracyMatte; - std::unordered_map accuracy; + std::map accuracy; // get params const bool run_TPS = (strcmp(method.get_cstring(), "Homography + Non-Rigid") == 0 || diff --git a/src/metrics.cpp b/src/metrics.cpp index d4da0a9b..8e8d6045 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -260,10 +260,10 @@ double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, return (ent1+ent2)/ent12; } -std::unordered_map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ +std::map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ // Metrics - std::unordered_map metrics; + std::map metrics; // Compute histograms int histSize = 256; @@ -300,13 +300,13 @@ std::unordered_map getAlignmentMetrics(Mat &im1, Mat &im2, } // do overall checks on keypoints and images -std::unordered_map getKeypointMetrics(std::vector &points1, +std::map getKeypointMetrics(std::vector &points1, std::vector &points2, Mat &im1, Mat &im2, Mat &h, Mat &mask) { // metrics list - std::unordered_map metrics; + std::map metrics; // Alignment report Rcout << "Keypoint Report: " << endl; diff --git a/src/metrics.h b/src/metrics.h index 75ea2ced..bbbab037 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -25,11 +25,11 @@ bool checkDegenerate(double pts1, double pts2); cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, cv::Size dsize, cv::Size ssize); -std::unordered_map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, +std::map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &h, cv::Mat &mask); // do overall checks on keypoints and images -std::unordered_map getKeypointMetrics(std::vector &points1, +std::map getKeypointMetrics(std::vector &points1, std::vector &points2, cv::Mat &im1, cv::Mat &im2, cv::Mat &h, cv::Mat &mask); From 03b851f72e24783e523e2775c5610d3bd7ec789d Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 16 Jul 2026 00:41:55 +0200 Subject: [PATCH 09/37] update manual alignment metrics --- R/RcppExports.R | 4 ++-- R/registration.R | 35 +++++++++++++++++++++++++++++--- man/VoltRon-methods.Rd | 8 ++++---- man/getRcppManualRegistration.Rd | 2 ++ man/vrLayer-methods.Rd | 4 ++-- man/vrSample-methods.Rd | 8 ++++---- src/RcppExports.cpp | 10 +++++---- src/manual_registration.cpp | 19 +++++++++++++---- src/matte_mi.cpp | 2 -- 9 files changed, 67 insertions(+), 25 deletions(-) diff --git a/R/RcppExports.R b/R/RcppExports.R index 8ab45350..2f976552 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -21,8 +21,8 @@ warpImageManual <- function(ref_image, query_image, mapping, width1, height1, wi .Call('_VoltRon_warpImageManual', PACKAGE = 'VoltRon', ref_image, query_image, mapping, width1, height1, width2, height2) } -manual_registeration_rawvector <- function(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, method, nonrigid) { - .Call('_VoltRon_manual_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, method, nonrigid) +manual_registeration_rawvector <- function(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid) { + .Call('_VoltRon_manual_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid) } manual_registeration_matrix <- function(query_data, reference_landmark, query_landmark, method, nonrigid) { diff --git a/R/registration.R b/R/registration.R index c4a590f0..e73431a5 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3045,6 +3045,20 @@ computeManualPairwiseTransform <- function( ref_image, target_landmark, reference_landmark, + invert_query = input[[paste0( + "negate_", + query_label, + "_image", + cur_map[1] + )]] == + "Yes", + invert_ref = input[[paste0( + "negate_", + ref_label, + "_image", + cur_map[2] + )]] == + "Yes", method = input$Method, nonrigid = if(is.null(input$nonrigid)) "None" else input$nonrigid ) @@ -3062,8 +3076,20 @@ computeManualPairwiseTransform <- function( tfx <- getSimpleITKAutomatedRegistration( ref_image = ref_image, query_image = query_image, - invert_query = FALSE, - invert_ref = FALSE, + invert_query = input[[paste0( + "negate_", + query_label, + "_image", + cur_map[1] + )]] == + "Yes", + invert_ref = input[[paste0( + "negate_", + ref_label, + "_image", + cur_map[2] + )]] == + "Yes", flipflop_query = FALSE, flipflop_ref = FALSE, rotate_query = FALSE, @@ -3106,6 +3132,8 @@ getRcppManualRegistration <- function( ref_image, query_landmark, reference_landmark, + invert_query = FALSE, + invert_ref = FALSE, method = "Homography", nonrigid = "TPS (OpenCV)" ) { @@ -3140,7 +3168,6 @@ getRcppManualRegistration <- function( query_landmark[, 2] <- dim(query_image)[3] - query_landmark[, 2] } - reg <- if(ncol(query_image) == 2){ manual_registeration_matrix( @@ -3156,6 +3183,8 @@ getRcppManualRegistration <- function( query_image, reference_landmark = reference_landmark, query_landmark = query_landmark, + invert_query = invert_query, + invert_ref = invert_ref, width1 = dim(ref_image)[2], height1 = dim(ref_image)[3], width2 = dim(query_image)[2], diff --git a/man/VoltRon-methods.Rd b/man/VoltRon-methods.Rd index 96bdf224..452d01bb 100644 --- a/man/VoltRon-methods.Rd +++ b/man/VoltRon-methods.Rd @@ -8,10 +8,10 @@ \alias{.DollarNames.VoltRon} \alias{[[,VoltRon,character,missing-method} \alias{[[,VoltRon-methods} -\alias{[[<-,VoltRon,character,missing-method} +\alias{[[<-,VoltRon,character,missing,ANY-method} \alias{[[<-,VoltRon-methods} \alias{[[,VoltRon,character,character-method} -\alias{[[<-,VoltRon,character,character-method} +\alias{[[<-,VoltRon,character,character,ANY-method} \title{Methods for VoltRon} \usage{ \method{$}{VoltRon}(x, i, ...) @@ -22,11 +22,11 @@ \S4method{[[}{VoltRon,character,missing}(x, i, j, ...) -\S4method{[[}{VoltRon,character,missing}(x, i, j, ...) <- value +\S4method{[[}{VoltRon,character,missing,ANY}(x, i, j, ...) <- value \S4method{[[}{VoltRon,character,character}(x, i, j, ...) -\S4method{[[}{VoltRon,character,character}(x, i, j, ...) <- value +\S4method{[[}{VoltRon,character,character,ANY}(x, i, j, ...) <- value } \arguments{ \item{x}{A VoltRon object} diff --git a/man/getRcppManualRegistration.Rd b/man/getRcppManualRegistration.Rd index 9cb3fc8a..763e601f 100644 --- a/man/getRcppManualRegistration.Rd +++ b/man/getRcppManualRegistration.Rd @@ -9,6 +9,8 @@ getRcppManualRegistration( ref_image, query_landmark, reference_landmark, + invert_query = FALSE, + invert_ref = FALSE, method = "Homography", nonrigid = "TPS (OpenCV)" ) diff --git a/man/vrLayer-methods.Rd b/man/vrLayer-methods.Rd index 90baad9d..11bdbf9b 100644 --- a/man/vrLayer-methods.Rd +++ b/man/vrLayer-methods.Rd @@ -3,12 +3,12 @@ \name{vrLayer-methods} \alias{vrLayer-methods} \alias{[[,vrLayer,character,ANY-method} -\alias{[[<-,vrLayer,character,ANY-method} +\alias{[[<-,vrLayer,character,ANY,ANY-method} \title{Methods for vrLayer objects} \usage{ \S4method{[[}{vrLayer,character,ANY}(x, i) -\S4method{[[}{vrLayer,character,ANY}(x, i) <- value +\S4method{[[}{vrLayer,character,ANY,ANY}(x, i) <- value } \arguments{ \item{x}{A vrLayer object} diff --git a/man/vrSample-methods.Rd b/man/vrSample-methods.Rd index 7ffb9b88..f9bba972 100644 --- a/man/vrSample-methods.Rd +++ b/man/vrSample-methods.Rd @@ -3,18 +3,18 @@ \name{vrSample-methods} \alias{vrSample-methods} \alias{[[,vrSample,character,ANY-method} -\alias{[[<-,vrSample,character,ANY-method} +\alias{[[<-,vrSample,character,ANY,ANY-method} \alias{[[,vrBlock,character,ANY-method} -\alias{[[<-,vrBlock,character,ANY-method} +\alias{[[<-,vrBlock,character,ANY,ANY-method} \title{Methods for vrSample objects} \usage{ \S4method{[[}{vrSample,character,ANY}(x, i) -\S4method{[[}{vrSample,character,ANY}(x, i) <- value +\S4method{[[}{vrSample,character,ANY,ANY}(x, i) <- value \S4method{[[}{vrBlock,character,ANY}(x, i) -\S4method{[[}{vrBlock,character,ANY}(x, i) <- value +\S4method{[[}{vrBlock,character,ANY,ANY}(x, i) <- value } \arguments{ \item{x}{A vrSample object} diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 387e6cdb..a4d3c176 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -101,8 +101,8 @@ BEGIN_RCPP END_RCPP } // manual_registeration_rawvector -Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, Rcpp::NumericMatrix reference_landmark, Rcpp::NumericMatrix query_landmark, const int width1, const int height1, const int width2, const int height2, Rcpp::String method, Rcpp::String nonrigid); -RcppExport SEXP _VoltRon_manual_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP reference_landmarkSEXP, SEXP query_landmarkSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP methodSEXP, SEXP nonrigidSEXP) { +Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, Rcpp::NumericMatrix reference_landmark, Rcpp::NumericMatrix query_landmark, const int width1, const int height1, const int width2, const int height2, const bool invert_query, const bool invert_ref, Rcpp::String method, Rcpp::String nonrigid); +RcppExport SEXP _VoltRon_manual_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP reference_landmarkSEXP, SEXP query_landmarkSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP invert_querySEXP, SEXP invert_refSEXP, SEXP methodSEXP, SEXP nonrigidSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -114,9 +114,11 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const int >::type height1(height1SEXP); Rcpp::traits::input_parameter< const int >::type width2(width2SEXP); Rcpp::traits::input_parameter< const int >::type height2(height2SEXP); + Rcpp::traits::input_parameter< const bool >::type invert_query(invert_querySEXP); + Rcpp::traits::input_parameter< const bool >::type invert_ref(invert_refSEXP); Rcpp::traits::input_parameter< Rcpp::String >::type method(methodSEXP); Rcpp::traits::input_parameter< Rcpp::String >::type nonrigid(nonrigidSEXP); - rcpp_result_gen = Rcpp::wrap(manual_registeration_rawvector(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, method, nonrigid)); + rcpp_result_gen = Rcpp::wrap(manual_registeration_rawvector(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid)); return rcpp_result_gen; END_RCPP } @@ -187,7 +189,7 @@ static const R_CallMethodDef CallEntries[] = { {"_VoltRon_warpRcppImage", (DL_FUNC) &_VoltRon_warpRcppImage, 7}, {"_VoltRon_warpImageAuto", (DL_FUNC) &_VoltRon_warpImageAuto, 7}, {"_VoltRon_warpImageManual", (DL_FUNC) &_VoltRon_warpImageManual, 7}, - {"_VoltRon_manual_registeration_rawvector", (DL_FUNC) &_VoltRon_manual_registeration_rawvector, 10}, + {"_VoltRon_manual_registeration_rawvector", (DL_FUNC) &_VoltRon_manual_registeration_rawvector, 12}, {"_VoltRon_manual_registeration_matrix", (DL_FUNC) &_VoltRon_manual_registeration_matrix, 5}, {"_VoltRon_applyRcppMapping", (DL_FUNC) &_VoltRon_applyRcppMapping, 2}, {"_VoltRon_build_snn_rank", (DL_FUNC) &_VoltRon_build_snn_rank, 1}, diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index d8a676c9..0d1a939b 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -6,6 +6,7 @@ // Library #include "auxiliary.h" +#include "image.h" #include "metrics.h" #include "matte_mi.h" @@ -99,6 +100,7 @@ void alignImagesTPS_points(Rcpp::NumericMatrix &query_data, // align images with FLANN algorithm void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &keypoints, Rcpp::NumericMatrix query_landmark, Rcpp::NumericMatrix reference_landmark, + const bool invert_query, const bool invert_ref, const bool run_Affine, const bool run_TPS, Mat1d &accuracyMatte, std::map &accuracy) @@ -138,10 +140,15 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // get alignment metrics cv::Mat alignmentMask = generateOverlapMask(im1Affine, h, im2.size(), im1Affine.size()); - accuracy = getAlignmentMetrics(im1Affine, im2, h, alignmentMask); - - // get matte metric - accuracyMatte = MatteMIMap(im2, im1Affine, alignmentMask, 50); + + // get matte metric, process + Mat im1Proc, im2Proc; + cvtColor(im1Affine, im1Proc, cv::COLOR_BGR2GRAY); + cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); + im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); + im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); if(!run_TPS){ @@ -248,6 +255,8 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, const int height1, const int width2, const int height2, + const bool invert_query, + const bool invert_ref, Rcpp::String method, Rcpp::String nonrigid) { @@ -275,6 +284,8 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, alignImagesAffineTPS(im, imReference, imReg, h, keypoints, query_landmark, reference_landmark, + invert_query, + invert_ref, run_Affine, run_TPS, accuracyMatte, accuracy); diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index ea402be1..bad678d8 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -535,13 +535,11 @@ cv::Mat1d MatteMIMap(const cv::Mat& fixed, linearPercentileFromSorted(movingGlobalValues, upperPercentile) }; - Rcout << fixedRange.min << " " << fixedRange.max << endl; if (!isValidRange(fixedRange)) { throw std::invalid_argument( "Invalid fixed intensity range."); } - Rcout << movingRange.min << " " << movingRange.max << endl; if (!isValidRange(movingRange)) { throw std::invalid_argument( "Invalid moving intensity range."); From 09a3cc69f29f31bf0c8dd9572c8ea2f1e33c92ea Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 16 Jul 2026 01:19:33 +0200 Subject: [PATCH 10/37] more updates --- R/auxiliary.R | 54 ++++++++++++++++++++++++++++++++---------------- R/registration.R | 30 ++++++++++++++++++++++----- src/metrics.cpp | 16 +++++++------- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/R/auxiliary.R b/R/auxiliary.R index 71ee0a62..4bb8a124 100644 --- a/R/auxiliary.R +++ b/R/auxiliary.R @@ -58,6 +58,42 @@ fixVoltRon <- function(object) { object } +#### +# Bioformats extensions for image pyramids #### +#### + +.PYRAMID_FORMATS <- c( + ".ome.tiff", # OME-TIFF pyramidal TIFF variants (OME-TIFF and related) + ".ome.tif", + ".ome.tf2", + ".ome.tf8", + ".ome.btf", + ".svs", # Aperio SVS + ".afi", # Aperio AFI + ".ndpi", # Hamamatsu NDPI + ".ndpis", # Hamamatsu NDPI variants + ".qptiff" # CODEX ? +) + +#### +# Alignment Metrics #### +#### + +.ALIGNMENT_ACCURACY_METRICS <- c( + "Intersection", + "Bhattacharyya", + "Matte's MI" +) + +.ALIGNMENT_KEYPOINT_METRICS <- c( + "#Keypoints", + "Inlier Ratio", + "Std. dev. (ref. keypoints)", + "Std. dev. (query keypoints)", + "Std. dev. (grid points)", + "Median distance" +) + #### # Matrix Operations #### #### @@ -326,24 +362,6 @@ getBasilisk <- function() { py_env } -#### -# Bioformats extensions for pyramids #### -#### - -.PYRAMID_FORMATS <- c( - ".ome.tiff", # OME-TIFF pyramidal TIFF variants (OME-TIFF and related) - ".ome.tif", - ".ome.tf2", - ".ome.tf8", - ".ome.btf", - ".svs", # Aperio SVS - ".afi", # Aperio AFI - ".ndpi", # Hamamatsu NDPI - ".ndpis", # Hamamatsu NDPI variants - ".qptiff" # CODEX ? -) - - #### # Other Auxiliary tools #### #### diff --git a/R/registration.R b/R/registration.R index e73431a5..aa252d96 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3222,8 +3222,18 @@ getRcppManualRegistration <- function( # check for null data matte_map <- if (!is.null(reg[[3]])) reg[[3]] else NA - alignment_stats <- - if (!is.null(reg[[4]])) reg[[4]] else NA + metrics <- .ALIGNMENT_ACCURACY_METRICS + alignment_stats <- { + if (!is.null(reg[[4]])){ + if(!all(names(reg[[4]]) %in% metrics)){ + stop("There are missing accuracy metrics!") + } else { + reg[[4]][metrics] + } + } else{ + NA + } + } return(list( transmat = reg[[1]], @@ -3687,9 +3697,19 @@ getRcppAutomatedRegistration <- function( if (!is.null(reg[[5]])) magick::image_read(reg[[5]]) else NA matte_map <- if (!is.null(reg[[6]])) reg[[6]] else NA - alignment_stats <- - if (!is.null(reg[[7]])) reg[[7]] else NA - + metrics <- c(.ALIGNMENT_ACCURACY_METRICS, .ALIGNMENT_KEYPOINT_METRICS) + alignment_stats <- { + if (!is.null(reg[[7]])){ + if(!all(names(reg[[7]]) %in% metrics)){ + stop("There are missing accuracy metrics!") + } else { + reg[[7]][metrics] + } + } else{ + NA + } + } + # return return(list( transmat = reg[[1]], diff --git a/src/metrics.cpp b/src/metrics.cpp index 8e8d6045..9f887a7e 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -315,6 +315,11 @@ std::map getKeypointMetrics(std::vector &point Rcout << " Calculated transformation matrix with " << points1.size() << " keypoints" << endl; metrics["#Keypoints"] = points1.size(); + // get inlier percentages + double ratio = checkInlierPercentage(mask); + Rcout << " Inlier Percentage: " << ratio << endl; + metrics["Inlier Ratio"] = ratio; + // points stand. dev. double points1_sd = cppSD(points1); double points2_sd = cppSD(points2); @@ -322,8 +327,8 @@ std::map getKeypointMetrics(std::vector &point Rcout << " WARNING: points may be in a degenerate configuration." << endl; } Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; - metrics["SD Keypoints (ref.)"] = points1_sd; - metrics["SD Keypoints (query)"] = points2_sd; + metrics["Std. dev. (ref. keypoints)"] = points1_sd; + metrics["Std. dev. (query keypoints)"] = points2_sd; // degenerate ? bool degenerate = checkDegenerate(points1_sd, points2_sd); @@ -332,7 +337,7 @@ std::map getKeypointMetrics(std::vector &point // check distribution of points double stddev = checkMappedGridDistribution(im2, h); Rcout << " Std dev of registered points: " << stddev << endl; - metrics["SD Grid points"] = stddev; + metrics["Std. dev. (grid points)"] = stddev; // warp keypoints and compare double md = medianMappingDistance(points1, points2, h); @@ -341,11 +346,6 @@ std::map getKeypointMetrics(std::vector &point Rcout << " WARNING: Transformation may be poor - mean euclidean distance of mapped source and destination key points is high!" << endl; } metrics["Median distance"] = md; - - // get inlier percentages - double ratio = checkInlierPercentage(mask); - Rcout << " Inlier Percentage: " << ratio << endl; - metrics["Inlier Ratio"] = ratio; // return is_degenerate; return metrics; From 194e272cf5bc1aacb000cbb1b49bc54b8e4f786e Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 16 Jul 2026 17:08:08 +0200 Subject: [PATCH 11/37] clean source, and update no-image alignment --- R/registration.R | 84 +++++++++++++++++++--------------- src/automated_registration.cpp | 7 +-- src/manual_registration.cpp | 4 -- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/R/registration.R b/R/registration.R index aa252d96..5ef06101 100644 --- a/R/registration.R +++ b/R/registration.R @@ -2952,30 +2952,34 @@ getManualRegisteration <- function( # Plot Matte lapply(register_ind, function(i) { - cur_alignment_image <- matte_map_list[[i]] - output[[paste0("plot_matte_map", i)]] <- renderPlot({ - if (!suppressWarnings(!is.matrix(cur_alignment_image))) { - cur_alignment_image <- - cur_alignment_image[nrow(cur_alignment_image):1,] - ggplot(reshape2::melt(cur_alignment_image), - aes(Var2, Var1, fill= value)) + - ggplot2::geom_tile() + - ggplot2::theme_void() + - ggplot2::coord_fixed(expand = FALSE) + - ggplot2::scale_fill_gradient(low = "#440154FF", - high = "#FDE725FF", - name = "Matte's MI") - } - }) + if(length(matte_map_list)){ + cur_alignment_image <- matte_map_list[[i]] + output[[paste0("plot_matte_map", i)]] <- renderPlot({ + if (!suppressWarnings(!is.matrix(cur_alignment_image))) { + cur_alignment_image <- + cur_alignment_image[nrow(cur_alignment_image):1,] + ggplot(reshape2::melt(cur_alignment_image), + aes(Var2, Var1, fill= value)) + + ggplot2::geom_tile() + + ggplot2::theme_void() + + ggplot2::coord_fixed(expand = FALSE) + + ggplot2::scale_fill_gradient(low = "#440154FF", + high = "#FDE725FF", + name = "Matte's MI") + } + }) + } }) # Plot Alignment Stats lapply(register_ind, function(i) { - cur_align_stats <- alignment_stats_list[[i]] - output[[paste0("alignment_stats", i)]] <- renderTable({ - data.frame(Metrics = names(cur_align_stats), - `Stats.` = cur_align_stats) - }) + if(length(alignment_stats_list)){ + cur_align_stats <- alignment_stats_list[[i]] + output[[paste0("alignment_stats", i)]] <- renderTable({ + data.frame(Metrics = names(cur_align_stats), + `Stats.` = cur_align_stats) + }) + } }) # Output summary @@ -3199,12 +3203,6 @@ getRcppManualRegistration <- function( reg[[1]] <- list(reg[[1]][[1]], NULL) } - # adjust matte mi map - tmp <- reg[[3]] - tmp[is.na(tmp)] <- 0 - tmp[tmp < 0] <- 0 - reg[[3]] <- tmp - # check for null images aligned_image <- if(ncol(reg[[2]]) == 2){ rownames(reg[[2]]) <- rownames(query_image) @@ -3220,19 +3218,29 @@ getRcppManualRegistration <- function( } # check for null data - matte_map <- - if (!is.null(reg[[3]])) reg[[3]] else NA - metrics <- .ALIGNMENT_ACCURACY_METRICS - alignment_stats <- { - if (!is.null(reg[[4]])){ - if(!all(names(reg[[4]]) %in% metrics)){ - stop("There are missing accuracy metrics!") - } else { - reg[[4]][metrics] + if(length(reg) > 2){ + matte_map <- + if (!is.null(reg[[3]])) { + tmp <- reg[[3]] + tmp[is.na(tmp)] <- 0 + tmp[tmp < 0] <- 0 + tmp + } else NA + metrics <- .ALIGNMENT_ACCURACY_METRICS + alignment_stats <- { + if (!is.null(reg[[4]])){ + if(!all(names(reg[[4]]) %in% metrics)){ + stop("There are missing accuracy metrics!") + } else { + reg[[4]][metrics] + } + } else{ + NA } - } else{ - NA - } + } + } else { + matte_map <- NULL + alignment_stats <- NULL } return(list( diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 810664ab..129e0935 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -622,8 +622,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } - // imwrite("img1_before.tif", im1Proc); - // Use homography to warp image if(h.rows == 2){ warpAffine(im1Proc, im1Proc, h, im2Proc.size()); @@ -642,7 +640,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get alignment metrics std::map image_metrics; - cv::Mat alignmentMask = generateOverlapMask(im1Proc, h, im2Proc.size(), im1.size()); + cv::Mat alignmentMask = generateOverlapMask(im1Proc, h, im2Proc.size(), im1Proc.size()); image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); // combine metrics @@ -672,9 +670,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get matte metric accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); - - // imwrite("img1.tif", im1Proc); - // imwrite("img2.tif", im2Proc); // Rcout << "DONE: warped query image" << endl; diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 0d1a939b..681ee1ee 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -133,10 +133,6 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cv::perspectiveTransform(query_mat, query_reg, h); } - // TODO: remove later - // imwrite("img1.tif", im1Affine); - // imwrite("img2.tif", im2); - // get alignment metrics cv::Mat alignmentMask = generateOverlapMask(im1Affine, h, im2.size(), im1Affine.size()); From 0401d3ec4372bf6015e71a0dab194c34bd340df9 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 16 Jul 2026 22:02:41 +0200 Subject: [PATCH 12/37] change interface of some functions --- src/automated_registration.cpp | 5 ++-- src/manual_registration.cpp | 48 ++++++++++++++++++++++++++++------ src/metrics.cpp | 30 ++++++++++++++++++--- src/metrics.h | 20 +++++++++++--- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 129e0935..d4e0c9a6 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -4,7 +4,6 @@ #include #include "opencv2/features2d.hpp" #include "opencv2/shape/shape_transformer.hpp" -// #include // Library #include "auxiliary.h" @@ -640,8 +639,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get alignment metrics std::map image_metrics; - cv::Mat alignmentMask = generateOverlapMask(im1Proc, h, im2Proc.size(), im1Proc.size()); - image_metrics = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); + cv::Mat alignmentMask = generateOverlapMask(im2Proc.size(), h, im1Proc.size()); + image_metrics = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); // combine metrics // std::map temp_map = keypoint_metrics; diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 681ee1ee..b13934c5 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -17,7 +17,10 @@ using namespace cv; // align images with TPS algorithm void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, - Rcpp::NumericMatrix query_landmark, Rcpp::NumericMatrix reference_landmark) + Rcpp::NumericMatrix query_landmark, Rcpp::NumericMatrix reference_landmark, + const bool invert_query, const bool invert_ref, + Mat1d &accuracyMatte, + std::map &accuracy) { // seed @@ -34,6 +37,9 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, for (unsigned int i = 0; i < ref_mat.size(); i++) matches.push_back(cv::DMatch(i, i, 0)); + // message + Rcout << "Running Thin-Plate-Spline Alignment" << endl; + // calculate transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); tps->estimateTransformation(ref_mat, query_mat, matches); @@ -47,14 +53,34 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, int x_max = max(im1.cols, im2.cols); // extend images - cv::copyMakeBorder(im1, im1, 0.0, (int) (y_max - im1.rows), 0.0, (x_max - im1.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + cv::copyMakeBorder(im1, im1, + 0.0, (int) (y_max - im1.rows), + 0.0, (x_max - im1.cols), + cv::BORDER_CONSTANT, + Scalar(0, 0, 0)); // transform image tps->warpImage(im1, im1Reg); - + + // resize image - cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), cv::Range(0,im2.size().width)); + cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), + cv::Range(0,im2.size().width)); im1Reg = im1Reg_cropped.clone(); + + // get alignment metrics + cv::Mat alignmentMask = generateOverlapMask(im2.size(), + tps, + im1Reg.size()); + + // get matte metric, process + Mat im1Proc, im2Proc; + cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); + cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); + im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); + im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } // align images with TPS algorithm @@ -134,8 +160,9 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k } // get alignment metrics - cv::Mat alignmentMask = generateOverlapMask(im1Affine, h, - im2.size(), im1Affine.size()); + cv::Mat alignmentMask = generateOverlapMask(im2.size(), + h, + im1Affine.size()); // get matte metric, process Mat im1Proc, im2Proc; @@ -143,7 +170,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); - accuracy = getAlignmentMetrics(im1Proc, im2Proc, h, alignmentMask); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); if(!run_TPS){ @@ -291,7 +318,12 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, if(strcmp(method.get_cstring(), "Non-Rigid") == 0){ alignImagesTPS(im, imReference, imReg, keypoints, - query_landmark, reference_landmark); + query_landmark, + reference_landmark, + invert_query, + invert_ref, + accuracyMatte, + accuracy); } // transformation matrix, can be either a matrix, set of keypoints or both diff --git a/src/metrics.cpp b/src/metrics.cpp index 9f887a7e..17c8b3ee 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -4,7 +4,6 @@ #include #include "opencv2/features2d.hpp" #include "opencv2/shape/shape_transformer.hpp" -// #include // Internal functions #include "auxiliary.h" @@ -138,8 +137,9 @@ bool checkDegenerate(double pts1, double pts2) { return is_degenerate; } -cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, - cv::Size dsize, cv::Size ssize) +cv::Mat generateOverlapMask(cv::Size dsize, + cv::Mat& h, + cv::Size ssize) { // generate mask cv::Mat mask = cv::Mat::ones(ssize, CV_8UC1) * 255; @@ -164,6 +164,28 @@ cv::Mat generateOverlapMask(cv::Mat& im, cv::Mat& h, return warped; } +cv::Mat generateOverlapMask(cv::Size dsize, + Ptr& tps, + cv::Size ssize) +{ + // generate mask + cv::Mat mask = cv::Mat::ones(ssize, CV_8UC1) * 255; + cv::Mat warped; + + // Keep masks crisp: nearest-neighbor only. + const int interp = cv::INTER_NEAREST; + const int borderMode = cv::BORDER_CONSTANT; + const cv::Scalar borderValue(0); + + // warp mask + tps->warpImage(mask, warped, + interp, borderMode, borderValue); + + // Force binary mask again. + cv::threshold(warped, warped, 0, 255, cv::THRESH_BINARY); + return warped; +} + double Entropy(cv::Mat& im1, cv::Mat& overlapMask, int bins = 256) { // Histogram settings @@ -260,7 +282,7 @@ double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, return (ent1+ent2)/ent12; } -std::map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &h, Mat &mask){ +std::map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &mask){ // Metrics std::map metrics; diff --git a/src/metrics.h b/src/metrics.h index bbbab037..e80f6a43 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -1,5 +1,11 @@ #include "Rcpp.h" #include +#include "opencv2/shape/shape_transformer.hpp" + +// Namespaces +using namespace Rcpp; +using namespace std; +using namespace cv; #ifndef METRICS_H #define METRICS_H @@ -22,11 +28,17 @@ void maskKeypoints(std::vector &keypoints1_good, std::vector& tps, + cv::Size ssize); -std::map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, - cv::Mat &h, cv::Mat &mask); +std::map getAlignmentMetrics(cv::Mat &im1, + cv::Mat &im2, + cv::Mat &mask); // do overall checks on keypoints and images std::map getKeypointMetrics(std::vector &points1, From 4d7d18b184245ee2503d771d323853d134965bdb Mon Sep 17 00:00:00 2001 From: Artur-man Date: Fri, 17 Jul 2026 15:51:49 +0200 Subject: [PATCH 13/37] rmarkdown doc check --- docs/voltronobjects.html | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/voltronobjects.html b/docs/voltronobjects.html index e9047c73..4b1e4b0b 100644 --- a/docs/voltronobjects.html +++ b/docs/voltronobjects.html @@ -683,8 +683,8 @@

Channel Names

vrImageChannelNames(melc_data)
 vrImageChannelNames(melc_data, assay = "MELC")
 vrImageChannelNames(melc_data, assay = "Assay1")
-
##        Assay    Layer         Sample Spatial  Channels
-## Assay1  MELC Section1 control_case_3    MELC DAPI,CD45
+
##        Assay    Layer         Sample Spatial           Channels
+## Assay1  MELC Section1 control_case_3    MELC DAPI,CD45,combined


@@ -726,6 +726,8 @@

Combining Image Channels

melc_data <- combineChannels(melc_data, 
                              channels = c("DAPI", "CD45"), colors = c("grey", "green"), 
                              channel_key = "combined")
+
## Warning in .local(object, ..., value = value): A channel with name 'combined' already exists in this vrImage object. 
+##  Overwriting ...

These new images can be stored as new channels within the same image object, and called later again

vrImageChannelNames(melc_data)
@@ -833,14 +835,14 @@

features

href="#assays">vrMainAssay)

selected_features <- vrFeatures(visium_data)
 selected_features[1:20]
-
##  [1] "Xkr4"          "Gm1992"        "Gm19938"       "Gm37381"       "Rp1"           "Sox17"         "Gm37587"       "Gm37323"       "Mrpl15"       
-## [10] "Lypla1"        "Tcea1"         "Rgs20"         "Gm16041"       "Atp6v1h"       "Oprk1"         "Npbwr1"        "Rb1cc1"        "4732440D04Rik"
-## [19] "Alkal1"        "St18"
+
##  [1] "Xkr4"          "Gm1992"        "Gm19938"       "Gm37381"       "Rp1"           "Sox17"         "Gm37587"      
+##  [8] "Gm37323"       "Mrpl15"        "Lypla1"        "Tcea1"         "Rgs20"         "Gm16041"       "Atp6v1h"      
+## [15] "Oprk1"         "Npbwr1"        "Rb1cc1"        "4732440D04Rik" "Alkal1"        "St18"
visium_data_subset <- subset(visium_data, features = selected_features[1:20])
 vrFeatures(visium_data_subset)
-
##  [1] "Xkr4"          "Gm1992"        "Gm19938"       "Gm37381"       "Rp1"           "Sox17"         "Gm37587"       "Gm37323"       "Mrpl15"       
-## [10] "Lypla1"        "Tcea1"         "Rgs20"         "Gm16041"       "Atp6v1h"       "Oprk1"         "Npbwr1"        "Rb1cc1"        "4732440D04Rik"
-## [19] "Alkal1"        "St18"
+
##  [1] "Xkr4"          "Gm1992"        "Gm19938"       "Gm37381"       "Rp1"           "Sox17"         "Gm37587"      
+##  [8] "Gm37323"       "Mrpl15"        "Lypla1"        "Tcea1"         "Rgs20"         "Gm16041"       "Atp6v1h"      
+## [15] "Oprk1"         "Npbwr1"        "Rb1cc1"        "4732440D04Rik" "Alkal1"        "St18"


From d68a1e85781fb282f4d5924a6e313eacdcad7951 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Fri, 17 Jul 2026 17:11:02 +0200 Subject: [PATCH 14/37] add optional header --- src/matte_mi.cpp | 3 +-- src/matte_mi.h | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index bad678d8..aad077e2 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -1,6 +1,5 @@ #include - -// OpenCV +#include #include // Internal functions diff --git a/src/matte_mi.h b/src/matte_mi.h index 1618484b..71899b04 100644 --- a/src/matte_mi.h +++ b/src/matte_mi.h @@ -1,4 +1,5 @@ #include "Rcpp.h" +#include #include #ifndef MATTE_MI_H From d462baa672a7104498147668401cbe5324cfcc21 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 22 Jul 2026 11:44:23 +0200 Subject: [PATCH 15/37] initial accuracy for fine alignment imp. --- R/RcppExports.R | 4 ++ R/registration.R | 29 +++++++-- src/RcppExports.cpp | 20 ++++++ src/accuracy.cpp | 60 +++++++++++++++++ src/automated_registration.cpp | 96 +++++++++++++++------------ src/image.cpp | 110 +++++++++++++++++++++++-------- src/image.h | 26 ++++++-- src/manual_registration.cpp | 116 ++++++++++++++++++++------------- src/metrics.cpp | 34 +++++++--- src/metrics.h | 5 +- 10 files changed, 366 insertions(+), 134 deletions(-) create mode 100644 src/accuracy.cpp diff --git a/R/RcppExports.R b/R/RcppExports.R index 2f976552..a641a94b 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -1,6 +1,10 @@ # Generated by using Rcpp::compileAttributes() -> do not edit by hand # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 +accuracy_rawvector <- function(ref_image, query_image, trans_mat, width1, height1, width2, height2, invert_query, invert_ref) { + .Call('_VoltRon_accuracy_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, trans_mat, width1, height1, width2, height2, invert_query, invert_ref) +} + automated_registeration_rawvector <- function(ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid) { .Call('_VoltRon_automated_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid) } diff --git a/R/registration.R b/R/registration.R index 47631d5d..405faefb 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3406,9 +3406,11 @@ getAutomatedRegisteration <- function( # Plot Alignment Stats lapply(register_ind, function(i) { cur_align_stats <- alignment_stats_list[[i]] + print(cur_align_stats) output[[paste0("alignment_stats", i)]] <- renderTable({ - data.frame(Metrics = names(cur_align_stats), - `Stats.` = cur_align_stats) + data.frame(Metrics = names(cur_align_stats[["coarse"]]), + `Coarse` = cur_align_stats[["coarse"]], + `Fine` = cur_align_stats[["fine"]]) }) }) @@ -3703,10 +3705,16 @@ getRcppAutomatedRegistration <- function( if (!is.null(reg[[4]])) magick::image_read(reg[[4]]) else NA overlay_image <- if (!is.null(reg[[5]])) magick::image_read(reg[[5]]) else NA + + # check alignment accuracy and matte maps matte_map <- if (!is.null(reg[[6]])) reg[[6]] else NA - metrics <- c(.ALIGNMENT_ACCURACY_METRICS, .ALIGNMENT_KEYPOINT_METRICS) - alignment_stats <- { + alignment_stats <- list() + print(reg[[7]]) + print(reg[[8]]) + metrics <- c(.ALIGNMENT_ACCURACY_METRICS, + .ALIGNMENT_KEYPOINT_METRICS) + alignment_stats[["coarse"]] <- { if (!is.null(reg[[7]])){ if(!all(names(reg[[7]]) %in% metrics)){ stop("There are missing accuracy metrics!") @@ -3717,6 +3725,19 @@ getRcppAutomatedRegistration <- function( NA } } + alignment_stats[["fine"]] <- { + # metrics <- .ALIGNMENT_ACCURACY_METRICS + if (!is.null(reg[[8]])){ + if(!all(names(reg[[8]]) %in% metrics)){ + stop("There are missing accuracy metrics!") + } else { + reg[[8]][metrics] + } + } else{ + NA + } + } + print(alignment_stats[["fine"]]) # return return(list( diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index a4d3c176..9db0931f 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -11,6 +11,25 @@ Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); #endif +// accuracy_rawvector +Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, Rcpp::NumericMatrix trans_mat, const int width1, const int height1, const int width2, const int height2, const bool invert_query, const bool invert_ref); +RcppExport SEXP _VoltRon_accuracy_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP trans_matSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP invert_querySEXP, SEXP invert_refSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< Rcpp::RawVector& >::type ref_image(ref_imageSEXP); + Rcpp::traits::input_parameter< Rcpp::RawVector& >::type query_image(query_imageSEXP); + Rcpp::traits::input_parameter< Rcpp::NumericMatrix >::type trans_mat(trans_matSEXP); + Rcpp::traits::input_parameter< const int >::type width1(width1SEXP); + Rcpp::traits::input_parameter< const int >::type height1(height1SEXP); + Rcpp::traits::input_parameter< const int >::type width2(width2SEXP); + Rcpp::traits::input_parameter< const int >::type height2(height2SEXP); + Rcpp::traits::input_parameter< const bool >::type invert_query(invert_querySEXP); + Rcpp::traits::input_parameter< const bool >::type invert_ref(invert_refSEXP); + rcpp_result_gen = Rcpp::wrap(accuracy_rawvector(ref_image, query_image, trans_mat, width1, height1, width2, height2, invert_query, invert_ref)); + return rcpp_result_gen; +END_RCPP +} // automated_registeration_rawvector Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, const int width1, const int height1, const int width2, const int height2, const float GOOD_MATCH_PERCENT, const int MAX_FEATURES, const bool invert_query, const bool invert_ref, Rcpp::String flipflop_query, Rcpp::String flipflop_ref, Rcpp::String rotate_query, Rcpp::String rotate_ref, Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid); RcppExport SEXP _VoltRon_automated_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP GOOD_MATCH_PERCENTSEXP, SEXP MAX_FEATURESSEXP, SEXP invert_querySEXP, SEXP invert_refSEXP, SEXP flipflop_querySEXP, SEXP flipflop_refSEXP, SEXP rotate_querySEXP, SEXP rotate_refSEXP, SEXP matcherSEXP, SEXP methodSEXP, SEXP nonrigidSEXP) { @@ -184,6 +203,7 @@ END_RCPP } static const R_CallMethodDef CallEntries[] = { + {"_VoltRon_accuracy_rawvector", (DL_FUNC) &_VoltRon_accuracy_rawvector, 9}, {"_VoltRon_automated_registeration_rawvector", (DL_FUNC) &_VoltRon_automated_registeration_rawvector, 17}, {"_VoltRon_replaceNaMatrix", (DL_FUNC) &_VoltRon_replaceNaMatrix, 2}, {"_VoltRon_warpRcppImage", (DL_FUNC) &_VoltRon_warpRcppImage, 7}, diff --git a/src/accuracy.cpp b/src/accuracy.cpp new file mode 100644 index 00000000..c6ae0f0f --- /dev/null +++ b/src/accuracy.cpp @@ -0,0 +1,60 @@ +#include + +// OpenCV +#include + +// Library +#include "auxiliary.h" +#include "image.h" +#include "metrics.h" +#include "matte_mi.h" + +// Namespaces +using namespace Rcpp; +using namespace std; +using namespace cv; + +// [[Rcpp::export]] +Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, + Rcpp::RawVector& query_image, + Rcpp::NumericMatrix trans_mat, + const int width1, + const int height1, + const int width2, + const int height2, + const bool invert_query, + const bool invert_ref) +{ + // results + Rcpp::List out(2); + + // Read images + cv::Mat imReference = imageToMat(ref_image, width1, height1); + cv::Mat im = imageToMat(query_image, width2, height2); + cv::Mat h = numericMatrixToMat(trans_mat); + + // get alignment metrics + cv::Mat alignmentMask = generateOverlapMask(imReference.size(), + h, + im.size()); + + // process + Mat im1Proc, im2Proc; + cvtColor(im, im1Proc, cv::COLOR_BGR2GRAY); + cvtColor(imReference, im2Proc, cv::COLOR_BGR2GRAY); + im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); + im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); + + // get metrics + std::map accuracy; + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); + out[0] = accuracy; // accuracy stats + + // get matte map + Mat1d accuracyMatte; + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + out[1] = matToNumericMatrix(accuracyMatte); // Matte MI metric + + // return + return out; +} \ No newline at end of file diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index d4e0c9a6..2c2a04a5 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -397,8 +397,8 @@ void getSIFTTransformationMatrix( // check variable bool check; - Rcout << "Calculating" << (run_Affine ? " Affine " : " Homography ") << "Transformation Matrix" << endl; - + Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; + Rcout << "Round 1: No histogram equalization" << endl; // find matches and points @@ -508,7 +508,7 @@ bool getORBTransformationMatrix( // check variable Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; - + // Find transformation matrix if(points1.size() > 0){ if(run_Affine){ @@ -571,7 +571,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, const char* rotate_query, const char* rotate_ref, const bool run_Affine, const bool run_TPS, Mat1d &accuracyMatte, - std::map &accuracy) + std::map &accuracy_coarse, + std::map &accuracy_fine) { // parameters @@ -602,7 +603,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, if(strcmp(matcher.get_cstring(), "BRUTE-FORCE") == 0){ // message - Rcout << "Running BRUTE-FORCE Alignment" << endl; + Rcout << "Running Coarse Alignment (BRUTE-FORCE)" << endl; // run ORB bool check; @@ -613,7 +614,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } else { // message - Rcout << "Running SIFT+FLANN Alignment" << ((run_TPS) ? " with TPS" : "") << endl; + // Rcout << "Running SIFT+FLANN Alignment" << ((run_TPS) ? " with TPS" : "") << endl; + Rcout << "Running Coarse Alignment (SIFT+FLANN)" << endl; // run SIFT getSIFTTransformationMatrix(im1Proc, im2Proc, h, mask, imMatches, @@ -640,12 +642,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get alignment metrics std::map image_metrics; cv::Mat alignmentMask = generateOverlapMask(im2Proc.size(), h, im1Proc.size()); - image_metrics = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); - - // combine metrics - // std::map temp_map = keypoint_metrics; - // temp_map.insert(image_metrics.begin(), image_metrics.end()); - // accuracy = temp_map; + image_metrics = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Coarse"); // combine metrics std::vector> temp_map; @@ -653,19 +650,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, std::copy(keypoint_metrics.begin(), keypoint_metrics.end(), std::back_inserter(temp_map)); std::copy(image_metrics.begin(), image_metrics.end(), std::back_inserter(temp_map)); std::map final_map(temp_map.begin(), temp_map.end()); - accuracy = final_map; - - // std::vector> temp_map; - // temp_map.reserve(keypoint_metrics.size() + image_metrics.size()); - // temp_map.insert(temp_map.end(), keypoint_metrics.begin(), keypoint_metrics.end()); - // temp_map.insert(temp_map.end(), image_metrics.begin(), image_metrics.end()); - // std::map accuracy(temp_map.begin(), temp_map.end()); - - // - // accuracy.reserve(keypoint_metrics.size() + image_metrics.size()); - // accuracy.insert(accuracy.end(), keypoint_metrics.begin(), keypoint_metrics.end()); - // accuracy.insert(accuracy.end(), image_metrics.begin(), image_metrics.end()); - // Rcout << accuracy.size() << std::endl; + accuracy_coarse = final_map; // get matte metric accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); @@ -688,8 +673,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // TPS is requested (only if FLANN succeeded) } else { - Rcout << "Calculating Thin-Plate-Spline Interpolation" << endl; - + Rcout << "Running Fine Alignment (Thin-Plate-Spline)" << endl; + // Filtered points (inliers) based on the mask std::vector filtered_points1; std::vector filtered_points2; @@ -722,21 +707,43 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, keypoints[0] = point2fToNumericMatrix(filtered_points2); keypoints[1] = point2fToNumericMatrix(filtered_points1_reg); - // determine extension limits for both images - int y_max = max(im1Proc.rows, im2.rows); - int x_max = max(im1Proc.cols, im2.cols); + // warp overlap mask + cv::imwrite("before.png", alignmentMask); + alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, + im2.rows, im2.cols, + cv::INTER_NEAREST); + // cv::Mat alignmentMask = generateOverlapMask(im2Proc, + // tps, + // im1Proc.size()); - // extend images - cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // transform image using trained tps + // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, cv::INTER_LINEAR); + // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, cv::INTER_LINEAR); - // transform image - tps->warpImage(im1Proc, im1Proc); - tps->warpImage(im1NormalProc, im1NormalProc); + // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, + // im2.rows, im2.cols, cv::INTER_LINEAR); + // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, + // im2.rows, im2.cols, cv::INTER_LINEAR); + + // // determine extension limits for both images + // int y_max = max(im1Proc.rows, im2.rows); + // int x_max = max(im1Proc.cols, im2.cols); + // + // // extend images and mask + // cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // + // // transform image + // tps->warpImage(im1Proc, im1Proc); + // tps->warpImage(im1NormalProc, im1NormalProc); + // + // // resize image + // im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + // im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - // resize image - im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + // get matte metric, process + accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); // change color map cv::addWeighted(im2Proc, 0.7, im1Proc, 0.3, 0, im1Proc); @@ -769,12 +776,12 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid) { // Return data - Rcpp::List out(7); + Rcpp::List out(8); Rcpp::List out_trans(2); Rcpp::List keypoints(2); Mat imOverlay, imReg, h, imMatches; Mat1d accuracyMatte; - std::map accuracy; + std::map accuracy_coarse, accuracy_fine; // Read reference image cv::Mat imReference = imageToMat(ref_image, width1, height1); @@ -797,7 +804,8 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R rotate_query.get_cstring(), rotate_ref.get_cstring(), run_Affine, run_TPS, accuracyMatte, - accuracy); + accuracy_coarse, + accuracy_fine); // transformation matrix, can be either a matrix, set of keypoints or both out_trans[0] = matToNumericMatrix(h.clone()); @@ -814,13 +822,15 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R out[3] = matToImage(imMatches); // keypoint matching image out[4] = matToImage(imOverlay); // overlay image out[5] = matToNumericMatrix(accuracyMatte); // Matte MI metric - out[6] = accuracy; // accuracy scores + out[6] = accuracy_coarse; // accuracy scores (coarse) + out[7] = accuracy_fine; // accuracy scores (fine) } else { out[2] = R_NilValue; out[3] = R_NilValue; out[4] = R_NilValue; out[5] = R_NilValue; out[6] = R_NilValue; + out[7] = R_NilValue; } // release diff --git a/src/image.cpp b/src/image.cpp index 5152d859..0d62bbe9 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -126,18 +126,43 @@ void scaledDrawMatches(cv::Mat im1, std::vector &keypoints1, // draw matches drawMatches(im1, keypoints1, im2, keypoints2, top_matches, imMatches); } + +cv::Mat warpTPSImage(cv::Mat& ref_image, + cv::Mat& query_image, + Ptr& tps, + const int border_x, + const int border_y, + const int interpolation){ -// [[Rcpp::export]] -Rcpp::RawVector warpRcppImage(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, - Rcpp::List mapping, - const int width1, const int height1, - const int width2, const int height2) -{ - // Read reference image - cv::Mat imReference = imageToMat(ref_image, width1, height1); + // determine extension limits for both images + int y_max = max(query_image.rows, border_y); + int x_max = max(query_image.cols, border_x); + + // extend images + cv::copyMakeBorder(query_image, query_image, + 0.0, (int) (y_max - query_image.rows), + 0.0, (x_max - query_image.cols), + cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + + // transform image + cv::Mat query_image_reg; + tps->warpImage(query_image, query_image_reg, interpolation); + + cv::imwrite("after.png", query_image_reg); + + // resize image + query_image_reg = query_image_reg( + cv::Range(0,ref_image.size().height), + cv::Range(0,ref_image.size().width)); + + // return + return query_image_reg.clone(); +} + +void warpImage(cv::Mat& ref_image, + cv::Mat& query_image, + Rcpp::List mapping){ - // Read image to be aligned - cv::Mat im = imageToMat(query_image, width2, height2); cv::Mat im_temp; // list @@ -155,11 +180,11 @@ Rcpp::RawVector warpRcppImage(Rcpp::RawVector ref_image, Rcpp::RawVector query_i // transform coordinates if(h.rows == 2){ - cv::warpAffine(im, im_temp, h, imReference.size()); + cv::warpAffine(query_image, im_temp, h, ref_image.size()); } else { - cv::warpPerspective(im, im_temp, h, imReference.size()); + cv::warpPerspective(query_image, im_temp, h, ref_image.size()); } - im = im_temp; + query_image = im_temp; } // non-rigid warping @@ -177,35 +202,66 @@ Rcpp::RawVector warpRcppImage(Rcpp::RawVector ref_image, Rcpp::RawVector query_i // calculate transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); - tps->estimateTransformation(ref_mat, query_mat, matches); - - // determine extension limits for both images - int y_max = max(im.rows, imReference.rows); - int x_max = max(im.cols, imReference.cols); - // extend images - cv::copyMakeBorder(im, im, 0.0, (int) (y_max - im.rows), 0.0, (x_max - im.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // estimate transformation + tps->estimateTransformation(ref_mat, query_mat, matches); - // transform image - tps->warpImage(im, im_temp); + // transform image using trained tps + im_temp = warpTPSImage(ref_image, query_image, tps, + ref_image.rows, ref_image.cols, cv::INTER_LINEAR); + // warpTPSImage(ref_image, query_image, tps, cv::INTER_LINEAR); + // im_temp = query_image; - // resize image - cv::Mat im_temp_cropped = im_temp(cv::Range(0,imReference.size().height), cv::Range(0,imReference.size().width)); - im_temp = im_temp_cropped.clone(); + // // determine extension limits for both images + // int y_max = max(query_image.rows, ref_image.rows); + // int x_max = max(query_image.cols, ref_image.cols); + // + // // extend images + // cv::copyMakeBorder(query_image, query_image, + // 0.0, (int) (y_max - query_image.rows), + // 0.0, (x_max - query_image.cols), + // cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // + // // transform image + // tps->warpImage(query_image, im_temp); + // + // // resize image + // cv::Mat im_temp_cropped = im_temp(cv::Range(0,ref_image.size().height), cv::Range(0,ref_image.size().width)); + // im_temp = im_temp_cropped.clone(); } else { // pass registered object - im_temp = im; + im_temp = query_image; } - im = im_temp; + query_image = im_temp; } +} + +// [[Rcpp::export]] +Rcpp::RawVector warpRcppImage(Rcpp::RawVector ref_image, + Rcpp::RawVector query_image, + Rcpp::List mapping, + const int width1, const int height1, + const int width2, const int height2) +{ + // Read reference image + cv::Mat imReference = imageToMat(ref_image, width1, height1); + + // Read image to be aligned + cv::Mat im = imageToMat(query_image, width2, height2); + + // warp image + warpImage(imReference, im, mapping); // return return matToImage(im); } +///// +// Legacy //// +///// // [[Rcpp::export]] Rcpp::RawVector warpImageAuto(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, diff --git a/src/image.h b/src/image.h index 6a9a7b26..34fe97ef 100644 --- a/src/image.h +++ b/src/image.h @@ -2,6 +2,10 @@ #include #include "opencv2/shape/shape_transformer.hpp" +using namespace Rcpp; +using namespace std; +using namespace cv; + #ifndef IMAGE_H #define IMAGE_H @@ -9,8 +13,10 @@ // Processing //// -cv::Mat preprocessImage(cv::Mat &im, const bool invert, const char* flipflop, const char* rotate); -cv::Mat reversepreprocessImage(cv::Mat &im, const char* flipflop, const char* rotate); +cv::Mat preprocessImage(cv::Mat &im, const bool invert, + const char* flipflop, const char* rotate); +cv::Mat reversepreprocessImage(cv::Mat &im, + const char* flipflop, const char* rotate); cv::Mat resize_image(cv::Mat &im, int width); std::vector resize_keypoints(std::vector &keypoints, cv::Mat &im, @@ -24,17 +30,27 @@ void scaledDrawMatches(cv::Mat im1, std::vector &keypoints1, // Warping //// -Rcpp::RawVector warpImage(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, +cv::Mat warpTPSImage(cv::Mat& ref_image, + cv::Mat& query_image, + Ptr& tps, + const int border_x, + const int border_y, + const int interpolation); + +Rcpp::RawVector warpImage(Rcpp::RawVector ref_image, + Rcpp::RawVector query_image, Rcpp::List mapping, const int width1, const int height1, const int width2, const int height2); -Rcpp::RawVector warpImageAuto(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, +Rcpp::RawVector warpImageAuto(Rcpp::RawVector ref_image, + Rcpp::RawVector query_image, Rcpp::List mapping, const int width1, const int height1, const int width2, const int height2); -Rcpp::RawVector warpImageManual(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, +Rcpp::RawVector warpImageManual(Rcpp::RawVector ref_image, + Rcpp::RawVector query_image, Rcpp::List mapping, const int width1, const int height1, const int width2, const int height2); diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index b13934c5..530ae928 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -38,8 +38,8 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, matches.push_back(cv::DMatch(i, i, 0)); // message - Rcout << "Running Thin-Plate-Spline Alignment" << endl; - + Rcout << "Running Course Alignment (Thin-Plate-Spline)" << endl; + // calculate transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); tps->estimateTransformation(ref_mat, query_mat, matches); @@ -48,38 +48,44 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, keypoints[0] = point2fToNumericMatrix(ref_mat); keypoints[1] = point2fToNumericMatrix(query_mat); - // determine extension limits for both images - int y_max = max(im1.rows, im2.rows); - int x_max = max(im1.cols, im2.cols); - - // extend images - cv::copyMakeBorder(im1, im1, - 0.0, (int) (y_max - im1.rows), - 0.0, (x_max - im1.cols), - cv::BORDER_CONSTANT, - Scalar(0, 0, 0)); - - // transform image - tps->warpImage(im1, im1Reg); - + // transform image using trained tps + im1Reg = warpTPSImage(im2, im1, tps, + im2.rows, im2.cols, cv::INTER_LINEAR); - // resize image - cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), - cv::Range(0,im2.size().width)); - im1Reg = im1Reg_cropped.clone(); + // // determine extension limits for both images + // int y_max = max(im1.rows, im2.rows); + // int x_max = max(im1.cols, im2.cols); + // + // // extend images + // cv::copyMakeBorder(im1, im1, + // 0.0, (int) (y_max - im1.rows), + // 0.0, (x_max - im1.cols), + // cv::BORDER_CONSTANT, + // Scalar(0, 0, 0)); + // + // // transform image + // tps->warpImage(im1, im1Reg); + // + // + // // resize image + // cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), + // cv::Range(0,im2.size().width)); + // im1Reg = im1Reg_cropped.clone(); - // get alignment metrics - cv::Mat alignmentMask = generateOverlapMask(im2.size(), - tps, - im1Reg.size()); - - // get matte metric, process + // process Mat im1Proc, im2Proc; cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); - accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); + + // get alignment mask + cv::Mat alignmentMask = generateOverlapMask(im2Proc, + tps, + im1Proc.size()); + + // get alignment metrics + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } @@ -147,6 +153,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // calculate homography transformation Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; + Mat im1Affine; std::vector query_reg; if(run_Affine){ @@ -159,18 +166,18 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cv::perspectiveTransform(query_mat, query_reg, h); } - // get alignment metrics + // get alignment metrics for course registration cv::Mat alignmentMask = generateOverlapMask(im2.size(), h, - im1Affine.size()); + im1.size()); - // get matte metric, process + // get matte metric, process image before Mat im1Proc, im2Proc; cvtColor(im1Affine, im1Proc, cv::COLOR_BGR2GRAY); cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); - accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); if(!run_TPS){ @@ -181,8 +188,8 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k } else { // message - Rcout << "Running Thin-Plate-Spline Alignment" << endl; - + Rcout << "Running Fine Alignment (Thin-Plate-Spline)" << endl; + // calculate TPS transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); tps->estimateTransformation(ref_mat, query_reg, matches); @@ -191,19 +198,37 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k keypoints[0] = point2fToNumericMatrix(ref_mat); keypoints[1] = point2fToNumericMatrix(query_reg); - // determine extension limits for both images - int y_max = max(im1Affine.rows, im2.rows); - int x_max = max(im1Affine.cols, im2.cols); + // transform image using trained tps + im1Reg = warpTPSImage(im2, im1Affine, tps, + im2.rows, im2.cols, cv::INTER_LINEAR); - // extend images - cv::copyMakeBorder(im1Affine, im1Affine, 0.0, (int) (y_max - im1Affine.rows), 0.0, (x_max - im1Affine.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // // determine extension limits for both images + // int y_max = max(im1Affine.rows, im2.rows); + // int x_max = max(im1Affine.cols, im2.cols); + // + // // extend images + // cv::copyMakeBorder(im1Affine, im1Affine, 0.0, (int) (y_max - im1Affine.rows), 0.0, (x_max - im1Affine.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // + // // transform image + // tps->warpImage(im1Affine, im1Reg); + // + // // resize image + // cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), cv::Range(0,im2.size().width)); + // im1Reg = im1Reg_cropped.clone(); - // transform image - tps->warpImage(im1Affine, im1Reg); + // get alignment metrics + cv::Mat alignmentMask = generateOverlapMask(im2, + tps, + im1Affine.size()); - // resize image - cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), cv::Range(0,im2.size().width)); - im1Reg = im1Reg_cropped.clone(); + // get matte metric, process + Mat im1Proc, im2Proc; + cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); + cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); + im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); + im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } } @@ -235,6 +260,7 @@ void alignImagesAffineTPS_points(Rcpp::NumericMatrix &query_data, // calculate homography transformation Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; + std::vector query_reg; std::vector query_data_reg; if(run_Affine){ @@ -250,8 +276,8 @@ void alignImagesAffineTPS_points(Rcpp::NumericMatrix &query_data, if(run_TPS){ // message - Rcout << "Running Thin-Plate-Spline Alignment" << endl; - + Rcout << "Running Fine Alignment (Thin-Plate-Spline)" << endl; + // calculate TPS transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); tps->estimateTransformation(ref_mat, query_reg, matches); diff --git a/src/metrics.cpp b/src/metrics.cpp index 17c8b3ee..d8d820cd 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -164,13 +164,13 @@ cv::Mat generateOverlapMask(cv::Size dsize, return warped; } -cv::Mat generateOverlapMask(cv::Size dsize, +cv::Mat generateOverlapMask(cv::Mat ref_image, Ptr& tps, cv::Size ssize) { // generate mask cv::Mat mask = cv::Mat::ones(ssize, CV_8UC1) * 255; - cv::Mat warped; + // cv::Mat warped; // Keep masks crisp: nearest-neighbor only. const int interp = cv::INTER_NEAREST; @@ -178,12 +178,29 @@ cv::Mat generateOverlapMask(cv::Size dsize, const cv::Scalar borderValue(0); // warp mask - tps->warpImage(mask, warped, - interp, borderMode, borderValue); + // Rcout << "artur" << endl; + // Rcout << mask.size() << endl; + // cv::imwrite("alignmentMask.png", mask); + // Rcout << "artur" << endl; + + // Rcout << + // tps->warpImage(mask, warped, + // interp, borderMode, borderValue); + // tps->warpImage(mask, mask, + // interp, borderMode, borderValue); + Rcout << ref_image.rows << " " << ref_image.cols << endl; + mask = warpTPSImage(ref_image, mask, tps, + ref_image.rows, ref_image.cols, interp); + + // warp mask + // Rcout << "artur" << endl; + // Rcout << mask.size() << endl; + // cv::imwrite("alignmentMask_after.png", mask); + // Rcout << "artur" << endl; // Force binary mask again. - cv::threshold(warped, warped, 0, 255, cv::THRESH_BINARY); - return warped; + cv::threshold(mask, mask, 0, 255, cv::THRESH_BINARY); + return mask; } double Entropy(cv::Mat& im1, cv::Mat& overlapMask, int bins = 256) { @@ -282,7 +299,8 @@ double NormalizedMutualInfo(cv::Mat& im1, cv::Mat& im2, return (ent1+ent2)/ent12; } -std::map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &mask){ +std::map getAlignmentMetrics(Mat &im1, Mat &im2, + Mat &mask, std::string type){ // Metrics std::map metrics; @@ -303,7 +321,7 @@ std::map getAlignmentMetrics(Mat &im1, Mat &im2, Mat &mask) cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX); // Summary - Rcout << "Alignment Accuracy (Course): " << endl; + Rcout << "Alignment Accuracy (" << type << "): " << endl; metrics["Intersection"] = cv::compareHist(hist1, hist2, cv::HISTCMP_INTERSECT); metrics["Bhattacharyya"] = cv::compareHist(hist1, hist2, cv::HISTCMP_BHATTACHARYYA); metrics["Matte's MI"] = MatteMI(im2, im1, mask, 50); diff --git a/src/metrics.h b/src/metrics.h index e80f6a43..1ca925bb 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -32,13 +32,14 @@ cv::Mat generateOverlapMask(cv::Size dsize, cv::Mat& h, cv::Size ssize); -cv::Mat generateOverlapMask(cv::Size dsize, +cv::Mat generateOverlapMask(cv::Mat ref_image, Ptr& tps, cv::Size ssize); std::map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, - cv::Mat &mask); + cv::Mat &mask, + std::string type); // do overall checks on keypoints and images std::map getKeypointMetrics(std::vector &points1, From 225c32ddde1721ed2da844740ad0dd72c81b83a4 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 22 Jul 2026 12:28:28 +0200 Subject: [PATCH 16/37] checking non-rigid fine accuracy errors --- R/registration.R | 7 +++-- src/automated_registration.cpp | 51 +++++++++++++++++----------------- src/image.cpp | 4 +-- src/metrics.cpp | 2 +- src/metrics.h | 2 +- 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/R/registration.R b/R/registration.R index 405faefb..ac1cd203 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3714,12 +3714,14 @@ getRcppAutomatedRegistration <- function( print(reg[[8]]) metrics <- c(.ALIGNMENT_ACCURACY_METRICS, .ALIGNMENT_KEYPOINT_METRICS) + metrics_set <- setNames(rep(NA, length(metrics)), metrics) alignment_stats[["coarse"]] <- { if (!is.null(reg[[7]])){ if(!all(names(reg[[7]]) %in% metrics)){ stop("There are missing accuracy metrics!") } else { - reg[[7]][metrics] + metrics_set[metrics] <- reg[[7]][metrics] + metrics_set } } else{ NA @@ -3731,7 +3733,8 @@ getRcppAutomatedRegistration <- function( if(!all(names(reg[[8]]) %in% metrics)){ stop("There are missing accuracy metrics!") } else { - reg[[8]][metrics] + metrics_set[metrics] <- reg[[8]][metrics] + metrics_set } } else{ NA diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 2c2a04a5..2d607377 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -709,37 +709,38 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // warp overlap mask cv::imwrite("before.png", alignmentMask); - alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, - im2.rows, im2.cols, - cv::INTER_NEAREST); - // cv::Mat alignmentMask = generateOverlapMask(im2Proc, - // tps, - // im1Proc.size()); - - // transform image using trained tps - // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, cv::INTER_LINEAR); - // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, cv::INTER_LINEAR); + cv::imwrite("before_image.png", im1Proc); + // alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, + // im2.rows, im2.cols, + // cv::INTER_NEAREST); + // // transform image using trained tps // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, // im2.rows, im2.cols, cv::INTER_LINEAR); // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, // im2.rows, im2.cols, cv::INTER_LINEAR); - // // determine extension limits for both images - // int y_max = max(im1Proc.rows, im2.rows); - // int x_max = max(im1Proc.cols, im2.cols); - // - // // extend images and mask - // cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - // cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - // - // // transform image - // tps->warpImage(im1Proc, im1Proc); - // tps->warpImage(im1NormalProc, im1NormalProc); - // - // // resize image - // im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - // im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + // determine extension limits for both images + int y_max = max(im1Proc.rows, im2.rows); + int x_max = max(im1Proc.cols, im2.cols); + + // extend images and mask + cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + cv::copyMakeBorder(alignmentMask, alignmentMask, 0.0, (int) (y_max - alignmentMask.rows), 0.0, (x_max - alignmentMask.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + + // transform image + tps->warpImage(im1Proc, im1Proc); + tps->warpImage(im1NormalProc, im1NormalProc); + tps->warpImage(alignmentMask, alignmentMask, cv::INTER_NEAREST); + + // resize image + im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + alignmentMask = alignmentMask(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + + cv::imwrite("after.png", alignmentMask); + cv::imwrite("after_image.png", im1Proc); // get matte metric, process accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); diff --git a/src/image.cpp b/src/image.cpp index 0d62bbe9..8861674f 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -130,8 +130,8 @@ void scaledDrawMatches(cv::Mat im1, std::vector &keypoints1, cv::Mat warpTPSImage(cv::Mat& ref_image, cv::Mat& query_image, Ptr& tps, - const int border_x, const int border_y, + const int border_x, const int interpolation){ // determine extension limits for both images @@ -148,8 +148,6 @@ cv::Mat warpTPSImage(cv::Mat& ref_image, cv::Mat query_image_reg; tps->warpImage(query_image, query_image_reg, interpolation); - cv::imwrite("after.png", query_image_reg); - // resize image query_image_reg = query_image_reg( cv::Range(0,ref_image.size().height), diff --git a/src/metrics.cpp b/src/metrics.cpp index d8d820cd..694c06c4 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -164,7 +164,7 @@ cv::Mat generateOverlapMask(cv::Size dsize, return warped; } -cv::Mat generateOverlapMask(cv::Mat ref_image, +cv::Mat generateOverlapMask(cv::Mat& ref_image, Ptr& tps, cv::Size ssize) { diff --git a/src/metrics.h b/src/metrics.h index 1ca925bb..77b3fc7b 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -32,7 +32,7 @@ cv::Mat generateOverlapMask(cv::Size dsize, cv::Mat& h, cv::Size ssize); -cv::Mat generateOverlapMask(cv::Mat ref_image, +cv::Mat generateOverlapMask(cv::Mat& ref_image, Ptr& tps, cv::Size ssize); From ac051cbf4afb8f5dda274294e2cd585768a101d1 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 22 Jul 2026 14:39:43 +0200 Subject: [PATCH 17/37] fix fine non-rigid bug in remasking points again --- src/automated_registration.cpp | 58 ++++++++++------------------------ 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index d4e0c9a6..07ada074 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -91,7 +91,9 @@ void getGoodMatches(std::vector> &matches12,std::vector& points1, std::vector& points2, float threshold = std::numeric_limits::epsilon()) { +void removeCloseMatches(std::vector& points1, + std::vector& points2, + float threshold = std::numeric_limits::epsilon()) { // Create a vector to store filtered points std::vector filtered_points1; @@ -410,7 +412,7 @@ void getSIFTTransformationMatrix( // equalize first image if fails if(!check){ - // clear points + // clear points, mask is reset itself points1.clear(); points2.clear(); @@ -429,7 +431,7 @@ void getSIFTTransformationMatrix( // equalize second image if fails if(!check){ - // clear points + // clear points, mask is reset itself points1.clear(); points2.clear(); @@ -447,7 +449,7 @@ void getSIFTTransformationMatrix( // last try with both equalized images if(!check){ - // clear points + // clear points, mask is reset itself points1.clear(); points2.clear(); @@ -634,18 +636,13 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } // get keypoint metrics - std::map keypoint_metrics; + std::map keypoint_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); // get alignment metrics std::map image_metrics; cv::Mat alignmentMask = generateOverlapMask(im2Proc.size(), h, im1Proc.size()); image_metrics = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask); - - // combine metrics - // std::map temp_map = keypoint_metrics; - // temp_map.insert(image_metrics.begin(), image_metrics.end()); - // accuracy = temp_map; // combine metrics std::vector> temp_map; @@ -654,19 +651,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, std::copy(image_metrics.begin(), image_metrics.end(), std::back_inserter(temp_map)); std::map final_map(temp_map.begin(), temp_map.end()); accuracy = final_map; - - // std::vector> temp_map; - // temp_map.reserve(keypoint_metrics.size() + image_metrics.size()); - // temp_map.insert(temp_map.end(), keypoint_metrics.begin(), keypoint_metrics.end()); - // temp_map.insert(temp_map.end(), image_metrics.begin(), image_metrics.end()); - // std::map accuracy(temp_map.begin(), temp_map.end()); - - // - // accuracy.reserve(keypoint_metrics.size() + image_metrics.size()); - // accuracy.insert(accuracy.end(), keypoint_metrics.begin(), keypoint_metrics.end()); - // accuracy.insert(accuracy.end(), image_metrics.begin(), image_metrics.end()); - // Rcout << accuracy.size() << std::endl; - + // get matte metric accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); @@ -690,37 +675,29 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, Rcout << "Calculating Thin-Plate-Spline Interpolation" << endl; - // Filtered points (inliers) based on the mask - std::vector filtered_points1; - std::vector filtered_points2; - for (int i = 0; i < mask.rows; i++) { - if (mask.at(i)) { - filtered_points1.push_back(points1[i]); - filtered_points2.push_back(points2[i]); - } - } - removeCloseMatches(filtered_points1, filtered_points2); + // remove close looking matches + removeCloseMatches(points1, points2); // transform query - std::vector filtered_points1_reg; + std::vector points1_reg; if (h.rows == 2){ - cv::transform(filtered_points1, filtered_points1_reg, h); + cv::transform(points1, points1_reg, h); } else { - cv::perspectiveTransform(filtered_points1, filtered_points1_reg, h); + cv::perspectiveTransform(points1, points1_reg, h); } // get TPS matches std::vector matches; - for (unsigned int i = 0; i < filtered_points2.size(); i++) + for (unsigned int i = 0; i < points2.size(); i++) matches.push_back(cv::DMatch(i, i, 0)); // calculate TPS transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); - tps->estimateTransformation(filtered_points2, filtered_points1_reg, matches); + tps->estimateTransformation(points2, points1_reg, matches); // save keypoints - keypoints[0] = point2fToNumericMatrix(filtered_points2); - keypoints[1] = point2fToNumericMatrix(filtered_points1_reg); + keypoints[0] = point2fToNumericMatrix(points2); + keypoints[1] = point2fToNumericMatrix(points1_reg); // determine extension limits for both images int y_max = max(im1Proc.rows, im2.rows); @@ -737,7 +714,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // resize image im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - // change color map cv::addWeighted(im2Proc, 0.7, im1Proc, 0.3, 0, im1Proc); From 559b61ea5ccc07e55efff5ac693a89db2ae617db Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 22 Jul 2026 15:08:58 +0200 Subject: [PATCH 18/37] fix mask alignment bug --- src/automated_registration.cpp | 91 +++++++++++++++------------------- src/metrics.cpp | 4 ++ 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 6865f70e..b4ab74a8 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -287,8 +287,6 @@ bool getSIFTTransformationMatrixSingle( Ptr sift = cv::SIFT::create(params.sift_nfeatures); computeSIFTTiles(im1Proc, keypoints1, descriptors1, sift, params); computeSIFTTiles(im2Proc, keypoints2, descriptors2, sift, params); - // Rcout << "Generated " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; - // Rcout << "DONE: SIFT based key-points detection and descriptors computation" << endl; // filter duplicates filterDuplicateKeypoints(keypoints1, descriptors1); @@ -297,7 +295,6 @@ bool getSIFTTransformationMatrixSingle( // get top key points keepTopKeypoints(keypoints1, descriptors1, params); keepTopKeypoints(keypoints2, descriptors2, params); - // Rcout << "Filtered other than " << keypoints1.size() << " and " << keypoints2.size() << " keypoints" << endl; /////////////////////// /// Compute FLANN ///// @@ -306,7 +303,6 @@ bool getSIFTTransformationMatrixSingle( // Match features using FLANN matching std::vector> matches12, matches21; getFLANNMatches(descriptors1, descriptors2, matches12, matches21); - // Rcout << "DONE: FLANN - Fast Library for Approximate Nearest Neighbors - descriptor matching" << endl; // TODO: can I release there now ? descriptors1.release(); @@ -315,8 +311,7 @@ bool getSIFTTransformationMatrixSingle( // Find good matches std::vector good_matches; getGoodMatches(matches12, matches21, good_matches); - // Rcout << "DONE: get good mutual matches by distance thresholding" << endl; - + /////////////////////// /// Find Homography /// /////////////////////// @@ -485,22 +480,19 @@ bool getORBTransformationMatrix( Ptr orb = ORB::create(MAX_FEATURES); orb->detectAndCompute(im1Proc, Mat(), keypoints1, descriptors1); orb->detectAndCompute(im2Proc, Mat(), keypoints2, descriptors2); - // Rcout << "DONE: orb based key-points detection and descriptors computation" << endl; - + // Match features. std::vector matches; Ptr matcher = DescriptorMatcher::create("BruteForce-Hamming"); matcher->match(descriptors1, descriptors2, matches, Mat()); - // Rcout << "DONE: BruteForce-Hamming - descriptor matching" << endl; - + // Sort matches by score std::sort(matches.begin(), matches.end()); // Remove not so good matches const int numGoodMatches = matches.size() * GOOD_MATCH_PERCENT; matches.erase(matches.begin()+numGoodMatches, matches.end()); - //Rcout << "DONE: get good matches by distance thresholding" << endl; - + // Extract location of good matches for( size_t i = 0; i < matches.size(); i++ ) { @@ -625,7 +617,10 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } - // Use homography to warp image + // warp mask and image + cv::Mat alignmentMask = generateOverlapMask(im2Proc.size(), + h, + im1Proc.size()); if(h.rows == 2){ warpAffine(im1Proc, im1Proc, h, im2Proc.size()); warpAffine(im1NormalProc, im1NormalProc, h, im2Proc.size()); @@ -639,12 +634,13 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get keypoint metrics std::map keypoint_metrics; - keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); + keypoint_metrics = getKeypointMetrics(points1, points2, + im1Proc, im2Proc, h, mask); // get alignment metrics std::map image_metrics; - cv::Mat alignmentMask = generateOverlapMask(im2Proc.size(), h, im1Proc.size()); - image_metrics = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Coarse"); + image_metrics = getAlignmentMetrics(im1Proc, im2Proc, + alignmentMask, "Coarse"); // combine metrics std::vector> temp_map; @@ -657,8 +653,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get matte metric accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); - // Rcout << "DONE: warped query image" << endl; - /////////////////////// /// Find Homography /// /////////////////////// @@ -702,39 +696,34 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, keypoints[1] = point2fToNumericMatrix(points1_reg); // warp overlap mask - cv::imwrite("before.png", alignmentMask); - cv::imwrite("before_image.png", im1Proc); - // alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, - // im2.rows, im2.cols, - // cv::INTER_NEAREST); - - // // transform image using trained tps - // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, - // im2.rows, im2.cols, cv::INTER_LINEAR); - // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, - // im2.rows, im2.cols, cv::INTER_LINEAR); - - // determine extension limits for both images - int y_max = max(im1Proc.rows, im2.rows); - int x_max = max(im1Proc.cols, im2.cols); - - // extend images and mask - cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - cv::copyMakeBorder(alignmentMask, alignmentMask, 0.0, (int) (y_max - alignmentMask.rows), 0.0, (x_max - alignmentMask.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - - // transform image - tps->warpImage(im1Proc, im1Proc); - tps->warpImage(im1NormalProc, im1NormalProc); - tps->warpImage(alignmentMask, alignmentMask, cv::INTER_NEAREST); - - // resize image - im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - alignmentMask = alignmentMask(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - - cv::imwrite("after.png", alignmentMask); - cv::imwrite("after_image.png", im1Proc); + alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, + im2.rows, im2.cols, + cv::INTER_NEAREST); + + // transform image using trained tps + im1Proc = warpTPSImage(im2Proc, im1Proc, tps, + im2.rows, im2.cols, cv::INTER_LINEAR); + im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, + im2.rows, im2.cols, cv::INTER_LINEAR); + + // // determine extension limits for both images + // int y_max = max(im1Proc.rows, im2.rows); + // int x_max = max(im1Proc.cols, im2.cols); + // + // // extend images and mask + // cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // cv::copyMakeBorder(alignmentMask, alignmentMask, 0.0, (int) (y_max - alignmentMask.rows), 0.0, (x_max - alignmentMask.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); + // + // // transform image + // tps->warpImage(im1Proc, im1Proc); + // tps->warpImage(im1NormalProc, im1NormalProc); + // tps->warpImage(alignmentMask, alignmentMask, cv::INTER_NEAREST); + // + // // resize image + // im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + // im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); + // alignmentMask = alignmentMask(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); // get matte metric, process accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); diff --git a/src/metrics.cpp b/src/metrics.cpp index 694c06c4..5ad4fe7c 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -150,6 +150,8 @@ cv::Mat generateOverlapMask(cv::Size dsize, const int borderMode = cv::BORDER_CONSTANT; const cv::Scalar borderValue(0); + cv::imwrite("before.png", mask); + // warp mask if (h.rows == 2){ cv::warpAffine(mask, warped, h, dsize, @@ -158,6 +160,8 @@ cv::Mat generateOverlapMask(cv::Size dsize, cv::warpPerspective(mask, warped, h, dsize, interp, borderMode, borderValue); } + + cv::imwrite("after.png", warped); // Force binary mask again. cv::threshold(warped, warped, 0, 255, cv::THRESH_BINARY); From e0c37390ef0c5f3722599b1694510874bb1ac9a2 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 22 Jul 2026 16:49:55 +0200 Subject: [PATCH 19/37] additional fixes to fine/coarse alignment accuracy --- R/registration.R | 50 +++++++++++++++++++++++----------- src/automated_registration.cpp | 17 ++++++++++-- src/manual_registration.cpp | 34 +++++++++++++---------- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/R/registration.R b/R/registration.R index ac1cd203..c7c46e6d 100644 --- a/R/registration.R +++ b/R/registration.R @@ -628,10 +628,10 @@ getAlignmentTabPanel <- function(len_images, centre, register_ind) { tabsetPanel( id = "inner_tabs", - tabPanel("Matte's MI Map", - imageOutput(paste0("plot_matte_map", i))), tabPanel("Alignment Stat.", tableOutput(paste0("alignment_stats", i))), + tabPanel("Matte's MI Map", + imageOutput(paste0("plot_matte_map", i))), tabPanel("Matching Keypoints", imageOutput(paste0("plot_keypoint_match", i))), @@ -2976,9 +2976,12 @@ getManualRegisteration <- function( if(length(alignment_stats_list)){ cur_align_stats <- alignment_stats_list[[i]] output[[paste0("alignment_stats", i)]] <- renderTable({ - data.frame(Metrics = names(cur_align_stats), - `Stats.` = cur_align_stats) - }) + tab <- data.frame(Metrics = names(cur_align_stats[["coarse"]]), + `Coarse` = cur_align_stats[["coarse"]]) + if(!all(is.na(cur_align_stats[["fine"]]))) + tab$Fine <- cur_align_stats[["fine"]] + tab + }, digits = 5, na = "") } }) @@ -3226,18 +3229,35 @@ getRcppManualRegistration <- function( tmp[tmp < 0] <- 0 tmp } else NA + + # alignment accuracy + alignment_stats <- list() metrics <- .ALIGNMENT_ACCURACY_METRICS - alignment_stats <- { + metrics_set <- setNames(rep(NA, length(metrics)), metrics) + alignment_stats[["coarse"]] <- { if (!is.null(reg[[4]])){ if(!all(names(reg[[4]]) %in% metrics)){ stop("There are missing accuracy metrics!") } else { - reg[[4]][metrics] + metrics_set[metrics] <- reg[[4]][metrics] + metrics_set + } + } else{ + NA + } + } + alignment_stats[["fine"]] <- { + if (!is.null(reg[[5]])){ + if(!all(names(reg[[5]]) %in% metrics)){ + stop("There are missing accuracy metrics!") + } else { + metrics_set[metrics] <- reg[[5]][metrics] + metrics_set } } else{ NA } - } + } } else { matte_map <- NULL alignment_stats <- NULL @@ -3406,12 +3426,13 @@ getAutomatedRegisteration <- function( # Plot Alignment Stats lapply(register_ind, function(i) { cur_align_stats <- alignment_stats_list[[i]] - print(cur_align_stats) output[[paste0("alignment_stats", i)]] <- renderTable({ - data.frame(Metrics = names(cur_align_stats[["coarse"]]), - `Coarse` = cur_align_stats[["coarse"]], - `Fine` = cur_align_stats[["fine"]]) - }) + tab <- data.frame(Metrics = names(cur_align_stats[["coarse"]]), + `Coarse` = cur_align_stats[["coarse"]]) + if(!all(is.na(cur_align_stats[["fine"]]))) + tab$Fine <- cur_align_stats[["fine"]] + tab + }, digits = 5, na = "") }) # Output summary @@ -3710,8 +3731,6 @@ getRcppAutomatedRegistration <- function( matte_map <- if (!is.null(reg[[6]])) reg[[6]] else NA alignment_stats <- list() - print(reg[[7]]) - print(reg[[8]]) metrics <- c(.ALIGNMENT_ACCURACY_METRICS, .ALIGNMENT_KEYPOINT_METRICS) metrics_set <- setNames(rep(NA, length(metrics)), metrics) @@ -3740,7 +3759,6 @@ getRcppAutomatedRegistration <- function( NA } } - print(alignment_stats[["fine"]]) # return return(list( diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index b4ab74a8..fae92572 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -697,14 +697,25 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // warp overlap mask alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, - im2.rows, im2.cols, + im2Proc.rows, im2Proc.cols, cv::INTER_NEAREST); // transform image using trained tps im1Proc = warpTPSImage(im2Proc, im1Proc, tps, - im2.rows, im2.cols, cv::INTER_LINEAR); + im2Proc.rows, im2Proc.cols, cv::INTER_LINEAR); im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, - im2.rows, im2.cols, cv::INTER_LINEAR); + im2Proc.rows, im2Proc.cols, cv::INTER_LINEAR); + + // // warp overlap mask + // alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, + // im2.rows, im2.cols, + // cv::INTER_NEAREST); + // + // // transform image using trained tps + // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, + // im2.rows, im2.cols, cv::INTER_LINEAR); + // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, + // im2.rows, im2.cols, cv::INTER_LINEAR); // // determine extension limits for both images // int y_max = max(im1Proc.rows, im2.rows); diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 530ae928..e5b99e4c 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -135,7 +135,8 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k const bool invert_query, const bool invert_ref, const bool run_Affine, const bool run_TPS, Mat1d &accuracyMatte, - std::map &accuracy) + std::map &accuracy_coarse, + std::map &accuracy_fine) { // seed cv::setRNGSeed(0); @@ -153,7 +154,8 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // calculate homography transformation Rcout << "Calculating" << (run_Affine ? " (Affine) " : " (Homography) ") << "Transformation Matrix" << endl; - + + // warp image Mat im1Affine; std::vector query_reg; if(run_Affine){ @@ -177,7 +179,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); - accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); + accuracy_coarse = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); if(!run_TPS){ @@ -198,6 +200,11 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k keypoints[0] = point2fToNumericMatrix(ref_mat); keypoints[1] = point2fToNumericMatrix(query_reg); + // warp overlap mask + alignmentMask = warpTPSImage(im2, alignmentMask, tps, + im2.rows, im2.cols, + cv::INTER_NEAREST); + // transform image using trained tps im1Reg = warpTPSImage(im2, im1Affine, tps, im2.rows, im2.cols, cv::INTER_LINEAR); @@ -216,18 +223,13 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), cv::Range(0,im2.size().width)); // im1Reg = im1Reg_cropped.clone(); - // get alignment metrics - cv::Mat alignmentMask = generateOverlapMask(im2, - tps, - im1Affine.size()); - // get matte metric, process Mat im1Proc, im2Proc; cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); - accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); + accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } } @@ -310,12 +312,12 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::String nonrigid) { // Return data - Rcpp::List out(4); + Rcpp::List out(5); Rcpp::List out_trans(2); Rcpp::List keypoints(2); Mat imReg, h; Mat1d accuracyMatte; - std::map accuracy; + std::map accuracy_coarse, accuracy_fine; // get params const bool run_TPS = (strcmp(method.get_cstring(), "Homography + Non-Rigid") == 0 || @@ -337,7 +339,8 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, invert_ref, run_Affine, run_TPS, accuracyMatte, - accuracy); + accuracy_coarse, + accuracy_fine); } // Non-rigid (TPS) only @@ -349,7 +352,7 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, invert_query, invert_ref, accuracyMatte, - accuracy); + accuracy_coarse); } // transformation matrix, can be either a matrix, set of keypoints or both @@ -357,10 +360,11 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, out_trans[1] = keypoints; out[0] = out_trans; - // registered image if exists + // registered image and accuracy if exists out[1] = matToImage(imReg.clone()); out[2] = matToNumericMatrix(accuracyMatte); // Matte MI metric - out[3] = accuracy; + out[3] = accuracy_coarse; + out[4] = accuracy_fine; return out; } From d8291239c8bd66a9919427f673366c3400227dcd Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 23 Jul 2026 03:13:20 +0200 Subject: [PATCH 20/37] initial mapping of mask for sitk --- R/RcppExports.R | 4 + R/registration.R | 208 +++++++++++++++++++----------------- src/RcppExports.cpp | 14 +++ src/accuracy.cpp | 20 ++-- src/auxiliary.cpp | 17 ++- src/auxiliary.h | 3 + src/manual_registration.cpp | 11 +- src/mapping.cpp | 7 -- src/metrics.cpp | 40 +++---- src/metrics.h | 8 +- 10 files changed, 179 insertions(+), 153 deletions(-) diff --git a/R/RcppExports.R b/R/RcppExports.R index a641a94b..cc90dc69 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -37,6 +37,10 @@ applyRcppMapping <- function(coords, mapping) { .Call('_VoltRon_applyRcppMapping', PACKAGE = 'VoltRon', coords, mapping) } +generateOverlapMask <- function(dsize, trans_mat, ssize) { + .Call('_VoltRon_generateOverlapMask', PACKAGE = 'VoltRon', dsize, trans_mat, ssize) +} + build_snn_rank <- function(neighbors) { .Call('_VoltRon_build_snn_rank', PACKAGE = 'VoltRon', neighbors) } diff --git a/R/registration.R b/R/registration.R index c7c46e6d..d365bd45 100644 --- a/R/registration.R +++ b/R/registration.R @@ -1398,94 +1398,6 @@ applyPerspectiveTransform <- function( return(object) } -#### -# Managing Mappings #### -#### - -manageMapping <- function(mappings) { - # check if all transformations are homography - allHomography <- suppressWarnings(all(lapply(mappings, function(map) { - nrow(map[[1]] > 0) && is.null(map[[2]]) - }))) - - # change the mapping - new_mappings <- list() - if (allHomography) { - mappings <- lapply(mappings, function(map) map[[1]]) - new_mappings <- list( - list(Reduce("%*%", mappings), NULL) - ) - } else { - new_mappings <- mappings - } - - # return - return(new_mappings) -} - -applyMapping <- function(coords, mapping){ - mapping_new <- mapping - if(!is.null(mapping[[1]][[2]])){ - if(is(mapping[[1]][[2]][[1]], "_p_itk__simple__TransformixImageFilter")){ - mapping_new[[1]] <- list(mapping[[1]][[1]], NULL) - coords <- applyRcppMapping(coords, mapping_new) - coords <- applySimpleITKMapping(coords, mapping[[1]][[2]][[1]]) - } else { - coords <- applyRcppMapping(coords, mapping) - } - } else { - coords <- applyRcppMapping(coords, mapping) - } - coords -} - -#' @importFrom utils write.table -applySimpleITKMapping <- function(coords, mapping){ - - # check SimpleITK - if (!requireNamespace('SimpleITK')) { - stop("Please install SimpleITK package!: ", - "remotes::install_github('BIMSBbioinfo/SimpleITKRInstaller')", - ", this is gonna take a while :)") - } - - # temp dir, delete later - tmpdir <- tempdir() - tmpdir <- file.path(tmpdir, "SimpleITK") - dir.create(tmpdir, showWarnings = FALSE) - - # get image - input_file <- file.path(tmpdir, "inputpoints.txt") - output_file <- file.path(tmpdir, "outputpoints.txt") - - # apply transformation - tfx <- mapping - suppressWarnings(file.remove(input_file, showWarnings = FALSE)) - cat("point\n", nrow(coords), "\n", file = input_file) - utils::write.table(coords, input_file, append = TRUE, - col.names = FALSE, row.names = FALSE, quote = FALSE) - tfx$SetOutputDirectory(tmpdir) - tfx$SetFixedPointSetFileName(input_file) - tmp <- tfx$Execute() - - # get points - lines_coords <- readLines(output_file) - coords <- do.call( - rbind, - lapply(lines_coords, function(x) { - tmp <- strsplit(strsplit(x, split = "\\t")[[1]][6], - split = " ")[[1]][c(5,6)] - as.numeric(tmp) - }) - ) - - # delete dir - unlink(tmpdir, recursive = TRUE) - - # return - coords -} - #### # Managing Parameters #### #### @@ -2566,6 +2478,99 @@ ggplot_to_magick <- function(plot, extent = NULL, width = 8, height = 6, dpi = 3 magick::image_read(tf) } +#### +# Managing Mappings #### +#### + +manageMapping <- function(mappings) { + # check if all transformations are homography + allHomography <- suppressWarnings(all(lapply(mappings, function(map) { + nrow(map[[1]] > 0) && is.null(map[[2]]) + }))) + + # change the mapping + new_mappings <- list() + if (allHomography) { + mappings <- lapply(mappings, function(map) map[[1]]) + new_mappings <- list( + list(Reduce("%*%", mappings), NULL) + ) + } else { + new_mappings <- mappings + } + + # return + return(new_mappings) +} + +applyMapping <- function(coords, mapping){ + mapping_new <- mapping + if(!is.null(mapping[[1]][[2]])){ + if(is(mapping[[1]][[2]][[1]], "_p_itk__simple__TransformixImageFilter")){ + mapping_new[[1]] <- list(mapping[[1]][[1]], NULL) + coords <- applyRcppMapping(coords, mapping_new) + coords <- applySimpleITKMapping(coords, mapping[[1]][[2]][[1]]) + } else { + coords <- applyRcppMapping(coords, mapping) + } + } else { + coords <- applyRcppMapping(coords, mapping) + } + coords +} + +#' @importFrom utils write.table +applySimpleITKMapping <- function(coords, mapping){ + + # check SimpleITK + if (!requireNamespace('SimpleITK')) { + stop("Please install SimpleITK package!: ", + "remotes::install_github('BIMSBbioinfo/SimpleITKRInstaller')", + ", this is gonna take a while :)") + } + + # temp dir, delete later + tmpdir <- tempdir() + tmpdir <- file.path(tmpdir, "SimpleITK") + dir.create(tmpdir, showWarnings = FALSE) + + # get image + input_file <- file.path(tmpdir, "inputpoints.txt") + output_file <- file.path(tmpdir, "outputpoints.txt") + + # apply transformation + tfx <- mapping + suppressWarnings(file.remove(input_file, showWarnings = FALSE)) + cat("point\n", nrow(coords), "\n", file = input_file) + utils::write.table(coords, input_file, append = TRUE, + col.names = FALSE, row.names = FALSE, quote = FALSE) + tfx$SetOutputDirectory(tmpdir) + tfx$SetFixedPointSetFileName(input_file) + tmp <- tfx$Execute() + + # get points + lines_coords <- readLines(output_file) + coords <- do.call( + rbind, + lapply(lines_coords, function(x) { + tmp <- strsplit(strsplit(x, split = "\\t")[[1]][6], + split = " ")[[1]][c(5,6)] + as.numeric(tmp) + }) + ) + + # delete dir + unlink(tmpdir, recursive = TRUE) + + # return + coords +} + + +#### +# Managing Transformations #### +#### + #' transformImage #' #' Apply given transformations to a magick image @@ -2708,7 +2713,6 @@ warpImage <- function(ref_image, query_image, mapping) { if(is(mapping[[1]][[2]][[2]], "_p_itk__simple__TransformixImageFilter")){ query_image <- magick::image_read(query_image) query_image <- warpSimpleITKImage( - ref_image = ref_image, query_image = query_image, mapping = mapping[[1]][[2]][[2]] ) @@ -2748,14 +2752,13 @@ warpImage <- function(ref_image, query_image, mapping) { #' #' Warping a query image given a homography image #' -#' @param ref_image reference image #' @param query_image query image #' @param mapping a list of the homography matrices and TPS keypoints #' #' @importFrom magick image_read image_data #' #' @export -warpSimpleITKImage <- function(ref_image, query_image, mapping) { +warpSimpleITKImage <- function(query_image, mapping) { # check SimpleITK if (!requireNamespace('SimpleITK')) { @@ -3829,7 +3832,6 @@ getSimpleITKAutomatedRegistration <- function( ref_image <- array(as.raw(ref_image), dim = dim(ref_image)) ref_image <- magick::image_read(ref_image) } - query_image <- rotateImage(query_image, as.numeric(rotate_query)) if (flipflop_query == "Flip") { query_image <- flipImage(query_image) @@ -3838,32 +3840,36 @@ getSimpleITKAutomatedRegistration <- function( } if(invert_query) query_image <- negateImage(query_image) - # query_image <- magick::image_negate(query_image) + + # generate coarse mapped mask + ref_info <- getImageInfo(ref_image) + query_info <- getImageInfo(query_image) + mask <- generateOverlapMask(c(ref_info$width, ref_info$height), + initial_mapping[[1]][[1]], + c(query_info$width, query_info$height)) + + # warp image query_image <- warpImage(ref_image = ref_image, query_image = query_image, mapping = initial_mapping) + # temp stop + stop() + # prepare images ref_image1 <- magick::as_EBImage(ref_image) - # ref_image1 <- EBImage::imageData(ref_image1) - # dim_img <- 1:length(dim(ref_image)) - # dim_img[1:2] <- rev(dim_img[1:2]) - # ref_image1 <- aperm(ref_image1, perm = c(2,1,3)) EBImage::writeImage(ref_image1, files = file.path(tmpdir, "ref_image.tiff"), compression = "LZW", reduce = TRUE) fixed <- SimpleITK::ReadImage(file.path(tmpdir, "ref_image.tiff"), 'sitkUInt8') query_image1 <- as_EBImage(query_image) - # dim_img <- 1:length(dim(query_image)) - # dim_img[1:2] <- rev(dim_img[1:2]) - # query_image1 <- EBImage::imageData(query_image1) - # query_image1 <- aperm(query_image1, perm = c(2,1)) EBImage::writeImage(query_image1, files = file.path(tmpdir, "query_image.tiff"), compression = "LZW", reduce = TRUE) moving <- SimpleITK::ReadImage(file.path(tmpdir, "query_image.tiff"), 'sitkUInt8') + mask <- SimpleITK::as.image(mask) # get registration for image elx <- SimpleITK::ElastixImageFilter() diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 9db0931f..773e86c9 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -168,6 +168,19 @@ BEGIN_RCPP return rcpp_result_gen; END_RCPP } +// generateOverlapMask +Rcpp::IntegerVector generateOverlapMask(Rcpp::NumericVector& dsize, Rcpp::NumericMatrix& trans_mat, Rcpp::NumericVector& ssize); +RcppExport SEXP _VoltRon_generateOverlapMask(SEXP dsizeSEXP, SEXP trans_matSEXP, SEXP ssizeSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< Rcpp::NumericVector& >::type dsize(dsizeSEXP); + Rcpp::traits::input_parameter< Rcpp::NumericMatrix& >::type trans_mat(trans_matSEXP); + Rcpp::traits::input_parameter< Rcpp::NumericVector& >::type ssize(ssizeSEXP); + rcpp_result_gen = Rcpp::wrap(generateOverlapMask(dsize, trans_mat, ssize)); + return rcpp_result_gen; +END_RCPP +} // build_snn_rank Rcpp::List build_snn_rank(Rcpp::IntegerMatrix neighbors); RcppExport SEXP _VoltRon_build_snn_rank(SEXP neighborsSEXP) { @@ -212,6 +225,7 @@ static const R_CallMethodDef CallEntries[] = { {"_VoltRon_manual_registeration_rawvector", (DL_FUNC) &_VoltRon_manual_registeration_rawvector, 12}, {"_VoltRon_manual_registeration_matrix", (DL_FUNC) &_VoltRon_manual_registeration_matrix, 5}, {"_VoltRon_applyRcppMapping", (DL_FUNC) &_VoltRon_applyRcppMapping, 2}, + {"_VoltRon_generateOverlapMask", (DL_FUNC) &_VoltRon_generateOverlapMask, 3}, {"_VoltRon_build_snn_rank", (DL_FUNC) &_VoltRon_build_snn_rank, 1}, {"_VoltRon_build_snn_number", (DL_FUNC) &_VoltRon_build_snn_number, 1}, {"_VoltRon_replacePatternInRcppVectorWrapper", (DL_FUNC) &_VoltRon_replacePatternInRcppVectorWrapper, 3}, diff --git a/src/accuracy.cpp b/src/accuracy.cpp index c6ae0f0f..9cc7817c 100644 --- a/src/accuracy.cpp +++ b/src/accuracy.cpp @@ -21,7 +21,7 @@ Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, const int width1, const int height1, const int width2, - const int height2, + const int height2, const bool invert_query, const bool invert_ref) { @@ -30,29 +30,29 @@ Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, // Read images cv::Mat imReference = imageToMat(ref_image, width1, height1); - cv::Mat im = imageToMat(query_image, width2, height2); + cv::Mat imReg = imageToMat(query_image, width1, height1); cv::Mat h = numericMatrixToMat(trans_mat); - // get alignment metrics - cv::Mat alignmentMask = generateOverlapMask(imReference.size(), - h, - im.size()); + // generate mask + cv::Mat maskReg = generateOverlapMask(imReference.size(), + h, + cv::Size(width2, height2)); // process Mat im1Proc, im2Proc; - cvtColor(im, im1Proc, cv::COLOR_BGR2GRAY); + cvtColor(imReg, im1Proc, cv::COLOR_BGR2GRAY); cvtColor(imReference, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); // get metrics std::map accuracy; - accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); - out[0] = accuracy; // accuracy stats + accuracy = getAlignmentMetrics(im1Proc, im2Proc, maskReg, "Course"); + out[0] = accuracy; // get matte map Mat1d accuracyMatte; - accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + accuracyMatte = MatteMIMap(im2Proc, im1Proc, maskReg, 50); out[1] = matToNumericMatrix(accuracyMatte); // Matte MI metric // return diff --git a/src/auxiliary.cpp b/src/auxiliary.cpp index 9e8c9cb3..74160f56 100644 --- a/src/auxiliary.cpp +++ b/src/auxiliary.cpp @@ -41,8 +41,6 @@ Rcpp::NumericMatrix replaceNaMatrix(Rcpp::NumericMatrix mat, int replace) { // Function to convert a cv::Mat object to a RawVector for magick images Rcpp::RawVector matToImage(const cv::Mat &mat) { - // profiler - // MemProfiler mp("Mat -> Image"); // Create RawVector object Rcpp::RawVector rawvec(mat.total() * mat.elemSize()); @@ -56,8 +54,6 @@ Rcpp::RawVector matToImage(const cv::Mat &mat) { // Function to convert a RawVector for magick images to a cv::Mat object cv::Mat imageToMat(Rcpp::RawVector &image_data, int width, int height) { - // profiler - // MemProfiler mp("Image -> Mat"); // Create cv::Mat object cv::Mat mat(height, width, CV_8UC3, image_data.begin()); @@ -68,6 +64,19 @@ cv::Mat imageToMat(Rcpp::RawVector &image_data, int width, int height) { return mat; } +// Function to convert a cv::Mat object to a RawVector for magick images +Rcpp::IntegerVector matToMask(const cv::Mat &mat) { + + // Create RawVector object + Rcpp::IntegerVector intvec(mat.total() * mat.elemSize()); + intvec.attr("dim") = Rcpp::Dimension(1, mat.cols, mat.rows); + + // Copy Mat data to RawVector + std::memcpy(intvec.begin(), mat.data, intvec.size()); + + return intvec; +} + // Function to convert a NumericMatrix object to a cv::Mat cv::Mat numericMatrixToMat(Rcpp::NumericMatrix nm) { cv::Mat m(nm.rows(), nm.cols(), CV_64F); diff --git a/src/auxiliary.h b/src/auxiliary.h index 6deb4419..5360c19e 100644 --- a/src/auxiliary.h +++ b/src/auxiliary.h @@ -19,6 +19,9 @@ Rcpp::NumericMatrix replaceNaMatrix(Rcpp::NumericMatrix mat, int replace); Rcpp::RawVector matToImage(const cv::Mat &mat); cv::Mat imageToMat(Rcpp::RawVector &image_data, int width, int height); +// cv::Mat vs Rcpp::RawVector(Image) with 2 dim (mostly for masks) +Rcpp::IntegerVector matToMask(const cv::Mat &mat); + // cv::Mat vs Rcpp::NumericMatrix cv::Mat numericMatrixToMat(Rcpp::NumericMatrix nm); Rcpp::NumericMatrix matToNumericMatrix(cv::Mat m); diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index e5b99e4c..0ecd4b1a 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -38,7 +38,7 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, matches.push_back(cv::DMatch(i, i, 0)); // message - Rcout << "Running Course Alignment (Thin-Plate-Spline)" << endl; + Rcout << "Running Coarse Alignment (Thin-Plate-Spline)" << endl; // calculate transformation Ptr tps = cv::createThinPlateSplineShapeTransformer(0); @@ -85,7 +85,7 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, im1Proc.size()); // get alignment metrics - accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Coarse"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } @@ -168,7 +168,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cv::perspectiveTransform(query_mat, query_reg, h); } - // get alignment metrics for course registration + // get alignment metrics for Coarse registration cv::Mat alignmentMask = generateOverlapMask(im2.size(), h, im1.size()); @@ -179,7 +179,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); - accuracy_coarse = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Course"); + accuracy_coarse = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Coarse"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); if(!run_TPS){ @@ -248,6 +248,9 @@ void alignImagesAffineTPS_points(Rcpp::NumericMatrix &query_data, RNG rng(12345); Scalar value; + // message + Rcout << "Running Coarse Alignment (Manual)" << endl; + // Get landmarks as Point2f std::vector query_mat = numericMatrixToPoint2f(query_landmark); std::vector ref_mat = numericMatrixToPoint2f(reference_landmark); diff --git a/src/mapping.cpp b/src/mapping.cpp index 2a10b90a..c3bbca0b 100644 --- a/src/mapping.cpp +++ b/src/mapping.cpp @@ -66,13 +66,6 @@ Rcpp::NumericMatrix applyRcppMapping(Rcpp::NumericMatrix coords, Rcpp::List mapp // apply transformation to coordinates tps->applyTransformation(coords_mat, coords_temp); - // // temp position - // std::vector query_mat_tmp; - // tps->applyTransformation(query_mat, query_mat_tmp); - // Rcout << query_mat << endl; - // Rcout << query_mat_tmp << endl; - // Rcout << ref_mat << endl; - } else { coords_temp = coords_mat; } diff --git a/src/metrics.cpp b/src/metrics.cpp index 5ad4fe7c..956d44ad 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -150,8 +150,6 @@ cv::Mat generateOverlapMask(cv::Size dsize, const int borderMode = cv::BORDER_CONSTANT; const cv::Scalar borderValue(0); - cv::imwrite("before.png", mask); - // warp mask if (h.rows == 2){ cv::warpAffine(mask, warped, h, dsize, @@ -160,8 +158,6 @@ cv::Mat generateOverlapMask(cv::Size dsize, cv::warpPerspective(mask, warped, h, dsize, interp, borderMode, borderValue); } - - cv::imwrite("after.png", warped); // Force binary mask again. cv::threshold(warped, warped, 0, 255, cv::THRESH_BINARY); @@ -174,39 +170,31 @@ cv::Mat generateOverlapMask(cv::Mat& ref_image, { // generate mask cv::Mat mask = cv::Mat::ones(ssize, CV_8UC1) * 255; - // cv::Mat warped; - + // Keep masks crisp: nearest-neighbor only. const int interp = cv::INTER_NEAREST; - const int borderMode = cv::BORDER_CONSTANT; - const cv::Scalar borderValue(0); + // const int borderMode = cv::BORDER_CONSTANT; + // const cv::Scalar borderValue(0); - // warp mask - // Rcout << "artur" << endl; - // Rcout << mask.size() << endl; - // cv::imwrite("alignmentMask.png", mask); - // Rcout << "artur" << endl; - - // Rcout << - // tps->warpImage(mask, warped, - // interp, borderMode, borderValue); - // tps->warpImage(mask, mask, - // interp, borderMode, borderValue); - Rcout << ref_image.rows << " " << ref_image.cols << endl; mask = warpTPSImage(ref_image, mask, tps, ref_image.rows, ref_image.cols, interp); - // warp mask - // Rcout << "artur" << endl; - // Rcout << mask.size() << endl; - // cv::imwrite("alignmentMask_after.png", mask); - // Rcout << "artur" << endl; - // Force binary mask again. cv::threshold(mask, mask, 0, 255, cv::THRESH_BINARY); return mask; } +// [[Rcpp::export]] +Rcpp::IntegerVector generateOverlapMask(Rcpp::NumericVector& dsize, + Rcpp::NumericMatrix& trans_mat, + Rcpp::NumericVector& ssize){ + cv::Mat h = numericMatrixToMat(trans_mat); + cv::Mat mask = generateOverlapMask(cv::Size((int) dsize[0], (int) dsize[1]), + h, + cv::Size((int) ssize[0], (int) ssize[1])); + return matToMask(mask); +} + double Entropy(cv::Mat& im1, cv::Mat& overlapMask, int bins = 256) { // Histogram settings diff --git a/src/metrics.h b/src/metrics.h index 77b3fc7b..bd809a7f 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -28,6 +28,7 @@ void maskKeypoints(std::vector &keypoints1_good, std::vector& tps, cv::Size ssize); +// cv::Mat generateOverlapMask(Rcpp::NumericVector dsize, +// Rcpp::NumericMatrix trans_mat, +// Rcpp::NumericVector ssize); + +// get alignment metrics std::map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, cv::Mat &mask, std::string type); -// do overall checks on keypoints and images +// do overall checks on keypoints and metrics std::map getKeypointMetrics(std::vector &points1, std::vector &points2, cv::Mat &im1, cv::Mat &im2, From b70e84da37d82ec8fa30a2586328aad86df94b30 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 23 Jul 2026 20:40:21 +0200 Subject: [PATCH 21/37] get accuracy for SimpleITK fine alignment --- R/RcppExports.R | 4 +- R/registration.R | 79 +++++++++++++++++++++++++++++++++++---- man/warpSimpleITKImage.Rd | 4 +- src/RcppExports.cpp | 20 +++++----- src/accuracy.cpp | 43 ++++++++++----------- src/auxiliary.cpp | 15 +++++--- 6 files changed, 115 insertions(+), 50 deletions(-) diff --git a/R/RcppExports.R b/R/RcppExports.R index cc90dc69..8ded2a70 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -1,8 +1,8 @@ # Generated by using Rcpp::compileAttributes() -> do not edit by hand # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 -accuracy_rawvector <- function(ref_image, query_image, trans_mat, width1, height1, width2, height2, invert_query, invert_ref) { - .Call('_VoltRon_accuracy_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, trans_mat, width1, height1, width2, height2, invert_query, invert_ref) +accuracy_rawvector <- function(ref_image, query_image, mask, width, height, type, overlay_images = TRUE) { + .Call('_VoltRon_accuracy_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, mask, width, height, type, overlay_images) } automated_registeration_rawvector <- function(ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid) { diff --git a/R/registration.R b/R/registration.R index d365bd45..1d6054a2 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3627,8 +3627,12 @@ computeAutomatedPairwiseTransform <- function( "rotate_", ref_label, "_image", cur_map[2])]], initial_mapping = list(reg[[1]]) ) - reg$aligned_image <- tfx$aligned_image reg[[1]][[2]] <- tfx$transformation + reg$aligned_image <- tfx$aligned_image + reg$overlay_image <- tfx$overlay_image + reg$matte_map <- tfx$matte_map + reg$alignment_stats$fine[names(tfx$alignment_metrics)] <- + tfx$alignment_metrics } # return transformation matrix and images @@ -3841,7 +3845,7 @@ getSimpleITKAutomatedRegistration <- function( if(invert_query) query_image <- negateImage(query_image) - # generate coarse mapped mask + # generate coarse mapped mask by warping ref_info <- getImageInfo(ref_image) query_info <- getImageInfo(query_image) mask <- generateOverlapMask(c(ref_info$width, ref_info$height), @@ -3853,10 +3857,7 @@ getSimpleITKAutomatedRegistration <- function( query_image = query_image, mapping = initial_mapping) - # temp stop - stop() - - # prepare images + # prepare images and masks ref_image1 <- magick::as_EBImage(ref_image) EBImage::writeImage(ref_image1, files = file.path(tmpdir, "ref_image.tiff"), @@ -3869,7 +3870,7 @@ getSimpleITKAutomatedRegistration <- function( compression = "LZW", reduce = TRUE) moving <- SimpleITK::ReadImage(file.path(tmpdir, "query_image.tiff"), 'sitkUInt8') - mask <- SimpleITK::as.image(mask) + mask <- SimpleITK::as.image(array(mask, rev(dim(mask)))) # get registration for image elx <- SimpleITK::ElastixImageFilter() @@ -3893,6 +3894,14 @@ getSimpleITKAutomatedRegistration <- function( tfx_image <- SimpleITK::TransformixImageFilter() tfx_image$LogToConsoleOff() tfx_image$SetTransformParameterMap(transform_param_map) + + # warp mask + tfx_image$SetMovingImage(mask) + tmp <- tfx_image$Execute() + aligned_mask <- SimpleITK::as.array(tfx_image$GetResultImage()) + aligned_mask <- array(aligned_mask, dim = c(dim(aligned_mask), 1)) + aligned_mask <- aperm(aligned_mask, c(2,1,3)) + aligned_mask <- magick::image_read(aligned_mask) # get transformation for the points and observations elx <- SimpleITK::ElastixImageFilter() @@ -3915,8 +3924,21 @@ getSimpleITKAutomatedRegistration <- function( # delete dir unlink(tmpdir, recursive = TRUE) + # calculate alignment accuracy + results <- getAlignmentAccuracy(ref_image, + aligned_image, + aligned_mask, + "Fine") + + # convert images + overlay_image <- + if (!is.null(results[[3]])) magick::image_read(results[[3]]) else NA + # return return(list(aligned_image = aligned_image, + alignment_metrics = results[[1]], + matte_map = results[[2]], + overlay_image = overlay_image, transformation = list( tfx_points = tfx_points, tfx_image = tfx_image @@ -4003,3 +4025,46 @@ getNonInteractiveRegistration <- function( ) ) } + +#### +# Accuracy #### +#### + +getAlignmentAccuracy <- function(ref_image, + query_image, + mask, + type){ + + # image info + ref_info <- getImageInfo(ref_image) + query_info <- getImageInfo(query_image) + + # ref image + if (inherits(ref_image, "ImageArray")) { + ref_image <- DelayedArray::realize(ref_image) + ref_image <- array(as.raw(ref_image), dim = dim(ref_image)) + } else { + ref_image <- magick::image_data(ref_image, channels = "rgb") + } + + # query image + if (inherits(query_image, "ImageArray")) { + query_image <- DelayedArray::realize(query_image) + query_image <- array(as.raw(query_image), dim = dim(query_image)) + } else { + query_image <- magick::image_data(query_image, channels = "rgb") + } + + # mask + mask <- magick::image_data(mask, channels = "rgb") + + # calculate alignment accuracy + accuracy_rawvector(ref_image, + query_image, + mask, + width = ref_info$width, + height = ref_info$height, + type, + overlay_images = TRUE) +} + diff --git a/man/warpSimpleITKImage.Rd b/man/warpSimpleITKImage.Rd index 03bbddd9..68fd3d83 100644 --- a/man/warpSimpleITKImage.Rd +++ b/man/warpSimpleITKImage.Rd @@ -4,11 +4,9 @@ \alias{warpSimpleITKImage} \title{getRcppWarpImage} \usage{ -warpSimpleITKImage(ref_image, query_image, mapping) +warpSimpleITKImage(query_image, mapping) } \arguments{ -\item{ref_image}{reference image} - \item{query_image}{query image} \item{mapping}{a list of the homography matrices and TPS keypoints} diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 773e86c9..2829135e 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -12,21 +12,19 @@ Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); #endif // accuracy_rawvector -Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, Rcpp::NumericMatrix trans_mat, const int width1, const int height1, const int width2, const int height2, const bool invert_query, const bool invert_ref); -RcppExport SEXP _VoltRon_accuracy_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP trans_matSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP invert_querySEXP, SEXP invert_refSEXP) { +Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, Rcpp::RawVector& mask, const int width, const int height, std::string type, bool overlay_images); +RcppExport SEXP _VoltRon_accuracy_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP maskSEXP, SEXP widthSEXP, SEXP heightSEXP, SEXP typeSEXP, SEXP overlay_imagesSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; Rcpp::traits::input_parameter< Rcpp::RawVector& >::type ref_image(ref_imageSEXP); Rcpp::traits::input_parameter< Rcpp::RawVector& >::type query_image(query_imageSEXP); - Rcpp::traits::input_parameter< Rcpp::NumericMatrix >::type trans_mat(trans_matSEXP); - Rcpp::traits::input_parameter< const int >::type width1(width1SEXP); - Rcpp::traits::input_parameter< const int >::type height1(height1SEXP); - Rcpp::traits::input_parameter< const int >::type width2(width2SEXP); - Rcpp::traits::input_parameter< const int >::type height2(height2SEXP); - Rcpp::traits::input_parameter< const bool >::type invert_query(invert_querySEXP); - Rcpp::traits::input_parameter< const bool >::type invert_ref(invert_refSEXP); - rcpp_result_gen = Rcpp::wrap(accuracy_rawvector(ref_image, query_image, trans_mat, width1, height1, width2, height2, invert_query, invert_ref)); + Rcpp::traits::input_parameter< Rcpp::RawVector& >::type mask(maskSEXP); + Rcpp::traits::input_parameter< const int >::type width(widthSEXP); + Rcpp::traits::input_parameter< const int >::type height(heightSEXP); + Rcpp::traits::input_parameter< std::string >::type type(typeSEXP); + Rcpp::traits::input_parameter< bool >::type overlay_images(overlay_imagesSEXP); + rcpp_result_gen = Rcpp::wrap(accuracy_rawvector(ref_image, query_image, mask, width, height, type, overlay_images)); return rcpp_result_gen; END_RCPP } @@ -216,7 +214,7 @@ END_RCPP } static const R_CallMethodDef CallEntries[] = { - {"_VoltRon_accuracy_rawvector", (DL_FUNC) &_VoltRon_accuracy_rawvector, 9}, + {"_VoltRon_accuracy_rawvector", (DL_FUNC) &_VoltRon_accuracy_rawvector, 7}, {"_VoltRon_automated_registeration_rawvector", (DL_FUNC) &_VoltRon_automated_registeration_rawvector, 17}, {"_VoltRon_replaceNaMatrix", (DL_FUNC) &_VoltRon_replaceNaMatrix, 2}, {"_VoltRon_warpRcppImage", (DL_FUNC) &_VoltRon_warpRcppImage, 7}, diff --git a/src/accuracy.cpp b/src/accuracy.cpp index 9cc7817c..2d585afd 100644 --- a/src/accuracy.cpp +++ b/src/accuracy.cpp @@ -17,37 +17,28 @@ using namespace cv; // [[Rcpp::export]] Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, - Rcpp::NumericMatrix trans_mat, - const int width1, - const int height1, - const int width2, - const int height2, - const bool invert_query, - const bool invert_ref) -{ + Rcpp::RawVector& mask, + const int width, + const int height, + std::string type, + bool overlay_images = true) { // results - Rcpp::List out(2); + Rcpp::List out(3); // Read images - cv::Mat imReference = imageToMat(ref_image, width1, height1); - cv::Mat imReg = imageToMat(query_image, width1, height1); - cv::Mat h = numericMatrixToMat(trans_mat); - - // generate mask - cv::Mat maskReg = generateOverlapMask(imReference.size(), - h, - cv::Size(width2, height2)); - + cv::Mat imReference = imageToMat(ref_image, width, height); + cv::Mat imReg = imageToMat(query_image, width, height); + cv::Mat maskReg = imageToMat(mask, width, height); + // process Mat im1Proc, im2Proc; cvtColor(imReg, im1Proc, cv::COLOR_BGR2GRAY); cvtColor(imReference, im2Proc, cv::COLOR_BGR2GRAY); - im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); - im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); + cvtColor(maskReg, maskReg, cv::COLOR_BGR2GRAY); // get metrics std::map accuracy; - accuracy = getAlignmentMetrics(im1Proc, im2Proc, maskReg, "Course"); + accuracy = getAlignmentMetrics(im1Proc, im2Proc, maskReg, type); out[0] = accuracy; // get matte map @@ -55,6 +46,16 @@ Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, accuracyMatte = MatteMIMap(im2Proc, im1Proc, maskReg, 50); out[1] = matToNumericMatrix(accuracyMatte); // Matte MI metric + // image overlay + if(overlay_images){ + cv::addWeighted(im2Proc, 0.7, im1Proc, 0.3, 0, im1Proc); + cvtColor(im1Proc, im1Proc, cv::COLOR_GRAY2BGR); + im1Proc = resize_image(im1Proc, 500); + out[2] = matToImage(im1Proc); + } else { + out[2] = R_NilValue; + } + // return return out; } \ No newline at end of file diff --git a/src/auxiliary.cpp b/src/auxiliary.cpp index 74160f56..f8f16a8d 100644 --- a/src/auxiliary.cpp +++ b/src/auxiliary.cpp @@ -67,12 +67,15 @@ cv::Mat imageToMat(Rcpp::RawVector &image_data, int width, int height) { // Function to convert a cv::Mat object to a RawVector for magick images Rcpp::IntegerVector matToMask(const cv::Mat &mat) { - // Create RawVector object - Rcpp::IntegerVector intvec(mat.total() * mat.elemSize()); - intvec.attr("dim") = Rcpp::Dimension(1, mat.cols, mat.rows); - - // Copy Mat data to RawVector - std::memcpy(intvec.begin(), mat.data, intvec.size()); + cv::Mat intMat; + mat.convertTo(intMat, CV_32S); + Rcpp::IntegerVector intvec(intMat.total()); + std::memcpy( + intvec.begin(), + intMat.data, + static_cast(intvec.size()) * sizeof(int) + ); + intvec.attr("dim") = Rcpp::Dimension(intMat.rows, intMat.cols); return intvec; } From 1bbe463877bd505944f1c818eb76a3b704477e59 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sat, 25 Jul 2026 17:04:54 +0200 Subject: [PATCH 22/37] optimization updates --- R/registration.R | 146 ++++++++++++++++++++++++++++++++++++++++------ src/auxiliary.cpp | 17 +++++- src/auxiliary.h | 1 + src/matte_mi.cpp | 86 ++++++++------------------- src/metrics.cpp | 1 + 5 files changed, 171 insertions(+), 80 deletions(-) diff --git a/R/registration.R b/R/registration.R index 1d6054a2..e3123a35 100644 --- a/R/registration.R +++ b/R/registration.R @@ -2478,6 +2478,18 @@ ggplot_to_magick <- function(plot, extent = NULL, width = 8, height = 6, dpi = 3 magick::image_read(tf) } +#' @noRd +convertToSitkImage <- function(img){ + # img_info <- magick::image_info(img) + img_data <- magick::image_data(img, channels = "gray") + dim_img <- dim(img_data) + img_data <- as.vector(img_data, mode = "integer") + # dim(img_data) <- c(3, img_info$width, img_info$height) + dim(img_data) <- dim_img + img_data <- aperm(img_data, c(2,3,1)) + SimpleITK::as.image(img_data, isVector = TRUE) +} + #### # Managing Mappings #### #### @@ -3851,6 +3863,7 @@ getSimpleITKAutomatedRegistration <- function( mask <- generateOverlapMask(c(ref_info$width, ref_info$height), initial_mapping[[1]][[1]], c(query_info$width, query_info$height)) + # mask <- magick::image_read(mask) # warp image query_image <- warpImage(ref_image = ref_image, @@ -3858,19 +3871,20 @@ getSimpleITKAutomatedRegistration <- function( mapping = initial_mapping) # prepare images and masks - ref_image1 <- magick::as_EBImage(ref_image) - EBImage::writeImage(ref_image1, - files = file.path(tmpdir, "ref_image.tiff"), - compression = "LZW", reduce = TRUE) - fixed <- SimpleITK::ReadImage(file.path(tmpdir, "ref_image.tiff"), - 'sitkUInt8') - query_image1 <- as_EBImage(query_image) - EBImage::writeImage(query_image1, - files = file.path(tmpdir, "query_image.tiff"), - compression = "LZW", reduce = TRUE) - moving <- SimpleITK::ReadImage(file.path(tmpdir, "query_image.tiff"), - 'sitkUInt8') + # magick::image_write(ref_image, file.path(tmpdir, "ref_image.tiff"), + # compression = "LZW") + # fixed <- SimpleITK::ReadImage(file.path(tmpdir, "ref_image.tiff"), + # 'sitkUInt8') + # magick::image_write(query_image, file.path(tmpdir, "query_image.tiff"), + # compression = "LZW") + # moving <- SimpleITK::ReadImage(file.path(tmpdir, "query_image.tiff"), + # 'sitkUInt8') + fixed <- convertToSitkImage(ref_image) + SimpleITK::Cast(fixed, "sitkUInt8") + moving <- convertToSitkImage(query_image) + SimpleITK::Cast(moving, "sitkUInt8") mask <- SimpleITK::as.image(array(mask, rev(dim(mask)))) + SimpleITK::Cast(mask, "sitkUInt8") # get registration for image elx <- SimpleITK::ElastixImageFilter() @@ -3881,15 +3895,27 @@ getSimpleITKAutomatedRegistration <- function( mp <- SimpleITK:::ReadParameterFile( system.file("extdata", "bspline_map.txt", package = "VoltRon") ) + + # grid_config <- makeGridSamplingSchedule( + # fixed_image = fixed, + # parameter_map = mp, + # target_samples = 50000L + # ) + # mp$set( + # "SampleGridSpacing", + # grid_config$parameter_values + # ) + elx$SetParameterMap(mp) - elx$LogToConsoleOff() + # elx$LogToConsoleOff() + elx$LogToConsoleOn() tmp <- elx$Execute() sitk_img <- SimpleITK::ReadImage(file.path(tmpdir, "result.0.tif")) arr <- SimpleITK::as.array(sitk_img) - arr8 <- 255 * (arr - min(arr)) / (max(arr) - min(arr)) - arr8 <- array(as.integer(arr8), dim = dim(arr)) - arr8 <- aperm(arr8, perm = c(2,1)) - aligned_image <- magick::image_read(as.raster(arr8 / 255)) + arr <- (arr - min(arr)) / (max(arr) - min(arr)) + arr <- array(arr, dim = dim(arr)) + arr <- aperm(arr, perm = c(2,1)) + aligned_image <- magick::image_read(as.raster(arr)) transform_param_map <- elx$GetTransformParameterMap() tfx_image <- SimpleITK::TransformixImageFilter() tfx_image$LogToConsoleOff() @@ -3945,6 +3971,92 @@ getSimpleITKAutomatedRegistration <- function( ))) } +makeGridSamplingSchedule <- function( + fixed_image, + parameter_map, + target_samples = 50000L, + number_of_resolutions = 10 +) { + image_size <- as.numeric(fixed_image$GetSize()) + + if (!is.numeric(target_samples) || + length(target_samples) != 1L || + !is.finite(target_samples) || + target_samples < 1) { + stop("'target_samples' must be a positive number.") + } + + number_of_dimensions <- length(image_size) + + # Default ITK schedule: + # nres = 4 -> 8, 4, 2, 1 + shrink_factors <- 2^rev( + seq.int(0L, number_of_resolutions - 1L) + ) + pyramid_schedule <- matrix( + rep(shrink_factors, each = number_of_dimensions), + nrow = number_of_resolutions, + ncol = number_of_dimensions, + byrow = TRUE + ) + + # ITK calculates the pyramid image size as: + # floor(original size / shrink factor), with a minimum of 1. + original_sizes <- matrix( + rep(image_size, times = number_of_resolutions), + nrow = number_of_resolutions, + ncol = number_of_dimensions, + byrow = TRUE + ) + + level_sizes <- floor(original_sizes / pyramid_schedule) + level_sizes[level_sizes < 1] <- 1 + + level_pixels <- apply(level_sizes, 1L, prod) + + # At small/coarse levels, use all pixels. + # At large/fine levels, choose spacing to stay around target_samples. + effective_target <- pmin( + as.double(target_samples), + level_pixels + ) + + spacing <- as.integer( + pmax( + 1, + ceiling(sqrt(level_pixels / effective_target)) + ) + ) + + spacing_matrix <- cbind(spacing, spacing) + + # Exact expected count before any mask filtering. + expected_samples <- + ( + 1 + (level_sizes[, 1] - 1) %/% spacing_matrix[, 1] + ) * + ( + 1 + (level_sizes[, 2] - 1) %/% spacing_matrix[, 2] + ) + + list( + # Elastix requires: + # sx0 sy0 sx1 sy1 ... + parameter_values = as.character(c(t(spacing_matrix))), + + report = data.frame( + resolution = seq_len(number_of_resolutions) - 1L, + pyramid_width = level_sizes[, 1], + pyramid_height = level_sizes[, 2], + shrink_x = pyramid_schedule[, 1], + shrink_y = pyramid_schedule[, 2], + grid_spacing_x = spacing_matrix[, 1], + grid_spacing_y = spacing_matrix[, 2], + expected_samples = expected_samples + ) + ) +} + #### # Non-interactive Image Registration #### #### diff --git a/src/auxiliary.cpp b/src/auxiliary.cpp index f8f16a8d..445ba29f 100644 --- a/src/auxiliary.cpp +++ b/src/auxiliary.cpp @@ -66,7 +66,7 @@ cv::Mat imageToMat(Rcpp::RawVector &image_data, int width, int height) { // Function to convert a cv::Mat object to a RawVector for magick images Rcpp::IntegerVector matToMask(const cv::Mat &mat) { - + cv::Mat intMat; mat.convertTo(intMat, CV_32S); Rcpp::IntegerVector intvec(intMat.total()); @@ -76,10 +76,23 @@ Rcpp::IntegerVector matToMask(const cv::Mat &mat) { static_cast(intvec.size()) * sizeof(int) ); intvec.attr("dim") = Rcpp::Dimension(intMat.rows, intMat.cols); - + return intvec; } +// // Function to convert a cv::Mat object to a RawVector for magick images +// Rcpp::RawVector matToMask(const cv::Mat &mat) { +// +// // Create RawVector object +// Rcpp::RawVector rawvec(mat.total() * mat.elemSize()); +// rawvec.attr("dim") = Rcpp::Dimension(mat.rows, mat.cols); +// +// // Copy Mat data to RawVector +// std::memcpy(rawvec.begin(), mat.data, rawvec.size()); +// +// return rawvec; +// } + // Function to convert a NumericMatrix object to a cv::Mat cv::Mat numericMatrixToMat(Rcpp::NumericMatrix nm) { cv::Mat m(nm.rows(), nm.cols(), CV_64F); diff --git a/src/auxiliary.h b/src/auxiliary.h index 5360c19e..9d9d5271 100644 --- a/src/auxiliary.h +++ b/src/auxiliary.h @@ -21,6 +21,7 @@ cv::Mat imageToMat(Rcpp::RawVector &image_data, int width, int height); // cv::Mat vs Rcpp::RawVector(Image) with 2 dim (mostly for masks) Rcpp::IntegerVector matToMask(const cv::Mat &mat); +// Rcpp::RawVector matToMask(const cv::Mat &mat); // cv::Mat vs Rcpp::NumericMatrix cv::Mat numericMatrixToMat(Rcpp::NumericMatrix nm); diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index aad077e2..f43fdf19 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -380,49 +380,6 @@ double linearPercentileFromSorted( return lower + fraction * (upper - lower); } -void validateChunkedMatteMIInputs( - cv::Mat1b& validGlobal, - std::size_t& validGlobalCount, - const cv::Mat& fixed, - const cv::Mat& moving, - const cv::Mat& mask, - ChunkSize chunkSize, - int bins) { - - const int height = fixed.rows; - const int width = fixed.cols; - - for (int y = 0; y < height; ++y) { - const double* fixedRow = fixed.ptr(y); - const double* movingRow = moving.ptr(y); - const double* maskRow = - mask.empty() ? nullptr : mask.ptr(y); - unsigned char* validRow = validGlobal.ptr(y); - - for (int x = 0; x < width; ++x) { - const bool insideMask = - maskRow == nullptr || static_cast(maskRow[x]); - - const bool valid = - insideMask && - std::isfinite(fixedRow[x]) && - std::isfinite(movingRow[x]); - - if (valid) { - validRow[x] = 1U; - ++validGlobalCount; - } - } - } - - // stop if no pixels are valid - if (validGlobalCount == 0U) { - throw std::invalid_argument( - "The mask contains no valid pixels."); - } - -} - // ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, cv::Mat1d MatteMIMap(const cv::Mat& fixed, const cv::Mat& moving, @@ -430,20 +387,6 @@ cv::Mat1d MatteMIMap(const cv::Mat& fixed, int bins = 50) { ChunkSize chunkSize = ChunkSize{}; - - // validate chunks and return validation map of pixels - // const int height = fixed.rows; - // const int width = fixed.cols; - // cv::Mat1b validGlobal(height, width, static_cast(0)); - // std::size_t validGlobalCount = 0U; - // validateChunkedMatteMIInputs(validGlobal, - // validGlobalCount, - // fixed, - // moving, - // mask, - // chunkSize, - // bins); - // Do I need these to be cv_64f ? cv::Mat fixed64; @@ -464,6 +407,7 @@ cv::Mat1d MatteMIMap(const cv::Mat& fixed, cv::Mat1b validGlobal(height, width, static_cast(0)); std::size_t validGlobalCount = 0U; + int temp_counter = 0; for (int y = 0; y < height; ++y) { const double* fixedRow = fixed64.ptr(y); const double* movingRow = moving64.ptr(y); @@ -475,6 +419,26 @@ cv::Mat1d MatteMIMap(const cv::Mat& fixed, const bool insideMask = maskRow == nullptr || static_cast(maskRow[x]); + // if(!std::isfinite(fixedRow[x])){ + // Rcout << "Fixed: "<< fixedRow[x] << endl; + // } + // if(!std::isfinite(movingRow[x])){ + // Rcout << "Moving: "<< movingRow[x] << endl; + // } + + // if(insideMask){ + // temp_counter += 1; + // if (temp_counter % 100 == 0){ + // Rcout << "Fixed: "<< fixedRow[x] << endl; + // Rcout << "Moving: "<< movingRow[x] << endl; + // } + // } + + // temp_counter += 1; + // if (temp_counter % 100 == 0){ + // Rcout << "Mask: "<< maskRow[x] << endl; + // } + const bool valid = insideMask && std::isfinite(fixedRow[x]) && @@ -640,7 +604,7 @@ double MatteMI( const cv::Mat& fixed, const cv::Mat& moving, const cv::Mat& mask, - int bins) { + int bins = 50) { const double nan = std::numeric_limits::quiet_NaN(); @@ -697,18 +661,18 @@ double MatteMI( fixed, y, fixedRowScratch); - + const double* movingRow = getRowAsDouble( moving, y, movingRowScratch); - + const unsigned char* maskRow = mask.empty() ? nullptr : mask8.ptr(y); - + for (int x = 0; x < width; ++x) { if (maskRow != nullptr && maskRow[x] == 0U) { diff --git a/src/metrics.cpp b/src/metrics.cpp index 956d44ad..9cd26830 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -193,6 +193,7 @@ Rcpp::IntegerVector generateOverlapMask(Rcpp::NumericVector& dsize, h, cv::Size((int) ssize[0], (int) ssize[1])); return matToMask(mask); + // return matToImage(mask); } double Entropy(cv::Mat& im1, cv::Mat& overlapMask, int bins = 256) { From fc6eb491d53c6af9d25ba8061146a6232ea616ac Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sat, 25 Jul 2026 22:02:41 +0200 Subject: [PATCH 23/37] fix aligner issue --- R/registration.R | 122 +++++++---------------------------------------- 1 file changed, 16 insertions(+), 106 deletions(-) diff --git a/R/registration.R b/R/registration.R index e3123a35..0253600f 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3880,35 +3880,26 @@ getSimpleITKAutomatedRegistration <- function( # moving <- SimpleITK::ReadImage(file.path(tmpdir, "query_image.tiff"), # 'sitkUInt8') fixed <- convertToSitkImage(ref_image) - SimpleITK::Cast(fixed, "sitkUInt8") + fixed <- SimpleITK::Cast(fixed, "sitkUInt8") moving <- convertToSitkImage(query_image) - SimpleITK::Cast(moving, "sitkUInt8") - mask <- SimpleITK::as.image(array(mask, rev(dim(mask)))) - SimpleITK::Cast(mask, "sitkUInt8") + moving <- SimpleITK::Cast(moving, "sitkUInt8") + # mask <- SimpleITK::as.image(array(mask, rev(dim(mask)))) + mask <- SimpleITK::as.image(array(as.integer(mask != 0L), + rev(dim(mask)))) + mask <- SimpleITK::Cast(mask, "sitkUInt8") # get registration for image elx <- SimpleITK::ElastixImageFilter() elx$SetOutputDirectory(tmpdir) elx$SetFixedImage(fixed) elx$SetMovingImage(moving) + elx$SetMovingMask(mask) parameterMapVector = SimpleITK::VectorOfParameterMap() mp <- SimpleITK:::ReadParameterFile( system.file("extdata", "bspline_map.txt", package = "VoltRon") ) - - # grid_config <- makeGridSamplingSchedule( - # fixed_image = fixed, - # parameter_map = mp, - # target_samples = 50000L - # ) - # mp$set( - # "SampleGridSpacing", - # grid_config$parameter_values - # ) - elx$SetParameterMap(mp) - # elx$LogToConsoleOff() - elx$LogToConsoleOn() + elx$LogToConsoleOff() tmp <- elx$Execute() sitk_img <- SimpleITK::ReadImage(file.path(tmpdir, "result.0.tif")) arr <- SimpleITK::as.array(sitk_img) @@ -3922,9 +3913,13 @@ getSimpleITKAutomatedRegistration <- function( tfx_image$SetTransformParameterMap(transform_param_map) # warp mask - tfx_image$SetMovingImage(mask) - tmp <- tfx_image$Execute() - aligned_mask <- SimpleITK::as.array(tfx_image$GetResultImage()) + tfx_mask <- SimpleITK::TransformixImageFilter() + tfx_mask$LogToConsoleOff() + tfx_mask$SetTransformParameterMap(transform_param_map) + mask$CopyInformation(moving) + tfx_mask$SetMovingImage(mask) + tmp <- tfx_mask$Execute() + aligned_mask <- SimpleITK::as.array(tfx_mask$GetResultImage()) aligned_mask <- array(aligned_mask, dim = c(dim(aligned_mask), 1)) aligned_mask <- aperm(aligned_mask, c(2,1,3)) aligned_mask <- magick::image_read(aligned_mask) @@ -3933,6 +3928,7 @@ getSimpleITKAutomatedRegistration <- function( elx <- SimpleITK::ElastixImageFilter() elx$SetOutputDirectory(tmpdir) elx$SetFixedImage(moving) + elx$SetFixedMask(mask) elx$SetMovingImage(fixed) parameterMapVector = SimpleITK::VectorOfParameterMap() mp <- SimpleITK:::ReadParameterFile( @@ -3971,92 +3967,6 @@ getSimpleITKAutomatedRegistration <- function( ))) } -makeGridSamplingSchedule <- function( - fixed_image, - parameter_map, - target_samples = 50000L, - number_of_resolutions = 10 -) { - image_size <- as.numeric(fixed_image$GetSize()) - - if (!is.numeric(target_samples) || - length(target_samples) != 1L || - !is.finite(target_samples) || - target_samples < 1) { - stop("'target_samples' must be a positive number.") - } - - number_of_dimensions <- length(image_size) - - # Default ITK schedule: - # nres = 4 -> 8, 4, 2, 1 - shrink_factors <- 2^rev( - seq.int(0L, number_of_resolutions - 1L) - ) - pyramid_schedule <- matrix( - rep(shrink_factors, each = number_of_dimensions), - nrow = number_of_resolutions, - ncol = number_of_dimensions, - byrow = TRUE - ) - - # ITK calculates the pyramid image size as: - # floor(original size / shrink factor), with a minimum of 1. - original_sizes <- matrix( - rep(image_size, times = number_of_resolutions), - nrow = number_of_resolutions, - ncol = number_of_dimensions, - byrow = TRUE - ) - - level_sizes <- floor(original_sizes / pyramid_schedule) - level_sizes[level_sizes < 1] <- 1 - - level_pixels <- apply(level_sizes, 1L, prod) - - # At small/coarse levels, use all pixels. - # At large/fine levels, choose spacing to stay around target_samples. - effective_target <- pmin( - as.double(target_samples), - level_pixels - ) - - spacing <- as.integer( - pmax( - 1, - ceiling(sqrt(level_pixels / effective_target)) - ) - ) - - spacing_matrix <- cbind(spacing, spacing) - - # Exact expected count before any mask filtering. - expected_samples <- - ( - 1 + (level_sizes[, 1] - 1) %/% spacing_matrix[, 1] - ) * - ( - 1 + (level_sizes[, 2] - 1) %/% spacing_matrix[, 2] - ) - - list( - # Elastix requires: - # sx0 sy0 sx1 sy1 ... - parameter_values = as.character(c(t(spacing_matrix))), - - report = data.frame( - resolution = seq_len(number_of_resolutions) - 1L, - pyramid_width = level_sizes[, 1], - pyramid_height = level_sizes[, 2], - shrink_x = pyramid_schedule[, 1], - shrink_y = pyramid_schedule[, 2], - grid_spacing_x = spacing_matrix[, 1], - grid_spacing_y = spacing_matrix[, 2], - expected_samples = expected_samples - ) - ) -} - #### # Non-interactive Image Registration #### #### From 798b990e82df7a329194284b7b5e2929965e2227 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sat, 25 Jul 2026 22:36:36 +0200 Subject: [PATCH 24/37] add seed to simpleitk alignment --- R/registration.R | 2 +- inst/extdata/bspline_map.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/R/registration.R b/R/registration.R index 0253600f..b458a3d9 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3893,7 +3893,7 @@ getSimpleITKAutomatedRegistration <- function( elx$SetOutputDirectory(tmpdir) elx$SetFixedImage(fixed) elx$SetMovingImage(moving) - elx$SetMovingMask(mask) + # elx$SetMovingMask(mask) parameterMapVector = SimpleITK::VectorOfParameterMap() mp <- SimpleITK:::ReadParameterFile( system.file("extdata", "bspline_map.txt", package = "VoltRon") diff --git a/inst/extdata/bspline_map.txt b/inst/extdata/bspline_map.txt index 12340a04..4b706fa8 100644 --- a/inst/extdata/bspline_map.txt +++ b/inst/extdata/bspline_map.txt @@ -34,4 +34,5 @@ (WriteResultImage "true") (WriteTransformParametersEachResolution "true") (ResultImageFormat "tif") -(ResultImagePixelType "unsigned char") \ No newline at end of file +(ResultImagePixelType "unsigned char") +(RandomSeed 121212) \ No newline at end of file From 2abeb0abf0d3a74ec5ec38ee8b982d4c96b6ce40 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sat, 25 Jul 2026 23:51:38 +0200 Subject: [PATCH 25/37] update NEWS, remove surplus code --- NEWS.md | 8 +++++--- src/automated_registration.cpp | 11 ----------- src/auxiliary.cpp | 13 ------------- src/image.cpp | 4 +--- src/matte_mi.cpp | 20 -------------------- src/metrics.cpp | 14 -------------- 6 files changed, 6 insertions(+), 64 deletions(-) diff --git a/NEWS.md b/NEWS.md index 036500fa..2fdeef9c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,13 +1,15 @@ # VoltRon 0.2.7 -- `transferData` now allows integrating single cell data object (`Seurat` or `SingleCellExperiment`) - for transfering features (e.g. gene expression) or metadata features (e.g. cell types, annotations). - `registerSpatialData` now allows registering assays with no images. In case of either one of the assays, do not have images, assays can be registered with only the manual approach. +- The Shiny interface for `registerSpatialData` now reports on the accuracy of the alignment using multiple + metrics such as Intersection, Bhattacharyya and Matte's Mutual Information. - An image-free alignment tutorial has been added where DBIT-Seq and a QuPath processed mIF experiment are aligned using manually selected landmarks. +- `transferData` now allows integrating single cell data object (`Seurat` or `SingleCellExperiment`) + for transfering features (e.g. gene expression) or metadata features (e.g. cell types, annotations). - Now `importImageData` and `importQuPathIF` functions only work with segments already converted to a - list by the user, or the `generateSegments` function whose arguement is an **sf** object. + list by the user, or the `generateSegments` function whose argument is an **sf** object. - The `formAssay` function now accepts only segments without the user manually generating coordinates (or centroids). - The `name` argument in functions like `vrImages`, `vrCoordinates` and `vrSegments` is replaced diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index fae92572..e53f65fb 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -705,17 +705,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, im2Proc.rows, im2Proc.cols, cv::INTER_LINEAR); im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, im2Proc.rows, im2Proc.cols, cv::INTER_LINEAR); - - // // warp overlap mask - // alignmentMask = warpTPSImage(im2Proc, alignmentMask, tps, - // im2.rows, im2.cols, - // cv::INTER_NEAREST); - // - // // transform image using trained tps - // im1Proc = warpTPSImage(im2Proc, im1Proc, tps, - // im2.rows, im2.cols, cv::INTER_LINEAR); - // im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, - // im2.rows, im2.cols, cv::INTER_LINEAR); // // determine extension limits for both images // int y_max = max(im1Proc.rows, im2.rows); diff --git a/src/auxiliary.cpp b/src/auxiliary.cpp index 445ba29f..4a128a35 100644 --- a/src/auxiliary.cpp +++ b/src/auxiliary.cpp @@ -80,19 +80,6 @@ Rcpp::IntegerVector matToMask(const cv::Mat &mat) { return intvec; } -// // Function to convert a cv::Mat object to a RawVector for magick images -// Rcpp::RawVector matToMask(const cv::Mat &mat) { -// -// // Create RawVector object -// Rcpp::RawVector rawvec(mat.total() * mat.elemSize()); -// rawvec.attr("dim") = Rcpp::Dimension(mat.rows, mat.cols); -// -// // Copy Mat data to RawVector -// std::memcpy(rawvec.begin(), mat.data, rawvec.size()); -// -// return rawvec; -// } - // Function to convert a NumericMatrix object to a cv::Mat cv::Mat numericMatrixToMat(Rcpp::NumericMatrix nm) { cv::Mat m(nm.rows(), nm.cols(), CV_64F); diff --git a/src/image.cpp b/src/image.cpp index 8861674f..030b23a8 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -207,9 +207,7 @@ void warpImage(cv::Mat& ref_image, // transform image using trained tps im_temp = warpTPSImage(ref_image, query_image, tps, ref_image.rows, ref_image.cols, cv::INTER_LINEAR); - // warpTPSImage(ref_image, query_image, tps, cv::INTER_LINEAR); - // im_temp = query_image; - + // // determine extension limits for both images // int y_max = max(query_image.rows, ref_image.rows); // int x_max = max(query_image.cols, ref_image.cols); diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index f43fdf19..3126ed25 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -419,26 +419,6 @@ cv::Mat1d MatteMIMap(const cv::Mat& fixed, const bool insideMask = maskRow == nullptr || static_cast(maskRow[x]); - // if(!std::isfinite(fixedRow[x])){ - // Rcout << "Fixed: "<< fixedRow[x] << endl; - // } - // if(!std::isfinite(movingRow[x])){ - // Rcout << "Moving: "<< movingRow[x] << endl; - // } - - // if(insideMask){ - // temp_counter += 1; - // if (temp_counter % 100 == 0){ - // Rcout << "Fixed: "<< fixedRow[x] << endl; - // Rcout << "Moving: "<< movingRow[x] << endl; - // } - // } - - // temp_counter += 1; - // if (temp_counter % 100 == 0){ - // Rcout << "Mask: "<< maskRow[x] << endl; - // } - const bool valid = insideMask && std::isfinite(fixedRow[x]) && diff --git a/src/metrics.cpp b/src/metrics.cpp index 9cd26830..fc588430 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -110,20 +110,6 @@ void maskKeypoints(std::vector &keypoints1_good, std::vector &keypoints1_good, std::vector &keypoints2_good, -// std::vector &keypoints1_masked, std::vector &keypoints2_masked, -// Mat &mask) -// { -// int j=0; -// for (int i = 0; i < mask.rows; i++) { -// if (mask.at(i)) { -// keypoints1_masked.push_back(keypoints1_good[i]); -// keypoints2_masked.push_back(keypoints2_good[i]); -// j++; -// } -// } -// } - // check if keypoints are degenerate bool checkDegenerate(double pts1, double pts2) { From 78a090070b5330b41523944a374ef002ee08e2a0 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 27 Jul 2026 12:00:31 +0200 Subject: [PATCH 26/37] return degenerate info --- R/auxiliary.R | 11 ++++++----- src/automated_registration.cpp | 1 + src/metrics.cpp | 33 +++++++++++++++------------------ 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/R/auxiliary.R b/R/auxiliary.R index 4bb8a124..7af6232a 100644 --- a/R/auxiliary.R +++ b/R/auxiliary.R @@ -87,11 +87,12 @@ fixVoltRon <- function(object) { .ALIGNMENT_KEYPOINT_METRICS <- c( "#Keypoints", - "Inlier Ratio", - "Std. dev. (ref. keypoints)", - "Std. dev. (query keypoints)", - "Std. dev. (grid points)", - "Median distance" + "Inlier Perc.", + "sd ref. kpts (>1?)", + "sd query kpts (>1?)", + "sd grid (in [w,h]?)", + "Median distance", + "Degenerate" ) #### diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index e53f65fb..9f406f4a 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -636,6 +636,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, std::map keypoint_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, im1Proc, im2Proc, h, mask); + is_faulty = (bool) keypoint_metrics["Degenerate"]; // get alignment metrics std::map image_metrics; diff --git a/src/metrics.cpp b/src/metrics.cpp index fc588430..059536fa 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -48,14 +48,7 @@ double checkMappedGridDistribution(Mat &im, Mat &h){ } // Compute the standard deviation of the transformed points - double gridpoints_reg_sd = cppSD(gridpoints_reg); - - // get warning message - if(gridpoints_reg_sd < 1.0 | gridpoints_reg_sd > max(height, width)){ - Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; - } - - return gridpoints_reg_sd; + return cppSD(gridpoints_reg); } bool checkMaskAbundance(Mat &mask){ @@ -337,28 +330,29 @@ std::map getKeypointMetrics(std::vector &point // get inlier percentages double ratio = checkInlierPercentage(mask); Rcout << " Inlier Percentage: " << ratio << endl; - metrics["Inlier Ratio"] = ratio; + metrics["Inlier Perc."] = ratio; // points stand. dev. double points1_sd = cppSD(points1); double points2_sd = cppSD(points2); - if(points1_sd < 1.0 | points2_sd < 1.0){ - Rcout << " WARNING: points may be in a degenerate configuration." << endl; - } Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; - metrics["Std. dev. (ref. keypoints)"] = points1_sd; - metrics["Std. dev. (query keypoints)"] = points2_sd; + metrics["sd ref. kpts (>1?)"] = points1_sd; + metrics["sd query kpts (>1?)"] = points2_sd; // degenerate ? - bool degenerate = checkDegenerate(points1_sd, points2_sd); - Rcout << "Registration is " << (degenerate ? "degenerate!" : "not degenerate!") << endl; + bool degenerate_points = checkDegenerate(points1_sd, points2_sd); + metrics["Degenerate"] = (double) degenerate_points; // check distribution of points double stddev = checkMappedGridDistribution(im2, h); Rcout << " Std dev of registered points: " << stddev << endl; - metrics["Std. dev. (grid points)"] = stddev; + if(stddev < 1.0 | stddev > max(im1.rows, im1.cols)){ + Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; + metrics["Degenerate"] = 1.0; + } + metrics["sd grid (in [w,h]?)"] = stddev; - // warp keypoints and compare + // warp keypoints and check median distances double md = medianMappingDistance(points1, points2, h); Rcout << " Median distance between points: " << md << endl; if(md > 3){ @@ -366,6 +360,9 @@ std::map getKeypointMetrics(std::vector &point } metrics["Median distance"] = md; + // report degenerate + Rcout << " WARNING: Registration is " << ((bool) metrics["Degenerate"] ? "degenerate!" : "not degenerate!") << endl; + // return is_degenerate; return metrics; } \ No newline at end of file From cbb56d4165b2e4463cdc915e9b2c3f838be88acb Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 27 Jul 2026 12:08:22 +0200 Subject: [PATCH 27/37] dont let simpleitk used in non-rigid only mode --- R/registration.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/registration.R b/R/registration.R index b458a3d9..8baddf24 100644 --- a/R/registration.R +++ b/R/registration.R @@ -844,7 +844,7 @@ updateParameterPanels <- function(len_images, params, input, output, session) { "nonrigid", choices = c( "TPS (OpenCV)", - "BSpline (SimpleITK)" + if(input$Method != "Non-Rigid") "BSpline (SimpleITK)" else NULL ), selected = "TPS (OpenCV)" ) From ba43b092b1d993c1b326aebf2c89ee3c95da8ce3 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 27 Jul 2026 13:57:33 +0200 Subject: [PATCH 28/37] update degenerate reporting --- src/metrics.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/metrics.cpp b/src/metrics.cpp index 059536fa..be038c29 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -361,7 +361,9 @@ std::map getKeypointMetrics(std::vector &point metrics["Median distance"] = md; // report degenerate - Rcout << " WARNING: Registration is " << ((bool) metrics["Degenerate"] ? "degenerate!" : "not degenerate!") << endl; + if((bool) metrics["Degenerate"]){ + Rcout << " WARNING: Registration is degenerate!") << endl; + } // return is_degenerate; return metrics; From cba7ad7fa9b354a7fbb93b27aed924045209d039 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 27 Jul 2026 14:37:34 +0200 Subject: [PATCH 29/37] some corrections based on codex review --- R/registration.R | 19 ++++++++++++++----- src/automated_registration.cpp | 2 +- src/manual_registration.cpp | 3 +-- src/metrics.cpp | 6 +++--- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/R/registration.R b/R/registration.R index 8baddf24..54dfbcd2 100644 --- a/R/registration.R +++ b/R/registration.R @@ -627,7 +627,7 @@ getAlignmentTabPanel <- function(len_images, centre, register_ind) { br(), tabsetPanel( - id = "inner_tabs", + id = paste("inner_tabs", "i"), tabPanel("Alignment Stat.", tableOutput(paste0("alignment_stats", i))), tabPanel("Matte's MI Map", @@ -3237,6 +3237,8 @@ getRcppManualRegistration <- function( # check for null data if(length(reg) > 2){ + + # check matte map matte_map <- if (!is.null(reg[[3]])) { tmp <- reg[[3]] @@ -3245,7 +3247,7 @@ getRcppManualRegistration <- function( tmp } else NA - # alignment accuracy + # check alignment statistics alignment_stats <- list() metrics <- .ALIGNMENT_ACCURACY_METRICS metrics_set <- setNames(rep(NA, length(metrics)), metrics) @@ -3746,9 +3748,17 @@ getRcppAutomatedRegistration <- function( overlay_image <- if (!is.null(reg[[5]])) magick::image_read(reg[[5]]) else NA - # check alignment accuracy and matte maps + # check matte maps matte_map <- - if (!is.null(reg[[6]])) reg[[6]] else NA + if (!is.null(reg[[6]])){ + tmp <- reg[[6]] + tmp[is.na(tmp)] <- 0 + tmp[tmp < 0] <- 0 + tmp + + } else NA + + # check alignment statistics alignment_stats <- list() metrics <- c(.ALIGNMENT_ACCURACY_METRICS, .ALIGNMENT_KEYPOINT_METRICS) @@ -3766,7 +3776,6 @@ getRcppAutomatedRegistration <- function( } } alignment_stats[["fine"]] <- { - # metrics <- .ALIGNMENT_ACCURACY_METRICS if (!is.null(reg[[8]])){ if(!all(names(reg[[8]]) %in% metrics)){ stop("There are missing accuracy metrics!") diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 9f406f4a..aac1652e 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -631,7 +631,7 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, Rcout << "WARNING: No transformation was found" << endl; return; } - + // get keypoint metrics std::map keypoint_metrics; keypoint_metrics = getKeypointMetrics(points1, points2, diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 0ecd4b1a..9e368443 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -224,11 +224,10 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // im1Reg = im1Reg_cropped.clone(); // get matte metric, process + // im2 is already processed Mat im1Proc, im2Proc; cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); - cvtColor(im2, im2Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); - im2Proc = preprocessImage(im2Proc, invert_ref, "None", "0"); accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } diff --git a/src/metrics.cpp b/src/metrics.cpp index be038c29..dcc48a33 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -336,8 +336,8 @@ std::map getKeypointMetrics(std::vector &point double points1_sd = cppSD(points1); double points2_sd = cppSD(points2); Rcout << " Std dev of points: x=" << points1_sd << " y=" << points2_sd << endl; - metrics["sd ref. kpts (>1?)"] = points1_sd; - metrics["sd query kpts (>1?)"] = points2_sd; + metrics["sd query kpts (>1?)"] = points1_sd; + metrics["sd ref. kpts (>1?)"] = points2_sd; // degenerate ? bool degenerate_points = checkDegenerate(points1_sd, points2_sd); @@ -362,7 +362,7 @@ std::map getKeypointMetrics(std::vector &point // report degenerate if((bool) metrics["Degenerate"]){ - Rcout << " WARNING: Registration is degenerate!") << endl; + Rcout << " WARNING: Registration is degenerate!" << endl; } // return is_degenerate; From e2357050ba1c819f72321ccb988cc007507cc988 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 27 Jul 2026 15:40:34 +0200 Subject: [PATCH 30/37] drop float operations from matte mi calculation --- src/matte_mi.cpp | 1110 ++++++++++++++-------------------------------- src/matte_mi.h | 47 +- 2 files changed, 341 insertions(+), 816 deletions(-) diff --git a/src/matte_mi.cpp b/src/matte_mi.cpp index 3126ed25..aa55ba23 100644 --- a/src/matte_mi.cpp +++ b/src/matte_mi.cpp @@ -1,40 +1,48 @@ -#include +#include "matte_mi.h" + +#include +#include +#include +#include +#include +#include #include +#include +#include + #include -// Internal functions -#include "auxiliary.h" -#include "image.h" +namespace { -// Namespaces -using namespace Rcpp; -using namespace std; -using namespace cv; +using Pixel = unsigned char; struct IntensityRange { double min; double max; }; -//// -// Chunk wise Matte MI -//// +struct ChunkSize { + int height = 50; + int width = 50; +}; + +struct GlobalCounts { + std::array fixed{}; + std::array moving{}; + std::size_t validPairs = 0U; +}; -double cubicBSpline(double u) { +// Source pixels remain CV_8U. All Mattes arithmetic remains double. +double cubicBSpline(double u) noexcept { u = std::abs(u); - if (u < 1.0) { const double u2 = u * u; - const double u3 = u2 * u; - - return (4.0 - 6.0 * u2 + 3.0 * u3) / 6.0; + return (4.0 - 6.0 * u2 + 3.0 * u2 * u) / 6.0; } - if (u < 2.0) { const double t = 2.0 - u; - return (t * t * t) / 6.0; + return t * t * t / 6.0; } - return 0.0; } @@ -43,518 +51,332 @@ double scaleToBinPosition( IntensityRange range, double low, double high) { - if (!std::isfinite(range.min) || !std::isfinite(range.max) || !(range.max > range.min)) { - throw std::invalid_argument("Invalid intensity range."); + throw std::invalid_argument("Invalid intensity range."); } - value = std::clamp(value, range.min, range.max); - - return low + - (value - range.min) * (high - low) / - (range.max - range.min); + return low + (value - range.min) * (high - low) / + (range.max - range.min); } -std::size_t roundToNearestEvenNonnegative(double x) { - const double lowerAsDouble = std::floor(x); - const double fraction = x - lowerAsDouble; - const auto lower = static_cast(lowerAsDouble); - - if (fraction < 0.5) { - return lower; - } - - if (fraction > 0.5) { - return lower + 1U; - } - +std::size_t roundToNearestEvenNonnegative(double x) noexcept { + const double lowerDouble = std::floor(x); + const double fraction = x - lowerDouble; + const auto lower = static_cast(lowerDouble); + if (fraction < 0.5) return lower; + if (fraction > 0.5) return lower + 1U; return (lower % 2U == 0U) ? lower : lower + 1U; } -bool isValidRange(IntensityRange range) { +bool isValidRange(IntensityRange range) noexcept { return std::isfinite(range.min) && std::isfinite(range.max) && range.max > range.min; } -/** - * Compute Mattes-style mutual information from paired sample values. - * - * Fixed samples use nearest-bin assignment. - * Moving samples use cubic B-spline Parzen smoothing. - * - * @param fixedValues Pointer to fixed-image sample values. - * @param movingValues Pointer to corresponding moving-image values. - * @param countPair Number of paired values. - * @param bins Number of histogram bins; must be >= 4. - * @param fixedRange Optional fixed intensity range. - * @param movingRange Optional moving intensity range. - * - * @return Mutual information in nats, or quiet NaN for degenerate data. - */ -double mattesMiFromValues( - const double* fixedValues, - const double* movingValues, - std::size_t countPair, - std::size_t bins = 64, - std::optional fixedRange = std::nullopt, - std::optional movingRange = std::nullopt) { - - const double nan = - std::numeric_limits::quiet_NaN(); - - if (countPair != 0U && - (fixedValues == nullptr || movingValues == nullptr)) { - throw std::invalid_argument("Input value pointer is null."); +int ceilDividePositive(int value, int divisor) noexcept { + return value / divisor + ((value % divisor) != 0 ? 1 : 0); +} + +void validateInputs( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins) { + if (fixed.empty() || moving.empty()) { + throw std::invalid_argument( + "fixed and moving images must not be empty."); } - - /* - * First pass: - * - count finite sample pairs - * - calculate automatic intensity ranges - */ - std::size_t validCount = 0; - - double fixedMin = std::numeric_limits::infinity(); - double fixedMax = -std::numeric_limits::infinity(); - - double movingMin = std::numeric_limits::infinity(); - double movingMax = -std::numeric_limits::infinity(); - - for (std::size_t i = 0; i < countPair; ++i) { - - if (!std::isfinite(fixedValues[i]) || - !std::isfinite(movingValues[i])) { - continue; - } - - ++validCount; - - fixedMin = std::min(fixedMin, fixedValues[i]); - fixedMax = std::max(fixedMax, fixedValues[i]); - - movingMin = std::min(movingMin, movingValues[i]); - movingMax = std::max(movingMax, movingValues[i]); + if (fixed.type() != CV_8UC1 || moving.type() != CV_8UC1) { + throw std::invalid_argument( + "The zero-conversion implementation expects fixed and moving " + "to be CV_8UC1."); } - - // Preserve the Python function's validation order. - if (validCount < 2U) { - return nan; + if (fixed.size() != moving.size()) { + throw std::invalid_argument( + "fixed and moving must have the same dimensions."); } - - if (bins < 4U) { + if (!mask.empty() && + (mask.type() != CV_8UC1 || mask.size() != fixed.size())) { throw std::invalid_argument( - "bins must be >= 4 for cubic B-spline smoothing."); + "mask must be empty or CV_8UC1 with the same dimensions."); } - - if (bins > - std::numeric_limits::max() / bins || - bins > - static_cast( - std::numeric_limits::max())) { - throw std::length_error( - "Histogram dimensions are too large."); + if (bins < 4) { + throw std::invalid_argument( + "bins must be >= 4 for cubic B-spline smoothing."); } - - /* - * Row: fixed-image bin - * Column: moving-image bin - */ - std::vector jointHistogram( - bins * bins, - 0.0); - - /* - * Second pass: construct the joint histogram. - */ - for (std::size_t i = 0; i < countPair; ++i) { - const double fixedValue = static_cast(fixedValues[i]); - const double movingValue = static_cast(movingValues[i]); - - if (!std::isfinite(fixedValue) || - !std::isfinite(movingValue)) { - continue; - } - - /* - * Fixed image: - * map to [0, bins - 1] and use nearest-bin assignment. - */ - const double fixedPosition = - scaleToBinPosition( - fixedValue, - *fixedRange, - 0.0, - static_cast(bins - 1U)); - - std::size_t fixedBin = - roundToNearestEvenNonnegative( - fixedPosition); - - fixedBin = std::min( - fixedBin, - bins - 1U); - - /* - * Moving image: - * map to [1, bins - 2] so the cubic kernel has - * room at both ends of the histogram. - */ - const double movingPosition = - scaleToBinPosition( - movingValue, - *movingRange, - 1.0, - static_cast(bins - 2U)); - - const auto baseBin = - static_cast( - std::floor(movingPosition)); - - /* - * A cubic B-spline contributes to at most four bins. - */ - for (int offset = -1; offset <= 2; ++offset) { - const std::ptrdiff_t movingBin = - baseBin + offset; - - if (movingBin < 0 || - movingBin >= - static_cast(bins)) { - continue; - } - - const double weight = - cubicBSpline(movingPosition - static_cast(movingBin)); - - if (weight <= 0.0) { - continue; - } - - jointHistogram[ - fixedBin * bins + - static_cast(movingBin) - ] += weight; +} + +GlobalCounts collectGlobalCounts( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask) { + GlobalCounts out; + for (int y = 0; y < fixed.rows; ++y) { + const Pixel* fixedRow = fixed.ptr(y); + const Pixel* movingRow = moving.ptr(y); + const Pixel* maskRow = mask.empty() ? nullptr : mask.ptr(y); + for (int x = 0; x < fixed.cols; ++x) { + if (maskRow != nullptr && maskRow[x] == 0U) continue; + ++out.fixed[fixedRow[x]]; + ++out.moving[movingRow[x]]; + ++out.validPairs; } } - - const double total = - std::accumulate( - jointHistogram.begin(), - jointHistogram.end(), - 0.0); - - if (!(total > 0.0)) { - return nan; + return out; +} + +Pixel valueAtRank( + const std::array& counts, + std::size_t rank) { + std::size_t cumulative = 0U; + for (std::size_t value = 0U; value < counts.size(); ++value) { + cumulative += counts[value]; + if (rank < cumulative) return static_cast(value); + } + throw std::out_of_range("Percentile rank is out of range."); +} + +// Exact NumPy-style linear percentile for CV_8U values, without sorting pixels. +double percentileFromCounts( + const std::array& counts, + std::size_t count, + double percentile) { + if (count == 0U) { + throw std::invalid_argument("Cannot calculate an empty percentile."); + } + if (!std::isfinite(percentile) || percentile < 0.0 || percentile > 100.0) { + throw std::invalid_argument("Percentile must be in [0, 100]."); + } + if (count == 1U) return static_cast(valueAtRank(counts, 0U)); + + const double index = static_cast(count - 1U) * percentile / 100.0; + const auto lowerIndex = static_cast(std::floor(index)); + const auto upperIndex = static_cast(std::ceil(index)); + const double fraction = index - static_cast(lowerIndex); + const double lower = static_cast(valueAtRank(counts, lowerIndex)); + const double upper = static_cast(valueAtRank(counts, upperIndex)); + return lower + fraction * (upper - lower); +} + +IntensityRange minMaxRangeFromCounts( + const std::array& counts, + std::size_t count) { + if (count == 0U) { + const double nan = std::numeric_limits::quiet_NaN(); + return {nan, nan}; + } + std::size_t minimum = 0U; + while (minimum < counts.size() && counts[minimum] == 0U) ++minimum; + std::size_t maximum = counts.size() - 1U; + while (maximum > 0U && counts[maximum] == 0U) --maximum; + return { + static_cast(minimum), + static_cast(maximum) + }; +} + +void addMattesPair( + double fixedValue, + double movingValue, + IntensityRange fixedRange, + IntensityRange movingRange, + std::size_t bins, + std::vector& jointHistogram) { + const double fixedPosition = scaleToBinPosition( + fixedValue, fixedRange, 0.0, static_cast(bins - 1U)); + std::size_t fixedBin = roundToNearestEvenNonnegative(fixedPosition); + fixedBin = std::min(fixedBin, bins - 1U); + + const double movingPosition = scaleToBinPosition( + movingValue, movingRange, 1.0, static_cast(bins - 2U)); + const auto baseBin = static_cast(std::floor(movingPosition)); + + for (int offset = -1; offset <= 2; ++offset) { + const std::ptrdiff_t movingBin = baseBin + offset; + if (movingBin < 0 || movingBin >= static_cast(bins)) { + continue; + } + const double weight = cubicBSpline( + movingPosition - static_cast(movingBin)); + if (weight <= 0.0) continue; + jointHistogram[ + fixedBin * bins + static_cast(movingBin) + ] += weight; } - - /* - * Marginal distributions. - */ +} + +double mutualInformationFromHistogram( + const std::vector& jointHistogram, + std::size_t bins) { + const double nan = std::numeric_limits::quiet_NaN(); + const double total = std::accumulate( + jointHistogram.begin(), jointHistogram.end(), 0.0); + if (!(total > 0.0) || !std::isfinite(total)) return nan; + std::vector px(bins, 0.0); std::vector py(bins, 0.0); - - for (std::size_t fixedBin = 0; - fixedBin < bins; - ++fixedBin) { - - for (std::size_t movingBin = 0; - movingBin < bins; - ++movingBin) { - - const double pxy = - jointHistogram[ - fixedBin * bins + movingBin + + for (std::size_t fixedBin = 0; fixedBin < bins; ++fixedBin) { + for (std::size_t movingBin = 0; movingBin < bins; ++movingBin) { + const double pxy = jointHistogram[ + fixedBin * bins + movingBin ] / total; - px[fixedBin] += pxy; py[movingBin] += pxy; } } - - /* - * MI = sum p(x,y) log(p(x,y) / (p(x)p(y))) - */ + double mi = 0.0; - - for (std::size_t fixedBin = 0; - fixedBin < bins; - ++fixedBin) { - - for (std::size_t movingBin = 0; - movingBin < bins; - ++movingBin) { - - const double pxy = - jointHistogram[ - fixedBin * bins + movingBin + for (std::size_t fixedBin = 0; fixedBin < bins; ++fixedBin) { + for (std::size_t movingBin = 0; movingBin < bins; ++movingBin) { + const double pxy = jointHistogram[ + fixedBin * bins + movingBin ] / total; - - const double pxPy = - px[fixedBin] * py[movingBin]; - + const double pxPy = px[fixedBin] * py[movingBin]; if (pxy > 0.0 && pxPy > 0.0) { - mi += - pxy * std::log(pxy / pxPy); + mi += pxy * std::log(pxy / pxPy); } } } - - // std::log is the natural logarithm, so MI is in nats. return mi; } -//// -// Matte MI -//// - -/** - * Spatial chunk dimensions. - * - * The field order intentionally follows the Python API: - * ChunkSize{height, width} - * - * This avoids cv::Size's opposite (width, height) ordering. - */ -struct ChunkSize { - int height = 50; - int width = 50; -}; - -/** - * Output of chunkedNmiMap(). - * - * Despite the legacy nmiMap name, the values are Mattes-style mutual - * information values, not normalized mutual information values. - * - * Matrix layouts: - * nmiMap : CV_64FC1, shape [chunk rows, chunk columns] - * bounds : CV_32SC4, each element is [y0, y1, x0, x1] - * centers : CV_64FC2, each element is [y center, x center] - */ -struct ChunkedNmiMapResult { - cv::Mat1d nmiMap; - cv::Mat_ bounds; - cv::Mat_ centers; -}; - -int ceilDividePositive(int value, int divisor) noexcept { - return value / divisor + ((value % divisor) != 0 ? 1 : 0); -} - -double linearPercentileFromSorted( - const std::vector& sortedValues, - double percentile) { - if (sortedValues.empty()) { - throw std::invalid_argument( - "Cannot calculate a percentile of an empty array."); +double mattesMiFromValues( + const Pixel* fixedValues, + const Pixel* movingValues, + std::size_t count, + std::size_t bins, + std::optional fixedRange = std::nullopt, + std::optional movingRange = std::nullopt) { + const double nan = std::numeric_limits::quiet_NaN(); + if (count != 0U && (fixedValues == nullptr || movingValues == nullptr)) { + throw std::invalid_argument("Input value pointer is null."); } - - if (!std::isfinite(percentile) || - percentile < 0.0 || percentile > 100.0) { + if (count < 2U) return nan; + if (bins < 4U) { throw std::invalid_argument( - "Percentile must be finite and in [0, 100]."); + "bins must be >= 4 for cubic B-spline smoothing."); } - - if (sortedValues.size() == 1U) { - return sortedValues.front(); + if (bins > std::numeric_limits::max() / bins) { + throw std::length_error("Histogram dimensions are too large."); } - - // Matches NumPy's default linear percentile interpolation: - // index = (N - 1) * percentile / 100. - const double index = - (static_cast(sortedValues.size() - 1U) * percentile) / - 100.0; - - const auto lowerIndex = - static_cast(std::floor(index)); - const auto upperIndex = - static_cast(std::ceil(index)); - const double fraction = index - static_cast(lowerIndex); - - const double lower = sortedValues[lowerIndex]; - const double upper = sortedValues[upperIndex]; - - return lower + fraction * (upper - lower); -} -// ChunkedNmiMapResult chunkedMatteMIMap(const cv::Mat& fixed, -cv::Mat1d MatteMIMap(const cv::Mat& fixed, - const cv::Mat& moving, - const cv::Mat& mask, - int bins = 50) { - - ChunkSize chunkSize = ChunkSize{}; - - // Do I need these to be cv_64f ? - cv::Mat fixed64; - cv::Mat moving64; - fixed.convertTo(fixed64, CV_64F); - moving.convertTo(moving64, CV_64F); - - // Do I need these to be cv_64f ? - cv::Mat mask64; - if (!mask.empty()) { - mask.convertTo(mask64, CV_64F); - } - - // validate - const int height = fixed.rows; - const int width = fixed.cols; - - cv::Mat1b validGlobal(height, width, static_cast(0)); - std::size_t validGlobalCount = 0U; - - int temp_counter = 0; - for (int y = 0; y < height; ++y) { - const double* fixedRow = fixed64.ptr(y); - const double* movingRow = moving64.ptr(y); - const double* maskRow = - mask64.empty() ? nullptr : mask64.ptr(y); - unsigned char* validRow = validGlobal.ptr(y); - - for (int x = 0; x < width; ++x) { - const bool insideMask = - maskRow == nullptr || static_cast(maskRow[x]); - - const bool valid = - insideMask && - std::isfinite(fixedRow[x]) && - std::isfinite(movingRow[x]); - - if (valid) { - validRow[x] = 1U; - ++validGlobalCount; - } + if (!fixedRange.has_value() || !movingRange.has_value()) { + std::array fixedCounts{}; + std::array movingCounts{}; + for (std::size_t i = 0; i < count; ++i) { + ++fixedCounts[fixedValues[i]]; + ++movingCounts[movingValues[i]]; } - } - - if (validGlobalCount == 0U) { - throw std::invalid_argument( - "The mask contains no valid pixels."); - } - - // Calculate one fixed-image range and one moving-image range globally, - // then reuse those ranges in every chunk. This makes chunk values - // comparable across the image. - std::vector fixedGlobalValues; - std::vector movingGlobalValues; - fixedGlobalValues.reserve(validGlobalCount); - movingGlobalValues.reserve(validGlobalCount); - - for (int y = 0; y < height; ++y) { - const double* fixedRow = fixed64.ptr(y); - const double* movingRow = moving64.ptr(y); - const double* maskRow = - mask64.empty() ? nullptr : mask64.ptr(y); - const unsigned char* validRow = - validGlobal.ptr(y); - - for (int x = 0; x < width; ++x) { - if (validRow[x] != 0U) { - fixedGlobalValues.push_back(fixedRow[x]); - movingGlobalValues.push_back(movingRow[x]); - } + if (!fixedRange.has_value()) { + fixedRange = minMaxRangeFromCounts(fixedCounts, count); } + if (!movingRange.has_value()) { + movingRange = minMaxRangeFromCounts(movingCounts, count); + } + } + + if (!isValidRange(*fixedRange) || !isValidRange(*movingRange)) return nan; + + std::vector jointHistogram(bins * bins, 0.0); + for (std::size_t i = 0; i < count; ++i) { + addMattesPair( + static_cast(fixedValues[i]), + static_cast(movingValues[i]), + *fixedRange, + *movingRange, + bins, + jointHistogram); } - - std::sort(fixedGlobalValues.begin(), fixedGlobalValues.end()); - std::sort(movingGlobalValues.begin(), movingGlobalValues.end()); - + return mutualInformationFromHistogram(jointHistogram, bins); +} + +} // namespace + +cv::Mat1d MatteMIMap( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins) { + validateInputs(fixed, moving, mask, bins); + + constexpr ChunkSize chunkSize{}; + constexpr std::size_t minValidPixels = 100U; + constexpr double minValidFraction = 0.10; constexpr double lowerPercentile = 0.5; constexpr double upperPercentile = 99.5; - + + const GlobalCounts global = collectGlobalCounts(fixed, moving, mask); + if (global.validPairs == 0U) { + throw std::invalid_argument("The mask contains no valid pixels."); + } + const IntensityRange fixedRange{ - linearPercentileFromSorted(fixedGlobalValues, lowerPercentile), - linearPercentileFromSorted(fixedGlobalValues, upperPercentile) + percentileFromCounts(global.fixed, global.validPairs, lowerPercentile), + percentileFromCounts(global.fixed, global.validPairs, upperPercentile) }; - - // Intentional correction from the pasted Python: movingRange is derived - // from moving-image values, not from fixed-image values. const IntensityRange movingRange{ - linearPercentileFromSorted(movingGlobalValues, lowerPercentile), - linearPercentileFromSorted(movingGlobalValues, upperPercentile) + percentileFromCounts(global.moving, global.validPairs, lowerPercentile), + percentileFromCounts(global.moving, global.validPairs, upperPercentile) }; - + if (!isValidRange(fixedRange)) { - throw std::invalid_argument( - "Invalid fixed intensity range."); + throw std::invalid_argument("Invalid fixed intensity range."); } - if (!isValidRange(movingRange)) { - throw std::invalid_argument( - "Invalid moving intensity range."); + throw std::invalid_argument("Invalid moving intensity range."); } - - const int nRows = ceilDividePositive(height, chunkSize.height); - const int nCols = ceilDividePositive(width, chunkSize.width); - - const double nan = - std::numeric_limits::quiet_NaN(); - - // only set nmimap, why need bounds and centers - cv::Mat1d NmiMap(nRows, nCols); - NmiMap.setTo(cv::Scalar(nan)); - // result.nmiMap.setTo(cv::Scalar(nan)); - // result.bounds.setTo(cv::Scalar::all(0)); - // result.centers.setTo(cv::Scalar::all(0)); - - constexpr std::size_t minValidPixels = 100U; - constexpr double minValidFraction = 0.10; - - std::vector fixedChunkValues; - std::vector movingChunkValues; - + + const int nRows = ceilDividePositive(fixed.rows, chunkSize.height); + const int nCols = ceilDividePositive(fixed.cols, chunkSize.width); + cv::Mat1d nmiMap(nRows, nCols); + nmiMap.setTo(cv::Scalar(std::numeric_limits::quiet_NaN())); + + const std::size_t maxChunkPixels = + static_cast(std::min(chunkSize.height, fixed.rows)) * + static_cast(std::min(chunkSize.width, fixed.cols)); + + std::vector fixedChunkValues; + std::vector movingChunkValues; + fixedChunkValues.reserve(maxChunkPixels); + movingChunkValues.reserve(maxChunkPixels); + for (int row = 0; row < nRows; ++row) { for (int col = 0; col < nCols; ++col) { const int y0 = row * chunkSize.height; const int x0 = col * chunkSize.width; - - // Written this way instead of y0 + chunkSize.height to avoid - // signed integer overflow for extreme dimensions. - const int y1 = y0 + std::min(chunkSize.height, height - y0); - const int x1 = x0 + std::min(chunkSize.width, width - x0); - + const int y1 = y0 + std::min(chunkSize.height, fixed.rows - y0); + const int x1 = x0 + std::min(chunkSize.width, fixed.cols - x0); const std::size_t totalPixels = static_cast(y1 - y0) * static_cast(x1 - x0); - + fixedChunkValues.clear(); movingChunkValues.clear(); - fixedChunkValues.reserve(totalPixels); - movingChunkValues.reserve(totalPixels); - + for (int y = y0; y < y1; ++y) { - const double* fixedRow = fixed64.ptr(y); - const double* movingRow = moving64.ptr(y); - const unsigned char* validRow = - validGlobal.ptr(y); - + const Pixel* fixedRow = fixed.ptr(y); + const Pixel* movingRow = moving.ptr(y); + const Pixel* maskRow = mask.empty() ? nullptr : mask.ptr(y); for (int x = x0; x < x1; ++x) { - if (validRow[x] != 0U) { - fixedChunkValues.push_back(fixedRow[x]); - movingChunkValues.push_back(movingRow[x]); - } + if (maskRow != nullptr && maskRow[x] == 0U) continue; + fixedChunkValues.push_back(fixedRow[x]); + movingChunkValues.push_back(movingRow[x]); } } - + const std::size_t validPixels = fixedChunkValues.size(); - - if (validPixels < minValidPixels) { - continue; - } - + if (validPixels < minValidPixels) continue; + const double validFraction = static_cast(validPixels) / - static_cast(totalPixels); - - if (validFraction < minValidFraction) { - continue; - } - - NmiMap(row, col) = mattesMiFromValues( + static_cast(totalPixels); + if (validFraction < minValidFraction) continue; + + nmiMap(row, col) = mattesMiFromValues( fixedChunkValues.data(), movingChunkValues.data(), validPixels, @@ -563,345 +385,59 @@ cv::Mat1d MatteMIMap(const cv::Mat& fixed, std::optional{movingRange}); } } - - return NmiMap; + + return nmiMap; } -const double* getRowAsDouble( - const cv::Mat& image, - int row, - cv::Mat& scratch) { - - if (image.depth() == CV_64F) { - return image.ptr(row); - } - - image.row(row).convertTo(scratch, CV_64F); - return scratch.ptr(0); +cv::Mat1d chunkedMatteMIMap( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins) { + return MatteMIMap(fixed, moving, mask, bins); } double MatteMI( const cv::Mat& fixed, const cv::Mat& moving, const cv::Mat& mask, - int bins = 50) { - - const double nan = - std::numeric_limits::quiet_NaN(); - - if (bins < 4U) { - throw std::invalid_argument( - "bins must be >= 4 for cubic B-spline smoothing."); + int bins) { + validateInputs(fixed, moving, mask, bins); + + const std::size_t binCount = static_cast(bins); + if (binCount > std::numeric_limits::max() / binCount) { + throw std::length_error("Histogram dimensions are too large."); } - - const std::size_t binCount = - static_cast(bins); - - if (binCount > - std::numeric_limits::max() / binCount) { - throw std::length_error( - "Histogram dimensions are too large."); + + const GlobalCounts global = collectGlobalCounts(fixed, moving, mask); + if (global.validPairs < 2U) { + return std::numeric_limits::quiet_NaN(); } - - /* - * Convert the mask to a conventional CV_8U binary mask. - * - * Every nonzero mask value becomes 255. - */ - cv::Mat1b mask8; - - if (!mask.empty()) { - cv::compare( - mask, - cv::Scalar::all(0), - mask8, - cv::CMP_NE); + + const IntensityRange fixedRange = + minMaxRangeFromCounts(global.fixed, global.validPairs); + const IntensityRange movingRange = + minMaxRangeFromCounts(global.moving, global.validPairs); + if (!isValidRange(fixedRange) || !isValidRange(movingRange)) { + return std::numeric_limits::quiet_NaN(); } - - const int height = fixed.rows; - const int width = fixed.cols; - - /* - * Only row-sized double buffers are needed. This avoids converting - * both complete images to CV_64F and avoids full-image value vectors. - */ - cv::Mat fixedRowScratch; - cv::Mat movingRowScratch; - - /* - * Utility that visits every valid pixel pair. - * - * This function performs no sampling. Every valid pair is delivered - * to the supplied visitor. - */ - auto visitValidPairs = [&](auto&& visitor) { - for (int y = 0; y < height; ++y) { - const double* fixedRow = - getRowAsDouble( - fixed, - y, - fixedRowScratch); - - const double* movingRow = - getRowAsDouble( - moving, - y, - movingRowScratch); - - const unsigned char* maskRow = - mask.empty() - ? nullptr - : mask8.ptr(y); - - for (int x = 0; x < width; ++x) { - if (maskRow != nullptr && - maskRow[x] == 0U) { - continue; - } - - const double fixedValue = fixedRow[x]; - const double movingValue = movingRow[x]; - - if (!std::isfinite(fixedValue) || - !std::isfinite(movingValue)) { - continue; - } - - visitor(fixedValue, movingValue); - } + + std::vector jointHistogram(binCount * binCount, 0.0); + for (int y = 0; y < fixed.rows; ++y) { + const Pixel* fixedRow = fixed.ptr(y); + const Pixel* movingRow = moving.ptr(y); + const Pixel* maskRow = mask.empty() ? nullptr : mask.ptr(y); + for (int x = 0; x < fixed.cols; ++x) { + if (maskRow != nullptr && maskRow[x] == 0U) continue; + addMattesPair( + static_cast(fixedRow[x]), + static_cast(movingRow[x]), + fixedRange, + movingRange, + binCount, + jointHistogram); } - }; - - /* - * First pass: - * determine full-image intensity ranges from every valid pixel pair. - * - * This matches the default behavior of your original Python - * _mattes_mi_from_values function when no explicit ranges are supplied. - */ - std::size_t validCount = 0U; - - double fixedMin = - std::numeric_limits::infinity(); - - double fixedMax = - -std::numeric_limits::infinity(); - - double movingMin = - std::numeric_limits::infinity(); - - double movingMax = - -std::numeric_limits::infinity(); - - visitValidPairs( - [&](double fixedValue, double movingValue) { - ++validCount; - - fixedMin = - std::min(fixedMin, fixedValue); - - fixedMax = - std::max(fixedMax, fixedValue); - - movingMin = - std::min(movingMin, movingValue); - - movingMax = - std::max(movingMax, movingValue); - }); - - if (validCount < 2U) { - return nan; - } - - const IntensityRange fixedRange{ - fixedMin, - fixedMax - }; - - const IntensityRange movingRange{ - movingMin, - movingMax - }; - - /* - * This follows the behavior of the supplied Python function: - * a constant fixed or moving image has an invalid range and returns NaN. - */ - if (!isValidRange(fixedRange) || - !isValidRange(movingRange)) { - return nan; - } - - /* - * Joint histogram: - * - * rows = fixed-image bins - * columns = moving-image bins - */ - std::vector jointHistogram( - binCount * binCount, - 0.0); - - /* - * Second pass: - * add every valid pixel pair to the Mattes joint histogram. - */ - visitValidPairs( - [&](double fixedValue, double movingValue) { - /* - * Fixed image: - * nearest-bin assignment over [0, bins - 1]. - */ - const double fixedPosition = - scaleToBinPosition( - fixedValue, - fixedRange, - 0.0, - static_cast(binCount - 1U)); - - std::size_t fixedBin = - roundToNearestEvenNonnegative( - fixedPosition); - - fixedBin = - std::min( - fixedBin, - binCount - 1U); - - /* - * Moving image: - * continuous coordinate over [1, bins - 2]. - * - * The one-bin margin leaves room for the cubic B-spline - * support at both histogram boundaries. - */ - const double movingPosition = - scaleToBinPosition( - movingValue, - movingRange, - 1.0, - static_cast(binCount - 2U)); - - const std::ptrdiff_t baseBin = - static_cast( - std::floor(movingPosition)); - - /* - * Cubic B-spline support covers at most four bins. - */ - for (int offset = -1; - offset <= 2; - ++offset) { - - const std::ptrdiff_t movingBin = - baseBin + - static_cast(offset); - - if (movingBin < 0 || - movingBin >= - static_cast( - binCount)) { - continue; - } - - const double weight = - cubicBSpline( - movingPosition - - static_cast(movingBin)); - - if (weight <= 0.0) { - continue; - } - - const std::size_t histogramIndex = - fixedBin * binCount + - static_cast( - movingBin); - - jointHistogram[histogramIndex] += - weight; - } - }); - - const double total = - std::accumulate( - jointHistogram.begin(), - jointHistogram.end(), - 0.0); - - if (!(total > 0.0) || - !std::isfinite(total)) { - return nan; - } - - /* - * Marginal probability distributions. - */ - std::vector px( - binCount, - 0.0); - - std::vector py( - binCount, - 0.0); - - for (std::size_t fixedBin = 0; - fixedBin < binCount; - ++fixedBin) { - - for (std::size_t movingBin = 0; - movingBin < binCount; - ++movingBin) { - - const std::size_t index = - fixedBin * binCount + - movingBin; - - const double pxy = - jointHistogram[index] / total; - - px[fixedBin] += pxy; - py[movingBin] += pxy; - } - } - - /* - * MI = sum p(x,y) log(p(x,y) / (p(x)p(y))) - */ - double mi = 0.0; - - for (std::size_t fixedBin = 0; - fixedBin < binCount; - ++fixedBin) { - - for (std::size_t movingBin = 0; - movingBin < binCount; - ++movingBin) { - - const std::size_t index = - fixedBin * binCount + - movingBin; - - const double pxy = - jointHistogram[index] / total; - - const double productOfMarginals = - px[fixedBin] * - py[movingBin]; - - if (pxy > 0.0 && - productOfMarginals > 0.0) { - - mi += - pxy * - std::log( - pxy / - productOfMarginals); - } - } - } - - // Natural logarithm: the result is in nats. - return mi; -} \ No newline at end of file + } + + return mutualInformationFromHistogram(jointHistogram, binCount); +} diff --git a/src/matte_mi.h b/src/matte_mi.h index 71899b04..9850d14a 100644 --- a/src/matte_mi.h +++ b/src/matte_mi.h @@ -1,35 +1,24 @@ -#include "Rcpp.h" -#include -#include +#ifndef VOLTRON_MATTE_MI_H +#define VOLTRON_MATTE_MI_H -#ifndef MATTE_MI_H -#define MATTE_MI_H +#include -struct IntensityRange { - double min; - double max; -}; +cv::Mat1d MatteMIMap( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins = 50); -double cubicBSpline(double u); +cv::Mat1d chunkedMatteMIMap( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins = 50); -double scaleToBinPosition(double value, IntensityRange range, - double low, double high); +double MatteMI( + const cv::Mat& fixed, + const cv::Mat& moving, + const cv::Mat& mask, + int bins = 50); -std::size_t roundToNearestEvenNonnegative(double x); - -bool isValidRange(IntensityRange range); - -double mattesMiFromValues(const double* fixedValues, - const double* movingValues, - std::size_t count, - std::size_t bins, - std::optional fixedRange = std::nullopt, - std::optional movingRange = std::nullopt); - -cv::Mat1d MatteMIMap(const cv::Mat& fixed, const cv::Mat& moving, - const cv::Mat& mask, int bins); - -double MatteMI(const cv::Mat& fixed, const cv::Mat& moving, - const cv::Mat& mask, int bins); - #endif \ No newline at end of file From 315af53dd89665314ff5916c84a90383b391bab5 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 27 Jul 2026 19:02:53 +0200 Subject: [PATCH 31/37] remove surplus declaration --- src/manual_registration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 9e368443..37a610e2 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -225,7 +225,7 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k // get matte metric, process // im2 is already processed - Mat im1Proc, im2Proc; + Mat im1Proc; cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); From e9ff2abb50fd19ab6f43006511e84f8b9b909635 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Tue, 28 Jul 2026 02:25:11 +0200 Subject: [PATCH 32/37] some performance updates --- R/registration.R | 29 +++++++++++++-------------- src/automated_registration.cpp | 19 ------------------ src/manual_registration.cpp | 36 +--------------------------------- 3 files changed, 15 insertions(+), 69 deletions(-) diff --git a/R/registration.R b/R/registration.R index 54dfbcd2..24613866 100644 --- a/R/registration.R +++ b/R/registration.R @@ -2973,13 +2973,16 @@ getManualRegisteration <- function( if (!suppressWarnings(!is.matrix(cur_alignment_image))) { cur_alignment_image <- cur_alignment_image[nrow(cur_alignment_image):1,] - ggplot(reshape2::melt(cur_alignment_image), - aes(Var2, Var1, fill= value)) + + cur_alignment_image <- + as.data.frame(as.table(cur_alignment_image)) + ggplot(cur_alignment_image, + aes(Var2, Var1, fill= Freq)) + ggplot2::geom_tile() + ggplot2::theme_void() + ggplot2::coord_fixed(expand = FALSE) + ggplot2::scale_fill_gradient(low = "#440154FF", high = "#FDE725FF", + # na.value = NA, name = "Matte's MI") } }) @@ -3242,7 +3245,7 @@ getRcppManualRegistration <- function( matte_map <- if (!is.null(reg[[3]])) { tmp <- reg[[3]] - tmp[is.na(tmp)] <- 0 + # tmp[is.na(tmp)] <- 0 tmp[tmp < 0] <- 0 tmp } else NA @@ -3428,8 +3431,10 @@ getAutomatedRegisteration <- function( if (!suppressWarnings(!is.matrix(cur_alignment_image))) { cur_alignment_image <- cur_alignment_image[nrow(cur_alignment_image):1,] - ggplot(reshape2::melt(cur_alignment_image), - aes(Var2, Var1, fill= value)) + + cur_alignment_image <- + as.data.frame(as.table(cur_alignment_image)) + ggplot(cur_alignment_image, + aes(Var2, Var1, fill= Freq)) + ggplot2::geom_tile() + ggplot2::theme_void() + ggplot2::coord_fixed(expand = FALSE) + @@ -3733,12 +3738,6 @@ getRcppAutomatedRegistration <- function( if (suppressWarnings(all(lapply(reg[[1]][[2]], is.null)))) { reg[[1]] <- list(reg[[1]][[1]], NULL) } - - # adjust matte mi map - tmp <- reg[[6]] - tmp[is.na(tmp)] <- 0 - tmp[tmp < 0] <- 0 - reg[[6]] <- tmp # check for failed registration aligned_image <- @@ -3752,7 +3751,7 @@ getRcppAutomatedRegistration <- function( matte_map <- if (!is.null(reg[[6]])){ tmp <- reg[[6]] - tmp[is.na(tmp)] <- 0 + # tmp[is.na(tmp)] <- 0 tmp[tmp < 0] <- 0 tmp @@ -3892,9 +3891,9 @@ getSimpleITKAutomatedRegistration <- function( fixed <- SimpleITK::Cast(fixed, "sitkUInt8") moving <- convertToSitkImage(query_image) moving <- SimpleITK::Cast(moving, "sitkUInt8") - # mask <- SimpleITK::as.image(array(mask, rev(dim(mask)))) - mask <- SimpleITK::as.image(array(as.integer(mask != 0L), - rev(dim(mask)))) + mask <- SimpleITK::as.image(array(mask, rev(dim(mask)))) + # mask <- SimpleITK::as.image(array(as.integer(mask != 0L), + # rev(dim(mask)))) mask <- SimpleITK::Cast(mask, "sitkUInt8") # get registration for image diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index aac1652e..3ebcf76a 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -706,25 +706,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, im2Proc.rows, im2Proc.cols, cv::INTER_LINEAR); im1NormalProc = warpTPSImage(im2Proc, im1NormalProc, tps, im2Proc.rows, im2Proc.cols, cv::INTER_LINEAR); - - // // determine extension limits for both images - // int y_max = max(im1Proc.rows, im2.rows); - // int x_max = max(im1Proc.cols, im2.cols); - // - // // extend images and mask - // cv::copyMakeBorder(im1Proc, im1Proc, 0.0, (int) (y_max - im1Proc.rows), 0.0, (x_max - im1Proc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - // cv::copyMakeBorder(im1NormalProc, im1NormalProc, 0.0, (int) (y_max - im1NormalProc.rows), 0.0, (x_max - im1NormalProc.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - // cv::copyMakeBorder(alignmentMask, alignmentMask, 0.0, (int) (y_max - alignmentMask.rows), 0.0, (x_max - alignmentMask.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - // - // // transform image - // tps->warpImage(im1Proc, im1Proc); - // tps->warpImage(im1NormalProc, im1NormalProc); - // tps->warpImage(alignmentMask, alignmentMask, cv::INTER_NEAREST); - // - // // resize image - // im1Proc = im1Proc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - // im1NormalProc = im1NormalProc(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); - // alignmentMask = alignmentMask(cv::Range(0,im2Proc.size().height), cv::Range(0,im2Proc.size().width)); // get matte metric, process accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 37a610e2..84908260 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -52,26 +52,6 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, im1Reg = warpTPSImage(im2, im1, tps, im2.rows, im2.cols, cv::INTER_LINEAR); - // // determine extension limits for both images - // int y_max = max(im1.rows, im2.rows); - // int x_max = max(im1.cols, im2.cols); - // - // // extend images - // cv::copyMakeBorder(im1, im1, - // 0.0, (int) (y_max - im1.rows), - // 0.0, (x_max - im1.cols), - // cv::BORDER_CONSTANT, - // Scalar(0, 0, 0)); - // - // // transform image - // tps->warpImage(im1, im1Reg); - // - // - // // resize image - // cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), - // cv::Range(0,im2.size().width)); - // im1Reg = im1Reg_cropped.clone(); - // process Mat im1Proc, im2Proc; cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); @@ -82,7 +62,7 @@ void alignImagesTPS(Mat &im1, Mat &im2, Mat &im1Reg, Rcpp::List &keypoints, // get alignment mask cv::Mat alignmentMask = generateOverlapMask(im2Proc, tps, - im1Proc.size()); + im1.size()); // get alignment metrics accuracy = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Coarse"); @@ -209,20 +189,6 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k im1Reg = warpTPSImage(im2, im1Affine, tps, im2.rows, im2.cols, cv::INTER_LINEAR); - // // determine extension limits for both images - // int y_max = max(im1Affine.rows, im2.rows); - // int x_max = max(im1Affine.cols, im2.cols); - // - // // extend images - // cv::copyMakeBorder(im1Affine, im1Affine, 0.0, (int) (y_max - im1Affine.rows), 0.0, (x_max - im1Affine.cols), cv::BORDER_CONSTANT, Scalar(0, 0, 0)); - // - // // transform image - // tps->warpImage(im1Affine, im1Reg); - // - // // resize image - // cv::Mat im1Reg_cropped = im1Reg(cv::Range(0,im2.size().height), cv::Range(0,im2.size().width)); - // im1Reg = im1Reg_cropped.clone(); - // get matte metric, process // im2 is already processed Mat im1Proc; From eba7b4c593f0b55358f6779a8447f97554fb931d Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 6 Aug 2026 17:39:44 +0200 Subject: [PATCH 33/37] some updates from claude review --- R/registration.R | 4 ++-- src/automated_registration.cpp | 21 ++++++++++++--------- src/metrics.cpp | 8 ++++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/R/registration.R b/R/registration.R index 24613866..fc262622 100644 --- a/R/registration.R +++ b/R/registration.R @@ -627,13 +627,13 @@ getAlignmentTabPanel <- function(len_images, centre, register_ind) { br(), tabsetPanel( - id = paste("inner_tabs", "i"), + id = paste0("inner_tabs", i), tabPanel("Alignment Stat.", tableOutput(paste0("alignment_stats", i))), tabPanel("Matte's MI Map", imageOutput(paste0("plot_matte_map", i))), tabPanel("Matching Keypoints", - imageOutput(paste0("plot_keypoint_match", i))), + imageOutput(paste0("plot_keypoint_match", i))) ) ) diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index 3ebcf76a..e1b3c2d5 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -618,15 +618,18 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } // warp mask and image - cv::Mat alignmentMask = generateOverlapMask(im2Proc.size(), - h, - im1Proc.size()); - if(h.rows == 2){ - warpAffine(im1Proc, im1Proc, h, im2Proc.size()); - warpAffine(im1NormalProc, im1NormalProc, h, im2Proc.size()); - } else if(h.rows == 3){ - warpPerspective(im1Proc, im1Proc, h, im2Proc.size()); - warpPerspective(im1NormalProc, im1NormalProc, h, im2Proc.size()); + cv::Mat alignmentMask; + if(!h.empty()){ + alignmentMask = generateOverlapMask(im2Proc.size(), + h, + im1Proc.size()); + if(h.rows == 2){ + warpAffine(im1Proc, im1Proc, h, im2Proc.size()); + warpAffine(im1NormalProc, im1NormalProc, h, im2Proc.size()); + } else if(h.rows == 3){ + warpPerspective(im1Proc, im1Proc, h, im2Proc.size()); + warpPerspective(im1NormalProc, im1NormalProc, h, im2Proc.size()); + } } else { Rcout << "WARNING: No transformation was found" << endl; return; diff --git a/src/metrics.cpp b/src/metrics.cpp index dcc48a33..150a0fb4 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -291,6 +291,8 @@ std::map getAlignmentMetrics(Mat &im1, Mat &im2, // Normalize histograms cv::normalize(hist1, hist1, 0, 1, cv::NORM_MINMAX); cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX); + hist1 /= cv::sum(hist1)[0]; + hist2 /= cv::sum(hist2)[0]; // Summary Rcout << "Alignment Accuracy (" << type << "): " << endl; @@ -344,9 +346,11 @@ std::map getKeypointMetrics(std::vector &point metrics["Degenerate"] = (double) degenerate_points; // check distribution of points - double stddev = checkMappedGridDistribution(im2, h); + // double stddev = checkMappedGridDistribution(im2, h); + double stddev = checkMappedGridDistribution(im1, h); Rcout << " Std dev of registered points: " << stddev << endl; - if(stddev < 1.0 | stddev > max(im1.rows, im1.cols)){ + // if(stddev < 1.0 | stddev > max(im1.rows, im1.cols)){ + if(stddev < 1.0 | stddev > max(im2.rows, im2.cols)){ Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; metrics["Degenerate"] = 1.0; } From 0b2142c2736122707ef960f7385118ff201a4439 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 6 Aug 2026 17:57:51 +0200 Subject: [PATCH 34/37] more updates from claude --- R/registration.R | 7 ++++--- src/manual_registration.cpp | 2 -- src/metrics.cpp | 2 -- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/R/registration.R b/R/registration.R index fc262622..9b1f5f36 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3121,8 +3121,11 @@ computeManualPairwiseTransform <- function( rotate_ref = FALSE, initial_mapping = list(reg[[1]]) ) - reg$aligned_image <- tfx$aligned_image reg[[1]][[2]] <- tfx$transformation + reg$aligned_image <- tfx$aligned_image + reg$matte_map <- tfx$matte_map + reg$alignment_stats$fine[names(tfx$alignment_metrics)] <- + tfx$alignment_metrics } # return transformation matrix and images @@ -3902,7 +3905,6 @@ getSimpleITKAutomatedRegistration <- function( elx$SetFixedImage(fixed) elx$SetMovingImage(moving) # elx$SetMovingMask(mask) - parameterMapVector = SimpleITK::VectorOfParameterMap() mp <- SimpleITK:::ReadParameterFile( system.file("extdata", "bspline_map.txt", package = "VoltRon") ) @@ -3938,7 +3940,6 @@ getSimpleITKAutomatedRegistration <- function( elx$SetFixedImage(moving) elx$SetFixedMask(mask) elx$SetMovingImage(fixed) - parameterMapVector = SimpleITK::VectorOfParameterMap() mp <- SimpleITK:::ReadParameterFile( system.file("extdata", "bspline_map.txt", package = "VoltRon") ) diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 84908260..1bb956ee 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -372,8 +372,6 @@ Rcpp::List manual_registeration_matrix(Rcpp::NumericMatrix query_data, keypoints, query_landmark, reference_landmark); - keypoints[0] = keypoints[0]; - keypoints[1] = keypoints[1]; } // transformation matrix, can be either a matrix, set of keypoints or both diff --git a/src/metrics.cpp b/src/metrics.cpp index 150a0fb4..8a9d6dc8 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -346,10 +346,8 @@ std::map getKeypointMetrics(std::vector &point metrics["Degenerate"] = (double) degenerate_points; // check distribution of points - // double stddev = checkMappedGridDistribution(im2, h); double stddev = checkMappedGridDistribution(im1, h); Rcout << " Std dev of registered points: " << stddev << endl; - // if(stddev < 1.0 | stddev > max(im1.rows, im1.cols)){ if(stddev < 1.0 | stddev > max(im2.rows, im2.cols)){ Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; metrics["Degenerate"] = 1.0; From 6d7f2b6a2f0fcb0355c73d765480d7350e9f15a3 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 6 Aug 2026 21:55:46 +0200 Subject: [PATCH 35/37] some checks --- R/RcppExports.R | 12 +++--- R/registration.R | 70 ++++++++++++++++++++++++--------- src/RcppExports.cpp | 27 +++++++------ src/accuracy.cpp | 11 ++++-- src/automated_registration.cpp | 71 +++++++++++++++++++++++----------- src/manual_registration.cpp | 26 +++++++++---- src/metrics.cpp | 8 ++-- src/metrics.h | 4 -- 8 files changed, 151 insertions(+), 78 deletions(-) diff --git a/R/RcppExports.R b/R/RcppExports.R index 8ded2a70..e534540a 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -1,12 +1,12 @@ # Generated by using Rcpp::compileAttributes() -> do not edit by hand # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 -accuracy_rawvector <- function(ref_image, query_image, mask, width, height, type, overlay_images = TRUE) { - .Call('_VoltRon_accuracy_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, mask, width, height, type, overlay_images) +accuracy_rawvector <- function(ref_image, query_image, mask, width, height, type, overlay_images = TRUE, compute_matte_map = TRUE) { + .Call('_VoltRon_accuracy_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, mask, width, height, type, overlay_images, compute_matte_map) } -automated_registeration_rawvector <- function(ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid) { - .Call('_VoltRon_automated_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid) +automated_registeration_rawvector <- function(ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid, compute_matte_map = TRUE) { + .Call('_VoltRon_automated_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid, compute_matte_map) } replaceNaMatrix <- function(mat, replace) { @@ -25,8 +25,8 @@ warpImageManual <- function(ref_image, query_image, mapping, width1, height1, wi .Call('_VoltRon_warpImageManual', PACKAGE = 'VoltRon', ref_image, query_image, mapping, width1, height1, width2, height2) } -manual_registeration_rawvector <- function(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid) { - .Call('_VoltRon_manual_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid) +manual_registeration_rawvector <- function(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid, compute_matte_map = TRUE) { + .Call('_VoltRon_manual_registeration_rawvector', PACKAGE = 'VoltRon', ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid, compute_matte_map) } manual_registeration_matrix <- function(query_data, reference_landmark, query_landmark, method, nonrigid) { diff --git a/R/registration.R b/R/registration.R index 9b1f5f36..c56ce166 100644 --- a/R/registration.R +++ b/R/registration.R @@ -3031,7 +3031,8 @@ computeManualPairwiseTransform <- function( keypoints_list, query_ind, ref_ind, - input + input, + compute_matte_map = TRUE ) { # determine the number of transformation to map from query to the reference indices <- query_ind:ref_ind @@ -3085,7 +3086,8 @@ computeManualPairwiseTransform <- function( )]] == "Yes", method = input$Method, - nonrigid = if(is.null(input$nonrigid)) "None" else input$nonrigid + nonrigid = if(is.null(input$nonrigid)) "None" else input$nonrigid, + compute_matte_map = compute_matte_map ) # run SimpleITK as fine registration @@ -3119,7 +3121,8 @@ computeManualPairwiseTransform <- function( flipflop_ref = FALSE, rotate_query = FALSE, rotate_ref = FALSE, - initial_mapping = list(reg[[1]]) + initial_mapping = list(reg[[1]]), + compute_matte_map = compute_matte_map ) reg[[1]][[2]] <- tfx$transformation reg$aligned_image <- tfx$aligned_image @@ -3248,7 +3251,6 @@ getRcppManualRegistration <- function( matte_map <- if (!is.null(reg[[3]])) { tmp <- reg[[3]] - # tmp[is.na(tmp)] <- 0 tmp[tmp < 0] <- 0 tmp } else NA @@ -3443,6 +3445,7 @@ getAutomatedRegisteration <- function( ggplot2::coord_fixed(expand = FALSE) + ggplot2::scale_fill_gradient(low = "#440154FF", high = "#FDE725FF", + # na.value = NA, name = "Matte's MI") } }) @@ -3488,7 +3491,8 @@ computeAutomatedPairwiseTransform <- function( channel_names, query_ind, ref_ind, - input + input, + compute_matte_map = TRUE ) { # determine the number of transformation to map from query to the reference indices <- query_ind:ref_ind @@ -3580,7 +3584,8 @@ computeAutomatedPairwiseTransform <- function( rotate_ref = input[[paste0("rotate_", ref_label, "_image", cur_map[2])]], matcher = input$Matcher, method = input$Method, - nonrigid = if(is.null(input$nonrigid)) "None" else input$nonrigid + nonrigid = if(is.null(input$nonrigid)) "None" else input$nonrigid, + compute_matte_map = compute_matte_map ) # update transformation matrix @@ -3647,7 +3652,8 @@ computeAutomatedPairwiseTransform <- function( )]], rotate_ref = input[[paste0( "rotate_", ref_label, "_image", cur_map[2])]], - initial_mapping = list(reg[[1]]) + initial_mapping = list(reg[[1]]), + compute_matte_map = compute_matte_map ) reg[[1]][[2]] <- tfx$transformation reg$aligned_image <- tfx$aligned_image @@ -3712,7 +3718,8 @@ getRcppAutomatedRegistration <- function( rotate_ref = "0", matcher = "FLANN", method = "Homography", - nonrigid = "TPS (OpenCV)" + nonrigid = "TPS (OpenCV)", + compute_matte_map = TRUE ) { ref_image <- magick::image_data(ref_image, channels = "rgb") query_image <- magick::image_data(query_image, channels = "rgb") @@ -3734,7 +3741,8 @@ getRcppAutomatedRegistration <- function( rotate_ref = rotate_ref, matcher = matcher, method = method, - nonrigid = nonrigid + nonrigid = nonrigid, + compute_matte_map = compute_matte_map ) # check for null keypoints @@ -3754,7 +3762,6 @@ getRcppAutomatedRegistration <- function( matte_map <- if (!is.null(reg[[6]])){ tmp <- reg[[6]] - # tmp[is.na(tmp)] <- 0 tmp[tmp < 0] <- 0 tmp @@ -3828,7 +3835,8 @@ getSimpleITKAutomatedRegistration <- function( flipflop_ref = "None", rotate_query = "0", rotate_ref = "0", - initial_mapping = NULL + initial_mapping = NULL, + compute_matte_map = TRUE ){ # check SimpleITK if (!requireNamespace('SimpleITK')) { @@ -3959,16 +3967,25 @@ getSimpleITKAutomatedRegistration <- function( results <- getAlignmentAccuracy(ref_image, aligned_image, aligned_mask, - "Fine") + "Fine", + compute_matte_map) # convert images overlay_image <- if (!is.null(results[[3]])) magick::image_read(results[[3]]) else NA + + # check matte maps + matte_map <- + if (!is.null(results[[2]])){ + tmp <- results[[2]] + tmp[tmp < 0] <- 0 + tmp + } else NA # return return(list(aligned_image = aligned_image, alignment_metrics = results[[1]], - matte_map = results[[2]], + matte_map = matte_map, overlay_image = overlay_image, transformation = list( tfx_points = tfx_points, @@ -4022,7 +4039,8 @@ getNonInteractiveRegistration <- function( channel_names = channel_names, query_ind = i, ref_ind = centre, - input = mapping_parameters + input = mapping_parameters, + compute_matte_map = FALSE ) } else { flag <- checkKeypoints(mapping_parameters$keypoints) @@ -4031,7 +4049,8 @@ getNonInteractiveRegistration <- function( keypoints_list = mapping_parameters$keypoints, query_ind = i, ref_ind = centre, - input = mapping_parameters + input = mapping_parameters, + compute_matte_map = FALSE ) } @@ -4061,15 +4080,27 @@ getNonInteractiveRegistration <- function( # Accuracy #### #### +#' getAlignmentAccuracy +#' +#' get accuracy measurements from two aligned images +#' +#' @param ref_image reference image +#' @param query_image query image +#' @param mask alignment mask +#' +#' @importFrom DelayedArray realize +#' @importFrom magick image_data +#' +#' @noRd getAlignmentAccuracy <- function(ref_image, query_image, mask, - type){ + type, + compute_matte_map = TRUE){ # image info ref_info <- getImageInfo(ref_image) - query_info <- getImageInfo(query_image) - + # ref image if (inherits(ref_image, "ImageArray")) { ref_image <- DelayedArray::realize(ref_image) @@ -4096,6 +4127,7 @@ getAlignmentAccuracy <- function(ref_image, width = ref_info$width, height = ref_info$height, type, - overlay_images = TRUE) + overlay_images = TRUE, + compute_matte_map = compute_matte_map) } diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 2829135e..20f8e9c6 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -12,8 +12,8 @@ Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); #endif // accuracy_rawvector -Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, Rcpp::RawVector& mask, const int width, const int height, std::string type, bool overlay_images); -RcppExport SEXP _VoltRon_accuracy_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP maskSEXP, SEXP widthSEXP, SEXP heightSEXP, SEXP typeSEXP, SEXP overlay_imagesSEXP) { +Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, Rcpp::RawVector& mask, const int width, const int height, std::string type, bool overlay_images, const bool compute_matte_map); +RcppExport SEXP _VoltRon_accuracy_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP maskSEXP, SEXP widthSEXP, SEXP heightSEXP, SEXP typeSEXP, SEXP overlay_imagesSEXP, SEXP compute_matte_mapSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -24,13 +24,14 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const int >::type height(heightSEXP); Rcpp::traits::input_parameter< std::string >::type type(typeSEXP); Rcpp::traits::input_parameter< bool >::type overlay_images(overlay_imagesSEXP); - rcpp_result_gen = Rcpp::wrap(accuracy_rawvector(ref_image, query_image, mask, width, height, type, overlay_images)); + Rcpp::traits::input_parameter< const bool >::type compute_matte_map(compute_matte_mapSEXP); + rcpp_result_gen = Rcpp::wrap(accuracy_rawvector(ref_image, query_image, mask, width, height, type, overlay_images, compute_matte_map)); return rcpp_result_gen; END_RCPP } // automated_registeration_rawvector -Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, const int width1, const int height1, const int width2, const int height2, const float GOOD_MATCH_PERCENT, const int MAX_FEATURES, const bool invert_query, const bool invert_ref, Rcpp::String flipflop_query, Rcpp::String flipflop_ref, Rcpp::String rotate_query, Rcpp::String rotate_ref, Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid); -RcppExport SEXP _VoltRon_automated_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP GOOD_MATCH_PERCENTSEXP, SEXP MAX_FEATURESSEXP, SEXP invert_querySEXP, SEXP invert_refSEXP, SEXP flipflop_querySEXP, SEXP flipflop_refSEXP, SEXP rotate_querySEXP, SEXP rotate_refSEXP, SEXP matcherSEXP, SEXP methodSEXP, SEXP nonrigidSEXP) { +Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, const int width1, const int height1, const int width2, const int height2, const float GOOD_MATCH_PERCENT, const int MAX_FEATURES, const bool invert_query, const bool invert_ref, Rcpp::String flipflop_query, Rcpp::String flipflop_ref, Rcpp::String rotate_query, Rcpp::String rotate_ref, Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid, const bool compute_matte_map); +RcppExport SEXP _VoltRon_automated_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP GOOD_MATCH_PERCENTSEXP, SEXP MAX_FEATURESSEXP, SEXP invert_querySEXP, SEXP invert_refSEXP, SEXP flipflop_querySEXP, SEXP flipflop_refSEXP, SEXP rotate_querySEXP, SEXP rotate_refSEXP, SEXP matcherSEXP, SEXP methodSEXP, SEXP nonrigidSEXP, SEXP compute_matte_mapSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -51,7 +52,8 @@ BEGIN_RCPP Rcpp::traits::input_parameter< Rcpp::String >::type matcher(matcherSEXP); Rcpp::traits::input_parameter< Rcpp::String >::type method(methodSEXP); Rcpp::traits::input_parameter< Rcpp::String >::type nonrigid(nonrigidSEXP); - rcpp_result_gen = Rcpp::wrap(automated_registeration_rawvector(ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid)); + Rcpp::traits::input_parameter< const bool >::type compute_matte_map(compute_matte_mapSEXP); + rcpp_result_gen = Rcpp::wrap(automated_registeration_rawvector(ref_image, query_image, width1, height1, width2, height2, GOOD_MATCH_PERCENT, MAX_FEATURES, invert_query, invert_ref, flipflop_query, flipflop_ref, rotate_query, rotate_ref, matcher, method, nonrigid, compute_matte_map)); return rcpp_result_gen; END_RCPP } @@ -118,8 +120,8 @@ BEGIN_RCPP END_RCPP } // manual_registeration_rawvector -Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, Rcpp::NumericMatrix reference_landmark, Rcpp::NumericMatrix query_landmark, const int width1, const int height1, const int width2, const int height2, const bool invert_query, const bool invert_ref, Rcpp::String method, Rcpp::String nonrigid); -RcppExport SEXP _VoltRon_manual_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP reference_landmarkSEXP, SEXP query_landmarkSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP invert_querySEXP, SEXP invert_refSEXP, SEXP methodSEXP, SEXP nonrigidSEXP) { +Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, Rcpp::RawVector query_image, Rcpp::NumericMatrix reference_landmark, Rcpp::NumericMatrix query_landmark, const int width1, const int height1, const int width2, const int height2, const bool invert_query, const bool invert_ref, Rcpp::String method, Rcpp::String nonrigid, const bool compute_matte_map); +RcppExport SEXP _VoltRon_manual_registeration_rawvector(SEXP ref_imageSEXP, SEXP query_imageSEXP, SEXP reference_landmarkSEXP, SEXP query_landmarkSEXP, SEXP width1SEXP, SEXP height1SEXP, SEXP width2SEXP, SEXP height2SEXP, SEXP invert_querySEXP, SEXP invert_refSEXP, SEXP methodSEXP, SEXP nonrigidSEXP, SEXP compute_matte_mapSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -135,7 +137,8 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const bool >::type invert_ref(invert_refSEXP); Rcpp::traits::input_parameter< Rcpp::String >::type method(methodSEXP); Rcpp::traits::input_parameter< Rcpp::String >::type nonrigid(nonrigidSEXP); - rcpp_result_gen = Rcpp::wrap(manual_registeration_rawvector(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid)); + Rcpp::traits::input_parameter< const bool >::type compute_matte_map(compute_matte_mapSEXP); + rcpp_result_gen = Rcpp::wrap(manual_registeration_rawvector(ref_image, query_image, reference_landmark, query_landmark, width1, height1, width2, height2, invert_query, invert_ref, method, nonrigid, compute_matte_map)); return rcpp_result_gen; END_RCPP } @@ -214,13 +217,13 @@ END_RCPP } static const R_CallMethodDef CallEntries[] = { - {"_VoltRon_accuracy_rawvector", (DL_FUNC) &_VoltRon_accuracy_rawvector, 7}, - {"_VoltRon_automated_registeration_rawvector", (DL_FUNC) &_VoltRon_automated_registeration_rawvector, 17}, + {"_VoltRon_accuracy_rawvector", (DL_FUNC) &_VoltRon_accuracy_rawvector, 8}, + {"_VoltRon_automated_registeration_rawvector", (DL_FUNC) &_VoltRon_automated_registeration_rawvector, 18}, {"_VoltRon_replaceNaMatrix", (DL_FUNC) &_VoltRon_replaceNaMatrix, 2}, {"_VoltRon_warpRcppImage", (DL_FUNC) &_VoltRon_warpRcppImage, 7}, {"_VoltRon_warpImageAuto", (DL_FUNC) &_VoltRon_warpImageAuto, 7}, {"_VoltRon_warpImageManual", (DL_FUNC) &_VoltRon_warpImageManual, 7}, - {"_VoltRon_manual_registeration_rawvector", (DL_FUNC) &_VoltRon_manual_registeration_rawvector, 12}, + {"_VoltRon_manual_registeration_rawvector", (DL_FUNC) &_VoltRon_manual_registeration_rawvector, 13}, {"_VoltRon_manual_registeration_matrix", (DL_FUNC) &_VoltRon_manual_registeration_matrix, 5}, {"_VoltRon_applyRcppMapping", (DL_FUNC) &_VoltRon_applyRcppMapping, 2}, {"_VoltRon_generateOverlapMask", (DL_FUNC) &_VoltRon_generateOverlapMask, 3}, diff --git a/src/accuracy.cpp b/src/accuracy.cpp index 2d585afd..1374d4fa 100644 --- a/src/accuracy.cpp +++ b/src/accuracy.cpp @@ -21,7 +21,8 @@ Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, const int width, const int height, std::string type, - bool overlay_images = true) { + bool overlay_images = true, + const bool compute_matte_map = true) { // results Rcpp::List out(3); @@ -43,8 +44,12 @@ Rcpp::List accuracy_rawvector(Rcpp::RawVector& ref_image, // get matte map Mat1d accuracyMatte; - accuracyMatte = MatteMIMap(im2Proc, im1Proc, maskReg, 50); - out[1] = matToNumericMatrix(accuracyMatte); // Matte MI metric + if(compute_matte_map){ + accuracyMatte = MatteMIMap(im2Proc, im1Proc, maskReg, 50); + out[1] = matToNumericMatrix(accuracyMatte); // Matte MI metric + } else { + out[1] = R_NilValue; + } // image overlay if(overlay_images){ diff --git a/src/automated_registration.cpp b/src/automated_registration.cpp index e1b3c2d5..dfec93c0 100644 --- a/src/automated_registration.cpp +++ b/src/automated_registration.cpp @@ -556,14 +556,25 @@ bool getORBTransformationMatrix( //// // align images with FLANN algorithm -void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, - Mat &imMatches, Mat &h, Rcpp::List &keypoints, - const float GOOD_MATCH_PERCENT, const int MAX_FEATURES, +void alignImages(Mat &im1, + Mat &im2, + Mat &im1Reg, + Mat &im1Overlay, + Mat &imMatches, + Mat &h, + Rcpp::List &keypoints, + const float GOOD_MATCH_PERCENT, + const int MAX_FEATURES, Rcpp::String matcher, - const bool invert_query, const bool invert_ref, - const char* flipflop_query, const char* flipflop_ref, - const char* rotate_query, const char* rotate_ref, - const bool run_Affine, const bool run_TPS, + const bool invert_query, + const bool invert_ref, + const char* flipflop_query, + const char* flipflop_ref, + const char* rotate_query, + const char* rotate_ref, + const bool run_Affine, + const bool run_TPS, + const bool compute_matte_map, Mat1d &accuracyMatte, std::map &accuracy_coarse, std::map &accuracy_fine) @@ -619,10 +630,17 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // warp mask and image cv::Mat alignmentMask; + std::map keypoint_metrics; if(!h.empty()){ alignmentMask = generateOverlapMask(im2Proc.size(), h, im1Proc.size()); + + // get keypoint metrics before warping + keypoint_metrics = getKeypointMetrics(points1, points2, + im1Proc, im2Proc, h, mask); + is_faulty = (bool) keypoint_metrics["Degenerate"]; + if(h.rows == 2){ warpAffine(im1Proc, im1Proc, h, im2Proc.size()); warpAffine(im1NormalProc, im1NormalProc, h, im2Proc.size()); @@ -635,12 +653,6 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, return; } - // get keypoint metrics - std::map keypoint_metrics; - keypoint_metrics = getKeypointMetrics(points1, points2, - im1Proc, im2Proc, h, mask); - is_faulty = (bool) keypoint_metrics["Degenerate"]; - // get alignment metrics std::map image_metrics; image_metrics = getAlignmentMetrics(im1Proc, im2Proc, @@ -655,7 +667,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, accuracy_coarse = final_map; // get matte metric - accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + if(compute_matte_map) + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); /////////////////////// /// Find Homography /// @@ -712,7 +725,8 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, // get matte metric, process accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); - accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + if(compute_matte_map) + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); // change color map cv::addWeighted(im2Proc, 0.7, im1Proc, 0.3, 0, im1Proc); @@ -735,14 +749,24 @@ void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &im1Overlay, } // [[Rcpp::export]] -Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::RawVector& query_image, - const int width1, const int height1, - const int width2, const int height2, - const float GOOD_MATCH_PERCENT, const int MAX_FEATURES, - const bool invert_query, const bool invert_ref, - Rcpp::String flipflop_query, Rcpp::String flipflop_ref, - Rcpp::String rotate_query, Rcpp::String rotate_ref, - Rcpp::String matcher, Rcpp::String method, Rcpp::String nonrigid) +Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, + Rcpp::RawVector& query_image, + const int width1, + const int height1, + const int width2, + const int height2, + const float GOOD_MATCH_PERCENT, + const int MAX_FEATURES, + const bool invert_query, + const bool invert_ref, + Rcpp::String flipflop_query, + Rcpp::String flipflop_ref, + Rcpp::String rotate_query, + Rcpp::String rotate_ref, + Rcpp::String matcher, + Rcpp::String method, + Rcpp::String nonrigid, + const bool compute_matte_map = true) { // Return data Rcpp::List out(8); @@ -772,6 +796,7 @@ Rcpp::List automated_registeration_rawvector(Rcpp::RawVector& ref_image, Rcpp::R flipflop_query.get_cstring(), flipflop_ref.get_cstring(), rotate_query.get_cstring(), rotate_ref.get_cstring(), run_Affine, run_TPS, + compute_matte_map, accuracyMatte, accuracy_coarse, accuracy_fine); diff --git a/src/manual_registration.cpp b/src/manual_registration.cpp index 1bb956ee..1474ee82 100644 --- a/src/manual_registration.cpp +++ b/src/manual_registration.cpp @@ -110,10 +110,18 @@ void alignImagesTPS_points(Rcpp::NumericMatrix &query_data, } // align images with FLANN algorithm -void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &keypoints, - Rcpp::NumericMatrix query_landmark, Rcpp::NumericMatrix reference_landmark, - const bool invert_query, const bool invert_ref, - const bool run_Affine, const bool run_TPS, +void alignImagesAffineTPS(Mat &im1, + Mat &im2, + Mat &im1Reg, + Mat &h, + Rcpp::List &keypoints, + Rcpp::NumericMatrix query_landmark, + Rcpp::NumericMatrix reference_landmark, + const bool invert_query, + const bool invert_ref, + const bool run_Affine, + const bool run_TPS, + const bool compute_matte_map, Mat1d &accuracyMatte, std::map &accuracy_coarse, std::map &accuracy_fine) @@ -195,7 +203,8 @@ void alignImagesAffineTPS(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h, Rcpp::List &k cvtColor(im1Reg, im1Proc, cv::COLOR_BGR2GRAY); im1Proc = preprocessImage(im1Proc, invert_query, "None", "0"); accuracy_fine = getAlignmentMetrics(im1Proc, im2Proc, alignmentMask, "Fine"); - accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); + if(compute_matte_map) + accuracyMatte = MatteMIMap(im2Proc, im1Proc, alignmentMask, 50); } } @@ -277,7 +286,8 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, const bool invert_query, const bool invert_ref, Rcpp::String method, - Rcpp::String nonrigid) + Rcpp::String nonrigid, + const bool compute_matte_map = true) { // Return data Rcpp::List out(5); @@ -305,7 +315,9 @@ Rcpp::List manual_registeration_rawvector(Rcpp::RawVector ref_image, query_landmark, reference_landmark, invert_query, invert_ref, - run_Affine, run_TPS, + run_Affine, + run_TPS, + compute_matte_map, accuracyMatte, accuracy_coarse, accuracy_fine); diff --git a/src/metrics.cpp b/src/metrics.cpp index 8a9d6dc8..2117d261 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -108,7 +108,7 @@ bool checkDegenerate(double pts1, double pts2) { // get warning message bool is_degenerate = FALSE; - if(pts1 < 1.0 | pts2 < 1.0){ + if(pts1 < 1.0 || pts2 < 1.0){ is_degenerate = TRUE; Rcout << "WARNING: points may be in a degenerate configuration." << endl; } @@ -289,8 +289,8 @@ std::map getAlignmentMetrics(Mat &im1, Mat &im2, hist2, 1, &histSize, &histRange); // Normalize histograms - cv::normalize(hist1, hist1, 0, 1, cv::NORM_MINMAX); - cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX); + // cv::normalize(hist1, hist1, 0, 1, cv::NORM_MINMAX); + // cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX); hist1 /= cv::sum(hist1)[0]; hist2 /= cv::sum(hist2)[0]; @@ -348,7 +348,7 @@ std::map getKeypointMetrics(std::vector &point // check distribution of points double stddev = checkMappedGridDistribution(im1, h); Rcout << " Std dev of registered points: " << stddev << endl; - if(stddev < 1.0 | stddev > max(im2.rows, im2.cols)){ + if(stddev < 1.0 || stddev > max(im2.rows, im2.cols)){ Rcout << " WARNING: Transformation may be poor - transformed points grid seem to be concentrated!" << endl; metrics["Degenerate"] = 1.0; } diff --git a/src/metrics.h b/src/metrics.h index bd809a7f..be75a2ac 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -37,10 +37,6 @@ cv::Mat generateOverlapMask(cv::Mat& ref_image, Ptr& tps, cv::Size ssize); -// cv::Mat generateOverlapMask(Rcpp::NumericVector dsize, -// Rcpp::NumericMatrix trans_mat, -// Rcpp::NumericVector ssize); - // get alignment metrics std::map getAlignmentMetrics(cv::Mat &im1, cv::Mat &im2, From 9807471203c847e90f817539ff9d3c4069884f73 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Fri, 7 Aug 2026 11:12:20 +0200 Subject: [PATCH 36/37] testing --- tests/testthat/test-registration.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-registration.R b/tests/testthat/test-registration.R index 9f82442a..58d3f98e 100644 --- a/tests/testthat/test-registration.R +++ b/tests/testthat/test-registration.R @@ -119,7 +119,7 @@ test_that("registeration non-rigid simpleitk", { query_spatdata = xenium_data, mapping_parameters = mapping_parameters_nonrigid, interactive = FALSE) - + print() # return expect_equal(1,1L) }) \ No newline at end of file From bdc875a156ca9e362720e3346ace0ce34eecd806 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Fri, 7 Aug 2026 11:17:00 +0200 Subject: [PATCH 37/37] testing --- tests/testthat/test-registration.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-registration.R b/tests/testthat/test-registration.R index 58d3f98e..9f82442a 100644 --- a/tests/testthat/test-registration.R +++ b/tests/testthat/test-registration.R @@ -119,7 +119,7 @@ test_that("registeration non-rigid simpleitk", { query_spatdata = xenium_data, mapping_parameters = mapping_parameters_nonrigid, interactive = FALSE) - print() + # return expect_equal(1,1L) }) \ No newline at end of file