update stitch algorithm and ver to 0.3.1
This commit is contained in:
+501
-1
@@ -30,9 +30,29 @@ class Stitcher:
|
||||
self.row_tol = config_stitcher["row_tol"]
|
||||
self.box_size = config_stitcher["box_size"]
|
||||
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:
|
||||
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):
|
||||
key = (ov_pos, dim, ndim)
|
||||
@@ -48,6 +68,15 @@ class Stitcher:
|
||||
return blend_coef
|
||||
|
||||
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
|
||||
stitch_calibration = self.stitch_calibration
|
||||
ncol = self.ncol
|
||||
@@ -101,6 +130,63 @@ class Stitcher:
|
||||
|
||||
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):
|
||||
|
||||
_dim = len(img1.shape)
|
||||
@@ -125,6 +211,418 @@ class Stitcher:
|
||||
|
||||
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):
|
||||
|
||||
img_corr = self.__correct_edge_bloom(img_src)
|
||||
@@ -621,4 +1119,6 @@ class Stitcher:
|
||||
if area < min_area or area > max_area or (abs(ww - hh) > self.max_aspect_ratio):
|
||||
continue
|
||||
filtered.append((x,y,ww,hh))
|
||||
return filtered
|
||||
return filtered
|
||||
|
||||
|
||||
Reference in New Issue
Block a user