Spaces:
Sleeping
Sleeping
jslmmfboom-coder commited on
Commit ·
e3c4e52
1
Parent(s): a1a87bc
Fix hang: cache images in memory before delete, use 2 workers, vis from cache
Browse files
app.py
CHANGED
|
@@ -46,7 +46,7 @@ dinov2_extractor = None
|
|
| 46 |
ocr_engine = None
|
| 47 |
bge_tokenizer = None
|
| 48 |
bge_model = None
|
| 49 |
-
executor = ThreadPoolExecutor(max_workers=
|
| 50 |
|
| 51 |
session_data = {
|
| 52 |
'startup_stats': None,
|
|
@@ -309,35 +309,45 @@ def _run_detect_pipeline(uploaded_paths, uname, ocr_engine, bge_tokenizer,
|
|
| 309 |
if log['is_same_scene'] and log['query_path'] in uploaded_paths:
|
| 310 |
same_scene_paths.add(log['query_path'])
|
| 311 |
|
| 312 |
-
# ★
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
visualization_data = []
|
| 314 |
for log in evaluation_logs:
|
| 315 |
-
if log.get('is_same_scene'):
|
| 316 |
-
|
|
|
|
|
|
|
| 317 |
if scene_type == 'complex':
|
| 318 |
-
|
| 319 |
-
if
|
| 320 |
-
|
| 321 |
-
# 重新用模型推理获取 patch 匹配
|
| 322 |
-
q_path = log.get('query_path', '')
|
| 323 |
-
h_path = log.get('history_path', '')
|
| 324 |
-
if os.path.exists(q_path) and os.path.exists(h_path):
|
| 325 |
-
try:
|
| 326 |
-
patch_info = dinov2_extractor.compute_patch_matches(q_path, h_path, top_k=50)
|
| 327 |
-
except Exception as e:
|
| 328 |
-
print(f" [可视化] 重新计算 patch 匹配失败: {e}")
|
| 329 |
-
if patch_info:
|
| 330 |
-
vis = _build_complex_visualization(log, patch_info)
|
| 331 |
-
if vis:
|
| 332 |
-
visualization_data.append(vis)
|
| 333 |
elif scene_type == 'text':
|
| 334 |
-
vis =
|
| 335 |
if vis:
|
| 336 |
visualization_data.append(vis)
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
for path in same_scene_paths:
|
| 340 |
-
delete_file(path)
|
| 341 |
|
| 342 |
per_image_results = {}
|
| 343 |
for orig, path in [(os.path.basename(p), p) for p in uploaded_paths]:
|
|
@@ -487,16 +497,10 @@ async def detect(
|
|
| 487 |
}
|
| 488 |
|
| 489 |
|
| 490 |
-
def
|
| 491 |
-
"""复杂场景可视化:DINOv2 patch 匹配连线
|
| 492 |
|
| 493 |
-
|
| 494 |
-
DINOv2 预处理流程:原始图 → Resize(518,短边) → CenterCrop(518)
|
| 495 |
-
Patch 是在 518x518 的图像上以 14px 为步长划分的网格。
|
| 496 |
-
要将 patch 坐标映射回原始图像:
|
| 497 |
-
1. patch_idx → (row, col) → 像素坐标 (518x518 空间)
|
| 498 |
-
2. 加上 CenterCrop 偏移 → Resize 后图像坐标
|
| 499 |
-
3. 除以 Resize 缩放因子 → 原始图像坐标
|
| 500 |
"""
|
| 501 |
try:
|
| 502 |
import cv2
|
|
@@ -504,20 +508,18 @@ def _build_complex_visualization(log_entry, patch_info):
|
|
| 504 |
|
| 505 |
q_path = log_entry.get('query_path', '')
|
| 506 |
h_path = log_entry.get('history_path', '')
|
|
|
|
| 507 |
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
img1 = cv2.imdecode(np.fromfile(q_path, dtype=np.uint8), cv2.IMREAD_COLOR)
|
| 512 |
-
img2 = cv2.imdecode(np.fromfile(h_path, dtype=np.uint8), cv2.IMREAD_COLOR)
|
| 513 |
if img1 is None or img2 is None:
|
| 514 |
return None
|
|
|
|
|
|
|
| 515 |
|
| 516 |
-
# 原始图片尺寸
|
| 517 |
orig_h1, orig_w1 = img1.shape[:2]
|
| 518 |
orig_h2, orig_w2 = img2.shape[:2]
|
| 519 |
|
| 520 |
-
# 缩放显示到统一高度
|
| 521 |
max_h = 400
|
| 522 |
scale1 = max_h / orig_h1
|
| 523 |
scale2 = max_h / orig_h2
|
|
@@ -533,7 +535,6 @@ def _build_complex_visualization(log_entry, patch_info):
|
|
| 533 |
q_orig_size = patch_info.get('query_image_size', (orig_w1, orig_h1))
|
| 534 |
h_orig_size = patch_info.get('hist_image_size', (orig_w2, orig_h2))
|
| 535 |
|
| 536 |
-
# CenterCrop 偏移和 Resize 缩放(query 和 hist 各自独立)
|
| 537 |
q_crop = patch_info.get('query_crop_offset', (0, 0))
|
| 538 |
h_crop = patch_info.get('hist_crop_offset', (0, 0))
|
| 539 |
q_resize = patch_info.get('query_resize_size', (518, 518))
|
|
@@ -541,11 +542,9 @@ def _build_complex_visualization(log_entry, patch_info):
|
|
| 541 |
|
| 542 |
q_n_h, q_n_w = q_grid
|
| 543 |
h_n_h, h_n_w = h_grid
|
| 544 |
-
input_size = 518
|
| 545 |
patch_size = 14
|
| 546 |
|
| 547 |
-
|
| 548 |
-
q_scale_resize = q_resize[1] / q_orig_size[1] # resize_h / orig_h
|
| 549 |
h_scale_resize = h_resize[1] / h_orig_size[1]
|
| 550 |
|
| 551 |
gap = 20
|
|
@@ -553,41 +552,20 @@ def _build_complex_visualization(log_entry, patch_info):
|
|
| 553 |
canvas[:h1, :w1] = img1_disp
|
| 554 |
canvas[:h2, w1+gap:] = img2_disp
|
| 555 |
|
| 556 |
-
def
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
# 在 518x518 输入图像上的像素坐标(patch 中心)
|
| 562 |
-
cx_518 = (col + 0.5) * patch_size
|
| 563 |
-
cy_518 = (row + 0.5) * patch_size
|
| 564 |
-
|
| 565 |
-
# 加上 CenterCrop 偏移,得到 Resize 后图像的坐标
|
| 566 |
-
cx_resize = cx_518 + crop_offset[0]
|
| 567 |
-
cy_resize = cy_518 + crop_offset[1]
|
| 568 |
-
|
| 569 |
-
# 反向缩放,得到原始图像坐标
|
| 570 |
-
cx_orig = cx_resize / scale_resize
|
| 571 |
-
cy_orig = cy_resize / scale_resize
|
| 572 |
-
|
| 573 |
-
# 乘以显示缩放因子,得到画布坐标
|
| 574 |
-
cx_canvas = int(cx_orig * display_scale)
|
| 575 |
-
cy_canvas = int(cy_orig * display_scale)
|
| 576 |
-
|
| 577 |
if is_right:
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
return cx_canvas, cy_canvas
|
| 581 |
|
| 582 |
for qi, hi, sim in matches:
|
| 583 |
-
q_cx, q_cy =
|
| 584 |
-
h_cx, h_cy =
|
| 585 |
|
| 586 |
-
# 颜色:相似度越高越绿,越低越黄
|
| 587 |
intensity = min(1.0, max(0.0, (sim - 0.3) / 0.7))
|
| 588 |
color = (0, int(200 * intensity + 55), int(255 * (1 - intensity)))
|
| 589 |
-
|
| 590 |
-
# 只绘制连线
|
| 591 |
cv2.line(canvas, (q_cx, q_cy), (h_cx, h_cy), color, 1, cv2.LINE_AA)
|
| 592 |
|
| 593 |
n_matches = len(matches)
|
|
@@ -614,25 +592,22 @@ def _build_complex_visualization(log_entry, patch_info):
|
|
| 614 |
return None
|
| 615 |
|
| 616 |
|
| 617 |
-
def
|
| 618 |
-
"""文本场景可视化:两张图并排 +
|
| 619 |
|
| 620 |
-
|
| 621 |
-
下方:BGE 语义相似度 + 共同/差异关键词统计(用 PIL 绘制中文)
|
| 622 |
"""
|
| 623 |
try:
|
| 624 |
import cv2
|
| 625 |
import numpy as np
|
| 626 |
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
| 627 |
|
| 628 |
q_path = log_entry.get('query_path', '')
|
| 629 |
h_path = log_entry.get('history_path', '')
|
| 630 |
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
img1 = cv2.imdecode(np.fromfile(q_path, dtype=np.uint8), cv2.IMREAD_COLOR)
|
| 635 |
-
img2 = cv2.imdecode(np.fromfile(h_path, dtype=np.uint8), cv2.IMREAD_COLOR)
|
| 636 |
if img1 is None or img2 is None:
|
| 637 |
return None
|
| 638 |
|
|
@@ -640,15 +615,12 @@ def _build_text_visualization(log_entry):
|
|
| 640 |
text2 = log_entry.get('history_text', '')
|
| 641 |
text_sim = log_entry.get('text_similarity', 0)
|
| 642 |
|
| 643 |
-
# 提取关键词
|
| 644 |
-
import re
|
| 645 |
words1 = set(re.findall(r'[a-zA-Z\u4e00-\u9fff]{2,}', text1))
|
| 646 |
words2 = set(re.findall(r'[a-zA-Z\u4e00-\u9fff]{2,}', text2))
|
| 647 |
common_words = words1 & words2
|
| 648 |
diff_words1 = words1 - words2
|
| 649 |
diff_words2 = words2 - words1
|
| 650 |
|
| 651 |
-
# 缩放图片
|
| 652 |
max_h = 300
|
| 653 |
scale1 = max_h / img1.shape[0]
|
| 654 |
scale2 = max_h / img2.shape[0]
|
|
@@ -658,16 +630,12 @@ def _build_text_visualization(log_entry):
|
|
| 658 |
h1, w1 = img1_s.shape[:2]
|
| 659 |
h2, w2 = img2_s.shape[:2]
|
| 660 |
|
| 661 |
-
# 构建画布:只放两张图并排
|
| 662 |
gap = 10
|
| 663 |
total_w = w1 + gap + w2
|
| 664 |
canvas = np.ones((max_h, total_w, 3), dtype=np.uint8) * 240
|
| 665 |
-
|
| 666 |
-
# 放图片
|
| 667 |
canvas[:max_h, :w1] = img1_s
|
| 668 |
canvas[:max_h, w1+gap:w1+gap+w2] = img2_s
|
| 669 |
|
| 670 |
-
# 用 PIL 绘制图片标注
|
| 671 |
canvas_rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
|
| 672 |
pil_img = Image.fromarray(canvas_rgb)
|
| 673 |
draw = ImageDraw.Draw(pil_img)
|
|
@@ -680,15 +648,12 @@ def _build_text_visualization(log_entry):
|
|
| 680 |
except Exception:
|
| 681 |
font_large = ImageFont.load_default()
|
| 682 |
|
| 683 |
-
# 图片标注
|
| 684 |
draw.text((10, 8), "Query (OCR text)", fill=(0, 0, 0), font=font_large)
|
| 685 |
draw.text((w1+gap+10, 8), "History (OCR text)", fill=(0, 0, 0), font=font_large)
|
| 686 |
|
| 687 |
common_list = sorted(common_words)[:20]
|
| 688 |
|
| 689 |
-
# 转回 OpenCV 格式
|
| 690 |
canvas = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
|
| 691 |
-
|
| 692 |
_, buffer = cv2.imencode('.jpg', canvas, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
| 693 |
img_b64 = base64.b64encode(buffer).decode('utf-8')
|
| 694 |
|
|
|
|
| 46 |
ocr_engine = None
|
| 47 |
bge_tokenizer = None
|
| 48 |
bge_model = None
|
| 49 |
+
executor = ThreadPoolExecutor(max_workers=2)
|
| 50 |
|
| 51 |
session_data = {
|
| 52 |
'startup_stats': None,
|
|
|
|
| 309 |
if log['is_same_scene'] and log['query_path'] in uploaded_paths:
|
| 310 |
same_scene_paths.add(log['query_path'])
|
| 311 |
|
| 312 |
+
# ★ 先把需要可视化的图片数据读入内存,再删除文件 ★
|
| 313 |
+
# 这样可视化构建不依赖磁盘文件,且不会被删除操作影响
|
| 314 |
+
_image_cache = {}
|
| 315 |
+
for log in evaluation_logs:
|
| 316 |
+
if not log.get('is_same_scene'):
|
| 317 |
+
continue
|
| 318 |
+
for key in ('query_path', 'history_path'):
|
| 319 |
+
p = log.get(key, '')
|
| 320 |
+
if p and p not in _image_cache and os.path.exists(p):
|
| 321 |
+
try:
|
| 322 |
+
import cv2, numpy as np
|
| 323 |
+
data = np.fromfile(p, dtype=np.uint8)
|
| 324 |
+
img = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
| 325 |
+
if img is not None:
|
| 326 |
+
_image_cache[p] = img
|
| 327 |
+
except Exception:
|
| 328 |
+
pass
|
| 329 |
+
|
| 330 |
+
# 删除同一场景的文件
|
| 331 |
+
for path in same_scene_paths:
|
| 332 |
+
delete_file(path)
|
| 333 |
+
|
| 334 |
+
# 构建可视化(使用内存中的图片数据)
|
| 335 |
visualization_data = []
|
| 336 |
for log in evaluation_logs:
|
| 337 |
+
if not log.get('is_same_scene'):
|
| 338 |
+
continue
|
| 339 |
+
scene_type = log.get('scene_type', 'complex')
|
| 340 |
+
try:
|
| 341 |
if scene_type == 'complex':
|
| 342 |
+
vis = _build_complex_vis_from_cache(log, _image_cache)
|
| 343 |
+
if vis:
|
| 344 |
+
visualization_data.append(vis)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
elif scene_type == 'text':
|
| 346 |
+
vis = _build_text_vis_from_cache(log, _image_cache)
|
| 347 |
if vis:
|
| 348 |
visualization_data.append(vis)
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f" [可视化] 生成失败: {e}")
|
|
|
|
|
|
|
| 351 |
|
| 352 |
per_image_results = {}
|
| 353 |
for orig, path in [(os.path.basename(p), p) for p in uploaded_paths]:
|
|
|
|
| 497 |
}
|
| 498 |
|
| 499 |
|
| 500 |
+
def _build_complex_vis_from_cache(log_entry, image_cache):
|
| 501 |
+
"""复杂场景可视化:DINOv2 patch 匹配连线
|
| 502 |
|
| 503 |
+
图片数据从 image_cache 中获取(内存),不依赖磁盘文件。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
"""
|
| 505 |
try:
|
| 506 |
import cv2
|
|
|
|
| 508 |
|
| 509 |
q_path = log_entry.get('query_path', '')
|
| 510 |
h_path = log_entry.get('history_path', '')
|
| 511 |
+
patch_info = log_entry.get('patch_match_info')
|
| 512 |
|
| 513 |
+
img1 = image_cache.get(q_path)
|
| 514 |
+
img2 = image_cache.get(h_path)
|
|
|
|
|
|
|
|
|
|
| 515 |
if img1 is None or img2 is None:
|
| 516 |
return None
|
| 517 |
+
if patch_info is None:
|
| 518 |
+
return None
|
| 519 |
|
|
|
|
| 520 |
orig_h1, orig_w1 = img1.shape[:2]
|
| 521 |
orig_h2, orig_w2 = img2.shape[:2]
|
| 522 |
|
|
|
|
| 523 |
max_h = 400
|
| 524 |
scale1 = max_h / orig_h1
|
| 525 |
scale2 = max_h / orig_h2
|
|
|
|
| 535 |
q_orig_size = patch_info.get('query_image_size', (orig_w1, orig_h1))
|
| 536 |
h_orig_size = patch_info.get('hist_image_size', (orig_w2, orig_h2))
|
| 537 |
|
|
|
|
| 538 |
q_crop = patch_info.get('query_crop_offset', (0, 0))
|
| 539 |
h_crop = patch_info.get('hist_crop_offset', (0, 0))
|
| 540 |
q_resize = patch_info.get('query_resize_size', (518, 518))
|
|
|
|
| 542 |
|
| 543 |
q_n_h, q_n_w = q_grid
|
| 544 |
h_n_h, h_n_w = h_grid
|
|
|
|
| 545 |
patch_size = 14
|
| 546 |
|
| 547 |
+
q_scale_resize = q_resize[1] / q_orig_size[1]
|
|
|
|
| 548 |
h_scale_resize = h_resize[1] / h_orig_size[1]
|
| 549 |
|
| 550 |
gap = 20
|
|
|
|
| 552 |
canvas[:h1, :w1] = img1_disp
|
| 553 |
canvas[:h2, w1+gap:] = img2_disp
|
| 554 |
|
| 555 |
+
def _patch_center(idx, n_w, crop_off, scale_r, disp_s, is_right, gap_off):
|
| 556 |
+
row, col = idx // n_w, idx % n_w
|
| 557 |
+
cx = int(((col + 0.5) * patch_size + crop_off[0]) / scale_r * disp_s)
|
| 558 |
+
cy = int(((row + 0.5) * patch_size + crop_off[1]) / scale_r * disp_s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
if is_right:
|
| 560 |
+
cx += gap_off
|
| 561 |
+
return cx, cy
|
|
|
|
| 562 |
|
| 563 |
for qi, hi, sim in matches:
|
| 564 |
+
q_cx, q_cy = _patch_center(qi, q_n_w, q_crop, q_scale_resize, scale1, False, 0)
|
| 565 |
+
h_cx, h_cy = _patch_center(hi, h_n_w, h_crop, h_scale_resize, scale2, True, w1 + gap)
|
| 566 |
|
|
|
|
| 567 |
intensity = min(1.0, max(0.0, (sim - 0.3) / 0.7))
|
| 568 |
color = (0, int(200 * intensity + 55), int(255 * (1 - intensity)))
|
|
|
|
|
|
|
| 569 |
cv2.line(canvas, (q_cx, q_cy), (h_cx, h_cy), color, 1, cv2.LINE_AA)
|
| 570 |
|
| 571 |
n_matches = len(matches)
|
|
|
|
| 592 |
return None
|
| 593 |
|
| 594 |
|
| 595 |
+
def _build_text_vis_from_cache(log_entry, image_cache):
|
| 596 |
+
"""文本场景可视化:两张图并排 + 标注
|
| 597 |
|
| 598 |
+
图片数据从 image_cache 中获取(内存),不依赖磁盘文件。
|
|
|
|
| 599 |
"""
|
| 600 |
try:
|
| 601 |
import cv2
|
| 602 |
import numpy as np
|
| 603 |
from PIL import Image, ImageDraw, ImageFont
|
| 604 |
+
import re
|
| 605 |
|
| 606 |
q_path = log_entry.get('query_path', '')
|
| 607 |
h_path = log_entry.get('history_path', '')
|
| 608 |
|
| 609 |
+
img1 = image_cache.get(q_path)
|
| 610 |
+
img2 = image_cache.get(h_path)
|
|
|
|
|
|
|
|
|
|
| 611 |
if img1 is None or img2 is None:
|
| 612 |
return None
|
| 613 |
|
|
|
|
| 615 |
text2 = log_entry.get('history_text', '')
|
| 616 |
text_sim = log_entry.get('text_similarity', 0)
|
| 617 |
|
|
|
|
|
|
|
| 618 |
words1 = set(re.findall(r'[a-zA-Z\u4e00-\u9fff]{2,}', text1))
|
| 619 |
words2 = set(re.findall(r'[a-zA-Z\u4e00-\u9fff]{2,}', text2))
|
| 620 |
common_words = words1 & words2
|
| 621 |
diff_words1 = words1 - words2
|
| 622 |
diff_words2 = words2 - words1
|
| 623 |
|
|
|
|
| 624 |
max_h = 300
|
| 625 |
scale1 = max_h / img1.shape[0]
|
| 626 |
scale2 = max_h / img2.shape[0]
|
|
|
|
| 630 |
h1, w1 = img1_s.shape[:2]
|
| 631 |
h2, w2 = img2_s.shape[:2]
|
| 632 |
|
|
|
|
| 633 |
gap = 10
|
| 634 |
total_w = w1 + gap + w2
|
| 635 |
canvas = np.ones((max_h, total_w, 3), dtype=np.uint8) * 240
|
|
|
|
|
|
|
| 636 |
canvas[:max_h, :w1] = img1_s
|
| 637 |
canvas[:max_h, w1+gap:w1+gap+w2] = img2_s
|
| 638 |
|
|
|
|
| 639 |
canvas_rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
|
| 640 |
pil_img = Image.fromarray(canvas_rgb)
|
| 641 |
draw = ImageDraw.Draw(pil_img)
|
|
|
|
| 648 |
except Exception:
|
| 649 |
font_large = ImageFont.load_default()
|
| 650 |
|
|
|
|
| 651 |
draw.text((10, 8), "Query (OCR text)", fill=(0, 0, 0), font=font_large)
|
| 652 |
draw.text((w1+gap+10, 8), "History (OCR text)", fill=(0, 0, 0), font=font_large)
|
| 653 |
|
| 654 |
common_list = sorted(common_words)[:20]
|
| 655 |
|
|
|
|
| 656 |
canvas = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
|
|
|
|
| 657 |
_, buffer = cv2.imencode('.jpg', canvas, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
| 658 |
img_b64 = base64.b64encode(buffer).decode('utf-8')
|
| 659 |
|