feat: 增加视频截帧功能,支持--video参数自动截帧+OCR
This commit is contained in:
@@ -4,19 +4,36 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Batch frame processing
|
- **视频截帧**: 直接从视频文件截取帧图片
|
||||||
- PaddleOCR/Tesseract support
|
- **批量处理**: 支持处理帧目录
|
||||||
- Confidence scoring
|
- **双引擎**: EasyOCR / Tesseract
|
||||||
- Text merging
|
- **置信度评分**: 每帧识别结果带置信度
|
||||||
|
- **文本合并**: 自动合并所有帧的识别结果
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Process frames
|
# 方式1: 直接从视频截帧并OCR(推荐)
|
||||||
|
python ocr_processor.py --video ./video.flv --output ocr.json
|
||||||
|
|
||||||
|
# 方式2: 处理已截取的帧目录
|
||||||
python ocr_processor.py --input ./frames/ --output ocr.json
|
python ocr_processor.py --input ./frames/ --output ocr.json
|
||||||
|
|
||||||
# With custom FPS
|
# 指定截帧帧率(默认1帧/秒)
|
||||||
python ocr_processor.py --input ./frames/ --fps 0.1 --output ocr.json
|
python ocr_processor.py --video ./video.flv --fps 2 --output ocr.json
|
||||||
|
|
||||||
|
# 同时生成合并的文本文件
|
||||||
|
python ocr_processor.py --video ./video.flv --output ocr.json --merge
|
||||||
|
|
||||||
|
# 使用 Tesseract 引擎
|
||||||
|
python ocr_processor.py --input ./frames/ --engine tesseract --output ocr.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow 中的使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 步骤5: OCR处理(视频无字幕时)
|
||||||
|
python ocr_processor.py --video ${CONTENT_VIDEO_PATH} --output ${CONTENT_DIR}/ocr.json --merge
|
||||||
```
|
```
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
@@ -24,14 +41,15 @@ python ocr_processor.py --input ./frames/ --fps 0.1 --output ocr.json
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"engine": "easyocr",
|
||||||
"frames": {
|
"total_frames": 619,
|
||||||
"frame_0001.jpg": {
|
"results": [
|
||||||
"texts": [{"text": "字幕", "confidence": 0.95}]
|
{
|
||||||
}
|
"source": "0001.jpg",
|
||||||
},
|
"text": "字幕文字",
|
||||||
"merged_text": "合并的字幕文本..."
|
"confidence": 0.95
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,4 +58,9 @@ python ocr_processor.py --input ./frames/ --fps 0.1 --output ocr.json
|
|||||||
```bash
|
```bash
|
||||||
cp -r kb-ocr-processor/ ~/skills/
|
cp -r kb-ocr-processor/ ~/skills/
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 依赖说明
|
||||||
|
# - easyocr: 主要OCR引擎(自动下载模型)
|
||||||
|
# - pytesseract: Tesseract引擎
|
||||||
|
# - ffmpeg: 视频截帧(apt install ffmpeg)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -216,6 +216,50 @@ class OCRProcessor:
|
|||||||
print(f"Merged text saved to {text_path}")
|
print(f"Merged text saved to {text_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_frames(video_path: str, output_dir: str, fps: float = 1.0) -> str:
|
||||||
|
"""
|
||||||
|
使用 ffmpeg 从视频中截取帧图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_path: 视频文件路径
|
||||||
|
output_dir: 输出目录
|
||||||
|
fps: 每秒截取帧数 (默认: 1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
帧图片目录路径
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ffmpeg 命令: -i 输入 -vf fps=fps 输出格式
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-i", video_path,
|
||||||
|
"-vf", f"fps={fps}",
|
||||||
|
"-q:v", "2", # 质量等级 (2=高质量)
|
||||||
|
str(output_dir / "%04d.jpg")
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"🔄 截取视频帧: {video_path}")
|
||||||
|
print(f" 输出目录: {output_dir}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# 统计截取的帧数
|
||||||
|
frame_count = len(list(output_dir.glob("*.jpg")))
|
||||||
|
print(f"✅ 截取完成: {frame_count} 帧")
|
||||||
|
return str(output_dir)
|
||||||
|
else:
|
||||||
|
print(f"❌ ffmpeg 错误: {result.stderr}")
|
||||||
|
return ""
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("❌ 错误: 未找到 ffmpeg,请先安装: apt install ffmpeg")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""命令行入口"""
|
"""命令行入口"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@@ -226,20 +270,27 @@ def main():
|
|||||||
# 使用 EasyOCR 处理帧目录
|
# 使用 EasyOCR 处理帧目录
|
||||||
python ocr_processor.py --input ./frames/ --output ./ocr.json
|
python ocr_processor.py --input ./frames/ --output ./ocr.json
|
||||||
|
|
||||||
|
# 直接从视频截帧并 OCR
|
||||||
|
python ocr_processor.py --video ./video.flv --output ./ocr.json
|
||||||
|
|
||||||
# 使用 Tesseract 处理
|
# 使用 Tesseract 处理
|
||||||
python ocr_processor.py --input ./frames/ --output ./ocr.json --engine tesseract
|
python ocr_processor.py --input ./frames/ --output ./ocr.json --engine tesseract
|
||||||
|
|
||||||
# 指定采样帧率
|
# 指定截帧帧率
|
||||||
python ocr_processor.py --input ./frames/ --output ./ocr.json --fps 0.5
|
python ocr_processor.py --video ./video.flv --output ./ocr.json --fps 2
|
||||||
|
|
||||||
# 同时生成合并的文本文件
|
# 同时生成合并的文本文件
|
||||||
python ocr_processor.py --input ./frames/ --output ./ocr.json --merge
|
python ocr_processor.py --input ./frames/ --output ./ocr.json --merge
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("--input", "-i", required=True, help="输入帧图片目录")
|
# 输入源:帧目录 或 视频文件(二选一)
|
||||||
|
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
input_group.add_argument("--input", "-i", help="输入帧图片目录")
|
||||||
|
input_group.add_argument("--video", "-v", help="输入视频文件(会自动截帧)")
|
||||||
|
|
||||||
parser.add_argument("--output", "-o", required=True, help="输出 JSON 文件路径")
|
parser.add_argument("--output", "-o", required=True, help="输出 JSON 文件路径")
|
||||||
parser.add_argument("--fps", "-f", type=float, default=0.1, help="采样帧率 (默认: 0.1)")
|
parser.add_argument("--fps", "-f", type=float, default=1.0, help="截帧帧率 (默认: 1帧/秒)")
|
||||||
parser.add_argument("--engine", "-e", choices=["easyocr", "tesseract"],
|
parser.add_argument("--engine", "-e", choices=["easyocr", "tesseract"],
|
||||||
default="easyocr", help="OCR 引擎 (默认: easyocr)")
|
default="easyocr", help="OCR 引擎 (默认: easyocr)")
|
||||||
parser.add_argument("--merge", action="store_true", help="同时保存合并的文本文件")
|
parser.add_argument("--merge", action="store_true", help="同时保存合并的文本文件")
|
||||||
@@ -249,11 +300,25 @@ def main():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 如果指定了视频文件,先截帧
|
||||||
|
input_dir = args.input
|
||||||
|
if args.video:
|
||||||
|
if not os.path.exists(args.video):
|
||||||
|
print(f"❌ 视频文件不存在: {args.video}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 创建临时帧目录
|
||||||
|
frames_dir = Path(args.video).stem + "_frames"
|
||||||
|
input_dir = extract_frames(args.video, frames_dir, fps=args.fps)
|
||||||
|
|
||||||
|
if not input_dir:
|
||||||
|
return 1
|
||||||
|
|
||||||
# 初始化处理器
|
# 初始化处理器
|
||||||
processor = OCRProcessor(engine=args.engine, languages=args.languages)
|
processor = OCRProcessor(engine=args.engine, languages=args.languages)
|
||||||
|
|
||||||
# 处理帧
|
# 处理帧
|
||||||
results = processor.process_frames(args.input, fps=args.fps, progress_file=args.progress)
|
results = processor.process_frames(input_dir, fps=args.fps, progress_file=args.progress)
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
# 保存结果
|
# 保存结果
|
||||||
|
|||||||
@@ -243,16 +243,16 @@ def main():
|
|||||||
|
|
||||||
parser.add_argument('--check', action='store_true', help='查重')
|
parser.add_argument('--check', action='store_true', help='查重')
|
||||||
parser.add_argument('--url', type=str, help='要检查的URL')
|
parser.add_argument('--url', type=str, help='要检查的URL')
|
||||||
parser.add_argument('--archive', type=str, help='归档的文件路径')
|
parser.add_argument('--archive-file', type=str, help='归档的文件路径')
|
||||||
parser.add_argument('--base-path', type=str, help='获取知识库根目录')
|
parser.add_argument('--base-path', type=str, help='获取知识库根目录')
|
||||||
|
|
||||||
parser.add_argument('--pending', action='store_true', help='url解析目录')
|
parser.add_argument('--pending', action='store_true', help='获取url解析目录')
|
||||||
parser.add_argument('--metadata', action='store_true', help='获取元数据目录')
|
parser.add_argument('--metadata', action='store_true', help='获取元数据目录')
|
||||||
parser.add_argument('--content', action='store_true', help='获取内容目录')
|
parser.add_argument('--content', action='store_true', help='获取内容目录')
|
||||||
parser.add_argument('--content_downloads', action='store_true', help='获取主内容下载目录')
|
parser.add_argument('--content_downloads', action='store_true', help='获取主内容下载目录')
|
||||||
parser.add_argument('--content_subtitles', action='store_true', help='获取字幕目录')
|
parser.add_argument('--content_subtitles', action='store_true', help='获取字幕目录')
|
||||||
parser.add_argument('--analyzed', action='store_true', help='获取深度分析目录')
|
parser.add_argument('--analyzed', action='store_true', help='获取深度分析目录')
|
||||||
parser.add_argument('--archive', action='store_true', help='获取归档目录')
|
parser.add_argument('--archive_dir', action='store_true', help='获取归档目录')
|
||||||
parser.add_argument('--archive_subscriptions', action='store_true', help='获取订阅信息目录')
|
parser.add_argument('--archive_subscriptions', action='store_true', help='获取订阅信息目录')
|
||||||
parser.add_argument('--extract_platform', type=str, help='要提取平台名称的文件名')
|
parser.add_argument('--extract_platform', type=str, help='要提取平台名称的文件名')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -263,10 +263,16 @@ def main():
|
|||||||
result = check_duplicate(base_path, args.url)
|
result = check_duplicate(base_path, args.url)
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
elif args.archive:
|
elif args.archive_file:
|
||||||
result = archive_and_git(args.archive, base_path)
|
result = archive_and_git(args.archive_file, base_path)
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
elif args.archive_dir:
|
||||||
|
path = dirs.get("archive")
|
||||||
|
if path:
|
||||||
|
print(json.dumps({"path": str(path)}, ensure_ascii=False, indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user