Simon
x
65a6efd
Raw
History Blame Contribute Delete
10.5 kB
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())