Skip to content

Commit 364dbe8

Browse files
committed
hybrid
1 parent 53f0502 commit 364dbe8

9 files changed

Lines changed: 1476 additions & 7 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ reports/
1111
organized_outputs/
1212
patch_outputs/
1313
pipeline_outputs/
14-
pipeline_outputs_test/
14+
pipeline_outputs_test/
15+
hybrid_outputs/
16+
clip1_yolo/

fp_filter/FP过滤二分类使用说明.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,98 @@ python fp_filter/csv_to_yolo_txt.py \
207207
```
208208

209209
运行后会在 CSV 同目录生成 `<csv_stem>_yolo_labels` 目录,里面每帧对应一个 `.txt`(默认同时会为无检测帧创建空文件,除非使用 `--no-save-empty`)。
210+
211+
---
212+
213+
## 第五步:融合推理(ROI 内 WASB+FP,ROI 外 YOLO)
214+
215+
### 一键 Pipeline 已集成原图尺度标签生成
216+
217+
现在 `run_inference_pipeline.py` 已将 `fp_filter/csv_to_original_yolo.py` 纳入自动流程。
218+
219+
执行:
220+
221+
```powershell
222+
python run_inference_pipeline.py ^
223+
--dataset-root datasets/tennis_predict ^
224+
--crop-left 650 --crop-top 51 ^
225+
--orig-w 1920 --orig-h 1080
226+
```
227+
228+
运行完成后会在当前时间戳输出目录下新增:
229+
230+
- `stage5_original_yolo_labels/`
231+
- 每个 `*_predictions_filtered.csv` 对应一个子目录
232+
- 子目录命名规则:`<csv_stem>_orig_yolo_labels`
233+
- 目录中是可直接给 `hybrid_predict.py --wasb-labels-dir` 使用的原图尺度 YOLO 标签
234+
235+
如不希望 Stage 5 为空帧写空 txt,可增加参数:`--orig-no-save-empty`
236+
237+
### 场景
238+
239+
你当前的目标是对 **1920×1080 原图**做最终推理,但在固定区域内(`left=650, top=51, right=1236, bottom=339`)优先采用 WASB+FP_Filter 的结果,其余区域采用 YOLO。
240+
241+
该方案可避免两个模型在 ROI 内重复检测,同时保留 YOLO 在 ROI 外的覆盖能力。
242+
243+
### 推荐流程(稳定离线融合)
244+
245+
1. 先按既有流程跑出 FP 过滤结果 CSV:
246+
247+
- `*_predictions_filtered.csv`(坐标仍是裁剪图坐标系)
248+
249+
2. 将 FP 结果映射回原图 YOLO 坐标:
250+
251+
- 使用 `fp_filter/csv_to_original_yolo.py`
252+
- 得到 `--wasb-labels-dir`(每帧一个 txt,原图归一化坐标)
253+
254+
3. 对原图运行融合脚本 `hybrid_predict.py`
255+
256+
- ROI 内:丢弃 YOLO 框,保留 WASB+FP 框
257+
- ROI 外:保留 YOLO 框
258+
- 最终输出统一 YOLO 标签
259+
260+
### 融合脚本
261+
262+
- 脚本位置:`hybrid_predict.py`(项目根目录)
263+
- 主要参数:
264+
- `--input-folder`: 原图目录(1920×1080)
265+
- `--output-folder`: 融合结果 txt 输出目录
266+
- `--yolo-model`: YOLO 权重
267+
- `--wasb-labels-dir`: `csv_to_original_yolo.py` 生成的标签目录
268+
- `--left --top --right --bottom`: ROI 四点坐标
269+
- `--inside-policy`: ROI 归属规则(`center``any_overlap`
270+
- `--nms-iou`: 融合后 NMS(0 表示关闭)
271+
272+
### 命令示例
273+
274+
```powershell
275+
conda activate zsh
276+
277+
python hybrid_predict.py ^
278+
--input-folder datasets/tennis_predict/match1/clip1 ^
279+
--output-folder hybrid_outputs/match1_clip1_labels ^
280+
--yolo-model yolov8n_1280_1113.pt ^
281+
--wasb-labels-dir fp_filter/patch_outputs/patches_prediction/match1_clip1_orig_yolo_labels ^
282+
--conf 0.5 ^
283+
--orig-w 1920 --orig-h 1080 ^
284+
--left 650 --top 51 --right 1236 --bottom 339 ^
285+
--inside-policy center ^
286+
--nms-iou 0.0 ^
287+
--max-images 200
288+
```
289+
290+
其中 `--max-images` 用于快速小样本验证;确认无误后可去掉该参数处理全量数据。
291+
292+
### 可靠性与健壮性设计说明
293+
294+
- **ROI 四点完整使用**`left/top/right/bottom` 用于完整定义 ROI,而不是只使用偏移量。
295+
- **越界与参数校验**:运行前校验 `left < right``top < bottom`、阈值范围是否合法。
296+
- **容错读取标签**:WASB txt 某行格式异常时仅跳过该行并告警,不中断全流程。
297+
- **可选冲突抑制**:可通过 `--nms-iou` 启用融合后按类别 NMS,减少边界重复框。
298+
- **空帧一致性**:默认写空 txt,便于后续训练/评估流程保持帧对齐。
299+
300+
### 关键建议
301+
302+
- 若你希望“边界附近也强制交给 WASB”,可用 `--inside-policy any_overlap`
303+
- 若 ROI 外 YOLO 框与 ROI 内 WASB 框在边界处仍有重合,可设置 `--nms-iou 0.3`(可按效果调优)。
304+
- 如果最终训练只关心单类别球,确保 YOLO 分支和 WASB 分支类别 ID 定义一致。

fp_filter/crop_frames.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
将图片按指定矩形区域裁剪
3+
原图区域尺寸为:1920*1080,裁剪后区域尺寸为:586*288
4+
裁剪区域: 左上(left,top) 右下(right,bottom),可通过参数指定
5+
"""
6+
7+
import argparse
8+
from pathlib import Path
9+
10+
from PIL import Image
11+
12+
13+
def parse_args():
14+
parser = argparse.ArgumentParser(description="批量裁剪图片指定区域")
15+
parser.add_argument(
16+
"-i", "--input",
17+
type=str,
18+
required=True,
19+
help="输入图片所在文件夹路径",
20+
)
21+
parser.add_argument(
22+
"-o", "--output",
23+
type=str,
24+
required=True,
25+
help="裁剪后图片保存文件夹路径",
26+
)
27+
parser.add_argument(
28+
"--left",
29+
type=int,
30+
default=650,
31+
help="裁剪区域左边界 (默认: 650)",
32+
)
33+
parser.add_argument(
34+
"--top",
35+
type=int,
36+
default=51,
37+
help="裁剪区域上边界 (默认: 61)",
38+
)
39+
parser.add_argument(
40+
"--right",
41+
type=int,
42+
default=1236,
43+
help="裁剪区域右边界 (默认: 1236)",
44+
)
45+
parser.add_argument(
46+
"--bottom",
47+
type=int,
48+
default=339,
49+
help="裁剪区域下边界 (默认: 349)",
50+
)
51+
return parser.parse_args()
52+
53+
54+
def main():
55+
args = parse_args()
56+
source_dir = Path(args.input)
57+
output_dir = Path(args.output)
58+
crop_box = (args.left, args.top, args.right, args.bottom)
59+
60+
if not source_dir.exists():
61+
print(f"错误: 输入目录不存在: {source_dir}")
62+
return
63+
64+
# 创建输出目录
65+
output_dir.mkdir(parents=True, exist_ok=True)
66+
67+
# 获取所有 jpg 图片
68+
image_files = sorted(source_dir.glob("*.jpg"))
69+
total = len(image_files)
70+
71+
if total == 0:
72+
print(f"在 {source_dir} 中未找到 .jpg 图片")
73+
return
74+
75+
print(f"找到 {total} 张图片,开始裁剪...")
76+
success_count = 0
77+
error_count = 0
78+
79+
for i, img_path in enumerate(image_files):
80+
try:
81+
with Image.open(img_path) as img:
82+
# 裁剪指定区域
83+
cropped = img.crop(crop_box)
84+
# 保存到输出目录,保持原文件名
85+
out_path = output_dir / img_path.name
86+
cropped.save(out_path, quality=95)
87+
success_count += 1
88+
89+
# 每处理 5000 张打印进度
90+
if (i + 1) % 500 == 0:
91+
print(f" 已处理 {i + 1}/{total} 张...")
92+
except Exception as e:
93+
error_count += 1
94+
print(f" 处理失败: {img_path.name} - {e}")
95+
96+
print(f"\n完成! 成功: {success_count}, 失败: {error_count}")
97+
print(f"输出目录: {output_dir}")
98+
99+
100+
if __name__ == "__main__":
101+
main()
102+
103+
# python crop_yuedong_frames.py -i right_yuedong_frames -o right_yuedong_up
104+
105+
# python crop_frames.py --input ..\datasets\tennis_predict\match1\clip1 --output ..\datasets\tennis_predict\match1\clip1

0 commit comments

Comments
 (0)