Files
Bird_detect_cascade_service_V2/app/batch_runner_raw.py
T
2026-08-21 14:19:14 +08:00

396 lines
11 KiB
Python

from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from typing import Dict, List
import cv2
from app.config import settings
from app.pipeline import BirdCascadePipeline
from app.utils import (
decode_image_from_bytes,
draw_results,
safe_stem,
save_json,
write_image_xml,
write_video_xml,
)
from app.video_pipeline import VideoResultSmoother
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}
VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".m4v"}
def now_timestamp_minute() -> str:
return time.strftime("%Y%m%d%H%M")
def is_image(path: Path) -> bool:
return path.suffix.lower() in IMAGE_EXTS
def is_video(path: Path) -> bool:
return path.suffix.lower() in VIDEO_EXTS
def collect_inputs(input_path: Path, recursive: bool = False) -> List[Path]:
if input_path.is_file():
if is_image(input_path) or is_video(input_path):
return [input_path]
raise ValueError(f"不支持的文件类型: {input_path}")
if not input_path.is_dir():
raise FileNotFoundError(f"输入路径不存在: {input_path}")
iterator = input_path.rglob("*") if recursive else input_path.glob("*")
files = []
for p in iterator:
if p.is_file() and (is_image(p) or is_video(p)):
files.append(p)
files.sort()
return files
def make_output_dirs(input_path: Path, output_base: Path) -> Dict[str, Path]:
timestamp = now_timestamp_minute()
input_name = safe_stem(input_path.name)
output_root = output_base / f"{timestamp}_{input_name}"
if output_root.exists():
idx = 1
while True:
candidate = output_base / f"{timestamp}_{input_name}_{idx:02d}"
if not candidate.exists():
output_root = candidate
break
idx += 1
result_dir = output_root / "result"
xml_dir = output_root / "xml"
json_dir = output_root / "json"
result_dir.mkdir(parents=True, exist_ok=True)
xml_dir.mkdir(parents=True, exist_ok=True)
json_dir.mkdir(parents=True, exist_ok=True)
return {
"root": output_root,
"result": result_dir,
"xml": xml_dir,
"json": json_dir,
}
def unique_path(path: Path) -> Path:
"""
如果不同子目录里存在同名文件,避免覆盖。
例如 result/1.jpg 已存在,则自动变成 result/1_01.jpg。
"""
if not path.exists():
return path
stem = path.stem
suffix = path.suffix
parent = path.parent
idx = 1
while True:
candidate = parent / f"{stem}_{idx:02d}{suffix}"
if not candidate.exists():
return candidate
idx += 1
def process_image(
image_path: Path,
pipeline: BirdCascadePipeline,
dirs: Dict[str, Path],
) -> Dict:
img_bgr = decode_image_from_bytes(image_path.read_bytes())
output = pipeline.predict(img_bgr, return_image=False)
results = output["results"]
result_img_path = unique_path(dirs["result"] / f"{image_path.stem}.jpg")
json_path = unique_path(dirs["json"] / f"{image_path.stem}.json")
xml_path = unique_path(dirs["xml"] / f"{image_path.stem}.xml")
annotated = draw_results(img_bgr, results)
cv2.imwrite(str(result_img_path), annotated)
json_data = {
"success": True,
"type": "image",
"source_path": str(image_path),
"result_path": str(result_img_path),
"xml_path": str(xml_path),
"json_path": str(json_path),
"image_width": output["image_width"],
"image_height": output["image_height"],
"num_objects": output["num_objects"],
"results": results,
}
save_json(json_path, json_data)
write_image_xml(
xml_path=xml_path,
image_filename=result_img_path.name,
image_path=str(result_img_path),
width=int(output["image_width"]),
height=int(output["image_height"]),
results=results,
)
return json_data
def process_video(
video_path: Path,
pipeline: BirdCascadePipeline,
dirs: Dict[str, Path],
sample_interval: int,
) -> Dict:
result_video_path = unique_path(dirs["result"] / f"{video_path.stem}.mp4")
json_path = unique_path(dirs["json"] / f"{video_path.stem}.json")
xml_path = unique_path(dirs["xml"] / f"{video_path.stem}.xml")
cap = cv2.VideoCapture(str(video_path))
if not cap.isOpened():
raise ValueError(f"视频打开失败: {video_path}")
fps = cap.get(cv2.CAP_PROP_FPS)
if fps is None or fps <= 0:
fps = 25.0
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter(str(result_video_path), fourcc, fps, (width, height))
smoother = VideoResultSmoother(
hold_frames=settings.smooth_hold_frames,
alpha=settings.box_smooth_alpha,
)
frame_results = []
processed_frames = 0
detected_frames = 0
frame_index = 0
start_time = time.time()
try:
while True:
ok, frame = cap.read()
if not ok:
break
should_process = frame_index % sample_interval == 0
if should_process:
output = pipeline.predict(frame, return_image=False)
raw_results = output["results"]
processed_frames += 1
if settings.smooth_video_boxes:
results = smoother.update(raw_results, frame_index)
else:
results = raw_results
if len(results) > 0:
detected_frames += 1
frame_results.append(
{
"frame_index": frame_index,
"time_sec": round(frame_index / fps, 3),
"num_objects": len(results),
"results": results,
}
)
else:
if settings.smooth_video_boxes:
results = smoother.get_for_skipped_frame(frame_index)
else:
results = []
annotated_frame = draw_results(frame, results)
writer.write(annotated_frame)
frame_index += 1
finally:
cap.release()
writer.release()
elapsed = round(time.time() - start_time, 3)
summary = {
"success": True,
"type": "video",
"source_path": str(video_path),
"result_path": str(result_video_path),
"xml_path": str(xml_path),
"json_path": str(json_path),
"video_info": {
"width": width,
"height": height,
"fps": fps,
"total_frames": total_frames,
},
"sample_interval": sample_interval,
"smooth_video_boxes": settings.smooth_video_boxes,
"smooth_hold_frames": settings.smooth_hold_frames,
"box_smooth_alpha": settings.box_smooth_alpha,
"processed_frames": processed_frames,
"detected_frames": detected_frames,
"elapsed_sec": elapsed,
}
json_data = {
"summary": summary,
"frames": frame_results,
}
save_json(json_path, json_data)
write_video_xml(
xml_path=xml_path,
video_filename=result_video_path.name,
video_path=str(result_video_path),
width=width,
height=height,
fps=fps,
total_frames=total_frames,
sample_interval=sample_interval,
frame_results=frame_results,
)
return summary
def run_batch(
input_path: str,
output_base: str,
recursive: bool,
sample_interval: int,
) -> Dict:
input_path_obj = Path(input_path).resolve()
output_base_obj = Path(output_base).resolve()
output_base_obj.mkdir(parents=True, exist_ok=True)
dirs = make_output_dirs(input_path_obj, output_base_obj)
files = collect_inputs(input_path_obj, recursive=recursive)
if len(files) == 0:
raise ValueError(f"没有找到图片或视频文件: {input_path_obj}")
pipeline = BirdCascadePipeline()
success_results = []
failed_results = []
for idx, file_path in enumerate(files, start=1):
print(f"[{idx}/{len(files)}] processing: {file_path}")
try:
if is_image(file_path):
result = process_image(
image_path=file_path,
pipeline=pipeline,
dirs=dirs,
)
elif is_video(file_path):
result = process_video(
video_path=file_path,
pipeline=pipeline,
dirs=dirs,
sample_interval=sample_interval,
)
else:
continue
success_results.append(result)
except Exception as e:
failed = {
"source_path": str(file_path),
"error": str(e),
}
failed_results.append(failed)
print(f"[ERROR] {file_path}: {e}")
index_json = {
"success": len(failed_results) == 0,
"input_path": str(input_path_obj),
"output_root": str(dirs["root"]),
"result_dir": str(dirs["result"]),
"xml_dir": str(dirs["xml"]),
"json_dir": str(dirs["json"]),
"total_files": len(files),
"success_files": len(success_results),
"failed_files": len(failed_results),
"failed": failed_results,
}
index_path = dirs["root"] / "index.json"
index_path.write_text(
json.dumps(index_json, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print("\nDone.")
print(f"Output root: {dirs['root']}")
print(f"Result dir : {dirs['result']}")
print(f"XML dir : {dirs['xml']}")
print(f"JSON dir : {dirs['json']}")
print(f"Index JSON : {index_path}")
return index_json
def main():
parser = argparse.ArgumentParser(description="Bird cascade batch inference")
parser.add_argument(
"--input",
required=True,
help="输入路径,可以是单张图片、单个视频,或包含图片/视频的文件夹",
)
parser.add_argument(
"--output",
default="/data2/zhangw_data/Bird_detect_cascade_service/runs",
help="输出根目录,默认直接输出到项目 runs 下",
)
parser.add_argument(
"--recursive",
action="store_true",
help="输入为文件夹时,是否递归处理子文件夹",
)
parser.add_argument(
"--sample-interval",
type=int,
default=settings.video_sample_interval,
help="视频抽帧间隔",
)
args = parser.parse_args()
run_batch(
input_path=args.input,
output_base=args.output,
recursive=args.recursive,
sample_interval=args.sample_interval,
)
if __name__ == "__main__":
main()