Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
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 filesAdditive 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>
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")
|