sohamnk commited on
Commit
d996f25
Β·
verified Β·
1 Parent(s): b31d91c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +34 -46
app.py CHANGED
@@ -1,10 +1,7 @@
1
- import sys
2
- sys.stdout.reconfigure(line_buffering=True)
3
-
4
  # --------------------------------------------------------------------------
5
  # UNIFIED AI SERVICE FOR LOST & FOUND V2
6
  # --------------------------------------------------------------------------
7
- # This Flask application combines two matching engines:
8
  # 1. Text Engine: Analyzes structured text fields (brand, material, etc.)
9
  # using text embeddings and specific comparison logic.
10
  # 2. Image Engine: Analyzes multiple images per item by segmenting the
@@ -42,7 +39,7 @@ app = Flask(__name__)
42
  TEXT_FIELD_WEIGHTS = { "brand": 1.0, "material": 1.0, "markings": 1.0, "colors": 1.0, "size": 1.0 }
43
  TEXT_FIELDS_TO_EMBED = ["brand", "material", "markings"]
44
  SCORE_WEIGHTS = { "text_score": 0.5, "image_score": 0.5 }
45
- FINAL_SCORE_THRESHOLD = 0.55 # A higher threshold for better quality matches
46
 
47
  # --- Model Loading ---
48
  print("="*50)
@@ -77,8 +74,7 @@ print("="*50)
77
  # ==========================================================================
78
 
79
  # --- Text Processing Helpers ---
80
- def get_text_embedding(text: str) -> np.ndarray:
81
- """Generates a normalized embedding for a given text string."""
82
  if not text or not text.strip(): return None
83
  instruction = "Represent this sentence for searching relevant passages: "
84
  inputs = tokenizer_text(instruction + text, return_tensors='pt', padding=True, truncation=True, max_length=512).to(device)
@@ -86,16 +82,14 @@ def get_text_embedding(text: str) -> np.ndarray:
86
  outputs = model_text(**inputs)
87
  embedding = outputs.last_hidden_state[:, 0, :]
88
  embedding = torch.nn.functional.normalize(embedding, p=2, dim=1)
89
- return embedding.cpu().numpy()[0]
90
 
91
  def cosine_similarity(vec1, vec2):
92
- """Calculates cosine similarity between two vectors."""
93
  if vec1 is None or vec2 is None: return 0.0
94
  vec1, vec2 = np.array(vec1), np.array(vec2)
95
  return float(np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)))
96
 
97
  def calculate_color_similarity(colors1: list, colors2: list) -> float:
98
- """Calculates Jaccard similarity for two lists of colors."""
99
  if not colors1 and not colors2: return 1.0
100
  if not colors1 or not colors2: return 0.0
101
  set1, set2 = set(c.lower() for c in colors1), set(c.lower() for c in colors2)
@@ -105,7 +99,6 @@ def calculate_color_similarity(colors1: list, colors2: list) -> float:
105
 
106
  # --- Image Processing Helpers ---
107
  def segment_guided_object(image: Image.Image, object_label: str) -> Image.Image:
108
- """Segments an object from an image using a text label."""
109
  prompt = f"a {object_label}."
110
  image_rgb = image.convert("RGB")
111
  image_np = np.array(image_rgb)
@@ -120,10 +113,11 @@ def segment_guided_object(image: Image.Image, object_label: str) -> Image.Image:
120
  )
121
 
122
  if not results or len(results[0]['boxes']) == 0:
123
- return image # Return full image if object not detected
 
124
 
125
  sam_predictor.set_image(image_np)
126
- box = results[0]['boxes'][0].cpu().numpy().astype(int) # Use the highest confidence box
127
  masks, _, _ = sam_predictor.predict(box=box, multimask_output=False)
128
 
129
  mask = masks[0]
@@ -134,12 +128,10 @@ def segment_guided_object(image: Image.Image, object_label: str) -> Image.Image:
134
  return Image.fromarray(object_rgba, 'RGBA')
135
 
136
  def extract_visual_features(segmented_image_rgba: Image.Image) -> dict:
137
- """Extracts shape, color, and texture features from a segmented RGBA image."""
138
  image_np = np.array(segmented_image_rgba)
139
  bgr_image = cv2.cvtColor(image_np[:, :, :3], cv2.COLOR_RGB2BGR)
140
  mask = image_np[:, :, 3]
141
 
142
- # Shape Features (Hu Moments)
143
  contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
144
  shape_features = np.zeros(7)
145
  if contours:
@@ -149,11 +141,9 @@ def extract_visual_features(segmented_image_rgba: Image.Image) -> dict:
149
  hu_moments = cv2.HuMoments(moments).flatten()
150
  shape_features = -np.sign(hu_moments) * np.log10(np.abs(hu_moments) + 1e-7)
151
 
152
- # Color Features (3D Histogram)
153
  color_hist = cv2.calcHist([bgr_image], [0, 1, 2], mask, [8, 8, 8], [0, 256, 0, 256, 0, 256])
154
  cv2.normalize(color_hist, color_hist)
155
 
156
- # Texture Features (Local Binary Pattern)
157
  gray_image = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2GRAY)
158
  lbp = feature.local_binary_pattern(gray_image, P=24, R=3, method="uniform")
159
  (texture_hist, _) = np.histogram(lbp[mask > 0], bins=np.arange(0, 27), range=(0, 26))
@@ -167,7 +157,6 @@ def extract_visual_features(segmented_image_rgba: Image.Image) -> dict:
167
  }
168
 
169
  def calculate_dynamic_weights(all_shape_scores, all_color_scores, stability_factor=0.4):
170
- """Calculates robust dynamic weights based on score dispersion."""
171
  shape_scores, color_scores = np.array(all_shape_scores), np.array(all_color_scores)
172
 
173
  def get_iqr(scores):
@@ -177,13 +166,10 @@ def calculate_dynamic_weights(all_shape_scores, all_color_scores, stability_fact
177
 
178
  shape_dispersion = get_iqr(shape_scores)
179
  color_dispersion = get_iqr(color_scores)
180
-
181
  inv_shape_disp = 1 / (shape_dispersion + stability_factor)
182
  inv_color_disp = 1 / (color_dispersion + stability_factor)
183
-
184
  total_inv_disp = inv_shape_disp + inv_color_disp
185
-
186
- remaining_weight = 0.8 # Texture is fixed at 0.2
187
 
188
  shape_weight = remaining_weight * (inv_shape_disp / total_inv_disp) if total_inv_disp > 0 else remaining_weight / 2
189
  color_weight = remaining_weight * (inv_color_disp / total_inv_disp) if total_inv_disp > 0 else remaining_weight / 2
@@ -200,40 +186,43 @@ def health_check():
200
 
201
  @app.route('/process', methods=['POST'])
202
  def process_item():
203
- """
204
- Receives item data (text fields + image URLs) and returns a
205
- JSON object enriched with all extracted AI features.
206
- """
207
  try:
208
  data = request.json
209
  print(f"\n[PROCESS] Received request for object: {data.get('objectName')}")
210
 
211
  # --- 1. Process Text Features ---
 
212
  response = {
213
  "canonicalLabel": data.get('objectName', '').lower().strip(),
214
  "brand_embedding": get_text_embedding(data.get('brand')),
215
  "material_embedding": get_text_embedding(data.get('material')),
216
  "markings_embedding": get_text_embedding(data.get('markings'))
217
  }
 
218
 
219
  # --- 2. Process Image Features ---
220
  visual_features_list = []
221
  if data.get('images'):
222
- for image_url in data['images']:
 
223
  try:
 
224
  img_response = requests.get(image_url, timeout=20)
225
  img_response.raise_for_status()
226
  image = Image.open(BytesIO(img_response.content))
227
 
 
228
  segmented_image = segment_guided_object(image, data['objectName'])
 
229
  features = extract_visual_features(segmented_image)
230
  visual_features_list.append(features)
 
231
  except Exception as e:
232
- print(f" [PROCESS] ⚠️ Could not process image {image_url}: {e}")
233
  continue
234
 
235
  response["visual_features"] = visual_features_list
236
- print(f" [PROCESS] βœ… Successfully processed features.")
237
  return jsonify(response), 200
238
 
239
  except Exception as e:
@@ -243,10 +232,6 @@ def process_item():
243
 
244
  @app.route('/compare', methods=['POST'])
245
  def compare_items():
246
- """
247
- Receives a query item and a list of search items, and returns
248
- a list of potential matches based on a hybrid score.
249
- """
250
  try:
251
  payload = request.json
252
  query_item = payload['queryItem']
@@ -255,11 +240,12 @@ def compare_items():
255
 
256
  results = []
257
  for item in search_list:
 
 
258
  try:
259
  # --- 1. Calculate Text Score ---
260
  total_text_score, total_text_weight = 0, 0
261
 
262
- # Compare embeddings
263
  for field in TEXT_FIELDS_TO_EMBED:
264
  q_emb = query_item.get(f"{field}_embedding")
265
  i_emb = item.get(f"{field}_embedding")
@@ -269,21 +255,20 @@ def compare_items():
269
  total_text_score += score * weight
270
  total_text_weight += weight
271
 
272
- # Compare colors
273
  if query_item.get('colors'):
274
  score = calculate_color_similarity(query_item['colors'], item.get('colors', []))
275
  weight = TEXT_FIELD_WEIGHTS.get('colors', 0)
276
  total_text_score += score * weight
277
  total_text_weight += weight
278
 
279
- # Compare size
280
  if query_item.get('size'):
281
- score = 1.0 if query_item['size'] == item.get('size') else 0.0
282
  weight = TEXT_FIELD_WEIGHTS.get('size', 0)
283
  total_text_score += score * weight
284
  total_text_weight += weight
285
 
286
  text_score = (total_text_score / total_text_weight) if total_text_weight > 0 else 0.0
 
287
 
288
  # --- 2. Calculate Image Score ---
289
  image_score = 0.0
@@ -292,15 +277,11 @@ def compare_items():
292
 
293
  if query_visuals and item_visuals:
294
  all_shape_scores, all_color_scores, all_texture_scores = [], [], []
295
-
296
  for q_vis in query_visuals:
297
  for i_vis in item_visuals:
298
- # Shape comparison
299
  shape_dist = cv2.matchShapes(np.array(q_vis["shape_features"], dtype="float32"), np.array(i_vis["shape_features"], dtype="float32"), cv2.CONTOURS_MATCH_I1, 0.0)
300
  all_shape_scores.append(1.0 / (1.0 + shape_dist))
301
- # Color comparison
302
  all_color_scores.append(cv2.compareHist(np.array(q_vis["color_features"], dtype="float32"), np.array(i_vis["color_features"], dtype="float32"), cv2.HISTCMP_CORREL))
303
- # Texture comparison
304
  all_texture_scores.append(cv2.compareHist(np.array(q_vis["texture_features"], dtype="float32"), np.array(i_vis["texture_features"], dtype="float32"), cv2.HISTCMP_CORREL))
305
 
306
  if all_shape_scores:
@@ -308,21 +289,28 @@ def compare_items():
308
  image_score = (weights["shape"] * max(all_shape_scores) +
309
  weights["color"] * max(all_color_scores) +
310
  weights["texture"] * max(all_texture_scores))
 
311
 
312
  # --- 3. Calculate Final Hybrid Score ---
313
- final_score = (SCORE_WEIGHTS['text_score'] * text_score + SCORE_WEIGHTS['image_score'] * image_score)
314
- if not query_visuals or not item_visuals:
 
315
  final_score = text_score # Default to text score if one has no image
 
 
316
 
317
  if final_score >= FINAL_SCORE_THRESHOLD:
318
- results.append({ "_id": str(item['_id']), "score": round(final_score, 4) })
 
 
 
319
 
320
  except Exception as e:
321
- print(f" [COMPARE] ⚠️ Skipping item {item.get('_id')} due to error: {e}")
322
  continue
323
 
324
  results.sort(key=lambda x: x["score"], reverse=True)
325
- print(f" [COMPARE] βœ… Search complete. Found {len(results)} potential matches.")
326
  return jsonify({"matches": results}), 200
327
 
328
  except Exception as e:
 
 
 
 
1
  # --------------------------------------------------------------------------
2
  # UNIFIED AI SERVICE FOR LOST & FOUND V2
3
  # --------------------------------------------------------------------------
4
+ # This Flask application combines two matching engines into a single service:
5
  # 1. Text Engine: Analyzes structured text fields (brand, material, etc.)
6
  # using text embeddings and specific comparison logic.
7
  # 2. Image Engine: Analyzes multiple images per item by segmenting the
 
39
  TEXT_FIELD_WEIGHTS = { "brand": 1.0, "material": 1.0, "markings": 1.0, "colors": 1.0, "size": 1.0 }
40
  TEXT_FIELDS_TO_EMBED = ["brand", "material", "markings"]
41
  SCORE_WEIGHTS = { "text_score": 0.5, "image_score": 0.5 }
42
+ FINAL_SCORE_THRESHOLD = 0.55
43
 
44
  # --- Model Loading ---
45
  print("="*50)
 
74
  # ==========================================================================
75
 
76
  # --- Text Processing Helpers ---
77
+ def get_text_embedding(text: str) -> list:
 
78
  if not text or not text.strip(): return None
79
  instruction = "Represent this sentence for searching relevant passages: "
80
  inputs = tokenizer_text(instruction + text, return_tensors='pt', padding=True, truncation=True, max_length=512).to(device)
 
82
  outputs = model_text(**inputs)
83
  embedding = outputs.last_hidden_state[:, 0, :]
84
  embedding = torch.nn.functional.normalize(embedding, p=2, dim=1)
85
+ return embedding.cpu().numpy()[0].tolist()
86
 
87
  def cosine_similarity(vec1, vec2):
 
88
  if vec1 is None or vec2 is None: return 0.0
89
  vec1, vec2 = np.array(vec1), np.array(vec2)
90
  return float(np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)))
91
 
92
  def calculate_color_similarity(colors1: list, colors2: list) -> float:
 
93
  if not colors1 and not colors2: return 1.0
94
  if not colors1 or not colors2: return 0.0
95
  set1, set2 = set(c.lower() for c in colors1), set(c.lower() for c in colors2)
 
99
 
100
  # --- Image Processing Helpers ---
101
  def segment_guided_object(image: Image.Image, object_label: str) -> Image.Image:
 
102
  prompt = f"a {object_label}."
103
  image_rgb = image.convert("RGB")
104
  image_np = np.array(image_rgb)
 
113
  )
114
 
115
  if not results or len(results[0]['boxes']) == 0:
116
+ print(f" [Segment] ⚠️ Warning: Could not detect '{object_label}'. Using full image.")
117
+ return image
118
 
119
  sam_predictor.set_image(image_np)
120
+ box = results[0]['boxes'][0].cpu().numpy().astype(int)
121
  masks, _, _ = sam_predictor.predict(box=box, multimask_output=False)
122
 
123
  mask = masks[0]
 
128
  return Image.fromarray(object_rgba, 'RGBA')
129
 
130
  def extract_visual_features(segmented_image_rgba: Image.Image) -> dict:
 
131
  image_np = np.array(segmented_image_rgba)
132
  bgr_image = cv2.cvtColor(image_np[:, :, :3], cv2.COLOR_RGB2BGR)
133
  mask = image_np[:, :, 3]
134
 
 
135
  contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
136
  shape_features = np.zeros(7)
137
  if contours:
 
141
  hu_moments = cv2.HuMoments(moments).flatten()
142
  shape_features = -np.sign(hu_moments) * np.log10(np.abs(hu_moments) + 1e-7)
143
 
 
144
  color_hist = cv2.calcHist([bgr_image], [0, 1, 2], mask, [8, 8, 8], [0, 256, 0, 256, 0, 256])
145
  cv2.normalize(color_hist, color_hist)
146
 
 
147
  gray_image = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2GRAY)
148
  lbp = feature.local_binary_pattern(gray_image, P=24, R=3, method="uniform")
149
  (texture_hist, _) = np.histogram(lbp[mask > 0], bins=np.arange(0, 27), range=(0, 26))
 
157
  }
158
 
159
  def calculate_dynamic_weights(all_shape_scores, all_color_scores, stability_factor=0.4):
 
160
  shape_scores, color_scores = np.array(all_shape_scores), np.array(all_color_scores)
161
 
162
  def get_iqr(scores):
 
166
 
167
  shape_dispersion = get_iqr(shape_scores)
168
  color_dispersion = get_iqr(color_scores)
 
169
  inv_shape_disp = 1 / (shape_dispersion + stability_factor)
170
  inv_color_disp = 1 / (color_dispersion + stability_factor)
 
171
  total_inv_disp = inv_shape_disp + inv_color_disp
172
+ remaining_weight = 0.8
 
173
 
174
  shape_weight = remaining_weight * (inv_shape_disp / total_inv_disp) if total_inv_disp > 0 else remaining_weight / 2
175
  color_weight = remaining_weight * (inv_color_disp / total_inv_disp) if total_inv_disp > 0 else remaining_weight / 2
 
186
 
187
  @app.route('/process', methods=['POST'])
188
  def process_item():
 
 
 
 
189
  try:
190
  data = request.json
191
  print(f"\n[PROCESS] Received request for object: {data.get('objectName')}")
192
 
193
  # --- 1. Process Text Features ---
194
+ print(" [PROCESS] Generating text embeddings...")
195
  response = {
196
  "canonicalLabel": data.get('objectName', '').lower().strip(),
197
  "brand_embedding": get_text_embedding(data.get('brand')),
198
  "material_embedding": get_text_embedding(data.get('material')),
199
  "markings_embedding": get_text_embedding(data.get('markings'))
200
  }
201
+ print(" [PROCESS] βœ… Text embeddings generated.")
202
 
203
  # --- 2. Process Image Features ---
204
  visual_features_list = []
205
  if data.get('images'):
206
+ print(f" [PROCESS] Processing {len(data['images'])} image(s)...")
207
+ for i, image_url in enumerate(data['images']):
208
  try:
209
+ print(f" - Processing image {i+1}: {image_url}")
210
  img_response = requests.get(image_url, timeout=20)
211
  img_response.raise_for_status()
212
  image = Image.open(BytesIO(img_response.content))
213
 
214
+ print(" - Segmenting object...")
215
  segmented_image = segment_guided_object(image, data['objectName'])
216
+ print(" - Extracting visual features...")
217
  features = extract_visual_features(segmented_image)
218
  visual_features_list.append(features)
219
+ print(f" - βœ… Image {i+1} processed.")
220
  except Exception as e:
221
+ print(f" - ⚠️ Could not process image {image_url}: {e}")
222
  continue
223
 
224
  response["visual_features"] = visual_features_list
225
+ print(f" [PROCESS] βœ… Successfully processed all features.")
226
  return jsonify(response), 200
227
 
228
  except Exception as e:
 
232
 
233
  @app.route('/compare', methods=['POST'])
234
  def compare_items():
 
 
 
 
235
  try:
236
  payload = request.json
237
  query_item = payload['queryItem']
 
240
 
241
  results = []
242
  for item in search_list:
243
+ item_id = item.get('_id')
244
+ print(f"\n - Comparing with item: {item_id} ({item.get('objectName')})")
245
  try:
246
  # --- 1. Calculate Text Score ---
247
  total_text_score, total_text_weight = 0, 0
248
 
 
249
  for field in TEXT_FIELDS_TO_EMBED:
250
  q_emb = query_item.get(f"{field}_embedding")
251
  i_emb = item.get(f"{field}_embedding")
 
255
  total_text_score += score * weight
256
  total_text_weight += weight
257
 
 
258
  if query_item.get('colors'):
259
  score = calculate_color_similarity(query_item['colors'], item.get('colors', []))
260
  weight = TEXT_FIELD_WEIGHTS.get('colors', 0)
261
  total_text_score += score * weight
262
  total_text_weight += weight
263
 
 
264
  if query_item.get('size'):
265
+ score = 1.0 if query_item.get('size') == item.get('size') else 0.0
266
  weight = TEXT_FIELD_WEIGHTS.get('size', 0)
267
  total_text_score += score * weight
268
  total_text_weight += weight
269
 
270
  text_score = (total_text_score / total_text_weight) if total_text_weight > 0 else 0.0
271
+ print(f" - Text Score: {text_score:.4f}")
272
 
273
  # --- 2. Calculate Image Score ---
274
  image_score = 0.0
 
277
 
278
  if query_visuals and item_visuals:
279
  all_shape_scores, all_color_scores, all_texture_scores = [], [], []
 
280
  for q_vis in query_visuals:
281
  for i_vis in item_visuals:
 
282
  shape_dist = cv2.matchShapes(np.array(q_vis["shape_features"], dtype="float32"), np.array(i_vis["shape_features"], dtype="float32"), cv2.CONTOURS_MATCH_I1, 0.0)
283
  all_shape_scores.append(1.0 / (1.0 + shape_dist))
 
284
  all_color_scores.append(cv2.compareHist(np.array(q_vis["color_features"], dtype="float32"), np.array(i_vis["color_features"], dtype="float32"), cv2.HISTCMP_CORREL))
 
285
  all_texture_scores.append(cv2.compareHist(np.array(q_vis["texture_features"], dtype="float32"), np.array(i_vis["texture_features"], dtype="float32"), cv2.HISTCMP_CORREL))
286
 
287
  if all_shape_scores:
 
289
  image_score = (weights["shape"] * max(all_shape_scores) +
290
  weights["color"] * max(all_color_scores) +
291
  weights["texture"] * max(all_texture_scores))
292
+ print(f" - Image Score: {image_score:.4f}")
293
 
294
  # --- 3. Calculate Final Hybrid Score ---
295
+ if query_visuals and item_visuals:
296
+ final_score = (SCORE_WEIGHTS['text_score'] * text_score + SCORE_WEIGHTS['image_score'] * image_score)
297
+ else:
298
  final_score = text_score # Default to text score if one has no image
299
+
300
+ print(f" - Final Hybrid Score: {final_score:.4f}")
301
 
302
  if final_score >= FINAL_SCORE_THRESHOLD:
303
+ print(f" - βœ… ACCEPTED (Score >= {FINAL_SCORE_THRESHOLD})")
304
+ results.append({ "_id": str(item_id), "score": round(final_score, 4) })
305
+ else:
306
+ print(f" - ❌ REJECTED (Score < {FINAL_SCORE_THRESHOLD})")
307
 
308
  except Exception as e:
309
+ print(f" - ⚠️ Skipping item {item_id} due to error: {e}")
310
  continue
311
 
312
  results.sort(key=lambda x: x["score"], reverse=True)
313
+ print(f"\n[COMPARE] βœ… Search complete. Found {len(results)} potential matches.")
314
  return jsonify({"matches": results}), 200
315
 
316
  except Exception as e: