feat: support nonuniform stamp matching
This commit is contained in:
+50
-16
@@ -34,6 +34,7 @@ TOP_N = 5
|
|||||||
SCALE_MIN = 0.10
|
SCALE_MIN = 0.10
|
||||||
SCALE_MAX = 2.00
|
SCALE_MAX = 2.00
|
||||||
SCALE_STEP = 0.05
|
SCALE_STEP = 0.05
|
||||||
|
ASPECT_Y_FACTORS = (0.60, 0.70, 0.80, 0.90, 1.00, 1.10, 1.20)
|
||||||
|
|
||||||
ALPHA_THRESHOLD = 20
|
ALPHA_THRESHOLD = 20
|
||||||
LOW_SCORE_WARNING_THRESHOLD = 0.55
|
LOW_SCORE_WARNING_THRESHOLD = 0.55
|
||||||
@@ -96,6 +97,27 @@ def build_scales():
|
|||||||
return scales
|
return scales
|
||||||
|
|
||||||
|
|
||||||
|
def build_scale_variants():
|
||||||
|
variants = []
|
||||||
|
seen = set()
|
||||||
|
for scale in build_scales():
|
||||||
|
for aspect_y in ASPECT_Y_FACTORS:
|
||||||
|
scale_x = scale
|
||||||
|
scale_y = round(scale * aspect_y, 4)
|
||||||
|
key = (scale_x, scale_y, aspect_y)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
variants.append(
|
||||||
|
{
|
||||||
|
"scale_x": scale_x,
|
||||||
|
"scale_y": scale_y,
|
||||||
|
"aspect_y": aspect_y,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return variants
|
||||||
|
|
||||||
|
|
||||||
def safe_match_template(source, template, mask=None):
|
def safe_match_template(source, template, mask=None):
|
||||||
if template.shape[0] > source.shape[0] or template.shape[1] > source.shape[1]:
|
if template.shape[0] > source.shape[0] or template.shape[1] > source.shape[1]:
|
||||||
return None
|
return None
|
||||||
@@ -119,25 +141,28 @@ def safe_match_template(source, template, mask=None):
|
|||||||
return np.clip(result, 0.0, 1.0)
|
return np.clip(result, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
def resize_for_scale(image, scale, interpolation):
|
def resize_for_scale(image, scale_x, scale_y, interpolation):
|
||||||
height, width = image.shape[:2]
|
height, width = image.shape[:2]
|
||||||
new_width = max(1, int(round(width * scale)))
|
new_width = max(1, int(round(width * scale_x)))
|
||||||
new_height = max(1, int(round(height * scale)))
|
new_height = max(1, int(round(height * scale_y)))
|
||||||
return cv2.resize(image, (new_width, new_height), interpolation=interpolation)
|
return cv2.resize(image, (new_width, new_height), interpolation=interpolation)
|
||||||
|
|
||||||
|
|
||||||
def score_stamp(merged_rgb, merged_edges, stamp_path, scales):
|
def score_stamp(merged_rgb, merged_edges, stamp_path, scale_variants):
|
||||||
stamp_rgba = load_rgba(stamp_path)
|
stamp_rgba = load_rgba(stamp_path)
|
||||||
stamp_rgb, stamp_mask = crop_transparent_border(stamp_rgba)
|
stamp_rgb, stamp_mask = crop_transparent_border(stamp_rgba)
|
||||||
stamp_gray = cv2.cvtColor(stamp_rgb, cv2.COLOR_RGB2GRAY)
|
stamp_gray = cv2.cvtColor(stamp_rgb, cv2.COLOR_RGB2GRAY)
|
||||||
stamp_edges = cv2.Canny(stamp_gray, 80, 160)
|
stamp_edges = cv2.Canny(stamp_gray, 80, 160)
|
||||||
|
|
||||||
best = None
|
best = None
|
||||||
for scale in scales:
|
for variant in scale_variants:
|
||||||
scaled_rgb = resize_for_scale(stamp_rgb, scale, cv2.INTER_AREA)
|
scale_x = variant["scale_x"]
|
||||||
scaled_gray = resize_for_scale(stamp_gray, scale, cv2.INTER_AREA)
|
scale_y = variant["scale_y"]
|
||||||
scaled_edges = resize_for_scale(stamp_edges, scale, cv2.INTER_NEAREST)
|
aspect_y = variant["aspect_y"]
|
||||||
scaled_mask = resize_for_scale(stamp_mask, scale, cv2.INTER_NEAREST)
|
scaled_rgb = resize_for_scale(stamp_rgb, scale_x, scale_y, cv2.INTER_AREA)
|
||||||
|
scaled_gray = resize_for_scale(stamp_gray, scale_x, scale_y, cv2.INTER_AREA)
|
||||||
|
scaled_edges = resize_for_scale(stamp_edges, scale_x, scale_y, cv2.INTER_NEAREST)
|
||||||
|
scaled_mask = resize_for_scale(stamp_mask, scale_x, scale_y, cv2.INTER_NEAREST)
|
||||||
|
|
||||||
template_result = safe_match_template(merged_rgb, scaled_rgb, scaled_mask)
|
template_result = safe_match_template(merged_rgb, scaled_rgb, scaled_mask)
|
||||||
if template_result is None:
|
if template_result is None:
|
||||||
@@ -165,7 +190,9 @@ def score_stamp(merged_rgb, merged_edges, stamp_path, scales):
|
|||||||
"stamp_path": stamp_path,
|
"stamp_path": stamp_path,
|
||||||
"x": int(x),
|
"x": int(x),
|
||||||
"y": int(y),
|
"y": int(y),
|
||||||
"scale": float(scale),
|
"scale_x": float(scale_x),
|
||||||
|
"scale_y": float(scale_y),
|
||||||
|
"aspect_y": float(aspect_y),
|
||||||
"width": int(scaled_gray.shape[1]),
|
"width": int(scaled_gray.shape[1]),
|
||||||
"height": int(scaled_gray.shape[0]),
|
"height": int(scaled_gray.shape[0]),
|
||||||
}
|
}
|
||||||
@@ -178,12 +205,12 @@ def find_best_matches(merged_image, stamp_dir, top_n):
|
|||||||
merged_rgb = rgba_to_rgb(merged_rgba)
|
merged_rgb = rgba_to_rgb(merged_rgba)
|
||||||
merged_gray = rgba_to_gray(merged_rgba)
|
merged_gray = rgba_to_gray(merged_rgba)
|
||||||
merged_edges = cv2.Canny(merged_gray, 80, 160)
|
merged_edges = cv2.Canny(merged_gray, 80, 160)
|
||||||
scales = build_scales()
|
scale_variants = build_scale_variants()
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for stamp_path in iter_images(stamp_dir):
|
for stamp_path in iter_images(stamp_dir):
|
||||||
try:
|
try:
|
||||||
result = score_stamp(merged_rgb, merged_edges, stamp_path, scales)
|
result = score_stamp(merged_rgb, merged_edges, stamp_path, scale_variants)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Skipped: {} ({})".format(stamp_path, exc))
|
print("Skipped: {} ({})".format(stamp_path, exc))
|
||||||
continue
|
continue
|
||||||
@@ -211,7 +238,9 @@ def print_result(results, matched_count, merged_image, stamp_dir):
|
|||||||
print("edge_score: {:.4f}".format(best["edge_score"]))
|
print("edge_score: {:.4f}".format(best["edge_score"]))
|
||||||
print("stamp: {}".format(Path(best["stamp_path"]).resolve()))
|
print("stamp: {}".format(Path(best["stamp_path"]).resolve()))
|
||||||
print("location: x={}, y={}".format(best["x"], best["y"]))
|
print("location: x={}, y={}".format(best["x"], best["y"]))
|
||||||
print("scale: {:.4f}".format(best["scale"]))
|
print("scale_x: {:.4f}".format(best["scale_x"]))
|
||||||
|
print("scale_y: {:.4f}".format(best["scale_y"]))
|
||||||
|
print("aspect_y: {:.4f}".format(best["aspect_y"]))
|
||||||
print("size: {}x{}".format(best["width"], best["height"]))
|
print("size: {}x{}".format(best["width"], best["height"]))
|
||||||
if best["score"] < LOW_SCORE_WARNING_THRESHOLD:
|
if best["score"] < LOW_SCORE_WARNING_THRESHOLD:
|
||||||
print("warning: best score is low; please review manually.")
|
print("warning: best score is low; please review manually.")
|
||||||
@@ -219,14 +248,19 @@ def print_result(results, matched_count, merged_image, stamp_dir):
|
|||||||
print("")
|
print("")
|
||||||
print("Top {}:".format(len(results)))
|
print("Top {}:".format(len(results)))
|
||||||
for index, item in enumerate(results, 1):
|
for index, item in enumerate(results, 1):
|
||||||
|
line = (
|
||||||
|
"{}. {:.4f} template={:.4f} edge={:.4f} "
|
||||||
|
"scale_x={:.4f} scale_y={:.4f} aspect_y={:.4f} {}"
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
"{}. {:.4f} template={:.4f} edge={:.4f} scale={:.4f} {}"
|
line.format(
|
||||||
.format(
|
|
||||||
index,
|
index,
|
||||||
item["score"],
|
item["score"],
|
||||||
item["template_score"],
|
item["template_score"],
|
||||||
item["edge_score"],
|
item["edge_score"],
|
||||||
item["scale"],
|
item["scale_x"],
|
||||||
|
item["scale_y"],
|
||||||
|
item["aspect_y"],
|
||||||
Path(item["stamp_path"]).name,
|
Path(item["stamp_path"]).name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1606,3 +1606,23 @@
|
|||||||
- [x] 明确 `1000x1000 -> 1000x800` 可由 `scale_x=1.00`、`scale_y=0.80`、`aspect_y=0.80` 覆盖
|
- [x] 明确 `1000x1000 -> 1000x800` 可由 `scale_x=1.00`、`scale_y=0.80`、`aspect_y=0.80` 覆盖
|
||||||
- [x] 更新输出字段建议:从单一 `scale` 改为 `scale_x`、`scale_y`、`aspect_y`
|
- [x] 更新输出字段建议:从单一 `scale` 改为 `scale_x`、`scale_y`、`aspect_y`
|
||||||
- [x] 验证:文档路径、标题、示例和验收点检查通过
|
- [x] 验证:文档路径、标题、示例和验收点检查通过
|
||||||
|
|
||||||
|
### 19.34 独立脚本:支持宽高不等比例印花匹配 — `match_stamp.py`
|
||||||
|
|
||||||
|
前置阅读:
|
||||||
|
|
||||||
|
- `docs/12-stamp-template-matching.md`
|
||||||
|
- `match_stamp.py`
|
||||||
|
|
||||||
|
背景:
|
||||||
|
|
||||||
|
§19.33 已明确实际合成时印花可能被非等比例缩放,例如原始 `1000x1000` 合成后变成 `1000x800`。`match_stamp.py` 需要按文档把单一 `scale` 搜索扩展为基础 `scale` 加有限 `aspect_y` 候选,提升这类场景的匹配稳定性。
|
||||||
|
|
||||||
|
任务:
|
||||||
|
|
||||||
|
- [x] 新增 `ASPECT_Y_FACTORS = 0.60 / 0.70 / 0.80 / 0.90 / 1.00 / 1.10 / 1.20`
|
||||||
|
- [x] 将缩放搜索从单一 `scale` 改为 `scale_x=scale`、`scale_y=scale*aspect_y`
|
||||||
|
- [x] RGB 模板、边缘模板和 alpha mask 使用同一组 `scale_x / scale_y` 缩放
|
||||||
|
- [x] 最佳结果保存并打印 `scale_x`、`scale_y`、`aspect_y`,不再只打印单一 `scale`
|
||||||
|
- [x] 保持独立脚本约束:不引用项目代码、不写 CSV、不修改图片
|
||||||
|
- [x] 验证:`python -m py_compile match_stamp.py` 通过;临时构造 `100x100 -> 100x80` 非等比例样本,Top 1 命中正确印花且输出 `aspect_y=0.8000`
|
||||||
|
|||||||
Reference in New Issue
Block a user