From ad7b0d3e265d3d45bb93c8c77d67b9db2dff79d3 Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Thu, 22 Oct 2026 20:03:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=BC=E6=8E=A5=E5=81=B5=E6=B8=AC?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=B7=A6=E5=8F=B3=E6=8E=92=E9=99=A4=E9=82=8A?= =?UTF-8?q?=E8=B7=9D=EF=BC=8Cbox=20=E9=A0=90=E8=A8=AD=E6=8E=92=E9=99=A4?= =?UTF-8?q?=E5=8F=B3=E5=81=B4=E5=A4=9A=E5=87=BA=E7=9A=84=E4=B8=80=E6=AC=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 錯位模組畫面有 6x3 共 18 格,原本只能靠過渡門檻碰巧湊到 15 格且選錯格。stitcher.exclude_x 只篩選格子,box 設定為 [0, 240],可依感測器改寫;合成畫面測試與實機連續開啟 6 次全數成功。 Co-Authored-By: Claude Opus 5.5 --- docs/sensor-bundles.md | 10 +++++++++ sensors/box/sensor.json | 1 + src/orisys/stitch/api.py | 3 +++ src/orisys/stitch/boxes.py | 16 ++++++++++++++- tests/test_stitch.py | 42 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/sensor-bundles.md b/docs/sensor-bundles.md index c373fb3..6fb2d2f 100644 --- a/docs/sensor-bundles.md +++ b/docs/sensor-bundles.md @@ -107,6 +107,16 @@ selection. | `single_camera` | `camera` config | frames only - no deformation field, so no force | | `uv_unroll` | `camera` config, the mapped computational asset | unrolled frame, flow, force | +### Tile detection margin + +`compound_eye` detects `n_boxes_target` tiles (15 by default) when it opens. A module mounted off +centre can show an extra column at one edge, and detection then never settles on 15. +`stitcher.exclude_x` is `[left, right]` in raw-frame pixels: a detected region whose centre lies +within that distance of the edge is not counted. It changes only which tiles are chosen; the frames +used afterwards are untouched. The `box` bundle ships `[0, 240]`, which drops an extra right column +(about 170 px from the edge) and keeps the outer column of an aligned module (about 320 px). A +module whose extra column is on the left needs `[240, 0]` in its own copy of `sensor.json`. + `known_backends()` and `spec_for(...)` are the registry. `uv_unroll` is not yet materialised from a profile alone, because it needs geometry, a UV domain, a tactile processor and a capture object assembled together - that is `orisys.finger.FingerRuntime.open`'s shape. `create_processor` says so diff --git a/sensors/box/sensor.json b/sensors/box/sensor.json index 1349dd4..4c2a1af 100644 --- a/sensors/box/sensor.json +++ b/sensors/box/sensor.json @@ -13,6 +13,7 @@ "min_area_frac": 0.01, "max_area_frac": 0.05, "max_aspect_ratio": 50, + "exclude_x": [0, 240], "index": [1,2,3,6,7,8,11,12,13], "margin": 10, "nominal_ov_pos": 100, diff --git a/src/orisys/stitch/api.py b/src/orisys/stitch/api.py index 1fb7214..4ce8bf6 100644 --- a/src/orisys/stitch/api.py +++ b/src/orisys/stitch/api.py @@ -44,6 +44,8 @@ class Stitcher: self.box_size = config_stitcher["box_size"] # Physical tile count for box detection (often 15); grid n is the active stitch layout. self.n_boxes_target = int(config_stitcher.get("n_boxes_target", 15)) + # (left, right) raw-frame pixels whose tiles are not counted (extra column). + self.exclude_x = tuple(int(v) for v in config_stitcher.get("exclude_x", (0, 0))) self._blend_coef_cache = {} self._feature_stitch_cache = { @@ -246,6 +248,7 @@ class Stitcher: ncol=self.ncol, show=show, n_boxes_target=self.n_boxes_target, + exclude_x=self.exclude_x, ) self.boxes = boxes # Detection target is physical unit count; active stitch grid is nrow*ncol via index. diff --git a/src/orisys/stitch/boxes.py b/src/orisys/stitch/boxes.py index 72ab9c8..9fffc5a 100644 --- a/src/orisys/stitch/boxes.py +++ b/src/orisys/stitch/boxes.py @@ -72,8 +72,14 @@ def detect_nonblack_regions( show: bool = False, row_tol: float = 0.4, save_preview_path: str = "./config/compound_eye.png", + exclude_x: Sequence[int] = (0, 0), ) -> tuple[int, list[tuple[int, int, int, int]]]: - """Detect tile boxes; returns ``(n_boxes, boxes as x0,y0,x1,y1)``.""" + """Detect tile boxes; returns ``(n_boxes, boxes as x0,y0,x1,y1)``. + + ``exclude_x`` is ``(left, right)`` in raw-frame pixels: a region whose centre + lies that close to the left or right edge is ignored. A misaligned module can + show an extra column of tiles; this keeps it out of the count. + """ if img is None: raise ValueError("Input image is None.") @@ -81,6 +87,7 @@ def detect_nonblack_regions( n_boxes = 0 bk_thresh = 0 boxes: list[tuple[int, int, int, int]] = [] + exclude_left, exclude_right = (int(value) for value in exclude_x) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) v = hsv[:, :, 2] @@ -110,6 +117,11 @@ def detect_nonblack_regions( max_area_frac=max_area_frac, max_aspect_ratio=max_aspect_ratio, ) + boxes = [ + box + for box in boxes + if exclude_left <= box[0] + box[2] / 2 <= w - exclude_right + ] n_boxes = len(boxes) bk_thresh += bk_thresh_step @@ -193,6 +205,7 @@ def find_stitch_boxes( ncol: int, show: bool = False, n_boxes_target: int | None = None, + exclude_x: Sequence[int] = (0, 0), ): """Run bloom correction + detection; return ``(n_boxes, boxes, grid_ok)``. @@ -210,6 +223,7 @@ def find_stitch_boxes( n_boxes_target=target, show=show, row_tol=row_tol, + exclude_x=exclude_x, ) grid_ok = n_boxes == target return n_boxes, boxes, grid_ok diff --git a/tests/test_stitch.py b/tests/test_stitch.py index 574ac18..52de9fd 100644 --- a/tests/test_stitch.py +++ b/tests/test_stitch.py @@ -166,3 +166,45 @@ def test_compound_eye_processor_calls_stitcher(): def test_unknown_backend_raises(): with pytest.raises(ValueError): get_blend_backend("torch") + + +def _six_by_three_frame(): + """A 1920x1080 frame showing 6x3 tiles: one column more than the 15 detected.""" + img = np.zeros((1080, 1920, 3), dtype=np.uint8) + for cx in (160, 470, 790, 1110, 1430, 1750): + for cy in (180, 520, 860): + img[cy - 120:cy + 120, cx - 120:cx + 120] = 200 + return img + + +def _detect(img, **kwargs): + from orisys.stitch.boxes import detect_nonblack_regions + + return detect_nonblack_regions( + img, box_size=(250, 250), min_area_frac=0.01, max_area_frac=0.05, + max_aspect_ratio=50, save_preview_path="", **kwargs, + ) + + +def test_an_extra_tile_column_is_counted_without_an_exclusion(): + n_boxes, _boxes = _detect(_six_by_three_frame()) + assert n_boxes == 18 + + +@pytest.mark.parametrize("exclude_x", [(330, 0), (0, 330)]) +def test_exclude_x_masks_out_the_extra_column(exclude_x): + n_boxes, boxes = _detect(_six_by_three_frame(), exclude_x=exclude_x) + assert n_boxes == 15 + centres = sorted({(x0 + x1) // 2 for x0, _y0, x1, _y1 in boxes}) + assert (160 not in centres) == (exclude_x[0] > 0) + assert (1750 not in centres) == (exclude_x[1] > 0) + + +def test_the_shipped_right_margin_keeps_an_aligned_module(): + """[0, 240] drops an extra edge column but not the outer column of a 5x3 module.""" + img = np.zeros((1080, 1920, 3), dtype=np.uint8) + for cx in (324, 642, 960, 1278, 1596): + for cy in (180, 520, 860): + img[cy - 120:cy + 120, cx - 120:cx + 120] = 200 + assert _detect(img, exclude_x=(0, 240))[0] == 15 + assert _detect(_six_by_three_frame(), exclude_x=(0, 240))[0] == 15