| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626 |
- #!/usr/bin/env python3
- """
- 起重机挂钩数据集生成全流程
- ============================
- Step 1: 视频抽帧 → Step 2: Engagement筛选 → Step 3: Ready注入
- → Step 4: 质量过滤 → Step 5: 高阈值重推理 → Step 6: 质量排序 → Step 7: CVAT导出
- 用法:
- python pipeline.py # 运行全部步骤
- python pipeline.py --skip-extract # 跳过抽帧
- python pipeline.py --skip-extract --skip-select # 跳过多个步骤
- """
- import os
- import sys
- import re
- import time
- import shutil
- import zipfile
- import subprocess
- import numpy as np
- from pathlib import Path
- from concurrent.futures import ThreadPoolExecutor
- from config import *
- # ============================================================
- # Step 1: 视频抽帧 (FFmpeg NVDEC硬件加速)
- # ============================================================
- def step1_extract():
- print("=" * 60)
- print("Step 1: 视频抽帧 (FFmpeg NVDEC)")
- print("=" * 60)
- if EXTRACTED_DIR.exists():
- print(f" 已存在 {EXTRACTED_DIR},跳过抽帧")
- return
- videos = []
- for mp4 in VIDEO_DIR.rglob('*.mp4'):
- parts = mp4.parts
- try:
- cam_idx = parts.index(VIDEO_DIR_NAME)
- cam_id = parts[cam_idx + 1]
- cam_name = f'cam{cam_id.zfill(2)}'
- date = parts[cam_idx + 2]
- except (IndexError, ValueError):
- continue
- videos.append((mp4, cam_name, date))
- print(f" 找到 {len(videos)} 个视频")
- EXTRACTED_DIR.mkdir(parents=True, exist_ok=True)
- total_frames = 0
- for i, (mp4, cam_name, date) in enumerate(videos, 1):
- out_dir = EXTRACTED_DIR / cam_name
- out_dir.mkdir(exist_ok=True)
- stem = mp4.stem
- pattern = str(out_dir / f'{cam_name}_{date}_{stem}_%06d.jpg')
- cmd = ['ffmpeg', '-hide_banner', '-loglevel', 'error',
- '-hwaccel', 'nvdec', '-c:v', 'hevc_cuvid',
- '-i', str(mp4),
- '-qmin', '1', '-qmax', '1', '-vsync', '0', pattern]
- print(f" [{i}/{len(videos)}] {cam_name}/{date}/{stem}...", end=' ', flush=True)
- t0 = time.time()
- subprocess.run(cmd, check=True)
- n_frames = len(list(out_dir.glob(f'{cam_name}_{date}_{stem}_*.jpg')))
- for j in range(1, n_frames + 1):
- lbl = out_dir / f'{cam_name}_{date}_{stem}_{j:06d}.txt'
- if not lbl.exists():
- lbl.touch()
- elapsed = time.time() - t0
- total_frames += n_frames
- print(f"{n_frames} frames in {elapsed:.0f}s")
- print(f"\n 抽帧完成: {total_frames} 帧")
- # ============================================================
- # Step 2: Engagement评分 + 去重 + 排序选取
- # ============================================================
- def _score_and_pos(dets):
- """评分:hoist+ear共现=0.7, 仅ear=0.4, 中心距离奖励, 多hoist惩罚"""
- if len(dets) == 0:
- return 0
- has_hoist = np.any(dets[:, 0] == 0)
- has_ear = np.any(dets[:, 0] == 1)
- n_hoist = int(np.sum(dets[:, 0] == 0))
- score = 0.7 if (has_hoist and has_ear) else (0.4 if has_ear else 0.0)
- avg_xc = np.mean(dets[:, 1])
- avg_yc = np.mean(dets[:, 2])
- center_dist = ((avg_xc - 0.5)**2 + (avg_yc - 0.5)**2) ** 0.5
- score += max(0, 0.3 - center_dist) * 0.5
- if n_hoist > 1:
- score -= (n_hoist - 1) * 0.1
- return max(0, score)
- def _fix_duplicates(labels):
- """去重:ear保留最小框,hoist保留最大框"""
- for cls_id, keep_func in [(1, 'min'), (0, 'max')]:
- cls_mask = labels[:, 0] == cls_id
- if np.sum(cls_mask) <= 1:
- continue
- cls_dets = labels[cls_mask]
- areas = cls_dets[:, 3] * cls_dets[:, 4]
- keep_idx = np.argmin(areas) if keep_func == 'min' else np.argmax(areas)
- keep_mask = np.ones(len(labels), dtype=bool)
- cls_indices = np.where(cls_mask)[0]
- for ri in range(len(cls_dets)):
- if ri != keep_idx:
- keep_mask[cls_indices[ri]] = False
- labels = labels[keep_mask]
- return labels
- def step2_select():
- print("\n" + "=" * 60)
- print("Step 2: Engagement评分 + 去重 + 选取engaged帧")
- print("=" * 60)
- if OUTPUT_DIR.exists():
- shutil.rmtree(str(OUTPUT_DIR))
- cameras = sorted([d.name for d in EXTRACTED_IMG_DIR.iterdir() if d.is_dir()])
- all_kept = {}
- for cam_name in cameras:
- t0 = time.time()
- lbl_dir = EXTRACTED_LBL_DIR / cam_name
- # 读取所有标签
- labels = {}
- with os.scandir(str(lbl_dir)) as it:
- for entry in it:
- if not entry.name.endswith('.txt'):
- continue
- with open(entry.path) as f:
- data = f.read().strip()
- stem = entry.name[:-4]
- if data:
- rows = []
- for line in data.split('\n'):
- parts = line.split()
- if len(parts) >= 5:
- rows.append([float(x) for x in parts[:5]])
- labels[stem] = np.array(rows) if rows else np.zeros((0, 5))
- else:
- labels[stem] = np.zeros((0, 5))
- # 评分
- scores = {name: _score_and_pos(dets) for name, dets in labels.items()}
- # 去重
- for name, dets in labels.items():
- if len(dets) > 0:
- labels[name] = _fix_duplicates(dets)
- # 按分数排序
- ranked = sorted(scores.keys(), key=lambda n: scores[n], reverse=True)
- # 筛选
- selected = []
- last_seq = -999
- for name in ranked:
- if scores[name] < SCORE_THRESH:
- continue
- dets = labels[name]
- if not np.any((dets[:, 0] == 0) | (dets[:, 0] == 1)):
- continue
- m = re.search(r'_(\d{6})$', name)
- seq = int(m.group(1)) if m else 0
- if abs(seq - last_seq) < SEQ_GAP or seq == last_seq:
- continue
- selected.append(name)
- last_seq = seq
- # 创建输出目录 + 符号链接
- out_img = OUTPUT_DIR / 'images' / cam_name
- out_lbl = OUTPUT_DIR / 'labels' / cam_name
- out_img.mkdir(parents=True, exist_ok=True)
- out_lbl.mkdir(parents=True, exist_ok=True)
- img_dir = EXTRACTED_IMG_DIR / cam_name
- for name in selected:
- si = img_dir / f'{name}.jpg'
- sl = EXTRACTED_LBL_DIR / cam_name / f'{name}.txt'
- di = out_img / f'czzt_{name}.jpg'
- dl = out_lbl / f'czzt_{name}.txt'
- if si.exists() and not di.exists():
- os.symlink(str(si.resolve()), str(di))
- if sl.exists() and not dl.exists():
- os.symlink(str(sl.resolve()), str(dl))
- all_kept[cam_name] = len(selected)
- print(f" {cam_name}: {len(selected)} images in {time.time() - t0:.1f}s")
- total = sum(all_kept.values())
- print(f"\n Step 2 完成: {total} images")
- return all_kept
- # ============================================================
- # Step 3: 注入 ready 标签 (YOLOv5推理)
- # ============================================================
- def _calc_iou(boxA, boxB):
- x1 = max(boxA[0], boxB[0]); y1 = max(boxA[1], boxB[1])
- x2 = min(boxA[2], boxB[2]); y2 = min(boxA[3], boxB[3])
- inter = max(0, x2-x1) * max(0, y2-y1)
- areaA = (boxA[2]-boxA[0]) * (boxA[3]-boxA[1])
- areaB = (boxB[2]-boxB[0]) * (boxB[3]-boxB[1])
- union = areaA + areaB - inter
- return inter / union if union > 0 else 0
- def step3_inject_ready(cam_name):
- import torch
- import cv2
- img_dir = OUTPUT_DIR / 'images' / cam_name
- lbl_dir = OUTPUT_DIR / 'labels' / cam_name
- if not img_dir.exists():
- return 0
- images = sorted(img_dir.glob('czzt_*.jpg'))
- if not images:
- return 0
- sys.path.insert(0, YOLOV5_DIR)
- from models.common import DetectMultiBackend
- from utils.general import non_max_suppression, scale_boxes
- from utils.torch_utils import select_device
- device = select_device('')
- model = DetectMultiBackend(MODEL_PATH, device=device)
- model.eval()
- injected = 0
- for img_path in images:
- lbl_file = lbl_dir / f'{img_path.stem}.txt'
- # 读取已有标签
- existing = []
- if lbl_file.exists():
- with open(lbl_file) as f:
- for line in f:
- parts = line.strip().split()
- if len(parts) >= 5:
- existing.append([int(parts[0])] + [float(x) for x in parts[1:]])
- # 需要同时有hoist和ear才注入ready
- if not (any(r[0] == 0 for r in existing) and any(r[0] == 1 for r in existing)):
- continue
- # 推理
- img_bgr = cv2.imread(str(img_path))
- if img_bgr is None:
- continue
- img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
- h0, w0 = img_rgb.shape[:2]
- img_resized = cv2.resize(img_rgb, (IMG_WIDTH, IMG_HEIGHT))
- img_tensor = img_resized.transpose(2, 0, 1)
- img_tensor = np.ascontiguousarray(img_tensor)
- img_tensor = torch.from_numpy(img_tensor).to(device).float() / 255.0
- if img_tensor.ndimension() == 3:
- img_tensor = img_tensor.unsqueeze(0)
- pred = model(img_tensor)[0]
- pred = non_max_suppression(pred, READY_CONF_THRESH, READY_IOU_THRESH, classes=[2])
- if len(pred) == 0 or len(pred[0]) == 0:
- continue
- dets = pred[0]
- dets[:, :4] = scale_boxes(img_tensor.shape[2:], dets[:, :4], (h0, w0)).round()
- areas = (dets[:, 2] - dets[:, 0]) * (dets[:, 3] - dets[:, 1])
- # 收集ear框用于IoU过滤
- ear_boxes = []
- for r in existing:
- if r[0] == 1:
- ex, ey, ew, eh = r[1], r[2], r[3], r[4]
- ear_boxes.append([(ex-ew/2)*w0, (ey-eh/2)*h0, (ex+ew/2)*w0, (ey+eh/2)*h0])
- # 找最大的不与ear重叠的ready
- sorted_idx = torch.argsort(areas, descending=True)
- best = None
- for idx in sorted_idx:
- det = dets[idx]
- rbox = [det[0].item(), det[1].item(), det[2].item(), det[3].item()]
- if all(_calc_iou(rbox, ebox) <= READY_EAR_IOU_THRESH for ebox in ear_boxes):
- best = det
- break
- if best is None:
- continue
- xc = (best[0] + best[2]) / 2 / w0
- yc = (best[1] + best[3]) / 2 / h0
- w = (best[2] - best[0]) / w0
- h = (best[3] - best[1]) / h0
- with open(lbl_file, 'a') as f:
- f.write(f'2 {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}\n')
- injected += 1
- return injected
- def step3_inject():
- print("\n" + "=" * 60)
- print("Step 3: 注入 ready 标签 (YOLOv5推理)")
- print("=" * 60)
- cameras = sorted([d.name for d in (OUTPUT_DIR / 'images').iterdir() if d.is_dir()])
- total_injected = 0
- for cam_name in cameras:
- n = step3_inject_ready(cam_name)
- total_injected += n
- print(f" {cam_name}: injected ready to {n} images")
- print(f"\n Step 3 完成: {total_injected} ready labels injected")
- # ============================================================
- # Step 4: 质量过滤
- # ============================================================
- def step4_filter():
- print("\n" + "=" * 60)
- print("Step 4: 质量过滤")
- print("=" * 60)
- cameras = sorted([d.name for d in (OUTPUT_DIR / 'images').iterdir() if d.is_dir()])
- total_keep = 0
- for cam_name in cameras:
- img_dir = OUTPUT_DIR / 'images' / cam_name
- lbl_dir = OUTPUT_DIR / 'labels' / cam_name
- if not lbl_dir.exists():
- continue
- discard_ratio = discard_flat = discard_both = keep = 0
- for lbl_file in list(lbl_dir.glob('czzt_*.txt')):
- with open(lbl_file) as f:
- lines = f.readlines()
- hoist_areas, ready_areas, ear_aspects = [], [], []
- for line in lines:
- parts = line.strip().split()
- if len(parts) < 5:
- continue
- cls = int(parts[0])
- w, h = float(parts[3]), float(parts[4])
- if cls == 0:
- hoist_areas.append(w * h)
- elif cls == 2:
- ready_areas.append(w * h)
- elif cls == 1 and h > 0:
- ear_aspects.append(w / h)
- fail_ratio = (hoist_areas and ready_areas and
- max(ready_areas) / max(hoist_areas) >= READY_HOIST_RATIO_THRESH)
- fail_flat = (ear_aspects and all(a >= EAR_FLAT_ASPECT_THRESH for a in ear_aspects))
- if fail_ratio and fail_flat:
- discard_both += 1
- elif fail_ratio:
- discard_ratio += 1
- elif fail_flat:
- discard_flat += 1
- else:
- keep += 1
- continue
- img_file = img_dir / (lbl_file.stem + '.jpg')
- if img_file.exists():
- img_file.unlink()
- lbl_file.unlink()
- total_keep += keep
- print(f" {cam_name}: keep={keep}, discard_ratio={discard_ratio}, discard_flat={discard_flat}, discard_both={discard_both}")
- print(f"\n Step 4 完成: {total_keep} images after filtering")
- # ============================================================
- # Step 5: 重新YOLO推理 (高置信度阈值, 清除重叠框)
- # ============================================================
- def step5_reinfer():
- import torch
- import cv2
- print("\n" + "=" * 60)
- print(f"Step 5: 重新YOLO推理 (conf>={REINFER_CONF}, iou={REINFER_IOU})")
- print("=" * 60)
- sys.path.insert(0, YOLOV5_DIR)
- from models.common import DetectMultiBackend
- from utils.general import non_max_suppression, scale_boxes
- from utils.torch_utils import select_device
- device = select_device('')
- model = DetectMultiBackend(MODEL_PATH, device=device)
- model.eval()
- cameras = sorted([d.name for d in (OUTPUT_DIR / 'images').iterdir() if d.is_dir()])
- total_reinferred = 0
- for cam_name in cameras:
- img_dir = OUTPUT_DIR / 'images' / cam_name
- lbl_dir = OUTPUT_DIR / 'labels' / cam_name
- if not img_dir.exists():
- continue
- images = sorted(img_dir.glob('czzt_*.jpg'))
- if not images:
- continue
- reinferred = 0
- for img_path in images:
- img_bgr = cv2.imread(str(img_path))
- if img_bgr is None:
- continue
- img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
- h0, w0 = img_rgb.shape[:2]
- img_resized = cv2.resize(img_rgb, (IMG_WIDTH, IMG_HEIGHT))
- img_tensor = img_resized.transpose(2, 0, 1)
- img_tensor = np.ascontiguousarray(img_tensor)
- img_tensor = torch.from_numpy(img_tensor).to(device).float() / 255.0
- if img_tensor.ndimension() == 3:
- img_tensor = img_tensor.unsqueeze(0)
- pred = model(img_tensor)[0]
- pred = non_max_suppression(pred, REINFER_CONF, REINFER_IOU)
- lbl_file = lbl_dir / f'{img_path.stem}.txt'
- if len(pred) == 0 or len(pred[0]) == 0:
- lbl_file.write_text('')
- reinferred += 1
- continue
- dets = pred[0]
- dets[:, :4] = scale_boxes(img_tensor.shape[2:], dets[:, :4], (h0, w0)).round()
- with open(lbl_file, 'w') as f:
- for det in dets:
- cls_id = int(det[5])
- if cls_id not in CLASSES:
- continue
- xc = (det[0] + det[2]) / 2 / w0
- yc = (det[1] + det[3]) / 2 / h0
- w = (det[2] - det[0]) / w0
- h = (det[3] - det[1]) / h0
- f.write(f'{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}\n')
- reinferred += 1
- total_reinferred += reinferred
- print(f" {cam_name}: reinferred {reinferred} images")
- print(f"\n Step 5 完成: {total_reinferred} images reinferred")
- # ============================================================
- # Step 6: 质量排序取top MAX_PER_CAM
- # ============================================================
- def step6_select_top():
- print("\n" + "=" * 60)
- print(f"Step 6: 质量排序取top {MAX_PER_CAM}")
- print("=" * 60)
- cameras = sorted([d.name for d in (OUTPUT_DIR / 'images').iterdir() if d.is_dir()])
- for cam_name in cameras:
- img_dir = OUTPUT_DIR / 'images' / cam_name
- lbl_dir = OUTPUT_DIR / 'labels' / cam_name
- if not img_dir.exists():
- continue
- images = sorted(img_dir.glob('czzt_*.jpg'))
- if len(images) <= MAX_PER_CAM:
- print(f" {cam_name}: {len(images)} images (<= {MAX_PER_CAM}, no trimming)")
- continue
- def compute_quality(img_path):
- try:
- import cv2
- img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
- if img is None:
- return 0
- brightness = np.mean(img) / 255.0
- laplacian = cv2.Laplacian(img, cv2.CV_64F)
- sharpness = laplacian.var() / 1000.0
- return brightness * QUALITY_BRIGHTNESS_WEIGHT + min(sharpness, 1.0) * QUALITY_SHARPNESS_WEIGHT
- except Exception:
- return 0
- quality = {}
- with ThreadPoolExecutor(max_workers=16) as pool:
- for img_path in images:
- quality[img_path.name] = compute_quality(img_path)
- ranked = sorted(quality.keys(), key=lambda n: quality[n], reverse=True)
- keep_names = set(ranked[:MAX_PER_CAM])
- removed = 0
- for img_path in images:
- if img_path.name not in keep_names:
- img_path.unlink()
- lbl = lbl_dir / (img_path.stem + '.txt')
- if lbl.exists():
- lbl.unlink()
- removed += 1
- print(f" {cam_name}: kept {len(keep_names)}, removed {removed}")
- # ============================================================
- # Step 7: 生成 CVAT ZIP
- # ============================================================
- def step7_generate_cvat():
- print("\n" + "=" * 60)
- print("Step 7: 生成 CVAT ZIP")
- print("=" * 60)
- CVAT_ZIP_DIR.mkdir(parents=True, exist_ok=True)
- cameras = sorted([d.name for d in (OUTPUT_DIR / 'images').iterdir() if d.is_dir()])
- for cam_name in cameras:
- img_dir = OUTPUT_DIR / 'images' / cam_name
- lbl_dir = OUTPUT_DIR / 'labels' / cam_name
- img_names = sorted([f.name for f in img_dir.glob('czzt_*.jpg')])
- if not img_names:
- print(f" {cam_name}: 0 images, skipping")
- continue
- zip_path = CVAT_ZIP_DIR / f'{cam_name}.zip'
- obj_names_str = '\n'.join(CVAT_CLASSES) + '\n'
- obj_data = f'classes = {len(CVAT_CLASSES)}\ntrain = data/train.txt\nnames = data/obj.names\nbackup = backup/\n'
- train_lines = '\n'.join(f'data/obj_train_data/data/obj_train_data/{n}' for n in img_names) + '\n'
- with zipfile.ZipFile(str(zip_path), 'w', zipfile.ZIP_STORED) as zf:
- zf.writestr('obj.names', obj_names_str)
- zf.writestr('obj.data', obj_data)
- zf.writestr('train.txt', train_lines)
- for img_name in img_names:
- zf.write(str(img_dir / img_name), f'obj_train_data/data/obj_train_data/{img_name}')
- stem = img_name[:-4]
- lbl_file = lbl_dir / f'{stem}.txt'
- if lbl_file.exists():
- zf.write(str(lbl_file), f'obj_train_data/data/obj_train_data/{stem}.txt')
- print(f" {cam_name}: {len(img_names)} images -> {zip_path.name}")
- print(f"\n Step 7 完成: CVAT zips in {CVAT_ZIP_DIR}")
- # ============================================================
- # 最终统计
- # ============================================================
- def print_stats():
- print("\n" + "=" * 60)
- print("最终统计")
- print("=" * 60)
- cameras = sorted([d.name for d in (OUTPUT_DIR / 'images').iterdir() if d.is_dir()])
- total = 0
- label_counts = {0: 0, 1: 0, 2: 0}
- for cam_name in cameras:
- img_dir = OUTPUT_DIR / 'images' / cam_name
- lbl_dir = OUTPUT_DIR / 'labels' / cam_name
- n_img = len(list(img_dir.glob('czzt_*.jpg'))) if img_dir.exists() else 0
- n_lbl = {0: 0, 1: 0, 2: 0}
- if lbl_dir.exists():
- for lf in lbl_dir.glob('czzt_*.txt'):
- with open(lf) as f:
- for line in f:
- parts = line.strip().split()
- if len(parts) >= 5:
- c = int(parts[0])
- if c in n_lbl:
- n_lbl[c] += 1
- label_counts[c] += 1
- total += n_img
- print(f" {cam_name}: {n_img} images, hoist={n_lbl[0]}, ear={n_lbl[1]}, ready={n_lbl[2]}")
- print(f"\n 总计: {total} images")
- print(f" hoist(0): {label_counts[0]}")
- print(f" ear(1): {label_counts[1]}")
- print(f" ready(2): {label_counts[2]}")
- # ============================================================
- # Main
- # ============================================================
- def main():
- skip = set(sys.argv[1:])
- if '--skip-extract' not in skip:
- step1_extract()
- if '--skip-select' not in skip:
- step2_select()
- if '--skip-inject' not in skip:
- step3_inject()
- if '--skip-filter' not in skip:
- step4_filter()
- if '--skip-reinfer' not in skip:
- step5_reinfer()
- if '--skip-select-top' not in skip:
- step6_select_top()
- if '--skip-zip' not in skip:
- step7_generate_cvat()
- print_stats()
- if __name__ == '__main__':
- main()
|