File size: 7,979 Bytes
74de087
 
 
 
 
6ba6b88
74de087
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14ffea8
 
 
 
 
 
 
 
 
 
 
 
 
74de087
 
14ffea8
 
74de087
 
 
 
6ba6b88
 
74de087
b6c2f72
 
 
 
1f38341
 
b6c2f72
 
 
d45e25b
 
74de087
14ffea8
d45e25b
 
 
 
 
 
 
 
 
74de087
 
df1ec48
 
 
 
 
 
14ffea8
df1ec48
 
25b3cb1
 
 
 
 
 
 
12bd46e
14ffea8
74de087
 
14ffea8
74de087
 
 
 
 
 
 
 
6ba6b88
14ffea8
74de087
14ffea8
 
 
 
 
 
 
25b3cb1
 
 
 
 
14ffea8
 
 
 
 
 
25b3cb1
 
 
 
 
14ffea8
 
 
 
 
9dcaccd
14ffea8
 
 
 
 
 
25b3cb1
14ffea8
a77422a
14ffea8
 
 
 
 
 
9dcaccd
14ffea8
 
 
4c61d3e
cc76db0
14ffea8
 
 
 
 
 
 
 
 
 
 
 
 
 
12bd46e
aab2e6f
74de087
14ffea8
74de087
 
 
 
 
 
 
14ffea8
 
 
 
 
 
74de087
14ffea8
 
74de087
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import cv2
import numpy as np
import json
import gradio as gr
import os
import xml.etree.ElementTree as ET

# ---------------- Helper functions ----------------
def get_rotated_rect_corners(x, y, w, h, rotation_deg):
    rot_rad = np.deg2rad(rotation_deg)
    cos_r, sin_r = np.cos(rot_rad), np.sin(rot_rad)
    R = np.array([[cos_r, -sin_r], [sin_r, cos_r]])
    cx, cy = x + w/2, y + h/2
    local_corners = np.array([[-w/2,-h/2],[w/2,-h/2],[w/2,h/2],[-w/2,h/2]])
    rotated_corners = np.dot(local_corners, R.T)
    return (rotated_corners + np.array([cx,cy])).astype(np.float32)

def preprocess_gray_clahe(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
    return clahe.apply(gray)

def detect_and_match(img1_gray, img2_gray, method="SIFT", ratio_thresh=0.78):
    if method=="SIFT":
        detector=cv2.SIFT_create(nfeatures=5000); matcher=cv2.BFMatcher(cv2.NORM_L2)
    elif method=="ORB":
        detector=cv2.ORB_create(5000); matcher=cv2.BFMatcher(cv2.NORM_HAMMING)
    elif method=="BRISK":
        detector=cv2.BRISK_create(); matcher=cv2.BFMatcher(cv2.NORM_HAMMING)
    elif method=="KAZE":
        detector=cv2.KAZE_create(); matcher=cv2.BFMatcher(cv2.NORM_L2)
    elif method=="AKAZE":
        detector=cv2.AKAZE_create(); matcher=cv2.BFMatcher(cv2.NORM_HAMMING)
    else:
        return None,None,[]

    kp1, des1 = detector.detectAndCompute(img1_gray,None)
    kp2, des2 = detector.detectAndCompute(img2_gray,None)
    if des1 is None or des2 is None:
        return None,None,[]
    raw_matches = matcher.knnMatch(des1,des2,k=2)
    good = [m for m,n in raw_matches if m.distance < ratio_thresh*n.distance]
    return kp1, kp2, good

def parse_xml_points(xml_file):
    tree = ET.parse(xml_file)
    root = tree.getroot()
    transform = root.find('.//transform')
    points = {}
    for pt in transform.findall('.//point'):
        pt_type = pt.attrib['type']
        x = float(pt.attrib['x'])
        y = float(pt.attrib['y'])
        points[pt_type] = (x, y)
    return points

# ---------------- Fit-to-Box Helper ----------------
def fit_to_box(img, target_h=600, target_w=600):
    h, w = img.shape[:2]
    scale = min(target_w/w, target_h/h)  # preserve aspect ratio
    new_w, new_h = int(w*scale), int(h*scale)
    resized = cv2.resize(img, (new_w, new_h))
    # symmetric padding
    top = (target_h - new_h) // 2
    bottom = target_h - new_h - top
    left = (target_w - new_w) // 2
    right = target_w - new_w - left
    canvas = np.ones((target_h, target_w, 3), dtype=np.uint8) * 255
    canvas[top:top+new_h, left:left+new_w] = resized
    return canvas

# ---------------- Add Heading on Top ----------------
def add_heading(img, text):
    h, w = img.shape[:2]
    band_h = 40
    canvas = np.ones((h+band_h, w, 3), dtype=np.uint8) * 255
    canvas[band_h:] = img
    cv2.putText(canvas, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,0), 2, cv2.LINE_AA)
    return canvas

# ---------------- Placeholder if matching fails ----------------
def get_placeholder(method):
    canvas = np.ones((600, 1800, 3), dtype=np.uint8) * 255
    cv2.putText(canvas, f"{method}: Not enough feature matching", (50, 300),
                cv2.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), 3, cv2.LINE_AA)
    return canvas

# ---------------- Main Function ----------------
def homography_all_detectors(flat_file, persp_file, json_file, xml_file):
    flat_img = cv2.imread(flat_file)
    persp_img = cv2.imread(persp_file)

    mockup = json.load(open(json_file.name))
    roi_data = mockup["printAreas"][0]["position"]
    roi_x, roi_y = roi_data["x"], roi_data["y"]
    roi_w, roi_h = mockup["printAreas"][0]["width"], mockup["printAreas"][0]["height"]
    roi_rot_deg = mockup["printAreas"][0]["rotation"]

    flat_gray = preprocess_gray_clahe(flat_img)
    persp_gray = preprocess_gray_clahe(persp_img)

    xml_points = parse_xml_points(xml_file.name)

    methods = ["SIFT","ORB","BRISK","KAZE","AKAZE"]
    gallery_paths = []
    download_files = []

    for method in methods:
        kp1,kp2,good_matches = detect_and_match(flat_gray,persp_gray,method)
        if kp1 is None or kp2 is None or len(good_matches)<4:
            placeholder = get_placeholder(method)
            fname = f"{method.lower()}_placeholder.png"
            cv2.imwrite(fname, placeholder)
            gallery_paths.append(fname)
            download_files.append(fname)
            continue

        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)
        H,_ = cv2.findHomography(src_pts,dst_pts,cv2.RANSAC,5.0)
        if H is None:
            placeholder = get_placeholder(method)
            fname = f"{method.lower()}_placeholder.png"
            cv2.imwrite(fname, placeholder)
            gallery_paths.append(fname)
            download_files.append(fname)
            continue

        # 1. Flat image with ROI (from JSON)
        flat_with_roi = flat_img.copy()
        roi_corners_flat = get_rotated_rect_corners(roi_x,roi_y,roi_w,roi_h,roi_rot_deg)
        cv2.polylines(flat_with_roi,[roi_corners_flat.astype(int)],True,(0,255,0),2)
        for px,py in roi_corners_flat:  
            cv2.circle(flat_with_roi,(int(px),int(py)),5,(255,0,0),-1)

        # 2. Perspective with Homography ROI
        roi_corners_persp = cv2.perspectiveTransform(roi_corners_flat.reshape(-1,1,2),H).reshape(-1,2)
        persp_roi = persp_img.copy()
        cv2.polylines(persp_roi,[roi_corners_persp.astype(int)],True,(0,255,0),5)
        for px,py in roi_corners_persp:
            cv2.circle(persp_roi,(int(px),int(py)),5,(255,0,0),5)

        # 3. Perspective with GT ROI (from XML)
        xml_gt_img = persp_img.copy()
        ordered_pts = ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft']
        xml_polygon = [xml_points[pt] for pt in ordered_pts]
        pts = np.array(xml_polygon, np.int32).reshape((-1,1,2))
        cv2.polylines(xml_gt_img,[pts],isClosed=True,color=(255,0,0),thickness=5)

        # Convert to RGB + resize + add headings
        flat_rgb = add_heading(fit_to_box(cv2.cvtColor(flat_with_roi,cv2.COLOR_BGR2RGB),600,600), "Flat Image with ROI")
        roi_rgb = add_heading(fit_to_box(cv2.cvtColor(persp_roi,cv2.COLOR_BGR2RGB),600,600), "Perspective Homography ROI")
        xml_rgb = add_heading(fit_to_box(cv2.cvtColor(xml_gt_img,cv2.COLOR_BGR2RGB),600,600), "  Perspective GT ROI")

        # Concatenate side by side
        combined_row = np.hstack([flat_rgb, roi_rgb, xml_rgb])

        base_name = os.path.splitext(os.path.basename(persp_file))[0]
        file_name = f"{base_name}_{method.lower()}.png"
        cv2.imwrite(file_name, cv2.cvtColor(combined_row,cv2.COLOR_RGB2BGR))
        gallery_paths.append(file_name)
        download_files.append(file_name)

    while len(download_files)<5:
        download_files.append(None)

    return gallery_paths, download_files[0], download_files[1], download_files[2], download_files[3], download_files[4]

# ---------------- Gradio UI ----------------
iface = gr.Interface(
    fn=homography_all_detectors,
    inputs=[
        gr.Image(label="Upload Flat Image",type="filepath"),
        gr.Image(label="Upload Perspective Image",type="filepath"),
        gr.File(label="Upload mockup.json",file_types=[".json"]),
        gr.File(label="Upload XML file",file_types=[".xml"])
    ],
    outputs=[
        gr.Gallery(label="Results per Detector",show_label=True),
        gr.File(label="Download SIFT Result"),
        gr.File(label="Download ORB Result"),
        gr.File(label="Download BRISK Result"),
        gr.File(label="Download KAZE Result"),
        gr.File(label="Download AKAZE Result")
    ],
    title="Homography ROI + XML GT",
    description="Flat with ROI + Perspective ROI (Homography + GT). Aspect ratio preserved, images centered in uniform boxes, headings added."
)

iface.launch()