File size: 17,588 Bytes
e16aadc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405df18
 
e16aadc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a3a826
e16aadc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a3a826
e16aadc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import os
import traceback
import numpy as np
from datetime import datetime

from module.file_manager import get_history_images, load_metadata, save_metadata
from module.text_classifier import classify_scene
from module.text_matcher import compare_text_scene, compare_text_scene_cached, compare_text_scene_pure_bge


DINO_PRESCREEN_THRESHOLD = 0.3


def evaluate_scene_consistency_cached(matcher, query_paths, username,
                                       ocr_engine=None, bge_tokenizer=None,
                                       bge_model=None, scene_type_q='complex',
                                       qd_search_fn=None, dinov2_extractor=None,
                                       precomputed_ocr_result=None,
                                      precomputed_bge_vec=None,
                                      qd_cache=None):
    """Qdrant ANN 搜索管线: DINOv2/BGE 预筛 + MASt3R 精确判定 + 早停

    流程 (与 test_5.ipynb 一致):
    文本场景:
      1. 提取查询图 OCR 文本 -> BGE 编码
      2. Qdrant ANN 搜索 (BGE 向量) -> top-K 候选
      3. 对候选逐一 compare_text_scene_pure_bge (纯BGE语义向量)
      4. 早停: 匹配到同场景即跳过剩余
    复杂场景:
      1. 提取查询图 DINOv2 特征
      2. Qdrant ANN 搜索 (DINOv2 向量) -> top-K 候选
      3. DINOv2 预筛: 相似度 < 0.3 直接跳过 MASt3R
      4. 通过预筛的候选 -> matcher.compare() (MASt3R 联合推理)
      5. 早停: 匹配到同场景即跳过剩余
    """
    evaluation_logs = []
    best_match_result = None
    best_match_score = -1

    import time as _t
    for q_path in query_paths:
        if scene_type_q == 'text' and bge_tokenizer is not None:
            _et0 = _t.perf_counter()
            if precomputed_ocr_result is not None:
                full_text = ' '.join(line[1][0] for line in precomputed_ocr_result[0]) if (precomputed_ocr_result and precomputed_ocr_result[0]) else ''
            else:
                from module.text_matcher import extract_text
                full_text, _ = extract_text(q_path, ocr_engine)
            _et1 = _t.perf_counter()
            if precomputed_bge_vec is not None:
                q_vec = precomputed_bge_vec
            else:
                from module.qdrant_manager import _bge_encode
                q_vec = _bge_encode(full_text, bge_tokenizer, bge_model)
            _et2 = _t.perf_counter()
            _qd_cache = qd_cache.get('text', {}) if (qd_cache and isinstance(qd_cache, dict)) else {}
            if not _qd_cache:
                from module.qdrant_manager import qdrant_query_scene
                _qd_cache = qdrant_query_scene(username, 'text')
            _et3 = _t.perf_counter()
            hits = []
            for _pid, _entry in _qd_cache.items():
                _h_path = _entry.get('path', '')
                if os.path.abspath(q_path) == os.path.abspath(_h_path):
                    continue
                if not os.path.exists(_h_path):
                    continue
                _h_vec = _entry.get('_vector', _entry.get('bge_vector', np.zeros(512, dtype=np.float32)))
                _sim = float(np.dot(q_vec, _h_vec))
                if _sim >= 0.3:
                    hits.append((_sim, _pid, _entry))
            hits.sort(key=lambda x: x[0], reverse=True)
            hits = hits[:3]
            print(f'  [TIME-EVAL] text_extract: {_et1-_et0:.3f}s  bge_encode: {_et2-_et1:.3f}s  qdrant_scroll: {_et3-_et2:.3f}s  python_search: {_t.perf_counter()-_et3:.3f}s  n_hits: {len(hits)}')

            for score, pid, entry in hits:
                h_path = entry.get('path', '')
                if os.path.abspath(q_path) == os.path.abspath(h_path):
                    continue
                try:
                    result = compare_text_scene_pure_bge(
                        q_path, entry, ocr_engine, bge_tokenizer, bge_model,
                        precomputed_text=full_text, precomputed_bge=q_vec
                    )
                    log_entry = {
                        'query_image': os.path.basename(q_path),
                        'history_image': entry.get('filename', ''),
                        'query_path': q_path,
                        'history_path': h_path,
                        'scene_type': 'text',
                        'doc_score1': 0.0,
                        'doc_score2': 0.0,
                        'similarity_score': result['similarity_score'],
                        'match_count': result.get('match_count', 0),
                        'raw_match_count': 0,
                        'inlier_ratio': result.get('inlier_ratio', 0.0),
                        'avg_confidence': result.get('avg_confidence', 0.0),
                        'text_similarity': result.get('text_similarity'),
                        'ssim_score': result.get('ssim_score'),
                        'seal_bonus': result.get('seal_bonus'),
                        'is_same_scene': result['is_same_scene'],
                        'gamma_info': [],
                        'bge_search_score': round(score, 4),
                        'query_text': full_text,
                        'history_text': entry.get('ocr_text', ''),
                    }
                except Exception as e:
                    log_entry = {
                        'query_image': os.path.basename(q_path),
                        'history_image': entry.get('filename', ''),
                        'query_path': q_path,
                        'history_path': h_path,
                        'scene_type': 'text',
                        'doc_score1': 0.0, 'doc_score2': 0.0,
                        'similarity_score': 0.0,
                        'match_count': 0, 'raw_match_count': 0,
                        'inlier_ratio': 0.0, 'avg_confidence': 0.0,
                        'is_same_scene': False, 'gamma_info': [],
                        'error': str(e),
                    }
                evaluation_logs.append(log_entry)

                if log_entry['is_same_scene']:
                    if log_entry['similarity_score'] > best_match_score:
                        best_match_score = log_entry['similarity_score']
                        best_match_result = log_entry.copy()
                    print(f"  [早停] {os.path.basename(q_path)}{log_entry['history_image']} 匹配为同场景, 跳过剩余比对")
                    break
                elif log_entry['similarity_score'] > best_match_score:
                    best_match_score = log_entry['similarity_score']
                    best_match_result = log_entry.copy()

        elif scene_type_q == 'complex' and dinov2_extractor is not None:
            from module.qdrant_manager import _dinov2_encode
            q_vec = _dinov2_encode(q_path, dinov2_extractor)
            if not isinstance(q_vec, np.ndarray):
                q_vec = q_vec.cpu().numpy() if hasattr(q_vec, 'cpu') else np.array(q_vec, dtype=np.float32)
            _qd_cache_c = qd_cache.get('complex', {}) if (qd_cache and isinstance(qd_cache, dict)) else {}
            if not _qd_cache_c:
                from module.qdrant_manager import qdrant_query_scene
                _qd_cache_c = qdrant_query_scene(username, 'complex')
            hits = []
            for _pid, _entry in _qd_cache_c.items():
                _h_path = _entry.get('path', '')
                if os.path.abspath(q_path) == os.path.abspath(_h_path):
                    continue
                if not os.path.exists(_h_path):
                    continue
                _h_vec = _entry.get('_vector', np.zeros(384, dtype=np.float32))
                _sim = float(np.dot(q_vec, _h_vec))
                if _sim >= DINO_PRESCREEN_THRESHOLD:
                    hits.append((_sim, _pid, _entry))
            hits.sort(key=lambda x: x[0], reverse=True)
            hits = hits[:3]

            for score, pid, entry in hits:
                h_path = entry.get('path', '')
                if os.path.abspath(q_path) == os.path.abspath(h_path):
                    continue
                dino_sim = score

                if dino_sim < DINO_PRESCREEN_THRESHOLD:
                    log_entry = {
                        'query_image': os.path.basename(q_path),
                        'history_image': entry.get('filename', ''),
                        'query_path': q_path,
                        'history_path': h_path,
                        'scene_type': 'complex',
                        'doc_score1': 0.0, 'doc_score2': 0.0,
                        'similarity_score': round(dino_sim, 4),
                        'match_count': 0, 'raw_match_count': 0,
                        'inlier_ratio': 0.0, 'avg_confidence': 0.0,
                        'dinov2_similarity': round(dino_sim, 4),
                        'is_same_scene': False, 'gamma_info': [],
                        'mast3r_skipped': True,
                        'dino_search_score': round(dino_sim, 4),
                    }
                else:
                    try:
                        result = matcher.compare(q_path, h_path, dinov2_sim_override=dino_sim)
                        log_entry = {
                            'query_image': os.path.basename(q_path),
                            'history_image': entry.get('filename', ''),
                            'query_path': q_path,
                            'history_path': h_path,
                            'scene_type': 'complex',
                            'doc_score1': 0.0, 'doc_score2': 0.0,
                            'similarity_score': result['similarity_score'],
                            'match_count': result['match_count'],
                            'raw_match_count': result['raw_match_count'],
                            'inlier_ratio': result['inlier_ratio'],
                            'avg_confidence': result['avg_confidence'],
                            'mast3r_is_same': result.get('mast3r_is_same'),
                            'dinov2_is_same': result.get('dinov2_is_same'),
                            'dinov2_similarity': result.get('dinov2_similarity'),
                            'is_same_scene': result['is_same_scene'],
                            'gamma_info': result.get('gamma_info', []),
                            'patch_match_info': result.get('patch_match_info'),
                            'mast3r_skipped': False,
                            'dino_search_score': round(dino_sim, 4),
                        }
                    except Exception as e:
                        log_entry = {
                            'query_image': os.path.basename(q_path),
                            'history_image': entry.get('filename', ''),
                            'query_path': q_path,
                            'history_path': h_path,
                            'scene_type': 'complex',
                            'doc_score1': 0.0, 'doc_score2': 0.0,
                            'similarity_score': 0.0,
                            'match_count': 0, 'raw_match_count': 0,
                            'inlier_ratio': 0.0, 'avg_confidence': 0.0,
                            'is_same_scene': False, 'gamma_info': [],
                            'error': str(e),
                        }
                evaluation_logs.append(log_entry)

                if log_entry['is_same_scene']:
                    if log_entry['similarity_score'] > best_match_score:
                        best_match_score = log_entry['similarity_score']
                        best_match_result = log_entry.copy()
                    print(f"  [早停] {os.path.basename(q_path)}{log_entry['history_image']} 匹配为同场景, 跳过剩余比对")
                    break
                elif log_entry['similarity_score'] > best_match_score:
                    best_match_score = log_entry['similarity_score']
                    best_match_result = log_entry.copy()

    any_same = any(log['is_same_scene'] for log in evaluation_logs)
    final_result = not any_same

    return final_result, evaluation_logs, best_match_result


def evaluate_scene_consistency(matcher, query_paths, history_paths,
                               ocr_engine=None, bge_tokenizer=None, bge_model=None):
    evaluation_logs = []
    best_match_result = None
    best_match_score = -1

    for q_path in query_paths:
        for h_path in history_paths:
            if os.path.abspath(q_path) == os.path.abspath(h_path):
                continue

            try:
                scene_type1, doc_score1, detail1 = 'complex', 0.0, {}
                scene_type2, doc_score2, detail2 = 'complex', 0.0, {}
                if ocr_engine is not None:
                    scene_type1, doc_score1, detail1, _ = classify_scene(q_path, ocr_engine)
                    scene_type2, doc_score2, detail2, _ = classify_scene(h_path, ocr_engine)

                if scene_type1 == 'text' and scene_type2 == 'text' and bge_tokenizer is not None:
                    result = compare_text_scene(
                        q_path, h_path, ocr_engine, bge_tokenizer, bge_model
                    )
                    log_entry = {
                        'query_image': os.path.basename(q_path),
                        'history_image': os.path.basename(h_path),
                        'query_path': q_path,
                        'history_path': h_path,
                        'scene_type': 'text',
                        'doc_score1': doc_score1,
                        'doc_score2': doc_score2,
                        'similarity_score': result['similarity_score'],
                        'match_count': result.get('match_count', 0),
                        'raw_match_count': 0,
                        'inlier_ratio': result.get('inlier_ratio', 0.0),
                        'avg_confidence': result.get('avg_confidence', 0.0),
                        'text_similarity': result.get('text_similarity'),
                        'ssim_score': result.get('ssim_score'),
                        'seal_bonus': result.get('seal_bonus'),
                        'is_same_scene': result['is_same_scene'],
                        'gamma_info': [],
                    }
                else:
                    result = matcher.compare(q_path, h_path)
                    log_entry = {
                        'query_image': os.path.basename(q_path),
                        'history_image': os.path.basename(h_path),
                        'query_path': q_path,
                        'history_path': h_path,
                        'scene_type': 'complex',
                        'doc_score1': doc_score1,
                        'doc_score2': doc_score2,
                        'similarity_score': result['similarity_score'],
                        'match_count': result['match_count'],
                        'raw_match_count': result['raw_match_count'],
                        'inlier_ratio': result['inlier_ratio'],
                        'avg_confidence': result['avg_confidence'],
                        'mast3r_is_same': result.get('mast3r_is_same'),
                        'dinov2_is_same': result.get('dinov2_is_same'),
                        'dinov2_similarity': result.get('dinov2_similarity'),
                        'is_same_scene': result['is_same_scene'],
                        'gamma_info': result.get('gamma_info', []),
                        'patch_match_info': result.get('patch_match_info'),
                    }
            except Exception as e:
                log_entry = {
                    'query_image': os.path.basename(q_path),
                    'history_image': os.path.basename(h_path),
                    'query_path': q_path,
                    'history_path': h_path,
                    'scene_type': 'unknown',
                    'doc_score1': 0.0, 'doc_score2': 0.0,
                    'similarity_score': 0.0,
                    'match_count': 0, 'raw_match_count': 0,
                    'inlier_ratio': 0.0, 'avg_confidence': 0.0,
                    'is_same_scene': False, 'gamma_info': [],
                    'error': str(e),
                    'traceback': traceback.format_exc(),
                }
            evaluation_logs.append(log_entry)

            if log_entry['is_same_scene']:
                if log_entry['similarity_score'] > best_match_score:
                    best_match_score = log_entry['similarity_score']
                    best_match_result = log_entry.copy()
                print(f"  [早停] {os.path.basename(q_path)}{log_entry['history_image']} 匹配为同场景, 跳过剩余比对")
                break
            elif log_entry['similarity_score'] > best_match_score:
                best_match_score = log_entry['similarity_score']
                best_match_result = log_entry.copy()

    any_same = any(log['is_same_scene'] for log in evaluation_logs)
    final_result = not any_same

    return final_result, evaluation_logs, best_match_result


def update_metadata(user_dir, saved_info, per_image_results):
    metadata = load_metadata(user_dir)
    for original_name, save_path in saved_info:
        img_result = per_image_results.get(save_path, '未知')
        img_meta = {
            'filename': os.path.basename(save_path),
            'path': os.path.abspath(save_path),
            'upload_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'original_name': original_name,
            'evaluation_result': img_result,
        }
        metadata['images'].append(img_meta)
    save_metadata(user_dir, metadata)