File size: 10,499 Bytes
8759ac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4d56d5
 
 
 
 
 
 
 
 
 
 
8759ac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
badc90c
8759ac4
 
 
 
 
 
 
 
 
 
 
8217c57
 
 
 
65a6efd
 
 
8217c57
 
8759ac4
 
 
 
 
 
8217c57
 
 
 
 
8759ac4
 
 
8217c57
 
 
8759ac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8217c57
badc90c
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import gradio as gr
import torch
import torch.nn.functional as F
import cv2
import numpy as np
from PIL import Image, ImageDraw
from transformers import AutoImageProcessor, AutoModel, CLIPProcessor, CLIPModel

# ==============================================================================
# 1. Global Initialization & Memory Management
# ==============================================================================
# We load models globally so they cache in memory on startup, not on every click.
device = "cuda" if torch.cuda.is_available() else "cpu"

print("Loading DINOv2...")
dino_processor = AutoImageProcessor.from_pretrained("facebook/dinov2-base")
dino_model = AutoModel.from_pretrained("facebook/dinov2-base").to(device)
dino_model.eval() # Prevent gradient tracking

print("Loading CLIP...")
clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
clip_model.eval() # Prevent gradient tracking

# ==============================================================================
# 2. Pipeline Functions
# ==============================================================================

def compute_semantic_similarity(img_a: Image.Image, img_b: Image.Image) -> float:
    """
    Model: CLIP
    Legal Concept: The "Idea" / Semantic Referent
    """
    inputs = clip_processor(images=[img_a, img_b], return_tensors="pt").to(device)
    
    with torch.no_grad():
        image_features = clip_model.get_image_features(**inputs)
        
    # --- FIX: Handle Transformers 5.x object returns ---
    if not isinstance(image_features, torch.Tensor):
        if hasattr(image_features, "image_embeds"):
            image_features = image_features.image_embeds
        elif hasattr(image_features, "pooler_output"):
            image_features = image_features.pooler_output
        else:
            # Fallback for tuple-like object behavior
            image_features = image_features[1] if isinstance(image_features, tuple) and len(image_features) > 1 else image_features[0]
    # ---------------------------------------------------
            
    # Normalize and compute cosine similarity
    image_features = F.normalize(image_features, p=2, dim=-1)
    score = F.cosine_similarity(image_features[0].unsqueeze(0), image_features[1].unsqueeze(0))
    
    return round(score.item(), 4)

def compute_structural_similarity(img_a: Image.Image, img_b: Image.Image):
    """
    Model: OpenCV Canny
    Legal Concept: "Substantial Similarity" (Layout / Composition)
    """
    # Convert to grayscale numpy arrays
    arr_a = np.array(img_a.convert('L'))
    arr_b = np.array(img_b.convert('L'))
    
    # Extract structural edges
    edges_a = cv2.Canny(arr_a, 100, 200)
    edges_b = cv2.Canny(arr_b, 100, 200)
    
    # Calculate structural overlap using Intersection over Union (IoU) of edges
    # We resize edges_b to match edges_a to ensure matrix math works
    edges_b_resized = cv2.resize(edges_b, (edges_a.shape[1], edges_a.shape[0]))
    
    intersection = np.logical_and(edges_a > 0, edges_b_resized > 0).sum()
    union = np.logical_or(edges_a > 0, edges_b_resized > 0).sum()
    
    iou_score = intersection / union if union != 0 else 0.0
    
    return round(iou_score, 4), Image.fromarray(edges_a), Image.fromarray(edges_b)

def compute_patch_similarity(img_a: Image.Image, img_b: Image.Image):
    """
    Model: DINOv2
    Legal Concept: "Fragmented Literal Similarity" (Scattered Literal Copying)
    """
    # Resize to a fixed multiple of patch size (14) so we have a known grid
    # 224x224 gives us a 16x16 grid of patches (256 total patches)
    target_size = (224, 224)
    img_a_resized = img_a.resize(target_size)
    img_b_resized = img_b.resize(target_size)
    
    inputs_a = dino_processor(images=img_a_resized, return_tensors="pt").to(device)
    inputs_b = dino_processor(images=img_b_resized, return_tensors="pt").to(device)
    
    with torch.no_grad():
        out_a = dino_model(**inputs_a)
        out_b = dino_model(**inputs_b)
        
    # Isolate patches (skip CLS token) and normalize. Shape: (256, 768)
    emb_a = F.normalize(out_a.last_hidden_state[:, 1:, :].squeeze(0), p=2, dim=-1)
    emb_b = F.normalize(out_b.last_hidden_state[:, 1:, :].squeeze(0), p=2, dim=-1)
    
    # Compute N x M similarity matrix using dot product
    sim_matrix = torch.matmul(emb_a, emb_b.T) # Shape: (256, 256)
    
    # Mutual Nearest Neighbors logic to filter out noise
    best_b_for_a = torch.argmax(sim_matrix, dim=1)
    best_a_for_b = torch.argmax(sim_matrix, dim=0)
    
    matches = []
    # Threshold for what we consider "copied" (adjust based on testing)
    SIMILARITY_THRESHOLD = 0.85 
    
    for a_idx in range(len(best_b_for_a)):
        b_idx = best_b_for_a[a_idx]
        if best_a_for_b[b_idx] == a_idx: # It's a mutual match
            score = sim_matrix[a_idx, b_idx].item()
            if score >= SIMILARITY_THRESHOLD:
                matches.append((a_idx, b_idx, score))
                
    # Calculate overall patch score based on percentage of matching patches
    patch_score = len(matches) / 256.0
    
    # --- Visual Evidence Generation ---
    combined_vis = Image.new('RGB', (target_size[0] * 2, target_size[1]))
    combined_vis.paste(img_a_resized, (0, 0))
    combined_vis.paste(img_b_resized, (target_size[0], 0))
    draw = ImageDraw.Draw(combined_vis)
    
    grid_size = 16
    patch_size = 14
    
    for a_idx, b_idx, score in matches:
        # Image A coordinates
        ay = (a_idx // grid_size) * patch_size
        ax = (a_idx % grid_size) * patch_size
        
        # Image B coordinates (shifted X by the width of Image A)
        by = (b_idx // grid_size) * patch_size
        bx = (b_idx % grid_size) * patch_size + target_size[0]
        
        # Draw bounding boxes
        draw.rectangle([ax, ay, ax + patch_size, ay + patch_size], outline="red", width=2)
        draw.rectangle([bx, by, bx + patch_size, by + patch_size], outline="red", width=2)
        
        # Draw connecting line
        center_a = (ax + patch_size // 2, ay + patch_size // 2)
        center_b = (bx + patch_size // 2, by + patch_size // 2)
        draw.line([center_a, center_b], fill="lime", width=1)
        
    return round(patch_score, 4), combined_vis

# ==============================================================================
# 3. Main Orchestration Function
# ==============================================================================

def analyze_images(image_a, image_b):
    if image_a is None or image_b is None:
        raise gr.Error("Please upload both images.")

    # 1. Semantic Match
    semantic_score = compute_semantic_similarity(image_a, image_b)
    
    # 2. Structural Match
    struct_score, edge_a, edge_b = compute_structural_similarity(image_a, image_b)
    
    # 3. Patch Match
    patch_score, patch_vis = compute_patch_similarity(image_a, image_b)
    
    return (
        semantic_score, 
        struct_score, 
        patch_score, 
        patch_vis,
        edge_a, 
        edge_b
    )

# ==============================================================================
# 4. Gradio UI / UX
# ==============================================================================

with gr.Blocks() as demo:
    gr.Markdown("# Assistive Diagnostic Framework for Copyright Infringement")
    gr.Markdown("Upload two images to compare them across semantic, structural, and literal fragment dimensions.")
    
    with gr.Row():
        with gr.Column():
            img_in_a = gr.Image(type="pil", label="Image A (Original)")
        with gr.Column():
            img_in_b = gr.Image(type="pil", label="Image B (Suspected Copy)")
            
    btn_analyze = gr.Button("Analyze Similarity", variant="primary")
    
    gr.Markdown("### Assessment Metrics & Legal Context")
    
    # Context pulled directly from the paper
    gr.Markdown("""
    * **Semantic Match (Idea-Expression Dichotomy):** Evaluates if works share an underlying conceptual basis. Models like CLIP link visual data to semantic concepts to bridge the "semantic gap". This acts as an initial threshold, as copyright protects concrete expression rather than mere abstract ideas.
    * **Structural Match (Substantial Similarity):** Detects structural relationships through spatial mapping. This addresses how the specific combination and arrangement of individual elements can form part of the protected expression.
    * **Patch Match (Fragmented Literal Similarity):** Identifies "scattered literal copying" where specific fragments of a protected work are reproduced directly or in a highly proximate manner, without the new work as a whole having to resemble the original. Localized patch-based matching algorithms divide the image to detect these copied fragments regardless of where they are placed in the new works.
    """)

    with gr.Row():
        score_semantic = gr.Number(label="Semantic Match (CLIP) - Idea", show_label=True)
        score_struct = gr.Number(label="Structural Match (Edge IoU) - Layout", show_label=True)
        score_patch = gr.Number(label="Patch Match (DINOv2) - Fragmented Literal", show_label=True)
        
    gr.Markdown("### Visual Evidence")
    
    # Replaced Tabs with a Row containing two Columns for side-by-side display
    with gr.Row():
        with gr.Column():
            gr.Markdown("#### Fragmented Literal Similarity (DINOv2)")
            gr.Markdown("**Red boxes and green lines indicate mutually correlating local patches (Similarity > 0.85)**")
            vis_patch = gr.Image(label="Patch Mapping Visualization", type="pil")
            
        with gr.Column():
            gr.Markdown("#### Substantial Similarity (Edge Detection)")
            gr.Markdown("**Comparison of spatial arrangements and structural boundaries.**")
            with gr.Row():
                vis_edge_a = gr.Image(label="Image A Edges", type="pil")
                vis_edge_b = gr.Image(label="Image B Edges", type="pil")

    btn_analyze.click(
        fn=analyze_images,
        inputs=[img_in_a, img_in_b],
        outputs=[
            score_semantic, 
            score_struct, 
            score_patch, 
            vis_patch,
            vis_edge_a, 
            vis_edge_b 
        ]
    )

if __name__ == "__main__":
    # Remember to keep your theme in the launch method if you are using Gradio 6.0!
    demo.launch(theme=gr.themes.Soft())