File size: 19,571 Bytes
90ca518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430

import os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from utils.distance import calculate_distances_numpy, calculate_normal_consistency_score
import os
import numpy as np
from tqdm import tqdm
import trimesh
import json
import glob
import multiprocessing # 导入多进程模块
MIN_FACE_AMOUNT = 0
MAX_FACE_AMOUNT = 100000
# ===============================================================
# 第一步:你的距离计算函数 (无变化)
# ===============================================================
pc_num = 1024

def normalize(mesh, file_path):
    # ... 现有代码 ...
    bounds = np.array([mesh.vertices.min(axis=0), mesh.vertices.max(axis=0)])
    mesh.vertices = mesh.vertices - (bounds[0] + bounds[1])[None, :] / 2
    mesh.vertices = mesh.vertices / (bounds[1] - bounds[0]).max()
    mesh.vertices = mesh.vertices.clip(-0.5, 0.5) # 限制顶点坐标在[-0.5, 0.5] 

# ===============================================================
# 第二步:读取 .obj 文件的函数 (无变化)
# ===============================================================
def sample_from_obj(file_path, alter=False):
    # ... 现有代码 ...
    """
    从 .obj 文件中加载顶点坐标。
    """
    mesh = trimesh.load(file_path, force="mesh")
    if alter:
        mesh.vertices = mesh.vertices[:, [1,2,0]]
    normalize(mesh, file_path)
    points, face_idx = mesh.sample(pc_num, return_index=True)
    normals = mesh.face_normals[face_idx]
    return points,normals

# ===============================================================
# [修改] 法向量一致性计算
# ===============================================================

# ===============================================================
# 第三步:[修改] 多进程工作函数
# ===============================================================

def process_common_file_worker(base_name, gt_folder, folder_paths):
    """
    [已修改] 这是一个工作进程执行的任务:
    处理单个 base_name 对应的所有文件夹。
    它会加载一次GT文件,然后遍历所有预测文件夹进行计算。
    返回 L1, L2 CD 距离, |NC| 和 NC。
    """
    
    # 1. 查找并加载 GT 文件
    gt_filename = base_name + '.glb'
    # ... 现有代码 ...
    gt_filepath = os.path.join(gt_folder, gt_filename)

    if not os.path.exists(gt_filepath):
        gt_filename = base_name + '.obj'
    # ... 现有代码 ...
        gt_filepath = os.path.join(gt_folder, gt_filename)
        if not os.path.exists(gt_filepath):
            # 警告:在工作进程中打印 (print) 不是线程安全的,
    # ... 现有代码 ...
            # 但返回空列表是安全的,主进程会处理它。
            # print(f"\n警告:共同文件 '{base_name}' 对应的GT文件 '{gt_filepath}' 不存在,已跳过。")
            return [] # 返回空列表表示此文件处理失败
    mesh = trimesh.load(gt_filepath, force="mesh")
    if len(mesh.faces) < MIN_FACE_AMOUNT or len(mesh.faces) > MAX_FACE_AMOUNT:
        return []
        
    try:
        pcd_gt, normal_gt = sample_from_obj(gt_filepath)
        if pcd_gt is None or pcd_gt.size == 0:
    # ... 现有代码 ...
            # print(f"\n警告:加载GT文件 {gt_filename} 失败或点云为空。已跳过。")
            return []
    except Exception as e:
    # ... 现有代码 ...
        # print(f"\n加载GT文件 {gt_filename} 时发生错误: {e}。已跳过。")
        return []

    # 2. 遍历所有文件夹,加载 Pred 文件并计算
    results_for_this_file = []

    for folder_path in folder_paths:
        pred_filename = base_name + '_y.obj'
    # ... 现有代码 ...
        pred_filepath = os.path.join(folder_path, pred_filename)
        
        try:
            pcd_pred, normal_pred = sample_from_obj(pred_filepath)
            if pcd_pred is None or pcd_pred.size == 0:
    # ... 现有代码 ...
                # print(f"\n警告:加载文件 {pred_filepath} 失败或点云为空。已跳过。")
                results_for_this_file.append((folder_path, None, None, None, None, None)) # [修改] 6 个 None
                continue
            
            # [修改] 接收 L1 和 L2 CD
            chamfer_l2, hausdorff, chamfer_l1 = calculate_distances_numpy(pcd_gt, pcd_pred)
            # [修改] 接收两个 NC 分数
            normal_error_abs, normal_error_real = calculate_normal_consistency_score(pcd_gt, normal_gt, pcd_pred, normal_pred)
            
            # [修改] 存储有效结果 (6个值)
            results_for_this_file.append((folder_path, chamfer_l2, hausdorff, normal_error_abs, chamfer_l1, normal_error_real))
            
        except Exception as e:
    # ... 现有代码 ...
            # print(f"\n计算文件对 {pred_filepath} 时发生错误: {e}。已跳过。")
            results_for_this_file.append((folder_path, None, None, None, None, None)) # [修改] 6 个 None
    
    # 3. 返回此 base_name 对应的所有结果
    return results_for_this_file

def starmap_helper(args):
    # ... 现有代码 ...
    """
    [新增] 辅助函数,用于解包 pool.imap 的参数,使其能接受多个参数。
    """
    return process_common_file_worker(*args)

# ===============================================================
# 第四步:[修改] 评估函数,使用多进程
# ===============================================================

def evaluate_common_files(folder_paths, gt_folder, base_dir):
    """
    [已修改] 评估多个文件夹中 *共有* 的文件,并计算总平均指标和逐文件夹的平均指标。
    现在使用 multiprocessing.Pool 来并行处理文件。
    """
    if not folder_paths:
    # ... 现有代码 ...
        print("错误:没有提供用于评估共同文件的文件夹。")
        return None

    print("\n" + "="*80)
    # ... 现有代码 ...
    print(" 正在计算共同文件指标 ".center(80, "="))
    print(f"总共 {len(folder_paths)} 个文件夹参与比较。")

    # 1. 查找每个文件夹中的文件基础名(不含 '_y.obj')(无变化)
    files_by_folder = {}
    # ... 现有代码 ...
    for folder_path in folder_paths:
        try:
            all_files = os.listdir(folder_path)
    # ... 现有代码 ...
            base_names = set([f.replace('_y.obj', '') for f in all_files
                if (f.endswith('_y.obj') and not f.endswith('_gt_y.obj') and not f.endswith("input_y.obj"))]
                )
            files_by_folder[folder_path] = base_names
    # ... 现有代码 ...
            print(f" ‐ 文件夹 '{os.path.basename(folder_path)}' 找到 {len(base_names)} 个文件。")
        except Exception as e:
            print(f" ‐ 错误:无法读取文件夹 {folder_path}: {e}")
    # ... 现有代码 ...
            files_by_folder[folder_path] = set()

    # 2. 找到所有文件夹中 *共有* 的文件基础名 (无变化)
    common_base_names = set.intersection(*files_by_folder.values())
    
    if not common_base_names:
    # ... 现有代码 ...
        print("\n错误:在所有文件夹中没有找到任何共有的 `_y.obj` 文件。")
        print("="*80)
        return None

    print(f"\n在所有文件夹中找到了 {len(common_base_names)} 个共同文件。将使用多进程处理...")

    # 3. [修改] 初始化指标存储
    global_chamfer_l2 = []
    global_hausdorff = []
    global_normal_abs = [] # [修改]
    global_chamfer_l1 = []
    global_normal_real = [] # [新增]
    
    per_folder_metrics = {path: {'chamfer_l2': [], 'hausdorff': [], 'normal_abs': [], 'chamfer_l1': [], 'normal_real': []} for path in folder_paths} # [修改]

    # 4. [修改] 设置并运行多进程任务
    
    # 准备任务参数列表
    # ... 现有代码 ...
    task_args = [(base_name, gt_folder, folder_paths) for base_name in common_base_names]
    all_results_list = []
    
    # 确定进程数,可以适当调整,例如 os.cpu_count()
    # ... 现有代码 ...
    # 留出2个核心给系统,防止卡死
    num_processes = max(1, 60) 
    print(f"启动 {num_processes} 个工作进程...")

    # 创建进程池
    # ... 现有代码 ...
    with multiprocessing.Pool(processes=num_processes) as pool:
        # 使用 pool.imap 来迭代任务,并用 tqdm 显示进度条
        # imap 会按顺序返回结果,同时保持并行执行
    # ... 现有代码 ...
        for result in tqdm(pool.imap(starmap_helper, task_args), 
                           total=len(common_base_names), 
                           desc="评估共同文件"):
            all_results_list.append(result)

    # 5. [修改] 聚合所有进程返回的结果
    print("\n正在聚合所有进程的结果...")
    valid_pairs_processed = 0
    for results_for_this_file in all_results_list:
        if not results_for_this_file:
    # ... 现有代码 ...
            # GT 文件加载失败或跳过
            continue
            
        # [修改] 解包 L1, L2, |NC|, NC
        for (folder_path, chamfer_l2, hausdorff, normal_error_abs, chamfer_l1, normal_error_real) in results_for_this_file:
            if chamfer_l2 is not None and hausdorff is not None and normal_error_abs is not None and chamfer_l1 is not None and normal_error_real is not None:
                # 这是一个有效的结果
                global_chamfer_l2.append(chamfer_l2)
                global_hausdorff.append(hausdorff)
                global_normal_abs.append(normal_error_abs) # [修改]
                global_chamfer_l1.append(chamfer_l1)
                global_normal_real.append(normal_error_real) # [新增]
                
                per_folder_metrics[folder_path]['chamfer_l2'].append(chamfer_l2)
                per_folder_metrics[folder_path]['hausdorff'].append(hausdorff)
                per_folder_metrics[folder_path]['normal_abs'].append(normal_error_abs) # [修改]
                per_folder_metrics[folder_path]['chamfer_l1'].append(chamfer_l1)
                per_folder_metrics[folder_path]['normal_real'].append(normal_error_real) # [新增]
                valid_pairs_processed += 1
            else:
    # ... 现有代码 ...
                # 预测文件加载失败或计算错误
                # 可以在这里添加日志
                pass
    
    print(f"聚合完成。总共处理了 {valid_pairs_processed} 个有效的 (GT, Pred) 文件对。")

    # 6. [修改] 计算最终平均值
    if not global_chamfer_l2: # [修改]
        print("\n错误:未能成功计算任何共同文件的指标。")
        print("="*80)
        return None

    # [修改] 计算总平均
    global_avg_results = {
        "evaluated_pairs_total": len(global_chamfer_l2),
        "common_files_count": len(common_base_names),
        "folders_count": len(folder_paths),
        "avg_chamfer_distance_l2": np.mean(global_chamfer_l2),
        "avg_hausdorff_distance": np.mean(global_hausdorff),
        "avg_normal_consistency_abs": np.mean(global_normal_abs), # [修改]
        "avg_chamfer_distance_l1": np.mean(global_chamfer_l1),
        "avg_normal_consistency_real": np.mean(global_normal_real) # [新增]
    }

    # [修改] 计算逐文件夹平均
    per_folder_avg_results = {}
    for folder_path, metrics in per_folder_metrics.items():
        folder_name = os.path.basename(folder_path)
        if metrics['chamfer_l2']: # [修改]
            per_folder_avg_results[folder_name] = {
                "evaluated_pairs": len(metrics['chamfer_l2']),
                "avg_chamfer_distance_l2": np.mean(metrics['chamfer_l2']),
                "avg_hausdorff_distance": np.mean(metrics['hausdorff']),
                "avg_normal_consistency_abs": np.mean(metrics['normal_abs']), # [修改]
                "avg_chamfer_distance_l1": np.mean(metrics['chamfer_l1']),
                "avg_normal_consistency_real": np.mean(metrics['normal_real']) # [新增]
            }
        else:
            per_folder_avg_results[folder_name] = {
                "evaluated_pairs": 0,
                "avg_chamfer_distance_l2": 0,
                "avg_hausdorff_distance": 0,
                "avg_normal_consistency_abs": 0, # [修改]
                "avg_chamfer_distance_l1": 0,
                "avg_normal_consistency_real": 0 # [新增]
            }
            
    # 7. [修改] 打印共同文件评估总结
    print("\n--- 共同文件评估总结 ---")
    print(f"\n全局平均 (基于 {len(global_chamfer_l2)} 个总比较对, {len(common_base_names)} 个文件 x {len(folder_paths)} 个文件夹):")
    print(f" ‐ 平均倒角距离 (L2): {global_avg_results['avg_chamfer_distance_l2']:.6f}")
    print(f" ‐ 平均倒角距离 (L1): {global_avg_results['avg_chamfer_distance_l1']:.6f}")
    print(f" ‐ 平均豪斯多夫距离: {global_avg_results['avg_hausdorff_distance']:.6f}")
    print(f" ‐ 平均法向量 |NC| (Abs): {global_avg_results['avg_normal_consistency_abs']:.6f}") # [修改]
    print(f" ‐ 平均法向量 NC (Real): {global_avg_results['avg_normal_consistency_real']:.6f}") # [新增]
    
    print("\n逐文件夹平均 (仅基于共同文件):")
    for folder_name, results in per_folder_avg_results.items():
        print(f" ‐ 文件夹: {folder_name}")
        # [修改] 打印所有6个指标
        print(f" ‐ 倒角(L2): {results['avg_chamfer_distance_l2']:.6f}, 倒角(L1): {results['avg_chamfer_distance_l1']:.6f}, 豪斯多夫: {results['avg_hausdorff_distance']:.6f}, 法向量|NC|: {results['avg_normal_consistency_abs']:.6f}, 法向量NC: {results['avg_normal_consistency_real']:.6f} (共 {results['evaluated_pairs']} 对)")
    
    print("="*80)
    
    # 8. 保存结果 (无变化)
    final_results = {
    # ... 现有代码 ...
        "global_average_on_common_files": global_avg_results,
        "per_folder_average_on_common_files": per_folder_avg_results
    }
    
    output_filepath = os.path.join(base_dir, "common_evaluation_results.json")
    # ... 现有代码 ...
    try:
        with open(output_filepath, 'w', encoding='utf-8') as f:
            json.dump(final_results, f, ensure_ascii=False, indent=4)
    # ... 现有代码 ...
        print(f"\n共同文件评估结果已保存到: {output_filepath}")
    except Exception as e:
        print(f"\n错误:保存共同文件结果时失败: {e}")
    # ... 现有代码 ...
        
    return final_results


# --- [主函数:run_evaluation_on_common_files_only] (打印部分已修改) ---
def run_evaluation_on_common_files_only(target_folders, gt_folder, base_dir=None):
    """
    [无变化]
    在手动指定的多个文件夹上 *仅* 运行 *共同文件* 评估。
    """
    
    # 1. 验证输入的文件夹列表
    # ... 现有代码 ...
    if not isinstance(target_folders, list) or not target_folders:
        print("错误:未提供任何文件夹用于评估。请检查 'target_folders' 列表。")
        return

    # 2. 检查路径有效性并过滤非文件夹
    # ... 现有代码 ...
    valid_folders = []
    for f_path in target_folders:
        if os.path.isdir(f_path):
    # ... 现有代码 ...
            valid_folders.append(f_path)
        else:
            print(f"警告:提供的路径 '{f_path}' 不是一个有效的文件夹,已跳过。")
    
    if not valid_folders:
    # ... 现有代码 ...
        print("错误:提供的列表中没有找到任何有效的文件夹。")
        return

    # 确保排序一致
    # ... 现有代码 ...
    valid_folders.sort()
    
    # 3. 确定用于保存汇总文件的 base_dir
    if base_dir is None:
    # ... 现有代码 ...
        # 如果未指定,使用第一个有效文件夹的父目录
        base_dir = os.path.dirname(valid_folders[0])
        print(f"信息:未指定 base_dir,将使用 '{base_dir}' 存放共同文件评估结果。")
    elif not os.path.isdir(base_dir):
    # ... 现有代码 ...
        print(f"警告:指定的 base_dir '{base_dir}' 不存在。将改用第一个文件夹的父目录。")
        base_dir = os.path.dirname(valid_folders[0])
    
    print(f"\n找到了 {len(valid_folders)} 个有效的文件夹,将评估它们的共同文件:")
    # ... 现有代码 ...
    for folder in valid_folders:
        print(f" - {folder}") # 打印完整路径

    # 4. 第1部分:直接进行共同文件评估
    # ... 现有代码 ...
    print("\n" + "#"*80)
    print(" 开始共同文件评估 (仅计算所有文件夹共有的文件) ".center(80, "#"))
    print("#"*80)
    
    # [调用修改后的函数]
    # ... 现有代码 ...
    common_results = evaluate_common_files(valid_folders, gt_folder, base_dir)

    # 5. [修改] 第2部分:打印最终的汇总报告
    print("\n" + "#"*80)
    print(" 最终汇总报告 ".center(80, "#"))
    print("#"*80)

    print("\n--- 共同文件评估结果 (仅基于共同文件) ---")
    if common_results:
        # 打印全局平均
        global_avg = common_results.get("global_average_on_common_files", {})
        if global_avg:
            print(f"\n [总平均 (基于 {global_avg.get('common_files_count', 0)} 个共同文件)]")
            print(f" ‐ 倒角(L2): {global_avg.get('avg_chamfer_distance_l2', 0):.6f}, "
                  f"倒角(L1): {global_avg.get('avg_chamfer_distance_l1', 0):.6f}, "
                  f"豪斯多夫: {global_avg.get('avg_hausdorff_distance', 0):.6f}, "
                  f"法向量|NC|: {global_avg.get('avg_normal_consistency_abs', 0):.6f}, " # [修改]
                  f"法向量NC: {global_avg.get('avg_normal_consistency_real', 0):.6f}") # [新增]
        
        # 打印逐文件夹平均
        per_folder_avg = common_results.get("per_folder_average_on_common_files", {})
        if per_folder_avg:
            print("\n [逐文件夹平均 (仅共同文件)]")
            for folder_name, results in per_folder_avg.items():
                print(f" ‐ 文件夹: {folder_name}")
                print(f" ‐ 倒角(L2): {results.get('avg_chamfer_distance_l2', 0):.6f}, "
                      f"倒角(L1): {results.get('avg_chamfer_distance_l1', 0):.6f}, "
                      f"豪斯多夫: {results.get('avg_hausdorff_distance', 0):.6f}, "
                      f"法向量|NC|: {results.get('avg_normal_consistency_abs', 0):.6f}, " # [修改]
                      f"法向量NC: {results.get('avg_normal_consistency_real', 0):.6f} " # [新增]
                      f"(共 {results.get('evaluated_pairs', 0)} 对)")
    else:
        print(" (没有共同文件的评估结果)")
        
    print("\n" + "="*80)
    print("所有评估任务已完成。")
    print("="*80)


# ===============================================================
# 第五步:运行主程序 (无变化)
# ===============================================================
        
if __name__ == "__main__":
    import argparse
    ap = argparse.ArgumentParser(description="评测: 预测目录(含 <case>_y.obj) vs GT 目录(<case>.glb/.obj)")
    ap.add_argument("folders", nargs="+", help="一个或多个预测目录(只评估各目录共有的 case)")
    ap.add_argument("--gt", required=True, help="Ground Truth 目录")
    ap.add_argument("--out", default=None, help="汇总 JSON 的输出目录(默认第一个预测目录)")
    a = ap.parse_args()
    if not os.path.isdir(a.gt):
        raise SystemExit(f"GT 目录不存在: {a.gt}")
    run_evaluation_on_common_files_only(a.folders, a.gt, base_dir=a.out)