phone-detect-test 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phone_detect_test-0.2.0/PKG-INFO +203 -0
- phone_detect_test-0.2.0/README.md +176 -0
- phone_detect_test-0.2.0/phone_detect_test/__init__.py +12 -0
- phone_detect_test-0.2.0/phone_detect_test/__main__.py +10 -0
- phone_detect_test-0.2.0/phone_detect_test/audit.py +155 -0
- phone_detect_test-0.2.0/phone_detect_test/clean.py +246 -0
- phone_detect_test-0.2.0/phone_detect_test/cli.py +184 -0
- phone_detect_test-0.2.0/phone_detect_test/detect.py +93 -0
- phone_detect_test-0.2.0/phone_detect_test/download_weights.py +76 -0
- phone_detect_test-0.2.0/phone_detect_test/evaluate.py +106 -0
- phone_detect_test-0.2.0/phone_detect_test/prepare.py +82 -0
- phone_detect_test-0.2.0/phone_detect_test/resample.py +56 -0
- phone_detect_test-0.2.0/phone_detect_test/server.py +773 -0
- phone_detect_test-0.2.0/phone_detect_test/train.py +50 -0
- phone_detect_test-0.2.0/phone_detect_test/utils.py +307 -0
- phone_detect_test-0.2.0/phone_detect_test/val.py +47 -0
- phone_detect_test-0.2.0/phone_detect_test/web/index.html +2776 -0
- phone_detect_test-0.2.0/phone_detect_test.egg-info/PKG-INFO +203 -0
- phone_detect_test-0.2.0/phone_detect_test.egg-info/SOURCES.txt +24 -0
- phone_detect_test-0.2.0/phone_detect_test.egg-info/dependency_links.txt +1 -0
- phone_detect_test-0.2.0/phone_detect_test.egg-info/entry_points.txt +2 -0
- phone_detect_test-0.2.0/phone_detect_test.egg-info/requires.txt +16 -0
- phone_detect_test-0.2.0/phone_detect_test.egg-info/top_level.txt +1 -0
- phone_detect_test-0.2.0/pyproject.toml +49 -0
- phone_detect_test-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phone-detect-test
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 考场手机检测:一条命令启动监控台网页,另附检测/训练/数据清洗/标签审计 CLI
|
|
5
|
+
Author: xunfei
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: yolo,object-detection,phone-detection,dataset-clean,label-audit
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: ultralytics>=8.4.0
|
|
14
|
+
Requires-Dist: opencv-python
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: pyyaml
|
|
17
|
+
Requires-Dist: pillow
|
|
18
|
+
Requires-Dist: fastapi>=0.100
|
|
19
|
+
Requires-Dist: uvicorn>=0.20
|
|
20
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
21
|
+
Provides-Extra: cpu
|
|
22
|
+
Requires-Dist: torch>=2.0.0; extra == "cpu"
|
|
23
|
+
Requires-Dist: torchvision>=0.15.0; extra == "cpu"
|
|
24
|
+
Provides-Extra: full
|
|
25
|
+
Requires-Dist: torch>=2.0.0; extra == "full"
|
|
26
|
+
Requires-Dist: torchvision>=0.15.0; extra == "full"
|
|
27
|
+
|
|
28
|
+
# phone-detect-test
|
|
29
|
+
|
|
30
|
+
考场手机检测命令行工具:**检测 / 训练 / 数据清洗 / 标签审计**,一套命令搞定。
|
|
31
|
+
|
|
32
|
+
基于 YOLO(Ultralytics)封装,把「目标检测项目从数据到上线」的完整流程收敛成统一 CLI,
|
|
33
|
+
不依赖 WorkBuddy,`pip install` 后即可在任意终端使用。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 30 秒上手:一条命令打开监控台
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install phone-detect-test
|
|
41
|
+
phone-detect-test web
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`web` 会依次完成:**解析/下载模型权重 → 启动推理服务 → 托管监控台网页 → 自动打开浏览器**。
|
|
45
|
+
页面和接口在**同一个端口**(默认 8080),所以不需要再单独起一个静态服务器,也没有跨域问题。
|
|
46
|
+
|
|
47
|
+
| 参数 | 作用 |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `--port 9000` | 换端口(默认 8080,被占用会自动顺延并打印实际地址) |
|
|
50
|
+
| `--no-browser` | 不自动打开浏览器(服务器/远程场景) |
|
|
51
|
+
| `--device cpu` | 强制用 CPU |
|
|
52
|
+
| `--imgsz 512` | 降分辨率提速 |
|
|
53
|
+
| `--host 0.0.0.0` | 允许局域网访问 |
|
|
54
|
+
| `--weights D:\p\phone_best.pt` | 用本地权重,跳过下载 |
|
|
55
|
+
|
|
56
|
+
> 摄像头必须通过 `127.0.0.1` / `localhost` 访问(浏览器安全上下文要求),
|
|
57
|
+
> 所以别用 `file://` 直接双击页面。
|
|
58
|
+
|
|
59
|
+
## 安装
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install phone-detect-test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> `web` 命令需要 `fastapi` / `uvicorn` / `python-multipart`,已写进依赖,装包时自动带上。
|
|
66
|
+
|
|
67
|
+
### 安装 torch(必选其一)
|
|
68
|
+
|
|
69
|
+
`torch` 体积大且有 CPU/CUDA 之分,因此**不随包强制安装**,请按你的硬件二选一:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 有 NVIDIA 显卡(推荐,速度快 ~10 倍)
|
|
73
|
+
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124
|
|
74
|
+
|
|
75
|
+
# 没有显卡(纯 CPU)
|
|
76
|
+
pip install torch torchvision
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> 也可直接 `pip install phone-detect-test[cpu]`(等价于装 CPU 版 torch)。
|
|
80
|
+
|
|
81
|
+
验证 GPU 是否可用:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 快速开始
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# 1. 检测单张图 / 目录
|
|
93
|
+
phone-detect-test detect --source img.jpg --weights phone_best.pt
|
|
94
|
+
phone-detect-test detect --source test_images/ --output out.json
|
|
95
|
+
|
|
96
|
+
# 2. 检测并画出框
|
|
97
|
+
phone-detect-test detect --source img.jpg --draw --draw-dir vis/
|
|
98
|
+
|
|
99
|
+
# 3. 整理数据集
|
|
100
|
+
phone-detect-test prepare --src raw_data/ --out dataset/
|
|
101
|
+
|
|
102
|
+
# 4. 继续训练
|
|
103
|
+
phone-detect-test train --data dataset/data.yaml --epochs 100
|
|
104
|
+
|
|
105
|
+
# 5. 验证集评估 + 搜索最优阈值
|
|
106
|
+
phone-detect-test val --data dataset/data.yaml --sweep
|
|
107
|
+
|
|
108
|
+
# 6. 标签污染审计(找出可疑标注)
|
|
109
|
+
phone-detect-test audit --images dataset/images/train --labels dataset/labels/train --out-dir suspicious/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 命令一览
|
|
115
|
+
|
|
116
|
+
| 命令 | 功能 | 来源 |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `web` | **启动监控台网页 + 推理接口**(一条命令,自动开浏览器) | `web/index.html` + `server/app.py` |
|
|
119
|
+
| `detect` | 单图 / 目录批量检测,输出框或 JSON | `predict.py` |
|
|
120
|
+
| `train` | 在自己数据集上继续训练 / 微调 | `train.py` |
|
|
121
|
+
| `val` | 验证集评估 + 置信度阈值搜索 | `val.py` |
|
|
122
|
+
| `prepare` | 整理图片+标签为 train/val 并生成 data.yaml | `prepare_data.py` |
|
|
123
|
+
| `clean` | 一键清洗数据集(去重/修正/漏标误标检查) | `clean_data.py` |
|
|
124
|
+
| `audit` | 标签污染审计(论文核心方法) | `suspicious_samples.py` |
|
|
125
|
+
| `resample` | 正负样本重采样 | `resample_dataset.py` |
|
|
126
|
+
| `evaluate` | 离线评测(复刻赛题评分规则) | `evaluate.py` |
|
|
127
|
+
|
|
128
|
+
每个命令加 `--help` 查看完整参数,例如 `phone-detect-test detect --help`。
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 模型权重
|
|
133
|
+
|
|
134
|
+
本包**不内置权重**。解析优先级:
|
|
135
|
+
|
|
136
|
+
1. `--weights` 显式指定的路径
|
|
137
|
+
2. 本机自动发现(当前目录、`weights/`、`com_phone/weights/` 下的 `phone_best.pt` / `best.pt`)
|
|
138
|
+
3. 从 `PHONE_DETECT_WEIGHTS_URL` 环境变量(或源码里 `DEFAULT_WEIGHTS_URL`)下载
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 显式指定
|
|
142
|
+
phone-detect-test detect --source img.jpg --weights /path/to/phone_best.pt
|
|
143
|
+
|
|
144
|
+
# 配置下载地址(把权重传到 GitHub Release 后)
|
|
145
|
+
export PHONE_DETECT_WEIGHTS_URL="https://github.com/you/repo/releases/download/v1.0/phone_best.pt"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 数据格式
|
|
151
|
+
|
|
152
|
+
单类别检测,标注里 `class` 固定为 `0`,YOLO 归一化格式:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
class x_center y_center width height
|
|
156
|
+
0 0.512 0.430 0.120 0.250
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
目录结构:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
dataset/
|
|
163
|
+
├── images/
|
|
164
|
+
│ ├── train/*.jpg
|
|
165
|
+
│ └── val/*.jpg
|
|
166
|
+
└── labels/
|
|
167
|
+
├── train/*.txt # 空 txt = 无目标(负样本,很重要)
|
|
168
|
+
└── val/*.txt
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 关于「标签污染审计」
|
|
174
|
+
|
|
175
|
+
这是本项目最有价值的方法论。核心结论:
|
|
176
|
+
|
|
177
|
+
> 自动化的「模型否决式」标注清洗会误删大量真目标;且**用清洗模型自己验证自己**会导致
|
|
178
|
+
> 指标系统性虚高 / 失真。审计必须用**独立模型**交叉回检。
|
|
179
|
+
|
|
180
|
+
`audit` 命令就是这套方法的可复现工具——用独立模型回检标注,输出三类可疑样本
|
|
181
|
+
(漏标 / 错框 / 预测无对应)及可视化对比图。
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 开发
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 可编辑安装
|
|
189
|
+
pip install -e .
|
|
190
|
+
|
|
191
|
+
# 构建分发包
|
|
192
|
+
pip install build && python -m build
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 页面文件是「两份」,改完必须同步
|
|
196
|
+
|
|
197
|
+
监控台页面的**唯一源文件**是仓库里的 `web/index.html`;包里还有一份副本
|
|
198
|
+
(`phone_detect_test/web/index.html`),因为 setuptools 不能把包目录之外的文件打进 wheel。
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python packaging/sync_web.py # 改完 web/index.html 后同步进包
|
|
202
|
+
python packaging/sync_web.py --check # 发布前校验两份是否一致(不一致退出码 1)
|
|
203
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# phone-detect-test
|
|
2
|
+
|
|
3
|
+
考场手机检测命令行工具:**检测 / 训练 / 数据清洗 / 标签审计**,一套命令搞定。
|
|
4
|
+
|
|
5
|
+
基于 YOLO(Ultralytics)封装,把「目标检测项目从数据到上线」的完整流程收敛成统一 CLI,
|
|
6
|
+
不依赖 WorkBuddy,`pip install` 后即可在任意终端使用。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 30 秒上手:一条命令打开监控台
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install phone-detect-test
|
|
14
|
+
phone-detect-test web
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`web` 会依次完成:**解析/下载模型权重 → 启动推理服务 → 托管监控台网页 → 自动打开浏览器**。
|
|
18
|
+
页面和接口在**同一个端口**(默认 8080),所以不需要再单独起一个静态服务器,也没有跨域问题。
|
|
19
|
+
|
|
20
|
+
| 参数 | 作用 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `--port 9000` | 换端口(默认 8080,被占用会自动顺延并打印实际地址) |
|
|
23
|
+
| `--no-browser` | 不自动打开浏览器(服务器/远程场景) |
|
|
24
|
+
| `--device cpu` | 强制用 CPU |
|
|
25
|
+
| `--imgsz 512` | 降分辨率提速 |
|
|
26
|
+
| `--host 0.0.0.0` | 允许局域网访问 |
|
|
27
|
+
| `--weights D:\p\phone_best.pt` | 用本地权重,跳过下载 |
|
|
28
|
+
|
|
29
|
+
> 摄像头必须通过 `127.0.0.1` / `localhost` 访问(浏览器安全上下文要求),
|
|
30
|
+
> 所以别用 `file://` 直接双击页面。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install phone-detect-test
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> `web` 命令需要 `fastapi` / `uvicorn` / `python-multipart`,已写进依赖,装包时自动带上。
|
|
39
|
+
|
|
40
|
+
### 安装 torch(必选其一)
|
|
41
|
+
|
|
42
|
+
`torch` 体积大且有 CPU/CUDA 之分,因此**不随包强制安装**,请按你的硬件二选一:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 有 NVIDIA 显卡(推荐,速度快 ~10 倍)
|
|
46
|
+
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124
|
|
47
|
+
|
|
48
|
+
# 没有显卡(纯 CPU)
|
|
49
|
+
pip install torch torchvision
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> 也可直接 `pip install phone-detect-test[cpu]`(等价于装 CPU 版 torch)。
|
|
53
|
+
|
|
54
|
+
验证 GPU 是否可用:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 快速开始
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. 检测单张图 / 目录
|
|
66
|
+
phone-detect-test detect --source img.jpg --weights phone_best.pt
|
|
67
|
+
phone-detect-test detect --source test_images/ --output out.json
|
|
68
|
+
|
|
69
|
+
# 2. 检测并画出框
|
|
70
|
+
phone-detect-test detect --source img.jpg --draw --draw-dir vis/
|
|
71
|
+
|
|
72
|
+
# 3. 整理数据集
|
|
73
|
+
phone-detect-test prepare --src raw_data/ --out dataset/
|
|
74
|
+
|
|
75
|
+
# 4. 继续训练
|
|
76
|
+
phone-detect-test train --data dataset/data.yaml --epochs 100
|
|
77
|
+
|
|
78
|
+
# 5. 验证集评估 + 搜索最优阈值
|
|
79
|
+
phone-detect-test val --data dataset/data.yaml --sweep
|
|
80
|
+
|
|
81
|
+
# 6. 标签污染审计(找出可疑标注)
|
|
82
|
+
phone-detect-test audit --images dataset/images/train --labels dataset/labels/train --out-dir suspicious/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 命令一览
|
|
88
|
+
|
|
89
|
+
| 命令 | 功能 | 来源 |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `web` | **启动监控台网页 + 推理接口**(一条命令,自动开浏览器) | `web/index.html` + `server/app.py` |
|
|
92
|
+
| `detect` | 单图 / 目录批量检测,输出框或 JSON | `predict.py` |
|
|
93
|
+
| `train` | 在自己数据集上继续训练 / 微调 | `train.py` |
|
|
94
|
+
| `val` | 验证集评估 + 置信度阈值搜索 | `val.py` |
|
|
95
|
+
| `prepare` | 整理图片+标签为 train/val 并生成 data.yaml | `prepare_data.py` |
|
|
96
|
+
| `clean` | 一键清洗数据集(去重/修正/漏标误标检查) | `clean_data.py` |
|
|
97
|
+
| `audit` | 标签污染审计(论文核心方法) | `suspicious_samples.py` |
|
|
98
|
+
| `resample` | 正负样本重采样 | `resample_dataset.py` |
|
|
99
|
+
| `evaluate` | 离线评测(复刻赛题评分规则) | `evaluate.py` |
|
|
100
|
+
|
|
101
|
+
每个命令加 `--help` 查看完整参数,例如 `phone-detect-test detect --help`。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 模型权重
|
|
106
|
+
|
|
107
|
+
本包**不内置权重**。解析优先级:
|
|
108
|
+
|
|
109
|
+
1. `--weights` 显式指定的路径
|
|
110
|
+
2. 本机自动发现(当前目录、`weights/`、`com_phone/weights/` 下的 `phone_best.pt` / `best.pt`)
|
|
111
|
+
3. 从 `PHONE_DETECT_WEIGHTS_URL` 环境变量(或源码里 `DEFAULT_WEIGHTS_URL`)下载
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 显式指定
|
|
115
|
+
phone-detect-test detect --source img.jpg --weights /path/to/phone_best.pt
|
|
116
|
+
|
|
117
|
+
# 配置下载地址(把权重传到 GitHub Release 后)
|
|
118
|
+
export PHONE_DETECT_WEIGHTS_URL="https://github.com/you/repo/releases/download/v1.0/phone_best.pt"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 数据格式
|
|
124
|
+
|
|
125
|
+
单类别检测,标注里 `class` 固定为 `0`,YOLO 归一化格式:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
class x_center y_center width height
|
|
129
|
+
0 0.512 0.430 0.120 0.250
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
目录结构:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
dataset/
|
|
136
|
+
├── images/
|
|
137
|
+
│ ├── train/*.jpg
|
|
138
|
+
│ └── val/*.jpg
|
|
139
|
+
└── labels/
|
|
140
|
+
├── train/*.txt # 空 txt = 无目标(负样本,很重要)
|
|
141
|
+
└── val/*.txt
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 关于「标签污染审计」
|
|
147
|
+
|
|
148
|
+
这是本项目最有价值的方法论。核心结论:
|
|
149
|
+
|
|
150
|
+
> 自动化的「模型否决式」标注清洗会误删大量真目标;且**用清洗模型自己验证自己**会导致
|
|
151
|
+
> 指标系统性虚高 / 失真。审计必须用**独立模型**交叉回检。
|
|
152
|
+
|
|
153
|
+
`audit` 命令就是这套方法的可复现工具——用独立模型回检标注,输出三类可疑样本
|
|
154
|
+
(漏标 / 错框 / 预测无对应)及可视化对比图。
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 开发
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# 可编辑安装
|
|
162
|
+
pip install -e .
|
|
163
|
+
|
|
164
|
+
# 构建分发包
|
|
165
|
+
pip install build && python -m build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 页面文件是「两份」,改完必须同步
|
|
169
|
+
|
|
170
|
+
监控台页面的**唯一源文件**是仓库里的 `web/index.html`;包里还有一份副本
|
|
171
|
+
(`phone_detect_test/web/index.html`),因为 setuptools 不能把包目录之外的文件打进 wheel。
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python packaging/sync_web.py # 改完 web/index.html 后同步进包
|
|
175
|
+
python packaging/sync_web.py --check # 发布前校验两份是否一致(不一致退出码 1)
|
|
176
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""phone-detect-test:考场手机检测工具。
|
|
2
|
+
|
|
3
|
+
一条命令启动监控台(网页 + 推理接口,同端口):
|
|
4
|
+
phone-detect-test web
|
|
5
|
+
|
|
6
|
+
也可作为 CLI 工具,覆盖目标检测任务的完整生命周期:
|
|
7
|
+
检测(detect)、训练(train)、评估(val)、数据集整理(prepare)、
|
|
8
|
+
数据清洗(clean)、标签污染审计(audit)、正负样本重采样(resample)、
|
|
9
|
+
离线评分(evaluate)、权重下载(download-weights)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""audit:标签污染审计。
|
|
3
|
+
|
|
4
|
+
这是本项目论文的核心方法:用「一个独立模型」回检已有标注,找出三类可疑样本——
|
|
5
|
+
1. missing_gt :无标注但模型高置信检出 -> 可能漏标
|
|
6
|
+
2. gt_unmatched :有标注但 GT 框没有任何预测匹配 -> 可能错误标注
|
|
7
|
+
3. pred_unmatched:模型高置信检出但 GT 无对应 -> 可能漏标
|
|
8
|
+
|
|
9
|
+
关键原则(来自论文结论):审计用的模型必须「独立于」当初做标注/清洗的模型,
|
|
10
|
+
否则会陷入"自己验证自己"的循环,指标失真。
|
|
11
|
+
"""
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .utils import IMG_EXTS, pick_device, resolve_weights
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _compute_iou(box1, box2):
|
|
19
|
+
"""box = (xc, yc, w, h) 归一化。"""
|
|
20
|
+
x1min, y1min = box1[0] - box1[2] / 2, box1[1] - box1[3] / 2
|
|
21
|
+
x1max, y1max = box1[0] + box1[2] / 2, box1[1] + box1[3] / 2
|
|
22
|
+
x2min, y2min = box2[0] - box2[2] / 2, box2[1] - box2[3] / 2
|
|
23
|
+
x2max, y2max = box2[0] + box2[2] / 2, box2[1] + box2[3] / 2
|
|
24
|
+
ix1, iy1 = max(x1min, x2min), max(y1min, y2min)
|
|
25
|
+
ix2, iy2 = min(x1max, x2max), min(y1max, y2max)
|
|
26
|
+
inter = max(0, ix2 - ix1) * max(0, iy2 - iy1)
|
|
27
|
+
a1 = (x1max - x1min) * (y1max - y1min)
|
|
28
|
+
a2 = (x2max - x2min) * (y2max - y2min)
|
|
29
|
+
union = a1 + a2 - inter
|
|
30
|
+
return inter / union if union > 0 else 0.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_gt(label_path, class_id):
|
|
34
|
+
boxes = []
|
|
35
|
+
p = Path(label_path)
|
|
36
|
+
if not p.exists():
|
|
37
|
+
return boxes
|
|
38
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
39
|
+
parts = line.strip().split()
|
|
40
|
+
if len(parts) < 5:
|
|
41
|
+
continue
|
|
42
|
+
if int(parts[0]) != class_id:
|
|
43
|
+
continue
|
|
44
|
+
xc, yc, w, h = map(float, parts[1:5])
|
|
45
|
+
boxes.append((xc, yc, w, h))
|
|
46
|
+
return boxes
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run(args):
|
|
50
|
+
from ultralytics import YOLO
|
|
51
|
+
|
|
52
|
+
images_dir = Path(args.images)
|
|
53
|
+
labels_dir = Path(args.labels)
|
|
54
|
+
out_dir = Path(args.out_dir)
|
|
55
|
+
vis_dir = out_dir / "_pred_visual"
|
|
56
|
+
if not images_dir.exists():
|
|
57
|
+
raise SystemExit(f"图片目录不存在:{images_dir}")
|
|
58
|
+
if not labels_dir.exists():
|
|
59
|
+
raise SystemExit(f"标签目录不存在:{labels_dir}")
|
|
60
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
vis_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
weights, _ = resolve_weights(args.weights,
|
|
64
|
+
weights_url=getattr(args, "weights_url", None))
|
|
65
|
+
device = pick_device(args.device)
|
|
66
|
+
model = YOLO(weights)
|
|
67
|
+
print(f"[审计] 模型 {weights} | 设备 {device}")
|
|
68
|
+
print(f"[审计] 类别 id={args.class_id} | 独立回检阈值 conf>={args.conf_pred_no_gt}")
|
|
69
|
+
|
|
70
|
+
stats = {"total": 0, "missing_gt": 0, "gt_unmatched": 0, "pred_unmatched": 0}
|
|
71
|
+
records = []
|
|
72
|
+
|
|
73
|
+
for img_path in sorted(images_dir.iterdir()):
|
|
74
|
+
if img_path.suffix.lower() not in IMG_EXTS:
|
|
75
|
+
continue
|
|
76
|
+
stats["total"] += 1
|
|
77
|
+
label_path = labels_dir / f"{img_path.stem}.txt"
|
|
78
|
+
|
|
79
|
+
r = model(str(img_path), conf=args.conf, verbose=False)[0]
|
|
80
|
+
pred_boxes = []
|
|
81
|
+
for i in range(len(r.boxes)):
|
|
82
|
+
if int(r.boxes.cls[i]) != args.class_id:
|
|
83
|
+
continue
|
|
84
|
+
conf = float(r.boxes.conf[i])
|
|
85
|
+
xc, yc, w, h = r.boxes.xywhn[i].tolist()
|
|
86
|
+
pred_boxes.append((conf, xc, yc, w, h))
|
|
87
|
+
|
|
88
|
+
gt_boxes = _read_gt(label_path, args.class_id)
|
|
89
|
+
reason = None
|
|
90
|
+
|
|
91
|
+
if len(gt_boxes) == 0:
|
|
92
|
+
if any(p[0] >= args.conf_pred_no_gt for p in pred_boxes):
|
|
93
|
+
reason = "missing_gt"
|
|
94
|
+
stats["missing_gt"] += 1
|
|
95
|
+
else:
|
|
96
|
+
for gt in gt_boxes:
|
|
97
|
+
max_iou = max((_compute_iou(gt, p[1:]) for p in pred_boxes), default=0)
|
|
98
|
+
if max_iou < args.iou_gt_no_pred:
|
|
99
|
+
reason = "gt_unmatched"
|
|
100
|
+
stats["gt_unmatched"] += 1
|
|
101
|
+
break
|
|
102
|
+
if reason is None:
|
|
103
|
+
for conf, box in [ (p[0], p[1:]) for p in pred_boxes ]:
|
|
104
|
+
if conf >= args.conf_pred_no_gt:
|
|
105
|
+
max_iou = max((_compute_iou(box, gt) for gt in gt_boxes), default=0)
|
|
106
|
+
if max_iou < args.match_iou:
|
|
107
|
+
reason = "pred_unmatched"
|
|
108
|
+
stats["pred_unmatched"] += 1
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
if reason is not None:
|
|
112
|
+
records.append((img_path.name, reason))
|
|
113
|
+
shutil.copy2(img_path, out_dir / img_path.name)
|
|
114
|
+
if label_path.exists():
|
|
115
|
+
shutil.copy2(label_path, out_dir / label_path.name)
|
|
116
|
+
_draw_compare(img_path, gt_boxes, pred_boxes, vis_dir)
|
|
117
|
+
|
|
118
|
+
_write_summary(stats, records, out_dir)
|
|
119
|
+
print(f"\n[完成] 总图片 {stats['total']},可疑 {len(records)} 个")
|
|
120
|
+
print(f" 漏标 {stats['missing_gt']} | GT无法匹配 {stats['gt_unmatched']} | 预测无对应 {stats['pred_unmatched']}")
|
|
121
|
+
print(f" 输出目录:{out_dir}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _draw_compare(img_path, gt_boxes, pred_boxes, vis_dir):
|
|
125
|
+
import cv2
|
|
126
|
+
img = cv2.imread(str(img_path))
|
|
127
|
+
if img is None:
|
|
128
|
+
return
|
|
129
|
+
h, w = img.shape[:2]
|
|
130
|
+
for (xc, yc, bw, bh) in gt_boxes:
|
|
131
|
+
x1, y1 = int((xc - bw / 2) * w), int((yc - bh / 2) * h)
|
|
132
|
+
x2, y2 = int((xc + bw / 2) * w), int((yc + bh / 2) * h)
|
|
133
|
+
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
134
|
+
for (conf, xc, yc, bw, bh) in pred_boxes:
|
|
135
|
+
x1, y1 = int((xc - bw / 2) * w), int((yc - bh / 2) * h)
|
|
136
|
+
x2, y2 = int((xc + bw / 2) * w), int((yc + bh / 2) * h)
|
|
137
|
+
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
|
|
138
|
+
cv2.putText(img, f"{conf:.2f}", (x1, max(0, y1 - 5)),
|
|
139
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
|
|
140
|
+
cv2.imwrite(str(vis_dir / img_path.name), img)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _write_summary(stats, records, out_dir):
|
|
144
|
+
p = out_dir / "audit_report.txt"
|
|
145
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
146
|
+
f.write("标签污染审计报告\n" + "=" * 40 + "\n")
|
|
147
|
+
f.write(f"总图片: {stats['total']}\n")
|
|
148
|
+
f.write(f"可疑总数: {len(records)}\n")
|
|
149
|
+
f.write(f" - 缺失标注(漏标): {stats['missing_gt']}\n")
|
|
150
|
+
f.write(f" - GT无法匹配(错框): {stats['gt_unmatched']}\n")
|
|
151
|
+
f.write(f" - 预测无对应(漏标): {stats['pred_unmatched']}\n\n")
|
|
152
|
+
f.write("可疑图片列表:\n")
|
|
153
|
+
for name, reason in records:
|
|
154
|
+
f.write(f"{name}\t{reason}\n")
|
|
155
|
+
print(f"[报告] {p}")
|