1. 背景描述:
共8-9台设备固定点位,24小时录像,10天~20天不等,共计600G+ video。需要从中找出船只图像,送标,训练。提高检测率,为后续跟踪等模块做支持。
参考教程:视频局部区域移动检测, 删除相似帧 - 知乎 (zhihu.com)
上述截图来自于:视频局部区域移动检测, 删除相似帧 - 知乎 (zhihu.com)
2. 清洗流程
如下图所示,清洗流程为:mkv->imgs->avi->dedup->imgs->筛选标注
3. 难点解决
- mkv→imgs速度慢:瓶颈在写入图片速度,写入设备的图片/小文件写入速度是制约瓶颈;比如每秒可写入多少个小文件;小文件的写入速度。
- 解决办法:①使用高效的小文件写入磁盘 m.2 SSD来做写入设备,速度差异比较;② 脚本应用多开,exe + 解析路径;解析脚本是单线程,多开来同时处理;每个脚本单独所占资源开销小,CPU占用低、内存占用低、磁盘占用低;多开可以充分利用计算资源;备注:没有采用多线程的方式,原因待详细解释。主要是多线程没用。
- 场景有远近距离,去冗余去重复帧的参数阈值要差异化。原因:远近距离中的目标大小/长宽/面积大小差异大,在重复帧的筛选过程中,参数需要差异化。
现象:小目标的场景,丢失了小目标;或者重复帧太多,水面的稍微变化全提取出来;要不重复帧提取多了,要不提取少了。- 所以针对每个区域,单独处理;会麻烦一点,但是相比于后续筛选图像,效率大大提高。还是将这个操作前置。
4. 使用教程
1. 先生成ROI区域
from utils import get_rectangle_point
hours_path = 'E:/aomen_data_imgs_20240326_orivideo/a5_y_3/20240312/00'
for index in range(1, 16):
# index = 1
file_path = hours_path +'/' + str(index) + '.avi'
min_x, min_y, width, height = get_rectangle_point(file_path)
print('剪切坐标 ',index,'\t', min_x, min_y, width, height)
coordinates = {
1: [741, 182, 157, 81],
2: [4, 173, 891, 126],
3: [4, 164, 894, 175],
4: [6, 149, 891, 216],
5: [6, 142, 889, 224],
6: [3, 138, 894, 156],
7: [6, 133, 890, 137],
8: [3, 125, 637, 68],
9: [4, 125, 894, 145],
10: [4, 137, 894, 162],
11: [5, 141, 893, 167],
12: [4, 99, 895, 199],
13: [2, 94, 894, 197],
14: [4, 101, 887, 105],
15: [9, 94, 609, 106]
}
from utils import remove_video_dup
import os
device_path_list = [
'I:/aomen_data_imgs_20240326/a5_y_3'] # 现在只要给定设备路径就可以了
output_rootDir = 'E:/aomen_video_20240326/'
for device in device_path_list:
subfolders_day = [f.path for f in os.scandir(device) if f.is_dir()]
for day_path in subfolders_day:
subfolders = [f.path for f in os.scandir(day_path) if f.is_dir()]
# print(subfolders)
for hours_path in subfolders:
# for index in range(1, 16):
for index in [5]: # 都少写了一个
# 由于距离远近不一样了,所以要分区域来做了,这里要重新给值了
remove_video_dup(hours_path,coordinates,mpdecimate_lo=64*45, mpdecimate_hi=64*100, index=index, output_rootDir=output_rootDir)
utils.py
import os
import cv2
def list_last_level_folders(folder_path):
"""
功能:实现批量Img进入文件夹
# 替换为你实际的文件夹路径
folder_path = "E:\\aomen_data_imgs"
last_level_folders = list_last_level_folders(folder_path)
# print("Last level folders:")
# for folder in last_level_folders:
# print(folder)
# 写入到 output.txt 文件
output_file_path = "all_hours_output.txt"
with open(output_file_path, 'w') as file:
# file.write(f"{folder_path}\n")
for folder in last_level_folders:
file.write(f"{folder}\n")
print(f"Last level folders written to {output_file_path}")
"""
folders = []
def _list_last_level_folders(current_path):
nonlocal folders
try:
if os.path.exists(current_path) and os.path.isdir(current_path):
# 判断当前文件夹是否有子文件夹
subfolders = [entry.name for entry in os.scandir(current_path) if entry.is_dir()]
if not subfolders:
# 没有子文件夹,添加到列表
folders.append(current_path)
else:
# 递归获取子文件夹
for entry in os.scandir(current_path):
if entry.is_dir():
_list_last_level_folders(entry.path)
except Exception as e:
print(f"Error: {e}")
_list_last_level_folders(folder_path)
return folders
def read_path_from_txt(output_file_path):
""" usage:
output_file_path = "all_hours_output.txt"
read_path_from_txt(output_file_path)
"""
# output_file_path = "all_hours_output.txt"
f=open(output_file_path)
line = f.readline().strip() #读取第一行
txt=[]
txt.append(line)
while line: # 直到读取完文件
line = f.readline().strip() # 读取一行文件,包括换行符
txt.append(line)
f.close() # 关闭文件
# print(txt)
return txt
def from_picture_2_video(path, index=None):
""" 给定hours_path 可以生产该小时下的15个路径的img->avi
org-19:00---0,1,....,15
|--0
|--0a0.jpg 1a0.jpg,...,1000a0.jpg
res-19:00
|-0.avi, 1.avi,...., 15.avi
usage:
hours_path = 'H:/aomen_data_imgs/20231229/19_start/' # hours_path
for index in range(0,32):
from_picture_2_video(hours_path, index=index)
"""
if index is None:
# image_folder = 'H:/aomen_data_imgs/20231229/19_start/0'
# video_name = 'H:/aomen_data_imgs/20231229/19_start/0'+'.avi'
image_folder = path
video_name = path + '.avi'
else:
# image_folder = 'H:/aomen_data_imgs/20231229/19_start/' + str(index)
# video_name = 'H:/aomen_data_imgs/20231229/19_start/' + str(index) + '.avi'
image_folder = path + str(index)
video_name = path + str(index) + '.avi'
# 获取图像文件列表
images = [img for img in os.listdir(image_folder) if img.endswith(".jpg")]
# 获取图像文件列表并按照数字顺序排序
# images = sorted([img for img in os.listdir(image_folder) if img.endswith(".jpg")], key=lambda x: int(x.split('.')[0]))
frame = cv2.imread(os.path.join(image_folder, images[0]))
# 获取图像尺寸
height, width, layers = frame.shape
# 视频编解码器,可以根据需要更改
# fourcc = cv2.VideoWriter_fourcc(*'XVID')
# # video = cv2.VideoWriter(video_name, fourcc, 1, (width, height))
# video = cv2.VideoWriter(video_name, fourcc, 1, (width, height))
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 或者使用 -1
video = cv2.VideoWriter(video_name, fourcc, 1, (width, height))
# 将图像写入视频
for image in images:
video.write(cv2.imread(os.path.join(image_folder, image)))
cv2.destroyAllWindows()
video.release()
# 新的用法
# hours_path = 'H:/aomen_data_imgs/20231229/19_start/' # hours_path
import re
import os
# image_folder = txt[66]
def getImageFileListFromFolderPath(image_folder):
"""
image_folder='F:/aomen_data_imgs/20231229/00/0' 没有最后那个斜杠
获取00小时0,1,...,15视角下的所有*a0.jpg名字,并按照a前面的圈数来进行排序
排序之后再拼接为avi视频,保证顺序,也许对dup环节有用。
"""
# 提取数字部分的函数
def extract_number(filename):
match = re.search(r'(\d+)', filename)
return int(match.group()) if match else float('inf') # 如果没有匹配到数字,返回无穷大
images = [img for img in os.listdir(image_folder) if img.endswith(".jpg")]
# 对文件名进行排序
sorted_filenames = sorted(images, key=extract_number)
# images = sorted([img for img in os.listdir(image_folder) if img.endswith(".jpg")], key=lambda x: int(x.split('.')[0]))
output_file_path = image_folder + "\\filelist.txt" # 前面image_folder='F:/aomen_data_imgs/20231229/00/0' 没有最后那个斜杠 否则这里要出错
with open(output_file_path, 'w') as file:
for folder in sorted_filenames:
file.write(f"{folder}\n")
def ffmpge_imgs2vido(path, isOpenFFmpeg=True):
"""
使用ffmpeg将imgs->avi
需要使用powershell命令
"""
# python执行powershell指令
import subprocess
import os
image_folder = path
video_name = path + '.avi'
# filelist_path = image_folder+'/filelist.txt'
getImageFileListFromFolderPath(image_folder=image_folder)
powershell_command = fr'(Get-Content "{image_folder}\filelist.txt") | ForEach-Object {{"file $_"}} | Set-Content (Join-Path -Path "{image_folder}" -ChildPath "filelist.txt")'
# 执行PowerShell指令
result = subprocess.run(['powershell', powershell_command], capture_output=True, text=True)
# 下面开始执行ffmpeg指令了:和盘符有关,指明mmfpeg.exe到底在哪里
if isOpenFFmpeg: # 只生产filelinst,不进行操作,不知道为什么会卡在这里,所以会命令行
ffmpeg_command = f"G:/zhr/ffmpeg6/ffmpeg.exe -r 1 -f concat -i {image_folder}/filelist.txt -vcodec libx264 -crf 25 -pix_fmt yuv420p {video_name}"
result = subprocess.run(['powershell', ffmpeg_command], capture_output=True, text=True)
print(ffmpeg_command)
"""
,,获取澳门数据每个区域的截断ROI区域信息:只要大桥前面的数据
"""
import cv2
# 鼠标响应函数(opencv画矩形框,并获取左上角的坐标与长宽)
def _rectangular_box(event, x, y, flags, param):
"""
鼠标响应函数(opencv画矩形框,并获取左上角的坐标与长宽)
:param event: 鼠标事件
:param x: 坐标
:param y: 坐标
:param flags:
:param param: 传递进来的参数(这里传入的是视频的第一帧)
:return:
"""
global point1, point2, min_x, min_y, width, height
img = param.copy()
if event == cv2.EVENT_LBUTTONDOWN: # 左键点击
point1 = (x, y)
cv2.circle(img, point1, 10, (0, 255, 0), 5)
cv2.imshow('image', img)
elif event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_LBUTTON): # 按住左键拖曳
cv2.rectangle(img, point1, (x, y), (255, 0, 0), 5)
cv2.imshow('image', img)
elif event == cv2.EVENT_LBUTTONUP: # 左键释放
point2 = (x, y)
cv2.rectangle(img, point1, point2, (0, 255, 255), 4)
cv2.imshow('image', img)
min_x = min(point1[0], point2[0])
min_y = min(point1[1], point2[1])
width = abs(point1[0] - point2[0])
height = abs(point1[1] - point2[1])
def get_rectangle_point(file_path):
"""
opencv画矩形框,并获取左上角的坐标与长宽
:return: 矩形框 左上角的坐标, 和长宽
"""
cap_temp = cv2.VideoCapture(file_path)
if not cap_temp.isOpened():
print("无法打开视频文件。请检查文件路径。")
exit()
ret, frame = cap_temp.read() # 读取第一帧
cv2.namedWindow('image', 0)
# https://stackoverflow.com/questions/50783177/opencv-the-function-is-not-implemented-rebuild-the-library-with-windows
# cv2.resizeWindow('image', 1920, 1080) # 设置窗口大小, 否则显示不完全, 不方便画框
cv2.resizeWindow('image', frame.shape[0], frame.shape[1])
cv2.setMouseCallback('image', _rectangular_box, frame) # 设置鼠标回调函数
cv2.imshow('image', frame)
cv2.waitKey(0) # 按任意键退出矩形选择
cap_temp.release() # 回收资源
cv2.destroyWindow('image') # 关闭窗口
return min_x, min_y, width, height
"""
环境:
python: 3.10
ffmpeg: 6.0-essentials_build
opencv: 4.8.0
CPU: i5 9300h
GPU: GTX 1050(3G) # 未使用
使用场景:
1.只想对感兴趣区域做相似度判断, 删除相似帧
2.对1产生的结果, 需要保留原视频完整的画面
使用场景限制:
1.感兴趣区域不宜过大,影响压缩日志生成速度, 区域越大,日志生成速度越慢
2.感兴趣区域不宜有经常变化的物体,影响抽帧的速度,变化越多,抽帧越慢. 例:不能把时间水印框入感兴趣区域, 不能把随风而动的东西框入感兴趣区域
3.以上两点, 需要同时满足, 否则请跳转到尝试二, 下载宝藏up主的工具, 效率更高
食用方式:
1.修改 mpdecimate_***开头的几个参数值, 以达到自己想要的效果, 建议用小视频测试参数值,速度快. 提示: hi和lo越大,删除的帧越多
2.修改 file_path 原视频文件路径
3.启动程序
"""
import os
import sys
import time
import re
import cv2
def get_dateName_hoursName(hours_path=None):
"""
hours_path = 'H:/aomen_data_imgs/20231229/19_start'
get_dateName_hoursName(hours_path)
"""
if hours_path is None:
print("path is None:",hours_path)
return None, None
path_parts = os.path.split(hours_path) # 使用 os.path.split 分割路径,得到一个包含两个元素的元组
hours_name = path_parts[-1]
dateName = os.path.split(path_parts[-2])[-1]
# folder_name = path_parts[-2] # 取出最后一个元素,即文件夹或文件的名称
# print(folder_name)
# path_parts
return dateName, hours_name
def makedir_in_newRootDir(hours_path=None, output_rootDir=None):
"""
hours_path = 'H:/aomen_data_imgs/20231229/19_start/'
output_rootDir = 'H:/aomen_video_2Annotations/'
makedir_in_newRootDir(hours_path, output_rootDir)
"""
if hours_path is not None and output_rootDir is not None:
# 获取驱动器和路径
drive, path = os.path.splitdrive(hours_path)
new_dir = os.path.join(output_rootDir, os.path.relpath(hours_path, drive+'/aomen_data_imgs_20240326')).replace('\\', '/') # 使用 replace('/', '\\') 将路径中的斜杠替换为反斜杠。
# 递归创建目录
try:
os.makedirs(new_dir, exist_ok=True)
except FileExistsError:
pass
return new_dir
def remove_video_dup(hours_path, coordinates, mpdecimate_lo,mpdecimate_hi, index=0, output_rootDir=None):
"""
设置保存之后的文件格式为
同时每个视频区域的ROI点是固定的,所以可以把所有区域的一次性导出来的。
20231229_19_0.mp4 # 日期+小时+区域编号+帧号
20231229_19_0_1.jpg, 20231229_19_0_2.jpg
usage:
hours_path = 'H:/aomen_data_imgs/20231229/19_start/'
remove_video_dup(hours_path, index=4)
输入路径:日期/小时
输出路径:给一个根目录,然后mkdir日期+小时
index为0.avi, 1.avi
每个小时输出保存名字要修改为:20231229_19_0.mp4 # 这样每个小时就只有32个vidieo;每个video大小不一样。
还可以对video进行合并标注。
"""
if output_rootDir is None:
output_rootDir = hours_path
file_path = hours_path +'/' + str(index) + '.avi'
print(file_path)
# file_path = 'H:/aomen_data_imgs/20231229/19_start/3.avi' # 源文件地址
temp_file_path = '' # 一开始转码后的临时文件地址, 留空
global point1, point2, min_x, min_y, width, height # 在图像上画矩形框,所需的全局变量
mpdecimate_max = 0 # 官方默认 0
mpdecimate_keep = 0 # 官方默认 0 这个程序里该参数未启用, 启用会报错, 没细研究
# 64位下的:0~100 65536=FFFF=16位;int类型。
mpdecimate_hi = 64*50 # 官方默认 64*12 [5,12]太细了,太多了,不合适。
mpdecimate_lo = 64*12 # 官方默认 64*5 [45,100]又太大了
mpdecimate_frac = 0.33 # 官方默认 0.33
log_encoding = 'utf8' # 日志编码格式
# skip_frame: 两次时间差值相距超过270帧, 则直接跳转到该帧操作, 不再逐帧跳过,
# skip_frame: 该值由i5 9300h CPU测试而来, 此CPU下cap.grab()连续跳过270帧耗时与cap.set(cv2.CAP_PROP_POS_MSEC, pts_time)耗时相当
skip_frame = 270
# 将原视频转码
def transcoding_video(file_path):
"""
将原视频快速转码, 以去除海康威视导出视频里的损坏帧, 防止抽帧时按照时间跳转出现灰屏
40W帧,耗时几秒钟
"""
# global temp_file_path
temp_file_path = f'temp_video.{file_path.split(".")[-1]}' # 只取文件后缀名字 .avi
print(temp_file_path)
# 一定要删除历史缓存,我靠
import os
if(os.path.isfile(temp_file_path)): # 如果存在就删除
os.remove(temp_file_path)
print("remove temp_video.avi successfully")
else:
print("temp_video.avi do not exit!")
# os.system(f'H:/zhr/video_dup/ffmpeg6/ffmpeg.exe -ss 0 -i {file_path} -map 0:v:0 -c copy -y {temp_file_path}')
print(f'G:/zhr/ffmpeg6/ffmpeg.exe -ss 0 -i {file_path} -map 0:v:0 -c copy -y {temp_file_path}')
os.system(f'G:/zhr/ffmpeg6/ffmpeg.exe -ss 0 -i {file_path} -map 0:v:0 -c copy -y {temp_file_path}')
# 复制一个备份
# 'H:/zhr/video_dup/ffmpeg6/ffmpeg.exe -ss 0 -i H:/aomen_data_imgs/20231229/19_start/4.avi -map 0:v:0 -c copy -y temp_video.avi'
time.sleep(1) # 加上一个睡眠时间, 否则下一步的画矩形框可能无法弹窗
# 生成视频压缩日志文件
def crate_mpdecimate_log(file_path, min_x, min_y, width, height):
temp_file_path = f'temp_video.{file_path.split(".")[-1]}'
"""
先剪切, 再计算相似度, 将需要保留的帧信息, 写入日志文件(剪切面积越大, 执行速度越慢, 剪切的过程相当于一次转码)
如果拥有高端显卡, 可以尝试将下面的指令拆分为两部分执行, 先crop生成一个新视频, 再对新视频mpdecimate提取日志, 总耗时可能会减少
"""
# CPU编解码, 40W帧 耗时: 576秒 'H:/zhr/video_dup/ffmpeg6/ffmpeg.exe -i temp_video.avi -vf crop=890:596:5:416,mpdecimate=max=0:hi=6400:lo=2880:frac=0.33,showinfo -f null - > mpdecimate_log.txt 2>&1'
instruct = f"G:/zhr/ffmpeg6/ffmpeg.exe -i {temp_file_path} -vf crop={width}:{height}:{min_x}:{min_y}," \
f"mpdecimate=max={mpdecimate_max}:hi={mpdecimate_hi}:lo={mpdecimate_lo}:frac={mpdecimate_frac}," \
f"showinfo -f null - > mpdecimate_log.txt 2>&1"
print(instruct)
print('压缩日志生成中, 请等待, 该过程没有进度条展示并且有较高CPU占用, 请耐心等待')
os.system(instruct)
# 读取需要保留的帧的时间信息
def read_time_message():
try:
with open('./mpdecimate_log.txt', encoding=log_encoding) as f:
time_message = f.read()
except UnicodeError:
print(f'日志格式编码错误, {log_encoding} 无法读取日志文件')
exit()
for item in re.finditer(r'n:.*?pts_time: *(?P<pts_time>\d+(\.\d+)?)', time_message):
yield item.group('pts_time')
# 视频时间维度压缩
def video_time_compress(temp_file_path,hours_path, output_rootDir):
cap = cv2.VideoCapture(temp_file_path)
print(temp_file_path)
if not cap.isOpened():
print("无法打开视频文件。请检查文件路径。")
exit()
fourcc = cv2.VideoWriter_fourcc('M', 'P', '4', '2') # 初始化视频写入器
# fourcc = cv2.VideoWriter_fourcc('M', 'P', '4')
# fourcc = cv2.VideoWriter_fourcc(*'XVID')
fps = cap.get(cv2.CAP_PROP_FPS) # 帧率
cv_total_fps = cap.get(7) # 总帧数
cv_total_msec = int(cv_total_fps / fps * 1000) # 总时长, 秒
print(f'原视频总帧数:{cv_total_fps}, 帧率:{fps}, 总时长:{cv_total_msec / 1000}s')
# 为了批量处理,这里需要改为
# save_path
if True:
new_dir = makedir_in_newRootDir(hours_path, output_rootDir) # 新路径下创建文件夹
print(new_dir)
else:
try: # 输出目录
os.mkdir('compress_file') # 创建文件夹,路径,文件名 这里写根目录和创建文件夹
except FileExistsError:
pass
if False:
new_name = file_path.split('/')[-1].split('.')[0] + '_compress.avi' # 输出视频名称 前面是文件名 4_compress.avi 这里写文件名
else:
dateName, hourName = get_dateName_hoursName(hours_path)
new_name = new_dir + '/' + dateName + '_' + hourName + '_' + str(index) + '.mp4' # 输出文件名称,日期——小时——序号.avi
# 新路径 + 新文件名
print(new_name)
out = cv2.VideoWriter(f'{new_name}', fourcc, fps, (int(cap.get(3)), int(cap.get(4))))
# out = cv2.VideoWriter(f'./compress_file/{new_name}', fourcc, fps, (int(cap.get(3)), int(cap.get(4))))
# cap是读取的temp_video.avi,这里面已经是处理好了的相似性;然后去里面拿数据就可以了
for frame in extraction_frame(cap, cv_total_msec, fps): # 这个函数里面是提取动态目标的
out.write(frame) # 往out里面写图像帧
cap.release()
out.release()
# 抽帧
def extraction_frame(cap, cv_total_msec, fps):
"""
视频抽取指定时间的所有帧
:param cap: 待处理的原始视频
:param cv_total_msec: 总时长(毫秒)
:param fps: 帧率
:return: 被抽取的每一帧
"""
pts_time_last = 0
for pts_time_str in read_time_message():
pts_time = int(float(pts_time_str) * 1000) # 毫秒
# 如果相邻两帧时间差值相距超过 skip_frame 数量的帧, 则直接跳转到该帧操作, 不再逐帧跳过
# if (pts_time - pts_time_last) / 1000 * fps >= skip_frame:
# cap.set(cv2.CAP_PROP_POS_MSEC, pts_time) # 跳转到指定毫秒数, 比较耗时 ≈0.3秒
# ret, frame = cap.retrieve()
# pts_time_last = pts_time
# sys.stdout.write(f"\r 进度_抽帧:{round(pts_time / cv_total_msec * 100, 2)}")
# sys.stdout.flush()
# yield frame
# continue
while True:
ret = cap.grab() # 逐帧跳过
if not ret: break
if cap.get(cv2.CAP_PROP_POS_MSEC) < pts_time: continue # 逐帧跳过
ret, frame = cap.retrieve()
pts_time_last = pts_time
# print(round(pts_time / cv_total_msec * 100, 2))
sys.stdout.write(f"\r 进度:{round(pts_time / cv_total_msec * 100, 2)}")
sys.stdout.flush()
yield frame
break
# def main(file_path):
transcoding_video(file_path) # step1: org.avi --> tmp.avi copy
# min_x, min_y, width, height = get_rectangle_point(file_path) # step2: get roi points 可以写成配置文件 单独来获取
index_1_coordinates = coordinates.get(index)
min_x, min_y, width, height = index_1_coordinates
print('剪切坐标', min_x, min_y, width, height)
begin = time.time()
crate_mpdecimate_log(file_path, min_x, min_y, width, height) # step3: 创建日志信息 关键地方 给ffmpeg下指定,结果保存在log中
# 这个ROI区域的文件也是可以自己写成配置文件的!!!
end = time.time()
print('临时日志文件已生成')
print(f'生成日志耗时: {end - begin}')
begin = time.time()
temp_file_path = f'temp_video.{file_path.split(".")[-1]}'
# makedir_in_newRootDir(hours_path, output_rootDir) # step4: 把需要的路径生成好,保存路径
video_time_compress(temp_file_path, hours_path, output_rootDir)
end = time.time()
print(f'压缩耗时: {end - begin}') # 逐帧跳过压缩(总共40W帧,读取7300帧)耗时657秒 # 按时间节点跳帧压缩(读取7300帧)耗时: 129秒
# main(file_path)
# if __name__ == '__main__':
# main()
5. 多个mp4拼接为一个大的MP4文件
1. 查找所有同区域的mp4
2. 多个mp4合并
import os
def find_files(directory, pattern):
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(pattern):
yield os.path.join(root, file)
def write_file_paths_to_txt(directory, pattern, output_file):
with open(output_file, 'w') as f:
for file_path in find_files(directory, pattern):
f.write("file '{}'\n".format(file_path))
def merge_videos_from_txt(input_txt, output_file):
ffmpeg_cmd = "G:/zhr/ffmpeg6/ffmpeg.exe -f concat -safe 0 -i {} -c copy {}".format(input_txt, output_file)
os.system(ffmpeg_cmd)
# 搜索 *_4.mp4 文件的目录
search_directory = 'E:/aomen_video_20240326/a5_y_3'
# 要搜索的模式
file_pattern = '_5.mp4'
# 输出文本文件的路径
txt_output_path = 'a5_y_3_5.txt'
# 查找文件并将路径写入文本文件
write_file_paths_to_txt(search_directory, file_pattern, txt_output_path)
# 合并文本文件中列出的视频
merge_videos_from_txt(txt_output_path, 'a5_y_3_5.mp4')