jichao Claude Opus 4.8 (1M context) commited on
Commit
ee48108
·
1 Parent(s): 58b1c8f

Add get_patch_embeddings endpoint for ROI / patch-selection search

Browse files

Additive endpoint returning per-patch token embeddings (grid x grid) plus a
mean-pooled dense vector, for region-of-interest late-interaction search.
The existing /get_embedding endpoint is left untouched for backward compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +131 -0
app.py CHANGED
@@ -355,6 +355,118 @@ def get_embedding(image_pil: Image.Image, model_name: str, embedding_method: str
355
  "message": error_msg
356
  }
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  # --- Gradio Interface ---
359
  EXAMPLE_DIR = "examples"
360
  EXAMPLE_IMAGE = os.path.join(EXAMPLE_DIR, "sample_image.png")
@@ -410,6 +522,25 @@ with gr.Blocks() as iface:
410
  outputs=output_json
411
  )
412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  # --- Launch the Gradio App ---
414
  if __name__ == "__main__":
415
  iface.launch(server_name="0.0.0.0")
 
355
  "message": error_msg
356
  }
357
 
358
+ # --- Per-Patch Embedding Function (ROI / patch-selection search) ---
359
+ def get_patch_embeddings(image_pil: Image.Image, model_name: str = MULTI_FPS_MODEL_NAME) -> dict:
360
+ """Return per-patch token embeddings plus a mean-pooled dense embedding.
361
+
362
+ This is an additive endpoint used by the region-of-interest (ROI) search
363
+ flow: the client lets the user brush a grid of patches and uses the selected
364
+ patch tokens directly for a late-interaction (MaxSim) query. The existing
365
+ `/get_embedding` endpoint is intentionally left untouched for backward
366
+ compatibility.
367
+
368
+ Returns a dict with:
369
+ - model_name: the model used (must match the collection's document tokens)
370
+ - embedding_method: always 'mean pooling' (used for first-stage dense retrieval)
371
+ - data: [D] L2-normalized mean-pooled dense embedding (first stage)
372
+ - patch_tokens: [num_patches, D] L2-normalized per-patch tokens in row-major
373
+ order (grid x grid), for user-selected second-stage query
374
+ - grid: int side length of the patch grid (sqrt(num_patches), e.g. 14)
375
+ - num_patches: int total number of patch tokens
376
+ - message: 'Success' or an error description
377
+ """
378
+ if image_pil is None:
379
+ return {
380
+ "model_name": model_name,
381
+ "embedding_method": "mean pooling",
382
+ "data": None,
383
+ "patch_tokens": None,
384
+ "grid": None,
385
+ "num_patches": None,
386
+ "message": "Error: Please upload an image."
387
+ }
388
+
389
+ # Fall back to the known multi-token model if an unknown name is requested so
390
+ # the returned patch tokens stay comparable with the collection's doc tokens.
391
+ if model_name not in MODEL_CONFIGS:
392
+ print(f"Unknown model '{model_name}' for patch embeddings; falling back to '{MULTI_FPS_MODEL_NAME}'.")
393
+ model_name = MULTI_FPS_MODEL_NAME
394
+
395
+ if model_name not in LOADED_MODELS:
396
+ try:
397
+ print(f"Loading model {model_name} on demand (patch embeddings)...")
398
+ LOADED_MODELS[model_name] = load_model(model_name)
399
+ except Exception as e:
400
+ print(f"Error loading model {model_name}: {e}")
401
+ return {
402
+ "model_name": model_name,
403
+ "embedding_method": "mean pooling",
404
+ "data": None,
405
+ "patch_tokens": None,
406
+ "grid": None,
407
+ "num_patches": None,
408
+ "message": f"Error loading model '{model_name}'. Check logs."
409
+ }
410
+
411
+ model = LOADED_MODELS[model_name]
412
+ preprocess = get_preprocess(model_name)
413
+
414
+ try:
415
+ with torch.no_grad():
416
+ img_tensor = preprocess(image_pil).unsqueeze(0) # [1, C, 224, 224]
417
+ features = model.forward_features(img_tensor)
418
+ if isinstance(features, tuple):
419
+ features = features[0]
420
+
421
+ if len(features.shape) != 3 or features.shape[1] <= 1:
422
+ return {
423
+ "model_name": model_name,
424
+ "embedding_method": "mean pooling",
425
+ "data": None,
426
+ "patch_tokens": None,
427
+ "grid": None,
428
+ "num_patches": None,
429
+ "message": f"Error: Unexpected feature output shape from model '{model_name}'. Check logs."
430
+ }
431
+
432
+ patch_tokens = features[:, 1:] # (1, P, D) drop CLS token
433
+ num_patches = patch_tokens.shape[1]
434
+
435
+ # First-stage dense vector: mean pool over patch tokens, L2-normalized
436
+ dense = F.normalize(patch_tokens.mean(dim=1), p=2, dim=1) # (1, D)
437
+
438
+ # Per-patch tokens, L2-normalized so cosine == dot for the MaxSim query
439
+ patch_norm = F.normalize(patch_tokens, p=2, dim=-1) # (1, P, D)
440
+
441
+ grid = int(round(num_patches ** 0.5))
442
+
443
+ data = dense.squeeze(0).cpu().numpy().tolist()
444
+ patch_list = patch_norm.squeeze(0).cpu().numpy().tolist()
445
+
446
+ return {
447
+ "model_name": model_name,
448
+ "embedding_method": "mean pooling",
449
+ "data": data,
450
+ "patch_tokens": patch_list,
451
+ "grid": grid,
452
+ "num_patches": num_patches,
453
+ "message": "Success"
454
+ }
455
+
456
+ except Exception as e:
457
+ print(f"Error computing patch embeddings with model {model_name}: {e}")
458
+ import traceback
459
+ traceback.print_exc()
460
+ return {
461
+ "model_name": model_name,
462
+ "embedding_method": "mean pooling",
463
+ "data": None,
464
+ "patch_tokens": None,
465
+ "grid": None,
466
+ "num_patches": None,
467
+ "message": f"Error processing image with model '{model_name}'. Check logs."
468
+ }
469
+
470
  # --- Gradio Interface ---
471
  EXAMPLE_DIR = "examples"
472
  EXAMPLE_IMAGE = os.path.join(EXAMPLE_DIR, "sample_image.png")
 
522
  outputs=output_json
523
  )
524
 
525
+ # --- Per-patch (ROI) embeddings: additive endpoint, existing flow untouched ---
526
+ gr.Markdown("### Patch / Region-of-Interest Embeddings")
527
+ gr.Markdown(
528
+ "Returns per-patch token embeddings (grid x grid) plus a mean-pooled dense "
529
+ "vector, for region-of-interest late-interaction search."
530
+ )
531
+ with gr.Row():
532
+ with gr.Column(scale=1):
533
+ patch_button = gr.Button("Calculate Patch Embeddings (ROI)")
534
+ with gr.Column(scale=2):
535
+ patch_output_json = gr.JSON(label="Patch Embeddings (JSON)")
536
+
537
+ patch_button.click(
538
+ fn=get_patch_embeddings,
539
+ inputs=[input_image, model_selector],
540
+ outputs=patch_output_json,
541
+ api_name="get_patch_embeddings",
542
+ )
543
+
544
  # --- Launch the Gradio App ---
545
  if __name__ == "__main__":
546
  iface.launch(server_name="0.0.0.0")