File size: 4,250 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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从 NYU 训练集 795 张中按场景均匀抽取量化校准图片。

抽样规则:
    - 按场景类型(scene 去掉 _0001 后缀,如 kitchen、bedroom)分组;
    - 每个场景至少 1 张,其余名额按场景图片数比例分配(最大余数法);
    - 场景内按等距索引选取,结果确定可复现。

输出:
    <out-dir>/images/*.png        校准图片
    <out-dir>/selection.json      每张图的来源 mat_index / scene / 源文件
    <out-dir>/<name>.tar          给 pulsar2 的校准包,内部为 <name>/xxx.png

用法:
    ../../venv/bin/python src/tool/make_nyu_calibration.py \
        --source-dir datasets/nyu_train795 --count 48 \
        --out-dir datasets/nyu_calibration_48
"""

import argparse
import json
import shutil
import subprocess
from collections import defaultdict
from pathlib import Path

import cv2
import numpy as np


def scene_type(scene):
    return scene.rsplit('_', 1)[0]


def allocate(groups, count):
    """最大余数法分配名额,每个场景至少 1 张。"""
    total = sum(len(v) for v in groups.values())
    if count < len(groups):
        raise ValueError(f'抽取数量 {count} 少于场景数 {len(groups)},无法保证每场景 1 张')
    quota = {k: 1 for k in groups}
    left = count - len(groups)
    exact = {k: left * len(v) / total for k, v in groups.items()}
    for k, v in exact.items():
        quota[k] += int(np.floor(v))
    remain = count - sum(quota.values())
    order = sorted(exact, key=lambda k: exact[k] - np.floor(exact[k]), reverse=True)
    for k in order[:remain]:
        quota[k] += 1
    return quota


def main():
    parser = argparse.ArgumentParser(description='按场景均匀抽取 NYU 校准集')
    parser.add_argument('--source-dir', required=True, help='export_nyu_train795.py 的输出目录')
    parser.add_argument('--count', type=int, default=48, help='抽取张数,默认 48')
    parser.add_argument('--out-dir', required=True, help='校准集输出目录')
    parser.add_argument('--name', help='tar 包内目录名,默认取 out-dir 名')
    parser.add_argument('--ext', choices=('png', 'jpg'), default='png',
                        help='校准图片格式,默认 png;pulsar2 读取异常时可改 jpg')
    args = parser.parse_args()

    source = Path(args.source_dir)
    split = json.load(open(source / 'split.json'))
    groups = defaultdict(list)
    for record in split:
        groups[scene_type(record['scene'])].append(record)

    quota = allocate(groups, args.count)
    selected = []
    for scene, records in sorted(groups.items()):
        records = sorted(records, key=lambda r: r['file'])
        idx = np.linspace(0, len(records) - 1, quota[scene]).round().astype(int)
        selected.extend(records[i] for i in sorted(set(idx.tolist())))

    if len(selected) != args.count:
        raise RuntimeError(f'实际选出 {len(selected)} 张,与目标 {args.count} 不符')

    out_dir = Path(args.out_dir)
    image_dir = out_dir / 'images'
    image_dir.mkdir(parents=True, exist_ok=True)
    for record in selected:
        src = source / 'images' / record['file']
        if args.ext == 'png':
            dst = image_dir / record['file']
            shutil.copy2(src, dst)
        else:
            dst = image_dir / (Path(record['file']).stem + '.jpg')
            image = cv2.imread(str(src), cv2.IMREAD_COLOR)
            if image is None:
                raise RuntimeError(f'读取失败: {src}')
            cv2.imwrite(str(dst), image, [cv2.IMWRITE_JPEG_QUALITY, 95])

    with open(out_dir / 'selection.json', 'w') as fp:
        json.dump(selected, fp, indent=2)

    name = args.name or out_dir.name
    tar_path = out_dir / f'{name}.tar'
    subprocess.run(
        ['tar', '-cf', str(tar_path), '--transform', f's,^,{name}/,', '-C', str(image_dir)]
        + sorted(p.name for p in image_dir.iterdir()),
        check=True)

    print(f'已抽取 {len(selected)} 张,覆盖 {len(groups)} 个场景类型')
    for scene in sorted(groups):
        print(f'  {scene:<16} {quota[scene]:>3} 张 / 共 {len(groups[scene])}')
    print(f'校准包: {tar_path}')


if __name__ == '__main__':
    main()