From 56f7e54cce7aa0bc6e94b1cab56ffd9d95d606b0 Mon Sep 17 00:00:00 2001 From: Alexey Spizhevoy Date: Fri, 27 May 2011 11:41:35 +0000 Subject: [PATCH] added GC_COLOR_GRAD cost function type into opencv_stitching --- modules/stitching/exposure_compensate.cpp | 26 ++++- modules/stitching/exposure_compensate.hpp | 10 +- modules/stitching/main.cpp | 12 ++- modules/stitching/matchers.cpp | 32 +++--- modules/stitching/seam_finders.cpp | 160 +++++++++++++++++++++++++----- modules/stitching/seam_finders.hpp | 30 +++--- modules/stitching/util_inl.hpp | 9 +- 7 files changed, 217 insertions(+), 62 deletions(-) diff --git a/modules/stitching/exposure_compensate.cpp b/modules/stitching/exposure_compensate.cpp index 040b6a8..d91d573 100644 --- a/modules/stitching/exposure_compensate.cpp +++ b/modules/stitching/exposure_compensate.cpp @@ -45,6 +45,7 @@ using namespace std; using namespace cv; +using namespace cv::gpu; Ptr ExposureCompensator::createDefault(int type) @@ -53,6 +54,8 @@ Ptr ExposureCompensator::createDefault(int type) return new NoExposureCompensator(); if (type == OVERLAP) return new OverlapExposureCompensator(); + if (type == SEGMENT) + return new SegmentExposureCompensator(); CV_Error(CV_StsBadArg, "unsupported exposure compensation method"); return NULL; } @@ -61,13 +64,15 @@ Ptr ExposureCompensator::createDefault(int type) void OverlapExposureCompensator::feed(const vector &corners, const vector &images, const vector &masks) { + CV_Assert(corners.size() == images.size() && images.size() == masks.size()); + const int num_images = static_cast(images.size()); Mat_ N(num_images, num_images); N.setTo(0); Mat_ I(num_images, num_images); I.setTo(0); Rect dst_roi = resultRoi(corners, images); Mat subimg1, subimg2; - Mat_ submask1, submask2, overlap; + Mat_ submask1, submask2, intersect; for (int i = 0; i < num_images; ++i) { @@ -81,9 +86,9 @@ void OverlapExposureCompensator::feed(const vector &corners, const vector submask1 = masks[i](Rect(roi.tl() - corners[i], roi.br() - corners[i])); submask2 = masks[j](Rect(roi.tl() - corners[j], roi.br() - corners[j])); - overlap = submask1 & submask2; + intersect = submask1 & submask2; - N(i, j) = N(j, i) = countNonZero(overlap); + N(i, j) = N(j, i) = countNonZero(intersect); double Isum1 = 0, Isum2 = 0; for (int y = 0; y < roi.height; ++y) @@ -92,7 +97,7 @@ void OverlapExposureCompensator::feed(const vector &corners, const vector const Point3_* r2 = subimg2.ptr >(y); for (int x = 0; x < roi.width; ++x) { - if (overlap(y, x)) + if (intersect(y, x)) { Isum1 += sqrt(static_cast(sqr(r1[x].x) + sqr(r1[x].y) + sqr(r1[x].z))); Isum2 += sqrt(static_cast(sqr(r2[x].x) + sqr(r2[x].y) + sqr(r2[x].z))); @@ -122,7 +127,7 @@ void OverlapExposureCompensator::feed(const vector &corners, const vector } } - solve(A, b, gains_, DECOMP_SVD); + solve(A, b, gains_); } @@ -130,3 +135,14 @@ void OverlapExposureCompensator::apply(int index, Point /*corner*/, Mat &image, { image *= gains_(index, 0); } + + +void SegmentExposureCompensator::feed(const vector &/*corners*/, const vector &/*images*/, + const vector &/*masks*/) +{ +} + + +void SegmentExposureCompensator::apply(int /*index*/, Point /*corner*/, Mat &/*image*/, const Mat &/*mask*/) +{ +} \ No newline at end of file diff --git a/modules/stitching/exposure_compensate.hpp b/modules/stitching/exposure_compensate.hpp index 5aec7c7..5112bc5 100644 --- a/modules/stitching/exposure_compensate.hpp +++ b/modules/stitching/exposure_compensate.hpp @@ -48,7 +48,7 @@ class ExposureCompensator { public: - enum { NO, OVERLAP }; + enum { NO, OVERLAP, SEGMENT }; static cv::Ptr createDefault(int type); virtual void feed(const std::vector &corners, const std::vector &images, @@ -78,4 +78,12 @@ private: }; +class SegmentExposureCompensator : public ExposureCompensator +{ +public: + void feed(const std::vector &corners, const std::vector &images, + const std::vector &masks); + void apply(int index, cv::Point corner, cv::Mat &image, const cv::Mat &mask); +}; + #endif // __OPENCV_EXPOSURE_COMPENSATE_HPP__ \ No newline at end of file diff --git a/modules/stitching/main.cpp b/modules/stitching/main.cpp index 4f97eca..4c22fb8 100644 --- a/modules/stitching/main.cpp +++ b/modules/stitching/main.cpp @@ -71,7 +71,7 @@ void printUsage() << "\t[--wavecorrect (no|yes)]\n" << "\t[--warp (plane|cylindrical|spherical)]\n" << "\t[--exposcomp (no|overlap)]\n" - << "\t[--seam (no|voronoi|graphcut)]\n" + << "\t[--seam (no|voronoi|gc_color|gc_colorgrad)]\n" << "\t[--blend (no|feather|multiband)]\n" << "\t[--numbands ]\n" << "\t[--output ]\n\n"; @@ -95,7 +95,7 @@ int warp_type = Warper::SPHERICAL; int expos_comp_type = ExposureCompensator::OVERLAP; bool user_match_conf = false; float match_conf = 0.6f; -int seam_find_type = SeamFinder::GRAPH_CUT; +int seam_find_type = SeamFinder::GC_COLOR; int blend_type = Blender::MULTI_BAND; int numbands = 5; string result_name = "result.png"; @@ -201,6 +201,8 @@ int parseCmdArgs(int argc, char** argv) expos_comp_type = ExposureCompensator::NO; else if (string(argv[i + 1]) == "overlap") expos_comp_type = ExposureCompensator::OVERLAP; + else if (string(argv[i + 1]) == "segment") + expos_comp_type = ExposureCompensator::SEGMENT; else { cout << "Bad exposure compensation method\n"; @@ -214,8 +216,10 @@ int parseCmdArgs(int argc, char** argv) seam_find_type = SeamFinder::NO; else if (string(argv[i + 1]) == "voronoi") seam_find_type = SeamFinder::VORONOI; - else if (string(argv[i + 1]) == "graphcut") - seam_find_type = SeamFinder::GRAPH_CUT; + else if (string(argv[i + 1]) == "gc_color") + seam_find_type = SeamFinder::GC_COLOR; + else if (string(argv[i + 1]) == "gc_colorgrad") + seam_find_type = SeamFinder::GC_COLOR_GRAD; else { cout << "Bad seam finding method\n"; diff --git a/modules/stitching/matchers.cpp b/modules/stitching/matchers.cpp index a6a8964..9301306 100644 --- a/modules/stitching/matchers.cpp +++ b/modules/stitching/matchers.cpp @@ -65,8 +65,8 @@ namespace class CpuSurfFeaturesFinder : public FeaturesFinder { public: - inline CpuSurfFeaturesFinder(double hess_thresh, int num_octaves, int num_layers, - int num_octaves_descr, int num_layers_descr) + CpuSurfFeaturesFinder(double hess_thresh, int num_octaves, int num_layers, + int num_octaves_descr, int num_layers_descr) { detector_ = new SurfFeatureDetector(hess_thresh, num_octaves, num_layers); extractor_ = new SurfDescriptorExtractor(num_octaves_descr, num_layers_descr); @@ -80,20 +80,12 @@ namespace Ptr extractor_; }; - void CpuSurfFeaturesFinder::find(const Mat &image, ImageFeatures &features) - { - Mat gray_image; - CV_Assert(image.depth() == CV_8U); - cvtColor(image, gray_image, CV_BGR2GRAY); - detector_->detect(gray_image, features.keypoints); - extractor_->compute(gray_image, features.keypoints, features.descriptors); - } - + class GpuSurfFeaturesFinder : public FeaturesFinder { public: - inline GpuSurfFeaturesFinder(double hess_thresh, int num_octaves, int num_layers, - int num_octaves_descr, int num_layers_descr) + GpuSurfFeaturesFinder(double hess_thresh, int num_octaves, int num_layers, + int num_octaves_descr, int num_layers_descr) { surf_.keypointsRatio = 0.1f; surf_.hessianThreshold = hess_thresh; @@ -113,6 +105,17 @@ namespace int num_octaves_descr_, num_layers_descr_; }; + + void CpuSurfFeaturesFinder::find(const Mat &image, ImageFeatures &features) + { + Mat gray_image; + CV_Assert(image.depth() == CV_8U); + cvtColor(image, gray_image, CV_BGR2GRAY); + detector_->detect(gray_image, features.keypoints); + extractor_->compute(gray_image, features.keypoints, features.descriptors); + } + + void GpuSurfFeaturesFinder::find(const Mat &image, ImageFeatures &features) { GpuMat gray_image; @@ -132,7 +135,8 @@ namespace d_descriptors.download(features.descriptors); } -} +} // anonymous namespace + SurfFeaturesFinder::SurfFeaturesFinder(bool try_use_gpu, double hess_thresh, int num_octaves, int num_layers, int num_octaves_descr, int num_layers_descr) diff --git a/modules/stitching/seam_finders.cpp b/modules/stitching/seam_finders.cpp index 89f7394..c2eb8eb 100644 --- a/modules/stitching/seam_finders.cpp +++ b/modules/stitching/seam_finders.cpp @@ -52,8 +52,10 @@ Ptr SeamFinder::createDefault(int type) return new NoSeamFinder(); if (type == VORONOI) return new VoronoiSeamFinder(); - if (type == GRAPH_CUT) - return new GraphCutSeamFinder(); + if (type == GC_COLOR) + return new GraphCutSeamFinder(GraphCutSeamFinder::COST_COLOR); + if (type == GC_COLOR_GRAD) + return new GraphCutSeamFinder(GraphCutSeamFinder::COST_COLOR_GRAD); CV_Error(CV_StsBadArg, "unsupported seam finding method"); return NULL; } @@ -64,25 +66,33 @@ void PairwiseSeamFinder::find(const vector &src, const vector &corne { if (src.size() == 0) return; + + images_ = src; + corners_ = corners; + masks_ = masks; + for (size_t i = 0; i < src.size() - 1; ++i) { for (size_t j = i + 1; j < src.size(); ++j) { Rect roi; if (overlapRoi(corners[i], corners[j], src[i].size(), src[j].size(), roi)) - findInPair(src[i], src[j], corners[i], corners[j], roi, masks[i], masks[j]); + findInPair(i, j, roi); } } } -void VoronoiSeamFinder::findInPair(const Mat &img1, const Mat &img2, Point tl1, Point tl2, - Rect roi, Mat &mask1, Mat &mask2) +void VoronoiSeamFinder::findInPair(size_t first, size_t second, Rect roi) { const int gap = 10; Mat submask1(roi.height + 2 * gap, roi.width + 2 * gap, CV_8U); Mat submask2(roi.height + 2 * gap, roi.width + 2 * gap, CV_8U); + Mat img1 = images_[first], img2 = images_[second]; + Mat mask1 = masks_[first], mask2 = masks_[second]; + Point tl1 = corners_[first], tl2 = corners_[second]; + // Cut submasks with some gap for (int y = -gap; y < roi.height + gap; ++y) { @@ -127,27 +137,62 @@ void VoronoiSeamFinder::findInPair(const Mat &img1, const Mat &img2, Point tl1, } -class GraphCutSeamFinder::Impl +class GraphCutSeamFinder::Impl : public PairwiseSeamFinder { public: Impl(int cost_type, float terminal_cost, float bad_region_penalty) : cost_type_(cost_type), terminal_cost_(terminal_cost), bad_region_penalty_(bad_region_penalty) {} - void findInPair(const Mat &img1, const Mat &img2, Point tl1, Point tl2, - Rect roi, Mat &mask1, Mat &mask2); + void find(const vector &src, const vector &corners, vector &masks); + void findInPair(size_t first, size_t second, Rect roi); private: - void setGraphWeightsColor(const Mat &img1, const Mat &img2, const Mat &mask1, const Mat &mask2, - GCGraph &graph); + void setGraphWeightsColor(const Mat &img1, const Mat &img2, + const Mat &mask1, const Mat &mask2, GCGraph &graph); + void setGraphWeightsColorGrad(const Mat &img1, const Mat &img2, const Mat &dx1, const Mat &dx2, + const Mat &dy1, const Mat &dy2, const Mat &mask1, const Mat &mask2, + GCGraph &graph); + vector dx_, dy_; int cost_type_; float terminal_cost_; float bad_region_penalty_; }; -void GraphCutSeamFinder::Impl::setGraphWeightsColor(const Mat &img1, const Mat &img2, const Mat &mask1, const Mat &mask2, - GCGraph &graph) +void GraphCutSeamFinder::Impl::find(const vector &src, const vector &corners, + vector &masks) +{ + // Compute gradients + dx_.resize(src.size()); + dy_.resize(src.size()); + Mat dx, dy; + for (size_t i = 0; i < src.size(); ++i) + { + CV_Assert(src[i].channels() == 3); + Sobel(src[i], dx, CV_32F, 1, 0); + Sobel(src[i], dy, CV_32F, 0, 1); + dx_[i].create(src[i].size(), CV_32F); + dy_[i].create(src[i].size(), CV_32F); + for (int y = 0; y < src[i].rows; ++y) + { + const Point3f* dx_row = dx.ptr(y); + const Point3f* dy_row = dy.ptr(y); + float* dx_row_ = dx_[i].ptr(y); + float* dy_row_ = dy_[i].ptr(y); + for (int x = 0; x < src[i].cols; ++x) + { + dx_row_[x] = normL2(dx_row[x]); + dy_row_[x] = normL2(dy_row[x]); + } + } + } + PairwiseSeamFinder::find(src, corners, masks); +} + + +void GraphCutSeamFinder::Impl::setGraphWeightsColor(const Mat &img1, const Mat &img2, + const Mat &mask1, const Mat &mask2, GCGraph &graph) { const Size img_size = img1.size(); @@ -162,9 +207,8 @@ void GraphCutSeamFinder::Impl::setGraphWeightsColor(const Mat &img1, const Mat & } } - const float weight_eps = 1e-3f; - // Set regular edge weights + const float weight_eps = 1e-3f; for (int y = 0; y < img_size.height; ++y) { for (int x = 0; x < img_size.width; ++x) @@ -175,11 +219,9 @@ void GraphCutSeamFinder::Impl::setGraphWeightsColor(const Mat &img1, const Mat & float weight = normL2(img1.at(y, x), img2.at(y, x)) + normL2(img1.at(y, x + 1), img2.at(y, x + 1)) + weight_eps; - if (!mask1.at(y, x) || !mask1.at(y, x + 1) || !mask2.at(y, x) || !mask2.at(y, x + 1)) weight += bad_region_penalty_; - graph.addEdges(v, v + 1, weight, weight); } if (y < img_size.height - 1) @@ -187,11 +229,63 @@ void GraphCutSeamFinder::Impl::setGraphWeightsColor(const Mat &img1, const Mat & float weight = normL2(img1.at(y, x), img2.at(y, x)) + normL2(img1.at(y + 1, x), img2.at(y + 1, x)) + weight_eps; - if (!mask1.at(y, x) || !mask1.at(y + 1, x) || !mask2.at(y, x) || !mask2.at(y + 1, x)) weight += bad_region_penalty_; + graph.addEdges(v, v + img_size.width, weight, weight); + } + } + } +} + + +void GraphCutSeamFinder::Impl::setGraphWeightsColorGrad( + const Mat &img1, const Mat &img2, const Mat &dx1, const Mat &dx2, + const Mat &dy1, const Mat &dy2, const Mat &mask1, const Mat &mask2, + GCGraph &graph) +{ + const Size img_size = img1.size(); + + // Set terminal weights + for (int y = 0; y < img_size.height; ++y) + { + for (int x = 0; x < img_size.width; ++x) + { + int v = graph.addVtx(); + graph.addTermWeights(v, mask1.at(y, x) ? terminal_cost_ : 0.f, + mask2.at(y, x) ? terminal_cost_ : 0.f); + } + } + // Set regular edge weights + const float weight_eps = 1e-3f; + for (int y = 0; y < img_size.height; ++y) + { + for (int x = 0; x < img_size.width; ++x) + { + int v = y * img_size.width + x; + if (x < img_size.width - 1) + { + float grad = dx1.at(y, x) + dx1.at(y, x + 1) + + dx2.at(y, x) + dx2.at(y, x + 1) + weight_eps; + float weight = (normL2(img1.at(y, x), img2.at(y, x)) + + normL2(img1.at(y, x + 1), img2.at(y, x + 1))) / grad + + weight_eps; + if (!mask1.at(y, x) || !mask1.at(y, x + 1) || + !mask2.at(y, x) || !mask2.at(y, x + 1)) + weight += bad_region_penalty_; + graph.addEdges(v, v + 1, weight, weight); + } + if (y < img_size.height - 1) + { + float grad = dy1.at(y, x) + dy1.at(y + 1, x) + + dy2.at(y, x) + dy2.at(y + 1, x) + weight_eps; + float weight = (normL2(img1.at(y, x), img2.at(y, x)) + + normL2(img1.at(y + 1, x), img2.at(y + 1, x))) / grad + + weight_eps; + if (!mask1.at(y, x) || !mask1.at(y + 1, x) || + !mask2.at(y, x) || !mask2.at(y + 1, x)) + weight += bad_region_penalty_; graph.addEdges(v, v + img_size.width, weight, weight); } } @@ -199,15 +293,23 @@ void GraphCutSeamFinder::Impl::setGraphWeightsColor(const Mat &img1, const Mat & } -void GraphCutSeamFinder::Impl::findInPair(const Mat &img1, const Mat &img2, Point tl1, Point tl2, - Rect roi, Mat &mask1, Mat &mask2) +void GraphCutSeamFinder::Impl::findInPair(size_t first, size_t second, Rect roi) { - const int gap = 10; + Mat img1 = images_[first], img2 = images_[second]; + Mat dx1 = dx_[first], dx2 = dx_[second]; + Mat dy1 = dy_[first], dy2 = dy_[second]; + Mat mask1 = masks_[first], mask2 = masks_[second]; + Point tl1 = corners_[first], tl2 = corners_[second]; + const int gap = 10; Mat subimg1(roi.height + 2 * gap, roi.width + 2 * gap, CV_32FC3); Mat subimg2(roi.height + 2 * gap, roi.width + 2 * gap, CV_32FC3); Mat submask1(roi.height + 2 * gap, roi.width + 2 * gap, CV_8U); Mat submask2(roi.height + 2 * gap, roi.width + 2 * gap, CV_8U); + Mat subdx1(roi.height + 2 * gap, roi.width + 2 * gap, CV_32F); + Mat subdy1(roi.height + 2 * gap, roi.width + 2 * gap, CV_32F); + Mat subdx2(roi.height + 2 * gap, roi.width + 2 * gap, CV_32F); + Mat subdy2(roi.height + 2 * gap, roi.width + 2 * gap, CV_32F); // Cut subimages and submasks with some gap for (int y = -gap; y < roi.height + gap; ++y) @@ -220,11 +322,15 @@ void GraphCutSeamFinder::Impl::findInPair(const Mat &img1, const Mat &img2, Poin { subimg1.at(y + gap, x + gap) = img1.at(y1, x1); submask1.at(y + gap, x + gap) = mask1.at(y1, x1); + subdx1.at(y + gap, x + gap) = dx1.at(y1, x1); + subdy1.at(y + gap, x + gap) = dy1.at(y1, x1); } else { subimg1.at(y + gap, x + gap) = Point3f(0, 0, 0); submask1.at(y + gap, x + gap) = 0; + subdx1.at(y + gap, x + gap) = 0.f; + subdy1.at(y + gap, x + gap) = 0.f; } int y2 = roi.y - tl2.y + y; @@ -233,11 +339,15 @@ void GraphCutSeamFinder::Impl::findInPair(const Mat &img1, const Mat &img2, Poin { subimg2.at(y + gap, x + gap) = img2.at(y2, x2); submask2.at(y + gap, x + gap) = mask2.at(y2, x2); + subdx2.at(y + gap, x + gap) = dx2.at(y2, x2); + subdy2.at(y + gap, x + gap) = dy2.at(y2, x2); } else { subimg2.at(y + gap, x + gap) = Point3f(0, 0, 0); submask2.at(y + gap, x + gap) = 0; + subdx2.at(y + gap, x + gap) = 0.f; + subdy2.at(y + gap, x + gap) = 0.f; } } } @@ -252,6 +362,10 @@ void GraphCutSeamFinder::Impl::findInPair(const Mat &img1, const Mat &img2, Poin case GraphCutSeamFinder::COST_COLOR: setGraphWeightsColor(subimg1, subimg2, submask1, submask2, graph); break; + case GraphCutSeamFinder::COST_COLOR_GRAD: + setGraphWeightsColorGrad(subimg1, subimg2, subdx1, subdx2, subdy1, subdy2, + submask1, submask2, graph); + break; default: CV_Error(CV_StsBadArg, "unsupported pixel similarity measure"); } @@ -281,8 +395,8 @@ GraphCutSeamFinder::GraphCutSeamFinder(int cost_type, float terminal_cost, float : impl_(new Impl(cost_type, terminal_cost, bad_region_penalty)) {} -void GraphCutSeamFinder::findInPair(const Mat &img1, const Mat &img2, Point tl1, Point tl2, - Rect roi, Mat &mask1, Mat &mask2) +void GraphCutSeamFinder::find(const vector &src, const vector &corners, + vector &masks) { - impl_->findInPair(img1, img2, tl1, tl2, roi, mask1, mask2); + impl_->find(src, corners, masks); } diff --git a/modules/stitching/seam_finders.hpp b/modules/stitching/seam_finders.hpp index b9dca85..66c678d 100644 --- a/modules/stitching/seam_finders.hpp +++ b/modules/stitching/seam_finders.hpp @@ -47,7 +47,7 @@ class SeamFinder { public: - enum { NO, VORONOI, GRAPH_CUT }; + enum { NO, VORONOI, GC_COLOR, GC_COLOR_GRAD }; static cv::Ptr createDefault(int type); virtual ~SeamFinder() {} @@ -66,34 +66,36 @@ public: class PairwiseSeamFinder : public SeamFinder { public: - void find(const std::vector &src, const std::vector &corners, - std::vector &masks); + virtual void find(const std::vector &src, const std::vector &corners, + std::vector &masks); + protected: - virtual void findInPair(const cv::Mat &img1, const cv::Mat &img2, cv::Point tl1, cv::Point tl2, - cv::Rect roi, cv::Mat &mask1, cv::Mat &mask2) = 0; + virtual void findInPair(size_t first, size_t second, cv::Rect roi) = 0; + + std::vector images_; + std::vector corners_; + std::vector masks_; }; class VoronoiSeamFinder : public PairwiseSeamFinder { private: - void findInPair(const cv::Mat &img1, const cv::Mat &img2, cv::Point tl1, cv::Point tl2, - cv::Rect roi, cv::Mat &mask1, cv::Mat &mask2); + void findInPair(size_t first, size_t second, cv::Rect roi); }; -class GraphCutSeamFinder : public PairwiseSeamFinder +class GraphCutSeamFinder : public SeamFinder { public: - // TODO add COST_COLOR_GRAD support - enum { COST_COLOR }; - GraphCutSeamFinder(int cost_type = COST_COLOR, float terminal_cost = 10000.f, + enum { COST_COLOR, COST_COLOR_GRAD }; + GraphCutSeamFinder(int cost_type = COST_COLOR_GRAD, float terminal_cost = 10000.f, float bad_region_penalty = 1000.f); -private: - void findInPair(const cv::Mat &img1, const cv::Mat &img2, cv::Point tl1, cv::Point tl2, - cv::Rect roi, cv::Mat &mask1, cv::Mat &mask2); + void find(const std::vector &src, const std::vector &corners, + std::vector &masks); +private: class Impl; cv::Ptr impl_; }; diff --git a/modules/stitching/util_inl.hpp b/modules/stitching/util_inl.hpp index 12f8396..e96b153 100644 --- a/modules/stitching/util_inl.hpp +++ b/modules/stitching/util_inl.hpp @@ -92,9 +92,16 @@ B Graph::walkBreadthFirst(int from, B body) const // Some auxiliary math functions static inline +float normL2(const cv::Point3f& a) +{ + return a.x * a.x + a.y * a.y + a.z * a.z; +} + + +static inline float normL2(const cv::Point3f& a, const cv::Point3f& b) { - return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + (a.z - b.z) * (a.z - b.z); + return normL2(a - b); } -- 2.7.4