Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ removed no sooner than the next major (see `docs/API_STABILITY.md`).
public gradient, magnitude, and suppression images only to discard them.
Added safe strided `canny_into`, reusable `CannyWorkspace`, large-image
parallel stages, Python `out=`/workspace support, and a focused bit-exact
OpenCV comparison harness.
OpenCV comparison harness. Epic 118B/118D replace the full comparison-
magnitude image with parallel three-row rings and skip hysteresis when no
weak edges exist; 4K document-line reuse is 11.92× faster than the inspectable
path and measured 1.42× faster than OpenCV.
- **Exact Euclidean distance transform**: `spatialrust-vision` now computes
foreground-to-nearest-background L2 distances in linear time, supports
anisotropic pixel spacing, exposes a NumPy binding, and includes native
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ ratio; these are machine-specific measurements, not universal guarantees.
| Morphology open 5×5, reuse[^morphology-2026] | OpenCV 60.32× | OpenCV 16.25× | OpenCV 17.78× |
| Morphology open 511×511, allocate[^morphology-2026] | OpenCV 2.10× | **SpatialRust 2.61×** | **SpatialRust 2.40×** |
| Morphology open 511×511, reuse[^morphology-2026] | OpenCV 2.46× | **SpatialRust 3.25×** | **SpatialRust 2.77×** |
| Canny 3×3, reuse, document lines[^canny-2026] | OpenCV 1.77× | OpenCV 1.69× | OpenCV 1.65× |
| Canny 3×3, reuse, sensor noise[^canny-2026] | OpenCV 3.66× | OpenCV 1.92× | OpenCV 1.79× |
| Canny 3×3, reuse, document lines[^canny-2026] | OpenCV 1.40× | **SpatialRust 1.36×** | **SpatialRust 1.42×** |
| Canny 3×3, reuse, sensor noise[^canny-2026] | OpenCV 3.58× | OpenCV 1.68× | OpenCV 1.57× |
| Exact Euclidean distance transform, allocate | OpenCV 1.99× | OpenCV 1.85× | OpenCV 1.45× |
| Exact Euclidean distance transform, reuse | OpenCV 1.02× | OpenCV 1.06× | **SpatialRust 1.07×** |

Expand All @@ -186,12 +186,14 @@ records the exact environment and methodology.
5×5 latency by 20.7× at 1080p and 26.7× at 4K; OpenCV still leads the
standalone operation.

[^canny-2026]: The allocation-light 3×3 path keeps inspectable intermediates
opt-in, adds caller-owned output plus reusable `CannyWorkspace`, and
parallelizes magnitude, directional suppression, and packed output on large
images. The focused OpenCV 4.13 receipt is bit-exact across 300 randomized
images. OpenCV still leads both named workloads; the old 10.66×–12.65× row
described the superseded always-materialize-all-intermediates path.
[^canny-2026]: The 3×3 fast path keeps inspectable intermediates opt-in, adds
caller-owned output plus reusable `CannyWorkspace`, and replaces the full
`i32` magnitude image with a parallel three-row-per-worker ring. When no weak
edges exist, it also skips unnecessary hysteresis traversal. The focused
OpenCV 4.13 receipt is bit-exact across 300 randomized images. Document-line
medians are OpenCV/SpatialRust 2.900/2.138 ms at 1080p and 11.480/8.103 ms at
4K. Dense sensor noise remains an OpenCV win. Native 4K document lines improve
from 96.914 ms inspectable to 8.134 ms ring reuse (11.92×).

[^resize-2026]: The packed RGB8 half-scale path precomputes arbitrary-scale
Q11 sampling coefficients and specializes exact 2× downsampling as a
Expand Down
2 changes: 2 additions & 0 deletions bench/opencv_canny_comparison/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ profiles at VGA, 1080p, and 4K.

Results are workload- and machine-specific. The report records raw interleaved
samples, versions, thread count, OpenCL state, and caller-owned output timings.
It also records the reusable SpatialRust workspace's reserved byte count after
each profile.
1 change: 1 addition & 0 deletions bench/opencv_canny_comparison/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def spatialrust_reuse() -> np.ndarray:
"opencv_reuse": cv_reuse,
"spatialrust_reuse": sr_reuse,
"spatialrust_reuse_speedup": cv_reuse_ms / sr_reuse_ms,
"spatialrust_workspace_allocated_bytes": workspace.allocated_bytes,
}

receipt = environment(opencv_version=cv2.__version__, spatialrust_version=sr.__version__)
Expand Down
2 changes: 2 additions & 0 deletions crates/spatialrust-py/spatialrust.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ class CannyWorkspace:
def __init__(self) -> None: ...
@property
def capacity(self) -> int: ...
@property
def allocated_bytes(self) -> int: ...

def canny_image(
image: _U8Array,
Expand Down
5 changes: 5 additions & 0 deletions crates/spatialrust-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2799,6 +2799,11 @@ impl PyCannyWorkspace {
fn capacity(&self) -> usize {
self.inner.capacity()
}

#[getter]
fn allocated_bytes(&self) -> usize {
self.inner.allocated_bytes()
}
}

/// Detects edges in a grayscale uint8 image with Canny hysteresis.
Expand Down
1 change: 1 addition & 0 deletions crates/spatialrust-py/tests/test_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ def test_canny_image_reuses_output_and_workspace():
assert actual is output
np.testing.assert_array_equal(actual, expected)
assert workspace.capacity >= image.size
assert workspace.allocated_bytes > 0


def test_feature2d_corner_detectors_and_keypoint_metadata():
Expand Down
36 changes: 35 additions & 1 deletion crates/spatialrust-vision/benches/canny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,39 @@ fn benchmark_canny(c: &mut Criterion) {
group.finish();
}

criterion_group!(benches, benchmark_canny);
fn benchmark_canny_document_lines(c: &mut Criterion) {
let mut group = c.benchmark_group("canny_document_lines");
group.sample_size(10);
for &(name, width, height) in &[("1080p", 1920, 1080), ("4k", 3840, 2160)] {
let mut data = vec![0_u8; width * height];
for y in (20..height).step_by(80) {
for row in y..(y + 3).min(height) {
data[row * width + 10..row * width + width - 10].fill(255);
}
}
let image = Image::<u8, 1>::try_new(width, height, data).unwrap();
let options = CannyOptions {
low_threshold: 80.0,
high_threshold: 160.0,
l2_gradient: true,
..Default::default()
};
group.throughput(Throughput::Elements((width * height) as u64));
group.bench_function(BenchmarkId::new("inspectable", name), |b| {
b.iter(|| black_box(canny_with_intermediates(image.view(), options).unwrap()));
});
let mut output = Image::<u8, 1>::from_pixel(width, height, [0]).unwrap();
let mut workspace = CannyWorkspace::new();
canny_into(image.view(), options, output.view_mut(), &mut workspace).unwrap();
group.bench_function(BenchmarkId::new("ring_reuse", name), |b| {
b.iter(|| {
canny_into(image.view(), options, output.view_mut(), &mut workspace).unwrap();
black_box(output.as_slice());
});
});
}
group.finish();
}

criterion_group!(benches, benchmark_canny, benchmark_canny_document_lines);
criterion_main!(benches);
Loading
Loading