simonfrnd commited on
Commit
e77378b
·
verified ·
1 Parent(s): f4b3d1e

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +181 -0
main.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import torch
4
+ import torch.nn.functional as F
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image, ImageDraw
8
+ import io
9
+ import base64
10
+ from transformers import AutoImageProcessor, AutoModel, CLIPProcessor, CLIPModel
11
+
12
+ # ==============================================================================
13
+ # 1. Initialize FastAPI & CORS
14
+ # ==============================================================================
15
+ app = FastAPI(title="Copyright Diagnostic API")
16
+
17
+ # Crucial for allowing your React frontend to communicate with this backend
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ # ==============================================================================
27
+ # 2. Global Initialization & Memory Management
28
+ # ==============================================================================
29
+ device = "cuda" if torch.cuda.is_available() else "cpu"
30
+
31
+ print("Loading DINOv2...")
32
+ dino_processor = AutoImageProcessor.from_pretrained("facebook/dinov2-base")
33
+ dino_model = AutoModel.from_pretrained("facebook/dinov2-base").to(device)
34
+ dino_model.eval()
35
+
36
+ print("Loading CLIP...")
37
+ clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
38
+ clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
39
+ clip_model.eval()
40
+
41
+ # Helper function to convert PIL Image to Base64 string for React
42
+ def image_to_base64(img: Image.Image) -> str:
43
+ buffered = io.BytesIO()
44
+ img.save(buffered, format="JPEG")
45
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
46
+
47
+ # ==============================================================================
48
+ # 3. AI Pipeline Functions
49
+ # ==============================================================================
50
+
51
+ def compute_semantic_similarity(img_a: Image.Image, img_b: Image.Image) -> float:
52
+ inputs = clip_processor(images=[img_a, img_b], return_tensors="pt").to(device)
53
+
54
+ with torch.no_grad():
55
+ image_features = clip_model.get_image_features(**inputs)
56
+
57
+ # --- FIX: Handle Transformers 5.x object returns ---
58
+ if not isinstance(image_features, torch.Tensor):
59
+ if hasattr(image_features, "image_embeds"):
60
+ image_features = image_features.image_embeds
61
+ elif hasattr(image_features, "pooler_output"):
62
+ image_features = image_features.pooler_output
63
+ else:
64
+ image_features = image_features[1] if isinstance(image_features, tuple) and len(image_features) > 1 else image_features[0]
65
+
66
+ image_features = F.normalize(image_features, p=2, dim=-1)
67
+ score = F.cosine_similarity(image_features[0].unsqueeze(0), image_features[1].unsqueeze(0))
68
+ return round(score.item(), 4)
69
+
70
+ def compute_structural_similarity(img_a: Image.Image, img_b: Image.Image):
71
+ arr_a = np.array(img_a.convert('L'))
72
+ arr_b = np.array(img_b.convert('L'))
73
+
74
+ edges_a = cv2.Canny(arr_a, 100, 200)
75
+ edges_b = cv2.Canny(arr_b, 100, 200)
76
+
77
+ edges_b_resized = cv2.resize(edges_b, (edges_a.shape[1], edges_a.shape[0]))
78
+
79
+ intersection = np.logical_and(edges_a > 0, edges_b_resized > 0).sum()
80
+ union = np.logical_or(edges_a > 0, edges_b_resized > 0).sum()
81
+
82
+ iou_score = intersection / union if union != 0 else 0.0
83
+ return round(iou_score, 4), Image.fromarray(edges_a), Image.fromarray(edges_b)
84
+
85
+ def compute_patch_similarity(img_a: Image.Image, img_b: Image.Image):
86
+ target_size = (224, 224)
87
+ img_a_resized = img_a.resize(target_size)
88
+ img_b_resized = img_b.resize(target_size)
89
+
90
+ inputs_a = dino_processor(images=img_a_resized, return_tensors="pt").to(device)
91
+ inputs_b = dino_processor(images=img_b_resized, return_tensors="pt").to(device)
92
+
93
+ with torch.no_grad():
94
+ out_a = dino_model(**inputs_a)
95
+ out_b = dino_model(**inputs_b)
96
+
97
+ emb_a = F.normalize(out_a.last_hidden_state[:, 1:, :].squeeze(0), p=2, dim=-1)
98
+ emb_b = F.normalize(out_b.last_hidden_state[:, 1:, :].squeeze(0), p=2, dim=-1)
99
+
100
+ sim_matrix = torch.matmul(emb_a, emb_b.T)
101
+
102
+ best_b_for_a = torch.argmax(sim_matrix, dim=1)
103
+ best_a_for_b = torch.argmax(sim_matrix, dim=0)
104
+
105
+ matches = []
106
+ SIMILARITY_THRESHOLD = 0.85
107
+
108
+ for a_idx in range(len(best_b_for_a)):
109
+ b_idx = best_b_for_a[a_idx]
110
+ if best_a_for_b[b_idx] == a_idx:
111
+ score = sim_matrix[a_idx, b_idx].item()
112
+ if score >= SIMILARITY_THRESHOLD:
113
+ matches.append((a_idx, b_idx, score))
114
+
115
+ patch_score = len(matches) / 256.0
116
+
117
+ combined_vis = Image.new('RGB', (target_size[0] * 2, target_size[1]))
118
+ combined_vis.paste(img_a_resized, (0, 0))
119
+ combined_vis.paste(img_b_resized, (target_size[0], 0))
120
+ draw = ImageDraw.Draw(combined_vis)
121
+
122
+ grid_size = 16
123
+ patch_size = 14
124
+
125
+ for a_idx, b_idx, score in matches:
126
+ ay = (a_idx // grid_size) * patch_size
127
+ ax = (a_idx % grid_size) * patch_size
128
+ by = (b_idx // grid_size) * patch_size
129
+ bx = (b_idx % grid_size) * patch_size + target_size[0]
130
+
131
+ draw.rectangle([ax, ay, ax + patch_size, ay + patch_size], outline="red", width=2)
132
+ draw.rectangle([bx, by, bx + patch_size, by + patch_size], outline="red", width=2)
133
+
134
+ center_a = (ax + patch_size // 2, ay + patch_size // 2)
135
+ center_b = (bx + patch_size // 2, by + patch_size // 2)
136
+ draw.line([center_a, center_b], fill="lime", width=1)
137
+
138
+ return round(patch_score, 4), combined_vis
139
+
140
+ # ==============================================================================
141
+ # 4. API Endpoints
142
+ # ==============================================================================
143
+
144
+ @app.post("/analyze")
145
+ async def analyze_artworks(file_a: UploadFile = File(...), file_b: UploadFile = File(...)):
146
+ try:
147
+ # Read uploaded files into PIL Images
148
+ img_a = Image.open(io.BytesIO(await file_a.read())).convert("RGB")
149
+ img_b = Image.open(io.BytesIO(await file_b.read())).convert("RGB")
150
+
151
+ # Run pipelines
152
+ semantic_score = compute_semantic_similarity(img_a, img_b)
153
+ struct_score, edge_a, edge_b = compute_structural_similarity(img_a, img_b)
154
+ patch_score, patch_vis = compute_patch_similarity(img_a, img_b)
155
+
156
+ # Convert images to base64 for JSON transmission
157
+ patch_vis_b64 = image_to_base64(patch_vis)
158
+ edge_a_b64 = image_to_base64(edge_a)
159
+ edge_b_b64 = image_to_base64(edge_b)
160
+
161
+ # Return a clean JSON package to React
162
+ return {
163
+ "status": "success",
164
+ "scores": {
165
+ "semantic_idea": semantic_score,
166
+ "structural_layout": struct_score,
167
+ "fragmented_literal": patch_score
168
+ },
169
+ "visual_evidence": {
170
+ "patch_mapping_image": f"data:image/jpeg;base64,{patch_vis_b64}",
171
+ "edge_image_a": f"data:image/jpeg;base64,{edge_a_b64}",
172
+ "edge_image_b": f"data:image/jpeg;base64,{edge_b_b64}"
173
+ }
174
+ }
175
+
176
+ except Exception as e:
177
+ raise HTTPException(status_code=500, detail=str(e))
178
+
179
+ @app.get("/")
180
+ def read_root():
181
+ return {"message": "Diagnostic API is running. Send POST requests to /analyze"}