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:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user