2 次代码提交 6c6e90a3ae ... 64d260a74b

作者 SHA1 备注 提交日期
  qq1194550395 64d260a74b 20251215 1 周之前
  sunting 6c6e90a3ae Initial commit 7 月之前

+ 0 - 134
.gitignore

@@ -1,134 +0,0 @@
-# ---> Python
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# ---> Qt
-# C++ objects and libs
-
-*.slo
-*.lo
-*.o
-*.a
-*.la
-*.lai
-*.so
-*.dll
-*.dylib
-
-# Qt-es
-
-/.qmake.cache
-/.qmake.stash
-*.pro.user
-*.pro.user.*
-*.qbs.user
-*.qbs.user.*
-*.moc
-moc_*.cpp
-qrc_*.cpp
-ui_*.h
-Makefile*
-*-build-*
-
-# QtCreator
-
-*.autosave
-
-#QtCtreator Qml
-*.qmlproject.user
-*.qmlproject.user.*
-
-# ---> C++
-# Compiled Object files
-*.slo
-*.lo
-*.o
-*.obj
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Compiled Dynamic libraries
-*.so
-*.dylib
-*.dll
-
-# Fortran module files
-*.mod
-
-# Compiled Static libraries
-*.lai
-*.la
-*.a
-*.lib
-
-# Executables
-*.exe
-*.out
-*.app
-
-# ---> CUDA
-*.i
-*.ii
-*.gpu
-*.ptx
-*.cubin
-*.fatbin
-

+ 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()

+ 0 - 72
LICENSE

@@ -1,72 +0,0 @@
-Apache License 
-Version 2.0, January 2004 
-http://www.apache.org/licenses/
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
-"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
-
-"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
-
-"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
-
-"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
-
-"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
-
-"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
-
-"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
-
-(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
-
-(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
-
-(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
-
-(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
-
-You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
-To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
-
-Copyright [yyyy] [name of copyright owner]
-
-Licensed under the Apache License, Version 2.0 (the "License"); 
-you may not use this file except in compliance with the License. 
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software 
-distributed under the License is distributed on an "AS IS" BASIS, 
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
-See the License for the specific language governing permissions and 
-limitations under the License.

+ 0 - 3
README.md

@@ -1,3 +0,0 @@
-# day
-
-no

+ 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`。
+- 自动分割训练/验证集并生成标签。

二进制
SteamSetup.exe


二进制
__pycache__/json_to_yolo.cpython-312.pyc


二进制
__pycache__/video_frame_extractor.cpython-310.pyc


二进制
__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)

二进制
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)

二进制
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()

二进制
yolov8n-seg.pt