qq1194550395 il y a 2 jours
commit
64d260a74b

+ 8 - 0
.idea/data.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 94 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,94 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <value>
+          <list size="74">
+            <item index="0" class="java.lang.String" itemvalue="tqdm" />
+            <item index="1" class="java.lang.String" itemvalue="scipy" />
+            <item index="2" class="java.lang.String" itemvalue="h5py" />
+            <item index="3" class="java.lang.String" itemvalue="matplotlib" />
+            <item index="4" class="java.lang.String" itemvalue="labelme" />
+            <item index="5" class="java.lang.String" itemvalue="numpy" />
+            <item index="6" class="java.lang.String" itemvalue="opencv_python" />
+            <item index="7" class="java.lang.String" itemvalue="Pillow" />
+            <item index="8" class="java.lang.String" itemvalue="python-lsp-server" />
+            <item index="9" class="java.lang.String" itemvalue="PyYAML" />
+            <item index="10" class="java.lang.String" itemvalue="cycler" />
+            <item index="11" class="java.lang.String" itemvalue="locket" />
+            <item index="12" class="java.lang.String" itemvalue="patsy" />
+            <item index="13" class="java.lang.String" itemvalue="tables" />
+            <item index="14" class="java.lang.String" itemvalue="TBB" />
+            <item index="15" class="java.lang.String" itemvalue="mccabe" />
+            <item index="16" class="java.lang.String" itemvalue="certifi" />
+            <item index="17" class="java.lang.String" itemvalue="entrypoints" />
+            <item index="18" class="java.lang.String" itemvalue="anaconda-navigator" />
+            <item index="19" class="java.lang.String" itemvalue="bkcharts" />
+            <item index="20" class="java.lang.String" itemvalue="comtypes" />
+            <item index="21" class="java.lang.String" itemvalue="pywin32" />
+            <item index="22" class="java.lang.String" itemvalue="clyent" />
+            <item index="23" class="java.lang.String" itemvalue="navigator-updater" />
+            <item index="24" class="java.lang.String" itemvalue="terminado" />
+            <item index="25" class="java.lang.String" itemvalue="wincertstore" />
+            <item index="26" class="java.lang.String" itemvalue="xlwings" />
+            <item index="27" class="java.lang.String" itemvalue="win-unicode-console" />
+            <item index="28" class="java.lang.String" itemvalue="pyodbc" />
+            <item index="29" class="java.lang.String" itemvalue="boto" />
+            <item index="30" class="java.lang.String" itemvalue="daal4py" />
+            <item index="31" class="java.lang.String" itemvalue="cytoolz" />
+            <item index="32" class="java.lang.String" itemvalue="dask" />
+            <item index="33" class="java.lang.String" itemvalue="scikit-image" />
+            <item index="34" class="java.lang.String" itemvalue="brotlipy" />
+            <item index="35" class="java.lang.String" itemvalue="pycurl" />
+            <item index="36" class="java.lang.String" itemvalue="scikit-learn-intelex" />
+            <item index="37" class="java.lang.String" itemvalue="mypy-extensions" />
+            <item index="38" class="java.lang.String" itemvalue="pep8" />
+            <item index="39" class="java.lang.String" itemvalue="pycosat" />
+            <item index="40" class="java.lang.String" itemvalue="llvmlite" />
+            <item index="41" class="java.lang.String" itemvalue="vboxapi" />
+            <item index="42" class="java.lang.String" itemvalue="simplegeneric" />
+            <item index="43" class="java.lang.String" itemvalue="mkl-fft" />
+            <item index="44" class="java.lang.String" itemvalue="pkginfo" />
+            <item index="45" class="java.lang.String" itemvalue="conda" />
+            <item index="46" class="java.lang.String" itemvalue="sip" />
+            <item index="47" class="java.lang.String" itemvalue="urllib3" />
+            <item index="48" class="java.lang.String" itemvalue="pyreadline" />
+            <item index="49" class="java.lang.String" itemvalue="zope.event" />
+            <item index="50" class="java.lang.String" itemvalue="python-lsp-jsonrpc" />
+            <item index="51" class="java.lang.String" itemvalue="pytest" />
+            <item index="52" class="java.lang.String" itemvalue="conda-build" />
+            <item index="53" class="java.lang.String" itemvalue="xlwt" />
+            <item index="54" class="java.lang.String" itemvalue="you-get" />
+            <item index="55" class="java.lang.String" itemvalue="et-xmlfile" />
+            <item index="56" class="java.lang.String" itemvalue="Sphinx" />
+            <item index="57" class="java.lang.String" itemvalue="mpmath" />
+            <item index="58" class="java.lang.String" itemvalue="statsmodels" />
+            <item index="59" class="java.lang.String" itemvalue="unicodecsv" />
+            <item index="60" class="java.lang.String" itemvalue="argh" />
+            <item index="61" class="java.lang.String" itemvalue="torch" />
+            <item index="62" class="java.lang.String" itemvalue="torchvision" />
+            <item index="63" class="java.lang.String" itemvalue="pycocotools" />
+            <item index="64" class="java.lang.String" itemvalue="fonttools" />
+            <item index="65" class="java.lang.String" itemvalue="pathspec" />
+            <item index="66" class="java.lang.String" itemvalue="munkres" />
+            <item index="67" class="java.lang.String" itemvalue="backports.weakref" />
+            <item index="68" class="java.lang.String" itemvalue="conda-verify" />
+            <item index="69" class="java.lang.String" itemvalue="nltk" />
+            <item index="70" class="java.lang.String" itemvalue="black" />
+            <item index="71" class="java.lang.String" itemvalue="zict" />
+            <item index="72" class="java.lang.String" itemvalue="pytz" />
+            <item index="73" class="java.lang.String" itemvalue="click" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredIdentifiers">
+        <list>
+          <option value="tensorflow.*" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/data.iml" filepath="$PROJECT_DIR$/.idea/data.iml" />
+    </modules>
+  </component>
+</project>

+ 52 - 0
.idea/workspace.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ChangeListManager">
+    <list default="true" id="79ada782-d6ad-4190-85fb-b5e1315db518" name="Changes" comment="" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="MarkdownSettingsMigration">
+    <option name="stateVersion" value="1" />
+  </component>
+  <component name="ProjectColorInfo">{
+  &quot;associatedIndex&quot;: 5
+}</component>
+  <component name="ProjectId" id="333JKGQMrve2MdCU3tuaJamJdku" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent">{
+  &quot;keyToString&quot;: {
+    &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
+    &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
+    &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
+    &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
+  }
+}</component>
+  <component name="SharedIndexes">
+    <attachedChunks>
+      <set>
+        <option value="bundled-python-sdk-d68999036c7f-b11f5e8da5ad-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-233.14475.56" />
+      </set>
+    </attachedChunks>
+  </component>
+  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="79ada782-d6ad-4190-85fb-b5e1315db518" name="Changes" comment="" />
+      <created>1758539760106</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1758539760106</updated>
+      <workItem from="1758539762318" duration="2000" />
+      <workItem from="1760323230023" duration="8000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="3" />
+  </component>
+</project>

+ 116 - 0
20251102.py

@@ -0,0 +1,116 @@
+import os
+
+# ================= 核心配置 =================
+# 根据你的截图,你的根目录应该是这一层(包含 images 和 labels 的上一级)
+# 请务必核对这个路径是否正确!
+root_path = r"D:\data\20251210\20251210"
+
+img_folder_name = "images"
+lbl_folder_name = "labels"
+sub_folders = ["test", "train", "val"]
+global_index = 0
+digit_padding = 6
+# 支持的后缀(已忽略大小写)
+valid_image_exts = ['.jpg', '.jpeg', '.png', '.bmp']
+valid_label_exts = ['.txt', '.xml', '.json']
+# ===========================================
+
+def debug_sync_rename():
+    global global_index
+    
+    print("----------- 开始检查路径 -----------")
+    if not os.path.exists(root_path):
+        print(f"❌ 错误:根目录不存在 -> {root_path}")
+        print("请检查路径中是否有空格、拼写错误,或者盘符不对。")
+        input("按回车键退出...")
+        return
+    else:
+        print(f"✅ 根目录存在: {root_path}")
+
+    images_root = os.path.join(root_path, img_folder_name)
+    labels_root = os.path.join(root_path, lbl_folder_name)
+
+    if not os.path.exists(images_root):
+        print(f"❌ 错误:找不到 images 文件夹 -> {images_root}")
+        input("按回车键退出...")
+        return
+    
+    print(f"✅ images 目录存在: {images_root}")
+    print(f"✅ labels 目录 (期望路径): {labels_root}")
+    print("------------------------------------")
+
+    # 遍历子文件夹
+    for sub in sub_folders:
+        curr_img_dir = os.path.join(images_root, sub)
+        curr_lbl_dir = os.path.join(labels_root, sub)
+
+        print(f"\n正在扫描子文件夹: [{sub}]")
+        
+        if not os.path.exists(curr_img_dir):
+            print(f"⚠️ 跳过:找不到图片子目录 -> {curr_img_dir}")
+            continue
+
+        # 获取文件
+        try:
+            files = os.listdir(curr_img_dir)
+        except Exception as e:
+            print(f"❌ 读取文件夹出错: {e}")
+            continue
+
+        # 筛选图片
+        img_files = sorted([f for f in files if os.path.splitext(f)[1].lower() in valid_image_exts])
+        
+        if len(img_files) == 0:
+            print(f"⚠️ 在 {sub} 中没有找到支持的图片文件!")
+            print(f"   该文件夹下的前5个文件是: {files[:5]}")
+            continue
+        
+        print(f"   -> 发现 {len(img_files)} 张图片,准备处理...")
+
+        # 开始重命名循环
+        count = 0
+        for old_img_name in img_files:
+            stem, img_ext = os.path.splitext(old_img_name)
+            
+            # 构造新名字
+            new_stem = f"{global_index:0{digit_padding}d}"
+            
+            # 1. 改名图片
+            old_img_path = os.path.join(curr_img_dir, old_img_name)
+            new_img_name = f"{new_stem}{img_ext}"
+            new_img_path = os.path.join(curr_img_dir, new_img_name)
+            
+            # 2. 改名标签
+            label_renamed = False
+            for lbl_ext in valid_label_exts:
+                old_lbl_name = f"{stem}{lbl_ext}"
+                old_lbl_path = os.path.join(curr_lbl_dir, old_lbl_name)
+                
+                if os.path.exists(old_lbl_path):
+                    new_lbl_name = f"{new_stem}{lbl_ext}"
+                    new_lbl_path = os.path.join(curr_lbl_dir, new_lbl_name)
+                    
+                    if old_lbl_path != new_lbl_path:
+                        os.rename(old_lbl_path, new_lbl_path)
+                    label_renamed = True
+                    break # 找到对应标签就不找其他后缀了
+            
+            # 执行图片改名
+            if old_img_path != new_img_path:
+                os.rename(old_img_path, new_img_path)
+
+            global_index += 1
+            count += 1
+            
+            # 为了避免刷屏,只打印前3个和最后一个的处理记录
+            if count <= 3:
+                status = "同步修改成功" if label_renamed else "只改了图片(无标签)"
+                print(f"   [{status}] {old_img_name} -> {new_img_name}")
+
+        print(f"   -> {sub} 文件夹处理完毕。")
+
+    print(f"\n🎉 所有操作结束!最大序号停留在: {global_index - 1}")
+    input("按回车键退出...")
+
+if __name__ == "__main__":
+    debug_sync_rename()

+ 37 - 0
README_TRAIN.md

@@ -0,0 +1,37 @@
+# YOLOv8 训练说明
+
+准备:
+
+- 推荐创建并激活 Python 虚拟环境。
+- 编辑 `dataset_template.yaml`(或复制为 `dataset.yaml`),确保 `train`/`val`/`test` 指向你的图片文件夹,且 `nc` 与 `names` 正确。
+
+安装依赖(Windows PowerShell):
+
+```powershell
+python -m venv .venv; .\.venv\Scripts\Activate.ps1
+python -m pip install --upgrade pip
+pip install -r requirements-train.txt
+```
+
+训练示例(PowerShell):
+
+```powershell
+# 使用预训练的 yolov8n 模型,100 epochs,batch 16
+python train_yolov8.py --data dataset.yaml --model yolov8n.pt --epochs 100 --batch 16
+
+# 指定输出目录和实验名字
+python train_yolov8.py --data dataset.yaml --model yolov8n.pt --epochs 50 --project runs/train --name my_experiment
+
+# 在 CPU 上运行
+python train_yolov8.py --data dataset.yaml --model yolov8n.pt --device cpu
+```
+
+注意事项:
+
+- 如果你希望使用 GPU,请确保已正确安装与 CUDA 版本匹配的 `torch`。如果未安装 GPU 版 `torch`,程序会自动使用 CPU。
+- 如果数据集标签不是 YOLO 格式(每张图片对应一个 `.txt`,每行 `class x_center y_center width height`),请先转换为 YOLO 格式。
+- 训练结束后的权重和日志位于 `runs/train/<name>/` 下。
+
+如需我帮你:
+- 根据你的数据集结构生成 `dataset.yaml`。
+- 自动分割训练/验证集并生成标签。

BIN
SteamSetup.exe


BIN
__pycache__/json_to_yolo.cpython-312.pyc


BIN
__pycache__/video_frame_extractor.cpython-310.pyc


BIN
__pycache__/video_segment_extractor.cpython-312.pyc


+ 100 - 0
autolabel.py

@@ -0,0 +1,100 @@
+import os
+from ultralytics import YOLO
+
+def auto_label(image_dir, output_dir, model_path='model/best.pt', conf=0.5, save_segmentation=True):
+    """
+    使用 YOLO 模型自动标注图片并生成 TXT 标签文件。
+    """
+    # 检查模型文件是否存在
+    if not os.path.exists(model_path):
+        print(f"错误: 模型文件不存在 -> {model_path}")
+        # 尝试使用根目录下的 yolov8n-seg.pt 作为备选
+        fallback_model = 'yolov8n-seg.pt'
+        if os.path.exists(fallback_model):
+            print(f"尝试使用备选模型 -> {fallback_model}")
+            model_path = fallback_model
+        else:
+            return
+
+    # 加载模型
+    print(f"正在加载模型: {model_path} ...")
+    model = YOLO(model_path)
+
+    # 确保输出目录存在
+    if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+        print(f"创建输出目录: {output_dir}")
+
+    # 支持的图片格式
+    img_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
+    
+    # 获取图片列表
+    image_files = [f for f in os.listdir(image_dir) if f.lower().endswith(img_extensions)]
+    total_files = len(image_files)
+    
+    print(f"找到 {total_files} 张图片,开始自动标注...")
+
+    count = 0
+    for idx, img_file in enumerate(image_files):
+        img_path = os.path.join(image_dir, img_file)
+        txt_filename = os.path.splitext(img_file)[0] + ".txt"
+        txt_path = os.path.join(output_dir, txt_filename)
+
+        # 推理
+        # stream=True 可以节省内存,但对于单张处理差异不大
+        results = model(img_path, conf=conf, verbose=False)
+
+        for result in results:
+            with open(txt_path, 'w') as f:
+                # 优先尝试保存分割掩码
+                if save_segmentation and result.masks is not None:
+                    # result.masks.xyn 获取归一化的多边形坐标片段
+                    if hasattr(result.masks, 'xyn'):
+                        for i, seg in enumerate(result.masks.xyn):
+                            cls = int(result.boxes.cls[i]) # 对应的类别
+                            # 格式: class x1 y1 x2 y2 ...
+                            # flatten() 将数组展平,tolist() 转为列表
+                            coords = " ".join([f"{p[0]:.6f} {p[1]:.6f}" for p in seg])
+                            f.write(f"{cls} {coords}\n")
+                    else:
+                        pass # 兼容性处理
+                
+                # 如果没有分割结果或未启用分割,保存检测框
+                elif result.boxes is not None:
+                    for box in result.boxes:
+                        cls = int(box.cls)
+                        # xywhn: x_center, y_center, width, height (normalized)
+                        x, y, w, h = box.xywhn[0].tolist()
+                        f.write(f"{cls} {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n")
+        
+        count += 1
+        if count % 10 == 0:
+            print(f"进度: {count}/{total_files}")
+
+    print(f"完成!已生成 {count} 个标签文件。保存位置: {output_dir}")
+
+if __name__ == "__main__":
+    # --- 配置区域 (请在此修改路径) ---
+    
+    # 待标注图片文件夹 (例如: d:\data\20251204)
+    IMAGE_DIR = r'd:\data\20251204' 
+    
+    # 标签保存文件夹 (例如: d:\data\20251204\labels_auto)
+    OUTPUT_DIR = r'd:\data\20251204\labels_auto' 
+    
+    # 模型路径 (例如: d:\data\model\best.pt)
+    MODEL_PATH = r'd:\data\best.pt'
+    
+    # 置信度阈值
+    CONF_THRESHOLD = 0.5
+    
+    # 是否保存分割点 (True=保存多边形, False=只保存矩形框)
+    SAVE_SEGMENTATION = True 
+    # --------------------------------
+    
+    # 检查输入目录是否存在,防止报错
+    if not os.path.exists(IMAGE_DIR):
+        print(f"错误: 图片目录不存在 -> {IMAGE_DIR}")
+        print("请打开脚本修改 IMAGE_DIR 为正确的路径。")
+    else:
+        auto_label(IMAGE_DIR, OUTPUT_DIR, MODEL_PATH, CONF_THRESHOLD, SAVE_SEGMENTATION)

BIN
best.pt


+ 78 - 0
check.py

@@ -0,0 +1,78 @@
+import os
+import argparse
+from pathlib import Path
+
+def sync_folders(images_dir, labels_dir, dry_run=False):
+    images_dir = Path(images_dir)
+    labels_dir = Path(labels_dir)
+
+    if not images_dir.exists():
+        raise FileNotFoundError(f"Images directory not found: {images_dir}")
+    if not labels_dir.exists():
+        raise FileNotFoundError(f"Labels directory not found: {labels_dir}")
+
+    # 支持的图像扩展名(可根据需要修改)
+    IMG_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
+
+    # 获取所有图像文件(不含扩展名)和标签文件(不含扩展名)
+    image_files = {
+        f.stem: f for f in images_dir.iterdir()
+        if f.is_file() and f.suffix.lower() in IMG_EXTENSIONS
+    }
+    label_files = {
+        f.stem: f for f in labels_dir.iterdir()
+        if f.is_file() and f.suffix.lower() == '.txt'
+    }
+
+    image_names = set(image_files.keys())
+    label_names = set(label_files.keys())
+
+    # 找出不匹配的部分
+    images_only = image_names - label_names      # 有图无标签
+    labels_only = label_names - image_names      # 有标签无图
+
+    print(f"总图像数: {len(image_names)}")
+    print(f"总标签数: {len(label_names)}")
+    print(f"仅有图像(无对应标签): {len(images_only)} 个")
+    print(f"仅有标签(无对应图像): {len(labels_only)} 个")
+
+    if not images_only and not labels_only:
+        print("✅ 所有图像和标签已对齐!")
+        return
+
+    to_delete = []
+
+    # 默认策略:删除“仅有图像”和“仅有标签”的文件
+    for name in images_only:
+        to_delete.append(image_files[name])
+    for name in labels_only:
+        to_delete.append(label_files[name])
+
+    print("\n将删除以下文件:")
+    for f in to_delete:
+        print(f"  - {f}")
+
+    if dry_run:
+        print("\n[DRY RUN] 未执行实际删除。")
+        return
+
+    confirm = input("\n⚠️ 确认删除以上文件?(y/N): ").strip().lower()
+    if confirm == 'y':
+        for f in to_delete:
+            try:
+                f.unlink()
+                print(f"✅ 已删除: {f}")
+            except Exception as e:
+                print(f"❌ 删除失败: {f}, 错误: {e}")
+        print("清理完成。")
+    else:
+        print("操作已取消。")
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description="同步 images 和 labels 文件夹,删除不匹配的文件")
+    parser.add_argument("images_dir", help="图像文件夹路径")
+    parser.add_argument("labels_dir", help="标签文件夹路径")
+    parser.add_argument("--dry-run", action="store_true", help="仅显示将要删除的文件,不实际删除")
+
+    args = parser.parse_args()
+    sync_folders(args.images_dir, args.labels_dir, dry_run=args.dry_run)

+ 5 - 0
classes.txt

@@ -0,0 +1,5 @@
+0 fire
+1 dust
+2 move_machine
+3 open_machine
+4 close_machine

+ 11 - 0
dataset_template.yaml

@@ -0,0 +1,11 @@
+# YOLOv8 dataset YAML template
+# 修改路径以匹配你的数据集结构
+train: ./images/train
+val: ./images/val
+test: ./images/test
+
+# 类别数量
+nc: 1
+
+# 类别名字列表
+names: ['class0']

+ 101 - 0
frame_reorganizer.py

@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+图片帧重新组织脚本
+将extracted_frames_test文件夹中的所有图片按连续序号重命名并合并到一个目录
+"""
+
+import os
+import shutil
+from pathlib import Path
+import re
+
+def get_frame_number(filename):
+    """从文件名中提取帧序号"""
+    match = re.search(r'frame_(\d+)', filename)
+    return int(match.group(1)) if match else 0
+
+def collect_all_frames(source_dir):
+    """收集所有子文件夹中的图片文件"""
+    all_frames = []
+    source_path = Path(source_dir)
+    
+    # 获取所有子文件夹并按名称排序
+    subdirs = sorted([d for d in source_path.iterdir() if d.is_dir()])
+    
+    for subdir in subdirs:
+        print(f"处理文件夹: {subdir.name}")
+        
+        # 获取该文件夹中的所有jpg文件
+        jpg_files = list(subdir.glob('*.jpg'))
+        
+        # 按帧序号排序
+        jpg_files.sort(key=lambda x: get_frame_number(x.name))
+        
+        for jpg_file in jpg_files:
+            all_frames.append({
+                'original_path': jpg_file,
+                'folder_name': subdir.name,
+                'original_frame_num': get_frame_number(jpg_file.name)
+            })
+        
+        print(f"  找到 {len(jpg_files)} 个图片文件")
+    
+    return all_frames
+
+def reorganize_frames(source_dir, output_dir):
+    """重新组织和重命名图片文件"""
+    # 创建输出目录
+    output_path = Path(output_dir)
+    output_path.mkdir(exist_ok=True)
+    
+    # 收集所有图片文件
+    all_frames = collect_all_frames(source_dir)
+    
+    print(f"\n总共找到 {len(all_frames)} 个图片文件")
+    print("开始重命名和复制...")
+    
+    # 按新的连续序号重命名并复制
+    for new_index, frame_info in enumerate(all_frames):
+        # 生成新的文件名
+        new_filename = f"frame_{new_index:06d}.jpg"
+        new_path = output_path / new_filename
+        
+        # 复制文件
+        shutil.copy2(frame_info['original_path'], new_path)
+        
+        if new_index % 100 == 0:  # 每100个文件显示一次进度
+            print(f"  已处理: {new_index + 1}/{len(all_frames)}")
+    
+    print(f"\n完成!所有文件已重命名并复制到: {output_dir}")
+    print(f"文件命名范围: frame_000000.jpg 到 frame_{len(all_frames)-1:06d}.jpg")
+    
+    return len(all_frames)
+
+def main():
+    """主函数"""
+    # 设置路径
+    source_dir = r"d:\data\extracted_frames_test"
+    output_dir = r"d:\data\reorganized_frames"
+    
+    print("图片帧重新组织脚本")
+    print(f"源目录: {source_dir}")
+    print(f"输出目录: {output_dir}")
+    print("-" * 50)
+    
+    # 检查源目录是否存在
+    if not os.path.exists(source_dir):
+        print(f"错误: 源目录不存在 - {source_dir}")
+        return
+    
+    try:
+        # 执行重组织
+        total_files = reorganize_frames(source_dir, output_dir)
+        print(f"\n成功处理了 {total_files} 个图片文件")
+        
+    except Exception as e:
+        print(f"错误: {e}")
+        return
+
+if __name__ == "__main__":
+    main()

+ 505 - 0
json_to_yolo.py

@@ -0,0 +1,505 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+JSON标签到YOLO格式转换脚本
+支持多种常见的JSON标注格式转换为YOLO格式
+
+功能特性:
+- 支持LabelMe、COCO、YOLO等多种JSON格式
+- 矩形标注转换为YOLO边界框格式 (class_id x_center y_center width height)
+- 多边形标注保留所有点位信息 (class_id x1 y1 x2 y2 ... xn yn)
+- 自动归一化坐标到[0,1]范围
+- 支持自定义类别映射文件
+"""
+
+import os
+import json
+import glob
+from pathlib import Path
+import argparse
+
+def convert_bbox_to_yolo(bbox, img_width, img_height, format_type="xywh"):
+    """
+    将边界框坐标转换为YOLO格式(归一化的中心点坐标和宽高)
+    
+    Args:
+        bbox: 边界框坐标
+        img_width: 图片宽度
+        img_height: 图片高度
+        format_type: 输入格式类型 ("xywh", "xyxy", "coco")
+    
+    Returns:
+        tuple: (center_x, center_y, width, height) 归一化坐标
+    """
+    
+    if format_type == "xyxy":
+        # 格式: [x_min, y_min, x_max, y_max]
+        x_min, y_min, x_max, y_max = bbox
+        width = x_max - x_min
+        height = y_max - y_min
+        center_x = x_min + width / 2
+        center_y = y_min + height / 2
+        
+    elif format_type == "xywh":
+        # 格式: [x, y, width, height] (左上角坐标)
+        x, y, width, height = bbox
+        center_x = x + width / 2
+        center_y = y + height / 2
+        
+    elif format_type == "coco":
+        # COCO格式: [x, y, width, height] (左上角坐标)
+        x, y, width, height = bbox
+        center_x = x + width / 2
+        center_y = y + height / 2
+        
+    else:
+        raise ValueError(f"不支持的格式类型: {format_type}")
+    
+    # 归一化坐标
+    center_x_norm = center_x / img_width
+    center_y_norm = center_y / img_height
+    width_norm = width / img_width
+    height_norm = height / img_height
+    
+    return center_x_norm, center_y_norm, width_norm, height_norm
+
+def convert_polygon_to_yolo(points, img_width, img_height):
+    """
+    将多边形点位转换为YOLO格式(归一化坐标)
+    
+    Args:
+        points: 多边形点位列表 [[x1, y1], [x2, y2], ...]
+        img_width: 图片宽度
+        img_height: 图片高度
+    
+    Returns:
+        list: 归一化的点位坐标 [x1_norm, y1_norm, x2_norm, y2_norm, ...]
+    """
+    normalized_points = []
+    
+    for point in points:
+        x, y = point
+        # 归一化坐标
+        x_norm = x / img_width
+        y_norm = y / img_height
+        normalized_points.extend([x_norm, y_norm])
+    
+    return normalized_points
+
+def parse_labelme_json(json_data):
+    """
+    解析LabelMe格式的JSON文件
+    
+    Args:
+        json_data: JSON数据
+        
+    Returns:
+        list: 包含(class_name, bbox)的列表
+    """
+    annotations = []
+    img_width = json_data.get('imageWidth', 0)
+    img_height = json_data.get('imageHeight', 0)
+    
+    if img_width == 0 or img_height == 0:
+        raise ValueError("JSON文件中缺少图片尺寸信息")
+    
+    for shape in json_data.get('shapes', []):
+        label = shape.get('label', '')
+        shape_type = shape.get('shape_type', 'rectangle')
+        points = shape.get('points', [])
+        
+        if shape_type == 'rectangle' and len(points) == 2:
+            # 矩形格式: [[x1, y1], [x2, y2]]
+            x1, y1 = points[0]
+            x2, y2 = points[1]
+            
+            # 确保坐标顺序正确
+            x_min = min(x1, x2)
+            y_min = min(y1, y2)
+            x_max = max(x1, x2)
+            y_max = max(y1, y2)
+            
+            bbox = [x_min, y_min, x_max, y_max]
+            annotations.append((label, bbox, "xyxy", img_width, img_height))
+            
+        elif shape_type == 'polygon' and len(points) >= 3:
+            # 多边形格式: 保留所有点位信息
+            annotations.append((label, points, "polygon", img_width, img_height))
+    
+    return annotations
+
+def parse_coco_json(json_data):
+    """
+    解析COCO格式的JSON文件
+    
+    Args:
+        json_data: JSON数据
+        
+    Returns:
+        dict: 按图片ID分组的标注信息
+    """
+    # 构建类别映射
+    categories = {cat['id']: cat['name'] for cat in json_data.get('categories', [])}
+    
+    # 构建图片信息映射
+    images = {img['id']: img for img in json_data.get('images', [])}
+    
+    # 按图片分组标注
+    annotations_by_image = {}
+    
+    for ann in json_data.get('annotations', []):
+        image_id = ann['image_id']
+        category_id = ann['category_id']
+        bbox = ann['bbox']  # COCO格式: [x, y, width, height]
+        
+        if image_id not in annotations_by_image:
+            annotations_by_image[image_id] = []
+        
+        if image_id in images:
+            img_info = images[image_id]
+            img_width = img_info['width']
+            img_height = img_info['height']
+            class_name = categories.get(category_id, f'class_{category_id}')
+            
+            annotations_by_image[image_id].append((
+                class_name, bbox, "coco", img_width, img_height, img_info['file_name']
+            ))
+    
+    return annotations_by_image
+
+def parse_yolo_json(json_data):
+    """
+    解析自定义YOLO JSON格式
+    
+    Args:
+        json_data: JSON数据
+        
+    Returns:
+        list: 包含(class_name, bbox)的列表
+    """
+    annotations = []
+    img_width = json_data.get('image_width', json_data.get('width', 0))
+    img_height = json_data.get('image_height', json_data.get('height', 0))
+    
+    if img_width == 0 or img_height == 0:
+        raise ValueError("JSON文件中缺少图片尺寸信息")
+    
+    for obj in json_data.get('objects', json_data.get('annotations', [])):
+        class_name = obj.get('class', obj.get('category', obj.get('label', '')))
+        
+        # 支持多种边界框格式
+        if 'bbox' in obj:
+            bbox = obj['bbox']
+            bbox_format = obj.get('bbox_format', 'xywh')
+        elif 'bounding_box' in obj:
+            bbox = obj['bounding_box']
+            bbox_format = obj.get('bbox_format', 'xywh')
+        elif all(k in obj for k in ['x', 'y', 'width', 'height']):
+            bbox = [obj['x'], obj['y'], obj['width'], obj['height']]
+            bbox_format = 'xywh'
+        elif all(k in obj for k in ['x_min', 'y_min', 'x_max', 'y_max']):
+            bbox = [obj['x_min'], obj['y_min'], obj['x_max'], obj['y_max']]
+            bbox_format = 'xyxy'
+        else:
+            print(f"警告: 无法解析对象的边界框格式: {obj}")
+            continue
+        
+        annotations.append((class_name, bbox, bbox_format, img_width, img_height))
+    
+    return annotations
+
+def convert_json_to_yolo(json_file_path, output_dir, class_mapping=None, json_format="auto"):
+    """
+    将JSON标注文件转换为YOLO格式
+    
+    Args:
+        json_file_path: JSON文件路径
+        output_dir: 输出目录
+        class_mapping: 类别名称到ID的映射字典
+        json_format: JSON格式类型 ("auto", "labelme", "coco", "yolo")
+    """
+    
+    with open(json_file_path, 'r', encoding='utf-8') as f:
+        json_data = json.load(f)
+    
+    # 自动检测JSON格式
+    if json_format == "auto":
+        if 'shapes' in json_data and 'imageWidth' in json_data:
+            json_format = "labelme"
+        elif 'categories' in json_data and 'annotations' in json_data and 'images' in json_data:
+            json_format = "coco"
+        else:
+            json_format = "yolo"
+    
+    print(f"检测到JSON格式: {json_format}")
+    
+    # 解析JSON数据
+    if json_format == "labelme":
+        annotations = parse_labelme_json(json_data)
+        # 为LabelMe格式生成单个txt文件
+        base_name = Path(json_file_path).stem
+        output_file = os.path.join(output_dir, f"{base_name}.txt")
+        
+        with open(output_file, 'w', encoding='utf-8') as f:
+            for class_name, data, data_format, img_width, img_height in annotations:
+                # 获取类别ID
+                if class_mapping and class_name in class_mapping:
+                    class_id = class_mapping[class_name]
+                else:
+                    class_id = 0  # 默认类别ID
+                
+                if data_format == "polygon":
+                    # 处理多边形点位
+                    normalized_points = convert_polygon_to_yolo(data, img_width, img_height)
+                    # 写入YOLO格式的多边形标注
+                    points_str = ' '.join([f"{coord:.6f}" for coord in normalized_points])
+                    f.write(f"{class_id} {points_str}\n")
+                else:
+                    # 处理边界框
+                    center_x, center_y, width, height = convert_bbox_to_yolo(
+                        data, img_width, img_height, data_format
+                    )
+                    # 写入YOLO格式的边界框标注
+                    f.write(f"{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n")
+        
+        print(f"已生成: {output_file}")
+        
+    elif json_format == "coco":
+        annotations_by_image = parse_coco_json(json_data)
+        
+        for image_id, annotations in annotations_by_image.items():
+            if not annotations:
+                continue
+                
+            # 使用第一个标注的文件名信息
+            file_name = annotations[0][5]  # file_name
+            base_name = Path(file_name).stem
+            output_file = os.path.join(output_dir, f"{base_name}.txt")
+            
+            with open(output_file, 'w', encoding='utf-8') as f:
+                for class_name, bbox, bbox_format, img_width, img_height, _ in annotations:
+                    # 获取类别ID
+                    if class_mapping and class_name in class_mapping:
+                        class_id = class_mapping[class_name]
+                    else:
+                        class_id = 0  # 默认类别ID
+                    
+                    # 转换为YOLO格式
+                    center_x, center_y, width, height = convert_bbox_to_yolo(
+                        bbox, img_width, img_height, bbox_format
+                    )
+                    
+                    # 写入YOLO格式
+                    f.write(f"{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n")
+            
+            print(f"已生成: {output_file}")
+            
+    elif json_format == "yolo":
+        annotations = parse_yolo_json(json_data)
+        base_name = Path(json_file_path).stem
+        output_file = os.path.join(output_dir, f"{base_name}.txt")
+        
+        with open(output_file, 'w', encoding='utf-8') as f:
+            for class_name, bbox, bbox_format, img_width, img_height in annotations:
+                # 获取类别ID
+                if class_mapping and class_name in class_mapping:
+                    class_id = class_mapping[class_name]
+                else:
+                    class_id = 0  # 默认类别ID
+                
+                # 转换为YOLO格式
+                center_x, center_y, width, height = convert_bbox_to_yolo(
+                    bbox, img_width, img_height, bbox_format
+                )
+                
+                # 写入YOLO格式
+                f.write(f"{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n")
+        
+        print(f"已生成: {output_file}")
+
+def load_class_mapping(mapping_file):
+    """
+    从文件加载类别映射
+    
+    Args:
+        mapping_file: 映射文件路径 (支持txt和json格式)
+        
+    Returns:
+        dict: 类别名称到ID的映射
+    """
+    if not os.path.exists(mapping_file):
+        return None
+    
+    mapping = {}
+    
+    if mapping_file.endswith('.json'):
+        with open(mapping_file, 'r', encoding='utf-8') as f:
+            mapping = json.load(f)
+    else:
+        # txt格式兼容:
+        # 1) "类名"(行号作为ID)
+        # 2) "ID 类名" 或 "ID,类名"(显式ID与类名)
+        # 3) "类名 ID"(显式ID在末尾)
+        # 会自动忽略行首/行尾的空白与注释(# 开始的内容)
+        with open(mapping_file, 'r', encoding='utf-8') as f:
+            for i, raw in enumerate(f):
+                line = raw.strip()
+                if not line:
+                    continue
+                # 去除行内注释
+                if '#' in line:
+                    line = line.split('#', 1)[0].strip()
+                if not line:
+                    continue
+
+                cls_name = None
+                cls_id = None
+
+                # 尝试按逗号分隔(例如:"0,fire")
+                if ',' in line:
+                    parts = [p.strip() for p in line.split(',') if p.strip()]
+                    if len(parts) == 2 and parts[0].isdigit():
+                        cls_id = int(parts[0])
+                        cls_name = parts[1]
+
+                # 若未解析到,尝试按空白分隔(例如:"0 fire" 或 "fire 0" 或 "fire")
+                if cls_name is None:
+                    tokens = [t for t in line.split() if t]
+                    if len(tokens) == 1:
+                        # 仅类名:按行号作为ID
+                        cls_name = tokens[0]
+                        cls_id = i
+                    elif len(tokens) >= 2:
+                        # 两段或以上:尝试识别前后是否为ID
+                        if tokens[0].isdigit():
+                            # "ID 类名(可能包含空格)"
+                            cls_id = int(tokens[0])
+                            cls_name = ' '.join(tokens[1:])
+                        elif tokens[-1].isdigit():
+                            # "类名(可能包含空格) ID"
+                            cls_id = int(tokens[-1])
+                            cls_name = ' '.join(tokens[:-1])
+                        else:
+                            # 都不是数字,则将整行视为类名,按行号作为ID
+                            cls_name = ' '.join(tokens)
+                            cls_id = i
+
+                if cls_name:
+                    mapping[cls_name] = cls_id
+    
+    return mapping
+
+def main():
+    parser = argparse.ArgumentParser(description='JSON标签到YOLO格式转换工具')
+    parser.add_argument('input_path', help='输入JSON文件或包含JSON文件的目录')
+    parser.add_argument('-o', '--output', default='./20251124/yolo_labels', help='输出目录 (默认: ./yolo_labels)')
+    parser.add_argument('-c', '--classes', help='类别映射文件 (txt或json格式)')
+    parser.add_argument('-f', '--format', choices=['auto', 'labelme', 'coco', 'yolo'], 
+                       default='auto', help='JSON格式类型 (默认: auto)')
+    parser.add_argument('--test', action='store_true', help='测试模式,仅显示解析结果不生成文件')
+    
+    args = parser.parse_args()
+    
+    # 创建输出目录
+    output_dir = args.output
+    if not args.test:
+        os.makedirs(output_dir, exist_ok=True)
+    
+    # 加载类别映射
+    class_mapping = None
+    if args.classes:
+        class_mapping = load_class_mapping(args.classes)
+        if class_mapping:
+            print(f"已加载类别映射: {class_mapping}")
+        else:
+            print(f"警告: 无法加载类别映射文件: {args.classes}")
+    
+    # 处理输入路径
+    input_path = args.input_path
+    
+    if os.path.isfile(input_path):
+        # 单个文件
+        json_files = [input_path]
+    elif os.path.isdir(input_path):
+        # 目录中的所有JSON文件
+        json_files = glob.glob(os.path.join(input_path, "*.json"))
+    else:
+        print(f"错误: 输入路径不存在: {input_path}")
+        return
+    
+    if not json_files:
+        print(f"错误: 在 {input_path} 中没有找到JSON文件")
+        return
+    
+    print(f"找到 {len(json_files)} 个JSON文件")
+    
+    # 转换文件
+    success_count = 0
+    error_count = 0
+    
+    for json_file in json_files:
+        try:
+            print(f"\n处理文件: {json_file}")
+            
+            if args.test:
+                # 测试模式:仅解析和显示信息
+                with open(json_file, 'r', encoding='utf-8') as f:
+                    json_data = json.load(f)
+                
+                print(f"  JSON键: {list(json_data.keys())}")
+                
+                if 'shapes' in json_data:
+                    print(f"  LabelMe格式,包含 {len(json_data['shapes'])} 个标注")
+                elif 'annotations' in json_data:
+                    print(f"  COCO格式,包含 {len(json_data['annotations'])} 个标注")
+                else:
+                    print(f"  自定义格式")
+            else:
+                convert_json_to_yolo(json_file, output_dir, class_mapping, args.format)
+            
+            success_count += 1
+            
+        except Exception as e:
+            print(f"  错误: {e}")
+            error_count += 1
+    
+    print(f"\n转换完成:")
+    print(f"  成功: {success_count} 个文件")
+    print(f"  失败: {error_count} 个文件")
+    
+    if not args.test and success_count > 0:
+        print(f"  输出目录: {output_dir}")
+
+if __name__ == "__main__":
+    # 如果没有命令行参数,使用交互模式
+    import sys
+    if len(sys.argv) == 1:
+        print("JSON标签到YOLO格式转换工具")
+        print("=" * 50)
+        
+        # 交互式输入
+        input_path = input("请输入JSON文件或目录路径: ").strip()
+        if not input_path:
+            print("错误: 必须提供输入路径")
+            sys.exit(1)
+        
+        output_dir = input("请输入输出目录 (默认: ./yolo_labels): ").strip()
+        if not output_dir:
+            output_dir = "./yolo_labels"
+        
+        classes_file = input("请输入类别映射文件路径 (可选): ").strip()
+        
+        json_format = input("请输入JSON格式 (auto/labelme/coco/yolo, 默认: auto): ").strip()
+        if not json_format:
+            json_format = "auto"
+        
+        test_mode = input("是否启用测试模式?(y/N): ").strip().lower() == 'y'
+        
+        # 模拟命令行参数
+        sys.argv = ['json_to_yolo.py', input_path, '-o', output_dir, '-f', json_format]
+        if classes_file:
+            sys.argv.extend(['-c', classes_file])
+        if test_mode:
+            sys.argv.append('--test')
+    
+    main()

+ 69 - 0
num.py

@@ -0,0 +1,69 @@
+import os
+import shutil
+import random
+from datetime import datetime
+from pathlib import Path
+
+def copy_random_images(source_folder, num_images=1000):
+    """
+    从源文件夹随机抽取指定数量的图片,并复制到以当天日期命名的新文件夹中。
+    
+    :param source_folder: 源图片文件夹路径
+    :param num_images: 要抽取的图片数量,默认1000
+    """
+    # 支持的图片扩展名
+    image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
+    
+    # 获取当前日期作为目标文件夹名
+    today = datetime.now().strftime('%Y%m%d')
+    target_folder = Path(today)
+    
+    # 检查源文件夹是否存在
+    source_path = Path(source_folder)
+    if not source_path.exists():
+        print(f"错误:源文件夹 '{source_folder}' 不存在。")
+        return
+    
+    if not source_path.is_dir():
+        print(f"错误:'{source_folder}' 不是一个有效的文件夹。")
+        return
+    
+    # 获取所有图片文件
+    image_files = [
+        f for f in source_path.iterdir()
+        if f.is_file() and f.suffix.lower() in image_extensions
+    ]
+    
+    # 检查图片数量是否足够
+    if len(image_files) < num_images:
+        print(f"警告:源文件夹中只有 {len(image_files)} 张图片,少于要求的 {num_images} 张。将复制全部图片。")
+        num_images = len(image_files)
+    
+    # 随机抽样
+    selected_images = random.sample(image_files, num_images)
+    
+    # 创建目标文件夹
+    try:
+        target_folder.mkdir(exist_ok=True)
+        print(f"目标文件夹 '{today}' 已创建或已存在。")
+    except Exception as e:
+        print(f"无法创建目标文件夹 '{today}': {e}")
+        return
+    
+    # 复制图片
+    copied_count = 0
+    for img_file in selected_images:
+        try:
+            shutil.copy2(img_file, target_folder / img_file.name)
+            copied_count += 1
+        except Exception as e:
+            print(f"复制文件 {img_file.name} 时出错: {e}")
+    
+    print(f"已完成。共复制 {copied_count} 张图片到 '{today}' 文件夹。")
+
+# ================== 使用示例 ==================
+if __name__ == "__main__":
+    # 替换为你的源图片文件夹路径
+    source_dir = r"D:\data\extracted_frames_500"  # ← 修改这里为你自己的路径
+    
+    copy_random_images(source_dir, 1000)

BIN
output_segmented_video.mp4


+ 61 - 0
predict.py

@@ -0,0 +1,61 @@
+import cv2
+from ultralytics import YOLO
+
+# ------------------- 配置参数 -------------------
+VIDEO_PATH = '1234520251015_152225.mp4'
+WEIGHTS_PATH = 'yolov8n-seg.pt'
+OUTPUT_PATH = 'output_segmented_video.mp4'  # 输出视频路径
+CONFIDENCE_THRESHOLD = 0.5  # 置信度阈值
+SHOW_VIDEO = True  # 是否实时显示(可设为 False 以加快处理)
+# ------------------------------------------------
+
+# 加载 YOLOv8 分割模型
+model = YOLO(WEIGHTS_PATH)  # 自动加载模型结构和权重
+
+# 打开视频文件
+cap = cv2.VideoCapture(VIDEO_PATH)
+
+# 获取视频的宽高、帧率
+width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
+height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+fps = int(cap.get(cv2.CAP_PROP_FPS))
+
+# 定义视频写入器
+fourcc = cv2.VideoWriter_fourcc(*'mp4v')
+out = cv2.VideoWriter(OUTPUT_PATH, fourcc, fps, (width, height))
+
+# 检查视频是否成功打开
+if not cap.isOpened():
+    print("❌ 无法打开视频文件!请检查路径是否正确。")
+    exit()
+
+print("✅ 开始处理视频...")
+
+while True:
+    ret, frame = cap.read()
+    if not ret:
+        print("🔚 视频读取完成或结束。")
+        break
+
+    # 使用 YOLOv8 进行推理(分割)
+    results = model(frame, conf=CONFIDENCE_THRESHOLD, imgsz=640)
+
+    # 在原图上绘制分割结果
+    annotated_frame = results[0].plot()  # plot() 方法会自动画出边界框、分割掩码、标签等
+
+    # 写入输出视频
+    out.write(annotated_frame)
+
+    # 实时显示(可选)
+    if SHOW_VIDEO:
+        cv2.imshow('YOLOv8 Segmentation', annotated_frame)
+        if cv2.waitKey(1) & 0xFF == ord('q'):  # 按 'q' 键退出
+            print("🛑 用户中断。")
+            break
+
+# 释放资源
+cap.release()
+out.release()
+cv2.destroyAllWindows()
+
+print(f"✅ 处理完成!分割后的视频已保存为:{OUTPUT_PATH}")

+ 104 - 0
redataname.py

@@ -0,0 +1,104 @@
+import os
+import glob
+import uuid
+
+def batch_rename(dataset_root):
+    # 定义需要处理的子集
+    sub_sets = ['train', 'val', 'test']
+    
+    # 支持的图片扩展名 (根据你的实际情况添加)
+    valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
+
+    for subset in sub_sets:
+        # 构建路径
+        img_dir = os.path.join(dataset_root, 'images', subset)
+        label_dir = os.path.join(dataset_root, 'labels', subset)
+
+        # 检查文件夹是否存在
+        if not os.path.exists(img_dir) or not os.path.exists(label_dir):
+            print(f"Skipping {subset}: directories not found.")
+            continue
+
+        print(f"Processing: {subset} ...")
+
+        # 1. 获取所有图片文件
+        files = os.listdir(img_dir)
+        # 筛选出图片并排序(排序很重要,保证重命名的确定性)
+        image_files = sorted([f for f in files if os.path.splitext(f)[1].lower() in valid_extensions])
+
+        if not image_files:
+            print(f"  No images found in {subset}")
+            continue
+
+        # 2. 检查对应关系并准备重命名列表
+        # 格式: (原图片路径, 原标签路径, 目标图片名, 目标标签名)
+        rename_pairs = []
+        
+        count = 0
+        for img_name in image_files:
+            file_body, ext = os.path.splitext(img_name)
+            
+            # 假设标签是 .txt 格式
+            txt_name = file_body + ".txt"
+            src_img_path = os.path.join(img_dir, img_name)
+            src_txt_path = os.path.join(label_dir, txt_name)
+
+            # 检查标签文件是否存在
+            if os.path.exists(src_txt_path):
+                count += 1
+                # 生成新名字,例如 00001
+                new_name_body = f"{count:05d}" 
+                dst_img_name = new_name_body + ext
+                dst_txt_name = new_name_body + ".txt"
+                
+                rename_pairs.append({
+                    'src_img': src_img_path,
+                    'src_txt': src_txt_path,
+                    'dst_img': os.path.join(img_dir, dst_img_name),
+                    'dst_txt': os.path.join(label_dir, dst_txt_name)
+                })
+            else:
+                print(f"  Warning: No label found for {img_name}, skipping.")
+
+        print(f"  Found {len(rename_pairs)} pairs. Starting rename...")
+
+        # 3. 执行重命名 - 第一阶段:重命名为临时 UUID
+        # 这一步是为了防止 目标文件名 已经存在于文件夹中导致覆盖 (例如把 2.jpg 改为 1.jpg,但 1.jpg 还没处理)
+        for item in rename_pairs:
+            # 生成随机临时名
+            temp_token = str(uuid.uuid4())
+            item['temp_img'] = item['src_img'] + f".{temp_token}.tmp"
+            item['temp_txt'] = item['src_txt'] + f".{temp_token}.tmp"
+            
+            os.rename(item['src_img'], item['temp_img'])
+            os.rename(item['src_txt'], item['temp_txt'])
+
+        # 4. 执行重命名 - 第二阶段:从临时名改为目标名 (00001.jpg)
+        for item in rename_pairs:
+            os.rename(item['temp_img'], item['dst_img'])
+            os.rename(item['temp_txt'], item['dst_txt'])
+            
+        print(f"  {subset} done! Processed {len(rename_pairs)} pairs.")
+
+if __name__ == "__main__":
+    # 这里修改为你的数据集根目录路径
+    # 结构应为:
+    # my_dataset/
+    #   ├── images/
+    #   │     ├── train/
+    #   │     ├── val/
+    #   │     └── test/
+    #   └── labels/
+    #         ├── train/
+    #         ├── val/
+    #         └── test/
+    
+    dataset_path = r"20251210"  # 请修改这里!注意路径不要包含中文以免报错
+    
+    # 二次确认
+    confirm = input(f"Target path is: {dataset_path}\nHave you backed up your data? (y/n): ")
+    if confirm.lower() == 'y':
+        batch_rename(dataset_path)
+        print("\nAll operations completed.")
+    else:
+        print("Operation cancelled.")

+ 168 - 0
rename.py

@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+图片文件重命名脚本
+将 d:\data\123456\reorganized_frames 文件夹中的图片文件序号从881开始重新编号
+原文件: frame_000000.jpg -> frame_000880.jpg
+新文件: frame_000881.jpg -> frame_001761.jpg
+"""
+
+import os
+import re
+import shutil
+from pathlib import Path
+
+
+def rename_frames(source_dir, start_number=881, dry_run=True):
+    """
+    重命名帧文件,将序号从指定数字开始
+    
+    Args:
+        source_dir (str): 源文件夹路径
+        start_number (int): 起始序号,默认881
+        dry_run (bool): 是否为测试模式,True时只显示操作不实际执行
+    
+    Returns:
+        tuple: (成功数量, 失败数量, 错误列表)
+    """
+    source_path = Path(source_dir)
+    
+    if not source_path.exists():
+        print(f"错误: 源文件夹不存在: {source_dir}")
+        return 0, 0, [f"源文件夹不存在: {source_dir}"]
+    
+    # 获取所有符合格式的图片文件
+    frame_pattern = re.compile(r'^frame_(\d{6})\.jpg$')
+    files_to_rename = []
+    
+    for file_path in source_path.iterdir():
+        if file_path.is_file():
+            match = frame_pattern.match(file_path.name)
+            if match:
+                original_number = int(match.group(1))
+                files_to_rename.append((file_path, original_number))
+    
+    # 按原始序号排序
+    files_to_rename.sort(key=lambda x: x[1])
+    
+    print(f"找到 {len(files_to_rename)} 个符合格式的图片文件")
+    print(f"序号范围: {files_to_rename[0][1]} - {files_to_rename[-1][1]}")
+    print(f"新序号将从 {start_number} 开始")
+    print(f"模式: {'测试模式 (不实际执行)' if dry_run else '实际执行模式'}")
+    print("-" * 50)
+    
+    success_count = 0
+    error_count = 0
+    errors = []
+    
+    # 为了避免文件名冲突,我们需要使用临时文件名
+    # 先重命名为临时文件名,然后再重命名为最终文件名
+    temp_files = []
+    
+    try:
+        # 第一步:重命名为临时文件名
+        for i, (file_path, original_number) in enumerate(files_to_rename):
+            temp_name = f"temp_{i:06d}.jpg"
+            temp_path = file_path.parent / temp_name
+            
+            if dry_run:
+                print(f"[测试] {file_path.name} -> {temp_name}")
+            else:
+                try:
+                    file_path.rename(temp_path)
+                    temp_files.append((temp_path, i))
+                    print(f"临时重命名: {file_path.name} -> {temp_name}")
+                except Exception as e:
+                    error_msg = f"临时重命名失败: {file_path.name} -> {temp_name}, 错误: {str(e)}"
+                    print(error_msg)
+                    errors.append(error_msg)
+                    error_count += 1
+        
+        # 第二步:从临时文件名重命名为最终文件名
+        for temp_path, index in temp_files:
+            new_number = start_number + index
+            new_name = f"frame_{new_number:06d}.jpg"
+            final_path = temp_path.parent / new_name
+            
+            if dry_run:
+                print(f"[测试] temp_{index:06d}.jpg -> {new_name}")
+            else:
+                try:
+                    temp_path.rename(final_path)
+                    print(f"最终重命名: {temp_path.name} -> {new_name}")
+                    success_count += 1
+                except Exception as e:
+                    error_msg = f"最终重命名失败: {temp_path.name} -> {new_name}, 错误: {str(e)}"
+                    print(error_msg)
+                    errors.append(error_msg)
+                    error_count += 1
+        
+        # 如果是测试模式,计算成功数量
+        if dry_run:
+            success_count = len(files_to_rename)
+            
+    except Exception as e:
+        error_msg = f"重命名过程中发生未预期错误: {str(e)}"
+        print(error_msg)
+        errors.append(error_msg)
+        error_count += 1
+    
+    print("-" * 50)
+    print(f"操作完成!")
+    print(f"成功: {success_count} 个文件")
+    print(f"失败: {error_count} 个文件")
+    
+    if errors:
+        print("\n错误详情:")
+        for error in errors:
+            print(f"  - {error}")
+    
+    return success_count, error_count, errors
+
+
+def main():
+    """主函数"""
+    # 配置参数
+    source_directory = r"d:\data\123456\reorganized_frames"
+    start_number = 881
+    
+    print("=" * 60)
+    print("图片文件重命名工具")
+    print("=" * 60)
+    print(f"源文件夹: {source_directory}")
+    print(f"起始序号: {start_number}")
+    print()
+    
+    # 首先进行测试运行
+    print("🔍 执行测试运行...")
+    success, errors, error_list = rename_frames(source_directory, start_number, dry_run=True)
+    
+    if errors > 0:
+        print(f"\n❌ 测试运行发现 {errors} 个错误,请检查后再执行实际操作")
+        return
+    
+    print(f"\n✅ 测试运行成功,将处理 {success} 个文件")
+    
+    # 询问用户是否继续
+    while True:
+        user_input = input("\n是否执行实际重命名操作? (y/n): ").strip().lower()
+        if user_input in ['y', 'yes', '是']:
+            break
+        elif user_input in ['n', 'no', '否']:
+            print("操作已取消")
+            return
+        else:
+            print("请输入 y 或 n")
+    
+    # 执行实际重命名
+    print("\n🚀 执行实际重命名操作...")
+    success, errors, error_list = rename_frames(source_directory, start_number, dry_run=False)
+    
+    if errors == 0:
+        print(f"\n🎉 重命名操作完全成功! 共处理 {success} 个文件")
+    else:
+        print(f"\n⚠️  重命名操作完成,但有 {errors} 个错误")
+
+
+if __name__ == "__main__":
+    main()

+ 6 - 0
requirements-train.txt

@@ -0,0 +1,6 @@
+ultralytics
+torch
+torchvision
+opencv-python
+matplotlib
+tqdm

+ 143 - 0
s.py

@@ -0,0 +1,143 @@
+import os
+import random
+import shutil
+import time
+
+# ================= 🔧 配置区域 =================
+# 请确保这个路径和你截图里的路径一模一样
+root_path = r"D:\data\20251210\20251210"
+
+# 划分比例 8:1:1
+split_ratios = {"train": 0.8, "val": 0.1, "test": 0.1}
+
+# 后缀配置
+valid_image_exts = ['.jpg', '.jpeg', '.png', '.bmp']
+valid_label_exts = ['.txt', '.xml', '.json']
+# ===============================================
+
+def main():
+    print("==========================================")
+    print("      正在启动 8:1:1 随机划分程序")
+    print("==========================================\n")
+
+    images_root = os.path.join(root_path, "images")
+    labels_root = os.path.join(root_path, "labels")
+
+    # 1. 检查根目录
+    if not os.path.exists(images_root):
+        print(f"❌ 错误:找不到 images 文件夹!\n   试图寻找路径: {images_root}")
+        return
+    if not os.path.exists(labels_root):
+        print(f"❌ 错误:找不到 labels 文件夹!\n   试图寻找路径: {labels_root}")
+        return
+
+    print(f"✅ 路径检查通过: {root_path}")
+    
+    # 2. 收集文件
+    all_pairs = []
+    # 扫描 images 下的所有文件夹(包括 train, test, val 或者其他)
+    # 只要是在 images 目录下的子文件夹,都会被扫描
+    sub_dirs = [d for d in os.listdir(images_root) if os.path.isdir(os.path.join(images_root, d))]
+    
+    # 如果 images 下没有子文件夹,可能是图片直接放在了 images 根目录下?
+    # 为了兼容,如果下面没有文件夹,就扫描 images 本身
+    if not sub_dirs: 
+        sub_dirs = ["."] # 代表当前目录
+        print("⚠️ 提示:images 下没有子文件夹,将扫描 images 根目录...")
+
+    print(f"📂 正在扫描以下文件夹: {sub_dirs}")
+
+    for sub in sub_dirs:
+        # 处理路径:如果是 "." 则不拼接子目录
+        sub_img_dir = images_root if sub == "." else os.path.join(images_root, sub)
+        sub_lbl_dir = labels_root if sub == "." else os.path.join(labels_root, sub)
+        
+        if not os.path.exists(sub_lbl_dir):
+            # 如果对应的 label 文件夹不存在,跳过
+            continue
+
+        files = os.listdir(sub_img_dir)
+        count_folder = 0
+        for f in files:
+            stem, ext = os.path.splitext(f)
+            if ext.lower() in valid_image_exts:
+                img_path = os.path.join(sub_img_dir, f)
+                
+                # 找标签
+                lbl_path = None
+                for lbl_ext in valid_label_exts:
+                    potential_lbl = os.path.join(sub_lbl_dir, stem + lbl_ext)
+                    if os.path.exists(potential_lbl):
+                        lbl_path = potential_lbl
+                        break
+                
+                if lbl_path:
+                    all_pairs.append({'img': img_path, 'lbl': lbl_path})
+                    count_folder += 1
+        
+        print(f"   -> 在 [{sub}] 中找到 {count_folder} 对数据")
+
+    total = len(all_pairs)
+    print(f"\n📦 总共收集到: {total} 组数据")
+    
+    if total == 0:
+        print("❌ 没有找到任何匹配的图片和标签,请检查文件名是否对应(例如 001.jpg 是否有 001.txt)。")
+        return
+
+    # 3. 打乱与计算
+    print("🎲 正在打乱顺序...")
+    random.shuffle(all_pairs)
+    
+    n_train = int(total * split_ratios["train"])
+    n_val = int(total * split_ratios["val"])
+    n_test = total - n_train - n_val
+    
+    print(f"📊 划分数量 -> Train: {n_train}, Val: {n_val}, Test: {n_test}")
+
+    split_data = {
+        "train": all_pairs[:n_train],
+        "val": all_pairs[n_train : n_train + n_val],
+        "test": all_pairs[n_train + n_val :]
+    }
+
+    # 4. 移动文件
+    print("🚚 开始移动文件...")
+    
+    for split_name, items in split_data.items():
+        # 目标目录
+        dst_img_dir = os.path.join(images_root, split_name)
+        dst_lbl_dir = os.path.join(labels_root, split_name)
+        
+        os.makedirs(dst_img_dir, exist_ok=True)
+        os.makedirs(dst_lbl_dir, exist_ok=True)
+        
+        processed = 0
+        for item in items:
+            src_img, src_lbl = item['img'], item['lbl']
+            
+            # 只有当源路径不在目标路径时才移动
+            if os.path.dirname(src_img) != dst_img_dir:
+                try:
+                    shutil.move(src_img, os.path.join(dst_img_dir, os.path.basename(src_img)))
+                    shutil.move(src_lbl, os.path.join(dst_lbl_dir, os.path.basename(src_lbl)))
+                except Exception as e:
+                    print(f"   [Error] 移动失败: {os.path.basename(src_img)} -> {e}")
+            
+            processed += 1
+            # 每处理 500 个打印一次进度,防止看着像死机
+            if processed % 500 == 0:
+                print(f"   [{split_name}] 已处理 {processed} / {len(items)}")
+
+        print(f"   ✅ {split_name} 完成。")
+
+    print("\n🎉🎉🎉 全部处理完毕! 🎉🎉🎉")
+
+if __name__ == "__main__":
+    try:
+        main()
+    except Exception as e:
+        print(f"\n❌ 发生严重错误: {e}")
+    
+    # 这里的 input 是为了防止窗口直接关闭
+    print("\n--------------------------------")
+    input("程序运行结束,请按回车键关闭窗口...")

+ 70 - 0
split.py

@@ -0,0 +1,70 @@
+import os
+import shutil
+from pathlib import Path
+
+def split_folder_files(folder_path, num_parts=8):
+    """
+    将指定文件夹中的文件平均分成 num_parts 份,每份放入一个按序号命名的子文件夹中。
+    
+    :param folder_path: 要分割的文件夹路径(如 '20251023')
+    :param num_parts: 分成几份,默认为8
+    """
+    folder = Path(folder_path)
+    
+    if not folder.exists():
+        print(f"错误:文件夹 '{folder_path}' 不存在。")
+        return
+    
+    if not folder.is_dir():
+        print(f"错误:'{folder_path}' 不是一个文件夹。")
+        return
+    
+    # 获取所有文件(排除子文件夹)
+    files = [f for f in folder.iterdir() if f.is_file()]
+    
+    if len(files) == 0:
+        print("警告:文件夹中没有文件可分割。")
+        return
+    
+    # 打乱文件顺序以实现“随机”分布(可选)
+    # 如果希望保持顺序,注释掉下一行
+    # random.shuffle(files)
+    
+    # 计算每一份大致应有多少文件
+    base_size = len(files) // num_parts
+    remainder = len(files) % num_parts  # 多余的前几份多分一个
+    
+    start_idx = 0
+    for i in range(num_parts):
+        # 前 remainder 份多放一个文件
+        part_size = base_size + (1 if i < remainder else 0)
+        end_idx = start_idx + part_size
+        
+        part_files = files[start_idx:end_idx]
+        part_folder = folder / str(i + 1)  # 子文件夹名为 '1', '2', ..., '8'
+        
+        try:
+            part_folder.mkdir(exist_ok=True)
+            print(f"已创建文件夹: {part_folder}")
+        except Exception as e:
+            print(f"无法创建文件夹 {part_folder}: {e}")
+            continue
+        
+        # 复制文件到子文件夹
+        for file_path in part_files:
+            try:
+                shutil.copy2(file_path, part_folder / file_path.name)
+            except Exception as e:
+                print(f"复制文件 {file_path.name} 时出错: {e}")
+        
+        print(f"  → 放入 {len(part_files)} 个文件")
+        start_idx = end_idx
+    
+    print(f"✅ 已完成:将 {len(files)} 个文件分成 {num_parts} 份,存放在 '{folder_path}' 内的子文件夹中。")
+
+# ================== 使用示例 ==================
+if __name__ == "__main__":
+    target_folder = "20251023"  # 主文件夹名
+    parts = 8  # 可改为你需要的份数,如 5, 10 等
+    
+    split_folder_files(target_folder, num_parts=parts)

+ 124 - 0
split12.py

@@ -0,0 +1,124 @@
+import os
+import shutil
+import random
+from pathlib import Path
+
+def repartition_yolo_dataset_safe(
+    dataset_root: str,
+    train_ratio: float = 0.8,
+    val_ratio: float = 0.1,
+    test_ratio: float = 0.1,
+    seed: int = 42,
+    image_exts: tuple = ('.jpg', '.jpeg', '.png'),
+    class_names: list = None,
+    backup: bool = True
+):
+    dataset_root = Path(dataset_root)
+    images_root = dataset_root / 'images'
+    labels_root = dataset_root / 'labels'
+
+    splits = ['train', 'val', 'test']
+    
+    # 检查原始结构是否存在
+    for s in splits:
+        if not (images_root / s).exists() or not (labels_root / s).exists():
+            raise FileNotFoundError(f"缺少 {s} 目录,请确保数据集结构完整")
+
+    # === 1. 备份原始数据 ===
+    backup_dir = dataset_root / 'backup_original'
+    if backup and not backup_dir.exists():
+        print("📁 正在备份原始数据...")
+        shutil.copytree(images_root, backup_dir / 'images')
+        shutil.copytree(labels_root, backup_dir / 'labels')
+        print("✅ 备份完成")
+
+    # === 2. 收集所有图片路径(来自 train/val/test)===
+    all_images = []
+    for split in splits:
+        split_img_dir = images_root / split
+        for f in split_img_dir.iterdir():
+            if f.is_file() and f.suffix.lower() in image_exts:
+                all_images.append((split, f))  # 保存 (来源split, 文件路径)
+
+    if not all_images:
+        raise ValueError("未找到任何图片!")
+
+    print(f"🔍 找到 {len(all_images)} 张图片")
+
+    # === 3. 随机打乱并划分 ===
+    random.seed(seed)
+    random.shuffle(all_images)
+
+    total = len(all_images)
+    train_end = int(total * train_ratio)
+    val_end = train_end + int(total * val_ratio)
+
+    new_splits = {
+        'train': all_images[:train_end],
+        'val': all_images[train_end:val_end],
+        'test': all_images[val_end:]
+    }
+
+    # === 4. 创建新的目标目录(加 _new 后缀避免冲突)===
+    new_images = dataset_root / 'images_new'
+    new_labels = dataset_root / 'labels_new'
+
+    for split in splits:
+        (new_images / split).mkdir(parents=True, exist_ok=True)
+        (new_labels / split).mkdir(parents=True, exist_ok=True)
+
+    # === 5. 复制文件(从原位置 → 新位置)===
+    missing_labels = 0
+    for split_name, files in new_splits.items():
+        print(f"📦 {split_name}: {len(files)} 张")
+        for orig_split, img_path in files:
+            # 复制图片
+            dst_img = new_images / split_name / img_path.name
+            shutil.copy2(img_path, dst_img)
+
+            # 复制标签(标签在 labels/orig_split/ 下)
+            label_path = labels_root / orig_split / (img_path.stem + '.txt')
+            dst_label = new_labels / split_name / (img_path.stem + '.txt')
+            if label_path.exists():
+                shutil.copy2(label_path, dst_label)
+            else:
+                print(f"⚠️ 缺失标签: {label_path}")
+                missing_labels += 1
+
+    # === 6. 原子性替换:删除旧 images/labels,重命名 new 为正式名 ===
+    shutil.rmtree(images_root)
+    shutil.rmtree(labels_root)
+    shutil.move(str(new_images), str(images_root))
+    shutil.move(str(new_labels), str(labels_root))
+
+    # === 7. 生成 dataset.yaml(可选)===
+    if class_names:
+        yaml_content = f"""path: {dataset_root.resolve()}
+train: images/train
+val: images/val
+test: images/test
+
+names: {class_names}
+"""
+        with open(dataset_root / 'dataset.yaml', 'w', encoding='utf-8') as f:
+            f.write(yaml_content)
+        print(f"\n📄 已生成 dataset.yaml")
+
+    print(f"\n✅ 重划分成功!")
+    print(f"训练: {len(new_splits['train'])}, 验证: {len(new_splits['val'])}, 测试: {len(new_splits['test'])}")
+    if missing_labels:
+        print(f"⚠️ 共 {missing_labels} 个标签缺失")
+
+if __name__ == "__main__":
+    DATASET_ROOT = r"20251210/20251210"
+    #CLASS_NAMES = ["dust"]  # 替换为你的类别
+
+    repartition_yolo_dataset_safe(
+        dataset_root=DATASET_ROOT,
+        train_ratio=0.8,
+        val_ratio=0.1,
+        test_ratio=0.1,
+        seed=42,
+        #class_names=CLASS_NAMES,
+        backup=True
+    )

+ 303 - 0
split_dataset.py

@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+数据集划分脚本
+将datasets_merged中的图片和标签按照8:1:1比例划分为train/test/val数据集
+"""
+
+import os
+import shutil
+import random
+from pathlib import Path
+import argparse
+from typing import List, Tuple
+
+def get_paired_files(source_dir: str) -> List[Tuple[str, str]]:
+    """
+    获取配对的图片和标签文件
+    
+    Args:
+        source_dir: 源数据目录
+        
+    Returns:
+        配对文件列表 [(image_path, label_path), ...]
+    """
+    source_path = Path(source_dir)
+    
+    # 获取所有图片文件
+    image_files = list(source_path.glob("*.jpg"))
+    paired_files = []
+    
+    for img_file in image_files:
+        # 构造对应的标签文件路径
+        label_file = source_path / f"{img_file.stem}.txt"
+        
+        if label_file.exists():
+            paired_files.append((str(img_file), str(label_file)))
+        else:
+            print(f"警告: 图片 {img_file.name} 没有对应的标签文件")
+    
+    return paired_files
+
+def create_directory_structure(output_dir: str):
+    """
+    创建YOLO数据集目录结构
+    
+    Args:
+        output_dir: 输出目录
+    """
+    output_path = Path(output_dir)
+    
+    # 创建主要目录结构
+    directories = [
+        "images/train",
+        "images/test", 
+        "images/val",
+        "labels/train",
+        "labels/test",
+        "labels/val"
+    ]
+    
+    for directory in directories:
+        dir_path = output_path / directory
+        dir_path.mkdir(parents=True, exist_ok=True)
+        print(f"创建目录: {dir_path}")
+
+def split_dataset(paired_files: List[Tuple[str, str]], 
+                 train_ratio: float = 0.8, 
+                 test_ratio: float = 0.1, 
+                 val_ratio: float = 0.1) -> Tuple[List, List, List]:
+    """
+    按比例划分数据集
+    
+    Args:
+        paired_files: 配对文件列表
+        train_ratio: 训练集比例
+        test_ratio: 测试集比例
+        val_ratio: 验证集比例
+        
+    Returns:
+        (train_files, test_files, val_files)
+    """
+    # 验证比例
+    total_ratio = train_ratio + test_ratio + val_ratio
+    if abs(total_ratio - 1.0) > 1e-6:
+        raise ValueError(f"比例总和必须为1.0,当前为: {total_ratio}")
+    
+    # 随机打乱数据
+    random.shuffle(paired_files)
+    
+    total_files = len(paired_files)
+    train_count = int(total_files * train_ratio)
+    test_count = int(total_files * test_ratio)
+    
+    # 划分数据集
+    train_files = paired_files[:train_count]
+    test_files = paired_files[train_count:train_count + test_count]
+    val_files = paired_files[train_count + test_count:]
+    
+    print(f"数据集划分统计:")
+    print(f"  总文件数: {total_files}")
+    print(f"  训练集: {len(train_files)} ({len(train_files)/total_files*100:.1f}%)")
+    print(f"  测试集: {len(test_files)} ({len(test_files)/total_files*100:.1f}%)")
+    print(f"  验证集: {len(val_files)} ({len(val_files)/total_files*100:.1f}%)")
+    
+    return train_files, test_files, val_files
+
+def copy_files(file_list: List[Tuple[str, str]], 
+               output_dir: str, 
+               subset_name: str):
+    """
+    复制文件到目标目录
+    
+    Args:
+        file_list: 文件列表
+        output_dir: 输出目录
+        subset_name: 子集名称 (train/test/val)
+    """
+    output_path = Path(output_dir)
+    
+    for img_path, label_path in file_list:
+        img_file = Path(img_path)
+        label_file = Path(label_path)
+        
+        # 目标路径
+        target_img_dir = output_path / "images" / subset_name
+        target_label_dir = output_path / "labels" / subset_name
+        
+        target_img_path = target_img_dir / img_file.name
+        target_label_path = target_label_dir / label_file.name
+        
+        # 复制文件
+        try:
+            shutil.copy2(img_path, target_img_path)
+            shutil.copy2(label_path, target_label_path)
+        except Exception as e:
+            print(f"复制文件失败: {img_file.name} - {e}")
+
+def _read_class_names(classes_file: str):
+    """读取类别文件,支持以下格式:
+    - 每行一个类名:"fire"
+    - 带显式ID:"0 fire" 或 "fire 0" 或 "0,fire"
+    - 行内注释:以#开始的内容忽略
+    返回:仅类名组成的列表
+    """
+    names = []
+    with open(classes_file, 'r', encoding='utf-8') as f:
+        for raw in f:
+            line = raw.strip()
+            if not line:
+                continue
+            # 去除行内注释
+            if '#' in line:
+                line = line.split('#', 1)[0].strip()
+            if not line:
+                continue
+            # 逗号分隔:"id,name"
+            if ',' in line:
+                parts = [p.strip() for p in line.split(',') if p.strip()]
+                if len(parts) == 2:
+                    if parts[0].isdigit():
+                        names.append(parts[1])
+                        continue
+            tokens = [t for t in line.split() if t]
+            if not tokens:
+                continue
+            if tokens[0].isdigit():
+                # "ID 类名(可能包含空格)"
+                names.append(' '.join(tokens[1:]))
+            elif tokens[-1].isdigit():
+                # "类名(可能包含空格) ID"
+                names.append(' '.join(tokens[:-1]))
+            else:
+                names.append(line)
+    return names
+
+def create_dataset_yaml(output_dir: str, classes_file: str = None):
+    """
+    创建YOLO数据集配置文件
+    
+    Args:
+        output_dir: 输出目录
+        classes_file: 类别文件路径
+    """
+    output_path = Path(output_dir)
+    yaml_path = output_path / "dataset.yaml"
+    
+    # 读取类别信息
+    if classes_file and os.path.exists(classes_file):
+        classes = _read_class_names(classes_file)
+    else:
+        # 默认类别(若未提供classes文件)
+        classes = ['fire', 'dust', 'move_machine', 'open_machine', 'close_machine']
+    
+    # 生成YAML内容
+    yaml_content = f"""# YOLO数据集配置文件
+# 数据集路径 (相对于此文件的路径)
+path: {output_path.absolute()}
+train: images/train
+val: images/val
+test: images/test
+
+# 类别数量
+nc: {len(classes)}
+
+# 类别名称
+names: {classes}
+"""
+    
+    with open(yaml_path, 'w', encoding='utf-8') as f:
+        f.write(yaml_content)
+    
+    print(f"创建数据集配置文件: {yaml_path}")
+
+def main():
+    parser = argparse.ArgumentParser(description='YOLO数据集划分工具')
+    parser.add_argument('source_dir', help='源数据目录路径')
+    parser.add_argument('-o', '--output', default='./yolo_dataset', 
+                       help='输出目录路径 (默认: ./yolo_dataset)')
+    parser.add_argument('-c', '--classes', help='类别文件路径')
+    parser.add_argument('--train-ratio', type=float, default=0.8, 
+                       help='训练集比例 (默认: 0.8)')
+    parser.add_argument('--test-ratio', type=float, default=0.1, 
+                       help='测试集比例 (默认: 0.1)')
+    parser.add_argument('--val-ratio', type=float, default=0.1, 
+                       help='验证集比例 (默认: 0.1)')
+    parser.add_argument('--seed', type=int, default=42, 
+                       help='随机种子 (默认: 42)')
+    parser.add_argument('--dry-run', action='store_true', 
+                       help='仅显示划分统计,不实际复制文件')
+    
+    args = parser.parse_args()
+    
+    # 设置随机种子
+    random.seed(args.seed)
+    
+    # 验证源目录
+    if not os.path.exists(args.source_dir):
+        print(f"错误: 源目录不存在: {args.source_dir}")
+        return
+    
+    print(f"开始处理数据集...")
+    print(f"源目录: {args.source_dir}")
+    print(f"输出目录: {args.output}")
+    print(f"划分比例: 训练集{args.train_ratio} : 测试集{args.test_ratio} : 验证集{args.val_ratio}")
+    print(f"随机种子: {args.seed}")
+    print("-" * 50)
+    
+    # 获取配对文件
+    print("1. 扫描配对文件...")
+    paired_files = get_paired_files(args.source_dir)
+    
+    if not paired_files:
+        print("错误: 没有找到配对的图片和标签文件")
+        return
+    
+    print(f"找到 {len(paired_files)} 对配对文件")
+    
+    # 划分数据集
+    print("\n2. 划分数据集...")
+    train_files, test_files, val_files = split_dataset(
+        paired_files, args.train_ratio, args.test_ratio, args.val_ratio
+    )
+    
+    if args.dry_run:
+        print("\n[试运行模式] 仅显示统计信息,不实际复制文件")
+        return
+    
+    # 创建目录结构
+    print("\n3. 创建目录结构...")
+    create_directory_structure(args.output)
+    
+    # 复制文件
+    print("\n4. 复制文件...")
+    print("复制训练集文件...")
+    copy_files(train_files, args.output, "train")
+    
+    print("复制测试集文件...")
+    copy_files(test_files, args.output, "test")
+    
+    print("复制验证集文件...")
+    copy_files(val_files, args.output, "val")
+    
+    # 创建配置文件
+    print("\n5. 创建数据集配置文件...")
+    create_dataset_yaml(args.output, args.classes)
+    
+    print("\n" + "=" * 50)
+    print("数据集划分完成!")
+    print(f"输出目录: {os.path.abspath(args.output)}")
+    print("\n目录结构:")
+    print("yolo_dataset/")
+    print("├── images/")
+    print("│   ├── train/")
+    print("│   ├── test/")
+    print("│   └── val/")
+    print("├── labels/")
+    print("│   ├── train/")
+    print("│   ├── test/")
+    print("│   └── val/")
+    print("└── dataset.yaml")
+
+if __name__ == "__main__":
+    main()

+ 73 - 0
test_time_parser.py

@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+测试时间解析功能
+"""
+
+import sys
+sys.path.append('.')
+from video_segment_extractor import parse_time_to_seconds
+
+def test_time_parser():
+    """测试时间解析函数"""
+    test_cases = [
+        ("30", 30.0),
+        ("30.5", 30.5),
+        ("1:30", 90.0),
+        ("1:30.5", 90.5),
+        ("0:1:30", 90.0),
+        ("1:23:45", 5025.0),
+        ("2:0:0", 7200.0)
+    ]
+    
+    print("测试时间解析功能...")
+    print("=" * 50)
+    
+    all_passed = True
+    
+    for time_str, expected in test_cases:
+        try:
+            result = parse_time_to_seconds(time_str)
+            if abs(result - expected) < 0.001:  # 浮点数比较
+                status = "✅ PASS"
+            else:
+                status = "❌ FAIL"
+                all_passed = False
+            
+            print(f"{time_str:>8} -> {result:>8.1f}s (期望: {expected:>8.1f}s) {status}")
+            
+        except Exception as e:
+            print(f"{time_str:>8} -> ERROR: {e} ❌ FAIL")
+            all_passed = False
+    
+    print("=" * 50)
+    
+    # 测试错误情况
+    print("\n测试错误处理...")
+    error_cases = [
+        "abc",
+        "1:2:3:4",
+        "-10",
+        "1:-30"
+    ]
+    
+    for time_str in error_cases:
+        try:
+            result = parse_time_to_seconds(time_str)
+            print(f"{time_str:>8} -> {result:>8.1f}s ❌ FAIL (应该报错)")
+            all_passed = False
+        except Exception as e:
+            print(f"{time_str:>8} -> ERROR: {str(e)[:30]}... ✅ PASS")
+    
+    print("=" * 50)
+    
+    if all_passed:
+        print("\n🎉 所有测试通过!")
+        return True
+    else:
+        print("\n❌ 部分测试失败!")
+        return False
+
+if __name__ == "__main__":
+    success = test_time_parser()
+    sys.exit(0 if success else 1)

+ 130 - 0
testimg.py

@@ -0,0 +1,130 @@
+import cv2
+import os
+import random
+
+# ================= 配置区域 =================
+# 1. 图片文件夹路径
+IMG_DIR = r'20251204\images' 
+
+# 2. 标签文件夹路径 (YOLO格式的txt)
+LABEL_DIR = r'20251204\labels'
+
+# 3. 结果保存路径 (如果不填,默认在当前目录下生成 'output' 文件夹)
+OUTPUT_DIR = r'output_result'
+
+# 4. 类别名称列表 (按 classes.txt 的顺序或者是你的模型定义的顺序)
+# 如果不知道名字,可以留空,代码会直接显示 ID (如 '0', '1')
+CLASSES = ['fire', 'fust'] 
+# CLASSES = [] # 如果不想显示名字,保持为空列表即可
+
+# 5. 是否只显示有标签的图片?(True: 跳过无标签图片; False: 显示所有)
+ONLY_SHOW_LABELED = True
+# ===========================================
+
+def get_color(cls_id):
+    """为每个类别生成固定的随机颜色"""
+    random.seed(cls_id * 10) # 保证同一个类别的颜色在不同图片中是一样的
+    b = random.randint(0, 255)
+    g = random.randint(0, 255)
+    r = random.randint(0, 255)
+    return (b, g, r)
+
+def plot_one_box(x, img, color=None, label=None, line_thickness=None):
+    """画框的主函数"""
+    tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1  # 线宽
+    c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
+    cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
+    
+    if label:
+        tf = max(tl - 1, 1)  # 字体粗细
+        t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
+        c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
+        cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA)  # 填充文字背景
+        cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
+
+def yolo_to_xyxy(yolo_line, img_w, img_h):
+    """将YOLO格式 (class x_center y_center w h) 转换为 (x1, y1, x2, y2)"""
+    parts = yolo_line.strip().split()
+    cls_id = int(parts[0])
+    x_c = float(parts[1])
+    y_c = float(parts[2])
+    w = float(parts[3])
+    h = float(parts[4])
+
+    # 还原为像素坐标
+    x1 = int((x_c - w / 2) * img_w)
+    y1 = int((y_c - h / 2) * img_h)
+    x2 = int((x_c + w / 2) * img_w)
+    y2 = int((y_c + h / 2) * img_h)
+    
+    # 边界保护,防止画出图片外
+    x1 = max(0, x1)
+    y1 = max(0, y1)
+    x2 = min(img_w, x2)
+    y2 = min(img_h, y2)
+
+    return cls_id, x1, y1, x2, y2
+
+def main():
+    if not os.path.exists(OUTPUT_DIR):
+        os.makedirs(OUTPUT_DIR)
+    
+    print(f"开始处理... 结果将保存到: {OUTPUT_DIR}")
+    
+    img_files = [f for f in os.listdir(IMG_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
+    
+    for img_file in img_files:
+        img_path = os.path.join(IMG_DIR, img_file)
+        label_file = os.path.splitext(img_file)[0] + '.txt'
+        label_path = os.path.join(LABEL_DIR, label_file)
+        
+        # 读取图片
+        img = cv2.imread(img_path)
+        if img is None:
+            continue
+        h, w, _ = img.shape
+        
+        has_label = False
+        
+        # 检查是否有对应的txt文件
+        if os.path.exists(label_path):
+            with open(label_path, 'r') as f:
+                lines = f.readlines()
+                
+            for line in lines:
+                if not line.strip(): continue
+                has_label = True
+                
+                # 解析坐标
+                try:
+                    cls_id, x1, y1, x2, y2 = yolo_to_xyxy(line, w, h)
+                    
+                    # 获取颜色
+                    color = get_color(cls_id)
+                    
+                    # 获取标签名称
+                    if CLASSES and 0 <= cls_id < len(CLASSES):
+                        label_name = f"{CLASSES[cls_id]}"
+                    else:
+                        label_name = f"Class {cls_id}"
+                    
+                    # 画框
+                    plot_one_box([x1, y1, x2, y2], img, color=color, label=label_name)
+                except Exception as e:
+                    print(f"解析错误 {label_file}: {e}")
+
+        # 决定是否保存
+        if ONLY_SHOW_LABELED and not has_label:
+            continue
+            
+        save_path = os.path.join(OUTPUT_DIR, "vis_" + img_file)
+        cv2.imwrite(save_path, img)
+        # 如果想实时看,取消下面两行的注释(按任意键看下一张,按q退出)
+        # cv2.imshow('Check', img)
+        # if cv2.waitKey(0) == ord('q'): break
+
+    print("处理完成!")
+    cv2.destroyAllWindows()
+
+if __name__ == "__main__":
+    main()

+ 184 - 0
video_frame_extractor.py

@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+视频画面提取工具
+从指定视频文件的特定时间段提取画面帧并保存为图像文件
+"""
+
+import cv2
+import os
+import sys
+from pathlib import Path
+
+
+def extract_frames_from_video(video_path, start_time_seconds, end_time_seconds, output_dir, target_frame_count=500, image_extension="jpg", image_params=None):
+    """
+    从视频中提取指定时间段的画面帧
+    
+    参数:
+        video_path (str): 视频文件路径
+        start_time_seconds (int): 开始时间(秒)
+        end_time_seconds (int): 结束时间(秒)
+        output_dir (str): 输出目录路径
+        target_frame_count (int): 目标提取帧数,默认500帧
+    
+    返回:
+        bool: 提取是否成功
+    """
+    
+    # 检查视频文件是否存在
+    if not os.path.exists(video_path):
+        print(f"错误:视频文件不存在 - {video_path}")
+        return False
+    
+    # 创建输出目录
+    output_path = Path(output_dir)
+    output_path.mkdir(parents=True, exist_ok=True)
+    
+    # 打开视频文件
+    cap = cv2.VideoCapture(video_path)
+    
+    if not cap.isOpened():
+        print(f"错误:无法打开视频文件 - {video_path}")
+        return False
+    
+    # 获取视频基本信息
+    fps = cap.get(cv2.CAP_PROP_FPS)
+    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
+    duration = total_frames / fps
+    
+    print(f"视频信息:")
+    print(f"  帧率: {fps:.2f} FPS")
+    print(f"  总帧数: {total_frames}")
+    print(f"  总时长: {duration:.2f} 秒")
+    
+    # 计算开始和结束帧号
+    start_frame = int(start_time_seconds * fps)
+    end_frame = int(end_time_seconds * fps)
+    
+    # 检查时间范围是否有效
+    if start_frame >= total_frames:
+        print(f"错误:开始时间超出视频长度")
+        cap.release()
+        return False
+    
+    if end_frame > total_frames:
+        print(f"警告:结束时间超出视频长度,将提取到视频结尾")
+        end_frame = total_frames
+    
+    # 计算实际提取的帧数范围
+    frame_range = end_frame - start_frame
+    
+    if frame_range <= 0:
+        print(f"错误:无效的时间范围")
+        cap.release()
+        return False
+    
+    # 计算帧间隔以获得目标帧数
+    if frame_range <= target_frame_count:
+        # 如果总帧数少于目标帧数,提取所有帧
+        frame_interval = 1
+        actual_frame_count = frame_range
+    else:
+        # 计算间隔以获得约500帧
+        frame_interval = frame_range // target_frame_count
+        actual_frame_count = frame_range // frame_interval
+    
+    print(f"提取参数:")
+    print(f"  开始时间: {start_time_seconds}秒 (第{start_frame}帧)")
+    print(f"  结束时间: {end_time_seconds}秒 (第{end_frame}帧)")
+    print(f"  帧间隔: {frame_interval}")
+    print(f"  预计提取帧数: {actual_frame_count}")
+    
+    # 跳转到开始帧
+    cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
+    
+    extracted_count = 0
+    current_frame = start_frame
+    
+    print(f"开始提取画面...")
+    
+    while current_frame < end_frame:
+        ret, frame = cap.read()
+        
+        if not ret:
+            print(f"警告:读取帧失败,停止提取")
+            break
+        
+        # 只保存指定间隔的帧
+        if (current_frame - start_frame) % frame_interval == 0:
+            # 计算时间戳
+            timestamp_seconds = current_frame / fps
+            minutes = int(timestamp_seconds // 60)
+            seconds = int(timestamp_seconds % 60)
+            milliseconds = int((timestamp_seconds % 1) * 1000)
+            
+            # 生成文件名
+            filename = f"frame_{extracted_count:04d}_time_{minutes:02d}m{seconds:02d}s{milliseconds:03d}ms.{image_extension}"
+            output_file = output_path / filename
+
+            # 保存图像
+            success = cv2.imwrite(str(output_file), frame, image_params if image_params is not None else [])
+            
+            if success:
+                extracted_count += 1
+                if extracted_count % 50 == 0:
+                    print(f"  已提取 {extracted_count} 帧...")
+            else:
+                print(f"警告:保存图像失败 - {output_file}")
+        
+        current_frame += 1
+    
+    cap.release()
+    
+    print(f"提取完成!")
+    print(f"  成功提取 {extracted_count} 帧")
+    print(f"  输出目录: {output_dir}")
+    
+    return True
+
+
+def main():
+    """主函数"""
+    
+    # 视频文件路径
+    video_path = r"E:\20251204\D15_20251130235724.mp4"
+    
+    # 时间设置(28分钟到30分钟)
+    start_time_minutes = 0
+    end_time_minutes = 15
+    start_time_seconds = start_time_minutes * 60  # 28分钟 = 1680秒
+    end_time_seconds = end_time_minutes * 60      # 30分钟 = 1800秒
+    
+    # 输出目录
+    output_dir = r"D:\data\20251204\OUT5"
+    
+    # 目标提取帧数
+    target_frame_count = 100
+    
+    print(f"视频画面提取工具")
+    print(f"=" * 50)
+    print(f"输入视频: {video_path}")
+    print(f"提取时间段: {start_time_minutes}:00 - {end_time_minutes}:00")
+    print(f"目标帧数: {target_frame_count}")
+    print(f"输出目录: {output_dir}")
+    print(f"=" * 50)
+    
+    # 执行提取
+    success = extract_frames_from_video(
+        video_path=video_path,
+        start_time_seconds=start_time_seconds,
+        end_time_seconds=end_time_seconds,
+        output_dir=output_dir,
+        target_frame_count=target_frame_count
+    )
+    
+    if success:
+        print(f"\n✓ 画面提取成功完成!")
+    else:
+        print(f"\n✗ 画面提取失败!")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

+ 198 - 0
video_segment_extractor.py

@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+视频时间段提取工具
+功能:从视频文件中提取指定时间段的内容,并保存为图片帧序列
+作者:工程师
+"""
+
+import cv2
+import os
+import argparse
+import sys
+from pathlib import Path
+import time
+
+
+def parse_time_to_seconds(time_str):
+    """
+    将时间字符串转换为秒数
+    支持格式:
+    - 秒数:"30" 或 "30.5"
+    - 分:秒:"1:30" 或 "1:30.5"
+    - 时:分:秒:"0:1:30" 或 "0:1:30.5"
+    """
+    try:
+        if ':' not in time_str:
+            return float(time_str)
+        
+        parts = time_str.split(':')
+        if len(parts) == 2:  # MM:SS
+            minutes, seconds = parts
+            return int(minutes) * 60 + float(seconds)
+        elif len(parts) == 3:  # HH:MM:SS
+            hours, minutes, seconds = parts
+            return int(hours) * 3600 + int(minutes) * 60 + float(seconds)
+        else:
+            raise ValueError("时间格式不正确")
+    except ValueError as e:
+        raise ValueError(f"无法解析时间格式 '{time_str}': {e}")
+
+
+def extract_video_segment(video_path, start_time, end_time, output_dir, fps=None, prefix="frame"):
+    """
+    从视频中提取指定时间段的帧
+    
+    Args:
+        video_path (str): 输入视频文件路径
+        start_time (str): 开始时间(秒或时:分:秒格式)
+        end_time (str): 结束时间(秒或时:分:秒格式)
+        output_dir (str): 输出目录路径
+        fps (float): 提取帧率,None表示使用原视频帧率
+        prefix (str): 输出文件名前缀
+    
+    Returns:
+        tuple: (成功提取的帧数, 总处理时间)
+    """
+    # 检查输入文件
+    if not os.path.exists(video_path):
+        raise FileNotFoundError(f"视频文件不存在: {video_path}")
+    
+    # 创建输出目录
+    output_path = Path(output_dir)
+    output_path.mkdir(parents=True, exist_ok=True)
+    
+    # 解析时间
+    start_seconds = parse_time_to_seconds(start_time)
+    end_seconds = parse_time_to_seconds(end_time)
+    
+    if start_seconds >= end_seconds:
+        raise ValueError("开始时间必须小于结束时间")
+    
+    print(f"正在处理视频: {video_path}")
+    print(f"提取时间段: {start_seconds:.2f}s - {end_seconds:.2f}s")
+    print(f"输出目录: {output_dir}")
+    
+    # 打开视频文件
+    cap = cv2.VideoCapture(video_path)
+    if not cap.isOpened():
+        raise RuntimeError(f"无法打开视频文件: {video_path}")
+    
+    try:
+        # 获取视频信息
+        video_fps = cap.get(cv2.CAP_PROP_FPS)
+        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
+        video_duration = total_frames / video_fps
+        
+        print(f"视频信息: {video_fps:.2f} FPS, {total_frames} 帧, {video_duration:.2f}s")
+        
+        # 检查时间范围
+        if end_seconds > video_duration:
+            print(f"警告: 结束时间 {end_seconds:.2f}s 超过视频长度 {video_duration:.2f}s,将调整为视频结束时间")
+            end_seconds = video_duration
+        
+        # 设置提取帧率
+        extract_fps = fps if fps is not None else video_fps
+        frame_interval = video_fps / extract_fps if extract_fps <= video_fps else 1
+        
+        print(f"提取帧率: {extract_fps:.2f} FPS (每 {frame_interval:.2f} 帧提取一帧)")
+        
+        # 定位到开始时间
+        start_frame = int(start_seconds * video_fps)
+        end_frame = int(end_seconds * video_fps)
+        cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
+        
+        extracted_count = 0
+        current_frame = start_frame
+        next_extract_frame = start_frame
+        start_time_process = time.time()
+        
+        print(f"开始提取帧 ({start_frame} - {end_frame})...")
+        
+        while current_frame <= end_frame:
+            ret, frame = cap.read()
+            if not ret:
+                break
+            
+            # 检查是否需要提取当前帧
+            if current_frame >= next_extract_frame:
+                # 生成输出文件名
+                output_filename = f"{prefix}_{extracted_count:06d}.jpg"
+                output_filepath = output_path / output_filename
+                
+                # 保存帧
+                success = cv2.imwrite(str(output_filepath), frame)
+                if success:
+                    extracted_count += 1
+                    if extracted_count % 100 == 0:
+                        progress = (current_frame - start_frame) / (end_frame - start_frame) * 100
+                        print(f"已提取 {extracted_count} 帧 ({progress:.1f}%)")
+                else:
+                    print(f"警告: 无法保存帧到 {output_filepath}")
+                
+                # 计算下一个要提取的帧
+                next_extract_frame += frame_interval
+            
+            current_frame += 1
+        
+        process_time = time.time() - start_time_process
+        
+        print(f"\n提取完成!")
+        print(f"成功提取 {extracted_count} 帧")
+        print(f"处理时间: {process_time:.2f}s")
+        print(f"输出目录: {output_dir}")
+        
+        return extracted_count, process_time
+        
+    finally:
+        cap.release()
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="从视频文件中提取指定时间段的帧",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+时间格式示例:
+  30        - 30秒
+  1:30      - 1分30秒
+  0:1:30    - 1分30秒
+  1:23:45   - 1小时23分45秒
+  
+使用示例:
+  python video_segment_extractor.py input.mp4 10 60 -o frames
+  python video_segment_extractor.py input.mp4 0:10 1:00 -o frames --fps 10
+        """
+    )
+    
+    parser.add_argument("video", help="输入视频文件路径")
+    parser.add_argument("start_time", help="开始时间 (秒数或时:分:秒格式)")
+    parser.add_argument("end_time", help="结束时间 (秒数或时:分:秒格式)")
+    parser.add_argument("-o", "--output", default="extracted_frames", 
+                       help="输出目录 (默认: extracted_frames)")
+    parser.add_argument("--fps", type=float, 
+                       help="提取帧率 (默认使用原视频帧率)")
+    parser.add_argument("--prefix", default="frame", 
+                       help="输出文件名前缀 (默认: frame)")
+    
+    args = parser.parse_args()
+    
+    try:
+        extracted_count, process_time = extract_video_segment(
+            args.video,
+            args.start_time,
+            args.end_time,
+            args.output,
+            args.fps,
+            args.prefix
+        )
+        
+        print(f"\n任务完成! 共提取 {extracted_count} 帧,耗时 {process_time:.2f}s")
+        
+    except Exception as e:
+        print(f"错误: {e}", file=sys.stderr)
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

BIN
yolov8n-seg.pt