File size: 8,420 Bytes
efaa3d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
NYU Depth V2 Eigen test split (654 张) 相对深度精度评估。

输入:
    --mat     nyu_depth_v2_labeled.mat(GT 直接从这里读,不落盘)
    --split   export_nyu654_test.py 生成的 split.json
    --pred    一个或多个 <名称>=<NPZ 目录>,NPZ 由 infer 脚本 --dump-npz 生成

评估协议:
    1. 有效像素:rawDepths > 0 且 depths 在 (0, max_depth];
    2. 预测上采样到 GT 分辨率(bilinear);
    3. 相对深度对齐:
       - disparity(默认,MiDaS 协议):拟合 a*pred + b ≈ 1/gt,再取倒数得米制深度;
       - depth:拟合 a*pred + b ≈ gt;
       最小二乘在有效像素上求解;
    4. 逐图计算 AbsRel / SqRel / RMSE / RMSE_log / δ<1.25 / 1.25² / 1.25³,
       对 654 张取平均;
    5. 第一个 --pred 作为基准,其余输出相对掉点 (%)。

注意:未做 Eigen crop,绝对数值不能直接与论文表格对比;同一批图上不同后端的
相对掉点不受影响。

用法:
    ../../venv/bin/python src/tool/evaluate_nyu654_depth.py \
        --mat ../../datasets/nyu_depth_v2_labeled.mat \
        --split datasets/nyu654_test/split.json \
        --pred fp32=results/nyu654_fp32/npz axonnx=results/nyu654_axonnx/npz \
        --out results/nyu654_eval
"""

import argparse
import json
from pathlib import Path

import cv2
import h5py
import numpy as np
from tqdm import tqdm

METRICS = ('AbsRel', 'SqRel', 'RMSE', 'RMSE_log', 'd1', 'd2', 'd3')
SIGMA = (1.25, 1.25 ** 2, 1.25 ** 3)


def solve_scale_shift(pred, target, mask):
    """在 mask 上最小二乘求解 a, b 使 a*pred + b ≈ target。"""
    p = pred[mask]
    t = target[mask]
    A = np.stack([p, np.ones_like(p)], axis=1)
    (a, b), *_ = np.linalg.lstsq(A, t, rcond=None)
    return float(a), float(b)


def compute_metrics(pred_depth, gt_depth, mask, align, max_depth):
    clipped = 0.0
    if align == 'disparity':
        gt_target = 1.0 / gt_depth
        a, b = solve_scale_shift(pred_depth, gt_target, mask)
        if a <= 0:
            raise ValueError(f'disparity 对齐斜率为负 (a={a:.4f}),'
                             f'预测不是"越大越近"的视差量')
        aligned = a * pred_depth + b
        # 极少数图(低纹理/异常预测)会拟出负视差,裁剪到最大深度的下限
        neg = aligned[mask] <= 0
        clipped = float(neg.mean())
        aligned = np.maximum(aligned, 1.0 / max_depth)
        pred = 1.0 / aligned
    else:
        a, b = solve_scale_shift(pred_depth, gt_depth, mask)
        pred = a * pred_depth + b

    pred = pred[mask]
    gt = gt_depth[mask]
    if np.any(pred <= 0):
        raise ValueError('对齐后预测深度出现非正值')

    diff = pred - gt
    out = {
        'AbsRel': float(np.mean(np.abs(diff) / gt)),
        'SqRel': float(np.mean(diff ** 2 / gt)),
        'RMSE': float(np.sqrt(np.mean(diff ** 2))),
        'RMSE_log': float(np.sqrt(np.mean((np.log(pred) - np.log(gt)) ** 2))),
    }
    ratio = np.maximum(pred / gt, gt / pred)
    for name, s in zip(('d1', 'd2', 'd3'), SIGMA):
        out[name] = float(np.mean(ratio < s))
    return out, a, b, clipped


def load_predictions(path):
    """读取预测:汇总 NPZ(names/depths)、分块汇总目录,或旧的逐图 NPZ 目录。"""
    path = Path(path)
    if path.is_file():
        files = [path]
    elif path.is_dir():
        files = sorted(path.glob('*.npz'))
        if not files:
            raise ValueError(f'目录中没有 NPZ: {path}')
    else:
        raise FileNotFoundError(f'预测路径不存在: {path}')

    preds = {}
    for fp in files:
        with np.load(fp, allow_pickle=False) as data:
            if 'depths' in data and 'names' in data:
                for name, depth in zip(data['names'], data['depths']):
                    preds[str(name)] = depth
            elif 'depth' in data:
                preds[fp.name[:-len('.npz')]] = data['depth']
            else:
                raise ValueError(f'无法识别的 NPZ 格式: {fp}')
    return preds


def main():
    parser = argparse.ArgumentParser(description='NYU 654 相对深度评测')
    parser.add_argument('--mat', required=True)
    parser.add_argument('--split', required=True, help='split.json')
    parser.add_argument('--pred', nargs='+', required=True, help='名称=NPZ目录,第一个为基准')
    parser.add_argument('--out', required=True, help='结果目录')
    parser.add_argument('--align', choices=['disparity', 'depth'], default='disparity')
    parser.add_argument('--max-depth', type=float, default=10.0)
    args = parser.parse_args()

    pred_sets = []
    for item in args.pred:
        name, _, path = item.partition('=')
        if not path:
            raise ValueError(f'--pred 需要 名称=路径 格式: {item}')
        pred_sets.append((name, load_predictions(path)))

    split = json.load(open(args.split))
    out_dir = Path(args.out)
    out_dir.mkdir(parents=True, exist_ok=True)

    per_image = {name: [] for name, _ in pred_sets}
    failed = {name: [] for name, _ in pred_sets}
    with h5py.File(args.mat, 'r') as f:
        for record in tqdm(split, desc=f'eval({args.align})', ncols=80):
            i = record['mat_index']
            gt = np.asarray(f['depths'][i], dtype=np.float32)
            raw = np.asarray(f['rawDepths'][i], dtype=np.float32)
            mask = (raw > 0) & (gt > 0) & (gt <= args.max_depth)

            for name, preds in pred_sets:
                if record['file'] not in preds:
                    raise KeyError(f'{name} 缺少预测: {record["file"]}')
                pred = np.asarray(preds[record['file']], dtype=np.float32)
                if pred.ndim == 3:
                    pred = pred[0]
                pred = cv2.resize(pred, (gt.shape[1], gt.shape[0]), interpolation=cv2.INTER_LINEAR)
                try:
                    m, a, b, clipped = compute_metrics(pred, gt, mask, args.align, args.max_depth)
                except ValueError as error:
                    failed[name].append({'file': record['file'], 'reason': str(error)})
                    continue
                m.update({'file': record['file'], 'scale': a, 'shift': b,
                          'clipped_frac': clipped, 'valid_pixels': int(mask.sum())})
                per_image[name].append(m)

    results = {}
    for name, _ in pred_sets:
        rows = per_image[name]
        results[name] = {k: float(np.mean([r[k] for r in rows])) for k in METRICS}
        results[name]['p90_absrel'] = float(np.percentile([r['AbsRel'] for r in rows], 90))
        results[name]['clipped_images'] = int(sum(r['clipped_frac'] > 0 for r in rows))
        results[name]['clipped_pixel_frac'] = float(np.mean([r['clipped_frac'] for r in rows]))
        results[name]['evaluated_images'] = len(rows)
        results[name]['failed_images'] = len(failed[name])

    base = pred_sets[0][0]
    drops = {}
    for name, _ in pred_sets[1:]:
        drops[name] = {}
        for k in METRICS:
            b, v = results[base][k], results[name][k]
            drops[name][k] = float((v - b) / b * 100)
        b, v = results[base]['p90_absrel'], results[name]['p90_absrel']
        drops[name]['p90_absrel'] = float((v - b) / b * 100)

    summary = {
        'dataset': 'nyu_depth_v2_labeled / eigen test split',
        'num_images': len(split),
        'align': args.align,
        'max_depth': args.max_depth,
        'eigen_crop': False,
        'results': results,
        'baseline': base,
        'drop_percent': drops,
        'failed': failed,
        'per_image': {name: per_image[name] for name, _ in pred_sets},
    }
    with open(out_dir / 'metrics.json', 'w') as fp:
        json.dump(summary, fp, indent=2)

    print(f'\n{"指标":<10}' + ''.join(f'{n:>14}' for n, _ in pred_sets))
    for k in METRICS + ('p90_absrel',):
        print(f'{k:<10}' + ''.join(f'{results[n][k]:>14.4f}' for n, _ in pred_sets))
    for name, d in drops.items():
        print(f'\n{name} 相对 {base} 掉点 (%): ' +
              ', '.join(f'{k}={v:+.2f}' for k, v in d.items()))
    for name, _ in pred_sets:
        if failed[name]:
            print(f'\n{name}: {len(failed[name])} 张对齐失败(方向反了或输出异常),未计入均值')
    print(f'\n结果已保存: {out_dir / "metrics.json"}')


if __name__ == '__main__':
    main()