update stitch algorithm and ver to 0.3.1
This commit is contained in:
@@ -25,7 +25,7 @@ pip install orisys-{version}-{python-version}-{python-version}-{os}_{machine}.wh
|
|||||||
```
|
```
|
||||||
如
|
如
|
||||||
```bash
|
```bash
|
||||||
pip install orisys-0.3.0-cp310-cp310-win_amd64.whl
|
pip install orisys-0.3.1-cp310-cp310-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **安装 CuPy(根据您的 CUDA 版本选择):**
|
3. **安装 CuPy(根据您的 CUDA 版本选择):**
|
||||||
|
|||||||
+2
-2
@@ -21,11 +21,11 @@ def install_nuitka():
|
|||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
init_file = Path("src/orisys/__init__.py")
|
init_file = Path("src/orisys/__init__.py")
|
||||||
if not init_file.exists():
|
if not init_file.exists():
|
||||||
return "0.3.0"
|
return "0.3.1"
|
||||||
for line in init_file.read_text(encoding="utf-8").splitlines():
|
for line in init_file.read_text(encoding="utf-8").splitlines():
|
||||||
if line.startswith("__version__"):
|
if line.startswith("__version__"):
|
||||||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
return "0.3.0"
|
return "0.3.1"
|
||||||
|
|
||||||
|
|
||||||
def find_compiled_extension(output_dir: Path) -> Path:
|
def find_compiled_extension(output_dir: Path) -> Path:
|
||||||
|
|||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
叠动科技软件使用手册
|
叠动科技软件使用手册
|
||||||
版本: 0.3.0
|
版本: 0.3.1
|
||||||
更新日期: 2026年3月6日
|
更新日期: 2026年3月6日
|
||||||
|
|
||||||
# 简介
|
# 简介
|
||||||
@@ -57,9 +57,9 @@ orisys SDK 以 .whl 文件形式提供,您需要根据操作系统、Python
|
|||||||
## 下载正确的 .whl 文件
|
## 下载正确的 .whl 文件
|
||||||
从官方渠道获取 .whl 文件,文件名格式通常为:
|
从官方渠道获取 .whl 文件,文件名格式通常为:
|
||||||
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
||||||
例如:orisys-0.3.0-cp310-cp310-win_amd64.whl 表示:
|
例如:orisys-0.3.1-cp310-cp310-win_amd64.whl 表示:
|
||||||
|
|
||||||
版本:0.3.0
|
版本:0.3.1
|
||||||
Python 版本:3.10(cp310)
|
Python 版本:3.10(cp310)
|
||||||
操作系统:Windows(win)
|
操作系统:Windows(win)
|
||||||
硬件架构:64位(amd64)
|
硬件架构:64位(amd64)
|
||||||
@@ -71,7 +71,7 @@ Python 版本:3.10(cp310)
|
|||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# 请将文件名替换为您实际下载的文件名
|
# 请将文件名替换为您实际下载的文件名
|
||||||
pip install orisys-0.3.0-cp310-cp310-win_amd64.whl
|
pip install orisys-0.3.1-cp310-cp310-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装 CuPy(可选)
|
## 安装 CuPy(可选)
|
||||||
@@ -108,7 +108,7 @@ python
|
|||||||
import orisys
|
import orisys
|
||||||
print(orisys.__version__)
|
print(orisys.__version__)
|
||||||
```
|
```
|
||||||
如果输出版本号(例如 0.3.0),则说明安装成功。
|
如果输出版本号(例如 0.3.1),则说明安装成功。
|
||||||
|
|
||||||
# 使用方法
|
# 使用方法
|
||||||
## 运行示例程序
|
## 运行示例程序
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if configs_dir.exists():
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="orisys",
|
name="orisys",
|
||||||
version="0.3.0",
|
version="0.3.1",
|
||||||
description="Orisys - Optical Tactile Sensor SDK",
|
description="Orisys - Optical Tactile Sensor SDK",
|
||||||
author="Your Name",
|
author="Your Name",
|
||||||
package_dir={"": "src"},
|
package_dir={"": "src"},
|
||||||
|
|||||||
@@ -30,10 +30,30 @@ class Stitcher:
|
|||||||
self.row_tol = config_stitcher["row_tol"]
|
self.row_tol = config_stitcher["row_tol"]
|
||||||
self.box_size = config_stitcher["box_size"]
|
self.box_size = config_stitcher["box_size"]
|
||||||
self._blend_coef_cache = {}
|
self._blend_coef_cache = {}
|
||||||
|
self._feature_stitch_cache = {
|
||||||
|
"ref_idx": None,
|
||||||
|
"moving_indices": None,
|
||||||
|
"tile_shapes": None,
|
||||||
|
"transforms": None,
|
||||||
|
"canvas": {},
|
||||||
|
}
|
||||||
|
self._feature_stitch_frame_idx = 0
|
||||||
|
self._feature_stitch_print_every = 30
|
||||||
|
self._feature_stitch_pad = 5
|
||||||
|
self._feature_stitch_match_brightness = False
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(f"Stitcher initialized with config: {self.config_stitcher}")
|
print(f"Stitcher initialized with config: {self.config_stitcher}")
|
||||||
|
|
||||||
|
def configure_feature_stitch(self, *, match_brightness=None, pad=None, print_every=None):
|
||||||
|
"""Update runtime options for feature-based stitching."""
|
||||||
|
if match_brightness is not None:
|
||||||
|
self._feature_stitch_match_brightness = bool(match_brightness)
|
||||||
|
if pad is not None:
|
||||||
|
self._feature_stitch_pad = max(0, int(pad))
|
||||||
|
if print_every is not None:
|
||||||
|
self._feature_stitch_print_every = max(1, int(print_every))
|
||||||
|
|
||||||
def _get_blend_coef(self, ov_pos, dim, ndim):
|
def _get_blend_coef(self, ov_pos, dim, ndim):
|
||||||
key = (ov_pos, dim, ndim)
|
key = (ov_pos, dim, ndim)
|
||||||
coef = self._blend_coef_cache.get(key)
|
coef = self._blend_coef_cache.get(key)
|
||||||
@@ -48,6 +68,15 @@ class Stitcher:
|
|||||||
return blend_coef
|
return blend_coef
|
||||||
|
|
||||||
def stitch_rt_optimized(self, change_sv=False, s_mean=28., v_mean=180., blend=True):
|
def stitch_rt_optimized(self, change_sv=False, s_mean=28., v_mean=180., blend=True):
|
||||||
|
feature_stitched = self.stitch_feature_optimized(
|
||||||
|
blend=blend,
|
||||||
|
match_brightness=self._feature_stitch_match_brightness,
|
||||||
|
pad=self._feature_stitch_pad,
|
||||||
|
print_every=self._feature_stitch_print_every,
|
||||||
|
)
|
||||||
|
if feature_stitched is not None:
|
||||||
|
return feature_stitched
|
||||||
|
|
||||||
tile_list = self.tile_list
|
tile_list = self.tile_list
|
||||||
stitch_calibration = self.stitch_calibration
|
stitch_calibration = self.stitch_calibration
|
||||||
ncol = self.ncol
|
ncol = self.ncol
|
||||||
@@ -101,6 +130,63 @@ class Stitcher:
|
|||||||
|
|
||||||
return img_stitched_height
|
return img_stitched_height
|
||||||
|
|
||||||
|
def stitch_feature_optimized(self, blend=True, match_brightness=False, pad=5, print_every=30):
|
||||||
|
"""
|
||||||
|
Stitch the current tile grid by estimating tile-to-reference transforms once and
|
||||||
|
reusing them across frames. Falls back to overlap stitching when feature matching
|
||||||
|
cannot build a valid layout.
|
||||||
|
"""
|
||||||
|
plan = self._get_feature_stitch_plan()
|
||||||
|
if plan is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ref_idx, t_ref, moving_indices, moving_tiles = plan
|
||||||
|
self._feature_stitch_frame_idx += 1
|
||||||
|
frame_idx = self._feature_stitch_frame_idx
|
||||||
|
tile_shapes = tuple(tile.shape[:2] for tile in self.tile_list[: self.nrow * self.ncol])
|
||||||
|
cache = self._feature_stitch_cache
|
||||||
|
cache_changed = (
|
||||||
|
cache["ref_idx"] != ref_idx
|
||||||
|
or cache["moving_indices"] != moving_indices
|
||||||
|
or cache["tile_shapes"] != tile_shapes
|
||||||
|
)
|
||||||
|
|
||||||
|
if cache_changed:
|
||||||
|
cache["ref_idx"] = ref_idx
|
||||||
|
cache["moving_indices"] = moving_indices
|
||||||
|
cache["tile_shapes"] = tile_shapes
|
||||||
|
cache["transforms"] = None
|
||||||
|
cache["canvas"] = {}
|
||||||
|
|
||||||
|
H_use_list = cache["transforms"]
|
||||||
|
if H_use_list is None:
|
||||||
|
do_verbose_first = self.verbose and frame_idx % print_every == 0
|
||||||
|
H_use_list = []
|
||||||
|
for j, tile in enumerate(moving_tiles):
|
||||||
|
H_use_j, _ = self._estimate_bfmatcher_stitch_transform(
|
||||||
|
t_ref,
|
||||||
|
tile,
|
||||||
|
verbose=(do_verbose_first and j == 0),
|
||||||
|
)
|
||||||
|
H_use_list.append(H_use_j)
|
||||||
|
cache["transforms"] = H_use_list
|
||||||
|
|
||||||
|
if not any(H is not None for H in H_use_list):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._render_feature_tiles(
|
||||||
|
t_ref,
|
||||||
|
moving_tiles,
|
||||||
|
H_use_list,
|
||||||
|
pad=pad,
|
||||||
|
frame_idx=frame_idx,
|
||||||
|
print_every=print_every,
|
||||||
|
verbose=self.verbose,
|
||||||
|
match_brightness=match_brightness,
|
||||||
|
canvas_cache=cache["canvas"],
|
||||||
|
blend=blend,
|
||||||
|
)
|
||||||
|
|
||||||
def __find_ov(self, img1, img2, ov_pos, dim=1):
|
def __find_ov(self, img1, img2, ov_pos, dim=1):
|
||||||
|
|
||||||
_dim = len(img1.shape)
|
_dim = len(img1.shape)
|
||||||
@@ -125,6 +211,418 @@ class Stitcher:
|
|||||||
|
|
||||||
return ov_pos, ov_1, ov_2
|
return ov_pos, ov_1, ov_2
|
||||||
|
|
||||||
|
def _get_feature_stitch_plan(self):
|
||||||
|
if not self.isstitch:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_tiles = self.nrow * self.ncol
|
||||||
|
if total_tiles <= 1 or len(self.tile_list) < total_tiles:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use the center tile as the reference frame for a stable translation-only canvas.
|
||||||
|
ref_idx = (self.nrow // 2) * self.ncol + (self.ncol // 2)
|
||||||
|
if ref_idx >= total_tiles:
|
||||||
|
return None
|
||||||
|
|
||||||
|
moving_indices = tuple(i for i in range(total_tiles) if i != ref_idx)
|
||||||
|
moving_tiles = [self.tile_list[i] for i in moving_indices]
|
||||||
|
return ref_idx, self.tile_list[ref_idx], moving_indices, moving_tiles
|
||||||
|
|
||||||
|
def _tile_to_bgr(self, img):
|
||||||
|
if img.ndim == 2:
|
||||||
|
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
||||||
|
return img
|
||||||
|
|
||||||
|
def _tile_to_gray(self, bgr):
|
||||||
|
if bgr.ndim == 2:
|
||||||
|
return bgr
|
||||||
|
return cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
|
def _ensure_u8_gray(self, gray):
|
||||||
|
if gray.dtype == np.uint8:
|
||||||
|
return gray
|
||||||
|
return cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
||||||
|
|
||||||
|
def _preprocess_gray_for_orb(self, g0, g1):
|
||||||
|
g0 = cv2.GaussianBlur(g0, (5, 5), 0)
|
||||||
|
g1 = cv2.GaussianBlur(g1, (5, 5), 0)
|
||||||
|
g0 = cv2.equalizeHist(g0)
|
||||||
|
g1 = cv2.equalizeHist(g1)
|
||||||
|
return g0, g1
|
||||||
|
|
||||||
|
def _estimate_bfmatcher_stitch_transform(
|
||||||
|
self,
|
||||||
|
t0,
|
||||||
|
t1,
|
||||||
|
*,
|
||||||
|
ransac_reproj_thresh=5.0,
|
||||||
|
ratio=0.9,
|
||||||
|
orb_nfeatures=5000,
|
||||||
|
verbose=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Estimate a 3x3 transform mapping tile coordinates into the reference tile frame.
|
||||||
|
"""
|
||||||
|
t0_bgr = self._tile_to_bgr(t0)
|
||||||
|
t1_bgr = self._tile_to_bgr(t1)
|
||||||
|
g0 = self._ensure_u8_gray(self._tile_to_gray(t0_bgr))
|
||||||
|
g1 = self._ensure_u8_gray(self._tile_to_gray(t1_bgr))
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"BF stitcher debug: t0 dtype={t0.dtype}, t1 dtype={t1.dtype}, "
|
||||||
|
f"g0 dtype={g0.dtype}, g0 range=({np.min(g0)}, {np.max(g0)}), "
|
||||||
|
f"g1 dtype={g1.dtype}, g1 range=({np.min(g1)}, {np.max(g1)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
g0, g1 = self._preprocess_gray_for_orb(g0, g1)
|
||||||
|
orb = cv2.ORB_create(
|
||||||
|
nfeatures=orb_nfeatures,
|
||||||
|
scaleFactor=1.2,
|
||||||
|
nlevels=8,
|
||||||
|
edgeThreshold=15,
|
||||||
|
fastThreshold=5,
|
||||||
|
)
|
||||||
|
k0, d0 = orb.detectAndCompute(g0, None)
|
||||||
|
k1, d1 = orb.detectAndCompute(g1, None)
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"ok": False,
|
||||||
|
"method": None,
|
||||||
|
"inliers": 0,
|
||||||
|
"mean_err": None,
|
||||||
|
"good_matches": 0,
|
||||||
|
"k0": k0,
|
||||||
|
"k1": k1,
|
||||||
|
"t0_bgr": t0_bgr,
|
||||||
|
"t1_bgr": t1_bgr,
|
||||||
|
}
|
||||||
|
if d0 is None or d1 is None or len(k0) < 4 or len(k1) < 4:
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
"OpenCV BFMatcher stitcher: not enough features.",
|
||||||
|
f"k0={0 if k0 is None else len(k0)}",
|
||||||
|
f"k1={0 if k1 is None else len(k1)}",
|
||||||
|
f"d0_is_none={d0 is None}",
|
||||||
|
f"d1_is_none={d1 is None}",
|
||||||
|
)
|
||||||
|
return None, info
|
||||||
|
|
||||||
|
matches = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False).knnMatch(d0, d1, k=2)
|
||||||
|
good = []
|
||||||
|
for m_n in matches:
|
||||||
|
if len(m_n) != 2:
|
||||||
|
continue
|
||||||
|
m, n = m_n
|
||||||
|
if m.distance < ratio * n.distance:
|
||||||
|
good.append(m)
|
||||||
|
|
||||||
|
info["good_matches"] = len(good)
|
||||||
|
if verbose:
|
||||||
|
print(f"BFMatcher good matches: {len(good)}")
|
||||||
|
if len(good) < 4:
|
||||||
|
return None, info
|
||||||
|
|
||||||
|
pts0 = np.float32([k0[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
|
||||||
|
pts1 = np.float32([k1[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
|
||||||
|
method = "affine_partial2d"
|
||||||
|
M, inlier_mask = cv2.estimateAffinePartial2D(
|
||||||
|
pts1,
|
||||||
|
pts0,
|
||||||
|
method=cv2.RANSAC,
|
||||||
|
ransacReprojThreshold=ransac_reproj_thresh,
|
||||||
|
)
|
||||||
|
if M is not None:
|
||||||
|
H = np.vstack([M, [0, 0, 1]]).astype(np.float64)
|
||||||
|
else:
|
||||||
|
method = "homography"
|
||||||
|
H, inlier_mask = cv2.findHomography(pts1, pts0, cv2.RANSAC, ransac_reproj_thresh)
|
||||||
|
|
||||||
|
info["method"] = method
|
||||||
|
info["inliers"] = int(inlier_mask.sum()) if inlier_mask is not None else 0
|
||||||
|
if verbose:
|
||||||
|
print(f"Estimated {method} inliers: {info['inliers']}")
|
||||||
|
if H is None:
|
||||||
|
return None, info
|
||||||
|
|
||||||
|
if abs(H[2, 2]) > 1e-12:
|
||||||
|
H = H / H[2, 2]
|
||||||
|
|
||||||
|
mean_err = 1e9
|
||||||
|
if inlier_mask is not None:
|
||||||
|
inlier_bool = inlier_mask.ravel().astype(bool)
|
||||||
|
if np.any(inlier_bool):
|
||||||
|
projected = cv2.perspectiveTransform(pts1[inlier_bool], H)
|
||||||
|
err = np.linalg.norm(
|
||||||
|
projected.reshape(-1, 2) - pts0[inlier_bool].reshape(-1, 2),
|
||||||
|
axis=1,
|
||||||
|
)
|
||||||
|
mean_err = float(err.mean()) if err.size else 1e9
|
||||||
|
info["mean_err"] = mean_err
|
||||||
|
info["ok"] = True
|
||||||
|
return H, info
|
||||||
|
|
||||||
|
def _translation_from_H_to_ref(self, H):
|
||||||
|
if H is None:
|
||||||
|
return 0.0, 0.0
|
||||||
|
H = np.asarray(H, dtype=np.float64)
|
||||||
|
if abs(H[2, 2]) > 1e-12:
|
||||||
|
H = H / H[2, 2]
|
||||||
|
pt = H @ np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
|
||||||
|
return float(pt[0, 0]), float(pt[1, 0])
|
||||||
|
|
||||||
|
def _tile_weight_map(self, h, w, margin=16):
|
||||||
|
"""
|
||||||
|
Linear ramp weight that is 0 at tile edges and 1.0 at margin pixels inward.
|
||||||
|
Used for feathered blending at tile overlaps.
|
||||||
|
"""
|
||||||
|
wy = np.ones(h, dtype=np.float32)
|
||||||
|
wx = np.ones(w, dtype=np.float32)
|
||||||
|
m = min(margin, h // 2, w // 2)
|
||||||
|
if m > 0:
|
||||||
|
ramp = np.linspace(0.0, 1.0, m + 1, dtype=np.float32)[1:]
|
||||||
|
wy[:m] = ramp
|
||||||
|
wy[-m:] = ramp[::-1]
|
||||||
|
wx[:m] = ramp
|
||||||
|
wx[-m:] = ramp[::-1]
|
||||||
|
return wy[:, None] * wx[None, :]
|
||||||
|
|
||||||
|
def _blend_tile_inplace(self, blend, wsum, cov, src, weight, y0, x0):
|
||||||
|
"""Copy non-overlap tiles directly; blend only when this tile touches existing content."""
|
||||||
|
h, w = src.shape[:2]
|
||||||
|
H, W = blend.shape[:2]
|
||||||
|
y0d, x0d = max(0, y0), max(0, x0)
|
||||||
|
y1d, x1d = min(H, y0 + h), min(W, x0 + w)
|
||||||
|
if y0d >= y1d or x0d >= x1d:
|
||||||
|
return
|
||||||
|
|
||||||
|
sy0, sx0 = y0d - y0, x0d - x0
|
||||||
|
sh, sw = y1d - y0d, x1d - x0d
|
||||||
|
w_patch = weight[sy0 : sy0 + sh, sx0 : sx0 + sw]
|
||||||
|
blend_patch = blend[y0d:y1d, x0d:x1d]
|
||||||
|
wsum_patch = wsum[y0d:y1d, x0d:x1d]
|
||||||
|
cov_patch = cov[y0d:y1d, x0d:x1d]
|
||||||
|
src_patch = src[sy0 : sy0 + sh, sx0 : sx0 + sw]
|
||||||
|
|
||||||
|
if not np.any(cov_patch):
|
||||||
|
blend_patch[:] = src_patch
|
||||||
|
wsum_patch[:] = w_patch
|
||||||
|
else:
|
||||||
|
total = wsum_patch + w_patch
|
||||||
|
alpha = w_patch / np.maximum(total, 1e-6)
|
||||||
|
src_patch_f = src_patch.astype(np.float32, copy=False)
|
||||||
|
blend_patch += (src_patch_f - blend_patch) * alpha[:, :, None]
|
||||||
|
wsum_patch[:] = total
|
||||||
|
|
||||||
|
cov_patch[:] = 1
|
||||||
|
|
||||||
|
def _mean_gray_bgr(self, bgr):
|
||||||
|
return float(cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).mean())
|
||||||
|
|
||||||
|
def _match_bgr_brightness(self, tile_bgr, ref_mean_gray):
|
||||||
|
delta = int(round(ref_mean_gray - self._mean_gray_bgr(tile_bgr)))
|
||||||
|
if delta == 0:
|
||||||
|
return tile_bgr
|
||||||
|
return np.clip(tile_bgr.astype(np.int16) + delta, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _largest_fully_covered_rect(self, cov):
|
||||||
|
if cov.size == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mask = cov > 0
|
||||||
|
if not np.any(mask):
|
||||||
|
return None
|
||||||
|
|
||||||
|
h, w = mask.shape
|
||||||
|
heights = np.zeros(w, dtype=np.int32)
|
||||||
|
best_area = 0
|
||||||
|
best_rect = None
|
||||||
|
for y in range(h):
|
||||||
|
heights = np.where(mask[y], heights + 1, 0)
|
||||||
|
stack = []
|
||||||
|
for x in range(w + 1):
|
||||||
|
cur_h = heights[x] if x < w else 0
|
||||||
|
start = x
|
||||||
|
while stack and stack[-1][1] > cur_h:
|
||||||
|
start0, h0 = stack.pop()
|
||||||
|
area = h0 * (x - start0)
|
||||||
|
if area > best_area:
|
||||||
|
best_area = area
|
||||||
|
best_rect = (y - h0 + 1, y + 1, start0, x)
|
||||||
|
start = start0
|
||||||
|
if not stack or stack[-1][1] < cur_h:
|
||||||
|
stack.append((start, cur_h))
|
||||||
|
return best_rect
|
||||||
|
|
||||||
|
def _compute_final_crop_rect(self, cov):
|
||||||
|
valid = cov > 0
|
||||||
|
if not np.any(valid):
|
||||||
|
return None
|
||||||
|
|
||||||
|
ys, xs = np.where(valid)
|
||||||
|
by0, by1 = int(ys.min()), int(ys.max()) + 1
|
||||||
|
bx0, bx1 = int(xs.min()), int(xs.max()) + 1
|
||||||
|
rect2 = self._largest_fully_covered_rect(cov[by0:by1, bx0:bx1])
|
||||||
|
if rect2 is None:
|
||||||
|
return by0, by1, bx0, bx1
|
||||||
|
|
||||||
|
y0, y1, x0, x1 = rect2
|
||||||
|
return by0 + y0, by0 + y1, bx0 + x0, bx0 + x1
|
||||||
|
|
||||||
|
def _geom_cache_key(self, t_ref_bgr, tiles_bgr_c, pad):
|
||||||
|
ref_h, ref_w = t_ref_bgr.shape[:2]
|
||||||
|
shapes = tuple(tb.shape[:2] for tb in tiles_bgr_c)
|
||||||
|
return ref_h, ref_w, shapes, pad
|
||||||
|
|
||||||
|
def _render_feature_tiles(
|
||||||
|
self,
|
||||||
|
t_ref_bgr,
|
||||||
|
tiles_bgr,
|
||||||
|
H_use_list,
|
||||||
|
*,
|
||||||
|
pad=5,
|
||||||
|
frame_idx=0,
|
||||||
|
print_every=30,
|
||||||
|
verbose=False,
|
||||||
|
match_brightness=False,
|
||||||
|
canvas_cache=None,
|
||||||
|
blend=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Composite multiple tiles into one canvas using translation only, where each tile
|
||||||
|
origin maps into the reference tile coordinates.
|
||||||
|
"""
|
||||||
|
if H_use_list is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if canvas_cache is None:
|
||||||
|
canvas_cache = {}
|
||||||
|
do_profile = verbose and frame_idx % print_every == 0
|
||||||
|
t_profile0 = cv2.getTickCount() / cv2.getTickFrequency() if do_profile else 0.0
|
||||||
|
|
||||||
|
t_ref_bgr = self._tile_to_bgr(t_ref_bgr)
|
||||||
|
h0c, w0c = t_ref_bgr.shape[:2]
|
||||||
|
if match_brightness:
|
||||||
|
ref_mean_gray = self._mean_gray_bgr(t_ref_bgr)
|
||||||
|
tiles_bgr_c = [self._match_bgr_brightness(self._tile_to_bgr(tb), ref_mean_gray) for tb in tiles_bgr]
|
||||||
|
else:
|
||||||
|
tiles_bgr_c = [self._tile_to_bgr(tb) for tb in tiles_bgr]
|
||||||
|
t_after_brightness = cv2.getTickCount() / cv2.getTickFrequency() if do_profile else 0.0
|
||||||
|
|
||||||
|
geom_key = self._geom_cache_key(t_ref_bgr, tiles_bgr_c, pad)
|
||||||
|
geom_cache_hit = canvas_cache.get("geom_key") == geom_key
|
||||||
|
if not geom_cache_hit:
|
||||||
|
ref_corners = np.array([[0, 0], [w0c, 0], [w0c, h0c], [0, h0c]], dtype=np.float64)
|
||||||
|
pts = [ref_corners]
|
||||||
|
for tile_bgr, H_use in zip(tiles_bgr_c, H_use_list):
|
||||||
|
if H_use is None:
|
||||||
|
continue
|
||||||
|
hi, wi = tile_bgr.shape[:2]
|
||||||
|
tx, ty = self._translation_from_H_to_ref(H_use)
|
||||||
|
pts.append(
|
||||||
|
np.array(
|
||||||
|
[[tx, ty], [tx + wi, ty], [tx + wi, ty + hi], [tx, ty + hi]],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
all_xy = np.concatenate(pts, axis=0)
|
||||||
|
x_min_f = float(all_xy[:, 0].min()) - pad
|
||||||
|
y_min_f = float(all_xy[:, 1].min()) - pad
|
||||||
|
x_max_f = float(all_xy[:, 0].max()) + pad
|
||||||
|
y_max_f = float(all_xy[:, 1].max()) + pad
|
||||||
|
out_w = max(1, int(np.ceil(x_max_f - x_min_f)))
|
||||||
|
out_h = max(1, int(np.ceil(y_max_f - y_min_f)))
|
||||||
|
ref_y0 = int(np.floor(-y_min_f))
|
||||||
|
ref_x0 = int(np.floor(-x_min_f))
|
||||||
|
|
||||||
|
pastes = []
|
||||||
|
for H_use in H_use_list:
|
||||||
|
if H_use is None:
|
||||||
|
pastes.append(None)
|
||||||
|
continue
|
||||||
|
tx, ty = self._translation_from_H_to_ref(H_use)
|
||||||
|
pastes.append((int(np.floor(ty - y_min_f)), int(np.floor(tx - x_min_f))))
|
||||||
|
|
||||||
|
canvas_cache["geom_key"] = geom_key
|
||||||
|
canvas_cache["out_w"] = out_w
|
||||||
|
canvas_cache["out_h"] = out_h
|
||||||
|
canvas_cache["ref_y0"] = ref_y0
|
||||||
|
canvas_cache["ref_x0"] = ref_x0
|
||||||
|
canvas_cache["pastes"] = pastes
|
||||||
|
canvas_cache["x_min_f"] = x_min_f
|
||||||
|
canvas_cache["y_min_f"] = y_min_f
|
||||||
|
canvas_cache["crop_rect"] = None
|
||||||
|
canvas_cache["blend_buf"] = np.zeros((out_h, out_w, 3), dtype=np.float32)
|
||||||
|
canvas_cache["wsum_buf"] = np.zeros((out_h, out_w), dtype=np.float32)
|
||||||
|
canvas_cache["cov_buf"] = np.zeros((out_h, out_w), dtype=np.uint8)
|
||||||
|
canvas_cache["weight_maps"] = {}
|
||||||
|
else:
|
||||||
|
out_w = canvas_cache["out_w"]
|
||||||
|
out_h = canvas_cache["out_h"]
|
||||||
|
ref_y0 = canvas_cache["ref_y0"]
|
||||||
|
ref_x0 = canvas_cache["ref_x0"]
|
||||||
|
pastes = canvas_cache["pastes"]
|
||||||
|
t_after_geom = cv2.getTickCount() / cv2.getTickFrequency() if do_profile else 0.0
|
||||||
|
|
||||||
|
blend_buf = canvas_cache["blend_buf"]
|
||||||
|
wsum = canvas_cache["wsum_buf"]
|
||||||
|
cov = canvas_cache["cov_buf"]
|
||||||
|
weight_maps = canvas_cache["weight_maps"]
|
||||||
|
blend_buf.fill(0)
|
||||||
|
wsum.fill(0)
|
||||||
|
cov.fill(0)
|
||||||
|
t_after_reset = cv2.getTickCount() / cv2.getTickFrequency() if do_profile else 0.0
|
||||||
|
|
||||||
|
blend_margin = 16 if blend else 0
|
||||||
|
blended_tiles = 0
|
||||||
|
for tile_bgr, H_use, paste in zip(tiles_bgr_c, H_use_list, pastes):
|
||||||
|
if H_use is None or paste is None:
|
||||||
|
continue
|
||||||
|
y0, x0 = paste
|
||||||
|
th, tw = tile_bgr.shape[:2]
|
||||||
|
wkey = (th, tw, blend_margin)
|
||||||
|
if wkey not in weight_maps:
|
||||||
|
weight_maps[wkey] = self._tile_weight_map(th, tw, blend_margin)
|
||||||
|
self._blend_tile_inplace(blend_buf, wsum, cov, tile_bgr, weight_maps[wkey], y0, x0)
|
||||||
|
blended_tiles += 1
|
||||||
|
|
||||||
|
ref_wkey = (h0c, w0c, blend_margin)
|
||||||
|
if ref_wkey not in weight_maps:
|
||||||
|
weight_maps[ref_wkey] = self._tile_weight_map(h0c, w0c, blend_margin)
|
||||||
|
self._blend_tile_inplace(blend_buf, wsum, cov, t_ref_bgr, weight_maps[ref_wkey], ref_y0, ref_x0)
|
||||||
|
t_after_blend = cv2.getTickCount() / cv2.getTickFrequency() if do_profile else 0.0
|
||||||
|
|
||||||
|
crop_rect = canvas_cache.get("crop_rect")
|
||||||
|
if crop_rect is None:
|
||||||
|
crop_rect = self._compute_final_crop_rect(cov)
|
||||||
|
canvas_cache["crop_rect"] = crop_rect
|
||||||
|
if crop_rect is not None:
|
||||||
|
y0, y1, x0, x1 = crop_rect
|
||||||
|
stitched = np.clip(blend_buf[y0:y1, x0:x1], 0, 255).astype(np.uint8)
|
||||||
|
else:
|
||||||
|
stitched = np.clip(blend_buf, 0, 255).astype(np.uint8)
|
||||||
|
t_after_finalize = cv2.getTickCount() / cv2.getTickFrequency() if do_profile else 0.0
|
||||||
|
|
||||||
|
if do_profile:
|
||||||
|
xmf = canvas_cache.get("x_min_f", 0.0)
|
||||||
|
ymf = canvas_cache.get("y_min_f", 0.0)
|
||||||
|
print(
|
||||||
|
f"stitch_feature_optimized: canvas out_w={out_w}, out_h={out_h}, "
|
||||||
|
f"x_min={xmf:.2f}, y_min={ymf:.2f}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"stitch_feature_optimized steps: "
|
||||||
|
f"brightness={t_after_brightness - t_profile0:.6f}s, "
|
||||||
|
f"geom={t_after_geom - t_after_brightness:.6f}s ({'hit' if geom_cache_hit else 'miss'}), "
|
||||||
|
f"reset={t_after_reset - t_after_geom:.6f}s, "
|
||||||
|
f"blend={t_after_blend - t_after_reset:.6f}s ({blended_tiles + 1} tiles incl ref), "
|
||||||
|
f"finalize={t_after_finalize - t_after_blend:.6f}s, "
|
||||||
|
f"total={t_after_finalize - t_profile0:.6f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
return stitched
|
||||||
|
|
||||||
def find_stitch_boxes(self,img_src,show = False):
|
def find_stitch_boxes(self,img_src,show = False):
|
||||||
|
|
||||||
img_corr = self.__correct_edge_bloom(img_src)
|
img_corr = self.__correct_edge_bloom(img_src)
|
||||||
@@ -622,3 +1120,5 @@ class Stitcher:
|
|||||||
continue
|
continue
|
||||||
filtered.append((x,y,ww,hh))
|
filtered.append((x,y,ww,hh))
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ A Python library for processing and analyzing optical tactile sensor data,
|
|||||||
including image stitching, optical flow tracking, and deformation decomposition.
|
including image stitching, optical flow tracking, and deformation decomposition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.1"
|
||||||
__author__ = "Orisys"
|
__author__ = "Orisys"
|
||||||
|
|
||||||
from .sensor import Sensor
|
from .sensor import Sensor
|
||||||
|
|||||||
+2
-1
@@ -44,7 +44,8 @@ def draw_arrows(frame, optical_flow, fps=True, threshold=5, grid_spacing=40, arr
|
|||||||
if mag[i] < threshold:
|
if mag[i] < threshold:
|
||||||
cv2.circle(image, pt1, radius=1, color=below_threshold_color, thickness=-1)
|
cv2.circle(image, pt1, radius=1, color=below_threshold_color, thickness=-1)
|
||||||
else:
|
else:
|
||||||
thickness = max(1, int(2 * mag[i] / mag.max())) if mag.max() > 0 else 2
|
# thickness = max(1, int(2 * mag[i] / mag.max())) if mag.max() > 0 else 2
|
||||||
|
thickness = 1
|
||||||
cv2.arrowedLine(image, pt1, pt2, color, thickness=thickness, tipLength=0.2)
|
cv2.arrowedLine(image, pt1, pt2, color, thickness=thickness, tipLength=0.2)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|||||||
+26
-558
@@ -6,471 +6,44 @@
|
|||||||
2. 循环获取图像并计算形变和触碰点
|
2. 循环获取图像并计算形变和触碰点
|
||||||
3. 读取各类传感器数据
|
3. 读取各类传感器数据
|
||||||
4. 可视化显示
|
4. 可视化显示
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
import argparse
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import orisys
|
import orisys
|
||||||
import numpy as np
|
|
||||||
import argparse
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
def _tile_to_bgr(img):
|
|
||||||
if img.ndim == 2:
|
|
||||||
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
|
||||||
return img
|
|
||||||
|
|
||||||
|
|
||||||
def _tile_to_gray_bgr(bgr):
|
|
||||||
if bgr.ndim == 2:
|
|
||||||
return bgr
|
|
||||||
return cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_u8_gray(gray):
|
|
||||||
if gray.dtype == np.uint8:
|
|
||||||
return gray
|
|
||||||
return cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
|
||||||
|
|
||||||
|
|
||||||
def _preprocess_gray_for_orb(g0, g1):
|
|
||||||
g0 = cv2.GaussianBlur(g0, (5, 5), 0)
|
|
||||||
g1 = cv2.GaussianBlur(g1, (5, 5), 0)
|
|
||||||
g0 = cv2.equalizeHist(g0)
|
|
||||||
g1 = cv2.equalizeHist(g1)
|
|
||||||
return g0, g1
|
|
||||||
|
|
||||||
|
|
||||||
def _translation_from_H_to_ref(H):
|
|
||||||
"""
|
|
||||||
Use only the translation part of H (tile -> reference): where tile origin (0,0) lands in ref coords.
|
|
||||||
Ignores rotation/scale/shear for fast compositing (slice paste).
|
|
||||||
"""
|
|
||||||
if H is None:
|
|
||||||
return 0.0, 0.0
|
|
||||||
H = np.asarray(H, dtype=np.float64)
|
|
||||||
if abs(H[2, 2]) > 1e-12:
|
|
||||||
H = H / H[2, 2]
|
|
||||||
pt = H @ np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
|
|
||||||
return float(pt[0, 0]), float(pt[1, 0])
|
|
||||||
|
|
||||||
|
|
||||||
def _paste_bgr(dst, src, y0, x0):
|
|
||||||
"""Paste src into dst at top-left (y0, x0), clipping to dst bounds."""
|
|
||||||
h, w = src.shape[:2]
|
|
||||||
H, W = dst.shape[:2]
|
|
||||||
y0d, x0d = max(0, y0), max(0, x0)
|
|
||||||
y1d, x1d = min(H, y0 + h), min(W, x0 + w)
|
|
||||||
if y0d >= y1d or x0d >= x1d:
|
|
||||||
return
|
|
||||||
sy0 = y0d - y0
|
|
||||||
sx0 = x0d - x0
|
|
||||||
sh = y1d - y0d
|
|
||||||
sw = x1d - x0d
|
|
||||||
dst[y0d:y1d, x0d:x1d] = src[sy0 : sy0 + sh, sx0 : sx0 + sw]
|
|
||||||
|
|
||||||
|
|
||||||
def _accumulate_tile_bgr(acc, wsum, src, y0, x0):
|
|
||||||
"""
|
|
||||||
Add tile BGR into float accumulators for mean blending where tiles overlap.
|
|
||||||
acc: (H,W,3) float32, wsum: (H,W) float32 counts per pixel.
|
|
||||||
"""
|
|
||||||
h, w = src.shape[:2]
|
|
||||||
H, W = acc.shape[:2]
|
|
||||||
y0d, x0d = max(0, y0), max(0, x0)
|
|
||||||
y1d, x1d = min(H, y0 + h), min(W, x0 + w)
|
|
||||||
if y0d >= y1d or x0d >= x1d:
|
|
||||||
return
|
|
||||||
sy0 = y0d - y0
|
|
||||||
sx0 = x0d - x0
|
|
||||||
sh = y1d - y0d
|
|
||||||
sw = x1d - x0d
|
|
||||||
patch = src[sy0 : sy0 + sh, sx0 : sx0 + sw].astype(np.float32, copy=False)
|
|
||||||
acc[y0d:y1d, x0d:x1d] += patch
|
|
||||||
wsum[y0d:y1d, x0d:x1d] += 1.0
|
|
||||||
|
|
||||||
|
|
||||||
def _finalize_mean_blend(acc, wsum):
|
|
||||||
"""Average accumulated colors; pixels with no coverage stay 0."""
|
|
||||||
out_h, out_w = wsum.shape
|
|
||||||
stitched = np.zeros((out_h, out_w, 3), dtype=np.uint8)
|
|
||||||
valid = wsum > 1e-6
|
|
||||||
stitched[valid] = np.clip(acc[valid] / wsum[valid, None], 0.0, 255.0).astype(np.uint8)
|
|
||||||
return stitched
|
|
||||||
|
|
||||||
|
|
||||||
def _h_list_signature(H_use_list):
|
|
||||||
"""Stable signature to detect when cached canvas geometry must be rebuilt."""
|
|
||||||
parts = []
|
|
||||||
for H in H_use_list:
|
|
||||||
if H is None:
|
|
||||||
parts.append("None")
|
|
||||||
else:
|
|
||||||
parts.append(np.asarray(H, dtype=np.float64).tobytes())
|
|
||||||
return tuple(parts)
|
|
||||||
|
|
||||||
|
|
||||||
# Tuning (if stitch position is wrong or unstable):
|
|
||||||
# 1) ORB: nfeatures, fastThreshold, edgeThreshold in ORB_create.
|
|
||||||
# 2) Lowe ratio: lower => stricter matches; higher => more matches, more outliers.
|
|
||||||
# 3) ransac_reproj_thresh: affine/homography RANSAC inlier threshold (pixels).
|
|
||||||
# 4) Temporal: smooth_alpha, min_inliers, max_mean_reproj_err.
|
|
||||||
def estimate_bfmatcher_stitch_transform(
|
|
||||||
t0,
|
|
||||||
t1,
|
|
||||||
*,
|
|
||||||
H_prev=None,
|
|
||||||
smooth_alpha=0.2,
|
|
||||||
min_inliers=30,
|
|
||||||
max_mean_reproj_err=8.0,
|
|
||||||
ransac_reproj_thresh=5.0,
|
|
||||||
ratio=0.9,
|
|
||||||
orb_nfeatures=5000,
|
|
||||||
verbose=True,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Estimate 3x3 transform H_use mapping t1 coordinates -> t0 coordinates (t0 is reference).
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
t0, t1 : ndarray
|
|
||||||
Tile images (BGR or grayscale).
|
|
||||||
H_prev : ndarray or None
|
|
||||||
Previous frame's H_use for temporal smoothing; pass None on first frame.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
H_use : ndarray (3,3) or None
|
|
||||||
Normalized homography/affine-as-homography. None if not enough features or no valid model.
|
|
||||||
H_prev_out : ndarray or None
|
|
||||||
Value to pass as H_prev on the next call (same as H_use when successful, else unchanged H_prev).
|
|
||||||
info : dict
|
|
||||||
Keys may include: ok, method, inliers, mean_err, good_matches, k0, k1, t0_bgr, t1_bgr (for viz).
|
|
||||||
"""
|
|
||||||
t0_bgr = _tile_to_bgr(t0)
|
|
||||||
t1_bgr = _tile_to_bgr(t1)
|
|
||||||
g0 = _ensure_u8_gray(_tile_to_gray_bgr(t0_bgr))
|
|
||||||
g1 = _ensure_u8_gray(_tile_to_gray_bgr(t1_bgr))
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
print(
|
|
||||||
f"BF stitcher debug: t0 dtype={t0.dtype}, t1 dtype={t1.dtype}, "
|
|
||||||
f"g0 dtype={g0.dtype}, g0 range=({np.min(g0)}, {np.max(g0)}), "
|
|
||||||
f"g1 dtype={g1.dtype}, g1 range=({np.min(g1)}, {np.max(g1)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
g0, g1 = _preprocess_gray_for_orb(g0, g1)
|
|
||||||
|
|
||||||
orb = cv2.ORB_create(
|
|
||||||
nfeatures=orb_nfeatures, scaleFactor=1.2, nlevels=8, edgeThreshold=15, fastThreshold=5
|
|
||||||
)
|
|
||||||
k0, d0 = orb.detectAndCompute(g0, None)
|
|
||||||
k1, d1 = orb.detectAndCompute(g1, None)
|
|
||||||
|
|
||||||
info = {
|
|
||||||
"ok": False,
|
|
||||||
"method": None,
|
|
||||||
"inliers": 0,
|
|
||||||
"mean_err": None,
|
|
||||||
"good_matches": 0,
|
|
||||||
"k0": k0,
|
|
||||||
"k1": k1,
|
|
||||||
"t0_bgr": t0_bgr,
|
|
||||||
"t1_bgr": t1_bgr,
|
|
||||||
}
|
|
||||||
|
|
||||||
if d0 is None or d1 is None or len(k0) < 4 or len(k1) < 4:
|
|
||||||
if verbose:
|
|
||||||
print(
|
|
||||||
"OpenCV BFMatcher stitcher: not enough features.",
|
|
||||||
f"k0={0 if k0 is None else len(k0)}",
|
|
||||||
f"k1={0 if k1 is None else len(k1)}",
|
|
||||||
f"d0_is_none={d0 is None}",
|
|
||||||
f"d1_is_none={d1 is None}",
|
|
||||||
)
|
|
||||||
return None, H_prev, info
|
|
||||||
|
|
||||||
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
|
|
||||||
matches = bf.knnMatch(d0, d1, k=2)
|
|
||||||
|
|
||||||
good = []
|
|
||||||
for m_n in matches:
|
|
||||||
if len(m_n) != 2:
|
|
||||||
continue
|
|
||||||
m, n = m_n
|
|
||||||
if m.distance < ratio * n.distance:
|
|
||||||
good.append(m)
|
|
||||||
info["good_matches"] = len(good)
|
|
||||||
info["good"] = good
|
|
||||||
if verbose:
|
|
||||||
print(f"BFMatcher good matches: {len(good)}")
|
|
||||||
|
|
||||||
if len(good) < 4:
|
|
||||||
return None, H_prev, info
|
|
||||||
|
|
||||||
pts0 = np.float32([k0[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
|
|
||||||
pts1 = np.float32([k1[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
|
|
||||||
|
|
||||||
method = "affine_partial2d"
|
|
||||||
M, inliers_aff = cv2.estimateAffinePartial2D(
|
|
||||||
pts1, pts0, method=cv2.RANSAC, ransacReprojThreshold=ransac_reproj_thresh
|
|
||||||
)
|
|
||||||
if M is not None:
|
|
||||||
H = np.vstack([M, [0, 0, 1]]).astype(np.float64)
|
|
||||||
inlier_mask = inliers_aff
|
|
||||||
else:
|
|
||||||
method = "homography"
|
|
||||||
H, inlier_mask = cv2.findHomography(pts1, pts0, cv2.RANSAC, ransac_reproj_thresh)
|
|
||||||
|
|
||||||
inliers = int(inlier_mask.sum()) if inlier_mask is not None else 0
|
|
||||||
info["method"] = method
|
|
||||||
info["inliers"] = inliers
|
|
||||||
if verbose:
|
|
||||||
print(f"Estimated {method} inliers: {inliers}")
|
|
||||||
|
|
||||||
if H is None:
|
|
||||||
info["transform_none"] = True
|
|
||||||
return None, H_prev, info
|
|
||||||
|
|
||||||
if abs(H[2, 2]) > 1e-12:
|
|
||||||
H = H / H[2, 2]
|
|
||||||
|
|
||||||
mean_err = 1e9
|
|
||||||
if inlier_mask is not None:
|
|
||||||
inlier_bool = inlier_mask.ravel().astype(bool)
|
|
||||||
if np.any(inlier_bool):
|
|
||||||
projected = cv2.perspectiveTransform(pts1[inlier_bool], H)
|
|
||||||
err = np.linalg.norm(
|
|
||||||
projected.reshape(-1, 2) - pts0[inlier_bool].reshape(-1, 2), axis=1
|
|
||||||
)
|
|
||||||
mean_err = float(err.mean()) if err.size else 1e9
|
|
||||||
info["mean_err"] = mean_err
|
|
||||||
|
|
||||||
H_use = H
|
|
||||||
if H_prev is not None:
|
|
||||||
if inliers < min_inliers or mean_err > max_mean_reproj_err:
|
|
||||||
H_use = H_prev
|
|
||||||
else:
|
|
||||||
H_use = smooth_alpha * H + (1.0 - smooth_alpha) * H_prev
|
|
||||||
if abs(H_use[2, 2]) > 1e-12:
|
|
||||||
H_use = H_use / H_use[2, 2]
|
|
||||||
|
|
||||||
info["ok"] = True
|
|
||||||
return H_use, H_use.copy(), info
|
|
||||||
|
|
||||||
|
|
||||||
def render_stitched_preview(
|
|
||||||
t0_bgr,
|
|
||||||
t1_bgr,
|
|
||||||
H_use,
|
|
||||||
*,
|
|
||||||
pad=None,
|
|
||||||
frame_idx=0,
|
|
||||||
print_every=30,
|
|
||||||
verbose=True,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Warp t1 with H_use (t1->t0), paste t0 on a fixed padded canvas. Returns stitched BGR image.
|
|
||||||
"""
|
|
||||||
if H_use is None:
|
|
||||||
return None
|
|
||||||
h0c, w0c = t0_bgr.shape[:2]
|
|
||||||
if pad is None:
|
|
||||||
pad = max(50, int(0.1 * w0c))
|
|
||||||
out_w = w0c + 2 * pad
|
|
||||||
out_h = h0c + 2 * pad
|
|
||||||
T_pad = np.array([[1, 0, pad], [0, 1, pad], [0, 0, 1]], dtype=np.float64)
|
|
||||||
H_total = T_pad @ H_use
|
|
||||||
|
|
||||||
if verbose and frame_idx % print_every == 0:
|
|
||||||
h1c, w1c = t1_bgr.shape[:2]
|
|
||||||
corners1 = np.float32([[0, 0], [w1c, 0], [w1c, h1c], [0, h1c]]).reshape(-1, 1, 2)
|
|
||||||
corners1_warped = cv2.perspectiveTransform(corners1, H_total).reshape(-1, 2)
|
|
||||||
corners0_out = np.float32(
|
|
||||||
[
|
|
||||||
[pad, pad],
|
|
||||||
[pad + w0c, pad],
|
|
||||||
[pad + w0c, pad + h0c],
|
|
||||||
[pad, pad + h0c],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
print("=== Stitch transform debug ===")
|
|
||||||
print("H_use (t1 -> t0):\n", H_use)
|
|
||||||
print("H_total (output canvas):\n", H_total)
|
|
||||||
print("t1 corners warped into output (x,y):", corners1_warped)
|
|
||||||
print("t0 corners in output (x,y):", corners0_out)
|
|
||||||
|
|
||||||
stitched = cv2.warpPerspective(t1_bgr, H_total, (out_w, out_h), flags=cv2.INTER_LINEAR)
|
|
||||||
stitched[pad : pad + h0c, pad : pad + w0c] = t0_bgr
|
|
||||||
return stitched
|
|
||||||
|
|
||||||
|
|
||||||
def _geom_cache_key(t_ref_bgr, tiles_bgr_c, H_use_list, pad):
|
|
||||||
"""Invalidate canvas layout when ref/tile sizes, transforms, or pad change."""
|
|
||||||
ref_h, ref_w = t_ref_bgr.shape[:2]
|
|
||||||
shapes = tuple(tb.shape[:2] for tb in tiles_bgr_c)
|
|
||||||
return (ref_h, ref_w, shapes, pad, _h_list_signature(H_use_list))
|
|
||||||
|
|
||||||
|
|
||||||
def render_multi_tiles_preview(
|
|
||||||
t_ref_bgr,
|
|
||||||
tiles_bgr,
|
|
||||||
H_use_list,
|
|
||||||
*,
|
|
||||||
pad=60,
|
|
||||||
frame_idx=0,
|
|
||||||
print_every=30,
|
|
||||||
verbose=False,
|
|
||||||
canvas_cache=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Composite multiple tiles into one canvas using translation only (where each tile's
|
|
||||||
origin maps in ref coords). No per-tile warpPerspective.
|
|
||||||
|
|
||||||
- t_ref_bgr: reference tile; output is in a shifted ref-aligned frame.
|
|
||||||
- each H_use maps tile_i -> reference; only the translation of origin (0,0) is used.
|
|
||||||
- tiles_bgr and H_use_list must have the same length.
|
|
||||||
- canvas_cache: optional dict, updated in place to reuse out_w/out_h and paste origins
|
|
||||||
when geometry is unchanged.
|
|
||||||
Overlapping regions use mean blending (equal weight per contributing tile).
|
|
||||||
"""
|
|
||||||
if H_use_list is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if canvas_cache is None:
|
|
||||||
canvas_cache = {}
|
|
||||||
|
|
||||||
t_ref_bgr = _tile_to_bgr(t_ref_bgr)
|
|
||||||
h0c, w0c = t_ref_bgr.shape[:2]
|
|
||||||
tiles_bgr_c = [_tile_to_bgr(tb) for tb in tiles_bgr]
|
|
||||||
|
|
||||||
geom_key = _geom_cache_key(t_ref_bgr, tiles_bgr_c, H_use_list, pad)
|
|
||||||
|
|
||||||
if canvas_cache.get("geom_key") != geom_key:
|
|
||||||
ref_corners = np.array([[0, 0], [w0c, 0], [w0c, h0c], [0, h0c]], dtype=np.float64)
|
|
||||||
pts = [ref_corners]
|
|
||||||
for tile_bgr, H_use in zip(tiles_bgr_c, H_use_list):
|
|
||||||
if H_use is None:
|
|
||||||
continue
|
|
||||||
hi, wi = tile_bgr.shape[:2]
|
|
||||||
tx, ty = _translation_from_H_to_ref(H_use)
|
|
||||||
pts.append(
|
|
||||||
np.array(
|
|
||||||
[[tx, ty], [tx + wi, ty], [tx + wi, ty + hi], [tx, ty + hi]],
|
|
||||||
dtype=np.float64,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
all_xy = np.concatenate(pts, axis=0)
|
|
||||||
x_min_f = float(all_xy[:, 0].min()) - pad
|
|
||||||
y_min_f = float(all_xy[:, 1].min()) - pad
|
|
||||||
x_max_f = float(all_xy[:, 0].max()) + pad
|
|
||||||
y_max_f = float(all_xy[:, 1].max()) + pad
|
|
||||||
|
|
||||||
out_w = max(1, int(np.ceil(x_max_f - x_min_f)))
|
|
||||||
out_h = max(1, int(np.ceil(y_max_f - y_min_f)))
|
|
||||||
ref_y0 = int(np.floor(0.0 - y_min_f))
|
|
||||||
ref_x0 = int(np.floor(0.0 - x_min_f))
|
|
||||||
|
|
||||||
pastes = []
|
|
||||||
for tile_bgr, H_use in zip(tiles_bgr_c, H_use_list):
|
|
||||||
if H_use is None:
|
|
||||||
pastes.append(None)
|
|
||||||
continue
|
|
||||||
tx, ty = _translation_from_H_to_ref(H_use)
|
|
||||||
pastes.append((int(np.floor(ty - y_min_f)), int(np.floor(tx - x_min_f))))
|
|
||||||
|
|
||||||
canvas_cache["geom_key"] = geom_key
|
|
||||||
canvas_cache["out_w"] = out_w
|
|
||||||
canvas_cache["out_h"] = out_h
|
|
||||||
canvas_cache["ref_y0"] = ref_y0
|
|
||||||
canvas_cache["ref_x0"] = ref_x0
|
|
||||||
canvas_cache["pastes"] = pastes
|
|
||||||
canvas_cache["x_min_f"] = x_min_f
|
|
||||||
canvas_cache["y_min_f"] = y_min_f
|
|
||||||
else:
|
|
||||||
out_w = canvas_cache["out_w"]
|
|
||||||
out_h = canvas_cache["out_h"]
|
|
||||||
ref_y0 = canvas_cache["ref_y0"]
|
|
||||||
ref_x0 = canvas_cache["ref_x0"]
|
|
||||||
pastes = canvas_cache["pastes"]
|
|
||||||
|
|
||||||
acc = np.zeros((out_h, out_w, 3), dtype=np.float32)
|
|
||||||
wsum = np.zeros((out_h, out_w), dtype=np.float32)
|
|
||||||
_accumulate_tile_bgr(acc, wsum, t_ref_bgr, ref_y0, ref_x0)
|
|
||||||
for tile_bgr, H_use, paste in zip(tiles_bgr_c, H_use_list, pastes):
|
|
||||||
if H_use is None or paste is None:
|
|
||||||
continue
|
|
||||||
y0, x0 = paste
|
|
||||||
_accumulate_tile_bgr(acc, wsum, tile_bgr, y0, x0)
|
|
||||||
stitched = _finalize_mean_blend(acc, wsum)
|
|
||||||
|
|
||||||
# Crop away any uncovered regions (which correspond to black padding at borders).
|
|
||||||
# Use `wsum` for robustness even if a tile's actual content is black.
|
|
||||||
valid = wsum > 1e-6
|
|
||||||
if np.any(valid):
|
|
||||||
ys, xs = np.where(valid)
|
|
||||||
y0c, y1c = int(ys.min()), int(ys.max()) + 1
|
|
||||||
x0c, x1c = int(xs.min()), int(xs.max()) + 1
|
|
||||||
stitched = stitched[y0c:y1c, x0c:x1c]
|
|
||||||
|
|
||||||
stitched = cv2.GaussianBlur(stitched, (5, 5), 0)
|
|
||||||
|
|
||||||
|
|
||||||
if verbose and frame_idx % print_every == 0:
|
|
||||||
xmf = canvas_cache.get("x_min_f", 0.0)
|
|
||||||
ymf = canvas_cache.get("y_min_f", 0.0)
|
|
||||||
print(
|
|
||||||
f"render_multi_tiles_preview: canvas out_w={out_w}, out_h={out_h}, "
|
|
||||||
f"x_min={xmf:.2f}, y_min={ymf:.2f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return stitched
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--video","-v", type=str, default="0", help="Camera ID / Video file path")
|
parser.add_argument("--video", "-v", type=str, default="0", help="Camera ID / Video file path")
|
||||||
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||||
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||||
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="Print detailed information")
|
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="Print detailed information")
|
||||||
|
parser.add_argument(
|
||||||
|
"--match-brightness",
|
||||||
|
dest="match_brightness",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable per-tile brightness matching",
|
||||||
|
)
|
||||||
|
parser.set_defaults(match_brightness=False)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
vid_src = int(args.video)
|
||||||
|
except ValueError:
|
||||||
|
vid_src = args.video
|
||||||
|
|
||||||
# 步骤1:创建传感器对象 (必要步骤),
|
# 步骤1:创建传感器对象 (必要步骤),
|
||||||
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
|
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
|
||||||
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
|
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
|
||||||
|
sensor = orisys.Sensor(vid_src, config_name=args.config, cal_path=args.cal, verbose=args.verbose)
|
||||||
# 方式1: 从视频文件读取
|
sensor.stitcher.configure_feature_stitch(match_brightness=args.match_brightness)
|
||||||
try:
|
|
||||||
input = int(args.video)
|
|
||||||
is_camera = True
|
|
||||||
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
|
||||||
except:
|
|
||||||
is_camera = False
|
|
||||||
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
|
||||||
|
|
||||||
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
|
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
|
||||||
|
|
||||||
# For stabilizing frame-to-frame stitching (reduce oscillation).
|
|
||||||
# Stores the last good transform (mapping t1 -> t0) and exponentially smooths it.
|
|
||||||
smooth_alpha = 0.2 # 0..1; higher => less smoothing, more jitter
|
|
||||||
min_inliers = 30 # if inliers are too low, keep previous transform
|
|
||||||
max_mean_reproj_err = 8.0 # if reprojection error is too high, keep previous transform
|
|
||||||
frame_idx = 0
|
|
||||||
print_every = 30 # reduce console spam; set to 1 for per-frame output
|
|
||||||
|
|
||||||
# Stitch a 3x3 (9 tiles) block from the row-major `tile_list`.
|
|
||||||
# For your 3x5 layout (nrow=3, ncol=5), this stitches the "middle 3x3"
|
|
||||||
# using the center tile of that 3x3 as the reference frame.
|
|
||||||
H_prev_tiles = {} # key: tile_list index, value: H_prev
|
|
||||||
# Cache BFMatcher-estimated transforms from the first frame.
|
|
||||||
H_use_list_cached = None
|
|
||||||
moving_indices_cached = None
|
|
||||||
crop_pad = 5 # tighter crop in stitched canvas
|
|
||||||
canvas_cache = {}
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# 步骤2:信息提取
|
# 步骤2:信息提取
|
||||||
# 步骤 2.1: 获取并拼接图像 (必要步骤)
|
# 步骤 2.1: 获取并拼接图像 (必要步骤)
|
||||||
@@ -478,118 +51,13 @@ def main():
|
|||||||
if img is None:
|
if img is None:
|
||||||
print("Failed to get image (end of stream or capture error). Exiting.")
|
print("Failed to get image (end of stream or capture error). Exiting.")
|
||||||
break
|
break
|
||||||
# boxes = sensor.stitcher.boxes
|
|
||||||
# draw boxes for checking
|
|
||||||
# for box in boxes:
|
|
||||||
# x0, y0, x1, y1 = box
|
|
||||||
# cv2.rectangle(sensor.frame, (x0, y0), (x1, y1), (0, 255, 0), 2)
|
|
||||||
# cv2.imshow("boxes", sensor.frame)
|
|
||||||
# cv2.waitKey(0)
|
|
||||||
|
|
||||||
tiles = sensor.stitcher.tile_list
|
if img is not None:
|
||||||
frame_idx += 1
|
cv2.imshow("OpenCV Stitch (feature-based)", img)
|
||||||
|
|
||||||
ncol = getattr(sensor.stitcher, "ncol", 0) or 0
|
# 步骤 2.2: 计算形变场 (必要步骤) — sensor.compute_deformation(...)
|
||||||
nrow = getattr(sensor.stitcher, "nrow", 0) or 0
|
|
||||||
|
|
||||||
grid_r, grid_c = 3, 3
|
|
||||||
needed = grid_r * grid_c
|
|
||||||
if len(tiles) < needed or ncol < grid_c or nrow < grid_r:
|
|
||||||
print(f"tile_list too small for 9-tile stitch: len={len(tiles)} ncol={ncol} nrow={nrow} needed={needed}")
|
|
||||||
cv2.waitKey(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Center the 3x3 block in the full (nrow x ncol) layout.
|
|
||||||
# Use the center tile of that 3x3 as reference frame.
|
|
||||||
start_r = (nrow - grid_r) // 2
|
|
||||||
start_c = (ncol - grid_c) // 2
|
|
||||||
|
|
||||||
idx_ref = (start_r + 1) * ncol + (start_c + 1)
|
|
||||||
if idx_ref < 0 or idx_ref >= len(tiles):
|
|
||||||
print("Computed reference tile index out of range:", idx_ref)
|
|
||||||
cv2.waitKey(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
t_ref = tiles[idx_ref]
|
|
||||||
|
|
||||||
# Stitcher: warp each moving tile into the reference tile coordinate system.
|
|
||||||
moving_tiles = []
|
|
||||||
moving_indices = []
|
|
||||||
for r in range(grid_r):
|
|
||||||
for c in range(grid_c):
|
|
||||||
idx = (start_r + r) * ncol + (start_c + c)
|
|
||||||
if idx == idx_ref:
|
|
||||||
continue
|
|
||||||
if idx < 0 or idx >= len(tiles):
|
|
||||||
continue
|
|
||||||
moving_tiles.append(tiles[idx])
|
|
||||||
moving_indices.append(idx)
|
|
||||||
|
|
||||||
# Only run BFMatcher transform estimation on the first frame.
|
|
||||||
if H_use_list_cached is None or moving_indices_cached != moving_indices:
|
|
||||||
H_use_list = []
|
|
||||||
do_verbose_first = (frame_idx % print_every == 0)
|
|
||||||
for j, (tile, tile_idx) in enumerate(zip(moving_tiles, moving_indices)):
|
|
||||||
H_prev_j = H_prev_tiles.get(tile_idx)
|
|
||||||
H_use_j, H_prev_j_out, info_j = estimate_bfmatcher_stitch_transform(
|
|
||||||
t_ref,
|
|
||||||
tile,
|
|
||||||
H_prev=H_prev_j,
|
|
||||||
smooth_alpha=smooth_alpha,
|
|
||||||
min_inliers=min_inliers,
|
|
||||||
max_mean_reproj_err=max_mean_reproj_err,
|
|
||||||
verbose=(do_verbose_first and j == 0),
|
|
||||||
)
|
|
||||||
H_prev_tiles[tile_idx] = H_prev_j_out
|
|
||||||
H_use_list.append(H_use_j)
|
|
||||||
|
|
||||||
if (
|
|
||||||
j == 0
|
|
||||||
and do_verbose_first
|
|
||||||
and info_j.get("good") is not None
|
|
||||||
and len(info_j["good"]) >= 4
|
|
||||||
):
|
|
||||||
draw = cv2.drawMatches(
|
|
||||||
info_j["t0_bgr"],
|
|
||||||
info_j["k0"],
|
|
||||||
info_j["t1_bgr"],
|
|
||||||
info_j["k1"],
|
|
||||||
info_j["good"],
|
|
||||||
None,
|
|
||||||
matchColor=(0, 255, 0),
|
|
||||||
singlePointColor=(255, 0, 0),
|
|
||||||
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
|
|
||||||
)
|
|
||||||
cv2.imshow("BFMatcher Matches (t_ref vs t_moving0)", draw)
|
|
||||||
|
|
||||||
H_use_list_cached = H_use_list
|
|
||||||
moving_indices_cached = list(moving_indices)
|
|
||||||
else:
|
|
||||||
H_use_list = H_use_list_cached
|
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
|
||||||
stitched = render_multi_tiles_preview(
|
|
||||||
t_ref,
|
|
||||||
moving_tiles,
|
|
||||||
H_use_list,
|
|
||||||
pad=crop_pad,
|
|
||||||
frame_idx=frame_idx,
|
|
||||||
print_every=print_every,
|
|
||||||
verbose=False,
|
|
||||||
canvas_cache=canvas_cache,
|
|
||||||
)
|
|
||||||
end_time = time.perf_counter()
|
|
||||||
print(f"render_multi_tiles_preview: {end_time - start_time} seconds")
|
|
||||||
|
|
||||||
if stitched is not None:
|
|
||||||
cv2.imshow("OpenCV Stitch (9 tiles, middle-ref stabilized)", stitched)
|
|
||||||
# cv2.imshow("tiles_1_2 (two rows, 2nd shifted)", canvas)
|
|
||||||
# cv2.waitKey(0)
|
|
||||||
# 步骤 2.2: 计算形变场 (必要步骤)
|
|
||||||
# is_touched = sensor.compute_deformation(check_motion=True, threshold=6)
|
|
||||||
# 步骤 2.3: 计算触碰点
|
# 步骤 2.3: 计算触碰点
|
||||||
|
# 步骤 3: 键盘控制
|
||||||
#步骤 3.5: 键盘控制
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
key = cv2.waitKey(1) & 0xFF
|
||||||
if key == ord("q"):
|
if key == ord("q"):
|
||||||
print("\n退出程序...")
|
print("\n退出程序...")
|
||||||
@@ -605,5 +73,5 @@ def main():
|
|||||||
sensor.disconnect() # 释放视频源和相关资源
|
sensor.disconnect() # 释放视频源和相关资源
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user