Skip to content

Add distance transform functionality - #594

Merged
sdebionne merged 13 commits into
boostorg:developfrom
harshitpant1:feature/distance_transform
Sep 3, 2026
Merged

sdebionne merged 13 commits into
boostorg:developfrom
harshitpant1:feature/distance_transform

Conversation

@harshitpant1

@harshitpant1 harshitpant1 commented Apr 3, 2021

Copy link
Copy Markdown
Contributor

Description

Adds Distance Transform functionality to GIL, using algorithms which run in linear time in the worst case.

References

Precise Euclidean distance transform -
http://www.theoryofcomputing.org/articles/v008a019/v008a019.pdf

Manhattan and chessboard distance transforms -
Principles of Digital Image Processing - core Techniques by Wilhelm Burger, Mark J.Burge.

Approximate Euclidean distance transform -
http://www.cmm.mines-paristech.fr/~marcoteg/cv/publi_pdf/MM_refs/1986_Borgefors_distance.pdf

Tasklist

  • Add test case(s)
  • Ensure all CI builds pass
  • Review and approve

@harshitpant1

harshitpant1 commented Apr 3, 2021

Copy link
Copy Markdown
Contributor Author

Aside from the first wikipedia test, all other test have been verified with openCV.

import numpy as np
import cv2 as cv

################################################################################################
# test_chessboard_uint16_t_input_uint8_t_output_distance_from_on_pixels

img2 = np.array([[255,   0, 255, 255, 255, 255, 255],
                 [255, 255, 255, 255, 255, 255, 255],
                 [255, 255, 255, 255, 255, 255, 255],
                 [255, 255, 255, 255, 255, 255, 255],
                 [255, 255, 255,   0, 255, 255, 255],
                 [255, 255, 255, 255, 255, 255, 255],
                 [255, 255, 255, 255, 255, 255, 255]], dtype = np.uint8)

#/ 8 bit (16 bit tested in gil) image used which is inverted to test distance from on pixels

test2_chessboard_three = cv.distanceTransform(img2, cv.DIST_C,cv.DIST_MASK_3)
print("\n test2_chessboard mask size 3: ")
print(test2_chessboard_three)

################################################################################################
# test_euclidean_approx_and_manhattan_uint8_t_input_float32_t_output_distance_from_off_pixels

img3 = np.array([
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 255],
 [255,   0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255,   0,   0,   0,   0,   0, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255,   0, 255, 255, 255,   0, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255,   0, 255, 255, 255,   0, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255,   0, 255, 255, 255,   0, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255,   0,   0,   0,   0,   0, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,   0, 255],
 [255,   0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,   0, 255],
 [255,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]], dtype = np.uint8)

test3_euclidean_five = cv.distanceTransform(img3, cv.DIST_L2,cv.DIST_MASK_5)
print("\n test3_euclidean approx mask size 5: ")
print(test3_euclidean_five)

test3_manhattan_five = cv.distanceTransform(img3, cv.DIST_L1,cv.DIST_MASK_5)
print("\n test3_manhattan approx mask size 5: ")
print(test3_manhattan_five)

################################################################################################
# test_all_uint8_t_input_float32_t_ouptut_distance_from_off_pixels
img4 = np.array([
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0]], dtype = np.uint8)


test4_precise_euclidean = cv.distanceTransform(img4, cv.DIST_L2,cv.DIST_MASK_PRECISE)
print("\n test4_ Precise Euclidean: ")
print(test4_precise_euclidean)

test4_euclidean_three = cv.distanceTransform(img4, cv.DIST_L2,cv.DIST_MASK_3)
print("\n test4_ Euclidean Approximation mask size 3 \n")
print(test4_euclidean_three)

test4_euclidean_five = cv.distanceTransform(img4, cv.DIST_L2,cv.DIST_MASK_5)
print("\n test4_ Euclidean Approximation mask size 5: ")
print(test4_euclidean_five)

test4_manhattan_three = cv.distanceTransform(img4, cv.DIST_L1,cv.DIST_MASK_3)
print("\n test4_ manhattan mask size 3: ")
print(test4_manhattan_three)

test4_manhattan_five = cv.distanceTransform(img4, cv.DIST_L1,cv.DIST_MASK_5)
print("\n test4_ manhattan mask size 5: ")
print(test4_manhattan_five)

test4_chessboard_three = cv.distanceTransform(img4, cv.DIST_C,cv.DIST_MASK_3)
print("\n test4_chessboard mask size 3: ")
print(test4_chessboard_three)

test4_chessboard_five = cv.distanceTransform(img4, cv.DIST_C,cv.DIST_MASK_5)
print("\n test4_chessboard mask size 3: ")
print(test4_chessboard_five)

@codecov

codecov Bot commented Apr 3, 2021

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.85%. Comparing base (4664a7e) to head (5ce9ca9).
⚠️ Report is 14 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #594      +/-   ##
===========================================
+ Coverage    82.05%   82.85%   +0.79%     
===========================================
  Files          117      119       +2     
  Lines         5384     5634     +250     
===========================================
+ Hits          4418     4668     +250     
  Misses         966      966              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@harshitpant1

harshitpant1 commented Jun 23, 2021

Copy link
Copy Markdown
Contributor Author

As you might have noticed at some places (especially in the test file) reformatting should have been done after using clang-format, but I had a feeling to keep things as they are after they have been automatically formatted.

Let me know about your preferences on this.

Note that the second last commit has proper manual formatting.

@mloskot mloskot added the cat/feature New feature or functionality label Jun 25, 2021
@mloskot

mloskot commented Jun 25, 2021

Copy link
Copy Markdown
Member

It is fine to try format any new code using the .clang-format that we are discussing as part of #596

harshitpant1 and others added 12 commits September 2, 2026 21:20
Use constexpr wherever preferred; Replace static const bool by inheriting std::true_type or std::false_type in metafunctions.
Moves 2 metafunctions: check_mask_size and check_distance_type to detail namespace and prefixes them with 'dt' (distance_transform) to avoid naming conflicts.
Uses the clang-format configuration provided in the latest commit of PR boostorg#596. Formatting is applied to test file and example file as well. I have made no changes whatsoever after using the clang-format.
@sdebionne

Copy link
Copy Markdown
Contributor

I read through all three algorithms against their cited references:

  • Chamfer mask-3 / mask-5 (two-pass forward/backward raster scan): weights match Borgefors' published optimal values (a=0.955, b=1.369 for 3×3 euclidean-approx; a=1, b=1.4, c=2.197 for 5×5), the forward/backward half-masks are structurally correct, and padding/border-sentinel indexing checked out with no out-of-bounds access at any boundary.
  • Precise Euclidean (Felzenszwalt–Huttenlocher lower-envelope-of-parabolas): the compute_1d_squared_euclidean_distance_transform code is a line-for-line match of the published pseudocode.

I verified the precise transform numerically against real Euclidean geometry (not just trusting the read-through): a 5×5 image with a single "off" pixel at the corner produced distances matching sqrt(x²+y²) exactly at every pixel.

One real inconsistency I found by probing degenerate input — images with no target pixel of the requested kind at all (e.g. distance_from::on_pixels on an all-black image):

  • The chamfer methods (mask-3/mask-5) correctly report a clamped "very large" sentinel (dst_channel_max, which is dt_infinite ≈ 1e9 for a float32 destination) for every pixel, since the sentinel just keeps propagating.
  • The precise method does not: it computes everything in squared distance internally, so the propagated sentinel is dt_infinite in squared-space, and the final sqrt(dt_infinite) ≈ 31622.8 is what actually comes out. Since dst_channel_max is still compared as dt_infinite (1e9), and 31622.8 < 1e9, the clamp never triggers — so instead of a recognizable "infinite" value, callers silently get a small, plausible-looking but meaningless distance (~31623), inconsistent with what the other two methods report for the exact same degenerate input.

@sdebionne
sdebionne force-pushed the feature/distance_transform branch from bd907af to 5ce9ca9 Compare September 2, 2026 20:40
@sdebionne sdebionne self-assigned this Sep 2, 2026
@sdebionne
sdebionne marked this pull request as draft September 2, 2026 20:42
@sdebionne

Copy link
Copy Markdown
Contributor

#584 needs to be merged first.

@sdebionne
sdebionne marked this pull request as ready for review September 2, 2026 21:22
@sdebionne
sdebionne requested a review from mloskot September 2, 2026 21:22

@mloskot mloskot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a brief review it LGTM

@sdebionne
sdebionne merged commit 30d181e into boostorg:develop Sep 3, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cat/feature New feature or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants