Shaun Zhou
|
f9a4032783
|
feat: train fixed-size classifiers and export class probabilities
|
2026-10-14 17:08:14 +08:00 |
|
Shaun Zhou
|
63bab36147
|
fix: suppress normal pseudo boxes in classifier exports
|
2026-10-14 15:31:52 +08:00 |
|
Shaun Zhou
|
03f667276b
|
feat: export classifier with YOLO OpenVINO interface
|
2026-10-14 12:00:36 +08:00 |
|
xzhou
|
d42a89ed13
|
fix(scripts): update default TILE_SIZE to 128 in retrain_from_label_studio.sh
|
2026-10-13 07:57:56 +00:00 |
|
 Shaun ZhouandClaude Opus 5
|
6a8941c18a
|
docs: 修正 clusters/ 是软链的说法
默认 link_mode 是 hard(scripts/cluster_unlabeled.py 与 configs/cluster/film.yaml
都是),README 里却有三处说成软链,与同一节上文"放的是硬链接"自相矛盾。
- 产出清单与导出小节改为硬链接。
- "导出的是真实文件(不是软链)"在硬链接下不成立——条目本身就是原图数据,
复制的意义是脱离 runs/ 路径、去掉排序前缀、避免删改牵连原图。
- 跨机器那条注记原本只讲失效软链,补成 SourceResolver 实际处理的三种条目
(硬链接走 inode 反查 assignments.csv / 有效软链 / 失效软链重定位)。
- scripts/export_clusters.py 的 docstring 是同一处错误,一并修正。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-10-09 16:04:01 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
29ff7a2c9b
|
docs: 清掉失效的 train.py 引用与 ONNX 导出依赖组
仓库里从来没有 train.py / eval.py,入口是 classify.py 与 detect.py。
- README 的计算后端与 SwanLab 示例改用 classify.py,看板路径跟着 work_dir
改为 runs/classify/swanlog。
- 删掉"train.py 的 train.restart_schedule 同理":restart_schedule 只在
engine/simclr.py 里实现,分类训练没有这个选项。
- 目录表里 configs/ 的子目录(原写 model/ dataset/ train/ eval/)与 scripts/
的描述改成实际内容;入口那行补上 detect.py。
- 导出走 OpenVINO,没有脚本用到 onnx/onnxscript,删掉 export 依赖组。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-10-08 14:57:11 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
c2bd5301b4
|
feat(deploy): 模型归档统一为 film_<边长>_<时间戳>
之前 160 的 OpenVINO 导出直接从 current.pt 导,文件名不带边长也不带时间戳,
同时留着 128 和 160 两套时分不清谁是谁,软链接又会被下次训练改写。
- 新增 scripts/export_openvino.py:把权重归档成 film_<边长>_<时间戳>.pt 并导出
同名的 OpenVINO 目录。边长从 checkpoint 读、时间戳从训练目录名取,所以同一次
训练无论何时归档名字都一样,也不会出现文件名与实际输入尺寸对不上。
- 重训脚本第 5 步自动归档;训练目录与切片数据集的名字也带上边长。
- openvino_predict.py 的示例路径跟着更新。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-30 15:11:45 +08:00 |
|
Shaun Zhou
|
d692ffdf6e
|
wip: C++ 参考实现(待编译验证)
|
2026-09-29 14:56:21 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
ada67466d4
|
feat(deploy): 不依赖 ultralytics 的 OpenVINO 推理脚本
只用 openvino + numpy + PIL,可以直接拷到部署设备上跑(连同 engine/tiling.py 与
engine/detection.py 三个文件)。
自己解码原始输出:前 4 行是像素 xywh、可能越界要裁剪,其余每行一个类别的分数且已过
sigmoid;导出的 metadata.yaml 里 end2end=false,必须自己做 NMS——实测同一个目标会出
3 个候选框。灰度图自动补成三通道。
图片已经是模型输入尺寸时直接推理;大于输入尺寸时加 --tile-size 自行切片再拼回。
给了 --labels 还会统计逐类 TP/FP/FN。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-24 17:50:53 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
ddd77653b5
|
fix(detect): 推理尺寸与训练尺寸不一致时报警
用 640 整图训练的权重跑 160 切片推理,结果看起来完全正常——照常出框、照常有置信度,
但类别系统性地错:实测 mosquito 的 TP 归零,760 张里 599 张被判成 blackdot,
而同一权重整图推理时 mosquito 有 92% 判对。这种错误不会自己暴露。
从 checkpoint 里读训练时的 imgsz,与当前切片尺寸不符就警告。三个调用点都接上:
可视化、批量推送预测、ML backend 加载权重时。visualize 的 summary.json 也补上
tile_size / tile_overlap,免得事后分不清某次产出是怎么跑的。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-24 15:33:41 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
6e746b7351
|
feat(scripts): 重训后同时维护 current.pt 与 best.pt
current.pt 指向最新一次训练的结果(后端与 push_predictions 都读它),
best.pt 指向历史上 mAP50 最高的那版,留作回退与对照。
数据集每次重训都在变,这个比较只是粗略参考而非严格的同条件对比,所以只用来
维护 best.pt 这个指针,不拦截 current.pt 的切换。
用服务器上三次真实训练验证过:首次建立、更好时更新、更差时保持不变。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-23 16:03:00 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
fad1fe9fbb
|
feat(scripts): 重训流程加入切片步骤
产线要求模型输入固定为 TILE_SIZE×TILE_SIZE 且禁止 resize,训练数据必须先切片,
否则训出来的模型与推理侧的输入形态对不上——实测 640 训练的模型在 160 的片上
一个框都出不来。
五步:导出 -> 装配 -> 切片 -> 训练(imgsz 跟着 TILE_SIZE)-> 切权重并重启后端。
重启时把 YOLO_TILE_SIZE / YOLO_TILE_OVERLAP 一并传给后端,保证两侧尺寸一致。
TILE_SIZE=0 可跳过切片,只用于和旧模型对比。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-23 15:22:58 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
23562448ba
|
fix(detect): 切片统计的口径
原先把"有一边小于 160"记为填充、否则记为裁剪,但 400x100 这种既要横向裁剪
又要纵向填充,会被算成纯填充。改成两个独立计数并说明可同时发生。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-23 15:13:18 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
474b7b50a0
|
feat(detect): 推理侧改为切片,与固定 160 输入一致
训练数据已经是固定边长的片,推理还把整图交给 ultralytics 就会被 letterbox 缩放,
与模型见过的形态不符。三个调用方(可视化、批量推送预测、ML backend)统一改成
切片推理再拼回原图。
- engine/tiling.merge_tiles / tiled_predict:映射回原图、裁掉镜像填充区的框、跨片去重。
推理框架留在调用方,几何部分可单测。
- engine/detection.nms:标准 NMS,另加按包含比例抑制。缺陷被切割线切开时,相邻片
只看到一角,给出的小残框与完整框 IoU 很小(面积悬殊),光靠 IoU 压不掉。
- engine/tiled_inference:ultralytics 的接线,内部分批,避免片数多时 OOM。
- 三处调用点加 --tile-size / --tile-overlap(后端为 YOLO_TILE_SIZE / YOLO_TILE_OVERLAP),
默认 160 / 32;设 0 退回整图推理,仅用于和旧模型对比。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-23 15:12:35 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
2a92275317
|
feat(detect): 固定 160x160 输入,只用裁剪与镜像填充,禁止缩放
产线要求送进模型的图固定为 N×N 且不许 resize——缺陷的绝对尺度是判别依据,
拉伸会把"5 像素的黑点"变成别的东西。
- engine/tiling.py:切片几何。边长 < N 镜像填充(reflect,真镜像,原图居中);
边长 >= N 按步长裁剪、末片贴边保证全覆盖。框跟着平移裁剪,露出面积低于
min_visibility 的丢弃;to_source 反向映射供推理时拼回原图。
- scripts/tile_dataset.py:整套数据集转换。有框图切出的空片会爆量
(524x959 能切 24 片),用 --max-empty-ratio 限量;原本无框的真负样本不受限。
- 训练配置 imgsz 改 160,并显式关掉 scale 与 translate——ultralytics 的默认值是
0.5 / 0.1 而不是 0,不写出来等于一直在做仿射缩放,违反约束。
尺寸正好等于 imgsz 时 letterbox 退化为空操作(r=1、dw=dh=0),核对过源码。
合成数据上验证:所有片正好 160x160、框区域与原图逐像素一致、填充是真镜像。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-23 12:27:57 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
1684fddd7f
|
feat(scripts): 重训脚本可跳过重启 ML backend
手动跑重训时会无条件启动后端——纯手动路线并不想要它常驻占显存。
RESTART_BACKEND=0 可跳过;被后端调用时保持默认 1(权重只在启动时加载一次,
不重启就换不了模型)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-22 13:36:25 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
3c964abaa2
|
fix(scripts): HTTP 错误带上响应体
raise_for_status 只给一行 URL,而 Label Studio 把真正原因写在响应体里。
批量导入返回 500 时看不到任何线索,排查只能靠猜。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-22 13:25:41 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
744e5fd18a
|
fix(scripts): push_predictions 的分页与令牌过期
两个在真实环境暴露的 bug:
1. /api/predictions/ 根本不分页——Label Studio 的 REST_FRAMEWORK 没设
DEFAULT_PAGINATION_CLASS,PAGE_SIZE 形同虚设,它一次返回全部预测并忽略 page。
原先按页翻永远不终止,实测翻到第 1798 页才因别的原因报错。改成只请求一次。
2. access token 默认只有 5 分钟(LS 未覆盖 SimpleJWT 默认值),而推上万条预测远超
5 分钟。所有请求统一走 request(),碰到 401 自动用 refresh token 续期后重试一次。
另加 --replace-all:用 Data Manager 的批量操作一次删光项目里所有预测,
比逐条删同版本快得多(适合版本号变了、旧预测留着没用的情况)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-22 13:17:53 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
9ead8c0a6e
|
fix(scripts): push_predictions 支持 JWT 个人访问令牌
服务器上的 Label Studio 关闭了旧式 token(jwt_auth_jwtsettings.legacy_api_tokens_enabled=0),
用 authtoken_token 里的 key 直接请求返回 401。个人访问令牌是 JWT refresh token,
要先 POST /api/token/refresh/ 换成 access token 再以 Bearer 发送。
脚本按令牌形态自动区分:三段点分的按 JWT 换取,否则沿用旧式 Token。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-22 12:13:20 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
dce2eef0a7
|
fix(ml-backend): 推理分批,避免 Retrieve Predictions 撑爆显存;新增批量推送脚本
Retrieve Predictions 会把选中的任务一次发给后端,而后端把整个列表交给
model.predict(source=paths)——ultralytics 不理会 batch 参数,会当成一个 batch,
于是显存爆掉。这与之前 visualize_detections 踩的是同一个坑。后端现在按
YOLO_BATCH(默认 16)自己分批。
另加 scripts/push_predictions.py:重训完在本地分批推理,再用批量导入接口
POST /api/projects/<id>/import/predictions 提交,不经过后端服务,显存与请求体都可控。
支持 --replace 先删同版本旧预测(预测是追加的,重复跑会堆两份)、--limit 与 --dry-run。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-22 12:11:06 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
8fa7a9d142
|
fix(ml-backend): 重训后真正切到新权重
重训脚本是 ML backend 的子进程,继承了后端 export 的 YOLO_WEIGHTS。启动脚本里
${YOLO_WEIGHTS:-默认值} 于是让旧值赢了——实测重训完成后后端仍加载旧权重,
current.pt 软链接形同虚设。同样被继承的还有 RETRAIN_EXTRA_SET,会让下次训练
沿用上次的冒烟参数(epochs=1)。
重启时显式传 YOLO_WEIGHTS 并 env -u 清掉 RETRAIN_EXTRA_SET。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-21 11:03:14 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
e61fd5887c
|
feat(ml-backend): Start Training 按钮真正触发重训,启动/重训脚本进 scripts/
原先点 Label Studio 的 Start Training 只是把计数加一:START_TRAINING 和普通标注事件
走同一条阈值判断,没配 RETRAIN_COMMAND 时更是静默什么都不做。实际使用中用户点了按钮、
以为在训练,其实权重一直没变。
- Start Training 无条件触发重训;攒够 RETRAIN_AFTER 条新标注也自动触发。
- 只数 ANNOTATION_CREATED:UPDATED / DELETED 是对已有标注的修改,不该把阈值顶到。
- 已有重训在跑时重复点击被忽略,不叠加启动。
- 没配 RETRAIN_COMMAND 时打警告说明不会真的训练,并在返回值里说清楚。
- 子进程输出重定向到 RETRAIN_LOG(默认 /tmp/retrain.log),训练进度有地方看。
scripts/start_ml_backend.sh、scripts/retrain_from_label_studio.sh 放进仓库,
可从工作区直接启动。重训末尾把 runs/detect/current.pt 软链接指向新权重,
启动脚本默认读它,换模型不必改脚本。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-21 11:00:01 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
154677032f
|
Revert "约束预测框互不重叠"(eba533a、9439cbb)
去掉强制非重叠:detect.py 不再给 DetectionValidator.postprocess 打补丁,
配置里移除 train.max_overlap 与 agnostic_nms,可视化脚本恢复为直接使用模型原始输出,
engine/detection 里的 suppress_overlaps / filter_overlapping_outputs 一并移除。
约束会把 mAP50 从 0.555 压到 0.483(precision 0.536->0.612),代价是按置信度抑制时
高分错框会压掉本可命中的低分框。需要时用 git revert 这次提交即可取回。
按类别分目录的可视化产出结构(36fff98)保留。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 15:34:13 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
36fff984b7
|
refactor(detect): 可视化产出改为按类别分目录,与 generate_boxes 一致
原先按 split 和结果类型(ok/missed/extra/none)分目录,层级太深。改成
<类别>/<图名>.png,与 runs/boxes_shap 的结构一致。类别取真值里框最多的那个;
负样本没有真值框,按文件名前缀归类(<类别>_<序号> 是 prepare_label_studio 的命名),
认不出来放 background/。
split 与结果类型仍记在 index.csv 的列里,排序筛选即可。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 15:06:13 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
eba533a5b2
|
feat(detect): 约束预测框互不重叠
人工标注的真值里一处缺陷只有一个框:11211 张图、151 张多框图,重叠框对为 0。
而模型预测在验证集上有 6.9% 的图存在重叠框——NMS 只在同类内、按固定 IoU 阈值抑制,
压不掉两类情况:同一处缺陷被两个类别各框一次(实测 IoU 高达 0.99),以及 IoU 低于
NMS 阈值的同类重叠(grease 66 对、wrinkle 45 对)。
- engine/detection.suppress_overlaps:按置信度贪心保留互不重叠的框,可设允许的
最大 IoU;另可丢弃被大框完全包住的小框(面积悬殊时 IoU 很小,但显然是重复表达)。
- visualize_detections 默认 --max-overlap 0(与真值一致),并放出 --nms-iou 与
类别无关 NMS 开关;--max-overlap -1 可看模型原始输出。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 11:19:47 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
43c8c4a179
|
fix(detect): 可视化推理自己按批切分,避免 CUDA OOM
model.predict 不理会传入的 batch 参数,把整个图片列表当成一个 batch,8969 张图在
16 GiB 显卡上直接 OOM(单次要分配 10.27 GiB)。改成自己按 --batch 切片调用。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 21:47:44 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
788bc36ab8
|
feat(detect): 全量推理可视化,预测框与真值框画在一起
scripts/visualize_detections.py:在整个数据集上推理,绿=真值、蓝=命中的预测、
红=没对上真值的预测,按结果类型分目录(ok / missed / extra / none),再配一份
index.csv(逐图的真值数、预测数、命中数、最高置信度)与 summary.json(逐类 TP/FP/FN)。
--only errors 只输出有问题的图。
匹配逻辑放在 engine/detection.py:按置信度贪心、IoU 阈值、类别需一致,与 mAP 的规则
一致,所以图上看到的漏检误检和指标算的是同一回事。same_class=False 可以单看定位准不准
——定位对但分类错,和完全没找到是两种问题。
train 与 val 分开放:训练集的图模型见过,那部分结果不能当泛化能力看。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 21:47:02 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
59d4b8a9c4
|
fix(detect): 钳掉 Label Studio 导出坐标的浮点溢出
真实导出里出现 height=1.0000000000000002:Label Studio 按百分比存框,导出时 /100
留下浮点误差。ultralytics 读标签时对 >1 的坐标直接报错,所以这点溢出必须钳掉,
否则装配出的数据集根本训不起来。
只钳 1e-6 量级的溢出,真正越界的坐标仍然报错。顺带把坐标统一写成 6 位小数。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 16:44:42 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
998e6601d4
|
feat(detect): 把 Label Studio 的 YOLO 导出装配成带 train/val 划分的数据集
scripts/build_yolo_dataset.py:导出(zip 或目录)+ 复核材料里的 images/ -> data/ 下
可训练的数据集。Label Studio 的 YOLO 导出只有标签,本地存储的图片不会打包进去
(get_local_path 对 local-files 直接返回磁盘路径,YOLO_WITH_IMAGES 也一样),
所以图片从准备复核材料的目录按文件名取,硬链接过去。
- 类别顺序照抄导出的 classes.txt:转换器把标签收进 set 再排序编号,与 labeling
config 的顺序无关,标签里的 class_id 是按它写的。
- 按类别分层切分,分层用"框最多的那个类别"——人工复核后一张图可能有多个类别的框。
- 标签逐行校验:字段数、类别下标、坐标范围。坏标签训练时不会崩,只会悄悄拉低指标。
- 空标签默认保留为负样本;标签找不到图直接报错,图多于标签则告警。
顺带把 link_or_copy 提到 utils/fs.py 共用(此前 generate_boxes 与
prepare_label_studio 各有一份)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 16:42:53 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
65bc6b2045
|
feat(boxes): 新增传统 CV 定位方法(--method cv),不依赖分类模型
背景差分:大半径方框均值估背景 → 残差(可选极性)→ 平滑 → 有无判定 → 按峰值归一化,
再交给与 gradcam / shap 同一套 cam_to_boxes 出框。只依赖 numpy。
与归因方法的差别:不需要 --checkpoint,分类器还没训练出来就能先跑一版框,也不受分类
精度影响;类别只能来自目录(--target label);找的是偏离局部背景的区域,而不是模型
据以判断类别的区域,所以对局部目标好、对铺满整图的纹理类给不出有意义的框。
有无缺陷是显式判定的:残差峰值要同时高于绝对对比度下限与噪声水平(MAD 估计)的 15 倍,
否则返回全零图。这条是必需的——实测纯噪声图的"峰值/MAD"稳定在 9 附近(最大 10.9,
几乎与噪声大小无关),有 0.25 对比度黑点时是 25~330;若只按峰值归一化,正常图的噪声
会被拉到 1,满图假框。阈值 15 取在两者之间。
顺带把积分图方框均值从 shap_explain.smooth 提到 engine/classical.box_mean 共用,
避免两处实现各自漂移。
实测:单张 0.011 秒(全量 11k 约 2 分钟);真实薄膜图上框能套住黑点、虫状物与划痕。
已知局限:背景估计在图像边界处对明暗阶跃有响应,贴边的交界可能出假框。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 11:03:13 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
32e56cb230
|
feat(boxes): generate_boxes.py 的 --target 默认改为 label
框的类别应该来自人工归类(图片所在目录),分类模型只负责定位。原先默认 predicted,
框的类别取模型 top1 预测,模型分错的图会把错误类别写进标签:boxes_shap 那次预测与
目录一致率 0.9915,约 95 张图的类别因此是错的。
两种取值只在预测与目录不一致的图上有区别,其余图解释的是同一个 logit、框完全相同。
--target predicted 仍保留。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 20:31:40 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
7dd0f89d18
|
fix(label-studio): 图片 URL 按实际的 DOCUMENT_ROOT 生成(--document-root)
原先任务里的图片 URL 固定是 /data/local-files/?d=images/<图名>,隐含假设输出目录就是
LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT。服务器上 DOCUMENT_ROOT=/home/deep/labeling,
Label Studio 会去找 /home/deep/labeling/images/<图名>,11211 张图全部 404。
- 新增 --document-root,缺省读 LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT;URL 写成相对它的
路径,Local files 存储路径随之算好并打印。
- 输出目录在 DOCUMENT_ROOT 之外时,在 DOCUMENT_ROOT 里建同名软链接指过去,产出仍留在
runs/,不用手动复制;DOCUMENT_ROOT 里已有同名真目录或指向别处的软链接时报错,不覆盖。
- 更正此前"软链接解析后会落到 DOCUMENT_ROOT 外面"的说法:核对 Label Studio 1.23.0
源码,safe_join 用 abspath、存储路径用 normpath 做字符串前缀比较,都不解析软链接。
- 测试照搬 localfiles_data() 的判定逐个任务验证能否被服务,并加入旧 URL 在
/home/deep/labeling 下全部 404 的回归用例。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 20:04:04 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
891f930de2
|
refactor: prepare_label_studio.py 的内容改动(补上 a3e08a9 漏掉的部分)
a3e08a9 只提交了 git mv 的改名,内容修改因 git add 的路径报错没有进入暂存区。
本提交补上:
- 缺省输出 runs/<source>_label_studio/,不再写到 data/;该目录同时是 Label Studio
的 DOCUMENT_ROOT,打印的导入步骤里提醒复核期间不要删除或 --overwrite。
- 总是生成 tasks.json 与 labeling config,去掉 --label-studio 开关。
- 不再写 data.yaml:复核材料不是训练集,没有 data.yaml 就不会被误拿去训练,
原先"train 与 val 相同"的告警也随之移除(detect.py 里的同类告警保留)。
- detect.py、configs/detect/film.yaml、README 里对旧脚本的引用改为新流程。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 17:26:22 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
a3e08a9bbc
|
refactor: prepare_yolo_dataset.py 改为 prepare_label_studio.py,产出放到 runs/
弱监督框要先在 Label Studio 里人工修正才能训练,这个脚本实际上只负责准备复核材料,
名字和产出位置都按这个职责调整:
- 改名 scripts/prepare_label_studio.py(git mv 保留历史),测试同步改名。
- 缺省输出 runs/<source>_label_studio/,不再写到 data/;该目录同时是 Label Studio
的 DOCUMENT_ROOT,打印的导入步骤里提醒复核期间不要删除或 --overwrite。
- 总是生成 tasks.json 与 labeling config,去掉 --label-studio 开关。
- 不再写 data.yaml:复核材料不是训练集,没有 data.yaml 就不会被误拿去训练,
原先"train 与 val 相同"的告警也随之移除(detect.py 里的同类告警保留)。
修正后的标签从 Label Studio 导出、划分 train/val 到 data/ 的脚本另写。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 17:26:05 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
af0897f8c7
|
refactor(detect): 数据集改为扁平结构、不划分 train/val,Label Studio 文件放到根目录
prepare_yolo_dataset.py 的产出先导入 Label Studio 复核,划分留到复核之后,于是:
- 去掉按类别分层的 train/val 切分以及 --val-ratio / --seed,images/ 与 labels/
下直接是文件;name_map.csv 去掉 split 列,summary.json 的逐类数量改为单列。
- label_studio/ 子目录的内容上移一层:扁平的数据集本身就是 label-studio-converter
要求的 <input>/{images,labels,classes.txt},tasks.json 与 labeling config 直接
生成在根目录,不再需要额外的硬链接视图,DOCUMENT_ROOT 即数据集根目录。
- data.yaml 的 train 与 val 只能指向同一个 images/。ultralytics 能读,但验证指标
算的是训练集本身,mAP 虚高、早停与 best.pt 的选择失真。脚本结束时告警,
detect.py 检测到 train == val 时也告警。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 17:19:53 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
1a43267b55
|
feat(detect): --label-studio 一步生成可导入 Label Studio 的复核视图
label-studio-converter import yolo 只认 <input>/{images,labels,classes.txt},且
os.listdir 不递归,看不见 train/val 子目录,直接喂数据集会 "No labels converted"。
按类别重命名后文件名全局唯一,于是在 <out>/label_studio/ 下把两个 split 拍平
(硬链接:Label Studio 本地存储会校验路径在 DOCUMENT_ROOT 之下,软链接可能越界),
再调用 converter 生成 tasks.json 与 labeling config。
- 框以 predictions 导入:它们是 SHAP/Grad-CAM 生成的,审核员提交后才成为
annotation,导出时没审过的框不会混进去。
- 不传 image_dims:真实数据有 3223 种尺寸,传了会写错每条标注的宽高。
- 任务数与图片数不符时报错,而不是静默少导。
- converter 自带的导入提示漏掉了 LABEL_STUDIO_LOCAL_FILES_* 环境变量(它在循环里
改写了 image_root_url,之后与默认值比较失败),静音后由脚本打印带实际路径的步骤。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 17:06:40 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
7adc5b6332
|
feat(detect): YOLO 数据集统一为 PNG、按类别重命名并附 classes.txt
- 图片统一成 PNG。pearlescent / blue / uv 的原始数据是 BMP,generate_boxes.py
按原后缀链接,于是 runs/boxes_shap 里 3685 张 BMP 与 7526 张 PNG 混在一起,
ripple 与 uneven 两类全是 BMP。按文件头(而非后缀)判断:真 PNG 照旧硬链接,
其余无损转换,不缩放像素,归一化框坐标原样有效;CMYK 等 PNG 存不下的模式转 RGB。
在真实数据上核对过 3685 张转换结果逐像素一致。
- 文件按 <类别>_<序号> 重新编号。类别取标签第一列,文件名不会与标签矛盾;
序号在类内按源路径排序、与划分无关,换种子重跑名字不变。新旧名对照写入
name_map.csv,源文件名里的正反面、位置、时间戳不丢。--keep-names 保留原名。
- 输出 classes.txt(行号即 class_id),供 LabelImg / X-AnyLabeling 复核框时使用。
- 撞名检查改为按输出文件名的小写比较:统一后缀后 a.png 与 a.bmp 会撞,
macOS 默认文件系统也不区分大小写。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 16:45:58 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
012b3efc73
|
feat(detect): 按来源类别处置无框图片(--empty-policy)
空标签在 YOLO 里的语义是"这张图什么都没有",是一个明确的负样本断言,而不是
"有缺陷但没能定位出来"。原先一律保留,于是 runs/boxes_gradcam 的 339 张空标签
里有 270 张是人工标成缺陷、Grad-CAM 却失手的图(blackdot 占该类 11%、black 占
24%),它们正以"此图无缺陷"的身份进入训练,等于在教模型抑制真实缺陷。
--empty-policy 接一个全局默认加若干 `类别=策略`:
keep 当负样本保留(默认)/ drop 丢弃 / full 退化成整图框
整图框需要重写标签而不是链接源文件(内容不同),且有了确定类别之后要跟着本类
一起参与分层划分。类别名或策略拼错直接报错——静默忽略会让策略看着生效其实没有。
没显式指定、又落到 keep 的类别会打一条告警。
替换掉原来的 --drop-empty:它是一刀切,会把 normal 那 69 张有价值的负样本一起扔掉。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-11 16:34:16 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
8dc2fe6a2e
|
feat(detect): 把弱监督框数据集链接到 data/ 并接上 YOLO 训练
第五步产出的框标签按类别分目录,便于人工核对;检测训练要的是 train/val
划分——类别写在标签第一列,不靠目录名。补上两个入口:
- scripts/prepare_yolo_dataset.py:拍平类别目录、按类别分层切分、硬链接到
data/。分层是必需的,black 只有 53 张,随机切容易让小类在验证集里挂零。
无框的图默认保留(检测训练里是压低误检的负样本);拍平后的同名冲突直接
报错而不是静默覆盖。
- detect.py + configs/detect/film.yaml:包一层 ultralytics,沿用本仓库的
配置继承、设备解析与 SwanLab 可视化。
选 YOLOv8 而不是 v11/v12:CLAUDE.md 限定只用 convolution 模型,YOLO11 骨架
里有 C2PSA、YOLO12 以注意力为主,只有 v8 的 C2f + SPPF 是纯卷积。
mosaic/mixup 关掉——每张图只有一个弱监督框,拼接后小目标被缩得更小,对
blackdot/whitedot 这类只占千分之几面积的类别是净伤害。
ultralytics 的 project 必须传绝对路径,相对路径会被它再拼到全局 settings
的 runs_dir 上,产出会跑到 runs/detect/runs/detect/<name> 去。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-11 12:05:50 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
86c86b105c
|
refactor: 按实际的五步工作流裁掉无关代码
实际工作流是:simclr 训练 -> 聚类 -> 人工归类 -> 分类训练 -> GradCAM/SHAP 生成
热力图与 box。据此移除不参与其中的检测/定位链路,共 31 个文件、约 2600 行:
- 模型:defect_net、postprocess、necks/、heads/center_head
- 数据:film_defect(YOLO 读取端)、target_encoder
- 引擎:trainer、evaluator、losses
- 入口:train.py、eval.py
- 脚本:convert_annotations、split_dataset、export_onnx、profile_inference
- 配置:configs/{train,eval,model,dataset}
- utils/labels(多类/单类标签模式,仅检测分类头使用)
共享模块按实际用量瘦身:engine/metrics 只留 AverageMeter(检测指标随之移除);
datasets/yolo 只留写出侧(IMAGE_SUFFIXES 与 write_data_yaml)——第 5 步产出的
YOLO 标签是给外部检测训练用的交换格式,读取端不再需要。
裁剪过程中修掉一个真 bug:generate_boxes 的 --stage 默认 6(layer3)对 tiny_conv
这类非 nn.Sequential 骨架直接抛异常。现在自动退回骨架最终输出并警告,
另加 --stage -1 显式选择最终输出。
五步工作流已端到端实跑验证(simclr 1 轮 -> 聚类 -> 导出 -> 分类 1 轮 ->
gradcam 与 shap 两种方法各生成一遍)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-11 10:27:20 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
57446c2f10
|
refactor: 合并为单一的框生成入口,gradcam / shap 可选,默认全量
此前是两个脚本:gradcam_boxes.py 全量跑但只有一种方法,shap_boxes.py 默认抽样
且强制输出对比图。两者合并为 scripts/generate_boxes.py:
- --method gradcam|shap 二选一,默认 gradcam
- 默认对全量数据生成;--limit / --per-class 降级为试参数用的可选项
- --min-area-ratio 缺省随方法而变(gradcam 0.02、shap 0.002):两者归因图分辨率差
16 倍,同一个小黑点在 128x128 上只占百来像素,共用阈值会被整个滤掉
产出改为符合 CLAUDE.md 工作流第 8 步的规格,是一个自成一体的 YOLO 数据集:
images/ 是原图硬链接、labels/ 是 YOLO 标签、heatmaps/ 是热力图叠加框。
images/ 与 labels/ 的对应关系符合仓库的 YOLO 约定,FilmDefectDataset 可直接读取,
把分类产出接回了检测训练链路(已加测试验证这条闭环)。
热力图目标类别改为模型 top1 预测(原为目录标注),--target label 可切回;
summary.json 记录两者一致率(实测 0.9873)。
修掉一个会静默丢标签的 bug:产出按预测类别分目录,不同源目录下的同名图
(dirA/x.png 与 dirB/x.png)若被预测到同一类会写同一个标签路径,后写的覆盖前者。
现在显式检测并中止,提示用 export_clusters.py --prefix-parent 重新导出。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-10 16:26:56 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
9aa87d31a6
|
feat(localization): SHAP Partition 定位方法,并把每图框数上限默认收紧为 1
新增基于 SHAP Partition explainer 的定位,与 Grad-CAM 形成互补对照:
- engine/shap_explain.py:对目标类别求 SHAP 归因(只保留正贡献),以及方框平滑。
遮挡用数值 masker 而非教程里的 inpaint_telea——后者依赖 opencv,而我们的图已做过
逐图标准化,用 0(即该图均值)遮挡语义更干净
- scripts/shap_boxes.py:按类别抽样生成标签,并输出「原图 | Grad-CAM | SHAP」并排图
- shap 放入可选依赖组:uv sync --group explain
实测(225 张,每类 15):SHAP 在小目标类别上明显更准,框面积占比
blackdot 0.019 vs 0.269(14 倍)、mosquito 0.030 vs 0.173、whitedot 0.076 vs 0.253。
原因是 Grad-CAM 受限于 8x8 特征图(每格 16x16 像素),几像素的黑点根本不可分辨。
代价是单张 0.55 秒,全量 11k 张约 1.7 小时,而 Grad-CAM 全量只要 2 分钟。
调参时发现的坑:SHAP 第一版全部出 0 框,因为沿用了给 8x8 调的
min_area_ratio=0.02(在 128x128 上等于 328 像素)。参数不能跨分辨率照搬,
SHAP 默认改为 0.002。
--max-boxes 两个脚本都默认改为 1(原为 2):一张裁片通常只对应一处缺陷。
库函数 cam_to_boxes 的默认值同步收紧,相关测试改为显式传参而不依赖默认值。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-10 16:10:50 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
fc7136fddf
|
feat: 用 Grad-CAM 从分类模型生成弱监督框标签
分类模型只学过"是什么缺陷",Grad-CAM 把分类分数对卷积特征的梯度当作通道重要性,
加权求和得到空间热力图,阈值化 + 连通域外接矩形即得到框标签,输出 YOLO 格式,
可直接接入既有的检测训练链路。
- engine/gradcam.py:整批一次 backward 的 Grad-CAM(各样本只回传自己那一类的分数)、
numpy 实现的 4-邻接连通域标记(不为这一处引入 scipy/opencv)、CAM 转归一化框
- scripts/gradcam_boxes.py:遍历数据集生成标签,并输出「原图 | 热力图 | 叠加+框」
三联图供人工核对;框的类别取自图片所在目录而非模型预测
两处按实图定下的默认值:
- --stage 默认 6(layer3, 8×8)。layer4 只有 4×4 太粗,layer2 响应太散导致 40% 出不了框
- --peak-ratio 0.75:次要连通域峰值需达全局最大值的 75%,否则会在无缺陷的角落刷假框;
实测把两框比例从 53% 降到 20%
全量 11,212 张已生成:61.8% 出 1 框、35.2% 出 2 框、3.0% 无框,每类留 25 张可视化。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-10 15:46:30 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
33d28dcc7c
|
feat(cluster): clusters/ 默认改用硬链接,便于直接在 runs/ 下继续分子类
软链接记的是绝对路径,从服务器拷到本机后全部失效,也不便于直接在 runs/ 下整理。
硬链接不占额外磁盘却表现得和真实文件一样:看图软件能开、可以随意挪动、建子目录
继续分子类,整个目录拷到别的机器也不会断链。
- 新增 link_mode: hard(默认)/ sym / copy;跨文件系统时 hard 自动退回软链并警告
- 落盘后打印各方式的条目数,便于确认实际用的是哪种
- export_clusters.py 适配硬链接:条目本身就是原图,但路径在 runs/ 下、名字还带
0125_ 排序前缀。按 inode 反查 assignments.csv 还原原图路径,这样导出的文件名与
--prefix-parent 才能拿到真正的来源目录;查不到则剥掉前缀后用文件名索引兜底
实测:14,579 张跑聚类后 clusters/ 全为硬链接(nlink=2、可直接打开);
把簇改名、建子目录挪图后再导出,排序前缀已剥除、来源目录正确还原为「反面纵黑点」,
120 张内容与原图逐字节一致。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-10 12:04:08 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
f8654f447b
|
feat(export): 改为按 clusters/ 目录导出,并重定位失效的软链
原来按 assignments.csv 导出,反映的是聚类当时的划分;而 clusters/ 目录会被人工
整理——挪动分错的图、把 cluster_03 改名成 grease。现在以整理后的目录为准。
- 扫描 clusters/ 下的子目录,目录名即类别名并原样保留(不再是数字编号)
- 文件名用原图的名字,去掉 clusters/ 里的 NNNN_ 排序前缀
- 失效软链的重定位:读出链接目标,按其中的 data/ 段落到本机 --source-root 上;
再不行用文件名在源目录里兜底索引(惰性构建,用不到就不扫盘)。
从服务器拷回来的 clusters/ 软链全部指向 /home/deep/... 在本机失效,这是常态
- 解析不到的条目列出并中止,--skip-missing 可忽略
- manifest 记录类别、每张图的原始路径与被跳过的条目
在真实目录上验证:11,212 张、15 个类别,软链 100% 失效但全部重定位成功,
导出后抽查 200 张与原图逐字节一致。跨数据集的数字文件名(blue/113.bmp 与
pearlescent/113.bmp)会被重名检查拦下,--prefix-parent 可化解。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-10 11:26:02 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
9b31473b27
|
feat(export): 支持把原父目录名作为文件名前缀
新增 --prefix-parent:复制时把图片原来的父目录名拼到文件名前
(grease/abc.png -> grease_abc.png)。导出后目录只剩簇号,来源信息本来会丢失,
加前缀能留住它,人工核对时一眼看出某张图原本在哪个目录。
- 重名检查改为按**目标文件名**判重:加前缀后原本会撞车的文件可能就不撞了;
不加前缀而撞车时,报错会提示可以试试这个开关
- manifest.json 记下 prefix_parent,导出的 file 字段也用实际写出的文件名
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-08 15:26:00 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
506fde0290
|
fix(cluster): 配置层级写错不再静默忽略,并始终打印生效的超参
聚类配置里 optimizer/epochs 等键都在 `simclr:` 段下。写到顶层会被完全忽略、
不报任何错——用户改了 lr 却毫无反应,极难排查。同样的坑还有:改了基类
configs/cluster/film.yaml 的 simclr.optimizer,但运行的是子配置
film_simclr_resnet18.yaml,子配置里的同名键会盖掉基类的修改。
- 顶层出现 optimizer / scheduler / epochs / temperature / projection /
save_interval / resume / checkpoint / log_interval 时警告并给出正确写法
- method != simclr 却写了 simclr: 段时提示该段不会生效
- SimCLRTrainer 初始化时无条件打印实际生效的 epochs / temperature /
optimizer / scheduler,不再只在续训时才打印
- 修掉一处假差异:旧存档没有 scheduler_cfg 字段时会误报 "scheduler None -> cosine",
那是"不知道"而非"当时没有调度器"
已验证 --set 覆盖、直接改 yaml、旧格式存档三条路径下 lr/weight_decay 均正常生效。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-07 19:11:31 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
0f5bb4a5c0
|
feat(cluster): 加 --train-only,自监督训练与聚类可分两步跑
engine/simclr.py 与 engine/cluster.py 本来就互不引用,唯一接口是
simclr_backbone.pth;耦合只在 CLI——method=simclr 会训练完直接聚类。
此前"只聚类"可以用 simclr.checkpoint 做到,"只训练"没有办法。
- --train-only:训练存完权重即退出,不抽特征也不聚类,并打印下一步的聚类命令
- 与 method=pretrained / simclr.checkpoint / --sweep-k 组合时立刻报错说明原因,
而不是跑完才发现没有产出
- 典型用法:GPU 机器上 --train-only 跑几十小时,之后拿权重反复调 k,每次约 1 分钟
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-07 11:38:59 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
5d395b7c25
|
chore: 数据迁到 data/raw/ 后同步所有路径引用
原始数据从 data/<数据集> 移到了 data/raw/<数据集>。
- 聚类配置的 dataset.root:film / pearlescent 均改为 data/raw/ 下
- 检测配置的 img_dir 与 label_dir 注释同步
- README、data/README、算法文档、流程图(含重新渲染的 PNG)与各脚本
docstring 里的路径示例一并更新
- data/README 补充目录约定:原始数据只读放 data/raw/,
export_clusters.py 的派生数据集放在 data/ 下与 raw/ 平级
- 顺手把 unlabeled 数据集加进 datasets/__init__.py:此前它没在包初始化时注册,
build_dataset({"name": "unlabeled"}) 只有在别处先 import 过该模块时才可用
迁移后重跑全量聚类,结果与迁移前逐项一致(轮廓 0.3255、稳定性 0.9641、
簇大小 [4507, 3795, 4849, 1428])。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-07 11:19:42 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
f8a52c9382
|
feat(scripts): 按聚类结果导出数据集
新增 scripts/export_clusters.py:读聚类产出的 assignments.csv,把原图复制成
data/<源目录名>_<骨架标签>_cluster_k<簇数>/,子目录用从 0 开始的数字编号。
- 复制真实文件(copy2 保留修改时间),不建软链、不改名
- 目录名自动推导:源目录名取各路径的公共父目录,骨架标签取自 summary.json 的
feature_source;--tag / --out 可覆盖
- manifest.json 记录来源 run、特征配置与每张图的原始路径,便于追溯
- 保护:源文件缺失、同簇内重名(保持原名会互相覆盖)直接报错;
目标目录非空需显式 --overwrite;--dry-run 只打印计划
- 聚类产出的 summary.json 新增 feature_source 字段(method / backbone /
feature_stages),供上面的自动命名与日后追溯
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-06 13:17:53 +08:00 |
|