File size: 7,920 Bytes
41ff7b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128b81a
 
 
 
 
41ff7b3
 
 
128b81a
 
 
 
 
 
 
 
 
41ff7b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128b81a
41ff7b3
128b81a
41ff7b3
 
 
 
 
128b81a
 
41ff7b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""滤光片外观质检 · 推理脚本(单文件,只依赖 onnxruntime + numpy + pillow)

用法
-----
单张图:
    python predict.py -i /path/to/filter_0001.png

整个目录(递归),输出 xlsx:
    python predict.py -i /path/to/images/ -o result.xlsx

同时输出缺陷类型:
    python predict.py -i /path/to/images/ -o result.xlsx --with-type

自定义判废阈值(默认用标定好的零误报阈值):
    python predict.py -i imgs/ -o r.xlsx --threshold 0.5

输出 CSV 而不是 xlsx(不需要 openpyxl):
    python predict.py -i imgs/ -o result.csv
"""
import os, sys, json, argparse, time, csv

try:
    import numpy as np
except ImportError:
    sys.exit('缺少 numpy:pip install numpy')
try:
    import onnxruntime as ort
except ImportError:
    sys.exit('缺少 onnxruntime:pip install onnxruntime')
try:
    from PIL import Image
except ImportError:
    sys.exit('缺少 Pillow:pip install pillow')

if getattr(sys, 'frozen', False):
    HERE = os.path.dirname(sys.executable)          # PyInstaller: exe 所在目录
    BUNDLED = getattr(sys, '_MEIPASS', HERE)        # 打进二进制里的资源
else:
    HERE = BUNDLED = os.path.dirname(os.path.abspath(__file__))
IMG_EXT = {'.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff'}


def find_model(name):
    """先找 exe 旁边(方便换模型),再找打进包里的。"""
    for d in (HERE, BUNDLED):
        p = os.path.join(d, name)
        if os.path.exists(p):
            return p
    return os.path.join(BUNDLED, name)


# ---------------------------------------------------------------- 预处理
def preprocess(path, size):
    """等比缩放到 size 内 + 居中补零 + 逐图灰世界白平衡 + 逐图 z-score。
    必须与训练时逐字节一致,否则结果不可信。"""
    im = Image.open(path).convert('RGB')
    w, h = im.size
    s = size / max(w, h)
    nw, nh = max(1, round(w * s)), max(1, round(h * s))
    im = im.resize((nw, nh), Image.BILINEAR)
    canvas = np.zeros((size, size, 3), np.uint8)
    y0, x0 = (size - nh) // 2, (size - nw) // 2
    canvas[y0:y0 + nh, x0:x0 + nw] = np.asarray(im)
    f = canvas.astype(np.float32)
    m = f.reshape(-1, 3).mean(0)                       # 灰世界
    f = f * (m.mean() / np.maximum(m, 1e-6))
    f = (f - f.mean()) / max(f.std(), 1e-6)            # z-score
    return np.transpose(f, (2, 0, 1))                  # HWC -> CHW


def softmax(x, axis=-1):
    e = np.exp(x - x.max(axis=axis, keepdims=True))
    return e / e.sum(axis=axis, keepdims=True)


# ---------------------------------------------------------------- 模型
class Model:
    def __init__(self, onnx_path):
        meta_p = onnx_path.replace('.onnx', '.json')
        self.meta = json.load(open(meta_p, encoding='utf-8')) if os.path.exists(meta_p) else {}
        self.size = int(self.meta.get('size', 320))
        self.classes = self.meta.get('classes', ['良品', '不良'])
        so = ort.SessionOptions()
        so.intra_op_num_threads = min(8, os.cpu_count() or 4)
        so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
        prov = ['CUDAExecutionProvider', 'CPUExecutionProvider'] \
            if 'CUDAExecutionProvider' in ort.get_available_providers() else ['CPUExecutionProvider']
        self.sess = ort.InferenceSession(onnx_path, so, providers=prov)
        self.provider = self.sess.get_providers()[0]
        self.iname = self.sess.get_inputs()[0].name

    def run(self, paths, bs=16):
        out = []
        for i in range(0, len(paths), bs):
            chunk = paths[i:i + bs]
            x = np.stack([preprocess(p, self.size) for p in chunk]).astype(np.float32)
            out.append(softmax(self.sess.run(None, {self.iname: x})[0], axis=1))
        return np.concatenate(out) if out else np.zeros((0, len(self.classes)))


def collect(inp):
    if os.path.isfile(inp):
        return [inp]
    fs = []
    for r, _, names in os.walk(inp):
        for n in sorted(names):
            if os.path.splitext(n)[1].lower() in IMG_EXT:
                fs.append(os.path.join(r, n))
    return sorted(fs)


def write_table(rows, header, out):
    ext = os.path.splitext(out)[1].lower()
    if ext in ('.xlsx', '.xls'):
        try:
            from openpyxl import Workbook
        except ImportError:
            out = os.path.splitext(out)[0] + '.csv'
            print('  未装 openpyxl,改写 CSV:' + out)
            ext = '.csv'
        else:
            wb = Workbook(); ws = wb.active; ws.title = '检测结果'
            ws.append(header)
            for r in rows:
                ws.append(r)
            for i, w in enumerate([38, 12, 12, 12, 12, 60], 1):
                if i <= len(header):
                    ws.column_dimensions[chr(64 + i)].width = w
            wb.save(out); return out
    with open(out, 'w', newline='', encoding='utf-8-sig') as f:
        w = csv.writer(f); w.writerow(header); w.writerows(rows)
    return out


def main():
    ap = argparse.ArgumentParser(description='滤光片外观质检推理')
    ap.add_argument('-i', '--input', required=True, help='图片文件或目录')
    ap.add_argument('-o', '--output', default=None, help='输出 xlsx/csv(目录模式下必填)')
    ap.add_argument('-m', '--model', default=None)
    ap.add_argument('--with-type', action='store_true', help='同时用四分类模型给出缺陷类型')
    ap.add_argument('--type-model', default=None)
    ap.add_argument('--threshold', type=float, default=None,
                    help='判废阈值(不良概率)。默认读 filter_binary.json 里标定好的值')
    ap.add_argument('--batch', type=int, default=16)
    a = ap.parse_args()

    if a.model is None: a.model = find_model('filter_binary.onnx')
    if a.type_model is None: a.type_model = find_model('filter_4class.onnx')
    files = collect(a.input)
    if not files:
        sys.exit(f'没找到图片:{a.input}')
    mdl = Model(a.model)
    thr = a.threshold if a.threshold is not None else float(mdl.meta.get('threshold', 0.5))
    print(f'模型 {os.path.basename(a.model)} | {mdl.provider} | 输入 {mdl.size}px | 判废阈值 {thr:.4f}')
    print(f'待检 {len(files)} 张')

    t0 = time.time()
    P = mdl.run(files, a.batch)
    score = P[:, 1:].sum(1)                       # 不良概率
    verdict = np.where(score >= thr, '不良', '良品')

    types = None
    if a.with_type and os.path.exists(a.type_model):
        tm = Model(a.type_model)
        T = tm.run(files, a.batch)
        names = tm.classes
        types = [names[i] for i in T.argmax(1)]
        tconf = T.max(1)
    dt = time.time() - t0
    print(f'完成 {len(files)} 张,用时 {dt:.1f}s({len(files)/max(dt,1e-9):.1f} 张/秒)')

    if len(files) == 1 and not a.output:
        print(f'\n  文件   : {os.path.basename(files[0])}')
        print(f'  判定   : {verdict[0]}')
        print(f'  不良概率: {score[0]:.4f}')
        if types:
            print(f'  缺陷类型: {types[0]}(置信度 {tconf[0]:.3f})')
        return

    out = a.output or 'result.xlsx'
    header = ['图片名', '判定', '不良概率']
    rows = []
    for i, p in enumerate(files):
        r = [os.path.basename(p), verdict[i], round(float(score[i]), 4)]
        if types:
            r += [types[i] if verdict[i] == '不良' else '', round(float(tconf[i]), 3) if verdict[i] == '不良' else '']
        r.append(os.path.relpath(p, a.input) if os.path.isdir(a.input) else p)
        rows.append(r)
    if types:
        header += ['缺陷类型', '类型置信度']
    header += ['相对路径']
    out = write_table(rows, header, out)
    n_bad = int((verdict == '不良').sum())
    print(f'\n  良品 {len(files)-n_bad} 张 | 不良 {n_bad} 张({n_bad/len(files):.1%})')
    print(f'  结果 -> {out}')


if __name__ == '__main__':
    main()