267 lines
8.4 KiB
Python
267 lines
8.4 KiB
Python
"""Find the source stamp image that best matches a merged clothing image.
|
|
|
|
This script is intentionally standalone and does not import cmbot project code.
|
|
Edit the paths below, or pass them on the command line:
|
|
|
|
python match_stamp.py --image output\20260623_094529\TY037\1_TY037.png --stamp-dir D:\stamps
|
|
|
|
It does not copy, move, rename, delete, or write image files. Results are
|
|
printed directly.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import cv2
|
|
import numpy as np
|
|
from PIL import Image
|
|
except ImportError as exc:
|
|
print("Missing dependency: {}".format(exc))
|
|
print("Install required packages: pip install opencv-python numpy Pillow")
|
|
raise
|
|
|
|
|
|
MERGED_IMAGE = r"D:\chengma\cmbot\output\20260623_094529\TY037\1_TY037.png"
|
|
STAMP_DIR = r"D:\chengma\印花和底图\已处理印花\卡通71(66大码200斤 KEKE已上)\横1"
|
|
|
|
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"}
|
|
TOP_N = 5
|
|
|
|
SCALE_MIN = 0.10
|
|
SCALE_MAX = 2.00
|
|
SCALE_STEP = 0.05
|
|
|
|
ALPHA_THRESHOLD = 20
|
|
LOW_SCORE_WARNING_THRESHOLD = 0.55
|
|
TEMPLATE_SCORE_WEIGHT = 0.75
|
|
EDGE_SCORE_WEIGHT = 0.25
|
|
MIN_MATCH_WIDTH = 24
|
|
MIN_MATCH_HEIGHT = 24
|
|
|
|
|
|
def is_image(path):
|
|
return path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS
|
|
|
|
|
|
def iter_images(root):
|
|
root = Path(root)
|
|
if not root.exists():
|
|
return
|
|
for path in sorted(root.rglob("*")):
|
|
if is_image(path):
|
|
yield path
|
|
|
|
|
|
def load_rgba(path):
|
|
"""Load with Pillow so Windows Chinese paths are handled reliably."""
|
|
with Image.open(str(path)) as image:
|
|
return np.array(image.convert("RGBA"))
|
|
|
|
|
|
def rgba_to_gray(rgba):
|
|
rgb = rgba[:, :, :3]
|
|
return cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
|
|
|
|
|
def rgba_to_rgb(rgba):
|
|
return rgba[:, :, :3]
|
|
|
|
|
|
def crop_transparent_border(rgba):
|
|
alpha = rgba[:, :, 3]
|
|
ys, xs = np.where(alpha > ALPHA_THRESHOLD)
|
|
if len(xs) == 0 or len(ys) == 0:
|
|
mask = np.ones(alpha.shape, dtype=np.uint8) * 255
|
|
return rgba[:, :, :3], mask
|
|
|
|
left = int(xs.min())
|
|
right = int(xs.max()) + 1
|
|
top = int(ys.min())
|
|
bottom = int(ys.max()) + 1
|
|
cropped = rgba[top:bottom, left:right]
|
|
mask = (cropped[:, :, 3] > ALPHA_THRESHOLD).astype(np.uint8) * 255
|
|
return cropped[:, :, :3], mask
|
|
|
|
|
|
def build_scales():
|
|
scales = []
|
|
value = SCALE_MIN
|
|
while value <= SCALE_MAX + 0.0001:
|
|
scales.append(round(value, 4))
|
|
value += SCALE_STEP
|
|
return scales
|
|
|
|
|
|
def safe_match_template(source, template, mask=None):
|
|
if template.shape[0] > source.shape[0] or template.shape[1] > source.shape[1]:
|
|
return None
|
|
if template.shape[0] < MIN_MATCH_HEIGHT or template.shape[1] < MIN_MATCH_WIDTH:
|
|
return None
|
|
if mask is not None and int(np.count_nonzero(mask)) < 9:
|
|
return None
|
|
|
|
match_mask = mask
|
|
if match_mask is not None and template.ndim == 3 and match_mask.ndim == 2:
|
|
channels = template.shape[2]
|
|
match_mask = cv2.merge([match_mask] * channels)
|
|
|
|
try:
|
|
result = cv2.matchTemplate(source, template, cv2.TM_CCORR_NORMED, mask=match_mask)
|
|
except cv2.error:
|
|
return None
|
|
|
|
result = np.asarray(result, dtype=np.float32)
|
|
result[~np.isfinite(result)] = -1.0
|
|
return np.clip(result, 0.0, 1.0)
|
|
|
|
|
|
def resize_for_scale(image, scale, interpolation):
|
|
height, width = image.shape[:2]
|
|
new_width = max(1, int(round(width * scale)))
|
|
new_height = max(1, int(round(height * scale)))
|
|
return cv2.resize(image, (new_width, new_height), interpolation=interpolation)
|
|
|
|
|
|
def score_stamp(merged_rgb, merged_edges, stamp_path, scales):
|
|
stamp_rgba = load_rgba(stamp_path)
|
|
stamp_rgb, stamp_mask = crop_transparent_border(stamp_rgba)
|
|
stamp_gray = cv2.cvtColor(stamp_rgb, cv2.COLOR_RGB2GRAY)
|
|
stamp_edges = cv2.Canny(stamp_gray, 80, 160)
|
|
|
|
best = None
|
|
for scale in scales:
|
|
scaled_rgb = resize_for_scale(stamp_rgb, scale, cv2.INTER_AREA)
|
|
scaled_gray = resize_for_scale(stamp_gray, scale, cv2.INTER_AREA)
|
|
scaled_edges = resize_for_scale(stamp_edges, scale, cv2.INTER_NEAREST)
|
|
scaled_mask = resize_for_scale(stamp_mask, scale, cv2.INTER_NEAREST)
|
|
|
|
template_result = safe_match_template(merged_rgb, scaled_rgb, scaled_mask)
|
|
if template_result is None:
|
|
continue
|
|
|
|
edge_result = safe_match_template(merged_edges, scaled_edges, scaled_mask)
|
|
if edge_result is None:
|
|
edge_result = np.zeros(template_result.shape, dtype=np.float32)
|
|
|
|
combined = (
|
|
TEMPLATE_SCORE_WEIGHT * template_result
|
|
+ EDGE_SCORE_WEIGHT * edge_result
|
|
)
|
|
_min_val, max_val, _min_loc, max_loc = cv2.minMaxLoc(combined)
|
|
x, y = max_loc
|
|
template_score = float(template_result[y, x])
|
|
edge_score = float(edge_result[y, x])
|
|
score = float(max_val)
|
|
|
|
if best is None or score > best["score"]:
|
|
best = {
|
|
"score": score,
|
|
"template_score": template_score,
|
|
"edge_score": edge_score,
|
|
"stamp_path": stamp_path,
|
|
"x": int(x),
|
|
"y": int(y),
|
|
"scale": float(scale),
|
|
"width": int(scaled_gray.shape[1]),
|
|
"height": int(scaled_gray.shape[0]),
|
|
}
|
|
|
|
return best
|
|
|
|
|
|
def find_best_matches(merged_image, stamp_dir, top_n):
|
|
merged_rgba = load_rgba(merged_image)
|
|
merged_rgb = rgba_to_rgb(merged_rgba)
|
|
merged_gray = rgba_to_gray(merged_rgba)
|
|
merged_edges = cv2.Canny(merged_gray, 80, 160)
|
|
scales = build_scales()
|
|
|
|
results = []
|
|
for stamp_path in iter_images(stamp_dir):
|
|
try:
|
|
result = score_stamp(merged_rgb, merged_edges, stamp_path, scales)
|
|
except Exception as exc:
|
|
print("Skipped: {} ({})".format(stamp_path, exc))
|
|
continue
|
|
if result is not None:
|
|
results.append(result)
|
|
|
|
results.sort(key=lambda item: item["score"], reverse=True)
|
|
return results[:top_n], len(results)
|
|
|
|
|
|
def print_result(results, matched_count, merged_image, stamp_dir):
|
|
print("Merged image: {}".format(Path(merged_image).resolve()))
|
|
print("Stamp dir: {}".format(Path(stamp_dir).resolve()))
|
|
print("Matched stamp candidates: {}".format(matched_count))
|
|
print("")
|
|
|
|
if not results:
|
|
print("No valid image match result.")
|
|
return
|
|
|
|
best = results[0]
|
|
print("Best match:")
|
|
print("score: {:.4f}".format(best["score"]))
|
|
print("template_score: {:.4f}".format(best["template_score"]))
|
|
print("edge_score: {:.4f}".format(best["edge_score"]))
|
|
print("stamp: {}".format(Path(best["stamp_path"]).resolve()))
|
|
print("location: x={}, y={}".format(best["x"], best["y"]))
|
|
print("scale: {:.4f}".format(best["scale"]))
|
|
print("size: {}x{}".format(best["width"], best["height"]))
|
|
if best["score"] < LOW_SCORE_WARNING_THRESHOLD:
|
|
print("warning: best score is low; please review manually.")
|
|
|
|
print("")
|
|
print("Top {}:".format(len(results)))
|
|
for index, item in enumerate(results, 1):
|
|
print(
|
|
"{}. {:.4f} template={:.4f} edge={:.4f} scale={:.4f} {}"
|
|
.format(
|
|
index,
|
|
item["score"],
|
|
item["template_score"],
|
|
item["edge_score"],
|
|
item["scale"],
|
|
Path(item["stamp_path"]).name,
|
|
)
|
|
)
|
|
|
|
|
|
def parse_args(argv):
|
|
parser = argparse.ArgumentParser(
|
|
description="Match one merged clothing image against a stamp directory."
|
|
)
|
|
parser.add_argument("--image", default=MERGED_IMAGE, help="Merged clothing image path")
|
|
parser.add_argument("--stamp-dir", default=STAMP_DIR, help="Stamp image directory")
|
|
parser.add_argument("--top", default=TOP_N, type=int, help="Number of results to print")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv=None):
|
|
args = parse_args(argv or sys.argv[1:])
|
|
merged_image = Path(args.image)
|
|
stamp_dir = Path(args.stamp_dir)
|
|
|
|
if not merged_image.is_file():
|
|
print("Merged image not found: {}".format(merged_image))
|
|
return 1
|
|
if not stamp_dir.is_dir():
|
|
print("Stamp directory not found: {}".format(stamp_dir))
|
|
return 1
|
|
|
|
started = time.time()
|
|
results, matched_count = find_best_matches(merged_image, stamp_dir, max(1, args.top))
|
|
print_result(results, matched_count, merged_image, stamp_dir)
|
|
print("")
|
|
print("Elapsed: {:.2f}s".format(time.time() - started))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|