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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
86c86b105c
commit
8dc2fe6a2e
@@ -587,6 +587,73 @@ runs/<name>/
|
||||
> 它们的框没有物理意义。真正有用的是 `blackdot`/`whitedot`/`mosquito`/`grease` 这些
|
||||
> 有局部目标的类别。
|
||||
|
||||
## 检测模型训练(YOLO)
|
||||
|
||||
上一步的产出按类别分目录,方便人工核对;检测训练要的是 train/val 划分——类别写在标签
|
||||
第一列,不靠目录名。所以先整理再训练:
|
||||
|
||||
```bash
|
||||
# 1) 拍平类别目录、分层切分、硬链接到 data/(不占额外磁盘)
|
||||
uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_gradcam
|
||||
# 先看计划加 --dry-run
|
||||
|
||||
# 2) 训练
|
||||
uv run python detect.py --config configs/detect/film.yaml
|
||||
|
||||
# 只评估 / 续训
|
||||
uv run python detect.py --config configs/detect/film.yaml --eval-only \
|
||||
--checkpoint runs/detect/film/weights/best.pt
|
||||
uv run python detect.py --config configs/detect/film.yaml --resume
|
||||
```
|
||||
|
||||
产出:
|
||||
|
||||
```
|
||||
data/boxes_gradcam/
|
||||
├── images/{train,val}/<图名>.png # 硬链接到 runs/,再到 data/raw/ 的原图
|
||||
├── labels/{train,val}/<图名>.txt
|
||||
├── data.yaml # path 写绝对路径,ultralytics 从哪跑都能读到
|
||||
└── summary.json # 划分明细、逐类数量、链接方式
|
||||
```
|
||||
|
||||
### 划分
|
||||
|
||||
按类别分层(`--val-ratio` 默认 0.2,`--seed` 默认 42,两次运行结果一致)。分层是必需的:
|
||||
`black` 只有 53 张、`others` 69 张,随机切很容易让某个小类在验证集里挂零,指标就成了空的。
|
||||
|
||||
无框的图(Grad-CAM 没框出东西的那 339 张,占 3%)**默认保留**——在检测训练里它们是负样本,
|
||||
对压低误检有用。不要时加 `--drop-empty`。
|
||||
|
||||
拍平之后不同类别目录下的同名文件会互相覆盖,脚本会直接报错而不是静默丢数据;
|
||||
真遇上就用 `scripts/export_clusters.py --prefix-parent` 重新导出。
|
||||
|
||||
### 模型与增强的取舍
|
||||
|
||||
- **选 YOLOv8 而不是 v11/v12**:CLAUDE.md 限定只用 convolution 模型,而 YOLO11 骨架里有
|
||||
C2PSA(position-sensitive attention)、YOLO12 更是以注意力为主。YOLOv8 的 C2f + SPPF 是纯卷积。
|
||||
`s` 是精度/速度折中——数据只有 1.1 万张且标签是弱监督的,模型再大容易把标注噪声一起学进去。
|
||||
- **关掉 mosaic / mixup**:它们把四张图拼一起,而我们每张图只有一个弱监督框,拼接后小目标被
|
||||
缩得更小,对 `blackdot`/`whitedot` 这类本就只占千分之几面积的类别是净伤害。
|
||||
- **关掉 hsv_h**:薄膜图近乎单色,色相扰动没有意义。翻转照旧开着(缺陷没有固定朝向)。
|
||||
- 冒烟测试用 `--set train.epochs=1 train.fraction=0.02 train.imgsz=320` 先把链路跑通。
|
||||
|
||||
> **指标怎么读。** 框来自 Grad-CAM/SHAP 的弱监督标注,不是人工真值。mAP 衡量的是"检测器
|
||||
> 能不能复现归因热力区域",不等于"缺陷定位有多准"。真值上的精度仍需人工抽检。
|
||||
|
||||
### 可视化
|
||||
|
||||
`train.swanlab.enabled: true` 时接 SwanLab 回调,训练曲线实时可看(`swanlab watch` 开本地看板)。
|
||||
ultralytics 自己还会在 `runs/detect/film/` 下写:
|
||||
|
||||
- `results.png` / `results.csv`——各项 loss 与 mAP 曲线
|
||||
- `confusion_matrix.png`、`confusion_matrix_normalized.png`
|
||||
- `BoxPR_curve.png` / `BoxP_curve.png` / `BoxR_curve.png` / `BoxF1_curve.png`
|
||||
- `train_batch*.jpg`、`val_batch*_labels.jpg` 与 `val_batch*_pred.jpg`——标注与预测的直观对照
|
||||
- `metrics.json`——mAP50-95 / mAP50 / P / R 与逐类 mAP50
|
||||
|
||||
> **许可提醒。** ultralytics 是 AGPL-3.0。模型若要随产品分发,需要开源整套代码或另购商业授权;
|
||||
> 仅内部使用不受影响。
|
||||
|
||||
## 方法
|
||||
|
||||
- **骨架**:`tiny_conv`(自研深度可分离卷积,默认)或 `torchvision`(resnet / mobilenet / shufflenet)。
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 在 Grad-CAM / SHAP 生成的弱监督框上训练 YOLO 检测模型。
|
||||
#
|
||||
# 前置步骤:
|
||||
# uv run python scripts/generate_boxes.py --config ... --method gradcam # 产出 runs/boxes_gradcam
|
||||
# uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_gradcam
|
||||
#
|
||||
# 用法:
|
||||
# uv run python detect.py --config configs/detect/film.yaml
|
||||
# uv run python detect.py --config configs/detect/film.yaml --eval-only \
|
||||
# --checkpoint runs/detect/film/weights/best.pt
|
||||
|
||||
work_dir: runs/detect/film
|
||||
seed: 42
|
||||
device: auto
|
||||
|
||||
data:
|
||||
yaml: data/boxes_gradcam/data.yaml
|
||||
|
||||
model:
|
||||
# 选 v8 而不是 v11/v12:CLAUDE.md 限定只用 convolution 模型,
|
||||
# 而 YOLO11 的骨架里有 C2PSA(position-sensitive attention)、YOLO12 更是以注意力为主。
|
||||
# YOLOv8 的 C2f + SPPF 是纯卷积。
|
||||
# s 是精度/速度的折中;数据只有 1.1 万张且标签是弱监督的,再大容易把标注噪声也学进去。
|
||||
weights: yolov8s.pt
|
||||
|
||||
train:
|
||||
epochs: 100
|
||||
imgsz: 640
|
||||
batch: 16
|
||||
workers: 8
|
||||
optimizer: auto
|
||||
patience: 20 # 20 轮没提升就早停
|
||||
cos_lr: true
|
||||
fraction: 1.0 # 只取训练集的一部分;冒烟测试时调小,如 0.02
|
||||
amp: true # 非 CUDA 后端 ultralytics 会自行退回关闭
|
||||
|
||||
# 数据增强。翻转按聚类阶段的结论开着(缺陷没有固定朝向);
|
||||
# 但 mosaic/mixup 关掉——它们会把四张图拼在一起,而我们每张图只有一个弱监督框,
|
||||
# 拼接后小目标被缩得更小,对 blackdot / whitedot 这类本来就只占千分之几面积的类别是伤害。
|
||||
fliplr: 0.5
|
||||
flipud: 0.5
|
||||
degrees: 0.0
|
||||
mosaic: 0.0
|
||||
mixup: 0.0
|
||||
hsv_h: 0.0 # 单色/近单色的薄膜图,色相扰动没有意义
|
||||
hsv_s: 0.3
|
||||
hsv_v: 0.3
|
||||
|
||||
plots: true # results.png / PR 曲线 / 混淆矩阵 / val_batch 预测图
|
||||
save_period: -1
|
||||
|
||||
swanlab:
|
||||
enabled: true
|
||||
project: film-detect
|
||||
name: null
|
||||
mode: local
|
||||
tags: [detect, yolov8s, gradcam-boxes]
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
"""在弱监督框标签上训练 YOLO 检测模型。
|
||||
|
||||
uv run python detect.py --config configs/detect/film.yaml
|
||||
uv run python detect.py --config configs/detect/film.yaml --eval-only \
|
||||
--checkpoint runs/detect/film/weights/best.pt
|
||||
|
||||
数据是 scripts/prepare_yolo_dataset.py 的产物(train/val 已划分好)。
|
||||
框来自 Grad-CAM / SHAP 的弱监督标注,不是人工标注——指标该按"与热力区域的一致性"
|
||||
来读,而不是当成真值上的检测精度。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from utils import load_config, seed_everything, setup_logging
|
||||
from utils.device import DeviceNotAvailableError, describe_device, resolve_device
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="薄膜缺陷检测(YOLO)训练/评估")
|
||||
parser.add_argument("--config", required=True, help="yaml 配置路径")
|
||||
parser.add_argument("--set", nargs="*", default=[], metavar="KEY=VALUE", help="覆盖配置项")
|
||||
parser.add_argument("--work-dir", default=None)
|
||||
parser.add_argument("--device", default=None, help="auto / cuda / mps / cpu")
|
||||
parser.add_argument("--resume", action="store_true", help="从 work_dir 里的 last.pt 续训")
|
||||
parser.add_argument("--eval-only", action="store_true", help="只在验证集上评估")
|
||||
parser.add_argument("--checkpoint", default=None, help="评估/微调用的权重 .pt")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def to_ultralytics_device(device) -> str | int:
|
||||
"""torch.device -> ultralytics 的 device 记法(cuda 用序号,其余用字符串)。"""
|
||||
if device.type == "cuda":
|
||||
return device.index or 0
|
||||
return device.type
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
cfg = load_config(args.config, overrides=args.set)
|
||||
if args.work_dir:
|
||||
cfg["work_dir"] = args.work_dir
|
||||
work_dir = Path(cfg.get("work_dir", "runs/detect"))
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger = setup_logging(log_file=work_dir / "detect.log")
|
||||
seed = int(cfg.get("seed", 42))
|
||||
seed_everything(seed)
|
||||
|
||||
try:
|
||||
device = resolve_device(args.device or cfg.get("device", "auto"))
|
||||
except DeviceNotAvailableError as exc:
|
||||
logger.error("%s", exc)
|
||||
return 1
|
||||
logger.info("设备: %s", describe_device(device))
|
||||
|
||||
data_yaml = Path(cfg["data"]["yaml"])
|
||||
if not data_yaml.exists():
|
||||
logger.error(
|
||||
"找不到 %s;先跑 scripts/prepare_yolo_dataset.py 把 runs/ 下的产出链接到 data/", data_yaml
|
||||
)
|
||||
return 1
|
||||
meta = yaml.safe_load(data_yaml.read_text(encoding="utf-8"))
|
||||
logger.info("数据集: %s(%d 类)", data_yaml, meta["nc"])
|
||||
|
||||
train_cfg = dict(cfg.get("train", {}))
|
||||
swanlab_cfg = dict(train_cfg.pop("swanlab", {}))
|
||||
|
||||
from ultralytics import YOLO
|
||||
from ultralytics import settings as ul_settings
|
||||
|
||||
# ultralytics 默认把数据集根指到 ~/datasets 之类的全局目录;我们的 data.yaml 里写的是
|
||||
# 绝对路径,但它仍会拿 datasets_dir 去拼相对路径,统一指到项目根省得踩坑。
|
||||
ul_settings.update({"datasets_dir": str(Path.cwd())})
|
||||
|
||||
weights = args.checkpoint or cfg["model"]["weights"]
|
||||
logger.info("模型: %s", weights)
|
||||
model = YOLO(weights)
|
||||
|
||||
if swanlab_cfg.pop("enabled", False):
|
||||
try:
|
||||
from swanlab.integration.ultralytics import add_swanlab_callback
|
||||
|
||||
os.environ.setdefault("SWANLAB_PROJ_NAME", str(swanlab_cfg.get("project", "film-detect")))
|
||||
import swanlab
|
||||
|
||||
swanlab.init(
|
||||
project=swanlab_cfg.get("project", "film-detect"),
|
||||
name=swanlab_cfg.get("name") or work_dir.name,
|
||||
mode=swanlab_cfg.get("mode", "local"),
|
||||
config=cfg,
|
||||
log_dir=str(work_dir / "swanlog"),
|
||||
tags=swanlab_cfg.get("tags") or [],
|
||||
)
|
||||
add_swanlab_callback(model)
|
||||
logger.info("SwanLab 已接入 YOLO 回调")
|
||||
except Exception as exc: # 可视化失败不该挡住训练
|
||||
logger.warning("SwanLab 接入失败,训练继续: %s", exc)
|
||||
|
||||
# ultralytics 自己管 project/name 目录。project 必须给绝对路径:相对路径会被它再拼到
|
||||
# 全局 settings 的 runs_dir 上,产出就跑到 runs/detect/runs/detect/<name> 去了。
|
||||
common = {
|
||||
"data": str(data_yaml.resolve()),
|
||||
"device": to_ultralytics_device(device),
|
||||
"project": str(work_dir.resolve().parent),
|
||||
"name": work_dir.name,
|
||||
"exist_ok": True,
|
||||
"seed": seed,
|
||||
}
|
||||
|
||||
if args.eval_only:
|
||||
metrics = model.val(split="val", plots=True, **common)
|
||||
else:
|
||||
model.train(resume=args.resume, **common, **train_cfg)
|
||||
# train 结束后 ultralytics 已在最优权重上跑过一次 val,这里显式再跑一次拿回指标对象
|
||||
metrics = model.val(split="val", plots=True, **common)
|
||||
|
||||
summary = {
|
||||
"map50_95": float(metrics.box.map),
|
||||
"map50": float(metrics.box.map50),
|
||||
"precision": float(metrics.box.mp),
|
||||
"recall": float(metrics.box.mr),
|
||||
"per_class_map50": {
|
||||
meta["names"][int(c)]: float(v)
|
||||
for c, v in zip(metrics.box.ap_class_index, metrics.box.ap50, strict=False)
|
||||
},
|
||||
}
|
||||
logger.info(
|
||||
"mAP50-95 %.4f | mAP50 %.4f | P %.4f | R %.4f",
|
||||
summary["map50_95"], summary["map50"], summary["precision"], summary["recall"],
|
||||
)
|
||||
(work_dir / "metrics.json").write_text(
|
||||
json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
logger.info(
|
||||
"曲线与混淆矩阵见 %s(results.png / *_curve.png / confusion_matrix*.png)",
|
||||
work_dir.resolve(),
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -15,6 +15,11 @@ dependencies = [
|
||||
"pyyaml>=6.0",
|
||||
"swanlab[dashboard]>=0.10.0",
|
||||
"matplotlib>=3.8",
|
||||
# 检测训练(detect.py)。注意 ultralytics 是 AGPL-3.0:如果模型要随产品分发,
|
||||
# 需要开源整套代码或另外向其购买商业授权。仅内部使用则不受影响。
|
||||
"ultralytics>=8.4",
|
||||
# SHAP 定位(scripts/generate_boxes.py --method shap)用
|
||||
"shap>=0.46",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python
|
||||
"""把 generate_boxes.py 的产出整理成 YOLO 训练所需的数据集,放到 data/ 下。
|
||||
|
||||
uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_gradcam
|
||||
|
||||
generate_boxes.py 的产出是**按类别**分目录的(便于人工核对):
|
||||
|
||||
```
|
||||
runs/boxes_gradcam/
|
||||
├── images/<类别>/<图名>.png
|
||||
└── labels/<类别>/<图名>.txt
|
||||
```
|
||||
|
||||
而 YOLO 检测训练要的是 **train/val 划分**——类别信息在标签文件的第一列里,
|
||||
不靠目录名。这一步做三件事:
|
||||
|
||||
1. 把按类别分的目录拍平,按 train/val 重新组织;
|
||||
2. **按类别分层**切分,小类不至于在验证集里挂零;
|
||||
3. 全部用**硬链接**,不占额外磁盘。
|
||||
|
||||
```
|
||||
data/<name>/
|
||||
├── images/{train,val}/<图名>.png
|
||||
├── labels/{train,val}/<图名>.txt
|
||||
└── data.yaml
|
||||
```
|
||||
|
||||
没有框的图(标签为空)默认保留:YOLO 训练里它们是负样本,有助于压低误检。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import yaml # noqa: E402
|
||||
|
||||
from datasets.yolo import IMAGE_SUFFIXES, write_data_yaml # noqa: E402
|
||||
from utils import setup_logging # noqa: E402
|
||||
|
||||
logger = None
|
||||
|
||||
|
||||
def link_or_copy(source: Path, destination: Path) -> str:
|
||||
"""硬链接;跨文件系统时退回复制。"""
|
||||
if destination.exists() or destination.is_symlink():
|
||||
destination.unlink()
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
os.link(source, destination)
|
||||
return "hard"
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.EXDEV:
|
||||
raise
|
||||
shutil.copy2(source, destination)
|
||||
return "copy"
|
||||
|
||||
|
||||
def first_class_id(label_path: Path) -> int | None:
|
||||
"""取标签里第一个框的类别下标;空标签(无框)返回 None。"""
|
||||
for line in label_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
return int(float(line.split()[0]))
|
||||
return None
|
||||
|
||||
|
||||
def collect(source: Path) -> list[tuple[Path, Path, int | None]]:
|
||||
"""配对 images/ 与 labels/,返回 (图片, 标签, 类别下标) 列表。"""
|
||||
images_root, labels_root = source / "images", source / "labels"
|
||||
if not images_root.is_dir() or not labels_root.is_dir():
|
||||
raise FileNotFoundError(f"{source} 下缺少 images/ 或 labels/,先跑 scripts/generate_boxes.py")
|
||||
|
||||
pairs = []
|
||||
missing = []
|
||||
for image in sorted(p for p in images_root.rglob("*") if p.suffix.lower() in IMAGE_SUFFIXES):
|
||||
label = labels_root / image.relative_to(images_root).with_suffix(".txt")
|
||||
if not label.exists():
|
||||
missing.append(image)
|
||||
continue
|
||||
pairs.append((image, label, first_class_id(label)))
|
||||
if missing:
|
||||
raise FileNotFoundError(
|
||||
f"有 {len(missing)} 张图没有对应的标签,例如 {missing[0]};"
|
||||
"产出目录可能不完整,重新跑一次 generate_boxes.py"
|
||||
)
|
||||
if not pairs:
|
||||
raise ValueError(f"{source} 下没有找到任何图片")
|
||||
return pairs
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global logger
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument("--source", required=True, help="generate_boxes.py 的产出目录")
|
||||
parser.add_argument("--out", default=None, help="输出目录;缺省 data/<source 目录名>")
|
||||
parser.add_argument("--data-root", default="data", help="自动推导时的根目录,默认 data")
|
||||
parser.add_argument("--val-ratio", type=float, default=0.2)
|
||||
parser.add_argument("--seed", type=int, default=42)
|
||||
parser.add_argument(
|
||||
"--drop-empty", action="store_true",
|
||||
help="丢弃没有框的图。默认保留——YOLO 训练里它们是负样本,有助于压低误检",
|
||||
)
|
||||
parser.add_argument("--overwrite", action="store_true", help="输出目录已存在时先清空")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只打印计划")
|
||||
args = parser.parse_args()
|
||||
|
||||
source = Path(args.source)
|
||||
out_dir = Path(args.out) if args.out else Path(args.data_root) / source.name
|
||||
logger = setup_logging()
|
||||
|
||||
try:
|
||||
pairs = collect(source)
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
logger.error("%s", exc)
|
||||
return 1
|
||||
|
||||
meta_path = source / "data.yaml"
|
||||
if not meta_path.exists():
|
||||
logger.error("找不到 %s", meta_path)
|
||||
return 1
|
||||
meta = yaml.safe_load(meta_path.read_text(encoding="utf-8"))
|
||||
names = [meta["names"][i] for i in sorted(meta["names"])]
|
||||
|
||||
empty = [p for p in pairs if p[2] is None]
|
||||
if args.drop_empty:
|
||||
pairs = [p for p in pairs if p[2] is not None]
|
||||
logger.info("来源: %s(%d 张,%d 类)", source, len(pairs), len(names))
|
||||
logger.info(
|
||||
"无框图片 %d 张:%s", len(empty), "已丢弃" if args.drop_empty else "保留为负样本"
|
||||
)
|
||||
|
||||
# 按类别分层切分;无框的图单独作为一组分配,避免它们全落进同一侧
|
||||
grouped: dict[int | None, list] = defaultdict(list)
|
||||
for pair in pairs:
|
||||
grouped[pair[2]].append(pair)
|
||||
|
||||
rng = random.Random(args.seed)
|
||||
splits: dict[str, list] = {"train": [], "val": []}
|
||||
for _class_id, group in sorted(grouped.items(), key=lambda kv: (kv[0] is None, kv[0])):
|
||||
order = group[:]
|
||||
rng.shuffle(order)
|
||||
n_val = max(1, round(len(order) * args.val_ratio)) if len(order) > 1 else 0
|
||||
splits["val"].extend(order[:n_val])
|
||||
splits["train"].extend(order[n_val:])
|
||||
|
||||
counts = {
|
||||
split: Counter(names[c] if c is not None else "(无框)" for _, _, c in items)
|
||||
for split, items in splits.items()
|
||||
}
|
||||
logger.info("划分: train %d / val %d", len(splits["train"]), len(splits["val"]))
|
||||
logger.info("%-14s %8s %8s", "类别", "train", "val")
|
||||
for name in names + ["(无框)"]:
|
||||
if counts["train"][name] or counts["val"][name]:
|
||||
logger.info("%-14s %8d %8d", name, counts["train"][name], counts["val"][name])
|
||||
|
||||
if args.dry_run:
|
||||
logger.info("--dry-run:未写入任何文件")
|
||||
return 0
|
||||
|
||||
if out_dir.exists() and any(out_dir.iterdir()):
|
||||
if not args.overwrite:
|
||||
logger.error("%s 已存在且非空;确认要覆盖请加 --overwrite", out_dir)
|
||||
return 1
|
||||
shutil.rmtree(out_dir)
|
||||
|
||||
modes: Counter[str] = Counter()
|
||||
seen: dict[str, Path] = {}
|
||||
for split, items in splits.items():
|
||||
for image, label, _ in items:
|
||||
# 拍平之后不同类别目录下的同名文件会撞车,显式挡住
|
||||
if image.name in seen:
|
||||
logger.error(
|
||||
"拍平后文件名冲突:%s 与 %s 同名。源数据集的文件名需要全局唯一,"
|
||||
"用 scripts/export_clusters.py --prefix-parent 重新导出即可",
|
||||
seen[image.name], image,
|
||||
)
|
||||
return 1
|
||||
seen[image.name] = image
|
||||
modes[link_or_copy(image, out_dir / "images" / split / image.name)] += 1
|
||||
link_or_copy(label, out_dir / "labels" / split / f"{image.stem}.txt")
|
||||
|
||||
write_data_yaml(out_dir / "data.yaml", names, root=out_dir.resolve(),
|
||||
train="images/train", val="images/val")
|
||||
(out_dir / "summary.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"source": str(source.resolve()),
|
||||
"num_images": len(pairs),
|
||||
"empty_labels": len(empty),
|
||||
"dropped_empty": args.drop_empty,
|
||||
"val_ratio": args.val_ratio,
|
||||
"seed": args.seed,
|
||||
"split_sizes": {k: len(v) for k, v in splits.items()},
|
||||
"per_class": {k: dict(v) for k, v in counts.items()},
|
||||
"link_modes": dict(modes),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
logger.info("链接方式: %s", dict(modes))
|
||||
logger.info("完成 -> %s", out_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -139,7 +139,13 @@ def test_every_shipped_config_builds_its_components(config_path):
|
||||
|
||||
# 分类配置
|
||||
model_cfg = cfg.get("model")
|
||||
if model_cfg:
|
||||
if model_cfg and "weights" in model_cfg:
|
||||
# 检测配置:model 是 ultralytics 的权重名(yolov8s.pt),不走本仓库的注册表。
|
||||
# 只校验它是纯卷积的那一档——CLAUDE.md 限定只用 convolution 模型,
|
||||
# YOLO11 的 C2PSA 与 YOLO12 都带注意力。
|
||||
assert "yolo11" not in model_cfg["weights"]
|
||||
assert "yolo12" not in model_cfg["weights"]
|
||||
elif model_cfg:
|
||||
import torch
|
||||
|
||||
from models import build_model
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"""把 generate_boxes.py 的产出整理成 YOLO 训练集。"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("PIL")
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
import yaml # noqa: E402
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
sys.path.insert(0, "scripts")
|
||||
import prepare_yolo_dataset as pyd # noqa: E402
|
||||
|
||||
CLASSES = ["grease", "normal", "whitedot"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def source(tmp_path):
|
||||
"""模拟 generate_boxes.py 的产出:images/<类别>/ + labels/<类别>/ + data.yaml。"""
|
||||
root = tmp_path / "runs" / "boxes"
|
||||
rng = np.random.default_rng(0)
|
||||
# whitedot 只有 2 张(小类),normal 里有 1 张无框(负样本)
|
||||
for class_id, (name, count) in enumerate(zip(CLASSES, (6, 5, 2), strict=True)):
|
||||
image_dir = root / "images" / name
|
||||
label_dir = root / "labels" / name
|
||||
image_dir.mkdir(parents=True)
|
||||
label_dir.mkdir(parents=True)
|
||||
for i in range(count):
|
||||
stem = f"{name}_{i}"
|
||||
Image.fromarray(rng.integers(0, 255, (32, 32, 3), dtype=np.uint8)).save(
|
||||
image_dir / f"{stem}.png"
|
||||
)
|
||||
empty = name == "normal" and i == 0
|
||||
(label_dir / f"{stem}.txt").write_text(
|
||||
"" if empty else f"{class_id} 0.5 0.5 0.2 0.2\n", encoding="utf-8"
|
||||
)
|
||||
(root / "data.yaml").write_text(
|
||||
yaml.safe_dump({"nc": len(CLASSES), "names": dict(enumerate(CLASSES))}), encoding="utf-8"
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
def run(source, out, *extra):
|
||||
argv = ["prepare_yolo_dataset.py", "--source", str(source), "--out", str(out), *extra]
|
||||
original, sys.argv = sys.argv, argv
|
||||
try:
|
||||
return pyd.main()
|
||||
finally:
|
||||
sys.argv = original
|
||||
|
||||
|
||||
def test_flattens_classes_and_splits(source, tmp_path):
|
||||
"""类别目录被拍平,train/val 分开,图片与标签一一对应。"""
|
||||
out = tmp_path / "data" / "boxes"
|
||||
assert run(source, out) == 0
|
||||
|
||||
for split in ("train", "val"):
|
||||
images = sorted(p.stem for p in (out / "images" / split).iterdir())
|
||||
labels = sorted(p.stem for p in (out / "labels" / split).iterdir())
|
||||
assert images == labels
|
||||
total = sum(len(list((out / "images" / s).iterdir())) for s in ("train", "val"))
|
||||
assert total == 13
|
||||
# 拍平后不应再有 <类别>/ 这一层
|
||||
assert not (out / "images" / "train" / "grease").exists()
|
||||
|
||||
|
||||
def test_hard_links_share_inode(source, tmp_path):
|
||||
"""用硬链接而不是复制,不占额外磁盘。"""
|
||||
out = tmp_path / "data" / "boxes"
|
||||
assert run(source, out) == 0
|
||||
linked = next((out / "images" / "train").iterdir())
|
||||
original = next(p for p in (source / "images").rglob("*.png") if p.name == linked.name)
|
||||
assert linked.stat().st_ino == original.stat().st_ino
|
||||
|
||||
|
||||
def test_small_class_present_in_both_splits(source, tmp_path):
|
||||
"""只有 2 张的小类也要在验证集里留下一张,否则它的指标是空的。"""
|
||||
out = tmp_path / "data" / "boxes"
|
||||
assert run(source, out) == 0
|
||||
per_class = json.loads((out / "summary.json").read_text(encoding="utf-8"))["per_class"]
|
||||
assert per_class["train"]["whitedot"] == 1
|
||||
assert per_class["val"]["whitedot"] == 1
|
||||
|
||||
|
||||
def test_empty_labels_kept_as_negatives_by_default(source, tmp_path):
|
||||
"""无框的图默认保留——YOLO 里它们是压低误检的负样本。"""
|
||||
out = tmp_path / "data" / "boxes"
|
||||
assert run(source, out) == 0
|
||||
summary = json.loads((out / "summary.json").read_text(encoding="utf-8"))
|
||||
assert summary["empty_labels"] == 1
|
||||
assert summary["num_images"] == 13
|
||||
|
||||
out_dropped = tmp_path / "data" / "dropped"
|
||||
assert run(source, out_dropped, "--drop-empty") == 0
|
||||
dropped = json.loads((out_dropped / "summary.json").read_text(encoding="utf-8"))
|
||||
assert dropped["num_images"] == 12
|
||||
|
||||
|
||||
def test_data_yaml_uses_absolute_path_and_source_names(source, tmp_path):
|
||||
"""data.yaml 要能被 ultralytics 从任意工作目录读到,类别顺序沿用来源。"""
|
||||
out = tmp_path / "data" / "boxes"
|
||||
assert run(source, out) == 0
|
||||
meta = yaml.safe_load((out / "data.yaml").read_text(encoding="utf-8"))
|
||||
assert Path(meta["path"]).is_absolute()
|
||||
assert meta["nc"] == 3
|
||||
assert [meta["names"][i] for i in sorted(meta["names"])] == CLASSES
|
||||
assert (Path(meta["path"]) / meta["train"]).is_dir()
|
||||
assert (Path(meta["path"]) / meta["val"]).is_dir()
|
||||
|
||||
|
||||
def test_reproducible_split(source, tmp_path):
|
||||
"""同一个种子两次运行,划分必须一致。"""
|
||||
first, second = tmp_path / "a", tmp_path / "b"
|
||||
assert run(source, first) == 0
|
||||
assert run(source, second) == 0
|
||||
for split in ("train", "val"):
|
||||
assert sorted(p.name for p in (first / "images" / split).iterdir()) == sorted(
|
||||
p.name for p in (second / "images" / split).iterdir()
|
||||
)
|
||||
|
||||
|
||||
def test_name_collision_after_flattening_is_reported(source, tmp_path, capsys):
|
||||
"""不同类别目录下的同名文件,拍平后会互相覆盖——必须报错而不是静默丢数据。"""
|
||||
duplicate = source / "images" / "normal" / "grease_0.png"
|
||||
Image.fromarray(np.zeros((32, 32, 3), dtype=np.uint8)).save(duplicate)
|
||||
(source / "labels" / "normal" / "grease_0.txt").write_text("1 0.5 0.5 0.2 0.2\n", encoding="utf-8")
|
||||
|
||||
assert run(source, tmp_path / "data" / "boxes") != 0
|
||||
assert "冲突" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_refuses_to_clobber_existing_output(source, tmp_path):
|
||||
"""输出目录非空时要求显式 --overwrite。"""
|
||||
out = tmp_path / "data" / "boxes"
|
||||
out.mkdir(parents=True)
|
||||
(out / "keep.txt").write_text("x", encoding="utf-8")
|
||||
assert run(source, out) != 0
|
||||
assert (out / "keep.txt").exists()
|
||||
assert run(source, out, "--overwrite") == 0
|
||||
assert not (out / "keep.txt").exists()
|
||||
|
||||
|
||||
def test_missing_label_is_an_error(source, tmp_path):
|
||||
"""图片多于标签说明来源目录不完整,不能悄悄跳过。"""
|
||||
(source / "labels" / "grease" / "grease_0.txt").unlink()
|
||||
assert run(source, tmp_path / "out") != 0
|
||||
|
||||
|
||||
def test_dry_run_writes_nothing(source, tmp_path):
|
||||
out = tmp_path / "data" / "boxes"
|
||||
assert run(source, out, "--dry-run") == 0
|
||||
assert not out.exists()
|
||||
@@ -6,17 +6,20 @@ resolution-markers = [
|
||||
"python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'win32'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
|
||||
"(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.13' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.13' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.13' and sys_platform == 'emscripten'",
|
||||
"(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -467,6 +470,34 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.19"
|
||||
@@ -638,15 +669,18 @@ resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform == 'linux'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'win32'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
|
||||
"(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.13' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.13' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.13' and sys_platform == 'emscripten'",
|
||||
"(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/27/72ae94ea5c8f7349ec1c229d4cd058feb799cbd0833ad6d1b47c919b37b7/llvmlite-0.49.0.tar.gz", hash = "sha256:00f16db782f4a13c78c5804aedc434e46794a77e89999a168f9401106270e50a", size = 194467, upload-time = "2026-08-11T16:26:00.489Z" }
|
||||
wheels = [
|
||||
@@ -857,9 +891,11 @@ dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "shap" },
|
||||
{ name = "swanlab", extra = ["dashboard"] },
|
||||
{ name = "torch" },
|
||||
{ name = "torchvision" },
|
||||
{ name = "ultralytics" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -881,9 +917,11 @@ requires-dist = [
|
||||
{ name = "numpy", specifier = ">=1.26" },
|
||||
{ name = "pillow", specifier = ">=10.3" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "shap", specifier = ">=0.46" },
|
||||
{ name = "swanlab", extras = ["dashboard"], specifier = ">=0.10.0" },
|
||||
{ name = "torch", specifier = ">=2.4" },
|
||||
{ name = "torchvision", specifier = ">=0.19" },
|
||||
{ name = "ultralytics", specifier = ">=8.4" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -948,15 +986,18 @@ resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform == 'linux'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'win32'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
|
||||
"(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.13' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.13' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.13' and sys_platform == 'emscripten'",
|
||||
"(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "llvmlite", version = "0.49.0", source = { registry = "https://pypi.org/simple" } },
|
||||
@@ -1276,6 +1317,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/bd/a0c8e737b6afda10e42a597787d53d5b66e00268df6f59184701eeae37d9/onnxscript-0.7.1-py3-none-any.whl", hash = "sha256:544763b7fdef49940cdd9412ff5135cbae96d59ac6bc1921457f21280f40f4b7", size = 721970, upload-time = "2026-06-29T23:33:23.298Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python"
|
||||
version = "5.0.0.93"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.12.0"
|
||||
@@ -1481,6 +1541,34 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polars"
|
||||
version = "1.44.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "polars-runtime-32" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/15/e8541eefc22fbc7ca89bcb5112298a153729f73cfbc0cf6a668e509f975c/polars-1.44.2.tar.gz", hash = "sha256:86c8e26b6c2de8c8d344bb910b74dfc47b118ac3fe0f19b44909467990a0b281", size = 766272, upload-time = "2026-09-09T07:42:08.859Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6d/3014112c7f717d1253223faa13b6db3ac3a64ed00ab2a3bc1b942bc9cdd4/polars-1.44.2-py3-none-any.whl", hash = "sha256:1bb331f17a40d9d931101533dcd33637b66edc61eb377b07020dac16a0f0377b", size = 865768, upload-time = "2026-09-09T07:40:12.053Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polars-runtime-32"
|
||||
version = "1.44.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/a1/a7eace6587b56f22cf2a21ab4d5e695db372dc23fd96accb68b1ec12660b/polars_runtime_32-1.44.2.tar.gz", hash = "sha256:b84842f7d621aaca7a52e165e19a24f89db45f8aa13744941430218419a14a67", size = 3172205, upload-time = "2026-09-09T07:42:10.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5b/a5215f82c3dd443dc5d6911b0d3e937f97056e0ef7753f7e123422481a18/polars_runtime_32-1.44.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1fd536720668ba203a16a20b08cd6b23057e407a0279cf36b2f35f879d6e3208", size = 48334014, upload-time = "2026-09-09T07:40:16.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e0/f3dc93fce4b4e99370db6a89001a1b8d3c606e3560d0d91dda809d6c6324/polars_runtime_32-1.44.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0fd43720c8222ae39919c8ff891636d53b352706087120e62f83544dd3ff782", size = 43331099, upload-time = "2026-09-09T07:40:21.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/4f/076626ce93ddd622203c4b27be2a96d034cf5b24110c52e96e6029f0ea33/polars_runtime_32-1.44.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf9b45040291dc1c6c588c837019c33557bde25ec536562a9cca9e1f6dfcc45", size = 46590853, upload-time = "2026-09-09T07:40:24.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/24/ed9982657c446dd5491b089370eea196725673570cfc61f7225a9fdd7ef0/polars_runtime_32-1.44.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1bafb441e99199a62c63bf1bbdc0ea09ee9776dbac2bf31452b5000fb1df2f7", size = 49912258, upload-time = "2026-09-09T07:40:29.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/42/5490ab360aa2406119825ad82203a5e2ff27a3a5893ca8e0b93c053a59a3/polars_runtime_32-1.44.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10c0c695a418407617b5159db7d9a21074a733e4c6d61275b6762f25cb31ca99", size = 46737310, upload-time = "2026-09-09T07:40:33.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8f/d741afb1dcd1848161189e017d27972e7e78556d8dce66b94d4235093706/polars_runtime_32-1.44.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c4a09fb14aad711526346efc0cb2015c2fd0555ce4118b6524e5debbaea65ff5", size = 49887654, upload-time = "2026-09-09T07:40:37.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e7/c61c1c7eea37705920fe7c1302d1dd80d1165db2b928f0da3eae6d1ebb75/polars_runtime_32-1.44.2-cp310-abi3-win_amd64.whl", hash = "sha256:8598e7a20efba70bb74978c7df7af7c606ff4d79b9b48fdd808250b189bc9a13", size = 51297967, upload-time = "2026-09-09T07:40:41.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/a0/d0dd0d2ec95fa328dd47055905fae53ba3cd79f11c8973326ebe75a49e4c/polars_runtime_32-1.44.2-cp310-abi3-win_arm64.whl", hash = "sha256:d51040d3ab40157f6db3c62be59cab5b80fb3c8d158924769c4982a1c8eef730", size = 44303345, upload-time = "2026-09-09T07:40:47.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettytable"
|
||||
version = "3.18.0"
|
||||
@@ -1687,12 +1775,14 @@ resolution-markers = [
|
||||
"python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'win32'",
|
||||
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
|
||||
"(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.13' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.13' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.13' and sys_platform == 'emscripten'",
|
||||
"(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/e4/975f0fa77fc3590820b4a3ac49704644b389795409bc12eb91729f845812/pyroaring-1.0.3.tar.gz", hash = "sha256:cd7392d1c010c9e41c11c62cd0610c8852e7e9698b1f7f6c2fcdefe50e7ef6da", size = 188688, upload-time = "2025-10-09T09:08:22.448Z" }
|
||||
wheels = [
|
||||
@@ -1735,7 +1825,8 @@ resolution-markers = [
|
||||
"python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
|
||||
"python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/46/a50510d080f8cb089303ec0f7cd80736b2949ca3d148f48f1cc90c49e345/pyroaring-1.1.0.tar.gz", hash = "sha256:f02e4021397ae02a139defdc6813b9942ab163de90affddd4ce4efbac299f619", size = 200298, upload-time = "2026-04-24T21:29:25.212Z" }
|
||||
wheels = [
|
||||
@@ -2425,6 +2516,57 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b5/689613037fe691d18eae075cd141089f3a3156146be14512df92d8a9ae8f/ujson-5.13.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:9bd0f2dd05937c3b089af316884de18c6f6182ddb8ffce597d2e7c7a9ba9f447", size = 41802, upload-time = "2026-06-14T22:36:34.523Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ultralytics"
|
||||
version = "8.4.146"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cloudpickle" },
|
||||
{ name = "filelock" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "nvidia-ml-py" },
|
||||
{ name = "opencv-python" },
|
||||
{ name = "pillow" },
|
||||
{ name = "polars" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "torch" },
|
||||
{ name = "torchvision" },
|
||||
{ name = "ultralytics-platform" },
|
||||
{ name = "ultralytics-thop" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/fc/f6803e6f09f6253b178973c7c0bcda8c5713c2f773d44eb7be98742db6db/ultralytics-8.4.146.tar.gz", hash = "sha256:73e826b090d35af5a54130be1875d648146d689cf281313dd1a15bc85c557051", size = 1228037, upload-time = "2026-09-09T17:38:42.123Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/35/13cf829b59560f7579d0e0d6cc010bc9debf4b564a2810a172e6a8f04f96/ultralytics-8.4.146-py3-none-any.whl", hash = "sha256:5c3618f4c5f7b89ad6afdf789bf4d90645054f4eb78c211aa7eed1e186fa65fc", size = 1444902, upload-time = "2026-09-09T17:38:37.091Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ultralytics-platform"
|
||||
version = "0.1.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/65/139e40fede6d790ab08a00be42e94e9bd597df9286470dc9767387264aa4/ultralytics_platform-0.1.39.tar.gz", hash = "sha256:b60589a611c006a5335fff990e1a4606cb3980175700edb58cbf57c4c68c787c", size = 64212, upload-time = "2026-09-11T00:40:16.267Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/58/a2344cbc809be1e1278fa669e12e29e514ec7786530f4f7bd39a5756d1f2/ultralytics_platform-0.1.39-py3-none-any.whl", hash = "sha256:ec36ba498d2b094cd92f084ca3e017e7b62f987c42bccb9502664ad397a9022f", size = 75763, upload-time = "2026-09-11T00:40:14.701Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ultralytics-thop"
|
||||
version = "2.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "torch" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/20/d6b6aaa8ecf7dbdeaf0c05f73bdcd04cf8ce468d936bc48dbcd368e75baf/ultralytics_thop-2.1.6.tar.gz", hash = "sha256:0ec2df8ebd3db35795e1f80cdc8bce6734446dbe989bca1b0c89396353f0f08c", size = 36364, upload-time = "2026-07-30T22:31:28.143Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/98/f1fa3d40d548c8a2a3eec33b7f856063bb6c7d51e16d5198b1f390b2c79d/ultralytics_thop-2.1.6-py3-none-any.whl", hash = "sha256:23f7b8ad124fa3432c1a7de9279102c4fdda699216032a7dff49f87ec3d1a3af", size = 30479, upload-time = "2026-07-30T22:31:26.874Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
|
||||
Reference in New Issue
Block a user