import sys import subprocess import tkinter as tk from tkinter import messagebox, filedialog, scrolledtext, ttk import json import csv import threading import time import os try: import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont except ImportError: root = tk.Tk() root.withdraw() if messagebox.askyesno("Dependency Error", "Required libraries are not installed. Would you like to try and install them now?"): try: subprocess.check_call([sys.executable, "-m", "pip", "install", "opencv-python", "numpy", "Pillow"]) messagebox.showinfo("Success", "Dependencies installed successfully. Please restart the application.") except Exception as e: messagebox.showerror("Installation Failed", f"Could not install dependencies. Please run 'pip install -r requirements.txt' manually.\n\nError: {e}") root.destroy() sys.exit() class ToolTip: def __init__(self, widget, text): self.widget = widget self.text = text self.tooltip = None self.widget.bind("", self.enter) self.widget.bind("", self.leave) def enter(self, event=None): x, y, _, _ = self.widget.bbox("insert") x += self.widget.winfo_rootx() + 25 y += self.widget.winfo_rooty() + 25 self.tooltip = tk.Toplevel(self.widget) self.tooltip.wm_overrideredirect(True) self.tooltip.wm_geometry(f"+{x}+{y}") label = tk.Label(self.tooltip, text=self.text, background="#ffffe0", relief="solid", borderwidth=1, wraplength=200) label.pack() def leave(self, event=None): if self.tooltip: self.tooltip.destroy() self.tooltip = None class MasterImageFinderApp: def __init__(self, root): self.root = root self.root.title("Master Image Finder") self.root.geometry("800x700") self.layouts_path = tk.StringVar() self.masters_path = tk.StringVar() self.upscale = tk.BooleanVar() self.denoise = tk.BooleanVar() self.sharpen = tk.BooleanVar() self.contrast = tk.BooleanVar() tk.Label(root, text="Layouts Folder:").pack(pady=5) tk.Entry(root, textvariable=self.layouts_path, width=100).pack(pady=5) tk.Button(root, text="Select Layouts Folder", command=self.select_layouts_folder).pack(pady=5) tk.Label(root, text="Master Images Folder:").pack(pady=5) tk.Entry(root, textvariable=self.masters_path, width=100).pack(pady=5) tk.Button(root, text="Select Master Images Folder", command=self.select_masters_folder).pack(pady=5) enhancement_frame = tk.LabelFrame(root, text="Advanced Enhancement Options", padx=10, pady=10) enhancement_frame.pack(pady=10, padx=10, fill="x") upscale_check = tk.Checkbutton(enhancement_frame, text="Smart Upscaling", variable=self.upscale) upscale_check.grid(row=0, column=0, sticky="w") ToolTip(upscale_check, "Enlarges small images to improve feature detection. Best for images under 400x400px.") denoise_check = tk.Checkbutton(enhancement_frame, text="Denoising", variable=self.denoise) denoise_check.grid(row=0, column=1, sticky="w", padx=10) ToolTip(denoise_check, "Removes digital noise and compression artifacts. Can be slow on large images.") sharpen_check = tk.Checkbutton(enhancement_frame, text="Sharpening", variable=self.sharpen) sharpen_check.grid(row=1, column=0, sticky="w") ToolTip(sharpen_check, "Enhances edges and fine details. Very fast.") contrast_check = tk.Checkbutton(enhancement_frame, text="Contrast Enhancement", variable=self.contrast) contrast_check.grid(row=1, column=1, sticky="w", padx=10) ToolTip(contrast_check, "Improves local contrast, making features in dark or washed-out areas more distinct.") self.run_button = tk.Button(root, text="Find Matches", command=self.run_finder_thread) self.run_button.pack(pady=20) self.progress = ttk.Progressbar(root, orient="horizontal", length=780, mode="determinate") self.progress.pack(pady=10) self.log_area = scrolledtext.ScrolledText(root, wrap=tk.WORD, width=100, height=15) self.log_area.pack(pady=10, padx=10) def select_layouts_folder(self): self.layouts_path.set(filedialog.askdirectory()) def select_masters_folder(self): self.masters_path.set(filedialog.askdirectory()) def log(self, message): self.root.after(0, self._log, message) def _log(self, message): self.log_area.insert(tk.END, message + "\n") self.log_area.see(tk.END) def update_progress(self, value): self.root.after(0, self.progress.config, {'value': value}) def run_finder_thread(self): self.run_button.config(state=tk.DISABLED) self.log_area.delete(1.0, tk.END) self.progress['value'] = 0 thread = threading.Thread(target=self.run_finder) thread.start() def run_finder(self): start_time = time.time() layouts_dir = self.layouts_path.get() masters_dir = self.masters_path.get() if not layouts_dir or not masters_dir: self.log("Error: Please select both folders.") self.run_button.config(state=tk.NORMAL) return output_dir = os.path.join(layouts_dir, "reports") if not os.path.isdir(output_dir): os.makedirs(output_dir) self.log(f"Created output directory at {output_dir}") try: results = self.find_matches(layouts_dir, masters_dir) self.create_html_report(results, output_dir, layouts_dir, masters_dir) end_time = time.time() total_matches = sum(1 for item in results if item['found']) self.log("\n--- Process Complete! ---") self.log(f"Found matches for {total_matches} out of {len(results)} layout images.") self.log(f"Total time: {end_time - start_time:.2f} seconds") self.log(f"Reports saved in: {output_dir}") except Exception as e: self.log(f"An error occurred: {e}") finally: self.run_button.config(state=tk.NORMAL) def find_matches(self, layouts_path, masters_path, min_good_matches=10, inlier_threshold_ratio=0.5): akaze = cv2.AKAZE_create() bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) sharpen_kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) layout_images = [f for f in os.listdir(layouts_path) if f.endswith(('.png', '.jpg', '.jpeg'))] master_images = [f for f in os.listdir(masters_path) if f.endswith(('.png', '.jpg', '.jpeg'))] results = [] master_descriptors = {} self.log("Preprocessing master images...") for i, master_image_name in enumerate(master_images): self.log(f" - Preprocessing {i+1}/{len(master_images)}: {master_image_name}") master_image_path = os.path.join(masters_path, master_image_name) master_img = cv2.imread(master_image_path, cv2.IMREAD_GRAYSCALE) if master_img is None: continue kp, des = akaze.detectAndCompute(master_img, None) if des is not None: master_descriptors[master_image_name] = (kp, des) total_layouts = len(layout_images) self.log("\nProcessing layout images...") for i, layout_image_name in enumerate(layout_images): self.update_progress((i / total_layouts) * 100) self.log(f" - Processing {i+1}/{total_layouts}: {layout_image_name}") layout_image_path = os.path.join(layouts_path, layout_image_name) layout_img_gray = cv2.imread(layout_image_path, cv2.IMREAD_GRAYSCALE) if layout_img_gray is None: self.log(f" - Could not read layout image.") continue enhancements_applied = [] if self.upscale.get() and (layout_img_gray.shape[0] < 400 or layout_img_gray.shape[1] < 400): layout_img_gray = cv2.resize(layout_img_gray, (0,0), fx=2.0, fy=2.0, interpolation=cv2.INTER_LANCZOS4) enhancements_applied.append("Upscaled") if self.denoise.get(): layout_img_gray = cv2.fastNlMeansDenoising(layout_img_gray, None, 10, 7, 21) enhancements_applied.append("Denoised") if self.sharpen.get(): layout_img_gray = cv2.filter2D(layout_img_gray, -1, sharpen_kernel) enhancements_applied.append("Sharpened") if self.contrast.get(): layout_img_gray = clahe.apply(layout_img_gray) enhancements_applied.append("Contrast Enhanced") if enhancements_applied: self.log(f" - Applied: {', '.join(enhancements_applied)}") kp1, des1 = akaze.detectAndCompute(layout_img_gray, None) if des1 is None: self.log(f" - No features found.") results.append({"layout": layout_image_name, "masters": [], "found": False, "enhancements": enhancements_applied}) continue all_possible_matches = [] for master_image_name, (kp2, des2) in master_descriptors.items(): matches = bf.knnMatch(des1, des2, k=2) good_matches = [m for m, n in matches if len(matches) > 1 and len(matches[0]) > 1 and m.distance < 0.75 * n.distance] if len(good_matches) > min_good_matches: src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) if mask is not None: inliers = np.sum(mask) if inliers > min_good_matches: all_possible_matches.append({"master": master_image_name, "inliers": int(inliers)}) if not all_possible_matches: results.append({"layout": layout_image_name, "masters": [], "found": False, "enhancements": enhancements_applied}) continue best_match = max(all_possible_matches, key=lambda x: x['inliers']) max_inliers = best_match['inliers'] confident_matches = [best_match] for match in all_possible_matches: if match != best_match and match['inliers'] > max_inliers * inlier_threshold_ratio: confident_matches.append(match) if confident_matches: self.log(f" - Found {len(confident_matches)} confident master image(s).") results.append({"layout": layout_image_name, "masters": confident_matches, "found": True, "enhancements": enhancements_applied}) else: results.append({"layout": layout_image_name, "masters": [], "found": False, "enhancements": enhancements_applied}) self.update_progress(100) return results def create_html_report(self, data, output_path, layouts_abs_path, masters_abs_path): report_path = os.path.join(output_path, 'report.html') total_matches = sum(1 for item in data if item['found']) total_layouts = len(data) html = f""" Image Match Report

Image Match Report

Found matches for {total_matches} out of {total_layouts} layout images.
""" html += "

Matched Layouts

" for item in data: if item['found']: layout_img_path = os.path.join(layouts_abs_path, item['layout']).replace('\\', '/') enhancements_str = f"

Enhancements: {', '.join(item['enhancements'])}

" if item['enhancements'] else "" html += f"
" html += f"

Layout Image

{item['layout']}

{enhancements_str}
" html += "
" for master_item in item['masters']: master_img_path = os.path.join(masters_abs_path, master_item['master']).replace('\\', '/') html += f"

{master_item['master']}

({master_item['inliers']} inliers)

" html += "
" html += "

Unmatched Layouts

Please review these manually.

" for item in data: if not item['found']: layout_img_path = os.path.join(layouts_abs_path, item['layout']).replace('\\', '/') html += f"

{item['layout']}

" html += """
""" with open(report_path, 'w', encoding='utf-8') as f: f.write(html) self.log(f"HTML report saved to {report_path}") if __name__ == "__main__": root = tk.Tk() app = MasterImageFinderApp(root) root.mainloop()