feat: 拼接偵測加入左右排除邊距,box 預設排除右側多出的一欄

錯位模組畫面有 6x3 共 18 格,原本只能靠過渡門檻碰巧湊到 15 格且選錯格。stitcher.exclude_x 只篩選格子,box 設定為 [0, 240],可依感測器改寫;合成畫面測試與實機連續開啟 6 次全數成功。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ykcheungab
2026-10-22 20:03:44 +08:00
parent f5738375fd
commit ad7b0d3e26
5 changed files with 71 additions and 1 deletions
+10
View File
@@ -107,6 +107,16 @@ selection.
| `single_camera` | `camera` config | frames only - no deformation field, so no force | | `single_camera` | `camera` config | frames only - no deformation field, so no force |
| `uv_unroll` | `camera` config, the mapped computational asset | unrolled frame, flow, 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 `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 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 assembled together - that is `orisys.finger.FingerRuntime.open`'s shape. `create_processor` says so
+1
View File
@@ -13,6 +13,7 @@
"min_area_frac": 0.01, "min_area_frac": 0.01,
"max_area_frac": 0.05, "max_area_frac": 0.05,
"max_aspect_ratio": 50, "max_aspect_ratio": 50,
"exclude_x": [0, 240],
"index": [1,2,3,6,7,8,11,12,13], "index": [1,2,3,6,7,8,11,12,13],
"margin": 10, "margin": 10,
"nominal_ov_pos": 100, "nominal_ov_pos": 100,
+3
View File
@@ -44,6 +44,8 @@ class Stitcher:
self.box_size = config_stitcher["box_size"] self.box_size = config_stitcher["box_size"]
# Physical tile count for box detection (often 15); grid n is the active stitch layout. # 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)) 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._blend_coef_cache = {}
self._feature_stitch_cache = { self._feature_stitch_cache = {
@@ -246,6 +248,7 @@ class Stitcher:
ncol=self.ncol, ncol=self.ncol,
show=show, show=show,
n_boxes_target=self.n_boxes_target, n_boxes_target=self.n_boxes_target,
exclude_x=self.exclude_x,
) )
self.boxes = boxes self.boxes = boxes
# Detection target is physical unit count; active stitch grid is nrow*ncol via index. # Detection target is physical unit count; active stitch grid is nrow*ncol via index.
+15 -1
View File
@@ -72,8 +72,14 @@ def detect_nonblack_regions(
show: bool = False, show: bool = False,
row_tol: float = 0.4, row_tol: float = 0.4,
save_preview_path: str = "./config/compound_eye.png", save_preview_path: str = "./config/compound_eye.png",
exclude_x: Sequence[int] = (0, 0),
) -> tuple[int, list[tuple[int, int, int, int]]]: ) -> 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: if img is None:
raise ValueError("Input image is None.") raise ValueError("Input image is None.")
@@ -81,6 +87,7 @@ def detect_nonblack_regions(
n_boxes = 0 n_boxes = 0
bk_thresh = 0 bk_thresh = 0
boxes: list[tuple[int, int, int, int]] = [] 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) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
v = hsv[:, :, 2] v = hsv[:, :, 2]
@@ -110,6 +117,11 @@ def detect_nonblack_regions(
max_area_frac=max_area_frac, max_area_frac=max_area_frac,
max_aspect_ratio=max_aspect_ratio, 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) n_boxes = len(boxes)
bk_thresh += bk_thresh_step bk_thresh += bk_thresh_step
@@ -193,6 +205,7 @@ def find_stitch_boxes(
ncol: int, ncol: int,
show: bool = False, show: bool = False,
n_boxes_target: int | None = None, n_boxes_target: int | None = None,
exclude_x: Sequence[int] = (0, 0),
): ):
"""Run bloom correction + detection; return ``(n_boxes, boxes, grid_ok)``. """Run bloom correction + detection; return ``(n_boxes, boxes, grid_ok)``.
@@ -210,6 +223,7 @@ def find_stitch_boxes(
n_boxes_target=target, n_boxes_target=target,
show=show, show=show,
row_tol=row_tol, row_tol=row_tol,
exclude_x=exclude_x,
) )
grid_ok = n_boxes == target grid_ok = n_boxes == target
return n_boxes, boxes, grid_ok return n_boxes, boxes, grid_ok
+42
View File
@@ -166,3 +166,45 @@ def test_compound_eye_processor_calls_stitcher():
def test_unknown_backend_raises(): def test_unknown_backend_raises():
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_blend_backend("torch") 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