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
Files changed (1) hide show
  1. app.py +57 -92
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=1)
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
- scene_type = log.get('scene_type', 'complex')
 
 
317
  if scene_type == 'complex':
318
- patch_info = log.get('patch_match_info')
319
- if patch_info is None and dinov2_extractor is not None:
320
- # patch_match_info 可能为 None(dinov2_sim_override 模式下)
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 = _build_text_visualization(log)
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 _build_complex_visualization(log_entry, patch_info):
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
- if not os.path.exists(q_path) or not os.path.exists(h_path):
509
- return None
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
- # 计算 Resize 缩放因子:原始图 → Resize 后图的缩放
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 _patch_to_center(patch_idx, n_w, crop_offset, scale_resize, display_scale, is_right, gap_offset):
557
- """将 patch 索引转换为画布上的中心点像素坐标"""
558
- row = patch_idx // n_w
559
- col = patch_idx % n_w
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
- cx_canvas += gap_offset
579
-
580
- return cx_canvas, cy_canvas
581
 
582
  for qi, hi, sim in matches:
583
- q_cx, q_cy = _patch_to_center(qi, q_n_w, q_crop, q_scale_resize, scale1, False, 0)
584
- h_cx, h_cy = _patch_to_center(hi, h_n_w, h_crop, h_scale_resize, scale2, True, w1 + gap)
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 _build_text_visualization(log_entry):
618
- """文本场景可视化:两张图并排 + BGE 相似度 + 关键词统计
619
 
620
- 上方:两张并排缩略图,标注 Query / History
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
- if not os.path.exists(q_path) or not os.path.exists(h_path):
632
- return None
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