Find small objects in large documents with the Object Finder

Fixes #6576

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-16 15:44:00 +00:00
co-authored by Claude Haiku 5.5
parent 2a952f164c
commit e5a4af7648
2 changed files with 50 additions and 1 deletions
+28 -1
View File
@@ -144,6 +144,10 @@ pub const OBJECT_AT_WORK_PX: usize = 60_000;
/// foreground, the cursor definite foreground, outside the box definite background), the part
/// connected to the cursor is kept and the boundary is re-cut at full resolution, as the
/// rectangle mode does.
///
/// When the whole-canvas pass finds nothing in a large document (an object only a few working
/// pixels wide blurs into its surroundings), the search is repeated at finer steps in windows of
/// the same working size around the point, down to full resolution.
pub fn object_at(sampler: &dyn Sampler, canvas: Rect, x: f32, y: f32, work_px: usize) -> Option<Region> {
if canvas.width() < 4 || canvas.height() < 4 || !x.is_finite() || !y.is_finite() {
return None;
@@ -152,7 +156,30 @@ pub fn object_at(sampler: &dyn Sampler, canvas: Rect, x: f32, y: f32, work_px: u
if !canvas.contains(px, py) {
return None;
}
let step = super::scale_for(canvas, work_px.max(1024));
let work_px = work_px.max(1024);
let mut step = super::scale_for(canvas, work_px);
if let Some(r) = object_in(sampler, canvas, px, py, step) {
return Some(r);
}
// Side (working pixels) of the square windows of the finer passes.
let side = (work_px as f64).sqrt() as usize;
while step > 1 {
step = (step / 4).max(1);
let half = (side.saturating_mul(step) / 2).min(i32::MAX as usize / 4) as i32;
let window = Rect::new(px.saturating_sub(half), py.saturating_sub(half), px.saturating_add(half), py.saturating_add(half)).intersect(&canvas);
if window.width() < 4 || window.height() < 4 {
continue;
}
if let Some(r) = object_in(sampler, window, px, py, step) {
return Some(r);
}
}
None
}
/// One [`object_at`] pass over `canvas` (the document or a window of it) at working step `step`;
/// `(px, py)` lies inside `canvas`.
fn object_in(sampler: &dyn Sampler, canvas: Rect, px: i32, py: i32, step: usize) -> Option<Region> {
let img = sampler.rgb_scaled(canvas, step);
let (w, h) = (img.w, img.h);
if w < 2 || h < 2 {
+22
View File
@@ -762,6 +762,28 @@ fn object_at_on_the_background_or_off_canvas_finds_nothing() {
assert!(grabcut::object_at(&s, canvas, 400.0, 10.0, 0).is_none());
}
/// #6576: a 24 px object in a 4000×3000 document is found (a 1000×800 one still is).
#[test]
fn object_at_finds_small_object_in_large_canvas() {
let (w, h) = (4000usize, 3000usize);
let canvas = Rect::new(0, 0, w as i32, h as i32);
for (ox, oy, ow, oh) in [(188usize, 138usize, 24usize, 24usize), (1500, 1100, 1000, 800)] {
let inside = |x: usize, y: usize| x >= ox && x < ox + ow && y >= oy && y < oy + oh;
let img = RgbImage::from_fn(w, h, |x, y| if inside(x, y) { [0.8, 0.1, 0.1] } else { [0.9, 0.9, 0.9] });
let truth: Vec<bool> = (0..w * h).map(|i| inside(i % w, i / w)).collect();
let s = ImageSampler { img: &img, origin: (0, 0) };
let (cx, cy) = ((ox + ow / 2) as f32, (oy + oh / 2) as f32);
let reg = grabcut::object_at(&s, canvas, cx, cy, grabcut::OBJECT_AT_WORK_PX);
let reg = reg.unwrap_or_else(|| panic!("the {ow}×{oh} object is not found"));
let v = iou(&reg, &truth, w, h);
assert!(v > 0.8, "{ow}×{oh}: IoU {v}");
}
// The flat background around the small object is still nothing.
let img = RgbImage::from_fn(w, h, |x, y| if (188..212).contains(&x) && (138..162).contains(&y) { [0.8, 0.1, 0.1] } else { [0.9, 0.9, 0.9] });
let s = ImageSampler { img: &img, origin: (0, 0) };
assert!(grabcut::object_at(&s, canvas, 400.0, 300.0, grabcut::OBJECT_AT_WORK_PX).is_none());
}
// ---------- Select Subject on the cases reported in #2788 ----------
/// A figure whose upper part is close to the background colour (a cream shirt on a near-white