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()
|