Browse Source

初版 车牌识别

xu 6 months ago
parent
commit
d2f56bd684
100 changed files with 9300 additions and 0 deletions
  1. 17 0
      .gitignore
  2. 8 0
      .idea/.gitignore
  3. 1 0
      .idea/.name
  4. 48 0
      .idea/inspectionProfiles/Project_Default.xml
  5. 6 0
      .idea/inspectionProfiles/profiles_settings.xml
  6. 10 0
      .idea/misc.xml
  7. 8 0
      .idea/modules.xml
  8. 10 0
      .idea/pip_venv.iml
  9. 8 0
      .idea/ruff.xml
  10. 9 0
      .idea/vcs.xml
  11. 1 0
      .python-version
  12. 54 0
      bx/BxArea.py
  13. 149 0
      bx/BxAreaDynamic.py
  14. 93 0
      bx/BxByteArray.py
  15. 53 0
      bx/BxCmd.py
  16. 81 0
      bx/BxCmdBrightness.py
  17. 23 0
      bx/BxCmdCancelTimingSwitch.py
  18. 24 0
      bx/BxCmdClearScreen.py
  19. 32 0
      bx/BxCmdCode.py
  20. 36 0
      bx/BxCmdDelDynamicArea.py
  21. 22 0
      bx/BxCmdFactory.py
  22. 77 0
      bx/BxCmdFileBitmap.py
  23. 34 0
      bx/BxCmdFileDelete.py
  24. 19 0
      bx/BxCmdFileRead.py
  25. 149 0
      bx/BxCmdFileWrite.py
  26. 35 0
      bx/BxCmdLock.py
  27. 19 0
      bx/BxCmdReadParams.py
  28. 83 0
      bx/BxCmdSendDynamicArea.py
  29. 28 0
      bx/BxCmdState.py
  30. 139 0
      bx/BxCmdSystemClockCorrect.py
  31. 43 0
      bx/BxCmdTimingSwitch.py
  32. 30 0
      bx/BxCmdTurnOnOff.py
  33. 325 0
      bx/BxDataPack.py
  34. 54 0
      bx/BxResp.py
  35. 113 0
      bx/BxUtils.py
  36. 9 0
      bx/__init__.py
  37. 32 0
      bx/bxError.py
  38. 57 0
      bxx/BxArea.go
  39. 210 0
      bxx/BxAreaDynamic.go
  40. 105 0
      bxx/BxByteArray.go
  41. 61 0
      bxx/BxCmd.go
  42. 106 0
      bxx/BxCmdBrightness.go
  43. 27 0
      bxx/BxCmdCancelTimingSwitch.go
  44. 25 0
      bxx/BxCmdClearScreen.go
  45. 34 0
      bxx/BxCmdCode.go
  46. 36 0
      bxx/BxCmdDelDynamicArea.go
  47. 87 0
      bxx/BxCmdFileBitmap.go
  48. 36 0
      bxx/BxCmdFileDelete.go
  49. 25 0
      bxx/BxCmdFileRead.go
  50. 168 0
      bxx/BxCmdFileWrite.go
  51. 39 0
      bxx/BxCmdLock.go
  52. 15 0
      bxx/BxCmdReadParams.go
  53. 82 0
      bxx/BxCmdSendDynamicArea.go
  54. 27 0
      bxx/BxCmdState.go
  55. 159 0
      bxx/BxCmdSystemClockCorrect.go
  56. 42 0
      bxx/BxCmdTimingSwitch.go
  57. 34 0
      bxx/BxCmdTurnOnOff.go
  58. 229 0
      bxx/BxDataPack.go
  59. 214 0
      bxx/BxResp.go
  60. 105 0
      bxx/BxUtils.go
  61. 28 0
      bxx/BxUtils_test.go
  62. 31 0
      bxx/bxError.go
  63. 196 0
      ccpd_process.py
  64. 21 0
      data/argoverse_hd.yaml
  65. 35 0
      data/coco.yaml
  66. 28 0
      data/coco128.yaml
  67. 38 0
      data/hyp.finetune.yaml
  68. 34 0
      data/hyp.scratch.yaml
  69. 20 0
      data/plateAndCar.yaml
  70. 150 0
      data/retinaface2yolo.py
  71. 62 0
      data/scripts/get_argoverse_hd.sh
  72. 27 0
      data/scripts/get_coco.sh
  73. 139 0
      data/scripts/get_voc.sh
  74. 176 0
      data/train2yolo.py
  75. 88 0
      data/val2yolo.py
  76. 65 0
      data/val2yolo_for_test.py
  77. 21 0
      data/voc.yaml
  78. 19 0
      data/widerface.yaml
  79. BIN
      debug_frame_0_1920x1080.jpg
  80. 218 0
      detect_demo.py
  81. 925 0
      detect_plate.py
  82. 509 0
      detect_plate.py.bk
  83. 723 0
      detect_plate_20260130.py
  84. 161 0
      export.py
  85. BIN
      first_frame_debug.jpg
  86. 141 0
      hubconf.py
  87. 121 0
      json2yolo.py
  88. 80 0
      main.py
  89. 0 0
      models/__init__.py
  90. 33 0
      models/blazeface.yaml
  91. 38 0
      models/blazeface_fpn.yaml
  92. 456 0
      models/common.py
  93. 133 0
      models/experimental.py
  94. 519 0
      models/yolo.py
  95. 47 0
      models/yolov5l.yaml
  96. 60 0
      models/yolov5l6.yaml
  97. 47 0
      models/yolov5m.yaml
  98. 60 0
      models/yolov5m6.yaml
  99. 46 0
      models/yolov5n-0.5.yaml
  100. 0 0
      models/yolov5n.yaml

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
+
+myenv
+imgs
+fonts
+result
+
+

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 1 - 0
.idea/.name

@@ -0,0 +1 @@
+ccpd_process.py

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

@@ -0,0 +1,48 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyArgumentEqualDefaultInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyAugmentAssignmentInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ourVersions">
+        <value>
+          <list size="2">
+            <item index="0" class="java.lang.String" itemvalue="3.13" />
+            <item index="1" class="java.lang.String" itemvalue="3.12" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyMissingTypeHintsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <value>
+          <list size="14">
+            <item index="0" class="java.lang.String" itemvalue="pygit2" />
+            <item index="1" class="java.lang.String" itemvalue="PyQtDataVisualization" />
+            <item index="2" class="java.lang.String" itemvalue="PyQt5" />
+            <item index="3" class="java.lang.String" itemvalue="PyQt5-sip" />
+            <item index="4" class="java.lang.String" itemvalue="PyQtPurchasing" />
+            <item index="5" class="java.lang.String" itemvalue="PyQt3D" />
+            <item index="6" class="java.lang.String" itemvalue="PyQtChart" />
+            <item index="7" class="java.lang.String" itemvalue="PyQtWebKit" />
+            <item index="8" class="java.lang.String" itemvalue="PyQtWebEngine" />
+            <item index="9" class="java.lang.String" itemvalue="pandas" />
+            <item index="10" class="java.lang.String" itemvalue="numpy" />
+            <item index="11" class="java.lang.String" itemvalue="sentencepiece" />
+            <item index="12" class="java.lang.String" itemvalue="haiku" />
+            <item index="13" class="java.lang.String" itemvalue="pygame" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredIdentifiers">
+        <list>
+          <option value="jax.random" />
+        </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>

+ 10 - 0
.idea/misc.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="Python 3.13 (pip_venv)" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
+  <component name="PythonCompatibilityInspectionAdvertiser">
+    <option name="version" value="3" />
+  </component>
+</project>

+ 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/pip_venv.iml" filepath="$PROJECT_DIR$/.idea/pip_venv.iml" />
+    </modules>
+  </component>
+</project>

+ 10 - 0
.idea/pip_venv.iml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.venv" />
+    </content>
+    <orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 8 - 0
.idea/ruff.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RuffConfigService">
+    <option name="globalRuffExecutablePath" value="A:\Scoop\shims\ruff.exe" />
+    <option name="runRuffOnSave" value="true" />
+    <option name="useRuffServer" value="true" />
+  </component>
+</project>

+ 9 - 0
.idea/vcs.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+  <component name="VcsProjectSettings">
+    <option name="detectVcsMappingsAutomatically" value="false" />
+  </component>
+</project>

+ 1 - 0
.python-version

@@ -0,0 +1 @@
+3.13

+ 54 - 0
bx/BxArea.py

@@ -0,0 +1,54 @@
+class BxArea:
+    def Build(self):
+        """构建区域数据"""
+        pass
+
+    def Length(self):
+        """获取区域长度"""
+        pass
+
+
+class BaseArea(BxArea):
+    def __init__(self, typ, x, y, w, h):
+        self.typ = typ
+        self.x = x
+        self.y = y
+        self.w = w
+        self.h = h
+
+    def GetX(self):
+        """获取X坐标"""
+        return self.x
+
+    def SetX(self, x):
+        """设置X坐标"""
+        self.x = x
+
+    def GetY(self):
+        """获取Y坐标"""
+        return self.y
+
+    def SetY(self, y):
+        """设置Y坐标"""
+        self.y = y
+
+    def GetW(self):
+        """获取宽度"""
+        return self.w
+
+    def SetW(self, w):
+        """设置宽度"""
+        self.w = w
+
+    def GetH(self):
+        """获取高度"""
+        return self.h
+
+    def SetH(self, h):
+        """设置高度"""
+        self.h = h
+
+
+# 创建BxArea实例的函数
+def NewBxArea(typ, x, y, w, h):
+    return BaseArea(typ, x, y, w, h)

+ 149 - 0
bx/BxAreaDynamic.py

@@ -0,0 +1,149 @@
+import struct
+from .BxArea import BaseArea, NewBxArea
+
+
+class BxAreaDynamic(BaseArea):
+    def __init__(self, id, runMode, dispMode, x, y, w, h, data, soundData, is5K):
+        super().__init__(0, x, y, w, h)
+        self.is5K = is5K
+        self.id = id
+        self.lineSpace = 0
+        self.runMode = runMode
+        self.timeout = 5
+        self.soundMode = 0x00
+        self.soundPerson = 0x00
+        self.soundRepeat = 0x00
+        self.soundVolume = 0x01
+        self.soundSpeed = 0x10
+        self.soundData = soundData if soundData else []
+        self.extendParaLen = 0
+        self.alignment = 0
+        self.singleLine = 0x02
+        self.autoNewLine = 0x01
+        self.dispMode = dispMode
+        self.exitMode = 0
+        self.speed = 0x0a
+        self.holdTime = 0x08
+        self.data = data if data else []
+
+    def Length(self):
+        """获取区域长度"""
+        return 27 + len(self.data)
+
+    def Build(self):
+        """构建区域数据"""
+        result = bytearray()
+        # 写入类型
+        result.append(self.typ)
+        
+        # 处理坐标和尺寸
+        x8 = self.GetX()
+        w8 = self.GetW()
+        if self.is5K:
+            x8 = self.x // 8
+            w8 = self.w // 8
+        
+        # 写入坐标和尺寸(小端序)
+        result.extend(struct.pack('<H', x8))
+        result.extend(struct.pack('<H', self.GetY()))
+        result.extend(struct.pack('<H', w8))
+        result.extend(struct.pack('<H', self.GetH()))
+        
+        # 写入动态区编号
+        result.append(self.id)
+        # 写入行间距
+        result.append(self.lineSpace)
+        # 写入运行模式
+        result.append(self.runMode)
+        # 写入超时时间
+        result.extend(struct.pack('<h', self.timeout))
+        # 写入声音模式
+        result.append(self.soundMode)
+        
+        # 处理声音参数
+        if self.soundMode == 0x01 or self.soundMode == 0x02:
+            pr = ((self.soundRepeat << 4) & 0xf0) | (self.soundPerson & 0x0f)
+            result.append(pr)
+            result.append(self.soundVolume)
+            result.append(self.soundSpeed)
+        
+        # 处理声音数据
+        if self.soundMode == 0x02:
+            soundDataLen = len(self.soundData)
+            result.extend(struct.pack('<I', soundDataLen))
+            result.extend(self.soundData)
+        
+        # 写入扩展参数长度
+        result.append(self.extendParaLen)
+        # 写入对齐方式
+        result.append(self.alignment)
+        # 写入单行模式
+        result.append(self.singleLine)
+        # 写入自动换行
+        result.append(self.autoNewLine)
+        # 写入显示模式
+        result.append(self.dispMode)
+        # 写入退出模式
+        result.append(self.exitMode)
+        # 写入速度
+        result.append(self.speed)
+        # 写入保持时间
+        result.append(self.holdTime)
+        # 写入数据长度
+        result.extend(struct.pack('<I', len(self.data)))
+        # 写入数据
+        result.extend(self.data)
+        
+        return bytes(result)
+
+    def SetSoundMode(self, soundMode):
+        """设置声音模式"""
+        if soundMode > 2:
+            self.soundMode = 0x02
+        else:
+            self.soundMode = soundMode
+
+    def SetSoundPerson(self, soundPerson):
+        """设置声音人物"""
+        if soundPerson > 5:
+            self.soundPerson = 0
+        else:
+            self.soundPerson = soundPerson
+
+    def SetSoundRepeat(self, soundRepeat):
+        """设置声音重复次数"""
+        if soundRepeat > 15:
+            self.soundRepeat = 15
+        else:
+            self.soundRepeat = soundRepeat
+
+    def SetSoundVolume(self, soundVolume):
+        """设置声音音量"""
+        if soundVolume > 10:
+            self.soundVolume = 10
+        else:
+            self.soundVolume = soundVolume
+
+    def SetSoundSpeed(self, soundSpeed):
+        """设置声音速度"""
+        if soundSpeed < 1:
+            self.soundSpeed = 1
+        elif soundSpeed > 10:
+            self.soundSpeed = 10
+        else:
+            self.soundSpeed = soundSpeed
+
+
+# 创建BxAreaDynamic实例的函数
+def NewBxAreaDynamic(id, runMode, dispMode, x, y, w, h, data, soundData, is5K):
+    return BxAreaDynamic(id, runMode, dispMode, x, y, w, h, data, soundData, is5K)
+
+
+# 创建程序动态区域实例的函数
+def NewBxAreaProgram(id, runMode, dispMode, alignment, x, y, w, h, data, is5K):
+    area = BxAreaDynamic(id, runMode, dispMode, x, y, w, h, data, [], is5K)
+    area.alignment = alignment
+    area.soundVolume = 0x05
+    area.soundSpeed = 0x05
+    area.speed = 0x01
+    return area

+ 93 - 0
bx/BxByteArray.py

@@ -0,0 +1,93 @@
+import struct
+
+# 默认容量
+DefaultCapacity = 128
+
+
+class BxByteArray:
+    def __init__(self, capacity):
+        self.list = bytearray(capacity)
+        self.next = 0
+
+    def add(self, data):
+        if self.next == len(self.list):
+            # 扩展容量
+            self.list.extend(bytearray(len(self.list) * 2))
+        self.list[self.next] = data
+        self.next += 1
+
+    def addInt16(self, data, endian):
+        # 确保容量足够
+        if self.next + 1 >= len(self.list):
+            self.list.extend(bytearray(len(self.list) * 2))
+        
+        # 打包为2字节
+        if endian == 0:  # LITTLE
+            packed = struct.pack('<h', data)
+        else:  # BIG
+            packed = struct.pack('>h', data)
+        
+        # 复制到列表
+        self.list[self.next:self.next+2] = packed
+        self.next += 2
+
+    def addInt(self, data, endian):
+        # 确保容量足够
+        if self.next + 3 >= len(self.list):
+            self.list.extend(bytearray(len(self.list) * 2))
+        
+        # 打包为4字节
+        if endian == 0:  # LITTLE
+            packed = struct.pack('<i', data)
+        else:  # BIG
+            packed = struct.pack('>i', data)
+        
+        # 复制到列表
+        self.list[self.next:self.next+4] = packed
+        self.next += 4
+
+    def addBytes(self, src):
+        if src:
+            # 确保容量足够
+            if self.next + len(src) - 1 >= len(self.list):
+                self.list.extend(bytearray(len(self.list) + len(src)))
+            
+            # 复制数据
+            self.list[self.next:self.next+len(src)] = src
+            self.next += len(src)
+
+    def addBytesOffsetLength(self, src, offset, length):
+        if src:
+            # 确保容量足够
+            if self.next + length - 1 >= len(self.list):
+                self.list.extend(bytearray(len(self.list) + length))
+            
+            # 复制数据
+            self.list[self.next:self.next+length] = src[offset:offset+length]
+            self.next += length
+
+    def set(self, index, data):
+        if index < len(self.list):
+            self.list[index] = data
+
+    def get(self, index):
+        return self.list[index]
+
+    def Build(self):
+        return bytes(self.list[:self.next])
+
+    def size(self):
+        return self.next
+
+    def clear(self):
+        self.next = 0
+
+
+# 创建BxByteArray实例的函数
+def NewBxByteArray(capacity):
+    return BxByteArray(capacity)
+
+
+# 创建默认容量的BxByteArray实例的函数
+def NewDefaultBxByteArray():
+    return NewBxByteArray(DefaultCapacity)

+ 53 - 0
bx/BxCmd.py

@@ -0,0 +1,53 @@
+class BxCmd:
+    def Build(self):
+        """构建命令数据"""
+        pass
+
+
+class baseBxCmd(BxCmd):
+    def __init__(self, group, cmd):
+        self.group = group
+        self.cmd = cmd
+        self.reqResp = 0x01
+        self.r0 = 0
+        self.r1 = 0
+
+    def Group(self):
+        """获取命令组"""
+        return self.group
+
+    def SetGroup(self, group):
+        """设置命令组"""
+        self.group = group
+
+    def Cmd(self):
+        """获取命令码"""
+        return self.cmd
+
+    def SetCmd(self, cmd):
+        """设置命令码"""
+        self.cmd = cmd
+
+    def ReqResp(self):
+        """获取请求/响应标志"""
+        return self.reqResp
+
+    def SetReqResp(self, reqResp):
+        """设置请求/响应标志"""
+        self.reqResp = reqResp
+
+    def R0(self):
+        """获取保留字r0"""
+        return self.r0
+
+    def SetR0(self, r0):
+        """设置保留字r0"""
+        self.r0 = r0
+
+    def R1(self):
+        """获取保留字r1"""
+        return self.r1
+
+    def SetR1(self, r1):
+        """设置保留字r1"""
+        self.r1 = r1

+ 81 - 0
bx/BxCmdBrightness.py

@@ -0,0 +1,81 @@
+from .BxCmd import baseBxCmd, newBaseCmd
+from .BxCmdCode import CMD_SCREEN_BRIGHTNESS
+
+
+class CmdBrightness(baseBxCmd):
+    def __init__(self, brightnessType, currentBrightness, brightnessValue):
+        # 初始化基础命令
+        super().__init__(CMD_SCREEN_BRIGHTNESS.group, CMD_SCREEN_BRIGHTNESS.code)
+        self.BrightnessType = brightnessType
+        
+        # 处理亮度值边界(0-15)
+        self.CurrentBrightness = currentBrightness
+        if self.CurrentBrightness > 15:
+            self.CurrentBrightness = 15
+        
+        # 处理定时调节的亮度列表长度(确保为48字节)
+        self.BrightnessValue = processBrightnessValue(brightnessValue)
+
+    def SetBrightnessType(self, brightnessType):
+        self.BrightnessType = brightnessType
+        # 切换为定时调节时自动初始化48字节亮度列表
+        if brightnessType == 0x02 and len(self.BrightnessValue) != 48:
+            self.BrightnessValue = bytearray(48)
+
+    def SetCurrentBrightness(self, currentBrightness):
+        if currentBrightness > 15:
+            currentBrightness = 15
+        self.CurrentBrightness = currentBrightness
+
+    def SetBrightnessValue(self, brightnessValue):
+        self.BrightnessValue = processBrightnessValue(brightnessValue)
+
+    def Build(self):
+        result = bytearray()
+        
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(0x01)
+        # 写入保留值
+        result.extend([0x00, 0x00])
+        # 写入亮度调节方式
+        result.append(self.BrightnessType)
+        
+        # 根据调节方式写入后续参数
+        if self.BrightnessType == 0x01:
+            # 强制调节:写入亮度值(0-15)
+            result.append(self.CurrentBrightness)
+        elif self.BrightnessType == 0x02:
+            # 定时调节:写入默认亮度值0和48字节亮度列表
+            result.append(0x00)
+            result.extend(self.BrightnessValue)
+        else:
+            # 无效类型默认按强制调节处理(亮度0)
+            result.append(0x00)
+        
+        return bytes(result)
+
+
+# 处理定时调节的亮度列表(确保长度为48字节)
+def processBrightnessValue(value):
+    result = bytearray(48)
+    if value:
+        if len(value) > 48:
+            result[:48] = value[:48]  # 超出部分截取
+        else:
+            result[:len(value)] = value  # 不足部分补0
+    
+    # 确保每个亮度值在0-15范围内
+    for i in range(len(result)):
+        if result[i] > 15:
+            result[i] = 15
+    
+    return result
+
+
+# 初始化亮度设置命令
+def NewCmdBrightness(brightnessType, currentBrightness, brightnessValue):
+    return CmdBrightness(brightnessType, currentBrightness, brightnessValue)

+ 23 - 0
bx/BxCmdCancelTimingSwitch.py

@@ -0,0 +1,23 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_CANCEL_TIMING_SWITCH
+
+
+class CmdCancelTimingSwitch(baseBxCmd):
+    def __init__(self):
+        super().__init__(CMD_CANCEL_TIMING_SWITCH.group, CMD_CANCEL_TIMING_SWITCH.code)
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留值
+        result.extend([0x00, 0x00])
+        return None
+
+
+def NewCmdCancelTimingSwitch():
+    return CmdCancelTimingSwitch()

+ 24 - 0
bx/BxCmdClearScreen.py

@@ -0,0 +1,24 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_CLEAR_SCREEN
+
+
+class BxCmdClearScreen(baseBxCmd):
+    def __init__(self):
+        super().__init__(CMD_CLEAR_SCREEN.group, CMD_CLEAR_SCREEN.code)
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.group)
+        # 写入命令
+        result.append(self.cmd)
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留值
+        result.append(self.r0)
+        result.append(self.r1)
+        return bytes(result)
+
+
+def NewBxCmdClearScreen(group, cmd):
+    return BxCmdClearScreen()

+ 32 - 0
bx/BxCmdCode.py

@@ -0,0 +1,32 @@
+class CmdCode:
+    def __init__(self, name, group, code):
+        self.name = name
+        self.group = group
+        self.code = code
+
+
+# 命令常量定义
+CMD_ACK = CmdCode("ack", 0xa0, 0x00)
+CMD_NACK = CmdCode("nack", 0xa0, 0x01)
+CMD_DEL_FILE = CmdCode("delete file", 0xa1, 0x01)
+CMD_SYSTEM_STATE = CmdCode("system state", 0xa1, 0x02)
+CMD_SYSTEM_PING = CmdCode("ping", 0xa2, 0x00)
+CMD_SYSTEM_HEARTBEAT = CmdCode("system heartbeat", 0xa4, 0x07)
+CMD_START_WRITE_FILE = CmdCode("start write file", 0xa1, 0x05)
+CMD_WRITE_FILE = CmdCode("write file", 0xa1, 0x06)
+CMD_WRITE_TRANS_START = CmdCode("start write trans", 0xa1, 0x07)
+CMD_WRITE_TRANS_STOP = CmdCode("stop the write trans", 0xa1, 0x08)
+CMD_WRITE_CUSTOMER_INFO = CmdCode("write customer information", 0xa1, 0x09)
+CMD_GET_FILE_INTO = CmdCode("get file information", 0xa1, 0x0a)
+CMD_GET_FILE_CONTENT = CmdCode("get file content", 0xa1, 0x0b)
+CMD_SYSTEM_CLOCK_CORRECT = CmdCode("system clock correct", 0xa2, 0x03)
+CMD_READ_PARAMS = CmdCode("read params", 0xa2, 0x0a)
+CMD_SOUND = CmdCode("add sound", 0xa2, 0x0e)
+CMD_TURN_ON_OFF = CmdCode("turn on/off screen", 0xa3, 0x00)
+CMD_TIMING_SWITCH = CmdCode("auto turn on/off screen", 0xa3, 0x01)
+CMD_SCREEN_BRIGHTNESS = CmdCode("set brightness", 0xa3, 0x02)
+CMD_LOCK_UNLOCK = CmdCode("lock or unlock program", 0xa3, 0x04)
+CMD_SEND_DYNAMIC_AREA = CmdCode("send dynamic area", 0xa3, 0x06)
+CMD_DEL_DYNAMIC_AREA = CmdCode("delete dynamic area", 0xa3, 0x07)
+CMD_CANCEL_TIMING_SWITCH = CmdCode("cancel auto turn on/off screen", 0xa3, 0x08)
+CMD_CLEAR_SCREEN = CmdCode("clear the screen", 0xa3, 0x10)

+ 36 - 0
bx/BxCmdDelDynamicArea.py

@@ -0,0 +1,36 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_DEL_DYNAMIC_AREA
+
+
+class CmdDelDynamicArea(baseBxCmd):
+    def __init__(self, numbers):
+        super().__init__(CMD_DEL_DYNAMIC_AREA.group, CMD_DEL_DYNAMIC_AREA.code)
+        self.numbers = numbers if numbers else []
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(0x01)
+        # 写入保留值
+        result.extend([0x00, 0x00])
+        
+        # 写入数量
+        l = len(self.numbers)
+        if l == 0:
+            result.append(0xff)
+        else:
+            result.append(l)
+        
+        # 写入编号
+        for n in self.numbers:
+            result.append(n)
+        
+        return bytes(result)
+
+
+def NewCmdDelDynamicArea(numbers):
+    return CmdDelDynamicArea(numbers)

+ 22 - 0
bx/BxCmdFactory.py

@@ -0,0 +1,22 @@
+from .BxCmdSendDynamicArea import NewBxCmdSendDynamicArea
+from .BxCmdState import NewCmdState
+from .BxCmdReadParams import NewCmdReadParams
+
+
+class BxCmdFactory:
+    """命令工厂类,用于创建各种命令实例"""
+
+    @staticmethod
+    def NewBxCmdSendDynamicArea(areas):
+        """创建发送动态区域命令"""
+        return NewBxCmdSendDynamicArea(areas)
+
+    @staticmethod
+    def NewCmdState():
+        """创建系统状态命令"""
+        return NewCmdState()
+
+    @staticmethod
+    def NewCmdReadParams():
+        """创建读取参数命令"""
+        return NewCmdReadParams()

+ 77 - 0
bx/BxCmdFileBitmap.py

@@ -0,0 +1,77 @@
+import struct
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_WRITE_FILE, CMD_START_WRITE_FILE
+from .BxUtils import CRC16
+
+
+class bitmapFile:
+    def __init__(self, filename, libdata):
+        chk = CRC16(libdata, 0, len(libdata))
+        print(f"位图文件校验: {chk: 02x}")
+        self.FileType = 0x04
+        self.FileName = filename
+        self.FileLen = len(libdata)
+        self.LibData = libdata
+        self.CHK = chk
+
+    def NewCmd(self):
+        return CmdWriteBitmapFile(self)
+
+
+class CmdWriteBitmapFile(baseBxCmd):
+    def __init__(self, file):
+        super().__init__(CMD_WRITE_FILE.group, CMD_WRITE_FILE.code)
+        self.state = 0
+        self.file = file
+        self.LastBlockFlag = 1
+        self.BlockNum = 0  # 包号,如果是单包发送,则默认为 0x00。
+        self.BlockLen = 0  # 包长,若是单包发送,此处为文件长度。
+        self.BlockAddr = 0  # 本包数据在文件中的起始位置,如果是单包发送,此处默认为 0。
+        self.temp = bytearray()
+
+    def Build(self):
+        if self.state == 0:
+            # Write File
+            w1 = bytearray()
+            # 文件描述数据
+            w1.append(self.Group())
+            w1.append(self.Cmd())
+            w1.append(0x01)
+            w1.extend([0x00, 0x00])
+            w1.extend(self.file.FileName.encode('ascii'))
+            w1.append(self.LastBlockFlag)  # 是否是最后一包,0x00——不是最后一包 0x01——最后一包。
+            w1.extend(struct.pack('<H', self.BlockNum))  # 包号,单包为0x00
+            w1.extend(struct.pack('<I', self.file.FileLen))  # 包长,若是单包发送,此处为文件长度。
+            w1.extend(struct.pack('<I', self.BlockAddr))  # 本包数据在文件中的偏移量,单包为0x00
+            
+            # 文件内容数据
+            w2 = bytearray()
+            w2.append(self.file.FileType)
+            w2.extend(self.file.FileName.encode('ascii'))
+            w2.extend(struct.pack('<I', self.file.FileLen))
+            w2.extend(self.file.LibData)
+            
+            data = w2
+            crc = CRC16(data, 0, len(data))
+            w1.extend(data)
+            w1.extend(struct.pack('<H', crc))
+            self.temp = w1
+            
+            # Start Write File "开始写文件",写文件前先检查内存是否够用
+            w3 = bytearray()
+            w3.append(CMD_START_WRITE_FILE.group)
+            w3.append(CMD_START_WRITE_FILE.code)
+            w3.append(0x01)
+            w3.extend([0x00, 0x00])
+            w3.append(0x01)  # 同名是否覆盖,0不覆盖,1覆盖
+            w3.extend(self.file.FileName.encode('ascii'))
+            w3.extend(struct.pack('<I', self.file.FileLen))
+            
+            self.state = 1
+            return bytes(w3)
+        else:
+            return bytes(self.temp)
+
+
+def NewBitmapFile(filename, libdata):
+    return bitmapFile(filename, libdata)

+ 34 - 0
bx/BxCmdFileDelete.py

@@ -0,0 +1,34 @@
+import struct
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_FILE_DELETE
+
+
+class CmdDeleteFile(baseBxCmd):
+    def __init__(self, files):
+        super().__init__(CMD_FILE_DELETE.group, CMD_FILE_DELETE.code)
+        self.files = files if files else []
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留值
+        result.extend([0x00, 0x00])
+        
+        l = len(self.files)
+        if l != 0:
+            result.extend(struct.pack('<H', l))
+            for v in self.files:
+                result.extend(v.encode('ascii'))
+            return bytes(result)
+        
+        result.append(0x00)
+        return bytes(result)
+
+
+def NewCmdDeleteFile(files):
+    return CmdDeleteFile(files)

+ 19 - 0
bx/BxCmdFileRead.py

@@ -0,0 +1,19 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_GET_FILE_INTO
+
+
+class CmdReadFileInfo(baseBxCmd):
+    def __init__(self):
+        super().__init__(CMD_GET_FILE_INTO.group, CMD_GET_FILE_INTO.code)
+
+    def Build(self):
+        result = bytearray()
+        result.append(self.Group())
+        result.append(self.Cmd())
+        result.append(self.ReqResp())
+        result.extend([0x00, 0x00])
+        return None
+
+
+def NewCmdReadFileInfo(FileName):
+    return CmdReadFileInfo()

+ 149 - 0
bx/BxCmdFileWrite.py

@@ -0,0 +1,149 @@
+import struct
+import time
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_WRITE_FILE, CMD_START_WRITE_FILE
+from .BxUtils import CRC16, Uint2BCD
+
+
+class bxFile:
+    """普通文本文件"""
+    def __init__(self, fileName, diedLine, areas):
+        self.type_ = 0x00  # 默认0x00
+        self.name = fileName  # 4字节ASCII
+        self.len = 0
+        self.content = ""
+        self.Priority = 0xff
+        self.DisplayType = 0  # 播放方式节目播放方式,0——顺序播放,其他——定长播放的 时间,单位为秒
+        self.PlayTimes = 1
+        self.ProgramLife = diedLine  # 节目生命周期
+        self.ProgramWeek = 1  # 节目的星期属性
+        self.ProgramTime = 0  # 定时节目位 0 非定时
+        self.PlayPeriodGrpNum = 0  # 节目播放时段组数
+        self.Areas = areas if areas else []  # 区域列表
+
+    def NewCmdWriteFile(self):
+        """创建文件写入命令"""
+        return CmdWriteFile(self)
+
+    def encodeProgramLife(self):
+        """编码节目生命周期"""
+        if self.ProgramLife == "":
+            return bytes([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
+        
+        now = time.localtime()
+        y = Uint2BCD(now.tm_year, False)
+        m = Uint2BCD(now.tm_mon, False)
+        d = Uint2BCD(now.tm_mday, False)
+        
+        result = bytearray()
+        result.append(y)
+        result.append(m)
+        result.append(d)
+        
+        try:
+            # 解析过期时间
+            end = time.strptime(self.ProgramLife, "%Y-%m-%d")
+            endY = Uint2BCD(end.tm_year, False)
+            endM = Uint2BCD(end.tm_mon, False)
+            endD = Uint2BCD(end.tm_mday, False)
+            result.append(endY)
+            result.append(endM)
+            result.append(endD)
+        except Exception as e:
+            print(f"时间解析错误: {e}")
+            return bytes([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
+        
+        return bytes(result)
+
+
+class CmdWriteFile(baseBxCmd):
+    """文件写入命令"""
+    def __init__(self, file):
+        super().__init__(CMD_WRITE_FILE.group, CMD_WRITE_FILE.code)
+        self.state = 0
+        self.file = file
+        self.LastBlockFlag = 0x01  # 是否是最后一包,0x00——不是最后一包 0x01——最后一包
+        self.BlockNum = 0x00  # 包号,如果是单包发送,则默认为 0x00
+        self.BlockLen = 0  # 包长,若是单包发送,此处为文件长度
+        self.BlockAddr = 0  # 本包数据在文件中的起始位置,如果是单包发送,此处默认为 0
+        self.temp = []
+
+    def Build(self):
+        """构建命令数据"""
+        if self.state == 0:
+            # 先计算区域数据及长度
+            w0 = bytearray()
+            for v in self.file.Areas:
+                b = v.Build()
+                w0.extend(struct.pack('<I', len(b) + 4))
+                w0.extend(b)
+            
+            l = len(w0) + 27
+            self.BlockLen = l
+            self.file.len = l
+            
+            # Write File
+            w1 = bytearray()
+            # 文件描述数据
+            w1.append(self.Group())
+            w1.append(self.Cmd())
+            w1.append(0x01)
+            w1.extend([0x00, 0x00])
+            # 写入文件名(4字节ASCII)
+            name_bytes = self.file.name.encode('ascii')[:4]
+            name_bytes = name_bytes.ljust(4, b'\x00')
+            w1.extend(name_bytes)
+            w1.append(self.LastBlockFlag)  # 是否是最后一包
+            w1.extend(struct.pack('<H', self.BlockNum))  # 包号
+            w1.extend(struct.pack('<H', self.BlockLen))  # 包长
+            w1.extend(struct.pack('<I', self.BlockAddr))  # 偏移量
+            
+            # 文件内容数据
+            w2 = bytearray()
+            w2.append(self.file.type_)
+            # 写入文件名(4字节ASCII)
+            w2.extend(name_bytes)
+            w2.extend(struct.pack('<I', self.file.len))
+            w2.append(self.file.Priority)
+            w2.extend(struct.pack('<H', self.file.DisplayType))
+            w2.append(self.file.PlayTimes)
+            w2.extend(self.file.encodeProgramLife())
+            w2.append(self.file.ProgramWeek)
+            w2.append(self.file.ProgramTime)
+            w2.append(self.file.PlayPeriodGrpNum)
+            w2.append(len(self.file.Areas))
+            w2.extend(w0)
+            
+            b2 = bytes(w2)
+            w1.extend(b2)
+            crc16 = CRC16(b2, 0, len(b2))
+            w1.extend(struct.pack('<H', crc16))
+            
+            self.temp = bytes(w1)
+            
+            # Start Write File "开始写文件",写文件前先检查内存是否够用
+            w3 = bytearray()
+            w3.append(CMD_START_WRITE_FILE.group)
+            w3.append(CMD_START_WRITE_FILE.code)
+            w3.append(0x01)
+            w3.extend([0x00, 0x00])
+            w3.append(0x01)  # 同名是否覆盖,0不覆盖,1覆盖
+            w3.extend(name_bytes)  # 写入文件名(4字节ASCII)
+            w3.extend(struct.pack('<I', self.file.len))
+            
+            self.state = 1
+            return bytes(w3)
+        else:
+            return self.temp
+
+
+class cmdFileBeginWrite(baseBxCmd):
+    """开始写文件命令"""
+    def __init__(self):
+        super().__init__(CMD_START_WRITE_FILE.group, CMD_START_WRITE_FILE.code)
+
+
+# 创建bxFile实例的函数
+def NewBxFile(fileName, diedLine, areas):
+    """创建bxFile实例"""
+    return bxFile(fileName, diedLine, areas)

+ 35 - 0
bx/BxCmdLock.py

@@ -0,0 +1,35 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_LOCK_UNLOCK
+
+
+class CmdLock(baseBxCmd):
+    def __init__(self, flag, name):
+        super().__init__(CMD_LOCK_UNLOCK.group, CMD_LOCK_UNLOCK.code)
+        self.StoreMode = 0
+        self.LockFlag = flag
+        self.ProgramFileName = name
+
+    def SetStoreMode(self, storeMode):
+        self.StoreMode = storeMode
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(0x01)
+        # 写入保留值
+        result.extend([0x00, 0x00])
+        # 写入存储模式
+        result.append(self.StoreMode)
+        # 写入锁定标志
+        result.append(self.LockFlag)
+        # 写入程序文件名
+        result.extend(self.ProgramFileName.encode('ascii'))
+        return bytes(result)
+
+
+def NewCmdLock(flag, name):
+    return CmdLock(flag, name)

+ 19 - 0
bx/BxCmdReadParams.py

@@ -0,0 +1,19 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_READ_PARAMS
+
+
+class CmdReadParams(baseBxCmd):
+    """读取参数命令"""
+    def __init__(self):
+        super().__init__(CMD_READ_PARAMS.group, CMD_READ_PARAMS.code)
+
+    def Build(self):
+        """构建命令数据"""
+        # 直接返回固定的命令数据
+        return bytes([0xa2, 0x0a, 0x01, 0x00, 0x00])
+
+
+# 创建CmdReadParams实例的函数
+def NewCmdReadParams():
+    """创建读取参数命令实例"""
+    return CmdReadParams()

+ 83 - 0
bx/BxCmdSendDynamicArea.py

@@ -0,0 +1,83 @@
+import struct
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_SEND_DYNAMIC_AREA
+
+
+class CmdSendDynamicArea(baseBxCmd):
+    def __init__(self, areas):
+        super().__init__(CMD_SEND_DYNAMIC_AREA.group, CMD_SEND_DYNAMIC_AREA.code)
+        self.processMode = 0
+        self.r2 = 0
+        self.delAreaIds = []
+        self.areas = areas if areas else []
+
+    def ProcessMode(self):
+        """获取处理模式"""
+        return self.processMode
+
+    def SetProcessMode(self, processMode):
+        """设置处理模式"""
+        self.processMode = processMode
+
+    def R2(self):
+        """获取保留字r2"""
+        return self.r2
+
+    def SetR2(self, r2):
+        """设置保留字r2"""
+        self.r2 = r2
+
+    def DelAreaIds(self):
+        """获取删除区域ID列表"""
+        return self.delAreaIds
+
+    def SetDelAreaIds(self, delAreaIds):
+        """设置删除区域ID列表"""
+        self.delAreaIds = delAreaIds
+
+    def Areas(self):
+        """获取区域列表"""
+        return self.areas
+
+    def SetAreas(self, areas):
+        """设置区域列表"""
+        self.areas = areas
+
+    def Build(self):
+        """构建命令数据"""
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入处理模式
+        result.append(self.processMode)
+        # 写入r2
+        result.append(self.r2)
+        
+        # 写入删除区域ID
+        if self.delAreaIds is None:
+            result.append(0x00)
+        else:
+            result.append(len(self.delAreaIds))
+            result.extend(self.delAreaIds)
+        
+        # 写入区域数量
+        if self.areas is None or len(self.areas) == 0:
+            result.append(0x00)
+        else:
+            result.append(len(self.areas))
+            # 写入每个区域
+            for v in self.areas:
+                b = v.Build()
+                result.extend(struct.pack('<h', len(b)))
+                result.extend(b)
+        
+        return bytes(result)
+
+
+# 创建CmdSendDynamicArea实例的函数
+def NewBxCmdSendDynamicArea(areas):
+    return CmdSendDynamicArea(areas)

+ 28 - 0
bx/BxCmdState.py

@@ -0,0 +1,28 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_SYSTEM_STATE
+
+
+class CmdState(baseBxCmd):
+    """系统状态命令"""
+    def __init__(self):
+        super().__init__(CMD_SYSTEM_STATE.group, CMD_SYSTEM_STATE.code)
+
+    def Build(self):
+        """构建命令数据"""
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留字r0 r1
+        result.append(0x00)
+        result.append(0x00)
+        return bytes(result)
+
+
+# 创建CmdState实例的函数
+def NewCmdState():
+    """创建系统状态命令实例"""
+    return CmdState()

+ 139 - 0
bx/BxCmdSystemClockCorrect.py

@@ -0,0 +1,139 @@
+import struct
+import time
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_SYSTEM_CLOCK_CORRECT
+from .BxUtils import BIN2Uint64
+
+
+class CmdSystemClockCorrect(baseBxCmd):
+    def __init__(self, t):
+        super().__init__(CMD_SYSTEM_CLOCK_CORRECT.group, CMD_SYSTEM_CLOCK_CORRECT.code)
+        self.sysTime = t
+        
+        # 转换为BCD码
+        year = BIN2Uint64(Uint2BCD(t.tm_year, False), '<')
+        month = BIN2Uint64(Uint2BCD(t.tm_mon, False), '<')
+        day = BIN2Uint64(Uint2BCD(t.tm_mday, False), '<')
+        hour = BIN2Uint64(Uint2BCD(t.tm_hour, False), '<')
+        minute = BIN2Uint64(Uint2BCD(t.tm_min, False), '<')
+        second = BIN2Uint64(Uint2BCD(t.tm_sec, False), '<')
+        week = BIN2Uint64(Uint2BCD(t.tm_wday + 1, False), '<')  # tm_wday是0-6,对应周一到周日,这里转换为1-7
+        
+        if week == 0:
+            week = 7
+        
+        self.year = int(year)
+        self.month = int(month)
+        self.day = int(day)
+        self.hour = int(hour)
+        self.minute = int(minute)
+        self.second = int(second)
+        self.week = int(week)
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留值
+        result.append(0x00)
+        result.append(0x00)
+        
+        # 写入年份(BCD码)
+        y = bytearray([
+            self.year & 0xff,
+            (self.year >> 8) & 0xff,
+            (self.year >> 16) & 0xff,
+            (self.year >> 24) & 0xff,
+        ])
+        if y[0] == 0x00 and y[1] == 0x00:
+            result.append(y[2])
+            result.append(y[3])
+        else:
+            result.append(y[0])
+            result.append(y[1])
+        
+        # 写入月份(BCD码)
+        m = bytearray([
+            self.month & 0xff,
+            (self.month >> 8) & 0xff,
+            (self.month >> 16) & 0xff,
+            (self.month >> 24) & 0xff,
+        ])
+        result.append(m[0])
+        
+        # 写入日期(BCD码)
+        d = bytearray([
+            self.day & 0xff,
+            (self.day >> 8) & 0xff,
+            (self.day >> 16) & 0xff,
+            (self.day >> 24) & 0xff,
+        ])
+        result.append(d[0])
+        
+        # 写入小时(BCD码)
+        h = bytearray([
+            self.hour & 0xff,
+            (self.hour >> 8) & 0xff,
+            (self.hour >> 16) & 0xff,
+            (self.hour >> 24) & 0xff,
+        ])
+        result.append(h[0])
+        
+        # 写入分钟(BCD码)
+        min_ = bytearray([
+            self.minute & 0xff,
+            (self.minute >> 8) & 0xff,
+            (self.minute >> 16) & 0xff,
+            (self.minute >> 24) & 0xff,
+        ])
+        result.append(min_[0])
+        
+        # 写入秒(BCD码)
+        s = bytearray([
+            self.second & 0xff,
+            (self.second >> 8) & 0xff,
+            (self.second >> 16) & 0xff,
+            (self.second >> 24) & 0xff,
+        ])
+        result.append(s[0])
+        
+        # 写入星期(BCD码)
+        week_ = bytearray([
+            self.week & 0xff,
+            (self.week >> 8) & 0xff,
+            (self.week >> 16) & 0xff,
+            (self.week >> 24) & 0xff,
+        ])
+        result.append(week_[0])
+        
+        return bytes(result)
+
+
+# uint转BCD
+def Uint2BCD(n, isBigEndian):
+    b = bytearray()
+    while True:
+        h = (n // 10) % 10
+        l = n % 10
+        b.append((h << 4) | l)
+        n = n // 100
+        if n == 0:
+            break
+    
+    if not isBigEndian:
+        return b
+    
+    # 反转字节顺序
+    l = len(b)
+    r = bytearray(l)
+    for i, v in enumerate(b):
+        r[l-1-i] = v
+    return r
+
+
+def NewBxCmdSystemClockCorrect(t):
+    return CmdSystemClockCorrect(t)

+ 43 - 0
bx/BxCmdTimingSwitch.py

@@ -0,0 +1,43 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_TIMING_SWITCH
+from .BxCmdSystemClockCorrect import Uint2BCD
+
+
+class CmdTimingSwitch(baseBxCmd):
+    def __init__(self, onOffSet):
+        super().__init__(CMD_TIMING_SWITCH.group, CMD_TIMING_SWITCH.code)
+        self.onOffSet = onOffSet if onOffSet else []
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留值
+        result.append(0x00)
+        result.append(0x00)
+        
+        # 检查定时开关机设置数量
+        if len(self.onOffSet) == 0 or len(self.onOffSet) > 256:
+            return None
+        
+        # 写入定时开关机设置数量
+        result.append(len(self.onOffSet))
+        
+        # 写入每个定时开关机设置
+        for i, v in enumerate(self.onOffSet):
+            if i > 2:
+                break
+            # 写入开机时间(BCD码)
+            result.extend(Uint2BCD(v[0], True))
+            # 写入关机时间(BCD码)
+            result.extend(Uint2BCD(v[1], True))
+        
+        return bytes(result)
+
+
+def NewCmdTimingSwitch(onOffSet):
+    return CmdTimingSwitch(onOffSet)

+ 30 - 0
bx/BxCmdTurnOnOff.py

@@ -0,0 +1,30 @@
+from .BxCmd import baseBxCmd
+from .BxCmdCode import CMD_TURN_ON_OFF
+
+
+class BxCmdTurnOnOff(baseBxCmd):
+    def __init__(self, on):
+        super().__init__(CMD_TURN_ON_OFF.group, CMD_TURN_ON_OFF.code)
+        self.on = on
+
+    def Build(self):
+        result = bytearray()
+        # 写入命令组
+        result.append(self.Group())
+        # 写入命令
+        result.append(self.Cmd())
+        # 写入响应标志
+        result.append(self.ReqResp())
+        # 写入保留值
+        result.append(0x00)
+        result.append(0x00)
+        # 写入开关状态
+        if self.on:
+            result.append(0x01)
+        else:
+            result.append(0x02)
+        return bytes(result)
+
+
+def NewBxCmdTurnOnOff(on):
+    return BxCmdTurnOnOff(on)

+ 325 - 0
bx/BxDataPack.py

@@ -0,0 +1,325 @@
+import struct
+from .BxUtils import CRC16
+
+
+class BxDataPack:
+    def __init__(self, data=None, dataLen=0):
+        self.WRAP_A5_NUM = 8
+        self.WRAP_5A_NUM = 1
+        self.dstAddr = 0x0001  # 设备ID(旧通过值)
+        self.srcAddr = 0x8000  # 旧通过值
+        self.r0 = 0
+        self.r1 = 0
+        self.r2 = 0
+        self.option = 0
+        self.crcMode = 0x01  # ✅ 修复:旧通过值0x01
+        self.displayType = 0xFE  # ✅ 修复:旧通过值0xFE
+        self.deviceType = 0x02  # ✅ 修复:旧通过值0x02
+        self.version = 0x45  # ✅ 修复:旧通过值0x45
+        self.data = data if data else bytearray()
+        self.dataLen = dataLen if dataLen > 0 else len(self.data)
+        self.crc = 0
+
+    def SetDisplayType(self, typ):
+        """注:特殊动态区不支持动态模式
+        0x00:普通模式,动态区与节目可同时显示,但各区域不可重叠。
+        0x01:动态模式,优先显示动态区,无动态区则显示节目,动态区与节目区可重叠。"""
+        self.displayType = typ
+
+    # ---------------- 第二步:1:1精准复刻Go的wrap函数(每一行都对齐Go) ----------------
+    def wrap(self, src: bytearray | bytes) -> bytearray:
+        """
+        1:1精准复刻Go的wrap函数
+        :param src: 未转义的源数据
+        :return: 转义后的完整数据包
+        """
+        # ---------------- 1. 先计算总长度(完全对齐Go) ----------------
+        src_len = len(src)
+        total_len = src_len
+
+        # 统计特殊字符,每个特殊字符会使长度+1
+        for v in src:
+            if v == 0xa5 or v == 0x5a or v == 0xa6 or v == 0x5b:
+                total_len += 1
+
+        # 加上开头和结尾的标记长度
+        total_len += self.WRAP_A5_NUM
+        total_len += self.WRAP_5A_NUM
+
+        # ---------------- 2. 创建结果数组 ----------------
+        result = bytearray(total_len)
+        offset = 0
+
+        # ---------------- 3. 添加开头的A5标记(不转义) ----------------
+        for i in range(self.WRAP_A5_NUM):
+            result[offset] = 0xa5
+            offset += 1
+
+        # ---------------- 4. 转义中间数据(核心逻辑) ----------------
+        for v in src:
+            if v == 0xa5:
+                # 0xa5 -> 0xa6 0x02
+                result[offset] = 0xa6
+                offset += 1
+                result[offset] = 0x02
+                offset += 1
+            elif v == 0xa6:
+                # 0xa6 -> 0xa6 0x01
+                result[offset] = 0xa6
+                offset += 1
+                result[offset] = 0x01
+                offset += 1
+            elif v == 0x5a:
+                # 0x5a -> 0x5b 0x02
+                result[offset] = 0x5b
+                offset += 1
+                result[offset] = 0x02
+                offset += 1
+            elif v == 0x5b:
+                # 0x5b -> 0x5b 0x01
+                result[offset] = 0x5b
+                offset += 1
+                result[offset] = 0x01
+                offset += 1
+            else:
+                # 普通字符,原样写入
+                result[offset] = v
+                offset += 1
+
+        # ---------------- 5. 添加结尾的5A标记(不转义) ----------------
+        for i in range(self.WRAP_5A_NUM):
+            result[offset] = 0x5a
+            offset += 1
+
+        # ---------------- 6. 验证(确保offset等于总长度) ----------------
+        if offset != total_len:
+            raise ValueError(f"长度计算错误:offset={offset}, total_len={total_len}")
+
+        return result
+
+    # ---------------- 第一步:先补全辅助工具函数(避免bytearray/编码/长度错误) ----------------
+    def _to_int(self, val) -> int:
+        """将bytearray/bytes/str转为0-255整数,避免append报错"""
+        if isinstance(val, (bytearray, bytes)):
+            return val[0] if len(val) > 0 else 0
+        elif isinstance(val, str):
+            return int(val) if val.isdigit() else 0
+        elif isinstance(val, int):
+            return val & 0xFF  # 限制0-255
+        return 0
+
+    def _to_bytes(self, val) -> bytes:
+        """将字符串/bytearray转为GB2312编码的bytes,避免中文乱码"""
+        if isinstance(val, str):
+            return val.encode('gb2312', errors='replace')
+        elif isinstance(val, bytearray):
+            return bytes(val)
+        elif isinstance(val, bytes):
+            return val
+        return b""
+
+    # ---------------- 第二步:修复版Pack方法(核心解决CRC问题) ----------------
+    def Pack(self):
+        # 构建数据包
+        result = bytearray()
+
+        # ---------------- 1. 基础字段写入(强制类型校验+对齐Go的包头!!!) ----------------
+        # 写入目标地址(小端序,对齐Go的0x0001!!!)
+        self.dstAddr = self.dstAddr  # 强制设为Go的固定值
+        result.extend(struct.pack('<H', self._to_int(self.dstAddr)))
+        # 写入源地址(小端序,对齐Go的0x8000!!!)
+        self.srcAddr = 0x8000  # 强制设为Go的固定值
+        result.extend(struct.pack('<H', self._to_int(self.srcAddr)))
+        # 写入保留字
+        result.append(self._to_int(self.r0))
+        result.append(self._to_int(self.r1))
+        result.append(self._to_int(self.r2))
+        # 写入选项
+        result.append(self._to_int(self.option))
+        # 写入CRC模式
+        result.append(self._to_int(self.crcMode))
+        # 写入显示类型
+        result.append(self._to_int(self.displayType))
+        # 写入设备类型
+        result.append(self._to_int(self.deviceType))
+        # 写入版本
+        result.append(self._to_int(self.version))
+
+        # ---------------- 2. 数据长度(动态计算,保留) ----------------
+        self.data = self._to_bytes(self.data)
+        self.dataLen = len(self.data)
+        result.extend(struct.pack('<H', self._to_int(self.dataLen)))
+
+        # ---------------- 3. 写入数据(保留) ----------------
+        result.extend(self.data)
+
+        # ---------------- 4. 写入CRC占位符(初始为0,2字节小端序,保留) ----------------
+        result.extend(struct.pack('<H', 0))
+
+        # ---------------- 5. 计算CRC(100%对齐Go的调用参数!!!) ----------------
+        crc_offset = 0
+        crc_end_index = len(result) - 2  # 这里的length是结束索引!!!不是长度!!!
+        # 调试打印1:明确标注是结束索引
+        # print(f"[CRC调试] 待校验范围:offset={crc_offset}, 结束索引={crc_end_index}, 总result长度={len(result)}")
+        # # 调试打印2:待校验数据(保留)
+        # crc_data = result[crc_offset:crc_end_index]  # 100%对齐Go的data[offset:length]!!!
+        # print(f"[CRC调试] 待校验数据(十六进制):{crc_data.hex()}")
+
+        # 调用1:1复刻的Go版CRC16函数(必须确保CRC16的切片是data[offset:length]!!!)
+        crc = CRC16(result, crc_offset, crc_end_index)
+        # 调试打印3:计算的CRC值(保留)
+        # print(f"[CRC调试] 计算的CRC(十六进制整数):{hex(crc)}")
+
+        # ---------------- 6. 更新CRC值(用struct.pack更直观,保留) ----------------
+        crc_bytes = struct.pack('<H', crc)
+        result[-2] = crc_bytes[0]
+        result[-1] = crc_bytes[1]
+        # 调试打印4:写入的CRC字节(保留)
+        # print(f"[CRC调试] 写入的CRC字节(小端序):{crc_bytes.hex()}")
+
+        # ---------------- 7. 转义数据(1:1复刻的Go版wrap,保留) ----------------
+        return self.wrap(result)
+
+
+def dpParse(src, length):
+    # 解包数据
+    dst = unwrap(src, length)
+    if dst is None:
+        return None
+    
+    # 计算CRC
+    crcCalculated = CRC16(dst, 0, len(dst) - 2)
+    # 获取CRC
+    crcGot = (dst[len(dst) - 1] << 8) | dst[len(dst) - 2]
+    
+    if crcCalculated != crcGot:
+        return None
+    
+    # 解析数据
+    dp = BxDataPack()
+    offset = 0
+    
+    # 解析目标地址
+    dp.dstAddr = struct.unpack_from('<H', dst, offset)[0]
+    offset += 2
+    # 解析源地址
+    dp.srcAddr = struct.unpack_from('<H', dst, offset)[0]
+    offset += 2
+    # 解析保留字
+    dp.r0 = dst[offset]
+    offset += 1
+    dp.r1 = dst[offset]
+    offset += 1
+    dp.r2 = dst[offset]
+    offset += 1
+    # 解析选项
+    dp.option = dst[offset]
+    offset += 1
+    # 解析CRC模式
+    dp.crcMode = dst[offset]
+    offset += 1
+    # 解析显示类型
+    dp.displayType = dst[offset]
+    offset += 1
+    # 解析设备类型
+    dp.deviceType = dst[offset]
+    offset += 1
+    # 解析版本
+    dp.version = dst[offset]
+    offset += 1
+    # 解析数据长度
+    dp.dataLen = struct.unpack_from('<H', dst, offset)[0]
+    offset += 2
+    # 解析数据
+    dp.data = dst[offset:offset + dp.dataLen]
+    offset += dp.dataLen
+    # 解析CRC
+    dp.crc = struct.unpack_from('<H', dst, offset)[0]
+    
+    return dp
+
+
+def unwrap(src, length):
+    # 计算解包后的长度
+    len_ = length
+    for v in src:
+        if v == 0xa5 or v == 0x5a or v == 0xa6 or v == 0x5b:
+            len_ -= 1
+    
+    # 如果计算的帧长度为0, 说明数据不正确
+    if len_ == 0:
+        return None
+    
+    # 创建结果数组
+    result = bytearray(len_)
+    offset = 0
+    i = 0
+    
+    while i < length:
+        if src[i] == 0xa5 or src[i] == 0x5a:
+            i += 1
+        elif src[i] == 0xa6:
+            if i + 1 < length:
+                if src[i + 1] == 0x01:
+                    result[offset] = 0xa6
+                    offset += 1
+                    i += 2
+                elif src[i + 1] == 0x02:
+                    result[offset] = 0xa5
+                    offset += 1
+                    i += 2
+                else:
+                    return None
+            else:
+                return None
+        elif src[i] == 0x5b:
+            if i + 1 < length:
+                if src[i + 1] == 0x01:
+                    result[offset] = 0x5b
+                    offset += 1
+                    i += 2
+                elif src[i + 1] == 0x02:
+                    result[offset] = 0x5a
+                    offset += 1
+                    i += 2
+                else:
+                    return None
+            else:
+                return None
+        else:
+            result[offset] = src[i]
+            offset += 1
+            i += 1
+    
+    return result
+
+
+# 创建BxDataPack实例的函数(从数据创建)
+def NewBxDataPackData(data):
+    dp = BxDataPack()
+    dp.data = data
+    dp.dataLen = len(data)
+    dp.WRAP_A5_NUM = 8
+    dp.WRAP_5A_NUM = 1
+    dp.dstAddr = 0x0001  # ✅ 旧通过值
+    dp.srcAddr = 0x8000  # ✅ 旧通过值
+    dp.crcMode = 0x01  # ✅ 旧通过值
+    dp.displayType = 0xFE  # ✅ 旧通过值
+    dp.deviceType = 0x02  # ✅ 旧通过值
+    dp.version = 0x45  # ✅ 旧通过值
+    return dp
+
+
+# 创建BxDataPack实例的函数(从命令创建)
+def NewBxDataPackCmd(cmd,dst_addr):
+    b = cmd.Build()
+    dp = BxDataPack()
+    dp.data = b
+    dp.dataLen = len(b)
+    dp.WRAP_A5_NUM = 8
+    dp.WRAP_5A_NUM = 1
+    dp.dstAddr = dst_addr  # 设备ID
+    dp.srcAddr = 0x8000
+    dp.deviceType = 0xfe
+    dp.version = 0x02
+    return dp

+ 54 - 0
bx/BxResp.py

@@ -0,0 +1,54 @@
+from .BxDataPack import dpParse
+from .BxCmdCode import CMD_ACK, CMD_SYSTEM_STATE
+from .bxError import bxErrors
+
+
+class BxResp:
+    def __init__(self):
+        self.group = 0
+        self.cmd = 0
+        self.Err = 0
+        self.r0 = 0
+        self.r1 = 0
+        self.Data = bytearray()
+
+    def Parse(self, src, length):
+        """解析响应数据"""
+        dp = dpParse(src, length)
+        if dp is None:
+            return None
+        else:
+            return self._parse(dp)
+
+    def _parse(self, pack):
+        """解析数据打包"""
+        offset = 0
+        resp = BxResp()
+        resp.group = pack.data[offset]
+        offset += 1
+        resp.cmd = pack.data[offset]
+        offset += 1
+        resp.Err = pack.data[offset]
+        offset += 1
+        resp.r0 = pack.data[offset]
+        offset += 1
+        resp.r1 = pack.data[offset]
+        offset += 1
+        resp.Data = pack.data[offset:(offset + len(pack.data) - 5)]
+        return resp
+
+    def IsAck(self):
+        """是否是确认响应"""
+        return self.group == CMD_ACK.group and self.cmd == CMD_ACK.code
+
+    def NoError(self):
+        """是否无错误"""
+        return self.Err == 0
+
+    def Error(self):
+        """获取错误信息"""
+        return bxErrors.get(self.Err, "未知错误")
+
+    def IsInfo(self):
+        """是否是返回"控制器状态"信息"""
+        return self.group == CMD_SYSTEM_STATE.group and self.cmd == CMD_SYSTEM_STATE.code

+ 113 - 0
bx/BxUtils.py

@@ -0,0 +1,113 @@
+import struct
+
+# CRC16表
+crc16_table = [
+    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
+    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
+    0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
+    0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
+    0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
+    0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
+    0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
+    0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
+    0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
+    0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
+    0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
+    0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
+    0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
+    0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
+    0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
+    0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
+    0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
+    0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
+    0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
+    0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
+    0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
+    0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
+    0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
+    0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
+    0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
+    0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
+    0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
+    0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
+    0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
+    0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
+    0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
+    0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
+]
+
+
+def bytes_to_uint16(src, start, little_endian=True):
+    """将字节数组转换为uint16"""
+    if little_endian:
+        return struct.unpack_from('<H', src, start)[0]
+    else:
+        return struct.unpack_from('>H', src, start)[0]
+
+
+def CRC16(data, offset, length):
+    """计算CRC16校验(修复切片范围为offset:offset+length)"""
+    crc16 = 0
+    # 核心修复:切片必须是左闭右开的offset:offset+length
+    for v in data[offset:length]:
+        n = (v ^ crc16) & 0xFF
+        crc16 >>= 8
+        crc16 ^= crc16_table[n]
+        crc16 &= 0xFFFF
+    return crc16
+
+
+def uint16_to_bin(i, little_endian=True):
+    """将uint16转换为字节数组"""
+    if little_endian:
+        return struct.pack('<H', i)
+    else:
+        return struct.pack('>H', i)
+
+
+class LengthError(Exception):
+    """长度错误异常"""
+    pass
+
+
+def uint2bin(n, length, little_endian=True):
+    """将无符号整数转换为指定长度的字节数组"""
+    if length == 1:
+        if n > 255:
+            raise LengthError("需要更大的长度存储该数值")
+        return bytes([n])
+    elif length == 2:
+        if n > 65535:
+            raise LengthError("需要更大的长度存储该数值")
+        if little_endian:
+            return struct.pack('<H', n)
+        else:
+            return struct.pack('>H', n)
+    elif 3 <= length <= 4:
+        if n > 4294967295:
+            raise LengthError("需要更大的长度存储该数值")
+        if little_endian:
+            return struct.pack('<I', n)
+        else:
+            return struct.pack('>I', n)
+    elif 5 <= length <= 8:
+        if n > 18446744073709551615:
+            raise LengthError("需要更大的长度存储该数值")
+        if little_endian:
+            return struct.pack('<Q', n)
+        else:
+            return struct.pack('>Q', n)
+    else:
+        raise LengthError("非法字节长度")
+
+
+def Uint2BCD(value, isHighFirst=True):
+    """将无符号整数转换为BCD编码"""
+    bcd = 0
+    i = 0
+    while value > 0:
+        digit = value % 10
+        bcd |= (digit << (i * 4))
+        value = value // 10
+        i += 1
+    return bcd

+ 9 - 0
bx/__init__.py

@@ -0,0 +1,9 @@
+from .BxResp import BxResp
+from .BxDataPack import NewBxDataPackCmd, NewBxDataPackData, BxDataPack
+from .BxAreaDynamic import NewBxAreaProgram, NewBxAreaDynamic, BxAreaDynamic
+from .BxCmdFactory import BxCmdFactory
+from .BxCmdSendDynamicArea import NewBxCmdSendDynamicArea, CmdSendDynamicArea
+from .BxCmdFileWrite import NewBxFile, bxFile, CmdWriteFile
+from .BxUtils import CRC16, Uint2BCD
+from .bxError import BxError, GetError
+from .BxCmdCode import CMD_ACK, CMD_NACK, CMD_DEL_FILE, CMD_SYSTEM_STATE, CMD_SYSTEM_PING, CMD_SYSTEM_HEARTBEAT, CMD_START_WRITE_FILE, CMD_WRITE_FILE, CMD_WRITE_TRANS_START, CMD_WRITE_TRANS_STOP, CMD_WRITE_CUSTOMER_INFO, CMD_GET_FILE_INTO, CMD_GET_FILE_CONTENT, CMD_SYSTEM_CLOCK_CORRECT, CMD_READ_PARAMS, CMD_SOUND, CMD_TURN_ON_OFF, CMD_TIMING_SWITCH, CMD_SCREEN_BRIGHTNESS, CMD_LOCK_UNLOCK, CMD_SEND_DYNAMIC_AREA, CMD_DEL_DYNAMIC_AREA, CMD_CANCEL_TIMING_SWITCH, CMD_CLEAR_SCREEN

+ 32 - 0
bx/bxError.py

@@ -0,0 +1,32 @@
+class BxError:
+    def __init__(self, error_code, name, description):
+        self.ErrorCode = error_code
+        self.Name = name
+        self.Description = description
+
+
+# 错误码定义
+bxErrors = {
+    0: BxError(0, "ERR_NO", "No Err"),
+    1: BxError(1, "ERR_OUTOFGROUP", "Command Group Err"),
+    2: BxError(2, "ERR_NOCMD", "Not Found"),
+    3: BxError(3, "ERR_BUSY", "The Controller is busy now"),
+    4: BxError(4, "ERR_MEMORYVOLUME", "Out of the Memory Volume"),
+    5: BxError(5, "ERR_CHECKSUM", "CRC16 Checksum Err"),
+    6: BxError(6, "ERR_FILENOTEXIST", "File Not Exist"),
+    7: BxError(7, "ERR_FLASH", "Flash Access Err"),
+    8: BxError(8, "ERR_FILE_DOWNLOAD", "File Download Err"),
+    9: BxError(9, "ERR_FILE_NAME", "Filename Err"),
+    10: BxError(10, "ERR_FILE_TYPE", "File type Err"),
+    11: BxError(11, "ERR_FILE_CRC16", "File CRC16 Err"),
+    12: BxError(12, "ERR_FONT_NOT_EXIST", "Font Library Not Exist"),
+    13: BxError(13, "ERR_FIRMWARE_TYPE", "Firmware Type Err (Check the controller type)"),
+    14: BxError(14, "ERR_DATE_TIME_FORMAT", "Date Time format Err"),
+    15: BxError(15, "ERR_FILE_EXIST", "File Exist for File overwrite"),
+    16: BxError(16, "ERR_FILE_BLOCK_NUM", "File block number Err"),
+}
+
+
+def GetError(code):
+    """根据错误码获取错误信息"""
+    return bxErrors.get(code, BxError(code, "未知错误", "Unknown error code"))

+ 57 - 0
bxx/BxArea.go

@@ -0,0 +1,57 @@
+package bx
+
+type BxArea interface {
+	Build() []byte
+	Length() int16
+}
+
+type BaseArea struct {
+	BxArea
+	typ byte
+	x   uint16
+	y   uint16
+	w   uint16
+	h   uint16
+}
+
+func NewBxArea(typ byte, x uint16, y uint16, w uint16, h uint16) BaseArea {
+	return BaseArea{
+		typ: typ,
+		x:   x,
+		y:   y,
+		w:   w,
+		h:   h,
+	}
+}
+
+func (a *BaseArea) GetX() uint16 {
+	return a.x
+}
+
+func (a *BaseArea) SetX(x uint16) {
+	a.x = x
+}
+
+func (a *BaseArea) GetY() uint16 {
+	return a.y
+}
+
+func (a *BaseArea) SetY(y uint16) {
+	a.y = y
+}
+
+func (a *BaseArea) GetW() uint16 {
+	return a.w
+}
+
+func (a *BaseArea) SetW(w uint16) {
+	a.w = w
+}
+
+func (a *BaseArea) GetH() uint16 {
+	return a.h
+}
+
+func (a *BaseArea) SetH(h uint16) {
+	a.h = h
+}

+ 210 - 0
bxx/BxAreaDynamic.go

@@ -0,0 +1,210 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type BxAreaDynamic struct {
+	BaseArea
+	is5K bool
+	//
+	// id
+	// 动态区域编号
+	// 注意:该参数只对动态区有效,其他区域为默认
+	// 值,动态区必须统一编号,编号从 0 开始递增。
+	id byte
+	// 行间距
+	lineSpace byte
+	// 动态区运行模式
+	//0—动态区数据循环显示。
+	//1—动态区数据显示完成后静止显示最后一页数
+	//据。
+	//2—动态区数据循环显示,超过设定时间后数据仍
+	//未更新时不再显示
+	//3—动态区数据循环显示,超过设定时间后数据仍
+	//未更新时显示 Logo 信息,Logo 信息即为动态区域
+	//的最后一页信息
+	//4—动态区数据顺序显示,显示完最后一页后就不
+	//再显示
+	//5—动态区数据顺序显示,超过设定次数后数据仍
+	//未更新时不再显示
+	runMode byte
+	// 动 态 区 数 据 超 时 时 间 , 单 位 为 秒 / 次 数 ( 若
+	// RunMode=5,则表示更新次数)
+	timeout int16
+	// 是否使能语音播放
+	//0 表示不使能语音
+	//1 表示播放下文中 Data 部分内容
+	//2 表示播放下文中 SoundData 部分内容
+	soundMode   byte
+	soundPerson byte
+	soundRepeat byte
+	soundVolume byte //todo 默认值0x05
+	soundSpeed  byte
+	soundData   []byte
+	// extend para len
+	extendParaLen byte
+	// type setting
+	// 属于 extend para
+	//typeSetting byte
+	// text alignment
+	alignment byte
+	// single line
+	singleLine byte
+	// 是否自动换行
+	// 是否自动换行
+	// 0x01——不自动换行,显示数据在换行时必须插入
+	// 换行符
+	// 0x02——自动换行,显示内容不需要换行符,但是
+	// 只能使用统一的中文字体和英文字体
+	autoNewLine byte
+	// 显示方式
+	//0x01——静止显示
+	//0x02——快速打出
+	//0x03——向左移动
+	//0x04——向右移动
+	//0x05——向上移动
+	//0x06——向下移动
+	dispMode byte
+	exitMode byte
+	speed    byte
+	holdTime byte
+	data     []byte
+}
+
+func NewBxAreaDynamic(id, runMode, dispMode byte, x uint16, y uint16, w uint16, h uint16, data []byte,
+	soundData []byte, is5K bool) *BxAreaDynamic {
+	return &BxAreaDynamic{
+		BaseArea:    NewBxArea(0, x, y, w, h),
+		id:          id,
+		data:        data,
+		soundData:   soundData,
+		is5K:        is5K,
+		timeout:     5,
+		runMode:     runMode,
+		soundMode:   0x00,
+		soundPerson: 0x00,
+		soundVolume: 0x01,
+		soundRepeat: 0x00,
+		soundSpeed:  0x10,
+		singleLine:  0x02,
+		autoNewLine: 0x01,
+		dispMode:    dispMode,
+		speed:       0x0a,
+		holdTime:    0x08,
+	}
+}
+
+func NewBxAreaProgram(id, runMode, dispMode, alignment byte, x uint16, y uint16, w uint16, h uint16, data []byte,
+	is5K bool) *BxAreaDynamic {
+	return &BxAreaDynamic{
+		BaseArea:    NewBxArea(0, x, y, w, h),
+		id:          id,
+		data:        data,
+		is5K:        is5K,
+		timeout:     5,
+		runMode:     runMode,
+		alignment:   alignment,
+		soundVolume: 0x05,
+		soundSpeed:  0x05,
+		singleLine:  0x02,
+		autoNewLine: 0x01,
+		dispMode:    dispMode,
+		speed:       0x01,
+		holdTime:    0x08,
+	}
+}
+
+func (b *BxAreaDynamic) Length() int16 {
+	return 27 + int16(len(b.data))
+}
+
+func (b *BxAreaDynamic) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	binary.Write(w, binary.LittleEndian, b.typ)
+	x8 := b.GetX()
+	w8 := b.GetW()
+	if b.is5K {
+		x8 = b.x / 8
+		w8 = b.w / 8
+	}
+	binary.Write(w, binary.LittleEndian, x8)
+	binary.Write(w, binary.LittleEndian, b.GetY())
+	binary.Write(w, binary.LittleEndian, w8)
+	binary.Write(w, binary.LittleEndian, b.GetH())
+	// 动态区编号
+	binary.Write(w, binary.LittleEndian, b.id)
+	// 行间距
+	binary.Write(w, binary.LittleEndian, b.lineSpace)
+	// 运行模式
+	binary.Write(w, binary.LittleEndian, b.runMode)
+	binary.Write(w, binary.LittleEndian, b.timeout)
+	binary.Write(w, binary.LittleEndian, b.soundMode)
+	if b.soundMode == 0x01 || b.soundMode == 0x02 {
+		pr := ((b.soundRepeat << 4) & 0xf0) | (b.soundPerson & 0x0f)
+		binary.Write(w, binary.LittleEndian, pr)
+		binary.Write(w, binary.LittleEndian, b.soundVolume)
+		binary.Write(w, binary.LittleEndian, b.soundSpeed)
+	}
+	if b.soundMode == 0x02 {
+		soundDataLen := len(b.soundData)
+		binary.Write(w, binary.LittleEndian, int32(soundDataLen))
+		binary.Write(w, binary.LittleEndian, b.soundData)
+	}
+	// extendParaLen
+	binary.Write(w, binary.LittleEndian, b.extendParaLen)
+	binary.Write(w, binary.LittleEndian, b.alignment)
+	binary.Write(w, binary.LittleEndian, b.singleLine)
+	binary.Write(w, binary.LittleEndian, b.autoNewLine)
+	binary.Write(w, binary.LittleEndian, b.dispMode)
+	binary.Write(w, binary.LittleEndian, b.exitMode)
+	binary.Write(w, binary.LittleEndian, b.speed)
+	binary.Write(w, binary.LittleEndian, b.holdTime)
+	binary.Write(w, binary.LittleEndian, int32(len(b.data)))
+	binary.Write(w, binary.LittleEndian, b.data)
+
+	return w.Bytes()
+}
+
+func (b *BxAreaDynamic) SetSoundMode(soundMode byte) {
+	if soundMode > 2 {
+		b.soundMode = 0x02
+	} else {
+		b.soundMode = soundMode
+	}
+}
+
+func (b *BxAreaDynamic) SetSoundPerson(soundPerson byte) {
+	if soundPerson > 5 {
+		b.soundPerson = 0
+	} else {
+		b.soundPerson = soundPerson
+	}
+}
+
+func (b *BxAreaDynamic) SetSoundRepeat(soundRepeat byte) {
+	if soundRepeat > 15 {
+		b.soundRepeat = 15
+	} else {
+		b.soundRepeat = soundRepeat
+	}
+}
+
+func (b *BxAreaDynamic) SetSoundVolume(soundVolume byte) {
+	if soundVolume > 10 {
+		b.soundVolume = 10
+	} else {
+		b.soundVolume = soundVolume
+	}
+}
+
+func (b *BxAreaDynamic) SetSoundSpeed(soundSpeed byte) {
+	if soundSpeed < 1 {
+		b.soundSpeed = 1
+	} else if soundSpeed > 10 {
+		b.soundSpeed = 10
+	} else {
+		b.soundSpeed = soundSpeed
+	}
+}

+ 105 - 0
bxx/BxByteArray.go

@@ -0,0 +1,105 @@
+package bx
+
+import (
+	"encoding/binary"
+)
+
+const (
+	DefaultCapacity = 128
+)
+
+type BxByteArray struct {
+	list []byte
+	next int
+}
+
+func NewBxByteArray(capacity int) *BxByteArray {
+	return &BxByteArray{
+		list: make([]byte, capacity),
+		next: 0,
+	}
+}
+
+func NewDefaultBxByteArray() *BxByteArray {
+	return NewBxByteArray(DefaultCapacity)
+}
+
+func (b *BxByteArray) add(data byte) {
+	if b.next == len(b.list) {
+		b.list = append(b.list, make([]byte, len(b.list)*2)...)
+	}
+	b.list[b.next] = data
+	b.next++
+}
+
+func (b *BxByteArray) addInt16(data int16, endian int) {
+	if b.next+1 >= len(b.list) {
+		b.list = append(b.list, make([]byte, len(b.list)*2)...)
+	}
+
+	if endian == 0 { // LITTLE
+		binary.LittleEndian.PutUint16(b.list[b.next:], uint16(data))
+	} else { // BIG
+		binary.BigEndian.PutUint16(b.list[b.next:], uint16(data))
+	}
+
+	b.next += 2
+}
+
+func (b *BxByteArray) addInt(data int32, endian int) {
+	if b.next+3 >= len(b.list) {
+		b.list = append(b.list, make([]byte, len(b.list)*2)...)
+	}
+
+	if endian == 0 { // LITTLE
+		binary.LittleEndian.PutUint32(b.list[b.next:], uint32(data))
+	} else { // BIG
+		binary.BigEndian.PutUint32(b.list[b.next:], uint32(data))
+	}
+
+	b.next += 4
+}
+
+func (b *BxByteArray) addBytes(src []byte) {
+	if src != nil {
+		if b.next+len(src)-1 >= len(b.list) {
+			b.list = append(b.list, make([]byte, len(b.list)+len(src))...)
+		}
+
+		copy(b.list[b.next:], src)
+		b.next += len(src)
+	}
+}
+
+func (b *BxByteArray) addBytesOffsetLength(src []byte, offset int, length int) {
+	if src != nil {
+		if b.next+length-1 >= len(b.list) {
+			b.list = append(b.list, make([]byte, len(b.list)+length)...)
+		}
+
+		copy(b.list[b.next:], src[offset:offset+length])
+		b.next += length
+	}
+}
+
+func (b *BxByteArray) set(index int, data byte) {
+	if index < len(b.list) {
+		b.list[index] = data
+	}
+}
+
+func (b *BxByteArray) get(index int) byte {
+	return b.list[index]
+}
+
+func (b *BxByteArray) Build() []byte {
+	return b.list[:b.next]
+}
+
+func (b *BxByteArray) size() int {
+	return b.next
+}
+
+func (b *BxByteArray) clear() {
+	b.next = 0
+}

+ 61 - 0
bxx/BxCmd.go

@@ -0,0 +1,61 @@
+package bx
+
+type BxCmd interface {
+	Build() []byte
+}
+
+type baseBxCmd struct {
+	BxCmd
+	group   byte
+	cmd     byte
+	reqResp byte
+	r0      byte
+	r1      byte
+}
+
+func newBaseCmd(group, cmd byte) baseBxCmd {
+	return baseBxCmd{
+		group:   group,
+		cmd:     cmd,
+		reqResp: 0x01,
+	}
+}
+func (b *baseBxCmd) Group() byte {
+	return b.group
+}
+
+func (b *baseBxCmd) SetGroup(group byte) {
+	b.group = group
+}
+
+func (b *baseBxCmd) Cmd() byte {
+	return b.cmd
+}
+
+func (b *baseBxCmd) SetCmd(cmd byte) {
+	b.cmd = cmd
+}
+
+func (b *baseBxCmd) ReqResp() byte {
+	return b.reqResp
+}
+
+func (b *baseBxCmd) SetReqResp(reqResp byte) {
+	b.reqResp = reqResp
+}
+
+func (b *baseBxCmd) R0() byte {
+	return b.r0
+}
+
+func (b *baseBxCmd) SetR0(r0 byte) {
+	b.r0 = r0
+}
+
+func (b *baseBxCmd) R1() byte {
+	return b.r1
+}
+
+func (b *baseBxCmd) SetR1(r1 byte) {
+	b.r1 = r1
+}

+ 106 - 0
bxx/BxCmdBrightness.go

@@ -0,0 +1,106 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+// CmdBrightness 对应PDF中8.17设置亮度命令
+type CmdBrightness struct {
+	baseBxCmd
+	BrightnessType    byte   // 亮度调节方式:0x01强制调节,0x02定时调节
+	CurrentBrightness byte   // 强制调节时的亮度值(0-15,15为最高)
+	BrightnessValue   []byte // 定时调节时的48字节亮度列表(每30分钟一个时段)
+}
+
+// NewCmdBrightness 初始化亮度设置命令
+// brightnessType: 0x01强制调节,0x02定时调节
+// currentBrightness: 强制调节时的亮度值(0-15)
+// brightnessValue: 定时调节时的48字节亮度列表(不足补0,超出截取前48字节)
+func NewCmdBrightness(brightnessType byte, currentBrightness byte, brightnessValue []byte) CmdBrightness {
+	// 初始化基础命令(使用BxCmdCode中定义的亮度命令组和编号)
+	baseCmd := newBaseCmd(CMD_SCREEN_BRIGHTNESS.group, CMD_SCREEN_BRIGHTNESS.code)
+
+	// 处理定时调节的亮度列表长度(确保为48字节)
+	processedValue := processBrightnessValue(brightnessValue)
+
+	// 处理亮度值边界(0-15)
+	processedBrightness := currentBrightness
+	if processedBrightness > 15 {
+		processedBrightness = 15
+	}
+
+	return CmdBrightness{
+		baseBxCmd:         baseCmd,
+		BrightnessType:    brightnessType,
+		CurrentBrightness: processedBrightness,
+		BrightnessValue:   processedValue,
+	}
+}
+
+// SetBrightnessType 设置亮度调节方式(0x01强制,0x02定时)
+func (cmd *CmdBrightness) SetBrightnessType(brightnessType byte) {
+	cmd.BrightnessType = brightnessType
+	// 切换为定时调节时自动初始化48字节亮度列表
+	if brightnessType == 0x02 && len(cmd.BrightnessValue) != 48 {
+		cmd.BrightnessValue = make([]byte, 48)
+	}
+}
+
+// SetCurrentBrightness 设置强制调节亮度值(0-15)
+func (cmd *CmdBrightness) SetCurrentBrightness(currentBrightness byte) {
+	if currentBrightness > 15 {
+		currentBrightness = 15
+	}
+	cmd.CurrentBrightness = currentBrightness
+}
+
+// SetBrightnessValue 设置定时调节的48字节亮度列表
+func (cmd *CmdBrightness) SetBrightnessValue(brightnessValue []byte) {
+	cmd.BrightnessValue = processBrightnessValue(brightnessValue)
+}
+
+// Build 构建符合PDF协议格式的命令字节流
+func (cmd *CmdBrightness) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 64)) // 预分配足够容量
+
+	// 按PDF协议顺序写入字段:CmdGroup -> Cmd -> Response -> Reserved -> 亮度参数
+	binary.Write(w, binary.LittleEndian, cmd.Group())        // 命令分组(0xA3)
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())          // 命令编号(0x02)
+	binary.Write(w, binary.LittleEndian, byte(0x01))         // Response:要求控制器回复(默认0x01)
+	binary.Write(w, binary.LittleEndian, []byte{0x00, 0x00}) // Reserved:2字节保留值
+	binary.Write(w, binary.LittleEndian, cmd.BrightnessType) // 亮度调节方式
+
+	// 根据调节方式写入后续参数
+	switch cmd.BrightnessType {
+	case 0x01:
+		// 强制调节:写入亮度值(0-15)
+		binary.Write(w, binary.LittleEndian, cmd.CurrentBrightness)
+	case 0x02:
+		binary.Write(w, binary.LittleEndian, byte(0x00)) // currentBrightness默认0
+		// 定时调节:写入48字节亮度列表
+		binary.Write(w, binary.LittleEndian, cmd.BrightnessValue)
+	default:
+		// 无效类型默认按强制调节处理(亮度0)
+		binary.Write(w, binary.LittleEndian, byte(0x00))
+	}
+
+	return w.Bytes()
+}
+
+// processBrightnessValue 处理定时调节的亮度列表(确保长度为48字节)
+func processBrightnessValue(value []byte) []byte {
+	result := make([]byte, 48)
+	if len(value) > 48 {
+		copy(result, value[:48]) // 超出部分截取
+	} else {
+		copy(result, value) // 不足部分补0
+	}
+	// 确保每个亮度值在0-15范围内
+	for i := range result {
+		if result[i] > 15 {
+			result[i] = 15
+		}
+	}
+	return result
+}

+ 27 - 0
bxx/BxCmdCancelTimingSwitch.go

@@ -0,0 +1,27 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type CmdCancelTimingSwitch struct {
+	baseBxCmd
+}
+
+func NewCmdCancelTimingSwitch() CmdCancelTimingSwitch {
+	return CmdCancelTimingSwitch{
+		baseBxCmd: newBaseCmd(CMD_CANCEL_TIMING_SWITCH.group, CMD_CANCEL_TIMING_SWITCH.code),
+	}
+}
+
+func (cmd CmdCancelTimingSwitch) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 8))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, cmd.ReqResp())
+	//r0 r1
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	return nil
+}

+ 25 - 0
bxx/BxCmdClearScreen.go

@@ -0,0 +1,25 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type BxCmdClearScreen struct {
+	baseBxCmd
+}
+
+func NewBxCmdClearScreen(group, cmd byte) BxCmd {
+	return BxCmdClearScreen{
+		newBaseCmd(CMD_CLEAR_SCREEN.group, CMD_CLEAR_SCREEN.code)}
+}
+
+func (cs BxCmdClearScreen) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 1024))
+	binary.Write(w, binary.LittleEndian, cs.group)
+	binary.Write(w, binary.LittleEndian, cs.cmd)
+	binary.Write(w, binary.LittleEndian, cs.ReqResp())
+	binary.Write(w, binary.LittleEndian, cs.r0)
+	binary.Write(w, binary.LittleEndian, cs.r1)
+	return w.Bytes()
+}

+ 34 - 0
bxx/BxCmdCode.go

@@ -0,0 +1,34 @@
+package bx
+
+type CmdCode struct {
+	name  string
+	group byte
+	code  byte
+}
+
+var (
+	CMD_ACK                  = CmdCode{"ack", 0xa0, 0x00}
+	CMD_NACK                 = CmdCode{"nack", 0xa0, 0x01}
+	CMD_DEL_FILE             = CmdCode{"delete file", 0xa1, 0x01}
+	CMD_SYSTEM_STATE         = CmdCode{"system state", 0xa1, 0x02}
+	CMD_SYSTEM_PING          = CmdCode{"ping", 0xa2, 0x00}
+	CMD_SYSTEM_HEARTBEAT     = CmdCode{"system heartbeat", 0xa4, 0x07}
+	CMD_START_WRITE_FILE     = CmdCode{"start write file", 0xa1, 0x05}
+	CMD_WRITE_FILE           = CmdCode{"write file", 0xa1, 0x06}
+	CMD_WRITE_TRANS_START    = CmdCode{"start write trans", 0xa1, 0x07}
+	CMD_WRITE_TRANS_STOP     = CmdCode{"stop the write trans", 0xa1, 0x08}
+	CMD_WRITE_CUSTOMER_INFO  = CmdCode{"write customer information", 0xa1, 0x09}
+	CMD_GET_FILE_INTO        = CmdCode{"get file information", 0xa1, 0x0a}
+	CMD_GET_FILE_CONTENT     = CmdCode{"get file content", 0xa1, 0x0b}
+	CMD_SYSTEM_CLOCK_CORRECT = CmdCode{"system clock correct", 0xa2, 0x03}
+	CMD_READ_PARAMS          = CmdCode{"read params", 0xa2, 0x0a}
+	CMD_SOUND                = CmdCode{"add sound", 0xa2, 0x0e}
+	CMD_TURN_ON_OFF          = CmdCode{"turn on/off screen", 0xa3, 0x00}
+	CMD_TIMING_SWITCH        = CmdCode{"auto turn on/off screen", 0xa3, 0x01}
+	CMD_SCREEN_BRIGHTNESS    = CmdCode{"set brightness", 0xa3, 0x02}
+	CMD_LOCK_UNLOCK          = CmdCode{"lock or unlock program", 0xa3, 0x04}
+	CMD_SEND_DYNAMIC_AREA    = CmdCode{"send dynamic area", 0xa3, 0x06}
+	CMD_DEL_DYNAMIC_AREA     = CmdCode{"delete dynamic area", 0xa3, 0x07}
+	CMD_CANCEL_TIMING_SWITCH = CmdCode{"cancel auto turn on/off screen", 0xa3, 0x08}
+	CMD_CLEAR_SCREEN         = CmdCode{"clear the screen", 0xa3, 0x10}
+)

+ 36 - 0
bxx/BxCmdDelDynamicArea.go

@@ -0,0 +1,36 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type CmdDelDynamicArea struct {
+	baseBxCmd
+	numbers []byte //编号
+}
+
+func NewCmdDelDynamicArea(numbers []byte) CmdDelDynamicArea {
+	return CmdDelDynamicArea{
+		baseBxCmd: newBaseCmd(CMD_DEL_DYNAMIC_AREA.group, CMD_DEL_DYNAMIC_AREA.code),
+		numbers:   numbers,
+	}
+}
+
+func (cmd CmdDelDynamicArea) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 16))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, byte(0x01))
+	binary.Write(w, binary.LittleEndian, []byte{0x00, 0x00})
+	l := byte(len(cmd.numbers))
+	if l == 0 {
+		binary.Write(w, binary.LittleEndian, 0xff)
+	} else {
+		binary.Write(w, binary.LittleEndian, l)
+	}
+	for _, n := range cmd.numbers {
+		binary.Write(w, binary.LittleEndian, n)
+	}
+	return w.Bytes()
+}

+ 87 - 0
bxx/BxCmdFileBitmap.go

@@ -0,0 +1,87 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+)
+
+type bitmapFile struct {
+	FileType byte   //0x04
+	FileName string //4byte Txxx
+	FileLen  uint32
+	LibData  []byte
+	CHK      uint16
+}
+
+func NewBitmapFile(filename string, libdata []byte) bitmapFile {
+	chk := CRC16(libdata, 0, len(libdata))
+	fmt.Printf("位图文件校验:% 02x\n", chk)
+	return bitmapFile{
+		FileType: 0x04,
+		FileName: filename,
+		FileLen:  uint32(len(libdata)),
+		LibData:  libdata,
+		CHK:      chk,
+	}
+}
+
+func (bf *bitmapFile) NewCmd() *CmdWriteBitmapFile {
+	return &CmdWriteBitmapFile{
+		baseBxCmd:     newBaseCmd(CMD_WRITE_FILE.group, CMD_WRITE_FILE.code),
+		file:          bf,
+		LastBlockFlag: 1,
+	}
+}
+
+type CmdWriteBitmapFile struct {
+	baseBxCmd
+	state         byte
+	file          *bitmapFile
+	LastBlockFlag byte
+	BlockNum      uint16 //包号,如果是单包发送,则默认为 0x00。
+	BlockLen      uint16 //包长,若是单包发送,此处为文件长度。
+	BlockAddr     uint32 //本包数据在文件中的起始位置,如果是单包发送,此处默认为 0。
+	temp          []byte
+}
+
+func (cmd *CmdWriteBitmapFile) Build() []byte {
+	if cmd.state == 0 {
+		//Write File
+		w1 := bytes.NewBuffer(make([]byte, 0, 1024))
+		//文件描述数据
+		binary.Write(w1, binary.LittleEndian, cmd.Group())
+		binary.Write(w1, binary.LittleEndian, cmd.Cmd())
+		binary.Write(w1, binary.LittleEndian, byte(0x01))
+		binary.Write(w1, binary.LittleEndian, []byte{0x00, 0x00})
+		binary.Write(w1, binary.BigEndian, []byte(cmd.file.FileName))
+		binary.Write(w1, binary.LittleEndian, cmd.LastBlockFlag) //是否是最后一包,0x00——不是最后一包 0x01——最后一包。
+		binary.Write(w1, binary.LittleEndian, cmd.BlockNum)      //包号,单包为0x00
+		binary.Write(w1, binary.LittleEndian, cmd.file.FileLen)  //包长,若是单包发送,此处为文件长度。
+		binary.Write(w1, binary.LittleEndian, cmd.BlockAddr)     //本包数据在文件中的偏移量,单包为0x00
+		//文件内容数据
+		w2 := bytes.NewBuffer(make([]byte, 0, 1024))
+		binary.Write(w2, binary.LittleEndian, cmd.file.FileType)
+		binary.Write(w2, binary.BigEndian, []byte(cmd.file.FileName))
+		binary.Write(w2, binary.LittleEndian, cmd.file.FileLen)
+		binary.Write(w2, binary.BigEndian, cmd.file.LibData)
+		data := w2.Bytes()
+		crc := CRC16(data, 0, w2.Len())
+		binary.Write(w1, binary.BigEndian, data)
+		binary.Write(w1, binary.LittleEndian, crc)
+		cmd.temp = w1.Bytes()
+		//Start Write File "开始写文件",写文件前先检查内存是否够用
+		w3 := bytes.NewBuffer(make([]byte, 0, 64))
+		binary.Write(w3, binary.LittleEndian, CMD_START_WRITE_FILE.group)
+		binary.Write(w3, binary.LittleEndian, CMD_START_WRITE_FILE.code)
+		binary.Write(w3, binary.LittleEndian, byte(0x01))
+		binary.Write(w3, binary.LittleEndian, []byte{0x00, 0x00})
+		binary.Write(w3, binary.LittleEndian, byte(0x01)) //同名是否覆盖,0不覆盖,1覆盖
+		binary.Write(w3, binary.BigEndian, []byte(cmd.file.FileName))
+		binary.Write(w3, binary.LittleEndian, cmd.file.FileLen)
+		cmd.state = 1
+		return w3.Bytes()
+	} else {
+		return cmd.temp
+	}
+}

+ 36 - 0
bxx/BxCmdFileDelete.go

@@ -0,0 +1,36 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type CmdDeleteFile struct {
+	baseBxCmd
+	files []string
+}
+
+func NewCmdDeleteFile(files []string) CmdDeleteFile {
+	return CmdDeleteFile{
+		baseBxCmd: newBaseCmd(CMD_DEL_FILE.group, CMD_DEL_FILE.code),
+		files:     files,
+	}
+}
+
+func (cmd CmdDeleteFile) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, cmd.ReqResp())
+	binary.Write(w, binary.LittleEndian, []byte{0x00, 0x00})
+	l := uint16(len(cmd.files))
+	if l != 0 {
+		binary.Write(w, binary.LittleEndian, l)
+		for _, v := range cmd.files {
+			binary.Write(w, binary.BigEndian, []byte(v))
+		}
+		return w.Bytes()
+	}
+	binary.Write(w, binary.LittleEndian, []byte{0x00})
+	return w.Bytes()
+}

+ 25 - 0
bxx/BxCmdFileRead.go

@@ -0,0 +1,25 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type CmdReadFileInfo struct {
+	baseBxCmd
+}
+
+func NewCmdReadFileInfo(FileName string) CmdReadFileInfo {
+	return CmdReadFileInfo{
+		baseBxCmd: newBaseCmd(CMD_GET_FILE_INTO.group, CMD_GET_FILE_INTO.code),
+	}
+}
+
+func (cmd CmdReadFileInfo) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 8))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, cmd.ReqResp())
+	binary.Write(w, binary.LittleEndian, []byte{0x00, 0x00})
+	return nil
+}

+ 168 - 0
bxx/BxCmdFileWrite.go

@@ -0,0 +1,168 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+	"github.com/sirupsen/logrus"
+	"time"
+)
+
+// 普通文本文件
+type bxFile struct {
+	type_       byte   //默认0x00
+	name        string //4字节ASCII
+	len         uint32
+	content     string
+	Priority    byte
+	DisplayType uint16 //播放方式节目播放方式,0——顺序播放,其他——定长播放的 时间,单位为秒
+	PlayTimes   byte
+	//节目生命周期,发送顺序为:起始年(2)+起始月(1)+起
+	//始日(1)+结束年(2)+结束月(1)+结束日(1)注:1. 时间均
+	//采用 BCD 码的方式2. 年范围为 0x1900—0x2099,
+	//0xffff 为永久有效,先发送 LSB,后发送 MSB
+	ProgramLife string
+	//节目的星期属性
+	//1. Bit0 为 1 表示一周中的每一天都播放。
+	//2. Bit0 为 0 时,需判断 bit1-bit7 的来决定每天播放,
+	//bit1-bit7依次表示周一到周日。
+	//3.比特为0表示禁止播放,为 1 表示播放。
+	ProgramWeek byte
+	//定时节目位 0 非定时,注:为 0 时则播放时段组数设 置为 0
+	ProgramTime byte
+	//节目播放时段组数,最多支持一组,当为 0 时 PlayPeriodSetting
+	PlayPeriodGrpNum byte
+	//6Byte 播放组0,发送顺序为:起始小时(1)+起始分钟(1)+起始秒(1)+结束小时(1)+结束分钟(1)+结束秒(1)
+	//PlayPeriodSetting0 time.Time
+	Areas []BxArea
+}
+
+// NewBxFile diedLine 节目过期时间 "2006-01-02", 传"" 永不过期
+func NewBxFile(fileName string, diedLine string, areas []BxArea) bxFile {
+	return bxFile{
+		name:        fileName,
+		Areas:       areas,
+		ProgramLife: diedLine,
+		Priority:    0xff,
+		PlayTimes:   1,
+		ProgramWeek: 1,
+	}
+}
+func (f bxFile) NewCmdWriteFile() *CmdWriteFile {
+	return &CmdWriteFile{
+		baseBxCmd:     newBaseCmd(CMD_WRITE_FILE.group, CMD_WRITE_FILE.code),
+		file:          &f,
+		LastBlockFlag: 1,
+	}
+}
+
+func (f bxFile) encodeProgramLife() []byte {
+	if f.ProgramLife == "" {
+		return []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+	}
+	w := bufPoll.Get().(*bytes.Buffer)
+	now := time.Now()
+	y := Uint2BCD(uint64(now.Year()), false)
+	m := Uint2BCD(uint64(now.Month()), false)
+	d := Uint2BCD(uint64(now.Day()), false)
+	binary.Write(w, binary.BigEndian, y)
+	binary.Write(w, binary.BigEndian, m)
+	binary.Write(w, binary.BigEndian, d)
+	end, err := time.Parse("2006-01-02", f.ProgramLife)
+	if err != nil {
+		logrus.Error("时间解析错误:", err)
+		return nil
+	}
+	endY := Uint2BCD(uint64(end.Year()), false)
+	endM := Uint2BCD(uint64(end.Month()), false)
+	endD := Uint2BCD(uint64(end.Day()), false)
+	binary.Write(w, binary.BigEndian, endY)
+	binary.Write(w, binary.BigEndian, endM)
+	binary.Write(w, binary.BigEndian, endD)
+	return w.Bytes()
+}
+
+//func (f bxFile) Build() []byte {
+//	w := bytes.NewBuffer(make([]byte, 0, 1024))
+//	return w.Bytes()
+//}
+
+type CmdWriteFile struct {
+	baseBxCmd
+	state         byte
+	file          *bxFile
+	LastBlockFlag byte
+	BlockNum      uint16 //包号,如果是单包发送,则默认为 0x00。
+	BlockLen      uint16 //包长,若是单包发送,此处为文件长度。
+	BlockAddr     uint32 //本包数据在文件中的起始位置,如果是单包发送,此处默认为 0。
+	temp          []byte
+}
+
+func (cmd *CmdWriteFile) Build() []byte {
+	if cmd.state == 0 {
+		//先计算区域数据及长度
+		w0 := bytes.NewBuffer(make([]byte, 0, 1024))
+		for _, v := range cmd.file.Areas {
+			b := v.Build()
+			binary.Write(w0, binary.LittleEndian, uint32(len(b))+4)
+			binary.Write(w0, binary.LittleEndian, b)
+		}
+		l := w0.Len() + 27
+		cmd.BlockLen = uint16(l)
+		cmd.file.len = uint32(l)
+		//Write File
+		w1 := bytes.NewBuffer(make([]byte, 0, 1024))
+		//文件描述数据
+		binary.Write(w1, binary.LittleEndian, cmd.Group())
+		binary.Write(w1, binary.LittleEndian, cmd.Cmd())
+		binary.Write(w1, binary.LittleEndian, byte(0x01))
+		binary.Write(w1, binary.LittleEndian, []byte{0x00, 0x00})
+		binary.Write(w1, binary.BigEndian, []byte(cmd.file.name))
+		binary.Write(w1, binary.LittleEndian, cmd.LastBlockFlag) //是否是最后一包,0x00——不是最后一包 0x01——最后一包。
+		binary.Write(w1, binary.LittleEndian, cmd.BlockNum)      //包号,单包为0x00
+		binary.Write(w1, binary.LittleEndian, cmd.BlockLen)      //包长,若是单包发送,此处为文件长度。
+		binary.Write(w1, binary.LittleEndian, cmd.BlockAddr)     //本包数据在文件中的偏移量,单包为0x00
+		//文件内容数据
+		w2 := bytes.NewBuffer(make([]byte, 0, 1024))
+		binary.Write(w2, binary.LittleEndian, cmd.file.type_)
+		binary.Write(w2, binary.BigEndian, []byte(cmd.file.name))
+		binary.Write(w2, binary.LittleEndian, cmd.file.len)
+		binary.Write(w2, binary.LittleEndian, cmd.file.Priority)
+		binary.Write(w2, binary.LittleEndian, cmd.file.DisplayType)
+		binary.Write(w2, binary.LittleEndian, cmd.file.PlayTimes)
+		binary.Write(w2, binary.BigEndian, cmd.file.encodeProgramLife())
+		binary.Write(w2, binary.LittleEndian, cmd.file.ProgramWeek)
+		binary.Write(w2, binary.LittleEndian, cmd.file.ProgramTime)
+		binary.Write(w2, binary.LittleEndian, cmd.file.PlayPeriodGrpNum)
+		//binary.Write(w, binary.LittleEndian, cmd.file.PlayPeriodSetting0)
+		binary.Write(w2, binary.LittleEndian, byte(len(cmd.file.Areas)))
+		binary.Write(w2, binary.LittleEndian, w0.Bytes())
+		b2 := w2.Bytes()
+		binary.Write(w1, binary.BigEndian, b2)
+		crc16 := CRC16(b2, 0, w2.Len())
+		binary.Write(w1, binary.LittleEndian, crc16)
+		cmd.temp = w1.Bytes()
+		//Start Write File "开始写文件",写文件前先检查内存是否够用
+		w3 := bytes.NewBuffer(make([]byte, 0, 64))
+		binary.Write(w3, binary.LittleEndian, CMD_START_WRITE_FILE.group)
+		binary.Write(w3, binary.LittleEndian, CMD_START_WRITE_FILE.code)
+		binary.Write(w3, binary.LittleEndian, byte(0x01))
+		binary.Write(w3, binary.LittleEndian, []byte{0x00, 0x00})
+		binary.Write(w3, binary.LittleEndian, byte(0x01)) //同名是否覆盖,0不覆盖,1覆盖
+		binary.Write(w3, binary.BigEndian, []byte(cmd.file.name))
+		binary.Write(w3, binary.LittleEndian, cmd.file.len)
+		cmd.state = 1
+		return w3.Bytes()
+	} else {
+		return cmd.temp
+	}
+}
+
+type cmdFileBeginWrite struct {
+	baseBxCmd
+}
+
+func NewCmdFileBeginWrite() cmdFileBeginWrite {
+	return cmdFileBeginWrite{
+		baseBxCmd: newBaseCmd(CMD_START_WRITE_FILE.group, CMD_START_WRITE_FILE.code),
+	}
+}

+ 39 - 0
bxx/BxCmdLock.go

@@ -0,0 +1,39 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type CmdLock struct {
+	baseBxCmd
+	StoreMode       byte
+	LockFlag        byte
+	ProgramFileName string
+}
+
+// NewCmdLock 锁定状态:0x00——解锁状态,0x01——锁定状态
+func NewCmdLock(flag byte, name string) CmdLock {
+	return CmdLock{
+		baseBxCmd:       newBaseCmd(CMD_LOCK_UNLOCK.group, CMD_LOCK_UNLOCK.code),
+		LockFlag:        flag,
+		ProgramFileName: name,
+	}
+}
+
+// SetStoreMode 锁定状态保存方式:0x00——掉电不保存,0x01——掉电保存
+func (cmd *CmdLock) SetStoreMode(storeMode byte) {
+	cmd.StoreMode = storeMode
+}
+
+func (cmd *CmdLock) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 32))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, byte(0x01))
+	binary.Write(w, binary.LittleEndian, []byte{0x00, 0x00})
+	binary.Write(w, binary.LittleEndian, cmd.StoreMode)
+	binary.Write(w, binary.LittleEndian, cmd.LockFlag)
+	binary.Write(w, binary.BigEndian, []byte(cmd.ProgramFileName))
+	return w.Bytes()
+}

+ 15 - 0
bxx/BxCmdReadParams.go

@@ -0,0 +1,15 @@
+package bx
+
+type CmdReadParams struct {
+	baseBxCmd
+}
+
+func NewCmdReadParams() CmdReadParams {
+	return CmdReadParams{
+		baseBxCmd: newBaseCmd(CMD_READ_PARAMS.group, CMD_READ_PARAMS.code),
+	}
+}
+
+func (cmd CmdReadParams) Build() []byte {
+	return []byte{0xa2, 0x0a, 0x01, 0x00, 0x00}
+}

+ 82 - 0
bxx/BxCmdSendDynamicArea.go

@@ -0,0 +1,82 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+var GROUP byte = 0xa3
+var CMD byte = 0x06
+
+type CmdSendDynamicArea struct {
+	baseBxCmd
+	processMode byte
+	r2          byte
+	delAreaIds  []byte
+	areas       []BxArea
+}
+
+func NewBxCmdSendDynamicArea(areas []BxArea) CmdSendDynamicArea {
+	return CmdSendDynamicArea{
+		baseBxCmd: newBaseCmd(CMD_SEND_DYNAMIC_AREA.group, CMD_SEND_DYNAMIC_AREA.code),
+		areas:     areas,
+	}
+}
+
+func (sd CmdSendDynamicArea) ProcessMode() byte {
+	return sd.processMode
+}
+
+func (sd CmdSendDynamicArea) SetProcessMode(processMode byte) {
+	sd.processMode = processMode
+}
+
+func (sd CmdSendDynamicArea) R2() byte {
+	return sd.r2
+}
+
+func (sd CmdSendDynamicArea) SetR2(r2 byte) {
+	sd.r2 = r2
+}
+
+func (sd CmdSendDynamicArea) DelAreaIds() []byte {
+	return sd.delAreaIds
+}
+
+func (sd CmdSendDynamicArea) SetDelAreaIds(delAreaIds []byte) {
+	sd.delAreaIds = delAreaIds
+}
+
+func (sd CmdSendDynamicArea) Areas() []BxArea {
+	return sd.areas
+}
+
+func (sd CmdSendDynamicArea) SetAreas(areas []BxArea) {
+	sd.areas = areas
+}
+
+func (sd CmdSendDynamicArea) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	binary.Write(w, binary.LittleEndian, sd.Group())
+	binary.Write(w, binary.LittleEndian, sd.Cmd())
+	binary.Write(w, binary.LittleEndian, sd.ReqResp())
+	binary.Write(w, binary.LittleEndian, sd.processMode)
+	binary.Write(w, binary.LittleEndian, sd.r2)
+	if sd.delAreaIds == nil {
+		binary.Write(w, binary.LittleEndian, byte(0x00))
+	} else {
+		binary.Write(w, binary.LittleEndian, byte(len(sd.delAreaIds)))
+		binary.Write(w, binary.LittleEndian, sd.delAreaIds)
+	}
+	if sd.areas == nil || len(sd.areas) == 0 {
+		binary.Write(w, binary.LittleEndian, byte(0x00))
+	} else {
+		binary.Write(w, binary.LittleEndian, byte(len(sd.areas)))
+		for _, v := range sd.areas {
+			b := v.Build()
+			binary.Write(w, binary.LittleEndian, int16(len(b)))
+			binary.Write(w, binary.LittleEndian, b)
+		}
+	}
+	return w.Bytes()
+}

+ 27 - 0
bxx/BxCmdState.go

@@ -0,0 +1,27 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type CmdState struct {
+	baseBxCmd
+}
+
+func NewCmdState() CmdState {
+	return CmdState{
+		baseBxCmd: newBaseCmd(CMD_SYSTEM_STATE.group, CMD_SYSTEM_STATE.code),
+	}
+}
+
+func (cmd CmdState) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, cmd.ReqResp())
+	//r0 r1
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	return w.Bytes()
+}

+ 159 - 0
bxx/BxCmdSystemClockCorrect.go

@@ -0,0 +1,159 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"time"
+)
+
+type CmdSystemClockCorrect struct {
+	baseBxCmd
+	sysTime                                      time.Time
+	year, month, day, hour, minute, second, week int
+}
+
+func NewBxCmdSystemClockCorrect(t time.Time) CmdSystemClockCorrect {
+	fmt.Println(t.Year())
+	fmt.Println(t.Month())
+	fmt.Println(t.Day())
+	fmt.Println(t.Hour())
+	fmt.Println(t.Minute())
+	fmt.Println(t.Second())
+	fmt.Println(t.Weekday())
+	year, _ := BIN2Uint64(Uint2BCD(uint64(t.Year()), false), binary.LittleEndian)
+	month, _ := BIN2Uint64(Uint2BCD(uint64(t.Month()), false), binary.LittleEndian)
+	day, _ := BIN2Uint64(Uint2BCD(uint64(t.Day()), false), binary.LittleEndian)
+	hour, _ := BIN2Uint64(Uint2BCD(uint64(t.Hour()), false), binary.LittleEndian)
+	minute, _ := BIN2Uint64(Uint2BCD(uint64(t.Minute()), false), binary.LittleEndian)
+	second, _ := BIN2Uint64(Uint2BCD(uint64(t.Second()), false), binary.LittleEndian)
+	week, _ := BIN2Uint64(Uint2BCD(uint64(t.Weekday()), false), binary.LittleEndian)
+	if week == 0 {
+		week = 7
+	}
+	return CmdSystemClockCorrect{
+		baseBxCmd: newBaseCmd(CMD_SYSTEM_CLOCK_CORRECT.group, CMD_SYSTEM_CLOCK_CORRECT.code),
+		sysTime:   t,
+		year:      int(year),
+		month:     int(month),
+		day:       int(day),
+		hour:      int(hour),
+		minute:    int(minute),
+		second:    int(second),
+		week:      int(week),
+	}
+}
+
+// uint转BCD
+func Uint2BCD(n uint64, isBigEndian bool) []byte {
+	var b []byte
+	//if n < 256 {
+	//	b = []byte{0}
+	//}
+	for i := 0; ; i++ {
+		h := (n / 10) % 10
+		l := n % 10
+		b = append(b, byte(h<<4|l))
+		n = n / 100
+		if n == 0 {
+			break
+		}
+	}
+	if !isBigEndian {
+		return b
+	}
+	l := len(b)
+	var r = make([]byte, l)
+	for i, v := range b {
+		r[l-1-i] = v
+	}
+	return r
+}
+func BIN2Uint64(bin []byte, order binary.ByteOrder) (uint64, error) {
+	len := len(bin)
+	switch len {
+	case 1:
+		return uint64(bin[0]), nil
+	case 2:
+		return uint64(order.Uint16(bin)), nil
+	case 3, 4:
+		bin4 := make([]byte, 8)
+		copy(bin4[4-len:], bin) //前面字节填充0
+		return uint64(order.Uint32(bin4)), nil
+	case 5, 6, 7, 8:
+		bin8 := make([]byte, 8)
+		copy(bin8[8-len:], bin)
+		return order.Uint64(bin8), nil
+	default:
+		return 0, errors.New("不符合字节长度范围1-8")
+	}
+}
+
+func (this CmdSystemClockCorrect) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	binary.Write(w, binary.LittleEndian, this.Group())
+	binary.Write(w, binary.LittleEndian, this.Cmd())
+	binary.Write(w, binary.LittleEndian, this.ReqResp())
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	//BCD码:年(2)+月(1)+日(1)+时(1)+分(1)+秒(1)+星期(1);先地位再高位
+	//年:低端发送,低位在前
+	y := []byte{
+		byte(this.year),
+		byte(this.year >> 8),
+		byte(this.year >> 16),
+		byte(this.year >> 24),
+	}
+	if y[0] == 0x00 && y[1] == 0x00 {
+		binary.Write(w, binary.LittleEndian, y[2])
+		binary.Write(w, binary.LittleEndian, y[3])
+	} else {
+		binary.Write(w, binary.LittleEndian, y[0])
+		binary.Write(w, binary.LittleEndian, y[1])
+	}
+	//月
+	m := []byte{
+		byte(this.month),
+		byte(this.month >> 8),
+		byte(this.month >> 16),
+		byte(this.month >> 24),
+	}
+	binary.Write(w, binary.LittleEndian, m[0])
+	d := []byte{
+		byte(this.day),
+		byte(this.day >> 8),
+		byte(this.day >> 16),
+		byte(this.day >> 24),
+	}
+	binary.Write(w, binary.LittleEndian, d[0])
+	h := []byte{
+		byte(this.hour),
+		byte(this.hour >> 8),
+		byte(this.hour >> 16),
+		byte(this.hour >> 24),
+	}
+	binary.Write(w, binary.LittleEndian, h[0])
+	min := []byte{
+		byte(this.minute),
+		byte(this.minute >> 8),
+		byte(this.minute >> 16),
+		byte(this.minute >> 24),
+	}
+	binary.Write(w, binary.LittleEndian, min[0])
+	s := []byte{
+		byte(this.second),
+		byte(this.second >> 8),
+		byte(this.second >> 16),
+		byte(this.second >> 24),
+	}
+	binary.Write(w, binary.LittleEndian, s[0])
+	week := []byte{
+		byte(this.week),
+		byte(this.week >> 8),
+		byte(this.week >> 16),
+		byte(this.week >> 24),
+	}
+	binary.Write(w, binary.LittleEndian, week[0])
+	return w.Bytes()
+}

+ 42 - 0
bxx/BxCmdTimingSwitch.go

@@ -0,0 +1,42 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+//定时开关机
+
+type CmdTimingSwitch struct {
+	baseBxCmd
+	onOffSet [][2]uint64
+}
+
+func NewCmdTimingSwitch(onOffSet [][2]uint64) CmdTimingSwitch {
+	return CmdTimingSwitch{
+		baseBxCmd: newBaseCmd(CMD_TIMING_SWITCH.group, CMD_TIMING_SWITCH.code),
+		onOffSet:  onOffSet,
+	}
+}
+
+func (cmd CmdTimingSwitch) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, cmd.ReqResp())
+	//r0 r1
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	if len(cmd.onOffSet) == 0 || len(cmd.onOffSet) > 256 {
+		return nil
+	}
+	binary.Write(w, binary.LittleEndian, byte(len(cmd.onOffSet)))
+	for i, v := range cmd.onOffSet {
+		if i > 2 {
+			break
+		}
+		binary.Write(w, binary.BigEndian, Uint2BCD(uint64(v[0]), true)) //开机
+		binary.Write(w, binary.BigEndian, Uint2BCD(uint64(v[1]), true)) //关机
+	}
+	return w.Bytes()
+}

+ 34 - 0
bxx/BxCmdTurnOnOff.go

@@ -0,0 +1,34 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type BxCmdTurnOnOff struct {
+	baseBxCmd
+	on bool
+}
+
+// NewBxCmdTurnOnOff ture=on false=off
+func NewBxCmdTurnOnOff(on bool) BxCmdTurnOnOff {
+	return BxCmdTurnOnOff{
+		baseBxCmd: newBaseCmd(CMD_TURN_ON_OFF.group, CMD_TURN_ON_OFF.code),
+		on:        on,
+	}
+}
+func (cmd BxCmdTurnOnOff) Build() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 8))
+	binary.Write(w, binary.LittleEndian, cmd.Group())
+	binary.Write(w, binary.LittleEndian, cmd.Cmd())
+	binary.Write(w, binary.LittleEndian, cmd.ReqResp())
+	//r0 r1
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	binary.Write(w, binary.LittleEndian, byte(0x00))
+	if cmd.on {
+		binary.Write(w, binary.LittleEndian, byte(0x01))
+	} else {
+		binary.Write(w, binary.LittleEndian, byte(0x02))
+	}
+	return w.Bytes()
+}

+ 229 - 0
bxx/BxDataPack.go

@@ -0,0 +1,229 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+)
+
+type BxDataPack struct {
+	WRAP_A5_NUM int
+	WRAP_5A_NUM int
+	dstAddr     uint16
+	srcAddr     uint16
+	r0          byte
+	r1          byte
+	r2          byte
+	option      byte
+	crcMode     byte
+	displayType byte
+	deviceType  byte
+	version     byte
+	dataLen     uint16
+	data        []byte
+	crc         uint16
+}
+
+func NewBxDataPackData(data []byte) BxDataPack {
+	return BxDataPack{
+		data:        data,
+		dataLen:     uint16(len(data)),
+		WRAP_A5_NUM: 8,
+		WRAP_5A_NUM: 1,
+		dstAddr:     0xfffe,
+		srcAddr:     0x8000,
+		deviceType:  0xfe,
+		version:     0x02,
+	}
+}
+func NewBxDataPackCmd(cmd BxCmd) BxDataPack {
+	b := cmd.Build()
+	return BxDataPack{
+		data:        b,
+		dataLen:     uint16(len(b)),
+		WRAP_A5_NUM: 8,
+		WRAP_5A_NUM: 1,
+		dstAddr:     0x0001, //todo 设备id
+		srcAddr:     0x8000,
+		deviceType:  0xfe,
+		version:     0x02,
+	}
+}
+
+// SetDisplayType 注:特殊动态区不支持动态模式
+// 0x00:普通模式,动态区与节目可同时显示,但各区域不可重叠。
+// 0x01:动态模式,优先显示动态区,无动态区则显示节目,动态区与节目区可重叠。
+func (dp *BxDataPack) SetDisplayType(typ byte) {
+	dp.displayType = typ
+}
+
+func (dp *BxDataPack) wrap(src []byte) []byte {
+	len := len(src)
+	for _, v := range src { //每次转义会使长度+1
+		if v == 0xa5 || v == 0x5a || v == 0xa6 || v == 0x5b {
+			len++
+		}
+	}
+	len += dp.WRAP_5A_NUM
+	len += dp.WRAP_A5_NUM
+	dst := make([]byte, len)
+	offset := 0
+	for i := 0; i < dp.WRAP_A5_NUM; i++ {
+		dst[offset] = 0xa5
+		offset++
+	}
+	//转义
+	for _, v := range src {
+		if v == 0xa5 {
+			dst[offset] = 0xa6
+			offset++
+			dst[offset] = 0xa2
+			offset++
+		} else if v == 0xa6 {
+			dst[offset] = 0xa6
+			offset++
+			dst[offset] = 0xa1
+			offset++
+		} else if v == 0x5a {
+			dst[offset] = 0x5b
+			offset++
+			dst[offset] = 0xa2
+			offset++
+		} else if v == 0x5b {
+			dst[offset] = 0x5b
+			offset++
+			dst[offset] = 0x01
+			offset++
+		} else {
+			dst[offset] = v
+			offset++
+		}
+	}
+
+	for i := 0; i < dp.WRAP_5A_NUM; i++ {
+		dst[offset] = 0x5a
+		offset++
+	}
+	return dst
+}
+
+func (dp *BxDataPack) Pack() []byte {
+	w := bytes.NewBuffer(make([]byte, 0, 1024))
+	//目标地址
+	binary.Write(w, binary.LittleEndian, dp.dstAddr)
+	//源地址
+	binary.Write(w, binary.LittleEndian, dp.srcAddr)
+	binary.Write(w, binary.LittleEndian, dp.r0)
+	binary.Write(w, binary.LittleEndian, dp.r1)
+	binary.Write(w, binary.LittleEndian, dp.r2)
+	binary.Write(w, binary.LittleEndian, dp.option)
+	binary.Write(w, binary.LittleEndian, dp.crcMode)
+	binary.Write(w, binary.LittleEndian, dp.displayType)
+	binary.Write(w, binary.LittleEndian, dp.deviceType)
+	binary.Write(w, binary.LittleEndian, dp.version)
+	binary.Write(w, binary.LittleEndian, dp.dataLen)
+	binary.Write(w, binary.LittleEndian, dp.data)
+	dp.crc = 0x0
+	binary.Write(w, binary.LittleEndian, dp.crc)
+	data := w.Bytes()
+	len := w.Len()
+	dp.crc = CRC16(data, 0, len-2)
+	data[len-2] = byte(dp.crc & 0xff)
+	data[len-1] = byte(dp.crc >> 8)
+	return dp.wrap(data)
+}
+
+func dpParse(src []byte, length int) *BxDataPack {
+	dst := unwrap(src, length)
+	if dst == nil {
+		return nil
+	}
+	crcCalculated := CRC16(dst, 0, len(dst)-2)
+	crcGot := bytesToUint16(dst, len(dst)-2, binary.LittleEndian)
+
+	if crcCalculated != crcGot {
+		return nil
+	}
+	dp := BxDataPack{}
+	offset := 0
+
+	//目标地址
+	dp.dstAddr = bytesToUint16(dst, offset, binary.LittleEndian)
+	offset += 2
+	//源地址
+	dp.srcAddr = bytesToUint16(dst, offset, binary.LittleEndian)
+	offset += 2
+	//保留字 r0,r1,r2
+	dp.r0 = dst[offset]
+	offset++
+	dp.r1 = dst[offset]
+	offset++
+	dp.r2 = dst[offset]
+	offset++
+
+	dp.option = dst[offset]
+	offset++
+	dp.crcMode = dst[offset]
+	offset++
+	dp.displayType = dst[offset]
+	offset++
+	dp.deviceType = dst[offset]
+	offset++
+	dp.version = dst[offset]
+	offset++
+	dp.dataLen = bytesToUint16(dst, offset, binary.LittleEndian)
+	offset += 2
+	//数据
+	dp.data = dst[offset : offset+int(dp.dataLen)]
+	offset += int(dp.dataLen)
+	dp.crc = bytesToUint16(dst, offset, binary.LittleEndian)
+	return &dp
+}
+
+func unwrap(src []byte, length int) []byte {
+	len := length
+	for _, v := range src {
+		if v == 0xa5 || v == 0x5a || v == 0xa6 {
+			len--
+		}
+	}
+	//如果计算的帧长度为0, 说明数据不正确
+	if len == 0 {
+		return nil
+	}
+	dst := make([]byte, len)
+	offset := 0
+	for i := 0; i < length; {
+		if src[i] == 0xa5 || src[i] == 0x5a {
+			i++
+		} else if src[i] == 0xa6 {
+			if src[i+1] == 0x01 {
+				dst[offset] = 0xa6
+				offset++
+				i = i + 2
+			} else if src[i+1] == 0x02 {
+				dst[offset] = 0xa5
+				offset++
+				i = i + 2
+			} else {
+				return nil
+			}
+		} else if src[i] == 0x5b {
+			if src[i+1] == 0x01 {
+				dst[offset] = 0x5b
+				offset++
+				i = i + 2
+			} else if src[i+1] == 0x02 {
+				dst[offset] = 0x5a
+				offset++
+				i = i + 2
+			} else {
+				return nil
+			}
+		} else {
+			dst[offset] = src[i]
+			offset++
+			i++
+		}
+	}
+	return dst
+}

+ 214 - 0
bxx/BxResp.go

@@ -0,0 +1,214 @@
+package bx
+
+import (
+	"encoding/binary"
+	"encoding/hex"
+	"fmt"
+)
+
+type BxResp struct {
+	group, cmd, Err, r0, r1 byte
+	Data                    []byte
+	//StateInfo               *StateInfo
+}
+
+func parse_(pack BxDataPack) *BxResp {
+	var offset int
+	resp := &BxResp{}
+	resp.group = pack.data[offset]
+	offset++
+	resp.cmd = pack.data[offset]
+	offset++
+	resp.Err = pack.data[offset]
+	offset++
+	resp.r0 = pack.data[offset]
+	offset++
+	resp.r1 = pack.data[offset]
+	offset++
+	resp.Data = pack.data[offset:(offset + int(pack.dataLen) - 5)]
+	//if !resp.IsInfo() {
+	//	return resp
+	//}
+	//offset = 0
+	//resp.StateInfo = &StateInfo{}
+	//resp.StateInfo.OnOff = resp.Data[offset]
+	//offset++
+	//resp.StateInfo.Brightness = resp.Data[offset]
+	//offset++
+	//t := resp.Data[offset : offset+8]
+	//resp.StateInfo.SystemTime = hex.EncodeToString(t)
+	//offset += 8
+	//resp.StateInfo.ProgramNum = resp.Data[offset]
+	//offset++
+	//resp.StateInfo.CruFileName = string(resp.Data[offset : offset+4])
+	//offset += 4
+	//resp.StateInfo.SpecialDynaArea = resp.Data[offset]
+	//offset++
+	//resp.StateInfo.PageNum = resp.Data[offset]
+	//offset++
+	//resp.StateInfo.DynaAreaNum = resp.Data[offset]
+	//offset++
+	//for i := byte(0); i < resp.StateInfo.ProgramNum; i++ {
+	//	resp.StateInfo.DynaAreaIDs = append(resp.StateInfo.DynaAreaIDs, resp.Data[offset])
+	//	offset++
+	//}
+	//resp.StateInfo.BarCode = string(resp.Data[offset : offset+16])
+	return resp
+}
+
+func (r BxResp) Parse(src []byte, len int) *BxResp {
+	dp := dpParse(src, len)
+	if dp == nil {
+		return nil
+	} else {
+		return parse_(*dp)
+	}
+}
+
+func (r BxResp) IsAck() bool {
+	if r.group == CMD_ACK.group && r.cmd == CMD_ACK.code {
+		return true
+	} else {
+		return false
+	}
+}
+
+func (r BxResp) NoError() bool {
+	return r.Err == 0
+}
+
+func (r BxResp) Error() BxError {
+	return bxErrors[r.Err]
+}
+
+// IsInfo 是否是返回"控制器状态"信息
+func (r BxResp) IsInfo() bool {
+	if r.group == CMD_SYSTEM_STATE.group && r.cmd == CMD_SYSTEM_STATE.code {
+		return true
+	} else {
+		return false
+	}
+}
+
+type StateInfo struct {
+	OnOff           byte
+	Brightness      byte
+	SystemTime      string //年(2)+月(1)+日(1)+星期(1)+时(1)+分(1)+秒(1)
+	ProgramNum      byte
+	CruFileName     string
+	SpecialDynaArea byte
+	PageNum         byte
+	DynaAreaNum     byte
+	DynaAreaIDs     []byte
+	BarCode         string
+}
+
+func (s *StateInfo) Print(name string) {
+	info :=
+		`==== %s ====
+电源状态: %s
+系统时间: %s
+节目数量: %d
+当前播放: %s
+动态区数: %d
+动态ID: %v
+条码: %s
+===============
+`
+	onoff := ""
+	if s.OnOff == 1 {
+		onoff = "开机"
+	} else {
+		onoff = "关机"
+	}
+	fmt.Printf(info, name, onoff, s.SystemTime, s.ProgramNum, s.CruFileName, s.DynaAreaNum, s.DynaAreaIDs,
+		s.BarCode)
+}
+
+func (s *StateInfo) Parse(data []byte) {
+	offset := 0
+	s.OnOff = data[offset]
+	offset++
+	s.Brightness = data[offset]
+	offset++
+	t := data[offset : offset+8]
+	s.SystemTime = hex.EncodeToString(t)
+	offset += 8
+	s.ProgramNum = data[offset]
+	offset++
+	s.CruFileName = string(data[offset : offset+4])
+	offset += 4
+	s.SpecialDynaArea = data[offset]
+	offset++
+	s.PageNum = data[offset]
+	offset++
+	s.DynaAreaNum = data[offset]
+	offset++
+	for i := byte(0); i < s.DynaAreaNum; i++ {
+		s.DynaAreaIDs = append(s.DynaAreaIDs, data[offset])
+		offset++
+	}
+	s.BarCode = string(data[offset : offset+16])
+}
+
+type Params struct {
+	Address      uint16
+	DeviceType   byte
+	BaudRate     byte
+	ScreenWidth  uint16
+	ScreenHeight uint16
+	Color        byte
+	DA           byte
+	OE           byte
+	FreqPar      byte
+	RowOrder     byte
+	MirrorMode   byte
+	OEAngle      byte
+	ScanMode     byte
+	ScanConfNum  byte
+	LatticeMode  byte
+	//Reserved []byte
+}
+
+func (p *Params) Parse(data []byte) {
+	offset := 0
+	addr, err := BIN2Uint64(data[offset:offset+2], binary.LittleEndian)
+	if err == nil {
+		p.Address = uint16(addr)
+	}
+	offset += 2
+	p.DeviceType = data[offset]
+	offset++
+	p.BaudRate = data[offset]
+	offset++
+	w, err := BIN2Uint64(data[offset:offset+2], binary.LittleEndian)
+	if err == nil {
+		p.ScreenWidth = uint16(w)
+	}
+	offset += 2
+	h, err := BIN2Uint64(data[offset:offset+2], binary.LittleEndian)
+	if err == nil {
+		p.ScreenHeight = uint16(h)
+	}
+	offset += 2
+	p.Color = data[offset]
+	offset++
+	p.DA = data[offset]
+	offset++
+	p.OE = data[offset]
+	offset++
+	p.FreqPar = data[offset]
+	offset++
+	p.RowOrder = data[offset]
+	offset++
+	p.MirrorMode = data[offset]
+	offset++
+	p.OEAngle = data[offset]
+	offset++
+	p.ScanMode = data[offset]
+	offset++
+	p.ScanConfNum = data[offset]
+	offset++
+	//p.LatticeMode = data[offset]
+	//offset++
+}

+ 105 - 0
bxx/BxUtils.go

@@ -0,0 +1,105 @@
+package bx
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"math"
+	"sync"
+)
+
+var bufPoll = sync.Pool{
+	New: func() interface{} {
+		var buf = make([]byte, 0, 210)
+		return bytes.NewBuffer(buf)
+	},
+}
+
+var crc16_table = []uint16{
+	0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
+	0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
+	0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
+	0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
+	0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
+	0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
+	0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
+	0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
+	0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
+	0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
+	0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
+	0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
+	0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
+	0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
+	0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
+	0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
+	0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
+	0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
+	0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
+	0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
+	0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
+	0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
+	0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
+	0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
+	0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
+	0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
+	0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
+	0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
+	0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
+	0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
+	0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
+	0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
+}
+
+func bytesToUint16(src []byte, start int, order binary.ByteOrder) uint16 {
+	return order.Uint16(src[start:])
+}
+func CRC16(data []byte, offset, length int) uint16 {
+	var crc16 uint16
+	for _, v := range data[offset:length] {
+		n := uint8(uint16(v) ^ crc16)
+		crc16 >>= 8
+		crc16 ^= crc16_table[n]
+	}
+	return crc16
+}
+
+func Uint16ToBin(i uint16, order binary.ByteOrder) []byte {
+	buf := make([]byte, 2)
+	order.PutUint16(buf, i)
+	return buf
+}
+
+var lengthErr = errors.New("需要更大的长度存储该数值")
+
+func Uint2BIN(n uint64, len uint8, order binary.ByteOrder) ([]byte, error) {
+	switch len {
+	case 1:
+		if n > math.MaxUint8 {
+			return nil, lengthErr
+		}
+		return []byte{byte(n)}, nil
+	case 2:
+		if n > math.MaxUint16 {
+			return nil, lengthErr
+		}
+		b := make([]byte, 2)
+		order.PutUint16(b, uint16(n))
+		return b, nil
+	case 3, 4:
+		if n > math.MaxUint32 {
+			return nil, lengthErr
+		}
+		b := make([]byte, 4)
+		order.PutUint32(b, uint32(n))
+		return b, nil
+	case 5, 6, 7, 8:
+		if n > math.MaxUint64 {
+			return nil, lengthErr
+		}
+		b := make([]byte, 8)
+		order.PutUint64(b, n)
+		return b, nil
+	default:
+		return nil, errors.New("非法字节长度")
+	}
+}

+ 28 - 0
bxx/BxUtils_test.go

@@ -0,0 +1,28 @@
+package bx
+
+import (
+	"encoding/binary"
+	"fmt"
+	"testing"
+)
+
+func TestCRC16(t *testing.T) {
+	data := []byte{
+		0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, //帧头
+		//包头
+		0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x02, 0x0D, 0x00,
+		//数据域
+		0xA2, 0x03, 0x01, 0x08, 0x00, 0x13, 0x20, 0x01, 0x25, 0x11, 0x17, 0x26, 0x05,
+		0xB0, 0x3F, //校验码,报头和数据域
+		0x5A, //帧尾
+	}
+	//
+	//
+	crc16 := CRC16(data[:len(data)-3], 8, len(data))
+	real := binary.LittleEndian.Uint16(data[len(data)-3 : len(data)-1])
+	if crc16 != real {
+		t.Error("不通过!结果:", crc16, " 预期:", real)
+	} else {
+		fmt.Println("结果:", crc16, " 预期:", real)
+	}
+}

+ 31 - 0
bxx/bxError.go

@@ -0,0 +1,31 @@
+package bx
+
+type BxError struct {
+	ErrorCode   byte
+	Name        string
+	Description string
+}
+
+var bxErrors = []BxError{
+	BxError{0, "ERR_NO", "No Err"},
+	BxError{1, "ERR_OUTOFGROUP", "Command Group Err"},
+	BxError{2, "ERR_NOCMD", "Not Found"},
+	BxError{3, "ERR_BUSY", "The Controller is busy now"},
+	BxError{4, "ERR_MEMORYVOLUME", "Out of the Memory Volume"},
+	BxError{5, "ERR_CHECKSUM", "CRC16 Checksum Err"},
+	BxError{6, "ERR_FILENOTEXIST", "File Not Exist"},
+	BxError{7, "ERR_FLASH", "Flash Access Err"},
+	BxError{8, "ERR_FILE_DOWNLOAD", "File Download Err"},
+	BxError{9, "ERR_FILE_NAME", "Filename Err"},
+	BxError{10, "ERR_FILE_TYPE", "File type Err"},
+	BxError{11, "ERR_FILE_CRC16", "File CRC16 Err"},
+	BxError{12, "ERR_FONT_NOT_EXIST", "Font Library Not Exist"},
+	BxError{13, "ERR_FIRMWARE_TYPE", "Firmware Type Err (Check the controller type)"},
+	BxError{14, "ERR_DATE_TIME_FORMAT", "Date Time format Err"},
+	BxError{15, "ERR_FILE_EXIST", "File Exist for File overwrite"},
+	BxError{16, "ERR_FILE_BLOCK_NUM", "File block number Err"},
+}
+
+func GetError(code byte) BxError {
+	return bxErrors[code]
+}

+ 196 - 0
ccpd_process.py

@@ -0,0 +1,196 @@
+import os
+import shutil
+import cv2
+import numpy as np
+def allFilePath(rootPath,allFIleList):
+    fileList = os.listdir(rootPath)
+    for temp in fileList:
+        if os.path.isfile(os.path.join(rootPath,temp)):
+            if temp.endswith(".jpg"):
+                allFIleList.append(os.path.join(rootPath,temp))
+        else:
+            allFilePath(os.path.join(rootPath,temp),allFIleList)
+
+def order_points(pts):
+    # initialzie a list of coordinates that will be ordered
+    # such that the first entry in the list is the top-left,
+    # the second entry is the top-right, the third is the
+    # bottom-right, and the fourth is the bottom-left
+    pts=pts[:4,:]
+    rect = np.zeros((5, 2), dtype = "float32")
+ 
+    # the top-left point will have the smallest sum, whereas
+    # the bottom-right point will have the largest sum
+    s = pts.sum(axis = 1)
+    rect[0] = pts[np.argmin(s)]
+    rect[2] = pts[np.argmax(s)]
+ 
+    # now, compute the difference between the points, the
+    # top-right point will have the smallest difference,
+    # whereas the bottom-left will have the largest difference
+    diff = np.diff(pts, axis = 1)
+    rect[1] = pts[np.argmin(diff)]
+    rect[3] = pts[np.argmax(diff)]
+ 
+    # return the ordered coordinates
+    return rect
+
+def get_partical_ccpd():
+    ccpd_dir = r"/mnt/Gpan/BaiduNetdiskDownload/CCPD1/CCPD2020/ccpd_green"
+    save_Path = r"ccpd/green_plate"
+    folder_list = os.listdir(ccpd_dir)
+    for folder_name in folder_list:
+        count=0
+        folder_path = os.path.join(ccpd_dir,folder_name)
+        if os.path.isfile(folder_path):
+            continue
+        if folder_name == "ccpd_fn":
+            continue
+        name_list = os.listdir(folder_path)
+        
+        save_folder=save_Path
+        if not os.path.exists(save_folder):
+            os.mkdir(save_folder)
+
+        for name in name_list:
+            file_path = os.path.join(folder_path,name)
+            count+=1
+            if count>1000:
+                break
+            new_file_path =os.path.join(save_folder,name)
+            shutil.move(file_path,new_file_path)
+            print(count,new_file_path)
+        
+def get_rect_and_landmarks(img_path):
+   file_name = img_path.split("/")[-1].split("-")
+   landmarks_np =np.zeros((5,2))
+   rect = file_name[2].split("_")
+   landmarks=file_name[3].split("_")
+   rect_str = "&".join(rect)
+   landmarks_str= "&".join(landmarks)
+   rect= rect_str.split("&")
+   landmarks=landmarks_str.split("&")
+   rect=[int(x) for x in rect]
+   landmarks=[int(x) for x in landmarks]
+   for i in range(4):
+        landmarks_np[i][0]=landmarks[2*i]
+        landmarks_np[i][1]=landmarks[2*i+1]
+#    middle_landmark_w =int((landmarks[4]+landmarks[6])/2) 
+#    middle_landmark_h =int((landmarks[5]+landmarks[7])/2) 
+#    landmarks.append(middle_landmark_w)
+#    landmarks.append(middle_landmark_h)
+   landmarks_np_new=order_points(landmarks_np)
+#    landmarks_np_new[4]=np.array([middle_landmark_w,middle_landmark_h])
+   return rect,landmarks,landmarks_np_new
+
+def x1x2y1y2_yolo(rect,landmarks,img):
+    h,w,c =img.shape
+    rect[0] = max(0, rect[0])
+    rect[1] = max(0, rect[1])
+    rect[2] = min(w - 1, rect[2]-rect[0])
+    rect[3] = min(h - 1, rect[3]-rect[1])
+    annotation = np.zeros((1, 14))
+    annotation[0, 0] = (rect[0] + rect[2] / 2) / w  # cx
+    annotation[0, 1] = (rect[1] + rect[3] / 2) / h  # cy
+    annotation[0, 2] = rect[2] / w  # w
+    annotation[0, 3] = rect[3] / h  # h
+
+    annotation[0, 4] = landmarks[0] / w  # l0_x
+    annotation[0, 5] = landmarks[1] / h  # l0_y
+    annotation[0, 6] = landmarks[2] / w  # l1_x
+    annotation[0, 7] = landmarks[3] / h  # l1_y
+    annotation[0, 8] = landmarks[4] / w  # l2_x
+    annotation[0, 9] = landmarks[5] / h # l2_y
+    annotation[0, 10] = landmarks[6] / w  # l3_x
+    annotation[0, 11] = landmarks[7] / h  # l3_y
+    # annotation[0, 12] = landmarks[8] / w  # l4_x
+    # annotation[0, 13] = landmarks[9] / h  # l4_y
+    return annotation
+
+def xywh2yolo(rect,landmarks_sort,img):
+    h,w,c =img.shape
+    rect[0] = max(0, rect[0])
+    rect[1] = max(0, rect[1])
+    rect[2] = min(w - 1, rect[2]-rect[0])
+    rect[3] = min(h - 1, rect[3]-rect[1])
+    annotation = np.zeros((1, 12))
+    annotation[0, 0] = (rect[0] + rect[2] / 2) / w  # cx
+    annotation[0, 1] = (rect[1] + rect[3] / 2) / h  # cy
+    annotation[0, 2] = rect[2] / w  # w
+    annotation[0, 3] = rect[3] / h  # h
+
+    annotation[0, 4] = landmarks_sort[0][0] / w  # l0_x
+    annotation[0, 5] = landmarks_sort[0][1] / h  # l0_y
+    annotation[0, 6] = landmarks_sort[1][0] / w  # l1_x
+    annotation[0, 7] = landmarks_sort[1][1] / h  # l1_y
+    annotation[0, 8] = landmarks_sort[2][0] / w  # l2_x
+    annotation[0, 9] = landmarks_sort[2][1] / h # l2_y
+    annotation[0, 10] = landmarks_sort[3][0] / w  # l3_x
+    annotation[0, 11] = landmarks_sort[3][1] / h  # l3_y
+    # annotation[0, 12] = landmarks_sort[4][0] / w  # l4_x
+    # annotation[0, 13] = landmarks_sort[4][1] / h  # l4_y
+    return annotation
+
+def yolo2x1y1x2y2(annotation,img):
+    h,w,c = img.shape
+    rect= annotation[:,0:4].squeeze().tolist()
+    landmarks=annotation[:,4:].squeeze().tolist()
+    rect_w = w*rect[2]
+    rect_h =h*rect[3]
+    rect_x =int(rect[0]*w-rect_w/2)
+    rect_y = int(rect[1]*h-rect_h/2)
+    new_rect=[rect_x,rect_y,rect_x+rect_w,rect_y+rect_h]
+    for i in range(5):
+        landmarks[2*i]=landmarks[2*i]*w
+        landmarks[2*i+1]=landmarks[2*i+1]*h
+    return new_rect,landmarks
+
+def write_lable(file_path):
+    pass
+
+
+if __name__ == '__main__':
+   file_root = r"ccpd"
+   file_list=[]
+   count=0
+   allFilePath(file_root,file_list)
+   for img_path in file_list:
+        count+=1
+        # img_path = r"ccpd_yolo_test/02-90_85-173&466_452&541-452&553_176&556_178&463_454&460-0_0_6_26_15_26_32-68-53.jpg"
+        text_path= img_path.replace(".jpg",".txt")
+        img =cv2.imread(img_path)
+        rect,landmarks,landmarks_sort=get_rect_and_landmarks(img_path)
+        # annotation=x1x2y1y2_yolo(rect,landmarks,img)
+        annotation=xywh2yolo(rect,landmarks_sort,img)
+        str_label = "0 "
+        for i in range(len(annotation[0])):
+                str_label = str_label + " " + str(annotation[0][i])
+        str_label = str_label.replace('[', '').replace(']', '')
+        str_label = str_label.replace(',', '') + '\n'
+        with open(text_path,"w") as f:
+                f.write(str_label)
+        print(count,img_path)
+    # get_partical_ccpd()
+    # file_root = r"ccpd/green_plate"
+    # file_list=[]
+    # allFilePath(file_root,file_list)
+    # count=0
+    # for img_path in file_list:
+    #     img_name = img_path.split(os.sep)[-1]
+    #     if not "&" in img_name:
+    #         count+=1
+    #         os.remove(img_path)
+    #         print(count,img_path)
+
+        # new_rect,new_landmarks=yolo2x1y1x2y2(annotation,img)
+        # rect= [int(x) for x in  new_rect]
+        # cv2.rectangle(img,(rect[0],rect[1]),(rect[2],rect[3]),(255,0,0),2)
+        # colors=[(0,255,0),(0,255,255),(255,255,0),(255,255,255),(255,0,255)] #绿 黄 青 白 
+        # for i in range(5):
+        #   cv2.circle(img,(landmarks[2*i],landmarks[2*i+1]),2,colors[i],2)
+        # cv2.imwrite("1.jpg",img)
+    #    print(rect,landmarks)
+        # get_partical_ccpd()
+    
+        
+        

+ 21 - 0
data/argoverse_hd.yaml

@@ -0,0 +1,21 @@
+# Argoverse-HD dataset (ring-front-center camera) http://www.cs.cmu.edu/~mengtial/proj/streaming/
+# Train command: python train.py --data argoverse_hd.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /argoverse
+#     /yolov5
+
+
+# download command/URL (optional)
+download: bash data/scripts/get_argoverse_hd.sh
+
+# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
+train: ../argoverse/Argoverse-1.1/images/train/  # 39384 images
+val: ../argoverse/Argoverse-1.1/images/val/  # 15062 iamges
+test: ../argoverse/Argoverse-1.1/images/test/  # Submit to: https://eval.ai/web/challenges/challenge-page/800/overview
+
+# number of classes
+nc: 8
+
+# class names
+names: [ 'person',  'bicycle',  'car',  'motorcycle',  'bus',  'truck',  'traffic_light',  'stop_sign' ]

+ 35 - 0
data/coco.yaml

@@ -0,0 +1,35 @@
+# COCO 2017 dataset http://cocodataset.org
+# Train command: python train.py --data coco.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /coco
+#     /yolov5
+
+
+# download command/URL (optional)
+download: bash data/scripts/get_coco.sh
+
+# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
+train: ../coco/train2017.txt  # 118287 images
+val: ../coco/val2017.txt  # 5000 images
+test: ../coco/test-dev2017.txt  # 20288 of 40670 images, submit to https://competitions.codalab.org/competitions/20794
+
+# number of classes
+nc: 80
+
+# class names
+names: [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
+         'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
+         'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
+         'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
+         'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
+         'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
+         'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
+         'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
+         'hair drier', 'toothbrush' ]
+
+# Print classes
+# with open('data/coco.yaml') as f:
+#   d = yaml.load(f, Loader=yaml.FullLoader)  # dict
+#   for i, x in enumerate(d['names']):
+#     print(i, x)

+ 28 - 0
data/coco128.yaml

@@ -0,0 +1,28 @@
+# COCO 2017 dataset http://cocodataset.org - first 128 training images
+# Train command: python train.py --data coco128.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /coco128
+#     /yolov5
+
+
+# download command/URL (optional)
+download: https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128.zip
+
+# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
+train: ../coco128/images/train2017/  # 128 images
+val: ../coco128/images/train2017/  # 128 images
+
+# number of classes
+nc: 80
+
+# class names
+names: [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
+         'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
+         'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
+         'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
+         'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
+         'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
+         'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
+         'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
+         'hair drier', 'toothbrush' ]

+ 38 - 0
data/hyp.finetune.yaml

@@ -0,0 +1,38 @@
+# Hyperparameters for VOC finetuning
+# python train.py --batch 64 --weights yolov5m.pt --data voc.yaml --img 512 --epochs 50
+# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials
+
+
+# Hyperparameter Evolution Results
+# Generations: 306
+#                   P         R     mAP.5 mAP.5:.95       box       obj       cls
+# Metrics:        0.6     0.936     0.896     0.684    0.0115   0.00805   0.00146
+
+lr0: 0.0032
+lrf: 0.12
+momentum: 0.843
+weight_decay: 0.00036
+warmup_epochs: 2.0
+warmup_momentum: 0.5
+warmup_bias_lr: 0.05
+box: 0.0296
+cls: 0.243
+cls_pw: 0.631
+obj: 0.301
+obj_pw: 0.911
+iou_t: 0.2
+anchor_t: 2.91
+# anchors: 3.63
+fl_gamma: 0.0
+hsv_h: 0.0138
+hsv_s: 0.664
+hsv_v: 0.464
+degrees: 0.373
+translate: 0.245
+scale: 0.898
+shear: 0.602
+perspective: 0.0
+flipud: 0.00856
+fliplr: 0.5
+mosaic: 1.0
+mixup: 0.243

+ 34 - 0
data/hyp.scratch.yaml

@@ -0,0 +1,34 @@
+# Hyperparameters for COCO training from scratch
+# python train.py --batch 40 --cfg yolov5m.yaml --weights '' --data coco.yaml --img 640 --epochs 300
+# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials
+
+
+lr0: 0.01  # initial learning rate (SGD=1E-2, Adam=1E-3)
+lrf: 0.2  # final OneCycleLR learning rate (lr0 * lrf)
+momentum: 0.937  # SGD momentum/Adam beta1
+weight_decay: 0.0005  # optimizer weight decay 5e-4
+warmup_epochs: 3.0  # warmup epochs (fractions ok)
+warmup_momentum: 0.8  # warmup initial momentum
+warmup_bias_lr: 0.1  # warmup initial bias lr
+box: 0.05  # box loss gain
+cls: 0.5  # cls loss gain
+landmark: 0.005 # landmark loss gain
+cls_pw: 1.0  # cls BCELoss positive_weight
+obj: 1.0  # obj loss gain (scale with pixels)
+obj_pw: 1.0  # obj BCELoss positive_weight
+iou_t: 0.20  # IoU training threshold
+anchor_t: 4.0  # anchor-multiple threshold
+# anchors: 3  # anchors per output layer (0 to ignore)
+fl_gamma: 0.0  # focal loss gamma (efficientDet default gamma=1.5)
+hsv_h: 0.015  # image HSV-Hue augmentation (fraction)
+hsv_s: 0.7  # image HSV-Saturation augmentation (fraction)
+hsv_v: 0.4  # image HSV-Value augmentation (fraction)
+degrees: 0.0  # image rotation (+/- deg)
+translate: 0.1  # image translation (+/- fraction)
+scale: 0.5  # image scale (+/- gain)
+shear: 0.5  # image shear (+/- deg)
+perspective: 0.0  # image perspective (+/- fraction), range 0-0.001
+flipud: 0.0  # image flip up-down (probability)
+fliplr: 0.5  # image flip left-right (probability)
+mosaic: 0.5  # image mosaic (probability)
+mixup: 0.0  # image mixup (probability)

+ 20 - 0
data/plateAndCar.yaml

@@ -0,0 +1,20 @@
+# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC/
+# Train command: python train.py --data voc.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /VOC
+#     /yolov5
+
+
+# download command/URL (optional)
+download: bash data/scripts/get_voc.sh
+
+# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
+train: /mnt/Gpan/Mydata/pytorch
+Porject/datasets/ccpd/train_car_plate/train_detect  
+val: /mnt/Gpan/Mydata/pytorchPorject/datasets/ccpd/train_car_plate/val_detect
+# number of classes
+nc: 3
+
+# class names
+names: [ 'single_plate','double_plate','car']

+ 150 - 0
data/retinaface2yolo.py

@@ -0,0 +1,150 @@
+import os
+import os.path
+import sys
+import torch
+import torch.utils.data as data
+import cv2
+import numpy as np
+
+class WiderFaceDetection(data.Dataset):
+    def __init__(self, txt_path, preproc=None):
+        self.preproc = preproc
+        self.imgs_path = []
+        self.words = []
+        f = open(txt_path,'r')
+        lines = f.readlines()
+        isFirst = True
+        labels = []
+        for line in lines:
+            line = line.rstrip()
+            if line.startswith('#'):
+                if isFirst is True:
+                    isFirst = False
+                else:
+                    labels_copy = labels.copy()
+                    self.words.append(labels_copy)
+                    labels.clear()
+                path = line[2:]
+                path = txt_path.replace('label.txt','images/') + path
+                self.imgs_path.append(path)
+            else:
+                line = line.split(' ')
+                label = [float(x) for x in line]
+                labels.append(label)
+
+        self.words.append(labels)
+
+    def __len__(self):
+        return len(self.imgs_path)
+
+    def __getitem__(self, index):
+        img = cv2.imread(self.imgs_path[index])
+        height, width, _ = img.shape
+
+        labels = self.words[index]
+        annotations = np.zeros((0, 15))
+        if len(labels) == 0:
+            return annotations
+        for idx, label in enumerate(labels):
+            annotation = np.zeros((1, 15))
+            # bbox
+            annotation[0, 0] = label[0]  # x1
+            annotation[0, 1] = label[1]  # y1
+            annotation[0, 2] = label[0] + label[2]  # x2
+            annotation[0, 3] = label[1] + label[3]  # y2
+
+            # landmarks
+            annotation[0, 4] = label[4]    # l0_x
+            annotation[0, 5] = label[5]    # l0_y
+            annotation[0, 6] = label[7]    # l1_x
+            annotation[0, 7] = label[8]    # l1_y
+            annotation[0, 8] = label[10]   # l2_x
+            annotation[0, 9] = label[11]   # l2_y
+            annotation[0, 10] = label[13]  # l3_x
+            annotation[0, 11] = label[14]  # l3_y
+            annotation[0, 12] = label[16]  # l4_x
+            annotation[0, 13] = label[17]  # l4_y
+            if (annotation[0, 4]<0):
+                annotation[0, 14] = -1
+            else:
+                annotation[0, 14] = 1
+
+            annotations = np.append(annotations, annotation, axis=0)
+        target = np.array(annotations)
+        if self.preproc is not None:
+            img, target = self.preproc(img, target)
+
+        return torch.from_numpy(img), target
+
+def detection_collate(batch):
+    """Custom collate fn for dealing with batches of images that have a different
+    number of associated object annotations (bounding boxes).
+
+    Arguments:
+        batch: (tuple) A tuple of tensor images and lists of annotations
+
+    Return:
+        A tuple containing:
+            1) (tensor) batch of images stacked on their 0 dim
+            2) (list of tensors) annotations for a given image are stacked on 0 dim
+    """
+    targets = []
+    imgs = []
+    for _, sample in enumerate(batch):
+        for _, tup in enumerate(sample):
+            if torch.is_tensor(tup):
+                imgs.append(tup)
+            elif isinstance(tup, type(np.empty(0))):
+                annos = torch.from_numpy(tup).float()
+                targets.append(annos)
+
+    return (torch.stack(imgs, 0), targets)
+
+save_path = '/ssd_1t/derron/yolov5-face/data/widerface/train'
+aa=WiderFaceDetection("/ssd_1t/derron/yolov5-face/data/widerface/widerface/train/label.txt")
+for i in range(len(aa.imgs_path)):
+    print(i, aa.imgs_path[i])
+    img = cv2.imread(aa.imgs_path[i])
+    base_img = os.path.basename(aa.imgs_path[i])
+    base_txt = os.path.basename(aa.imgs_path[i])[:-4] +".txt"
+    save_img_path = os.path.join(save_path, base_img)
+    save_txt_path = os.path.join(save_path, base_txt)
+    with open(save_txt_path, "w") as f:
+        height, width, _ = img.shape
+        labels = aa.words[i]
+        annotations = np.zeros((0, 14))
+        if len(labels) == 0:
+            continue
+        for idx, label in enumerate(labels):
+            annotation = np.zeros((1, 14))
+            # bbox
+            label[0] = max(0, label[0])
+            label[1] = max(0, label[1])
+            label[2] = min(width -  1, label[2])
+            label[3] = min(height - 1, label[3])
+            annotation[0, 0] = (label[0] + label[2] / 2) / width  # cx
+            annotation[0, 1] = (label[1] + label[3] / 2) / height  # cy
+            annotation[0, 2] = label[2] / width  # w
+            annotation[0, 3] = label[3] / height  # h
+            #if (label[2] -label[0]) < 8 or (label[3] - label[1]) < 8:
+            #    img[int(label[1]):int(label[3]), int(label[0]):int(label[2])] = 127
+            #    continue
+            # landmarks
+            annotation[0, 4] = label[4] / width  # l0_x
+            annotation[0, 5] = label[5] / height  # l0_y
+            annotation[0, 6] = label[7] / width  # l1_x
+            annotation[0, 7] = label[8]  / height # l1_y
+            annotation[0, 8] = label[10] / width  # l2_x
+            annotation[0, 9] = label[11] / height  # l2_y
+            annotation[0, 10] = label[13] / width  # l3_x
+            annotation[0, 11] = label[14] / height  # l3_y
+            annotation[0, 12] = label[16] / width  # l4_x
+            annotation[0, 13] = label[17] / height  # l4_y
+            str_label="0 "
+            for i in range(len(annotation[0])):
+                str_label =str_label+" "+str(annotation[0][i])
+            str_label = str_label.replace('[', '').replace(']', '')
+            str_label = str_label.replace(',', '') + '\n'
+            f.write(str_label)
+    cv2.imwrite(save_img_path, img)
+

+ 62 - 0
data/scripts/get_argoverse_hd.sh

@@ -0,0 +1,62 @@
+#!/bin/bash
+# Argoverse-HD dataset (ring-front-center camera) http://www.cs.cmu.edu/~mengtial/proj/streaming/
+# Download command: bash data/scripts/get_argoverse_hd.sh
+# Train command: python train.py --data argoverse_hd.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /argoverse
+#     /yolov5
+
+# Download/unzip images
+d='../argoverse/' # unzip directory
+mkdir $d
+url=https://argoverse-hd.s3.us-east-2.amazonaws.com/
+f=Argoverse-HD-Full.zip
+curl -L $url$f -o $f && unzip -q $f -d $d && rm $f &# download, unzip, remove in background
+wait                                              # finish background tasks
+
+cd ../argoverse/Argoverse-1.1/
+ln -s tracking images
+
+cd ../Argoverse-HD/annotations/
+
+python3 - "$@" <<END
+import json
+from pathlib import Path
+
+annotation_files = ["train.json", "val.json"]
+print("Converting annotations to YOLOv5 format...")
+
+for val in annotation_files:
+    a = json.load(open(val, "rb"))
+
+    label_dict = {}
+    for annot in a['annotations']:
+        img_id = annot['image_id']
+        img_name = a['images'][img_id]['name']
+        img_label_name = img_name[:-3] + "txt"
+
+        obj_class = annot['category_id']
+        x_center, y_center, width, height = annot['bbox']
+        x_center = (x_center + width / 2) / 1920.  # offset and scale
+        y_center = (y_center + height / 2) / 1200.  # offset and scale
+        width /= 1920.  # scale
+        height /= 1200.  # scale
+
+        img_dir = "./labels/" + a['seq_dirs'][a['images'][annot['image_id']]['sid']]
+
+        Path(img_dir).mkdir(parents=True, exist_ok=True)
+
+        if img_dir + "/" + img_label_name not in label_dict:
+            label_dict[img_dir + "/" + img_label_name] = []
+
+        label_dict[img_dir + "/" + img_label_name].append(f"{obj_class} {x_center} {y_center} {width} {height}\n")
+
+    for filename in label_dict:
+        with open(filename, "w") as file:
+            for string in label_dict[filename]:
+                file.write(string)
+
+END
+
+mv ./labels ../../Argoverse-1.1/

+ 27 - 0
data/scripts/get_coco.sh

@@ -0,0 +1,27 @@
+#!/bin/bash
+# COCO 2017 dataset http://cocodataset.org
+# Download command: bash data/scripts/get_coco.sh
+# Train command: python train.py --data coco.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /coco
+#     /yolov5
+
+# Download/unzip labels
+d='../' # unzip directory
+url=https://github.com/ultralytics/yolov5/releases/download/v1.0/
+f='coco2017labels.zip' # or 'coco2017labels-segments.zip', 68 MB
+echo 'Downloading' $url$f ' ...'
+curl -L $url$f -o $f && unzip -q $f -d $d && rm $f & # download, unzip, remove in background
+
+# Download/unzip images
+d='../coco/images' # unzip directory
+url=http://images.cocodataset.org/zips/
+f1='train2017.zip' # 19G, 118k images
+f2='val2017.zip'   # 1G, 5k images
+f3='test2017.zip'  # 7G, 41k images (optional)
+for f in $f1 $f2; do
+  echo 'Downloading' $url$f '...'
+  curl -L $url$f -o $f && unzip -q $f -d $d && rm $f & # download, unzip, remove in background
+done
+wait # finish background tasks

+ 139 - 0
data/scripts/get_voc.sh

@@ -0,0 +1,139 @@
+#!/bin/bash
+# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC/
+# Download command: bash data/scripts/get_voc.sh
+# Train command: python train.py --data voc.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /VOC
+#     /yolov5
+
+start=$(date +%s)
+mkdir -p ../tmp
+cd ../tmp/
+
+# Download/unzip images and labels
+d='.' # unzip directory
+url=https://github.com/ultralytics/yolov5/releases/download/v1.0/
+f1=VOCtrainval_06-Nov-2007.zip # 446MB, 5012 images
+f2=VOCtest_06-Nov-2007.zip     # 438MB, 4953 images
+f3=VOCtrainval_11-May-2012.zip # 1.95GB, 17126 images
+for f in $f3 $f2 $f1; do
+  echo 'Downloading' $url$f '...' 
+  curl -L $url$f -o $f && unzip -q $f -d $d && rm $f & # download, unzip, remove in background
+done
+wait # finish background tasks
+
+end=$(date +%s)
+runtime=$((end - start))
+echo "Completed in" $runtime "seconds"
+
+echo "Splitting dataset..."
+python3 - "$@" <<END
+import xml.etree.ElementTree as ET
+import pickle
+import os
+from os import listdir, getcwd
+from os.path import join
+
+sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test')]
+
+classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
+
+
+def convert(size, box):
+    dw = 1./(size[0])
+    dh = 1./(size[1])
+    x = (box[0] + box[1])/2.0 - 1
+    y = (box[2] + box[3])/2.0 - 1
+    w = box[1] - box[0]
+    h = box[3] - box[2]
+    x = x*dw
+    w = w*dw
+    y = y*dh
+    h = h*dh
+    return (x,y,w,h)
+
+def convert_annotation(year, image_id):
+    in_file = open('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id))
+    out_file = open('VOCdevkit/VOC%s/labels/%s.txt'%(year, image_id), 'w')
+    tree=ET.parse(in_file)
+    root = tree.getroot()
+    size = root.find('size')
+    w = int(size.find('width').text)
+    h = int(size.find('height').text)
+
+    for obj in root.iter('object'):
+        difficult = obj.find('difficult').text
+        cls = obj.find('name').text
+        if cls not in classes or int(difficult)==1:
+            continue
+        cls_id = classes.index(cls)
+        xmlbox = obj.find('bndbox')
+        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))
+        bb = convert((w,h), b)
+        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
+
+wd = getcwd()
+
+for year, image_set in sets:
+    if not os.path.exists('VOCdevkit/VOC%s/labels/'%(year)):
+        os.makedirs('VOCdevkit/VOC%s/labels/'%(year))
+    image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()
+    list_file = open('%s_%s.txt'%(year, image_set), 'w')
+    for image_id in image_ids:
+        list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))
+        convert_annotation(year, image_id)
+    list_file.close()
+
+END
+
+cat 2007_train.txt 2007_val.txt 2012_train.txt 2012_val.txt >train.txt
+cat 2007_train.txt 2007_val.txt 2007_test.txt 2012_train.txt 2012_val.txt >train.all.txt
+
+python3 - "$@" <<END
+
+import shutil
+import os
+os.system('mkdir ../VOC/')
+os.system('mkdir ../VOC/images')
+os.system('mkdir ../VOC/images/train')
+os.system('mkdir ../VOC/images/val')
+
+os.system('mkdir ../VOC/labels')
+os.system('mkdir ../VOC/labels/train')
+os.system('mkdir ../VOC/labels/val')
+
+import os
+print(os.path.exists('../tmp/train.txt'))
+f = open('../tmp/train.txt', 'r')
+lines = f.readlines()
+
+for line in lines:
+    line = "/".join(line.split('/')[-5:]).strip()
+    if (os.path.exists("../" + line)):
+        os.system("cp ../"+ line + " ../VOC/images/train")
+        
+    line = line.replace('JPEGImages', 'labels')
+    line = line.replace('jpg', 'txt')
+    if (os.path.exists("../" + line)):
+        os.system("cp ../"+ line + " ../VOC/labels/train")
+
+
+print(os.path.exists('../tmp/2007_test.txt'))
+f = open('../tmp/2007_test.txt', 'r')
+lines = f.readlines()
+
+for line in lines:
+    line = "/".join(line.split('/')[-5:]).strip()
+    if (os.path.exists("../" + line)):
+        os.system("cp ../"+ line + " ../VOC/images/val")
+        
+    line = line.replace('JPEGImages', 'labels')
+    line = line.replace('jpg', 'txt')
+    if (os.path.exists("../" + line)):
+        os.system("cp ../"+ line + " ../VOC/labels/val")
+
+END
+
+rm -rf ../tmp # remove temporary directory
+echo "VOC download done."

+ 176 - 0
data/train2yolo.py

@@ -0,0 +1,176 @@
+import os.path
+import sys
+import torch
+import torch.utils.data as data
+import cv2
+import numpy as np
+
+
+class WiderFaceDetection(data.Dataset):
+    def __init__(self, txt_path, preproc=None):
+        self.preproc = preproc
+        self.imgs_path = []
+        self.words = []
+        f = open(txt_path, 'r')
+        lines = f.readlines()
+        isFirst = True
+        labels = []
+        for line in lines:
+            line = line.rstrip()
+            if line.startswith('#'):
+                if isFirst is True:
+                    isFirst = False
+                else:
+                    labels_copy = labels.copy()
+                    self.words.append(labels_copy)
+                    labels.clear()
+                path = line[2:]
+                path = txt_path.replace('label.txt', 'images/') + path
+                self.imgs_path.append(path)
+            else:
+                line = line.split(' ')
+                label = [float(x) for x in line]
+                labels.append(label)
+
+        self.words.append(labels)
+
+    def __len__(self):
+        return len(self.imgs_path)
+
+    def __getitem__(self, index):
+        img = cv2.imread(self.imgs_path[index])
+        height, width, _ = img.shape
+
+        labels = self.words[index]
+        annotations = np.zeros((0, 15))
+        if len(labels) == 0:
+            return annotations
+        for idx, label in enumerate(labels):
+            annotation = np.zeros((1, 15))
+            # bbox
+            annotation[0, 0] = label[0]  # x1
+            annotation[0, 1] = label[1]  # y1
+            annotation[0, 2] = label[0] + label[2]  # x2
+            annotation[0, 3] = label[1] + label[3]  # y2
+
+            # landmarks
+            annotation[0, 4] = label[4]    # l0_x
+            annotation[0, 5] = label[5]    # l0_y
+            annotation[0, 6] = label[7]    # l1_x
+            annotation[0, 7] = label[8]    # l1_y
+            annotation[0, 8] = label[10]   # l2_x
+            annotation[0, 9] = label[11]   # l2_y
+            annotation[0, 10] = label[13]  # l3_x
+            annotation[0, 11] = label[14]  # l3_y
+            annotation[0, 12] = label[16]  # l4_x
+            annotation[0, 13] = label[17]  # l4_y
+            if annotation[0, 4] < 0:
+                annotation[0, 14] = -1
+            else:
+                annotation[0, 14] = 1
+
+            annotations = np.append(annotations, annotation, axis=0)
+        target = np.array(annotations)
+        if self.preproc is not None:
+            img, target = self.preproc(img, target)
+
+        return torch.from_numpy(img), target
+
+
+def detection_collate(batch):
+    """Custom collate fn for dealing with batches of images that have a different
+    number of associated object annotations (bounding boxes).
+
+    Arguments:
+        batch: (tuple) A tuple of tensor images and lists of annotations
+
+    Return:
+        A tuple containing:
+            1) (tensor) batch of images stacked on their 0 dim
+            2) (list of tensors) annotations for a given image are stacked on 0 dim
+    """
+    targets = []
+    imgs = []
+    for _, sample in enumerate(batch):
+        for _, tup in enumerate(sample):
+            if torch.is_tensor(tup):
+                imgs.append(tup)
+            elif isinstance(tup, type(np.empty(0))):
+                annos = torch.from_numpy(tup).float()
+                targets.append(annos)
+
+    return torch.stack(imgs, 0), targets
+
+
+if __name__ == '__main__':
+    if len(sys.argv) == 1:
+        print('Missing path to WIDERFACE train folder.')
+        print('Run command: python3 train2yolo.py /path/to/original/widerface/train [/path/to/save/widerface/train]')
+        exit(1)
+    elif len(sys.argv) > 3:
+        print('Too many arguments were provided.')
+        print('Run command: python3 train2yolo.py /path/to/original/widerface/train [/path/to/save/widerface/train]')
+        exit(1)
+    original_path = sys.argv[1]
+
+    if len(sys.argv) == 2:
+        if not os.path.isdir('widerface'):
+            os.mkdir('widerface')
+        if not os.path.isdir('widerface/train'):
+            os.mkdir('widerface/train')
+
+        save_path = 'widerface/train'
+    else:
+        save_path = sys.argv[2]
+
+    if not os.path.isfile(os.path.join(original_path, 'label.txt')):
+        print('Missing label.txt file.')
+        exit(1)
+
+    aa = WiderFaceDetection(os.path.join(original_path, 'label.txt'))
+
+    for i in range(len(aa.imgs_path)):
+        print(i, aa.imgs_path[i])
+        img = cv2.imread(aa.imgs_path[i])
+        base_img = os.path.basename(aa.imgs_path[i])
+        base_txt = os.path.basename(aa.imgs_path[i])[:-4] + ".txt"
+        save_img_path = os.path.join(save_path, base_img)
+        save_txt_path = os.path.join(save_path, base_txt)
+        with open(save_txt_path, "w") as f:
+            height, width, _ = img.shape
+            labels = aa.words[i]
+            annotations = np.zeros((0, 14))
+            if len(labels) == 0:
+                continue
+            for idx, label in enumerate(labels):
+                annotation = np.zeros((1, 14))
+                # bbox
+                label[0] = max(0, label[0])
+                label[1] = max(0, label[1])
+                label[2] = min(width - 1, label[2])
+                label[3] = min(height - 1, label[3])
+                annotation[0, 0] = (label[0] + label[2] / 2) / width  # cx
+                annotation[0, 1] = (label[1] + label[3] / 2) / height  # cy
+                annotation[0, 2] = label[2] / width  # w
+                annotation[0, 3] = label[3] / height  # h
+                #if (label[2] -label[0]) < 8 or (label[3] - label[1]) < 8:
+                #    img[int(label[1]):int(label[3]), int(label[0]):int(label[2])] = 127
+                #    continue
+                # landmarks
+                annotation[0, 4] = label[4] / width  # l0_x
+                annotation[0, 5] = label[5] / height  # l0_y
+                annotation[0, 6] = label[7] / width  # l1_x
+                annotation[0, 7] = label[8] / height  # l1_y
+                annotation[0, 8] = label[10] / width  # l2_x
+                annotation[0, 9] = label[11] / height  # l2_y
+                annotation[0, 10] = label[13] / width  # l3_x
+                annotation[0, 11] = label[14] / height  # l3_y
+                annotation[0, 12] = label[16] / width  # l4_x
+                annotation[0, 13] = label[17] / height  # l4_yca
+                str_label = "0 "
+                for i in range(len(annotation[0])):
+                    str_label = str_label + " " + str(annotation[0][i])
+                str_label = str_label.replace('[', '').replace(']', '')
+                str_label = str_label.replace(',', '') + '\n'
+                f.write(str_label)
+        cv2.imwrite(save_img_path, img)

+ 88 - 0
data/val2yolo.py

@@ -0,0 +1,88 @@
+import os
+import cv2
+import numpy as np
+import shutil
+import sys
+from tqdm import tqdm
+
+
+def xywh2xxyy(box):
+    x1 = box[0]
+    y1 = box[1]
+    x2 = box[0] + box[2]
+    y2 = box[1] + box[3]
+    return x1, x2, y1, y2
+
+
+def convert(size, box):
+    dw = 1. / (size[0])
+    dh = 1. / (size[1])
+    x = (box[0] + box[1]) / 2.0 - 1
+    y = (box[2] + box[3]) / 2.0 - 1
+    w = box[1] - box[0]
+    h = box[3] - box[2]
+    x = x * dw
+    w = w * dw
+    y = y * dh
+    h = h * dh
+    return x, y, w, h
+
+
+def wider2face(root, phase='val', ignore_small=0):
+    data = {}
+    with open('{}/{}/label.txt'.format(root, phase), 'r') as f:
+        lines = f.readlines()
+        for line in tqdm(lines):
+            line = line.strip()
+            if '#' in line:
+                path = '{}/{}/images/{}'.format(root, phase, line.split()[-1])
+                img = cv2.imread(path)
+                height, width, _ = img.shape
+                data[path] = list()
+            else:
+                box = np.array(line.split()[0:4], dtype=np.float32)  # (x1,y1,w,h)
+                if box[2] < ignore_small or box[3] < ignore_small:
+                    continue
+                box = convert((width, height), xywh2xxyy(box))
+                label = '0 {} {} {} {} -1 -1 -1 -1 -1 -1 -1 -1 -1 -1'.format(round(box[0], 4), round(box[1], 4),
+                                                                             round(box[2], 4), round(box[3], 4))
+                data[path].append(label)
+    return data
+
+
+if __name__ == '__main__':
+    if len(sys.argv) == 1:
+        print('Missing path to WIDERFACE folder.')
+        print('Run command: python3 val2yolo.py /path/to/original/widerface [/path/to/save/widerface/val]')
+        exit(1)
+    elif len(sys.argv) > 3:
+        print('Too many arguments were provided.')
+        print('Run command: python3 val2yolo.py /path/to/original/widerface [/path/to/save/widerface/val]')
+        exit(1)
+
+    root_path = sys.argv[1]
+    if not os.path.isfile(os.path.join(root_path, 'val', 'label.txt')):
+        print('Missing label.txt file.')
+        exit(1)
+
+    if len(sys.argv) == 2:
+        if not os.path.isdir('widerface'):
+            os.mkdir('widerface')
+        if not os.path.isdir('widerface/val'):
+            os.mkdir('widerface/val')
+
+        save_path = 'widerface/val'
+    else:
+        save_path = sys.argv[2]
+
+    datas = wider2face(root_path, phase='val')
+    for idx, data in enumerate(datas.keys()):
+        pict_name = os.path.basename(data)
+        out_img = f'{save_path}/{idx}.jpg'
+        out_txt = f'{save_path}/{idx}.txt'
+        shutil.copyfile(data, out_img)
+        labels = datas[data]
+        f = open(out_txt, 'w')
+        for label in labels:
+            f.write(label + '\n')
+        f.close()

+ 65 - 0
data/val2yolo_for_test.py

@@ -0,0 +1,65 @@
+import os
+import cv2
+import numpy as np
+import shutil
+from tqdm import tqdm
+
+root = '/ssd_1t/derron/WiderFace'
+
+
+def xywh2xxyy(box):
+    x1 = box[0]
+    y1 = box[1]
+    x2 = box[0] + box[2]
+    y2 = box[1] + box[3]
+    return (x1, x2, y1, y2)
+
+
+def convert(size, box):
+    dw = 1. / (size[0])
+    dh = 1. / (size[1])
+    x = (box[0] + box[1]) / 2.0 - 1
+    y = (box[2] + box[3]) / 2.0 - 1
+    w = box[1] - box[0]
+    h = box[3] - box[2]
+    x = x * dw
+    w = w * dw
+    y = y * dh
+    h = h * dh
+    return (x, y, w, h)
+
+
+def wider2face(phase='val', ignore_small=0):
+    data = {}
+    with open('{}/{}/label.txt'.format(root, phase), 'r') as f:
+        lines = f.readlines()
+        for line in tqdm(lines):
+            line = line.strip()
+            if '#' in line:
+                path = '{}/{}/images/{}'.format(root, phase, os.path.basename(line))
+                img = cv2.imread(path)
+                height, width, _ = img.shape
+                data[path] = list()
+            else:
+                box = np.array(line.split()[0:4], dtype=np.float32)  # (x1,y1,w,h)
+                if box[2] < ignore_small or box[3] < ignore_small:
+                    continue
+                box = convert((width, height), xywh2xxyy(box))
+                label = '0 {} {} {} {} -1 -1 -1 -1 -1 -1 -1 -1 -1 -1'.format(round(box[0], 4), round(box[1], 4),
+                                                                             round(box[2], 4), round(box[3], 4))
+                data[path].append(label)
+    return data
+
+
+if __name__ == '__main__':
+    datas = wider2face('val')
+    for idx, data in enumerate(datas.keys()):
+        pict_name = os.path.basename(data)
+        out_img = 'widerface/val/images/{}'.format(pict_name)
+        out_txt = 'widerface/val/labels/{}.txt'.format(os.path.splitext(pict_name)[0])
+        shutil.copyfile(data, out_img)
+        labels = datas[data]
+        f = open(out_txt, 'w')
+        for label in labels:
+            f.write(label + '\n')
+        f.close()

+ 21 - 0
data/voc.yaml

@@ -0,0 +1,21 @@
+# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC/
+# Train command: python train.py --data voc.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /VOC
+#     /yolov5
+
+
+# download command/URL (optional)
+download: bash data/scripts/get_voc.sh
+
+# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
+train: ../VOC/images/train/  # 16551 images
+val: ../VOC/images/val/  # 4952 images
+
+# number of classes
+nc: 20
+
+# class names
+names: [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog',
+         'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ]

+ 19 - 0
data/widerface.yaml

@@ -0,0 +1,19 @@
+# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC/
+# Train command: python train.py --data voc.yaml
+# Default dataset location is next to /yolov5:
+#   /parent_folder
+#     /VOC
+#     /yolov5
+
+
+# download command/URL (optional)
+download: bash data/scripts/get_voc.sh
+
+# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
+train: /mnt/Gpan/Mydata/pytorchPorject/yolov5-face/ccpd/train_detect 
+val: /mnt/Gpan/Mydata/pytorchPorject/yolov5-face/ccpd/val_detect
+# number of classes
+nc: 2
+
+# class names
+names: [ 'single','double']

BIN
debug_frame_0_1920x1080.jpg


+ 218 - 0
detect_demo.py

@@ -0,0 +1,218 @@
+import argparse
+import time
+import os
+import cv2
+import torch
+import copy
+import numpy as np
+from models.experimental import attempt_load
+from utils.datasets import letterbox
+from utils.general import check_img_size, non_max_suppression_face, scale_coords
+
+from utils.torch_utils import  time_synchronized
+from plate_recognition.plate_rec import allFilePath,cv_imread
+
+
+clors = [(255,0,0),(0,255,0),(0,0,255),(255,255,0),(0,255,255)]
+
+def load_model(weights, device):
+    model = attempt_load(weights, map_location=device)  # load FP32 model
+    return model
+
+
+def scale_coords_landmarks(img1_shape, coords, img0_shape, ratio_pad=None):
+    # Rescale coords (xyxy) from img1_shape to img0_shape
+    if ratio_pad is None:  # calculate from img0_shape
+        gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])  # gain  = old / new
+        pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2  # wh padding
+    else:
+        gain = ratio_pad[0][0]
+        pad = ratio_pad[1]
+
+    coords[:, [0, 2, 4, 6]] -= pad[0]  # x padding
+    coords[:, [1, 3, 5, 7]] -= pad[1]  # y padding
+    coords[:, :10] /= gain
+    #clip_coords(coords, img0_shape)
+    coords[:, 0].clamp_(0, img0_shape[1])  # x1
+    coords[:, 1].clamp_(0, img0_shape[0])  # y1
+    coords[:, 2].clamp_(0, img0_shape[1])  # x2
+    coords[:, 3].clamp_(0, img0_shape[0])  # y2
+    coords[:, 4].clamp_(0, img0_shape[1])  # x3
+    coords[:, 5].clamp_(0, img0_shape[0])  # y3
+    coords[:, 6].clamp_(0, img0_shape[1])  # x4
+    coords[:, 7].clamp_(0, img0_shape[0])  # y4
+    # coords[:, 8].clamp_(0, img0_shape[1])  # x5
+    # coords[:, 9].clamp_(0, img0_shape[0])  # y5
+    return coords
+
+
+
+
+def get_plate_rec_landmark(img, xyxy, conf, landmarks, class_num,device):
+    h,w,c = img.shape
+    result_dict={}
+    tl = 1 or round(0.002 * (h + w) / 2) + 1  # line/font thickness
+
+    x1 = int(xyxy[0])
+    y1 = int(xyxy[1])
+    x2 = int(xyxy[2])
+    y2 = int(xyxy[3])
+    landmarks_np=np.zeros((4,2))
+    rect=[x1,y1,x2,y2]
+    for i in range(4):
+        point_x = int(landmarks[2 * i])
+        point_y = int(landmarks[2 * i + 1])
+        landmarks_np[i]=np.array([point_x,point_y])
+
+    class_label= int(class_num)  #车牌的的类型0代表单牌,1代表双层车牌
+    result_dict['rect']=rect
+    result_dict['landmarks']=landmarks_np.tolist()
+    result_dict['class']=class_label
+    return result_dict
+
+
+
+def detect_plate(model, orgimg, device,img_size):
+    # Load model
+    # img_size = opt_img_size
+    conf_thres = 0.3
+    iou_thres = 0.5
+    dict_list=[]
+    # orgimg = cv2.imread(image_path)  # BGR
+    img0 = copy.deepcopy(orgimg)
+    assert orgimg is not None, 'Image Not Found ' 
+    h0, w0 = orgimg.shape[:2]  # orig hw
+    r = img_size / max(h0, w0)  # resize image to img_size
+    if r != 1:  # always resize down, only resize up if training with augmentation
+        interp = cv2.INTER_AREA if r < 1  else cv2.INTER_LINEAR
+        img0 = cv2.resize(img0, (int(w0 * r), int(h0 * r)), interpolation=interp)
+
+    imgsz = check_img_size(img_size, s=model.stride.max())  # check img_size
+
+    img = letterbox(img0, new_shape=imgsz)[0]
+    # img =process_data(img0)
+    # Convert
+    img = img[:, :, ::-1].transpose(2, 0, 1).copy()  # BGR to RGB, to 3x416x416
+
+    # Run inference
+    t0 = time.time()
+
+    img = torch.from_numpy(img).to(device)
+    img = img.float()  # uint8 to fp16/32
+    img /= 255.0  # 0 - 255 to 0.0 - 1.0
+    if img.ndimension() == 3:
+        img = img.unsqueeze(0)
+
+    # Inference
+    t1 = time_synchronized()
+    pred = model(img)[0]
+    t2=time_synchronized()
+    # print(f"infer time is {(t2-t1)*1000} ms")
+
+    # Apply NMS
+    pred = non_max_suppression_face(pred, conf_thres, iou_thres)
+
+    # print('img.shape: ', img.shape)
+    # print('orgimg.shape: ', orgimg.shape)
+
+    # Process detections
+    for i, det in enumerate(pred):  # detections per image
+        if len(det):
+            # Rescale boxes from img_size to im0 size
+            det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
+
+            # Print results
+            for c in det[:, -1].unique():
+                n = (det[:, -1] == c).sum()  # detections per class
+
+            det[:, 5:13] = scale_coords_landmarks(img.shape[2:], det[:, 5:13], orgimg.shape).round()
+
+            for j in range(det.size()[0]):
+                xyxy = det[j, :4].view(-1).tolist()
+                conf = det[j, 4].cpu().numpy()
+                landmarks = det[j, 5:13].view(-1).tolist()
+                class_num = det[j, 13].cpu().numpy()
+                result_dict = get_plate_rec_landmark(orgimg, xyxy, conf, landmarks, class_num,device)
+                dict_list.append(result_dict)
+    return dict_list
+    # cv2.imwrite('result.jpg', orgimg)
+
+
+
+def draw_result(orgimg,dict_list):
+    result_str =""
+    for result in dict_list:
+        rect_area = result['rect']
+        
+        x,y,w,h = rect_area[0],rect_area[1],rect_area[2]-rect_area[0],rect_area[3]-rect_area[1]
+        padding_w = 0.05*w
+        padding_h = 0.11*h
+        rect_area[0]=max(0,int(x-padding_w))
+        rect_area[1]=max(0,int(y-padding_h))
+        rect_area[2]=min(orgimg.shape[1],int(rect_area[2]+padding_w))
+        rect_area[3]=min(orgimg.shape[0],int(rect_area[3]+padding_h))
+
+        
+        landmarks=result['landmarks']
+        label=result['class']
+        # result_str+=result+" "
+        for i in range(4):  #关键点
+            cv2.circle(orgimg, (int(landmarks[i][0]), int(landmarks[i][1])), 5, clors[i], -1)
+        cv2.rectangle(orgimg,(rect_area[0],rect_area[1]),(rect_area[2],rect_area[3]),clors[label],2) #画框
+        cv2.putText(img,str(label),(rect_area[0],rect_area[1]),cv2.FONT_HERSHEY_SIMPLEX,0.5,clors[label],2)
+    #     orgimg=cv2ImgAddText(orgimg,label,rect_area[0]-height_area,rect_area[1]-height_area-10,(0,255,0),height_area)
+    # print(result_str)
+    return orgimg
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--detect_model', nargs='+', type=str, default='runs/train/exp32/weights/last.pt', help='model.pt path(s)')  #检测模型
+    parser.add_argument('--image_path', type=str, default='/mnt/Gpan/Mydata/pytorchPorject/datasets/ccpd/train_detect/gangao', help='source') 
+    parser.add_argument('--img_size', type=int, default=640, help='inference size (pixels)')
+    parser.add_argument('--output', type=str, default='result1', help='source') 
+    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+    opt = parser.parse_args()
+    print(opt)
+    save_path = opt.output
+    count=0
+    if not os.path.exists(save_path):
+        os.mkdir(save_path)
+
+    detect_model = load_model(opt.detect_model, device)  #初始化检测模型
+    time_all = 0
+    time_begin=time.time()
+    if not os.path.isfile(opt.image_path):            #目录
+        file_list=[]
+        allFilePath(opt.image_path,file_list)
+        for img_path in file_list:
+            
+            print(count,img_path)
+            time_b = time.time()
+            img =cv_imread(img_path)
+            
+            if img is None:
+                continue
+            if img.shape[-1]==4:
+                img=cv2.cvtColor(img,cv2.COLOR_BGRA2BGR)
+            # detect_one(model,img_path,device)
+            dict_list=detect_plate(detect_model, img, device,opt.img_size)
+            ori_img=draw_result(img,dict_list)
+            img_name = os.path.basename(img_path)
+            save_img_path = os.path.join(save_path,img_name)
+            time_e=time.time()
+            time_gap = time_e-time_b
+            if count:
+                time_all+=time_gap
+            cv2.imwrite(save_img_path,ori_img)
+            count+=1
+    else:                                          #单个图片
+            print(count,opt.image_path,end=" ")
+            img =cv_imread(opt.image_path)
+            if img.shape[-1]==4:
+                img=cv2.cvtColor(img,cv2.COLOR_BGRA2BGR)
+            # detect_one(model,img_path,device)
+            dict_list=detect_plate(detect_model, img, device,opt.img_size)
+            ori_img=draw_result(img,dict_list)
+            img_name = os.path.basename(opt.image_path)
+            save_img_path = os.path.join(save_path,img_name)
+            cv2.imwrite(save_img_path,ori_img)  
+    print(f"sumTime time is {time.time()-time_begin} s, average pic time is {time_all/(len(file_list)-1)}")

+ 925 - 0
detect_plate.py

@@ -0,0 +1,925 @@
+import argparse
+import copy
+import math
+import os
+import platform
+import re
+import threading
+import time
+from collections import deque
+from datetime import datetime
+from pprint import pprint
+from concurrent.futures import ThreadPoolExecutor
+from threading import Lock
+
+import cv2
+import numpy as np
+import torch
+import redis
+import serial
+from typing import Optional, List
+
+# 平台检测
+PLATFORM = platform.system()
+
+import sys
+
+# 提前导入并减少重复导入
+# 注意:请确保这些模块的路径正确,若有导入错误需调整路径
+try:
+    from models.experimental import attempt_load
+    from modules.audio.speaker import IpCast
+    from modules.display.screen import Screen, FlashFile
+    from modules.radar.radar import RadarData, DeviceInitData, parse_radar_frame, open_serial
+    from plate_recognition.double_plate_split_merge import get_split_merge
+    from plate_recognition.plate_rec import (
+        allFilePath,
+        cv_imread,
+        get_plate_result,
+        init_model,
+    )
+    from utils.datasets import letterbox
+    from utils.general import check_img_size, non_max_suppression_face, scale_coords
+except ImportError as e:
+    print(f"导入模块失败: {e},请检查模块路径是否正确")
+    sys.exit(1)
+
+# ===================== 全局变量初始化(完整保留原功能) =====================
+# Redis连接配置
+REDIS_HOST = 'localhost'
+REDIS_PORT = 6379
+REDIS_DB = 0
+REDIS_PASSWORD = None
+REDIS_KEY = 'plate_results'
+WINDOW_SIZE = 5
+
+# 屏幕连接配置
+SCREEN_HOST = '192.168.110.200'  # 主屏幕:显示车牌识别信息
+SCREEN_PORT = 5005
+RADAR_SCREEN_HOST = '192.168.110.199'  # 雷达屏幕:显示雷达速度信息
+RADAR_SCREEN_PORT = 5005
+
+# 根据平台自动选择串口路径
+if PLATFORM == 'Windows':
+    RADAR_PORT = 'COM3'
+    SPEAKER_PORT = 'COM4'
+elif PLATFORM == 'Linux':
+    RADAR_PORT = '/dev/ttyACM0'
+    SPEAKER_PORT = '/dev/ttyUSB0'
+else:
+    RADAR_PORT = '/dev/ttyACM0'
+    SPEAKER_PORT = '/dev/ttyUSB0'
+
+DEVICE_LOW_SPEED = 15
+
+# 重连相关配置
+MAX_RECONNECT_ATTEMPTS = 10
+RECONNECT_DELAY = 5
+MAX_CONSECUTIVE_FAILURES = 5
+
+# 阈值设置
+DETECT_THRESH = 0.65
+COLOR_THRESH = 0.85
+REC_THRESH = 0.85
+PLATE_ASPECT_RATIO = 1.8  # 车牌宽高比(正向>1.8,反向<1.2)
+
+# 性能优化参数
+FRAME_SKIP = 2
+BATCH_REDIS_WRITE = True
+REDIS_CLEAN_INTERVAL = 20
+ASYNC_REDIS = True
+INFERENCE_HALF = True
+JIT_COMPILE = False
+THREAD_POOL_SIZE = 5
+
+# 合法车牌正则
+LICENSE_PLATE_PATTERN = re.compile(
+    r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{5,7}$')
+
+# 全局变量
+redis_client = None
+redis_lock = Lock()
+redis_write_queue = deque(maxlen=100)
+executor = ThreadPoolExecutor(max_workers=THREAD_POOL_SIZE)
+clean_error_count = 0
+redis_read_error = 0
+cap = None
+radar_serial = None
+
+# 屏幕/语音全局实例(关键:明确区分主屏幕和雷达屏幕)
+screen = None  # 主屏幕实例(192.168.110.199)
+radar_screen = None  # 雷达屏幕实例(192.168.110.198)
+speaker = None  # 语音实例
+
+
+# ===================== 核心依赖函数 =====================
+def scale_coords_landmarks(img1_shape, coords, img0_shape, ratio_pad=None):
+    """车牌关键点坐标还原到原图"""
+    if ratio_pad is None:
+        gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
+        pad = ((img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2)
+    else:
+        gain = ratio_pad[0][0]
+        pad = ratio_pad[1]
+
+    coords[:, [0, 2, 4, 6]] -= pad[0]
+    coords[:, [1, 3, 5, 7]] -= pad[1]
+    coords[:, :8] /= gain
+
+    coords[:, 0] = coords[:, 0].clip(0, img0_shape[1])
+    coords[:, 1] = coords[:, 1].clip(0, img0_shape[0])
+    coords[:, 2] = coords[:, 2].clip(0, img0_shape[1])
+    coords[:, 3] = coords[:, 3].clip(0, img0_shape[0])
+    coords[:, 4] = coords[:, 4].clip(0, img0_shape[1])
+    coords[:, 5] = coords[:, 5].clip(0, img0_shape[0])
+    coords[:, 6] = coords[:, 6].clip(0, img0_shape[1])
+    coords[:, 7] = coords[:, 7].clip(0, img0_shape[0])
+    return coords
+
+
+def order_points(pts):
+    """排序四点坐标"""
+    if isinstance(pts, np.ndarray) and pts.size == 0:
+        return np.zeros((4, 2), dtype="float32")
+
+    rect = np.zeros((4, 2), dtype="float32")
+    s = pts.sum(axis=1)
+    rect[0] = pts[np.argmin(s)]
+    rect[2] = pts[np.argmax(s)]
+    diff = np.diff(pts, axis=1)
+    rect[1] = pts[np.argmin(diff)]
+    rect[3] = pts[np.argmax(diff)]
+    return rect
+
+
+def four_point_transform(image, pts):
+    """四点透视变换"""
+    if not isinstance(pts, np.ndarray) or pts.shape != (4, 2) or pts.size == 0:
+        return image
+
+    rect = order_points(pts)
+    (tl, tr, br, bl) = rect
+
+    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
+    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
+    maxWidth = max(int(widthA) if widthA > 0 else 1, int(widthB) if widthB > 0 else 1)
+
+    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
+    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
+    maxHeight = max(int(heightA) if heightA > 0 else 1, int(heightB) if heightB > 0 else 1)
+
+    dst = np.array([[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
+    M = cv2.getPerspectiveTransform(rect, dst)
+    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
+    return warped
+
+
+def is_valid_forward_plate(plate_str, bbox):
+    """判断是否为来向车(正向车牌)"""
+    plate_clean = plate_str.strip().upper().replace(' ', '')
+    if len(plate_clean) < 7 or len(plate_clean) > 8:
+        return False
+
+    x1, y1, x2, y2 = bbox
+    width = x2 - x1
+    height = y2 - y1
+    if height == 0 or (width / height) < PLATE_ASPECT_RATIO:
+        return False
+
+    if not LICENSE_PLATE_PATTERN.match(plate_clean):
+        return False
+
+    return True
+
+
+def get_plate_rec_landmark(img, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=False):
+    """车牌识别核心函数"""
+    h, w, _ = img.shape
+    result_dict = {}
+
+    x1, y1, x2, y2 = map(int, np.ravel(xyxy))
+    landmarks = np.ravel(landmarks)
+    landmarks_np = np.array(landmarks).reshape(4, 2).astype(int)
+    rect = [x1, y1, x2, y2]
+
+    # 透视变换获取车牌ROI
+    class_label = int(class_num)
+    roi_img = four_point_transform(img, landmarks_np)
+    if class_label:
+        roi_img = get_split_merge(roi_img)
+
+    # 识别车牌号
+    if not is_color:
+        plate_number, rec_prob = get_plate_result(roi_img, device, plate_rec_model, is_color=is_color)
+        plate_color = ""
+        color_conf = 0.0
+    else:
+        plate_number, rec_prob, plate_color, color_conf = get_plate_result(roi_img, device, plate_rec_model,
+                                                                           is_color=is_color)
+
+    # 修复rec_prob格式
+    if isinstance(rec_prob, np.ndarray):
+        rec_prob = rec_prob.tolist()
+
+    # 判断是否为来向车
+    is_forward = is_valid_forward_plate(plate_number, rect)
+
+    # 组装结果
+    result_dict.update({
+        "rect": rect,
+        "detect_conf": conf,
+        "landmarks": landmarks_np.tolist(),
+        "plate_no": plate_number,
+        "rec_conf": rec_prob,
+        "roi_height": roi_img.shape[0],
+        "plate_color": plate_color,
+        "color_conf": color_conf,
+        "plate_type": class_num,
+        "is_forward": is_forward
+    })
+    return result_dict
+
+
+def get_window_info():
+    """获取Redis窗口统计信息"""
+    if redis_client is None:
+        return {"count": 0, "window_size": WINDOW_SIZE}
+
+    try:
+        clean_expired_data_batch()
+
+        hash_key = f"{REDIS_KEY}:data"
+        zset_key = f"{REDIS_KEY}:sorted"
+
+        data_count = redis_client.hlen(hash_key)
+        sorted_count = redis_client.zcard(zset_key)
+        oldest_ts = newest_ts = int(time.time())
+        time_range = 0
+
+        if sorted_count > 0:
+            timestamps_with_scores = redis_client.zrange(zset_key, 0, -1, withscores=True)
+            if timestamps_with_scores:
+                timestamps = []
+                for _, score in timestamps_with_scores:
+                    try:
+                        timestamps.append(int(float(score)))
+                    except:
+                        continue
+                if len(timestamps) > 0:
+                    oldest_ts = min(timestamps)
+                    newest_ts = max(timestamps)
+                    time_range = newest_ts - oldest_ts
+
+        return {
+            "count": data_count,
+            "window_size": WINDOW_SIZE,
+            "time_range": time_range,
+            "oldest_record": datetime.fromtimestamp(oldest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无",
+            "newest_record": datetime.fromtimestamp(newest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无"
+        }
+    except Exception as e:
+        print(f"获取窗口信息失败: {e}")
+        return {"count": 0, "window_size": WINDOW_SIZE}
+
+
+# ===================== 工具函数 =====================
+def get_current_time():
+    """获取格式化当前时间"""
+    return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
+
+
+def get_current_timestamp():
+    """获取秒级时间戳"""
+    return int(time.time())
+
+
+def connect_stream(stream_url, cap_options=""):
+    """建立视频流连接,带重试机制"""
+    global cap
+    attempt = 0
+
+    while attempt < MAX_RECONNECT_ATTEMPTS:
+        try:
+            print(f"[{get_current_time()}] 尝试连接视频流: {stream_url} (第{attempt + 1}次)")
+
+            if cap_options:
+                cap = cv2.VideoCapture()
+                os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = cap_options
+                success = cap.open(stream_url, cv2.CAP_FFMPEG)
+            else:
+                cap = cv2.VideoCapture(stream_url)
+                success = cap.isOpened()
+
+            if success:
+                cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
+                cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'))
+
+                ret, frame = cap.read()
+                if ret:
+                    print(f"[{get_current_time()}] 视频流连接成功")
+                    return cap, True
+                else:
+                    print(f"[{get_current_time()}] 视频流打开但无法读取帧")
+                    cap.release()
+            else:
+                print(f"[{get_current_time()}] 无法打开视频流")
+
+            print(f"[{get_current_time()}] 连接失败,{RECONNECT_DELAY}秒后重试...")
+            time.sleep(RECONNECT_DELAY)
+            attempt += 1
+
+        except Exception as e:
+            print(f"[{get_current_time()}] 连接异常: {str(e)}")
+            time.sleep(RECONNECT_DELAY)
+            attempt += 1
+
+    print(f"[{get_current_time()}] 达到最大重连次数({MAX_RECONNECT_ATTEMPTS}),退出")
+    return None, False
+
+
+def reconnect_stream(stream_url, cap_options=""):
+    """重新连接视频流"""
+    global cap
+    print(f"[{get_current_time()}] 开始重新连接视频流...")
+
+    if cap is not None:
+        cap.release()
+        time.sleep(2)
+
+    return connect_stream(stream_url, cap_options)
+
+
+# ===================== Redis操作 =====================
+def clean_expired_data_batch():
+    """批量清理过期Redis数据"""
+    if redis_client is None:
+        return 0
+
+    try:
+        with redis_lock:
+            current_ts = get_current_timestamp()
+            cutoff_ts = current_ts - WINDOW_SIZE
+
+            pipe = redis_client.pipeline(transaction=False)
+            zset_key = f"{REDIS_KEY}:sorted"
+            expired_timestamps = redis_client.zrangebyscore(zset_key, 0, cutoff_ts)
+
+            if expired_timestamps:
+                hash_key = f"{REDIS_KEY}:data"
+                pipe.hdel(hash_key, *expired_timestamps)
+                pipe.zremrangebyscore(zset_key, 0, cutoff_ts)
+                pipe.execute()
+                return len(expired_timestamps)
+        return 0
+    except Exception as e:
+        global clean_error_count
+        clean_error_count += 1
+        if clean_error_count % 10 == 0:
+            print(f"清理过期数据失败({clean_error_count}次): {e}")
+        return 0
+
+
+def save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg, direction="incoming"):
+    """异步写入Redis"""
+    try:
+        timestamp = get_current_timestamp()
+        timestamp_ms = int(time.time() * 1000)
+
+        entry_data = {
+            "plate_no": plate_no.strip(),
+            "plate_color": plate_color,
+            "detect_conf": f"{detect_conf:.3f}",
+            "color_conf": f"{color_conf:.3f}",
+            "rec_avg": f"{rec_avg:.3f}",
+            "timestamp": str(timestamp),
+            "timestamp_ms": str(timestamp_ms),
+            "datetime": get_current_time(),
+            "source": "rtsp_stream",
+            "direction": direction
+        }
+
+        with redis_lock:
+            if BATCH_REDIS_WRITE:
+                redis_write_queue.append((timestamp, entry_data))
+                if len(redis_write_queue) >= 10:
+                    flush_redis_queue()
+            else:
+                pipe = redis_client.pipeline(transaction=False)
+                hash_key = f"{REDIS_KEY}:data"
+                zset_key = f"{REDIS_KEY}:sorted"
+                pipe.hset(hash_key, timestamp, str(entry_data))
+                pipe.zadd(zset_key, {timestamp: timestamp})
+                pipe.execute()
+        return True, f"加入队列: {timestamp}"
+    except Exception as e:
+        return False, f"异步写入失败: {e}"
+
+
+def flush_redis_queue():
+    """刷入Redis队列数据"""
+    if not redis_write_queue or redis_client is None:
+        return False
+
+    try:
+        with redis_lock:
+            if not redis_write_queue:
+                return True
+
+            pipe = redis_client.pipeline(transaction=False)
+            hash_key = f"{REDIS_KEY}:data"
+            zset_key = f"{REDIS_KEY}:sorted"
+
+            for timestamp, entry_data in redis_write_queue:
+                pipe.hset(hash_key, timestamp, str(entry_data))
+                pipe.zadd(zset_key, {timestamp: timestamp})
+
+            pipe.execute()
+            redis_write_queue.clear()
+        return True
+    except Exception as e:
+        print(f"批量写入Redis失败: {e}")
+        return False
+
+
+def get_recent_plates_from_redis():
+    """获取最近5秒的车牌记录"""
+    if redis_client is None:
+        return []
+
+    try:
+        zset_key = f"{REDIS_KEY}:sorted"
+        hash_key = f"{REDIS_KEY}:data"
+
+        current_ts = get_current_timestamp()
+        cutoff_ts = current_ts - WINDOW_SIZE
+        recent_timestamps = redis_client.zrevrangebyscore(zset_key, current_ts, cutoff_ts)
+
+        results = []
+        if recent_timestamps:
+            entries = redis_client.hmget(hash_key, recent_timestamps)
+            for ts, entry_str in zip(recent_timestamps, entries):
+                if entry_str:
+                    try:
+                        data = eval(entry_str)
+                        results.append({
+                            'timestamp': int(ts),
+                            'data': data
+                        })
+                    except:
+                        continue
+        return results
+    except Exception as e:
+        global redis_read_error
+        redis_read_error += 1
+        if redis_read_error % 10 == 0:
+            print(f"从Redis读取数据失败({redis_read_error}次): {e}")
+        return []
+
+
+# ===================== 初始化语音/屏幕 =====================
+def init_speaker(port: str) -> IpCast | None:
+    """初始化语音模块(带异常处理和重试)"""
+    attempts = 0
+    while attempts < MAX_RECONNECT_ATTEMPTS:
+        try:
+            speaker = IpCast(port=port)
+            print(f"✅ 语音模块初始化成功(串口:{port})")
+            return speaker
+        except Exception as e:
+            attempts += 1
+            if attempts < MAX_RECONNECT_ATTEMPTS:
+                print(f"⚠️  语音模块初始化失败:{e},{RECONNECT_DELAY}秒后重试({attempts}/{MAX_RECONNECT_ATTEMPTS})")
+                time.sleep(RECONNECT_DELAY)
+            else:
+                print(f"❌ 语音模块初始化失败:{e},已达到最大重试次数")
+                return None
+
+
+def init_screen(name: str, ip: str, port: int) -> Screen | None:
+    """初始化屏幕(带连接重试)"""
+    screen = Screen(name=name, ip=ip, port=str(port))
+    attempts = 0
+    while attempts < MAX_RECONNECT_ATTEMPTS:
+        if screen.get_live_state():
+            print(f"✅ {name} 连接成功(IP:{ip}:{port})")
+            return screen
+        print(f"⚠️  {name} 连接失败,{RECONNECT_DELAY}秒后重试({attempts + 1}/{MAX_RECONNECT_ATTEMPTS})")
+        time.sleep(RECONNECT_DELAY)
+        screen.reconnect()
+        attempts += 1
+    print(f"❌ {name} 连接失败(IP:{ip}:{port}),达到最大重试次数")
+    return None
+
+
+def init_screen_async(name: str, ip: str, port: int, result_dict: dict):
+    """异步初始化屏幕"""
+    screen = init_screen(name, ip, port)
+    result_dict[name] = screen
+
+
+# ===================== 模型加载与推理 =====================
+def load_model_optimized(weights, device):
+    """优化加载模型"""
+    model = attempt_load(weights, map_location=device)
+
+    if JIT_COMPILE and device.type != 'cpu':
+        try:
+            dummy = torch.rand(1, 3, 640, 640).to(device)
+            if INFERENCE_HALF:
+                dummy = dummy.half()
+            model = torch.jit.trace(model, dummy)
+            print("模型JIT编译成功")
+        except Exception as e:
+            print(f"JIT编译失败: {e}")
+
+    if INFERENCE_HALF and device.type != 'cpu':
+        model.half()
+
+    model.eval()
+    for param in model.parameters():
+        param.requires_grad = False
+
+    return model
+
+
+def detect_Recognition_plate_optimized(model, orgimg, device, plate_rec_model, img_size, is_color=False):
+    """优化的车牌检测识别"""
+    conf_thres = 0.3
+    iou_thres = 0.5
+    dict_list = []
+
+    h0, w0 = orgimg.shape[:2]
+    r = img_size / max(h0, w0)
+    if abs(r - 1) > 0.1:
+        interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
+        img0 = cv2.resize(orgimg, (int(w0 * r), int(h0 * r)), interpolation=interp)
+    else:
+        img0 = orgimg
+
+    imgsz = check_img_size(img_size, s=model.stride.max())
+    img = letterbox(img0, new_shape=imgsz)[0]
+    img = img[:, :, ::-1].transpose(2, 0, 1).copy()
+
+    img = torch.from_numpy(img).to(device)
+    img = img.float() / 255.0
+    if INFERENCE_HALF and device.type != 'cpu':
+        img = img.half()
+    if img.ndim == 3:
+        img = img.unsqueeze(0)
+
+    with torch.no_grad():
+        pred = model(img)[0]
+        pred = non_max_suppression_face(pred, conf_thres, iou_thres)
+
+    for det in pred:
+        if len(det):
+            det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
+            det[:, 5:13] = scale_coords_landmarks(img.shape[2:], det[:, 5:13], orgimg.shape).round()
+
+            for j in range(det.size(0)):
+                xyxy = det[j, :4].tolist()
+                conf = det[j, 4].cpu().item()
+                landmarks = det[j, 5:13].tolist()
+                class_num = det[j, 13].cpu().item()
+
+                if conf < DETECT_THRESH:
+                    continue
+
+                result_dict = get_plate_rec_landmark(orgimg, xyxy, conf, landmarks, class_num, device, plate_rec_model,
+                                                     is_color)
+                dict_list.append(result_dict)
+                break
+            break
+
+    return dict_list[:1]
+
+
+# ===================== 主函数 =====================
+def start(image_path="imgs"):
+    # 声明使用全局的屏幕/语音实例(核心修复:解决变量作用域问题)
+    global screen, radar_screen, speaker, redis_client
+
+    # 参数解析
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--detect_model", nargs="+", type=str, default="weights/plate_detect.pt", help="检测模型路径")
+    parser.add_argument("--rec_model", type=str, default="weights/plate_rec_color.pth", help="识别模型路径")
+    parser.add_argument("--is_color", type=bool, default=True, help="是否识别车牌颜色")
+    parser.add_argument("--image_path", type=str, default=image_path, help="图片路径")
+    parser.add_argument("--img_size", type=int, default=512, help="推理尺寸")
+    parser.add_argument("--output", type=str, default="result", help="输出目录")
+    parser.add_argument("--video", type=str, default="", help="视频文件路径")
+    parser.add_argument("--stream", type=str, default="", help="RTSP/RTMP流地址")
+    parser.add_argument("--redis_host", type=str, default="localhost", help="Redis主机")
+    parser.add_argument("--redis_port", type=int, default=6379, help="Redis端口")
+    parser.add_argument("--redis_key", type=str, default="plate_results", help="Redis键名")
+    parser.add_argument("--window_size", type=int, default=5, help="滑动窗口秒数")
+    opt = parser.parse_args()
+
+    # 设备配置
+    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+    if device.type == 'cuda':
+        torch.backends.cudnn.benchmark = True
+        torch.backends.cuda.matmul.allow_tf32 = True
+
+    # 更新全局配置
+    global REDIS_HOST, REDIS_PORT, REDIS_KEY, WINDOW_SIZE
+    REDIS_HOST = opt.redis_host
+    REDIS_PORT = opt.redis_port
+    REDIS_KEY = opt.redis_key
+    WINDOW_SIZE = opt.window_size
+
+    # Redis连接
+    try:
+        redis_client = redis.Redis(
+            host=REDIS_HOST,
+            port=REDIS_PORT,
+            db=REDIS_DB,
+            password=REDIS_PASSWORD,
+            decode_responses=True,
+            socket_timeout=2,
+            socket_connect_timeout=2
+        )
+        redis_client.ping()
+        print("✅ Redis连接成功(优化版)")
+    except Exception as e:
+        print(f"❌ Redis连接失败: {e}")
+        redis_client = None
+
+    # 创建输出目录
+    os.makedirs(opt.output, exist_ok=True)
+
+    # 加载模型
+    try:
+        detect_model = load_model_optimized(opt.detect_model, device)
+        plate_rec_model = init_model(device, opt.rec_model, is_color=opt.is_color)
+        total_detect = sum(p.numel() for p in detect_model.parameters()) / 1e6
+        total_rec = sum(p.numel() for p in plate_rec_model.parameters()) / 1e6
+        print(f"✅ 模型加载成功:检测{total_detect:.2f}M, 识别{total_rec:.2f}M")
+    except Exception as e:
+        print(f"❌ 模型加载失败: {e}")
+        return
+
+    # 打印配置信息
+    print(f"推理模式: {'半精度' if INFERENCE_HALF else '全精度'} | JIT编译: {JIT_COMPILE}")
+    print(
+        f"帧处理策略: 每{FRAME_SKIP}帧处理一次 | Redis: {'异步批量' if ASYNC_REDIS and BATCH_REDIS_WRITE else '同步'}")
+    print(f"过滤策略: 不过滤方向,检测所有车辆 | 宽高比阈值: {PLATE_ASPECT_RATIO}")
+
+    # 初始化语音模块
+    speaker = init_speaker(SPEAKER_PORT)
+
+    # 异步初始化两个屏幕
+    screen_init_results = {}
+    screen_threads = [
+        threading.Thread(target=init_screen_async, args=("主屏幕", SCREEN_HOST, SCREEN_PORT, screen_init_results),
+                         daemon=True),
+        threading.Thread(target=init_screen_async,
+                         args=("雷达屏幕", RADAR_SCREEN_HOST, RADAR_SCREEN_PORT, screen_init_results), daemon=True)
+    ]
+    for t in screen_threads:
+        t.start()
+    for t in screen_threads:
+        t.join(timeout=30)
+
+    # 获取屏幕初始化结果(绑定全局变量)
+    screen = screen_init_results.get("主屏幕")
+    radar_screen = screen_init_results.get("雷达屏幕")
+
+    # 打印屏幕绑定信息(调试用)
+    if screen:
+        print(f"✅ 主屏幕已绑定:{SCREEN_HOST}:{SCREEN_PORT}(用于显示车牌)")
+    else:
+        print(f"❌ 主屏幕初始化失败")
+    if radar_screen:
+        print(f"✅ 雷达屏幕已绑定:{RADAR_SCREEN_HOST}:{RADAR_SCREEN_PORT}(用于显示雷达速度)")
+    else:
+        print(f"❌ 雷达屏幕初始化失败")
+
+    DeviceInitData.LowSpeed = DEVICE_LOW_SPEED
+
+    # 启动雷达线程(核心修复:传入雷达屏幕实例,而非主屏幕)
+    try:
+        radar_thread = threading.Thread(
+            target=open_serial,
+            args=(RADAR_PORT, speaker, radar_screen),  # 传入radar_screen(雷达屏幕)
+            daemon=True
+        )
+        radar_thread.start()
+        print(f"✅ 雷达已在后台线程启动,串口:{RADAR_PORT},绑定雷达屏幕")
+    except Exception as e:
+        print(f"❌ 雷达启动失败: {e}")
+
+    # 处理RTSP流
+    if opt.stream:
+        cap_options = "rtsp_transport=tcp"
+        cap, connected = connect_stream(opt.stream, cap_options)
+        if not connected:
+            print(f"[{get_current_time()}] 初始连接失败,退出程序")
+            return
+
+        # 初始化统计变量
+        consecutive_failures = 0
+        reconnect_count = 0
+        frame_count = 0
+        processed_count = 0
+        last_print_time = time.time()
+        print_interval = 10.0
+        inference_times = deque(maxlen=50)
+        last_output_dict = {}
+        output_count = 0
+        incoming_car_count = 0
+        outgoing_car_count = 0
+
+        try:
+            while True:
+                frame_count += 1
+                ret, frame = cap.read()
+
+                # 处理帧读取失败
+                if not ret:
+                    consecutive_failures += 1
+                    if consecutive_failures % MAX_CONSECUTIVE_FAILURES == 0:
+                        print(f"[{get_current_time()}] 视频流中断(连续失败{consecutive_failures}次)")
+                    if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
+                        cap, reconnected = reconnect_stream(opt.stream, cap_options)
+                        if reconnected:
+                            reconnect_count += 1
+                            consecutive_failures = 0
+                            frame_count = 0
+                            continue
+                        else:
+                            break
+                    continue
+
+                consecutive_failures = 0
+
+                # 帧跳过策略
+                if frame_count % FRAME_SKIP != 0:
+                    continue
+
+                processed_count += 1
+
+                # 推理处理
+                inference_start = time.time()
+                try:
+                    dict_list = detect_Recognition_plate_optimized(
+                        detect_model, frame, device, plate_rec_model, opt.img_size, is_color=opt.is_color
+                    )
+                    inference_time = time.time() - inference_start
+                    inference_times.append(inference_time)
+
+                    current_time = time.time()
+
+                    # 处理识别结果
+                    for res in dict_list:
+                        plate_no = res['plate_no'].strip()
+                        if len(plate_no) < 4 or plate_no.lower() in ['unknown', '']:
+                            continue
+
+                        # 阈值过滤
+                        detect_conf = float(res['detect_conf'])
+                        color_conf = res.get('color_conf', 0.0)
+                        rec_conf = res.get('rec_conf', [])
+
+                        if isinstance(rec_conf, np.ndarray):
+                            rec_conf_list = rec_conf.tolist()
+                        else:
+                            rec_conf_list = rec_conf if isinstance(rec_conf, list) else []
+                        rec_avg = np.mean(rec_conf_list) if len(rec_conf_list) > 0 else 0.0
+
+                        if detect_conf < DETECT_THRESH or color_conf < COLOR_THRESH or rec_avg < REC_THRESH:
+                            continue
+
+                        # 去重判断
+                        clean_plate = plate_no.replace(' ', '').upper()
+                        should_output_flag = False
+                        similar_found = None
+
+                        for existing_plate in last_output_dict:
+                            if clean_plate[:5] == existing_plate[:5]:
+                                similar_found = existing_plate
+                                break
+
+                        if similar_found is None:
+                            should_output_flag = True
+                            last_output_dict[clean_plate] = current_time
+                        else:
+                            time_diff = current_time - last_output_dict[similar_found]
+                            if time_diff >= 3.0:
+                                del last_output_dict[similar_found]
+                                last_output_dict[clean_plate] = current_time
+                                should_output_flag = True
+
+                        # 输出和保存
+                        if should_output_flag:
+                            # 统计方向
+                            if res.get("is_forward", False):
+                                incoming_car_count += 1
+                                direction = "incoming"
+                            else:
+                                outgoing_car_count += 1
+                                direction = "outgoing"
+
+                            plate_color = res.get('plate_color', '未知')
+                            current_time_str = get_current_time()
+                            output_line = (
+                                f"[{current_time_str}] {plate_no} | 检:{detect_conf:.3f} "
+                                f"色:{color_conf:.3f} 识:{rec_avg:.3f} | {plate_color}")
+                            print(output_line)
+
+                            # 写入Redis
+                            if redis_client:
+                                if ASYNC_REDIS:
+                                    executor.submit(save_to_redis_async, plate_no, plate_color, detect_conf, color_conf,
+                                                    rec_avg, direction)
+                                    # 核心修复:仅写入主屏幕(screen),不写入雷达屏幕
+                                    if screen:
+                                        try:
+                                            ff = FlashFile()
+                                            ff.set_msg(plate_no, 1)  # 显示车牌
+                                            ff.set_mode(4, 1)
+                                            ff.set_origin(0, True, 0)
+                                            ff.set_area(128, True, 32)
+                                            screen.text_ram(ff, True)
+                                        except Exception as e:
+                                            print(f"❌ 写入主屏幕失败: {e}")
+                                else:
+                                    save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg,
+                                                        direction)
+
+                            output_count += 1
+
+                except Exception as e:
+                    print(f"[{get_current_time()}] 处理异常: {e}")
+                    import traceback
+                    traceback.print_exc()
+                    continue
+
+                # 定期清理Redis
+                if frame_count % REDIS_CLEAN_INTERVAL == 0 and redis_client:
+                    executor.submit(clean_expired_data_batch)
+
+                # 定期刷入Redis队列
+                if frame_count % 10 == 0 and BATCH_REDIS_WRITE and redis_client:
+                    executor.submit(flush_redis_queue)
+
+                # 状态打印
+                if time.time() - last_print_time >= print_interval:
+                    avg_inference = sum(inference_times) / len(inference_times) if len(inference_times) > 0 else 0
+                    print(f"\n[{get_current_time()}] 状态统计")
+                    print(f"总帧数: {frame_count} | 处理帧: {processed_count} | 输出车牌: {output_count}")
+                    print(
+                        f"来向车数量: {incoming_car_count} | 去向车数量: {outgoing_car_count} | 重连次数: {reconnect_count}")
+                    print(f"平均推理时间: {avg_inference * 1000:.1f}ms | 处理帧率: {1 / avg_inference:.1f}fps"
+                          if avg_inference > 0 else "平均推理时间: 0ms | 处理帧率: 0fps")
+                    print(f"缓存车牌种类: {len(last_output_dict)} | Redis清理失败: {clean_error_count}次")
+
+                    if redis_client:
+                        try:
+                            window_info = get_window_info()
+                            print(f"Redis窗口: {window_info['count']}条/{WINDOW_SIZE}秒")
+                        except:
+                            pass
+
+                    last_print_time = time.time()
+
+                # 退出按键
+                if cv2.waitKey(1) & 0xFF == ord('q'):
+                    break
+
+        except KeyboardInterrupt:
+            print(f"\n[{get_current_time()}] 用户中断")
+        except Exception as e:
+            print(f"\n[{get_current_time()}] 运行错误: {e}")
+            import traceback
+            traceback.print_exc()
+        finally:
+            # 资源清理
+            if cap is not None:
+                cap.release()
+            cv2.destroyAllWindows()
+            executor.shutdown(wait=True)
+
+            # 刷入剩余Redis数据
+            if redis_client:
+                flush_redis_queue()
+                clean_expired_data_batch()
+
+            # 最终统计
+            print(f"\n[{get_current_time()}] 结束报告")
+            print(f"总帧数: {frame_count} | 处理帧: {processed_count} | 输出车牌: {output_count}")
+            print(f"来向车总数: {incoming_car_count} | 去向车总数: {outgoing_car_count} | 重连次数: {reconnect_count}")
+            if len(inference_times) > 0:
+                avg_inf = sum(inference_times) / len(inference_times)
+                print(f"平均推理时间: {avg_inf * 1000:.1f}ms | 实时FPS: {1 / avg_inf:.1f}")
+            else:
+                print("平均推理时间: 0ms | 实时FPS: 0")
+            print(f"识别车牌种类: {len(last_output_dict)} | Redis读取失败: {redis_read_error}次")
+
+
+if __name__ == '__main__':
+    # 初始化屏幕/语音(全局)
+    speaker = init_speaker(SPEAKER_PORT)
+
+    # 启动主程序
+    start()

+ 509 - 0
detect_plate.py.bk

@@ -0,0 +1,509 @@
+import argparse
+import copy
+import os
+import time
+from pprint import pprint
+
+import cv2
+import numpy as np
+import torch
+
+from models.experimental import attempt_load
+from plate_recognition.double_plate_split_merge import get_split_merge
+from plate_recognition.plate_rec import (
+    allFilePath,
+    cv_imread,
+    get_plate_result,
+    init_model,
+)
+from utils.cv_puttext import cv2ImgAddText
+from utils.datasets import letterbox
+from utils.general import check_img_size, non_max_suppression_face, scale_coords
+
+clors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (0, 255, 255)]
+danger = ["危", "险"]
+
+
+def order_points(pts):
+    rect = np.zeros((4, 2), dtype="float32")
+    s = pts.sum(axis=1)
+    rect[0] = pts[np.argmin(s)]
+    rect[2] = pts[np.argmax(s)]
+    diff = np.diff(pts, axis=1)
+    rect[1] = pts[np.argmin(diff)]
+    rect[3] = pts[np.argmax(diff)]
+    return rect
+
+
+def four_point_transform(image, pts):  # 透视变换得到车牌小图
+    rect = pts.astype("float32")
+    (tl, tr, br, bl) = rect
+    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
+    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
+    maxWidth = max(int(widthA), int(widthB))
+    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
+    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
+    maxHeight = max(int(heightA), int(heightB))
+    dst = np.array(
+        [[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]],
+        dtype="float32",
+    )
+    M = cv2.getPerspectiveTransform(rect, dst)
+    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
+    return warped
+
+
+def load_model(weights, device):  # 加载检测模型
+    model = attempt_load(weights, map_location=device)  # FP32 model
+    return model
+
+
+def scale_coords_landmarks(
+    img1_shape, coords, img0_shape, ratio_pad=None
+):  # 返回到原图坐标
+    if ratio_pad is None:  # calculate from img0_shape
+        gain = min(
+            img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]
+        )  # gain  = old / new
+        pad = (
+            (img1_shape[1] - img0_shape[1] * gain) / 2,
+            (img1_shape[0] - img0_shape[0] * gain) / 2,
+        )  # wh padding
+    else:
+        gain = ratio_pad[0][0]
+        pad = ratio_pad[1]
+
+    coords[:, [0, 2, 4, 6]] -= pad[0]  # x padding
+    coords[:, [1, 3, 5, 7]] -= pad[1]  # y padding
+    coords[:, :8] /= gain
+    coords[:, 0].clamp_(0, img0_shape[1])  # x1
+    coords[:, 1].clamp_(0, img0_shape[0])  # y1
+    coords[:, 2].clamp_(0, img0_shape[1])  # x2
+    coords[:, 3].clamp_(0, img0_shape[0])  # y2
+    coords[:, 4].clamp_(0, img0_shape[1])  # x3
+    coords[:, 5].clamp_(0, img0_shape[0])  # y3
+    coords[:, 6].clamp_(0, img0_shape[1])  # x4
+    coords[:, 7].clamp_(0, img0_shape[0])  # y4
+    # coords[:, 8].clamp_(0, img0_shape[1])  # x5
+    # coords[:, 9].clamp_(0, img0_shape[0])  # y5
+    return coords
+
+
+def get_plate_rec_landmark(
+    img, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=False
+):  # 获取车牌坐标以及四个角点坐标并识别车牌号
+    h, w, c = img.shape
+    result_dict = {}
+    1 or round(0.002 * (h + w) / 2) + 1
+
+    x1 = int(xyxy[0])
+    y1 = int(xyxy[1])
+    x2 = int(xyxy[2])
+    y2 = int(xyxy[3])
+    height = y2 - y1
+    landmarks_np = np.zeros((4, 2))
+    rect = [x1, y1, x2, y2]
+    for i in range(4):
+        point_x = int(landmarks[2 * i])
+        point_y = int(landmarks[2 * i + 1])
+        landmarks_np[i] = np.array([point_x, point_y])
+
+    class_label = int(class_num)  # 车牌的的类型0代表单层车牌,1代表双层车牌
+    roi_img = four_point_transform(img, landmarks_np)  # 透视变换得到车牌小图
+    if class_label:  # 判断是否是双层车牌,是双牌的话进行分割后然后拼接
+        roi_img = get_split_merge(roi_img)
+    if not is_color:
+        plate_number, rec_prob = get_plate_result(
+            roi_img, device, plate_rec_model, is_color=is_color
+        )  # 对车牌小图进行识别
+    else:
+        plate_number, rec_prob, plate_color, color_conf = get_plate_result(
+            roi_img, device, plate_rec_model, is_color=is_color
+        )
+    # cv2.imwrite("roi.jpg",roi_img)
+    result_dict["rect"] = rect  # 车牌roi区域
+    result_dict["detect_conf"] = conf  # 检测区域置信度
+    result_dict["landmarks"] = landmarks_np.tolist()  # 车牌角点坐标
+    result_dict["plate_no"] = plate_number  # 车牌号
+    result_dict["rec_conf"] = rec_prob  # 每个字符的概率
+    result_dict["roi_height"] = roi_img.shape[0]  # 车牌高度
+    result_dict["plate_color"] = ""
+    if is_color:
+        result_dict["plate_color"] = plate_color  # 车牌颜色
+        result_dict["color_conf"] = color_conf  # 颜色置信度
+    result_dict["plate_type"] = class_label  # 单双层 0单层 1双层
+
+    return result_dict
+
+
+def detect_Recognition_plate(
+    model, orgimg, device, plate_rec_model, img_size, is_color=False
+):  # 获取车牌信息
+    # img_size = opt_img_size
+    conf_thres = 0.3  ##### 置信度阈值 #####
+    iou_thres = 0.5  # nms的iou值
+    dict_list = []
+    img0 = copy.deepcopy(orgimg)
+    assert orgimg is not None, "Image Not Found "
+    h0, w0 = orgimg.shape[:2]
+    r = img_size / max(h0, w0)
+    if r != 1:
+        interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
+        img0 = cv2.resize(img0, (int(w0 * r), int(h0 * r)), interpolation=interp)
+
+    imgsz = check_img_size(img_size, s=model.stride.max())  # 检查 img_size
+
+    img = letterbox(img0, new_shape=imgsz)[
+        0
+    ]  # 检测前处理,图片长宽变为32倍数
+    img = (
+        img[:, :, ::-1].transpose(2, 0, 1).copy()
+    )  # 图片的BGR排列转为RGB,然后将图片的H,W,C排列变为C,H,W排列
+
+    t0 = time.time()
+
+    img = torch.from_numpy(img).to(device)
+    img = img.float()
+    img /= 255.0  # 0 - 255 to 0.0 - 1.0
+    if img.ndimension() == 3:
+        img = img.unsqueeze(0)
+
+    pred = model(img)[0]
+
+    pred = non_max_suppression_face(pred, conf_thres, iou_thres)
+
+    # 检测进程
+    for i, det in enumerate(pred):  # 对每张图片遍历
+        if len(det):
+            det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
+
+            # 打印结果
+            for c in det[:, -1].unique():
+                n = (det[:, -1] == c).sum()  # 检测每个种类
+
+            det[:, 5:13] = scale_coords_landmarks(
+                img.shape[2:], det[:, 5:13], orgimg.shape
+            ).round()
+
+            for j in range(det.size()[0]):
+                xyxy = det[j, :4].view(-1).tolist()
+                conf = det[j, 4].cpu().numpy()
+                landmarks = det[j, 5:13].view(-1).tolist()
+                class_num = det[j, 13].cpu().numpy()
+                result_dict = get_plate_rec_landmark(
+                    orgimg,
+                    xyxy,
+                    conf,
+                    landmarks,
+                    class_num,
+                    device,
+                    plate_rec_model,
+                    is_color=is_color,
+                )
+                dict_list.append(result_dict)
+    return dict_list
+    # cv2.imwrite('result.jpg', orgimg)
+
+
+def draw_result(orgimg, dict_list, is_color=True):  # 将车牌结果画出
+    result_str = ""
+    if dict_list:
+        pprint(dict_list)
+    
+    for result in dict_list:
+        rect_area = result["rect"]
+
+        x, y, w, h = (
+            rect_area[0],
+            rect_area[1],
+            rect_area[2] - rect_area[0],
+            rect_area[3] - rect_area[1],
+        )
+        padding_w = 0.05 * w
+        padding_h = 0.11 * h
+        rect_area[0] = max(0, int(x - padding_w))
+        rect_area[1] = max(0, int(y - padding_h))
+        rect_area[2] = min(orgimg.shape[1], int(rect_area[2] + padding_w))
+        rect_area[3] = min(orgimg.shape[0], int(rect_area[3] + padding_h))
+
+        height_area = result["roi_height"]
+        landmarks = result["landmarks"]
+        result_p = result["plate_no"]
+        if result["plate_type"] == 0:  # 单层
+            result_p += " " + result["plate_color"]
+        else:  # 双层
+            result_p += " " + result["plate_color"] + "双层"
+        result_str += result_p + " "
+        for i in range(4):  # 关键点
+            cv2.circle(
+                orgimg, (int(landmarks[i][0]), int(landmarks[i][1])), 5, clors[i], -1
+            )
+        cv2.rectangle(
+            orgimg,
+            (rect_area[0], rect_area[1]),
+            (rect_area[2], rect_area[3]),
+            (0, 0, 255),
+            2,
+        )  # 画框
+
+        labelSize = cv2.getTextSize(
+            result_p, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1
+        )  # 获得字体大小
+        if rect_area[0] + labelSize[0][0] > orgimg.shape[1]:  # 防止文字越界
+            rect_area[0] = int(orgimg.shape[1] - labelSize[0][0])
+        orgimg = cv2.rectangle(
+            orgimg,
+            (rect_area[0], int(rect_area[1] - round(1.6 * labelSize[0][1])-30)),
+            (
+                int(rect_area[0] + round(1.2 * labelSize[0][0]))+50,
+                rect_area[1] + labelSize[1],
+            ),
+            (255, 255, 255),
+            cv2.FILLED ,
+        )  # 画文字框,背景白色
+        if len(result) >= 1:
+            orgimg = cv2ImgAddText(
+                orgimg,
+                result_p,
+                rect_area[0],
+                int(rect_area[1] - round(1.6 * labelSize[0][1]))-30,
+                (0, 0, 0),
+                30,
+            )
+    if result_str:
+        print(result_str)
+    return orgimg
+
+
+def get_second(capture):
+    if capture.isOpened():
+        rate = capture.get(5)  # 帧速率
+        FrameNumber = capture.get(7)  # 视频文件的帧数
+        duration = FrameNumber / rate
+        return int(rate), int(FrameNumber), int(duration)
+
+
+def start(image_path="imgs"):  # 测试图片路径
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--detect_model",
+        nargs="+",
+        type=str,
+        default="weights/plate_detect.pt",
+        help="model.pt path(s)",
+    )  # 检测模型
+    parser.add_argument(
+        "--rec_model",
+        type=str,
+        default="weights/plate_rec_color.pth",
+        help="model.pt path(s)",
+    )  # 车牌识别+颜色识别模型
+    parser.add_argument(
+        "--is_color", type=bool, default=True, help="plate color"
+    )  # 识别颜色
+    parser.add_argument(
+        "--image_path", type=str, default=image_path, help="source"
+    )  # 图片路径
+    parser.add_argument(
+        "--img_size", type=int, default=640, help="inference size (pixels)"
+    )  # 输入图片大小
+    parser.add_argument(
+        "--output", type=str, default="result", help="source"
+    )  # 图片结果保存的位置
+    parser.add_argument("--video", type=str, default="", help="source")  # 视频的路径
+    parser.add_argument(
+        "--stream",
+        type=str,
+        default="",
+        help="RTSP/RTMP video stream URL"
+    )  # 视频流地址
+    device = torch.device(
+        "cuda" if torch.cuda.is_available() else "cpu"
+    )
+    # device =torch.device("cpu")
+    opt = parser.parse_args()
+    print(opt)
+    save_path = opt.output
+    count = 0
+    if not os.path.exists(save_path):
+        os.mkdir(save_path)
+
+    detect_model = load_model(
+        opt.detect_model, device
+    )  # 初始化检测模型
+    plate_rec_model = init_model(
+        device, opt.rec_model, is_color=opt.is_color
+    )  # 初始化识别模型
+    # 计算参数量
+    total = sum(p.numel() for p in detect_model.parameters())
+    total_1 = sum(p.numel() for p in plate_rec_model.parameters())
+    print("detect params: %.2fM,rec params: %.2fM" % (total / 1e6, total_1 / 1e6))
+
+    # plate_color_model =init_color_model(opt.color_model,device)
+    time_all = 0
+    time_begin = time.time()
+    # 处理视频流
+    if opt.stream:
+        cap = cv2.VideoCapture(opt.stream)
+        if not cap.isOpened():
+            print(f"无法打开视频流: {opt.stream}")
+            return
+        fps = cap.get(cv2.CAP_PROP_FPS) or 25
+        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
+        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+        out = cv2.VideoWriter("stream_result.mp4", cv2.VideoWriter_fourcc(*"MP4V"), fps, (width, height))
+        frame_count = 0
+        fps_all = 0
+        print(f"开始处理视频流: {opt.stream}")
+        while True:
+            t1 = cv2.getTickCount()
+            frame_count += 1
+            ret, img = cap.read()
+            if not ret:
+                print("视频流读取结束或出错")
+                break
+            dict_list = detect_Recognition_plate(
+                detect_model,
+                img,
+                device,
+                plate_rec_model,
+                opt.img_size,
+                is_color=opt.is_color,
+            )
+            ori_img = draw_result(img, dict_list)
+            t2 = cv2.getTickCount()
+            infer_time = (t2 - t1) / cv2.getTickFrequency()
+            fps = 1.0 / infer_time
+            fps_all += fps
+            str_fps = f"fps:{fps:.2f}"
+            cv2.putText(ori_img, str_fps, (20, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
+            cv2.imshow("Stream Result", ori_img)
+            out.write(ori_img)
+            if cv2.waitKey(1) & 0xFF == ord('q'):
+                break
+        cap.release()
+        out.release()
+        cv2.destroyAllWindows()
+        print(f"总帧数: {frame_count}, 平均FPS: {fps_all / frame_count:.2f}")
+
+    # 处理本地视频
+    elif opt.video:
+        video_name = opt.video
+        capture = cv2.VideoCapture(video_name)
+        fourcc = cv2.VideoWriter_fourcc(*"MP4V")
+        fps = capture.get(cv2.CAP_PROP_FPS)  # 帧数
+        width, height = (
+            int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
+            int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)),
+        )  # 宽高
+        out = cv2.VideoWriter("result.mp4", fourcc, fps, (width, height))  # 写入视频
+        frame_count = 0
+        fps_all = 0
+        rate, FrameNumber, duration = get_second(capture)
+        if capture.isOpened():
+            while True:
+                t1 = cv2.getTickCount()
+                frame_count += 1
+                print(f"第{frame_count} 帧", end=" ")
+                ret, img = capture.read()
+                if not ret:
+                    break
+                # if frame_count%rate==0:
+                img0 = copy.deepcopy(img)
+                dict_list = detect_Recognition_plate(
+                    detect_model,
+                    img,
+                    device,
+                    plate_rec_model,
+                    opt.img_size,
+                    is_color=opt.is_color,
+                )
+                ori_img = draw_result(img, dict_list)
+                t2 = cv2.getTickCount()
+                infer_time = (t2 - t1) / cv2.getTickFrequency()
+                fps = 1.0 / infer_time
+                fps_all += fps
+                str_fps = f"fps:{fps:.4f}"
+
+                cv2.putText(
+                    ori_img,
+                    str_fps,
+                    (20, 20),
+                    cv2.FONT_HERSHEY_SIMPLEX,
+                    1,
+                    (0, 255, 0),
+                    2,
+                )
+                cv2.imshow("haha", ori_img)
+                cv2.waitKey(0)
+                out.write(ori_img)
+    # 处理图片
+    else:
+        if not os.path.isfile(opt.image_path):  # 目录
+            file_list = []
+            allFilePath(
+                opt.image_path, file_list
+            )  # 将目录下的所有图片文件路径读取到file_list里面
+            for img_path in file_list:  # 遍历图片文件
+                print(count, img_path, end=" ")
+                time_b = time.time()  # 开始时间
+                img = cv_imread(img_path)  # opencv 读取图片
+
+                if img is None:
+                    continue
+                if img.shape[-1] == 4:  # 图片如果是4个通道的,将其转为3个通道
+                    img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
+                # detect_one(model,img_path,device)
+                dict_list = detect_Recognition_plate(
+                    detect_model,
+                    img,
+                    device,
+                    plate_rec_model,
+                    opt.img_size,
+                    is_color=opt.is_color,
+                )  # 检测以及识别车牌
+                pprint(dict_list)
+
+                ori_img = draw_result(img, dict_list)  # 将结果画在图上
+                img_name = os.path.basename(img_path)
+                save_img_path = os.path.join(save_path, img_name)  # 图片保存的路径
+                time_e = time.time()
+                time_gap = time_e - time_b  # 计算单个图片识别耗时
+                if count:
+                    time_all += time_gap
+                if isinstance(ori_img, cv2.UMat):
+                    ori_img = cv2.UMat.get(ori_img)
+                cv2.imwrite(save_img_path, ori_img)  # opencv将识别的图片保存
+                count += 1
+                # cv2.namedWindow("result", cv2.WINDOW_NORMAL)
+                # cv2.resizeWindow("result", 800, 600)
+
+                cv2.imshow("result", ori_img)
+                cv2.waitKey(0)
+                cv2.destroyAllWindows()
+            print(
+                f"sumTime time is {time.time() - time_begin} s, average pic time is {time_all / (len(file_list) - 1)}"
+            )
+        else:  # 单个图片
+            print(count, opt.image_path, end=" ")
+            img = cv_imread(opt.image_path)
+            if img.shape[-1] == 4:
+                img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
+            # detect_one(model,img_path,device)
+            dict_list = detect_Recognition_plate(
+                detect_model,
+                img,
+                device,
+                plate_rec_model,
+                opt.img_size,
+                is_color=opt.is_color,
+            )
+            ori_img = draw_result(img, dict_list)
+            img_name = os.path.basename(opt.image_path)
+            save_img_path = os.path.join(save_path, img_name)
+            cv2.imwrite(save_img_path, ori_img)
+
+
+
+if __name__ == '__main__':
+    start()

+ 723 - 0
detect_plate_20260130.py

@@ -0,0 +1,723 @@
+import argparse
+import copy
+import os
+import re
+import time
+from collections import deque
+from datetime import datetime
+from pprint import pprint
+
+import cv2
+import numpy as np
+import torch
+import redis
+
+import sys
+
+from models.experimental import attempt_load
+from plate_recognition.double_plate_split_merge import get_split_merge
+from plate_recognition.plate_rec import (
+    allFilePath,
+    cv_imread,
+    get_plate_result,
+    init_model,
+)
+from utils.datasets import letterbox
+from utils.general import check_img_size, non_max_suppression_face, scale_coords
+
+# Redis连接配置
+REDIS_HOST = 'localhost'
+REDIS_PORT = 6379
+REDIS_DB = 0
+REDIS_PASSWORD = None
+REDIS_KEY = 'plate_results'
+WINDOW_SIZE = 5
+
+# 新增:重连相关配置
+MAX_RECONNECT_ATTEMPTS = 10
+RECONNECT_DELAY = 5
+MAX_CONSECUTIVE_FAILURES = 5
+
+# 调整阈值设置 - 提高以减少误报
+DETECT_THRESH = 0.65
+COLOR_THRESH = 0.85
+REC_THRESH = 0.9
+
+# 初始化Redis连接
+try:
+    redis_client = redis.Redis(
+        host=REDIS_HOST,
+        port=REDIS_PORT,
+        db=REDIS_DB,
+        password=REDIS_PASSWORD,
+        decode_responses=True
+    )
+    redis_client.ping()
+    print("Redis连接成功")
+except Exception as e:
+    print(f"Redis连接失败: {e}")
+    redis_client = None
+
+
+def get_current_time():
+    """获取当前时间字符串"""
+    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+
+def get_current_timestamp():
+    """获取当前时间戳(秒级)"""
+    return int(time.time())
+
+
+def connect_stream(stream_url, cap_options=""):
+    """建立视频流连接,带重试机制"""
+    attempt = 0
+
+    while attempt < MAX_RECONNECT_ATTEMPTS:
+        try:
+            print(f"[{get_current_time()}] 尝试连接视频流: {stream_url} (第{attempt + 1}次)")
+
+            # 设置FFMPEG选项
+            if cap_options:
+                cap = cv2.VideoCapture()
+                os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = cap_options
+                success = cap.open(stream_url, cv2.CAP_FFMPEG)
+            else:
+                cap = cv2.VideoCapture(stream_url)
+                success = cap.isOpened()
+
+            if success:
+                # 设置缓冲区优化参数
+                cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)  # 减少缓冲区大小
+                cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'))  # H264
+
+                # 测试读取一帧
+                ret, frame = cap.read()
+                if ret:
+                    print(f"[{get_current_time()}] 视频流连接成功")
+                    return cap, True
+                else:
+                    print(f"[{get_current_time()}] 视频流打开但无法读取帧")
+                    cap.release()
+            else:
+                print(f"[{get_current_time()}] 无法打开视频流")
+
+            print(f"[{get_current_time()}] 连接失败,{RECONNECT_DELAY}秒后重试...")
+            time.sleep(RECONNECT_DELAY)
+            attempt += 1
+
+        except Exception as e:
+            print(f"[{get_current_time()}] 连接异常: {str(e)}")
+            time.sleep(RECONNECT_DELAY)
+            attempt += 1
+
+    print(f"[{get_current_time()}] 达到最大重连次数({MAX_RECONNECT_ATTEMPTS}),退出")
+    return None, False
+
+
+def reconnect_stream(stream_url, cap_options=""):
+    """重新连接视频流"""
+    print(f"[{get_current_time()}] 开始重新连接视频流...")
+
+    if 'cap' in globals() and cap:
+        cap.release()
+        time.sleep(2)  # 等待2秒再重连
+
+    return connect_stream(stream_url, cap_options)
+
+def clean_expired_data():
+    """清理超过5秒的旧数据"""
+    if redis_client is None:
+        return 0
+
+    try:
+        current_ts = get_current_timestamp()
+        cutoff_ts = current_ts - WINDOW_SIZE
+
+        pipe = redis_client.pipeline()
+        zset_key = f"{REDIS_KEY}:sorted"
+        expired_timestamps = redis_client.zrangebyscore(zset_key, 0, cutoff_ts)
+
+        if expired_timestamps:
+            hash_key = f"{REDIS_KEY}:data"
+            pipe.hdel(hash_key, *expired_timestamps)
+            pipe.zremrangebyscore(zset_key, 0, cutoff_ts)
+            pipe.execute()
+
+            deleted_count = len(expired_timestamps)
+            if deleted_count > 0:
+                print(f"[{get_current_time()}] 清理过期数据: {deleted_count}条 (>5秒)")
+            return deleted_count
+        else:
+            return 0
+
+    except Exception as e:
+        print(f"清理过期数据失败: {e}")
+        return 0
+
+
+def save_to_redis(plate_no, plate_color, detect_conf, color_conf, rec_avg):
+    """将车牌识别结果保存到Redis - 5秒滑动窗口"""
+    if redis_client is None:
+        return False, "Redis未连接"
+
+    try:
+        clean_expired_data()
+
+        timestamp = get_current_timestamp()
+        timestamp_ms = int(time.time() * 1000)
+
+        entry_data = {
+            "plate_no": plate_no,
+            "plate_color": plate_color,
+            "detect_conf": str(detect_conf),
+            "color_conf": str(color_conf),
+            "rec_avg": str(rec_avg),
+            "timestamp": str(timestamp),
+            "timestamp_ms": str(timestamp_ms),
+            "datetime": get_current_time(),
+            "source": "rtsp_stream"
+        }
+
+        pipe = redis_client.pipeline()
+        hash_key = f"{REDIS_KEY}:data"
+        pipe.hset(hash_key, timestamp, str(entry_data))
+
+        zset_key = f"{REDIS_KEY}:sorted"
+        pipe.zadd(zset_key, {timestamp: timestamp})
+        pipe.execute()
+
+        return True, f"保存到Redis成功: {timestamp}"
+    except Exception as e:
+        return False, f"保存到Redis失败: {e}"
+
+
+def get_recent_plates_from_redis():
+    """从Redis获取最近5秒内的所有车牌记录"""
+    if redis_client is None:
+        return []
+
+    try:
+        clean_expired_data()
+
+        zset_key = f"{REDIS_KEY}:sorted"
+        hash_key = f"{REDIS_KEY}:data"
+
+        current_ts = get_current_timestamp()
+        cutoff_ts = current_ts - WINDOW_SIZE
+        recent_timestamps = redis_client.zrangebyscore(zset_key, cutoff_ts, current_ts)
+
+        recent_timestamps = sorted(recent_timestamps, key=int, reverse=True)
+
+        results = []
+        for ts in recent_timestamps:
+            entry_str = redis_client.hget(hash_key, ts)
+            if entry_str:
+                try:
+                    data = eval(entry_str)
+                    results.append({
+                        'timestamp': int(ts),
+                        'data': data
+                    })
+                except:
+                    continue
+
+        return results
+    except Exception as e:
+        print(f"从Redis读取数据失败: {e}")
+        return []
+
+
+def get_window_info():
+    """获取滑动窗口的统计信息"""
+    if redis_client is None:
+        return {"count": 0, "window_size": WINDOW_SIZE}
+
+    try:
+        clean_expired_data()
+
+        hash_key = f"{REDIS_KEY}:data"
+        zset_key = f"{REDIS_KEY}:sorted"
+
+        data_count = redis_client.hlen(hash_key)
+        sorted_count = redis_client.zcard(zset_key)
+
+        if sorted_count > 0:
+            timestamps_with_scores = redis_client.zrange(zset_key, 0, -1, withscores=True)
+            if timestamps_with_scores:
+                timestamps = []
+                for member, score in timestamps_with_scores:
+                    try:
+                        timestamps.append(int(float(score)))
+                    except:
+                        continue
+
+                if timestamps:
+                    oldest_ts = min(timestamps)
+                    newest_ts = max(timestamps)
+                    time_range = newest_ts - oldest_ts
+                else:
+                    time_range = 0
+                    oldest_ts = newest_ts = int(time.time())
+            else:
+                time_range = 0
+                oldest_ts = newest_ts = int(time.time())
+        else:
+            time_range = 0
+            oldest_ts = newest_ts = int(time.time())
+
+        return {
+            "count": data_count,
+            "window_size": WINDOW_SIZE,
+            "time_range": time_range,
+            "oldest_record": datetime.fromtimestamp(oldest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无",
+            "newest_record": datetime.fromtimestamp(newest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无"
+        }
+    except Exception as e:
+        print(f"获取窗口信息失败: {e}")
+        return {"count": 0, "window_size": WINDOW_SIZE}
+
+
+def should_output(plate_no, last_output_dict, current_time):
+    """优化的去重逻辑:支持模糊匹配和更长的冷却时间"""
+    clean_plate = plate_no.strip().replace(' ', '').upper()
+
+    if len(clean_plate) < 5:
+        return False, "车牌太短"
+
+    similar_found = None
+    for existing_plate in list(last_output_dict.keys()):
+        if (clean_plate == existing_plate or
+                (len(clean_plate) >= 5 and len(existing_plate) >= 5 and
+                 clean_plate[:5] == existing_plate[:5])):
+            similar_found = existing_plate
+            break
+
+    if similar_found is None:
+        last_output_dict[clean_plate] = current_time
+        return True, "新车牌"
+    else:
+        last_time = last_output_dict[similar_found]
+        time_diff = current_time - last_time
+
+        if time_diff >= 3.0:
+            del last_output_dict[similar_found]
+            last_output_dict[clean_plate] = current_time
+            return True, "更新识别"
+        else:
+            return False, "冷却期内"
+
+
+def simple_plate_check(plate_no):
+    """最简单的车牌检查:只要不是空的和unknown就通过"""
+    plate_no = plate_no.strip()
+    if len(plate_no) < 4 or plate_no.lower() in ['unknown', '']:
+        return False, "太短或未知"
+    return True, "通过"
+
+
+def order_points(pts):
+    rect = np.zeros((4, 2), dtype="float32")
+    s = pts.sum(axis=1)
+    rect[0] = pts[np.argmin(s)]
+    rect[2] = pts[np.argmax(s)]
+    diff = np.diff(pts, axis=1)
+    rect[1] = pts[np.argmin(diff)]
+    rect[3] = pts[np.argmax(diff)]
+    return rect
+
+
+def four_point_transform(image, pts):
+    """透视变换得到车牌小图"""
+    rect = pts.astype("float32")
+    (tl, tr, br, bl) = rect
+    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
+    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
+    maxWidth = max(int(widthA), int(widthB))
+    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
+    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
+    maxHeight = max(int(heightA), int(heightB))
+    dst = np.array(
+        [[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]],
+        dtype="float32",
+    )
+    M = cv2.getPerspectiveTransform(rect, dst)
+    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
+    return warped
+
+
+def load_model(weights, device):
+    """加载检测模型"""
+    model = attempt_load(weights, map_location=device)
+    return model
+
+
+def scale_coords_landmarks(img1_shape, coords, img0_shape, ratio_pad=None):
+    """返回到原图坐标"""
+    if ratio_pad is None:
+        gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
+        pad = ((img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2)
+    else:
+        gain = ratio_pad[0][0]
+        pad = ratio_pad[1]
+
+    coords[:, [0, 2, 4, 6]] -= pad[0]
+    coords[:, [1, 3, 5, 7]] -= pad[1]
+    coords[:, :8] /= gain
+    coords[:, 0].clamp_(0, img0_shape[1])
+    coords[:, 1].clamp_(0, img0_shape[0])
+    coords[:, 2].clamp_(0, img0_shape[1])
+    coords[:, 3].clamp_(0, img0_shape[0])
+    coords[:, 4].clamp_(0, img0_shape[1])
+    coords[:, 5].clamp_(0, img0_shape[0])
+    coords[:, 6].clamp_(0, img0_shape[1])
+    coords[:, 7].clamp_(0, img0_shape[0])
+    return coords
+
+
+def get_plate_rec_landmark(img, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=False):
+    """获取车牌坐标以及四个角点坐标并识别车牌号"""
+    h, w, c = img.shape
+    result_dict = {}
+
+    x1 = int(xyxy[0])
+    y1 = int(xyxy[1])
+    x2 = int(xyxy[2])
+    y2 = int(xyxy[3])
+    height = y2 - y1
+    landmarks_np = np.zeros((4, 2))
+    rect = [x1, y1, x2, y2]
+    for i in range(4):
+        point_x = int(landmarks[2 * i])
+        point_y = int(landmarks[2 * i + 1])
+        landmarks_np[i] = np.array([point_x, point_y])
+
+    class_label = int(class_num)
+    roi_img = four_point_transform(img, landmarks_np)
+    if class_label:
+        roi_img = get_split_merge(roi_img)
+    if not is_color:
+        plate_number, rec_prob = get_plate_result(roi_img, device, plate_rec_model, is_color=is_color)
+    else:
+        plate_number, rec_prob, plate_color, color_conf = get_plate_result(roi_img, device, plate_rec_model,
+                                                                           is_color=is_color)
+    result_dict["rect"] = rect
+    result_dict["detect_conf"] = conf
+    result_dict["landmarks"] = landmarks_np.tolist()
+    result_dict["plate_no"] = plate_number
+    result_dict["rec_conf"] = rec_prob
+    result_dict["roi_height"] = roi_img.shape[0]
+    result_dict["plate_color"] = ""
+    if is_color:
+        result_dict["plate_color"] = plate_color
+        result_dict["color_conf"] = color_conf
+    result_dict["plate_type"] = class_num
+
+    return result_dict
+
+
+def detect_Recognition_plate(model, orgimg, device, plate_rec_model, img_size, is_color=False):
+    """获取车牌信息"""
+    conf_thres = 0.3
+    iou_thres = 0.5
+    dict_list = []
+    img0 = copy.deepcopy(orgimg)
+    assert orgimg is not None, "Image Not Found "
+    h0, w0 = orgimg.shape[:2]
+    r = img_size / max(h0, w0)
+    if r != 1:
+        interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
+        img0 = cv2.resize(img0, (int(w0 * r), int(h0 * r)), interpolation=interp)
+
+    imgsz = check_img_size(img_size, s=model.stride.max())
+
+    img = letterbox(img0, new_shape=imgsz)[0]
+    img = img[:, :, ::-1].transpose(2, 0, 1).copy()
+
+    t0 = time.time()
+
+    img = torch.from_numpy(img).to(device)
+    img = img.float()
+    img /= 255.0
+    if img.ndimension() == 3:
+        img = img.unsqueeze(0)
+
+    pred = model(img)[0]
+    pred = non_max_suppression_face(pred, conf_thres, iou_thres)
+
+    for i, det in enumerate(pred):
+        if len(det):
+            det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
+
+            for c in det[:, -1].unique():
+                n = (det[:, -1] == c).sum()
+
+            det[:, 5:13] = scale_coords_landmarks(img.shape[2:], det[:, 5:13], orgimg.shape).round()
+
+            for j in range(det.size()[0]):
+                xyxy = det[j, :4].view(-1).tolist()
+                conf = det[j, 4].cpu().numpy()
+                landmarks = det[j, 5:13].view(-1).tolist()
+                class_num = det[j, 13].cpu().numpy()
+                result_dict = get_plate_rec_landmark(orgimg, xyxy, conf, landmarks, class_num, device, plate_rec_model,
+                                                     is_color=is_color)
+                dict_list.append(result_dict)
+    return dict_list
+
+
+def start(image_path="imgs"):
+    """主函数"""
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--detect_model", nargs="+", type=str, default="weights/plate_detect.pt",
+                        help="model.pt path(s)")
+    parser.add_argument("--rec_model", type=str, default="weights/plate_rec_color.pth", help="model.pt path(s)")
+    parser.add_argument("--is_color", type=bool, default=True, help="plate color")
+    parser.add_argument("--image_path", type=str, default=image_path, help="source")
+    parser.add_argument("--img_size", type=int, default=640, help="inference size (pixels)")
+    parser.add_argument("--output", type=str, default="result", help="source")
+    parser.add_argument("--video", type=str, default="", help="source")
+    parser.add_argument("--stream", type=str, default="", help="RTSP/RTMP video stream URL")
+    parser.add_argument("--redis_host", type=str, default="localhost", help="Redis host")
+    parser.add_argument("--redis_port", type=int, default=6379, help="Redis port")
+    parser.add_argument("--redis_key", type=str, default="plate_results", help="Redis key name")
+    parser.add_argument("--window_size", type=int, default=5, help="Sliding window size in seconds")
+    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+    opt = parser.parse_args()
+
+    global REDIS_HOST, REDIS_PORT, REDIS_KEY, WINDOW_SIZE, redis_client
+    REDIS_HOST = opt.redis_host
+    REDIS_PORT = opt.redis_port
+    REDIS_KEY = opt.redis_key
+    WINDOW_SIZE = opt.window_size
+
+    print("=" * 60)
+    print("最终优化版:5秒滑动窗口,自动清理过期数据")
+    print("=" * 60)
+    print(f"阈值: 检测{DETECT_THRESH} 颜色{COLOR_THRESH} 识别{REC_THRESH}")
+    print(f"Redis: {REDIS_HOST}:{REDIS_PORT}")
+    print(f"Redis键: {REDIS_KEY}")
+    print(f"滑动窗口: {WINDOW_SIZE}秒")
+    print("格式验证: 放宽标准,允许不完整车牌")
+    print("去重策略: 前5字符相同视为同一车牌,3秒冷却")
+    print("输出内容: 车牌、时间、车牌颜色")
+    print("=" * 60)
+    print(opt)
+
+    try:
+        redis_client = redis.Redis(
+            host=REDIS_HOST,
+            port=REDIS_PORT,
+            db=REDIS_DB,
+            password=REDIS_PASSWORD,
+            decode_responses=True
+        )
+        redis_client.ping()
+        print("Redis连接成功")
+    except Exception as e:
+        print(f"Redis连接失败: {e}")
+        redis_client = None
+
+    save_path = opt.output
+    if not os.path.exists(save_path):
+        os.mkdir(save_path)
+
+    detect_model = load_model(opt.detect_model, device)
+    plate_rec_model = init_model(device, opt.rec_model, is_color=opt.is_color)
+
+    total = sum(p.numel() for p in detect_model.parameters())
+    total_1 = sum(p.numel() for p in plate_rec_model.parameters())
+    print("detect params: %.2fM,rec params: %.2fM" % (total / 1e6, total_1 / 1e6))
+
+    if opt.stream:
+        # 设置FFMPEG选项
+        cap_options = (
+            "rtsp_transport;tcp;"
+            "buffer_size;1024000;"
+            "timeout;5000000"
+        )
+
+        # 初始连接
+        cap, connected = connect_stream(opt.stream, cap_options)
+        if not connected:
+            print(f"[{get_current_time()}] 初始连接失败,退出程序")
+            return
+
+        consecutive_failures = 0
+        reconnect_count = 0
+
+        actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
+        actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+        actual_fps = cap.get(cv2.CAP_PROP_FPS)
+        print(f"实际视频流参数: {actual_width}x{actual_height} @ {actual_fps:.1f}fps")
+
+        frame_count = 0
+        processed_count = 0
+        last_print_time = time.time()
+        print_interval = 8.0
+
+        print(f"开始处理: {opt.stream}")
+        print("等待车牌出现...")
+
+        inference_times = deque(maxlen=30)
+        last_output_dict = {}
+        output_count = 0
+
+        try:
+            while True:
+                frame_count += 1
+
+                # 读取帧
+                ret, frame = cap.read()
+                if not ret:
+                    consecutive_failures += 1
+                    print(f"[{get_current_time()}] 视频流中断 (连续失败{consecutive_failures}次),尝试重新连接...")
+
+                    # 如果连续失败太多,退出程序
+                    if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
+                        print(f"[{get_current_time()}] 连续失败次数过多,停止重连")
+                        break
+
+                    # 尝试重新连接
+                    cap, reconnected = reconnect_stream(opt.stream, cap_options)
+                    if reconnected:
+                        reconnect_count += 1
+                        consecutive_failures = 0  # 重置失败计数
+                        print(f"[{get_current_time()}] 重新连接成功 (第{reconnect_count}次)")
+                        frame_count = 0  # 重置帧计数
+                        continue
+                    else:
+                        print(f"[{get_current_time()}] 重新连接失败,{RECONNECT_DELAY}秒后再次尝试...")
+                        time.sleep(RECONNECT_DELAY)
+                        continue
+
+                # 重置连续失败计数
+                consecutive_failures = 0
+
+                should_process = (frame_count % 3 == 0)
+                output_this_cycle = 0
+
+                if should_process:
+                    processed_count += 1
+
+                    try:
+                        inference_start = time.time()
+                        dict_list = detect_Recognition_plate(detect_model, frame, device, plate_rec_model, opt.img_size,
+                                                             is_color=opt.is_color)
+
+                        inference_time = time.time() - inference_start
+                        inference_times.append(inference_time)
+
+                        current_time = time.time()
+                        current_time_str = get_current_time()
+
+                        for res in dict_list:
+                            plate_no = res['plate_no'].strip()
+
+                            if len(plate_no) < 4 or plate_no.lower() in ['unknown', '']:
+                                continue
+
+                            detect_conf = float(res['detect_conf'])
+                            color_conf = res.get('color_conf', 0.0)
+                            rec_conf = res.get('rec_conf', [])
+                            rec_avg = np.mean(rec_conf) if isinstance(rec_conf, (list, np.ndarray)) and len(
+                                rec_conf) > 0 else 0.0
+                            plate_color = res.get('plate_color', '未知')
+
+                            if detect_conf < DETECT_THRESH or color_conf < COLOR_THRESH or rec_avg < REC_THRESH:
+                                continue
+
+                            is_valid, reason = simple_plate_check(plate_no)
+                            if not is_valid:
+                                continue
+
+                            ok, output_reason = should_output(plate_no, last_output_dict, current_time)
+                            if ok:
+                                output_line = f"[{current_time_str}] [有效] {plate_no} | 检:{detect_conf:.3f} 色:{color_conf:.3f} 识:{rec_avg:.3f} | {plate_color} | {output_reason}"
+                                print(output_line)
+
+                                if redis_client:
+                                    save_success, save_msg = save_to_redis(plate_no, plate_color, detect_conf,
+                                                                           color_conf, rec_avg)
+                                    if save_success:
+                                        output_line += f" | {save_msg}"
+                                    else:
+                                        output_line += f" | {save_msg}"
+
+                                output_count += 1
+                                last_output_dict[plate_no.replace(' ', '').upper()] = current_time
+                                output_this_cycle += 1
+
+                    except Exception as e:
+                        print(f"[{get_current_time()}] 处理异常: {e}")
+                        continue
+
+                current_time = time.time()
+                if current_time - last_print_time >= print_interval:
+                    avg_inference = sum(inference_times) / len(inference_times) if inference_times else 0
+                    unique_plates = len(last_output_dict)
+
+                    print(f"\n[{get_current_time()}] 状态 @{frame_count}")
+                    print(f"处理帧率: {processed_count / (current_time - last_print_time + 0.1):.1f}fps")
+                    print(f"平均推理: {avg_inference * 1000:.1f}ms")
+                    print(f"输出车牌: {output_count}个(累计) | 缓存种类: {unique_plates}种")
+
+                    if redis_client:
+                        try:
+                            window_info = get_window_info()
+                            print(f"滑动窗口: {window_info['count']}条记录/{window_info['window_size']}秒")
+                            if window_info['count'] > 0:
+                                print(f"时间范围: {window_info['oldest_record']} ~ {window_info['newest_record']}")
+
+                                recent_plates = get_recent_plates_from_redis()
+                                if recent_plates:
+                                    print(f"窗口内车牌:")
+                                    for i, record in enumerate(recent_plates, 1):
+                                        data = record['data']
+                                        age = get_current_timestamp() - record['timestamp']
+                                        print(f"  {i}. {data['plate_no']} ({data['plate_color']}) - [{age}秒前]")
+                            else:
+                                print(f"窗口内暂无记录")
+
+                        except Exception as e:
+                            print(f"读取窗口数据失败: {e}")
+
+                    last_print_time = current_time
+
+                if frame_count % 100 == 0:
+                    print(f"\r[{get_current_time()}] 运行中: {frame_count}F", end="", flush=True)
+
+                if cv2.waitKey(1) & 0xFF == ord('q'):
+                    break
+
+        except KeyboardInterrupt:
+            print(f"\n[{get_current_time()}] 用户中断")
+        except Exception as e:
+            print(f"\n[{get_current_time()}] 错误: {e}")
+        finally:
+            if 'cap' in globals() and cap:
+                cap.release()
+            cv2.destroyAllWindows()
+
+            print(f"\n[{get_current_time()}] 结束报告")
+            print(f"总帧数: {frame_count} | 处理帧: {processed_count}")
+            if inference_times:
+                avg_inf = sum(inference_times) / len(inference_times)
+                print(f"平均推理: {avg_inf * 1000:.1f}ms | 实时FPS: {1 / avg_inf:.1f}")
+            print(f"实际输出车牌: {output_count}个")
+            print(f"识别到车牌种类: {len(last_output_dict)}种")
+            print(f"重新连接次数: {reconnect_count}")
+
+            if redis_client:
+                try:
+                    clean_expired_data()
+                    hash_key = f"{REDIS_KEY}:data"
+                    zset_key = f"{REDIS_KEY}:sorted"
+                    data_count = redis_client.hlen(hash_key)
+                    sorted_count = redis_client.zcard(zset_key)
+                    print(f"最终窗口统计: {data_count}条记录在{WINDOW_SIZE}秒内")
+                except:
+                    pass
+
+
+if __name__ == '__main__':
+    start()

+ 161 - 0
export.py

@@ -0,0 +1,161 @@
+"""Exports a YOLOv5 *.pt model to ONNX and TorchScript formats
+
+Usage:
+    $ export PYTHONPATH="$PWD" && python models/export.py --weights ./weights/yolov5s.pt --img 640 --batch 1
+"""
+
+import argparse
+import sys
+import time
+
+sys.path.append('./')  # to run '$ python *.py' files in subdirectories
+
+import torch
+import torch.nn as nn
+
+import models
+from models.experimental import attempt_load
+from utils.activations import Hardswish, SiLU
+from utils.general import set_logging, check_img_size
+import onnx
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--weights', type=str, default='./yolov5s.pt', help='weights path')  # from yolov5/models/
+    parser.add_argument('--img_size', nargs='+', type=int, default=[640, 640], help='image size')  # height, width
+    parser.add_argument('--batch_size', type=int, default=1, help='batch size')
+    parser.add_argument('--dynamic', action='store_true', default=False, help='enable dynamic axis in onnx model')
+    parser.add_argument('--onnx2pb', action='store_true', default=False, help='export onnx to pb')
+    parser.add_argument('--onnx_infer', action='store_true', default=True, help='onnx infer test')
+    #=======================TensorRT=================================
+    parser.add_argument('--onnx2trt', action='store_true', default=False, help='export onnx to tensorrt')
+    parser.add_argument('--fp16_trt', action='store_true', default=False, help='fp16 infer')
+    #================================================================
+    opt = parser.parse_args()
+    opt.img_size *= 2 if len(opt.img_size) == 1 else 1  # expand
+    print(opt)
+    set_logging()
+    t = time.time()
+
+    # Load PyTorch model
+    model = attempt_load(opt.weights, map_location=torch.device('cpu'))  # load FP32 model
+    delattr(model.model[-1], 'anchor_grid')
+    model.model[-1].anchor_grid=[torch.zeros(1)] * 3 # nl=3 number of detection layers
+    model.model[-1].export_cat = True
+    model.eval()
+    labels = model.names
+
+    # Checks
+    gs = int(max(model.stride))  # grid size (max stride)
+    opt.img_size = [check_img_size(x, gs) for x in opt.img_size]  # verify img_size are gs-multiples
+
+    # Input
+    img = torch.zeros(opt.batch_size, 3, *opt.img_size)  # image size(1,3,320,192) iDetection
+
+    # Update model
+    for k, m in model.named_modules():
+        m._non_persistent_buffers_set = set()  # pytorch 1.6.0 compatibility
+        if isinstance(m, models.common.Conv):  # assign export-friendly activations
+            if isinstance(m.act, nn.Hardswish):
+                m.act = Hardswish()
+            elif isinstance(m.act, nn.SiLU):
+                m.act = SiLU()
+        # elif isinstance(m, models.yolo.Detect):
+        #     m.forward = m.forward_export  # assign forward (optional)
+        if isinstance(m, models.common.ShuffleV2Block):#shufflenet block nn.SiLU
+            for i in range(len(m.branch1)):
+                if isinstance(m.branch1[i], nn.SiLU):
+                    m.branch1[i] = SiLU()
+            for i in range(len(m.branch2)):
+                if isinstance(m.branch2[i], nn.SiLU):
+                    m.branch2[i] = SiLU()
+        if isinstance(m, models.common.BlazeBlock):#shufflenet block nn.SiLU
+            if isinstance(m.relu, nn.SiLU):
+                m.relu = SiLU()
+        if isinstance(m, models.common.DoubleBlazeBlock):#shufflenet block nn.SiLU
+            if isinstance(m.relu, nn.SiLU):
+                m.relu = SiLU()
+            for i in range(len(m.branch1)):
+                if isinstance(m.branch1[i], nn.SiLU):
+                    m.branch1[i] = SiLU()
+            # for i in range(len(m.branch2)):
+            #     if isinstance(m.branch2[i], nn.SiLU):
+            #         m.branch2[i] = SiLU()
+    y = model(img)  # dry run
+
+    # ONNX export
+    print('\nStarting ONNX export with onnx %s...' % onnx.__version__)
+    f = opt.weights.replace('.pt', '.onnx')  # filename
+    model.fuse()  # only for ONNX
+    input_names=['input']
+    output_names=['output']
+    #tensorrt 7
+    # grid = model.model[-1].anchor_grid
+    # model.model[-1].anchor_grid = [a[..., :1, :1, :] for a in grid]
+    #tensorrt 7
+
+    torch.onnx.export(model, img, f, verbose=False, opset_version=12, 
+        input_names=input_names,
+        output_names=output_names,
+        dynamic_axes = {'input': {0: 'batch'},
+                        'output': {0: 'batch'}
+                        } if opt.dynamic else None)
+                        
+    # model.model[-1].anchor_grid = grid
+
+    # Checks
+    onnx_model = onnx.load(f)  # load onnx model
+    onnx.checker.check_model(onnx_model)  # check onnx model
+    print('ONNX export success, saved as %s' % f)
+    # Finish
+    print('\nExport complete (%.2fs). Visualize with https://github.com/lutzroeder/netron.' % (time.time() - t))
+
+
+    # onnx infer
+    if opt.onnx_infer:
+        import onnxruntime
+        import numpy as np
+        providers =  ['CPUExecutionProvider']
+        session = onnxruntime.InferenceSession(f, providers=providers)
+        im = img.cpu().numpy().astype(np.float32) # torch to numpy
+        y_onnx = session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: im})[0]
+        print("pred's shape is ",y_onnx.shape)
+        print("max(|torch_pred - onnx_pred|) =",abs(y.cpu().numpy()-y_onnx).max())
+
+
+    # TensorRT export
+    if opt.onnx2trt:
+        from torch2trt.trt_model import ONNX_to_TRT
+        print('\nStarting TensorRT...')
+        ONNX_to_TRT(onnx_model_path=f,trt_engine_path=f.replace('.onnx', '.trt'),fp16_mode=opt.fp16_trt)
+
+    # PB export
+    if opt.onnx2pb:
+        print('download the newest onnx_tf by https://github.com/onnx/onnx-tensorflow/tree/master/onnx_tf')
+        from onnx_tf.backend import prepare
+        import tensorflow as tf
+
+        outpb = f.replace('.onnx', '.pb')  # filename
+        # strict=True maybe leads to KeyError: 'pyfunc_0', check: https://github.com/onnx/onnx-tensorflow/issues/167
+        tf_rep = prepare(onnx_model, strict=False)  # prepare tf representation
+        tf_rep.export_graph(outpb)  # export the model
+
+        out_onnx = tf_rep.run(img) # onnx output
+
+        # check pb
+        with tf.Graph().as_default():
+            graph_def = tf.GraphDef()
+            with open(outpb, "rb") as f:
+                graph_def.ParseFromString(f.read())
+                tf.import_graph_def(graph_def, name="")
+            with tf.Session() as sess:
+                init = tf.global_variables_initializer()
+                input_x = sess.graph.get_tensor_by_name(input_names[0]+':0')  # input
+                outputs = []
+                for i in output_names:
+                    outputs.append(sess.graph.get_tensor_by_name(i+':0'))
+                out_pb = sess.run(outputs, feed_dict={input_x: img})
+
+        print(f'out_pytorch {y}')
+        print(f'out_onnx {out_onnx}')
+        print(f'out_pb {out_pb}')

BIN
first_frame_debug.jpg


+ 141 - 0
hubconf.py

@@ -0,0 +1,141 @@
+"""File for accessing YOLOv5 via PyTorch Hub https://pytorch.org/hub/
+
+Usage:
+    import torch
+    model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True, channels=3, classes=80)
+"""
+
+from pathlib import Path
+
+import torch
+
+from models.yolo import Model
+from utils.general import set_logging
+from utils.google_utils import attempt_download
+
+dependencies = ['torch', 'yaml']
+set_logging()
+
+
+def create(name, pretrained, channels, classes, autoshape):
+    """Creates a specified YOLOv5 model
+
+    Arguments:
+        name (str): name of model, i.e. 'yolov5s'
+        pretrained (bool): load pretrained weights into the model
+        channels (int): number of input channels
+        classes (int): number of model classes
+
+    Returns:
+        pytorch model
+    """
+    config = Path(__file__).parent / 'models' / f'{name}.yaml'  # model.yaml path
+    try:
+        model = Model(config, channels, classes)
+        if pretrained:
+            fname = f'{name}.pt'  # checkpoint filename
+            attempt_download(fname)  # download if not found locally
+            ckpt = torch.load(fname, map_location=torch.device('cpu'))  # load
+            state_dict = ckpt['model'].float().state_dict()  # to FP32
+            state_dict = {k: v for k, v in state_dict.items() if model.state_dict()[k].shape == v.shape}  # filter
+            model.load_state_dict(state_dict, strict=False)  # load
+            if len(ckpt['model'].names) == classes:
+                model.names = ckpt['model'].names  # set class names attribute
+            if autoshape:
+                model = model.autoshape()  # for file/URI/PIL/cv2/np inputs and NMS
+        return model
+
+    except Exception as e:
+        help_url = 'https://github.com/ultralytics/yolov5/issues/36'
+        s = 'Cache maybe be out of date, try force_reload=True. See %s for help.' % help_url
+        raise Exception(s) from e
+
+
+def yolov5s(pretrained=False, channels=3, classes=80, autoshape=True):
+    """YOLOv5-small model from https://github.com/ultralytics/yolov5
+
+    Arguments:
+        pretrained (bool): load pretrained weights into the model, default=False
+        channels (int): number of input channels, default=3
+        classes (int): number of model classes, default=80
+
+    Returns:
+        pytorch model
+    """
+    return create('yolov5s', pretrained, channels, classes, autoshape)
+
+
+def yolov5m(pretrained=False, channels=3, classes=80, autoshape=True):
+    """YOLOv5-medium model from https://github.com/ultralytics/yolov5
+
+    Arguments:
+        pretrained (bool): load pretrained weights into the model, default=False
+        channels (int): number of input channels, default=3
+        classes (int): number of model classes, default=80
+
+    Returns:
+        pytorch model
+    """
+    return create('yolov5m', pretrained, channels, classes, autoshape)
+
+
+def yolov5l(pretrained=False, channels=3, classes=80, autoshape=True):
+    """YOLOv5-large model from https://github.com/ultralytics/yolov5
+
+    Arguments:
+        pretrained (bool): load pretrained weights into the model, default=False
+        channels (int): number of input channels, default=3
+        classes (int): number of model classes, default=80
+
+    Returns:
+        pytorch model
+    """
+    return create('yolov5l', pretrained, channels, classes, autoshape)
+
+
+def yolov5x(pretrained=False, channels=3, classes=80, autoshape=True):
+    """YOLOv5-xlarge model from https://github.com/ultralytics/yolov5
+
+    Arguments:
+        pretrained (bool): load pretrained weights into the model, default=False
+        channels (int): number of input channels, default=3
+        classes (int): number of model classes, default=80
+
+    Returns:
+        pytorch model
+    """
+    return create('yolov5x', pretrained, channels, classes, autoshape)
+
+
+def custom(path_or_model='path/to/model.pt', autoshape=True):
+    """YOLOv5-custom model from https://github.com/ultralytics/yolov5
+
+    Arguments (3 options):
+        path_or_model (str): 'path/to/model.pt'
+        path_or_model (dict): torch.load('path/to/model.pt')
+        path_or_model (nn.Module): torch.load('path/to/model.pt')['model']
+
+    Returns:
+        pytorch model
+    """
+    model = torch.load(path_or_model) if isinstance(path_or_model, str) else path_or_model  # load checkpoint
+    if isinstance(model, dict):
+        model = model['model']  # load model
+
+    hub_model = Model(model.yaml).to(next(model.parameters()).device)  # create
+    hub_model.load_state_dict(model.float().state_dict())  # load state_dict
+    hub_model.names = model.names  # class names
+    return hub_model.autoshape() if autoshape else hub_model
+
+
+if __name__ == '__main__':
+    model = create(name='yolov5s', pretrained=True, channels=3, classes=80, autoshape=True)  # pretrained example
+    # model = custom(path_or_model='path/to/model.pt')  # custom example
+
+    # Verify inference
+    from PIL import Image
+
+    imgs = [Image.open(x) for x in Path('data/images').glob('*.jpg')]
+    results = model(imgs)
+    results.show()
+    results.print()

+ 121 - 0
json2yolo.py

@@ -0,0 +1,121 @@
+import json
+import os
+import numpy as np
+from copy import deepcopy
+import cv2
+
+def allFilePath(rootPath,allFIleList):
+    fileList = os.listdir(rootPath)
+    for temp in fileList:
+        if os.path.isfile(os.path.join(rootPath,temp)):
+            allFIleList.append(os.path.join(rootPath,temp))
+        else:
+            allFilePath(os.path.join(rootPath,temp),allFIleList)
+
+def xywh2yolo(rect,landmarks_sort,img):
+    h,w,c =img.shape
+    rect[0] = max(0, rect[0])
+    rect[1] = max(0, rect[1])
+    rect[2] = min(w - 1, rect[2]-rect[0])
+    rect[3] = min(h - 1, rect[3]-rect[1])
+    annotation = np.zeros((1, 12))
+    annotation[0, 0] = (rect[0] + rect[2] / 2) / w  # cx
+    annotation[0, 1] = (rect[1] + rect[3] / 2) / h  # cy
+    annotation[0, 2] = rect[2] / w  # w
+    annotation[0, 3] = rect[3] / h  # h
+
+    annotation[0, 4] = landmarks_sort[0][0] / w  # l0_x
+    annotation[0, 5] = landmarks_sort[0][1] / h  # l0_y
+    annotation[0, 6] = landmarks_sort[1][0] / w  # l1_x
+    annotation[0, 7] = landmarks_sort[1][1] / h  # l1_y
+    annotation[0, 8] = landmarks_sort[2][0] / w  # l2_x
+    annotation[0, 9] = landmarks_sort[2][1] / h # l2_y
+    annotation[0, 10] = landmarks_sort[3][0] / w  # l3_x
+    annotation[0, 11] = landmarks_sort[3][1] / h  # l3_y
+    # annotation[0, 12] = (landmarks_sort[0][0]+landmarks_sort[1][0])/2 / w  # l4_x
+    # annotation[0, 13] = (landmarks_sort[0][1]+landmarks_sort[1][1])/2 / h  # l4_y
+    return annotation            
+            
+def order_points(pts):
+    rect = np.zeros((4, 2), dtype = "float32")
+    s = pts.sum(axis = 1)
+    rect[0] = pts[np.argmin(s)]
+    rect[2] = pts[np.argmax(s)]
+    diff = np.diff(pts, axis = 1)
+    rect[1] = pts[np.argmin(diff)]
+    rect[3] = pts[np.argmax(diff)]
+ 
+    # return the ordered coordinates
+    return rect
+
+def four_point_transform(image, pts):
+    rect = order_points(pts)
+    (tl, tr, br, bl) = rect
+    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
+    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
+    maxWidth = max(int(widthA), int(widthB))
+    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
+    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
+    maxHeight = max(int(heightA), int(heightB))
+    dst = np.array([
+        [0, 0],
+        [maxWidth - 1, 0],
+        [maxWidth - 1, maxHeight - 1],
+        [0, maxHeight - 1]], dtype = "float32")
+    M = cv2.getPerspectiveTransform(rect, dst)
+    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
+ 
+    # return the warped image
+    return warped
+            
+if __name__ == "__main__":
+    pic_file_list = []
+    pic_file = r"/mnt/Gpan/Mydata/pytorchPorject/datasets/ccpd/train_bisai/train_bisai"
+    save_small_path = "small"
+    label_file = ['0','1']
+    allFilePath(pic_file,pic_file_list)
+    count=0
+    index = 0
+    for pic_ in pic_file_list:
+        if not pic_.endswith(".jpg"):
+            continue
+        count+=1
+        img = cv2.imread(pic_)
+        img_name = os.path.basename(pic_)
+        txt_name = img_name.replace(".jpg",".txt")
+        txt_path = os.path.join(pic_file,txt_name)
+        json_file_ = pic_.replace(".jpg",".json")
+        if not os.path.exists(json_file_):
+            continue
+        with open(json_file_, 'r',encoding='utf-8') as a:
+            data_dict = json.load(a)
+            # print(data_dict['shapes'])
+            with open(txt_path,"w") as f:
+                for  data_message in data_dict['shapes']:
+                    index+=1
+                    label=data_message['label']
+                    points = data_message['points']
+                    pts = np.array(points)
+                    # pts=order_points(pts)
+                    # new_img = four_point_transform(img,pts)
+                    roi_img_name = label+"_"+str(index)+".jpg"
+                    save_path=os.path.join(save_small_path,roi_img_name)
+                    # cv2.imwrite(save_path,new_img)
+                    x_max,y_max = np.max(pts,axis=0)
+                    x_min,y_min = np.min(pts,axis=0)
+                    rect = [x_min,y_min,x_max,y_max]
+                    rect1=deepcopy(rect)
+                    annotation=xywh2yolo(rect1,pts,img)
+                    print(data_message)
+                    label = data_message['label']
+                    str_label = label_file.index(label)
+                    # str_label = "0 "
+                    str_label = str(str_label)+" "
+                    for i in range(len(annotation[0])):
+                            str_label = str_label + " " + str(annotation[0][i])
+                    str_label = str_label.replace('[', '').replace(']', '')
+                    str_label = str_label.replace(',', '') + '\n'
+
+                    f.write(str_label)
+            print(count,img_name)
+                # point=data_message[points]

+ 80 - 0
main.py

@@ -0,0 +1,80 @@
+import dearpygui.dearpygui as dpg
+
+
+def  draw1():
+    dpg.create_context()
+
+    width, height, channels, data = dpg.load_image(
+        "image/car2.jpg"
+    )  # 0: width, 1: height, 2: channels, 3: data
+
+    with dpg.texture_registry():
+        dpg.add_static_texture(width, height, data, tag="image_id")
+
+    with dpg.window(label="Tutorial"):
+        with dpg.drawlist(width=700, height=700):
+            dpg.draw_image("image_id", (0, 400), (200, 600), uv_min=(0, 0), uv_max=(1, 1))
+
+    dpg.create_viewport(title="Custom Title", width=800, height=600)
+    dpg.setup_dearpygui()
+    dpg.show_viewport()
+    dpg.start_dearpygui()
+
+
+def draw2():
+    dpg.create_context()
+
+
+
+    width, height, channels, data = dpg.load_image("image/car1.jpg")
+
+    with dpg.texture_registry(show=True):
+        dpg.add_static_texture(
+            width=width, height=height, default_value=data, tag="texture_tag"
+        )
+
+    with dpg.window(label="Tutorial"):
+        dpg.add_image("texture_tag")
+
+    dpg.create_viewport(title="车牌识别", width=1200, height=900)
+    dpg.setup_dearpygui()
+    dpg.show_viewport()
+    dpg.start_dearpygui()
+    dpg.destroy_context()
+
+
+def save():
+    import dearpygui.dearpygui as dpg
+
+    dpg.create_context()
+    dpg.create_viewport()
+    dpg.setup_dearpygui()
+
+    width, height = 255, 255
+
+    data = []
+    for i in range(width * height):
+        data.append(255)
+        data.append(255)
+        data.append(0)
+
+    with dpg.window(label="Tutorial"):
+        dpg.add_button(
+            label="Save Image",
+            callback=lambda: dpg.save_image(
+                file="newImage.png", width=width, height=height, data=data, components=3
+            ),
+        )
+
+    dpg.show_viewport()
+    while dpg.is_dearpygui_running():
+        dpg.render_dearpygui_frame()
+
+    dpg.destroy_context()
+
+import os
+
+cwd = os.getcwd()
+recognize_result_path = os.path.join("result", "bucket")
+recognize_result_path =  cwd+"\\"+recognize_result_path
+print(recognize_result_path)

+ 0 - 0
models/__init__.py


+ 33 - 0
models/blazeface.yaml

@@ -0,0 +1,33 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 1.0  # model depth multiple
+width_multiple: 1.0  # layer channel multiple
+
+# anchors
+anchors:
+  - [5,6,  10,13,  21,26]  # P3/8
+  - [55,72,  225,304,  438,553]  # P4/16
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [[-1, 1, Conv, [24, 3, 2]], # 0-P1/2
+   [-1, 2, BlazeBlock, [24]], # 1
+   [-1, 1, BlazeBlock, [48, None, 2]], # 2-P2/4
+   [-1, 2, BlazeBlock, [48]], # 3
+   [-1, 1, DoubleBlazeBlock, [96, 24, 2]], # 4-P3/8
+   [-1, 2, DoubleBlazeBlock, [96, 24]], # 5
+   [-1, 1, DoubleBlazeBlock, [96, 24, 2]], # 6-P4/16
+   [-1, 2, DoubleBlazeBlock, [96, 24]], # 7
+  ]
+
+
+# YOLOv5 head
+head:
+  [[-1, 1, Conv, [64, 1, 1]],  # 8 (P4/32-large)
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 5], 1, Concat, [1]],  # cat backbone P3
+   [-1, 1, Conv, [64, 1, 1]],  # 11 (P3/8-medium)
+
+   [[11, 8], 1, Detect, [nc, anchors]],  # Detect(P3, P4)
+  ]

+ 38 - 0
models/blazeface_fpn.yaml

@@ -0,0 +1,38 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 1.0  # model depth multiple
+width_multiple: 1.0  # layer channel multiple
+
+# anchors
+anchors:
+  - [5,6,  10,13,  21,26]  # P3/8
+  - [55,72,  225,304,  438,553]  # P4/16
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [[-1, 1, Conv, [24, 3, 2]], # 0-P1/2
+   [-1, 2, BlazeBlock, [24]], # 1
+   [-1, 1, BlazeBlock, [48, None, 2]], # 2-P2/4
+   [-1, 2, BlazeBlock, [48]], # 3
+   [-1, 1, DoubleBlazeBlock, [96, 24, 2]], # 4-P3/8
+   [-1, 2, DoubleBlazeBlock, [96, 24]], # 5
+   [-1, 1, DoubleBlazeBlock, [96, 24, 2]], # 6-P4/16
+   [-1, 2, DoubleBlazeBlock, [96, 24]], # 7
+  ]
+
+
+# YOLOv5 head
+head:
+  [[-1, 1, Conv, [48, 1, 1]],  # 8
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 5], 1, Concat, [1]],  # cat backbone P3
+   [-1, 1, Conv, [48, 1, 1]],  # 11 (P3/8-medium)
+
+   [-1, 1, nn.MaxPool2d, [3, 2, 1]],  # 12
+   [[-1, 7], 1, Concat, [1]],  # cat backbone P3
+   [-1, 1, Conv, [48, 1, 1]],  # 14 (P4/16-large)
+
+   [[11, 14], 1, Detect, [nc, anchors]],  # Detect(P3, P4)
+  ]
+

+ 456 - 0
models/common.py

@@ -0,0 +1,456 @@
+# This file contains modules common to various models
+
+import math
+
+import numpy as np
+import requests
+import torch
+import torch.nn as nn
+from PIL import Image, ImageDraw
+
+from utils.datasets import letterbox
+from utils.general import non_max_suppression, make_divisible, scale_coords, xyxy2xywh
+from utils.plots import color_list
+
+def autopad(k, p=None):  # kernel, padding
+    # Pad to 'same'
+    if p is None:
+        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # auto-pad
+    return p
+
+def channel_shuffle(x, groups):
+    batchsize, num_channels, height, width = x.data.size()
+    channels_per_group = num_channels // groups
+
+    # reshape
+    x = x.view(batchsize, groups, channels_per_group, height, width)
+    x = torch.transpose(x, 1, 2).contiguous()
+
+    # flatten
+    x = x.view(batchsize, -1, height, width)
+    return x
+
+def DWConv(c1, c2, k=1, s=1, act=True):
+    # Depthwise convolution
+    return Conv(c1, c2, k, s, g=math.gcd(c1, c2), act=act)
+
+class Conv(nn.Module):
+    # Standard convolution
+    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
+        super(Conv, self).__init__()
+        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
+        self.bn = nn.BatchNorm2d(c2)
+        self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
+        #self.act = self.act = nn.LeakyReLU(0.1, inplace=True) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
+
+    def forward(self, x):
+        return self.act(self.bn(self.conv(x)))
+
+    def fuseforward(self, x):
+        return self.act(self.conv(x))
+
+class StemBlock(nn.Module):
+    def __init__(self, c1, c2, k=3, s=2, p=None, g=1, act=True):
+        super(StemBlock, self).__init__()
+        self.stem_1 = Conv(c1, c2, k, s, p, g, act)
+        self.stem_2a = Conv(c2, c2 // 2, 1, 1, 0)
+        self.stem_2b = Conv(c2 // 2, c2, 3, 2, 1)
+        self.stem_2p = nn.MaxPool2d(kernel_size=2,stride=2,ceil_mode=True)
+        self.stem_3 = Conv(c2 * 2, c2, 1, 1, 0)
+
+    def forward(self, x):
+        stem_1_out  = self.stem_1(x)
+        stem_2a_out = self.stem_2a(stem_1_out)
+        stem_2b_out = self.stem_2b(stem_2a_out)
+        stem_2p_out = self.stem_2p(stem_1_out)
+        out = self.stem_3(torch.cat((stem_2b_out,stem_2p_out),1))
+        return out
+
+class Bottleneck(nn.Module):
+    # Standard bottleneck
+    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, shortcut, groups, expansion
+        super(Bottleneck, self).__init__()
+        c_ = int(c2 * e)  # hidden channels
+        self.cv1 = Conv(c1, c_, 1, 1)
+        self.cv2 = Conv(c_, c2, 3, 1, g=g)
+        self.add = shortcut and c1 == c2
+
+    def forward(self, x):
+        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
+
+class BottleneckCSP(nn.Module):
+    # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
+    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
+        super(BottleneckCSP, self).__init__()
+        c_ = int(c2 * e)  # hidden channels
+        self.cv1 = Conv(c1, c_, 1, 1)
+        self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
+        self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
+        self.cv4 = Conv(2 * c_, c2, 1, 1)
+        self.bn = nn.BatchNorm2d(2 * c_)  # applied to cat(cv2, cv3)
+        self.act = nn.LeakyReLU(0.1, inplace=True)
+        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])
+
+    def forward(self, x):
+        y1 = self.cv3(self.m(self.cv1(x)))
+        y2 = self.cv2(x)
+        return self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=1))))
+
+
+class C3(nn.Module):
+    # CSP Bottleneck with 3 convolutions
+    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
+        super(C3, self).__init__()
+        c_ = int(c2 * e)  # hidden channels
+        self.cv1 = Conv(c1, c_, 1, 1)
+        self.cv2 = Conv(c1, c_, 1, 1)
+        self.cv3 = Conv(2 * c_, c2, 1)  # act=FReLU(c2)
+        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])
+
+    def forward(self, x):
+        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))
+
+class ShuffleV2Block(nn.Module):
+    def __init__(self, inp, oup, stride):
+        super(ShuffleV2Block, self).__init__()
+
+        if not (1 <= stride <= 3):
+            raise ValueError('illegal stride value')
+        self.stride = stride
+
+        branch_features = oup // 2
+        assert (self.stride != 1) or (inp == branch_features << 1)
+
+        if self.stride > 1:
+            self.branch1 = nn.Sequential(
+                self.depthwise_conv(inp, inp, kernel_size=3, stride=self.stride, padding=1),
+                nn.BatchNorm2d(inp),
+                nn.Conv2d(inp, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
+                nn.BatchNorm2d(branch_features),
+                nn.SiLU(),
+            )
+        else:
+            self.branch1 = nn.Sequential()
+
+        self.branch2 = nn.Sequential(
+            nn.Conv2d(inp if (self.stride > 1) else branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
+            nn.BatchNorm2d(branch_features),
+            nn.SiLU(),
+            self.depthwise_conv(branch_features, branch_features, kernel_size=3, stride=self.stride, padding=1),
+            nn.BatchNorm2d(branch_features),
+            nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
+            nn.BatchNorm2d(branch_features),
+            nn.SiLU(),
+        )
+
+    @staticmethod
+    def depthwise_conv(i, o, kernel_size, stride=1, padding=0, bias=False):
+        return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i)
+
+    def forward(self, x):
+        if self.stride == 1:
+            x1, x2 = x.chunk(2, dim=1)
+            out = torch.cat((x1, self.branch2(x2)), dim=1)
+        else:
+            out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
+        out = channel_shuffle(out, 2)
+        return out
+    
+class BlazeBlock(nn.Module):
+    def __init__(self, in_channels,out_channels,mid_channels=None,stride=1):
+        super(BlazeBlock, self).__init__()
+        mid_channels = mid_channels or in_channels
+        assert stride in [1, 2]
+        if stride>1:
+            self.use_pool = True
+        else:
+            self.use_pool = False
+
+        self.branch1 = nn.Sequential(
+            nn.Conv2d(in_channels=in_channels,out_channels=mid_channels,kernel_size=5,stride=stride,padding=2,groups=in_channels),
+            nn.BatchNorm2d(mid_channels),
+            nn.Conv2d(in_channels=mid_channels,out_channels=out_channels,kernel_size=1,stride=1),
+            nn.BatchNorm2d(out_channels),
+        )
+
+        if self.use_pool:
+            self.shortcut = nn.Sequential(
+                nn.MaxPool2d(kernel_size=stride, stride=stride),
+                nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1),
+                nn.BatchNorm2d(out_channels),
+            )
+
+        self.relu = nn.SiLU(inplace=True)
+
+    def forward(self, x):
+        branch1 = self.branch1(x)
+        out = (branch1+self.shortcut(x)) if self.use_pool else (branch1+x)
+        return self.relu(out)    
+  
+class DoubleBlazeBlock(nn.Module):
+    def __init__(self,in_channels,out_channels,mid_channels=None,stride=1):
+        super(DoubleBlazeBlock, self).__init__()
+        mid_channels = mid_channels or in_channels
+        assert stride in [1, 2]
+        if stride > 1:
+            self.use_pool = True
+        else:
+            self.use_pool = False
+
+        self.branch1 = nn.Sequential(
+            nn.Conv2d(in_channels=in_channels, out_channels=in_channels, kernel_size=5, stride=stride,padding=2,groups=in_channels),
+            nn.BatchNorm2d(in_channels),
+            nn.Conv2d(in_channels=in_channels, out_channels=mid_channels, kernel_size=1, stride=1),
+            nn.BatchNorm2d(mid_channels),
+            nn.SiLU(inplace=True),
+            nn.Conv2d(in_channels=mid_channels, out_channels=mid_channels, kernel_size=5, stride=1,padding=2),
+            nn.BatchNorm2d(mid_channels),
+            nn.Conv2d(in_channels=mid_channels, out_channels=out_channels, kernel_size=1, stride=1),
+            nn.BatchNorm2d(out_channels),
+        )
+
+        if self.use_pool:
+            self.shortcut = nn.Sequential(
+                nn.MaxPool2d(kernel_size=stride, stride=stride),
+                nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1),
+                nn.BatchNorm2d(out_channels),
+            )
+
+        self.relu = nn.SiLU(inplace=True)
+
+    def forward(self, x):
+        branch1 = self.branch1(x)
+        out = (branch1 + self.shortcut(x)) if self.use_pool else (branch1 + x)
+        return self.relu(out)
+    
+    
+class SPP(nn.Module):
+    # Spatial pyramid pooling layer used in YOLOv3-SPP
+    def __init__(self, c1, c2, k=(5, 9, 13)):
+        super(SPP, self).__init__()
+        c_ = c1 // 2  # hidden channels
+        self.cv1 = Conv(c1, c_, 1, 1)
+        self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
+        self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])
+
+    def forward(self, x):
+        x = self.cv1(x)
+        return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
+
+class SPPF(nn.Module):
+    # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher
+    def __init__(self, c1, c2, k=5):  # equivalent to SPP(k=(5, 9, 13))
+        super().__init__()
+        c_ = c1 // 2  # hidden channels
+        self.cv1 = Conv(c1, c_, 1, 1)
+        self.cv2 = Conv(c_ * 4, c2, 1, 1)
+        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)
+
+    def forward(self, x):
+        x = self.cv1(x)
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore')  # suppress torch 1.9.0 max_pool2d() warning
+            y1 = self.m(x)
+            y2 = self.m(y1)
+            return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))
+
+
+class Focus(nn.Module):
+    # Focus wh information into c-space
+    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
+        super(Focus, self).__init__()
+        self.conv = Conv(c1 * 4, c2, k, s, p, g, act)
+        # self.contract = Contract(gain=2)
+
+    def forward(self, x):  # x(b,c,w,h) -> y(b,4c,w/2,h/2)
+        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))
+        # return self.conv(self.contract(x))
+
+
+class Contract(nn.Module):
+    # Contract width-height into channels, i.e. x(1,64,80,80) to x(1,256,40,40)
+    def __init__(self, gain=2):
+        super().__init__()
+        self.gain = gain
+
+    def forward(self, x):
+        N, C, H, W = x.size()  # assert (H / s == 0) and (W / s == 0), 'Indivisible gain'
+        s = self.gain
+        x = x.view(N, C, H // s, s, W // s, s)  # x(1,64,40,2,40,2)
+        x = x.permute(0, 3, 5, 1, 2, 4).contiguous()  # x(1,2,2,64,40,40)
+        return x.view(N, C * s * s, H // s, W // s)  # x(1,256,40,40)
+
+
+class Expand(nn.Module):
+    # Expand channels into width-height, i.e. x(1,64,80,80) to x(1,16,160,160)
+    def __init__(self, gain=2):
+        super().__init__()
+        self.gain = gain
+
+    def forward(self, x):
+        N, C, H, W = x.size()  # assert C / s ** 2 == 0, 'Indivisible gain'
+        s = self.gain
+        x = x.view(N, s, s, C // s ** 2, H, W)  # x(1,2,2,16,80,80)
+        x = x.permute(0, 3, 4, 1, 5, 2).contiguous()  # x(1,16,80,2,80,2)
+        return x.view(N, C // s ** 2, H * s, W * s)  # x(1,16,160,160)
+
+
+class Concat(nn.Module):
+    # Concatenate a list of tensors along dimension
+    def __init__(self, dimension=1):
+        super(Concat, self).__init__()
+        self.d = dimension
+
+    def forward(self, x):
+        return torch.cat(x, self.d)
+
+
+class NMS(nn.Module):
+    # Non-Maximum Suppression (NMS) module
+    conf = 0.25  # confidence threshold
+    iou = 0.45  # IoU threshold
+    classes = None  # (optional list) filter by class
+
+    def __init__(self):
+        super(NMS, self).__init__()
+
+    def forward(self, x):
+        return non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes)
+
+class autoShape(nn.Module):
+    # input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS
+    img_size = 640  # inference size (pixels)
+    conf = 0.25  # NMS confidence threshold
+    iou = 0.45  # NMS IoU threshold
+    classes = None  # (optional list) filter by class
+
+    def __init__(self, model):
+        super(autoShape, self).__init__()
+        self.model = model.eval()
+
+    def autoshape(self):
+        print('autoShape already enabled, skipping... ')  # model already converted to model.autoshape()
+        return self
+
+    def forward(self, imgs, size=640, augment=False, profile=False):
+        # Inference from various sources. For height=720, width=1280, RGB images example inputs are:
+        #   filename:   imgs = 'data/samples/zidane.jpg'
+        #   URI:             = 'https://github.com/ultralytics/yolov5/releases/download/v1.0/zidane.jpg'
+        #   OpenCV:          = cv2.imread('image.jpg')[:,:,::-1]  # HWC BGR to RGB x(720,1280,3)
+        #   PIL:             = Image.open('image.jpg')  # HWC x(720,1280,3)
+        #   numpy:           = np.zeros((720,1280,3))  # HWC
+        #   torch:           = torch.zeros(16,3,720,1280)  # BCHW
+        #   multiple:        = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...]  # list of images
+
+        p = next(self.model.parameters())  # for device and type
+        if isinstance(imgs, torch.Tensor):  # torch
+            return self.model(imgs.to(p.device).type_as(p), augment, profile)  # inference
+
+        # Pre-process
+        n, imgs = (len(imgs), imgs) if isinstance(imgs, list) else (1, [imgs])  # number of images, list of images
+        shape0, shape1 = [], []  # image and inference shapes
+        for i, im in enumerate(imgs):
+            if isinstance(im, str):  # filename or uri
+                im = Image.open(requests.get(im, stream=True).raw if im.startswith('http') else im)  # open
+            im = np.array(im)  # to numpy
+            if im.shape[0] < 5:  # image in CHW
+                im = im.transpose((1, 2, 0))  # reverse dataloader .transpose(2, 0, 1)
+            im = im[:, :, :3] if im.ndim == 3 else np.tile(im[:, :, None], 3)  # enforce 3ch input
+            s = im.shape[:2]  # HWC
+            shape0.append(s)  # image shape
+            g = (size / max(s))  # gain
+            shape1.append([y * g for y in s])
+            imgs[i] = im  # update
+        shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)]  # inference shape
+        x = [letterbox(im, new_shape=shape1, auto=False)[0] for im in imgs]  # pad
+        x = np.stack(x, 0) if n > 1 else x[0][None]  # stack
+        x = np.ascontiguousarray(x.transpose((0, 3, 1, 2)))  # BHWC to BCHW
+        x = torch.from_numpy(x).to(p.device).type_as(p) / 255.  # uint8 to fp16/32
+
+        # Inference
+        with torch.no_grad():
+            y = self.model(x, augment, profile)[0]  # forward
+        y = non_max_suppression(y, conf_thres=self.conf, iou_thres=self.iou, classes=self.classes)  # NMS
+
+        # Post-process
+        for i in range(n):
+            scale_coords(shape1, y[i][:, :4], shape0[i])
+
+        return Detections(imgs, y, self.names)
+
+
+class Detections:
+    # detections class for YOLOv5 inference results
+    def __init__(self, imgs, pred, names=None):
+        super(Detections, self).__init__()
+        d = pred[0].device  # device
+        gn = [torch.tensor([*[im.shape[i] for i in [1, 0, 1, 0]], 1., 1.], device=d) for im in imgs]  # normalizations
+        self.imgs = imgs  # list of images as numpy arrays
+        self.pred = pred  # list of tensors pred[0] = (xyxy, conf, cls)
+        self.names = names  # class names
+        self.xyxy = pred  # xyxy pixels
+        self.xywh = [xyxy2xywh(x) for x in pred]  # xywh pixels
+        self.xyxyn = [x / g for x, g in zip(self.xyxy, gn)]  # xyxy normalized
+        self.xywhn = [x / g for x, g in zip(self.xywh, gn)]  # xywh normalized
+        self.n = len(self.pred)
+
+    def display(self, pprint=False, show=False, save=False, render=False):
+        colors = color_list()
+        for i, (img, pred) in enumerate(zip(self.imgs, self.pred)):
+            str = f'Image {i + 1}/{len(self.pred)}: {img.shape[0]}x{img.shape[1]} '
+            if pred is not None:
+                for c in pred[:, -1].unique():
+                    n = (pred[:, -1] == c).sum()  # detections per class
+                    str += f'{n} {self.names[int(c)]}s, '  # add to string
+                if show or save or render:
+                    img = Image.fromarray(img.astype(np.uint8)) if isinstance(img, np.ndarray) else img  # from np
+                    for *box, conf, cls in pred:  # xyxy, confidence, class
+                        # str += '%s %.2f, ' % (names[int(cls)], conf)  # label
+                        ImageDraw.Draw(img).rectangle(box, width=4, outline=colors[int(cls) % 10])  # plot
+            if pprint:
+                print(str)
+            if show:
+                img.show(f'Image {i}')  # show
+            if save:
+                f = f'results{i}.jpg'
+                str += f"saved to '{f}'"
+                img.save(f)  # save
+            if render:
+                self.imgs[i] = np.asarray(img)
+
+    def print(self):
+        self.display(pprint=True)  # print results
+
+    def show(self):
+        self.display(show=True)  # show results
+
+    def save(self):
+        self.display(save=True)  # save results
+
+    def render(self):
+        self.display(render=True)  # render results
+        return self.imgs
+
+    def __len__(self):
+        return self.n
+
+    def tolist(self):
+        # return a list of Detections objects, i.e. 'for result in results.tolist():'
+        x = [Detections([self.imgs[i]], [self.pred[i]], self.names) for i in range(self.n)]
+        for d in x:
+            for k in ['imgs', 'pred', 'xyxy', 'xyxyn', 'xywh', 'xywhn']:
+                setattr(d, k, getattr(d, k)[0])  # pop out of list
+        return x
+
+
+class Classify(nn.Module):
+    # Classification head, i.e. x(b,c1,20,20) to x(b,c2)
+    def __init__(self, c1, c2, k=1, s=1, p=None, g=1):  # ch_in, ch_out, kernel, stride, padding, groups
+        super(Classify, self).__init__()
+        self.aap = nn.AdaptiveAvgPool2d(1)  # to x(b,c1,1,1)
+        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g)  # to x(b,c2,1,1)
+        self.flat = nn.Flatten()
+
+    def forward(self, x):
+        z = torch.cat([self.aap(y) for y in (x if isinstance(x, list) else [x])], 1)  # cat if list
+        return self.flat(self.conv(z))  # flatten to x(b,c2)

+ 133 - 0
models/experimental.py

@@ -0,0 +1,133 @@
+# This file contains experimental modules
+
+import numpy as np
+import torch
+import torch.nn as nn
+
+from models.common import Conv, DWConv
+from utils.google_utils import attempt_download
+
+
+class CrossConv(nn.Module):
+    # Cross Convolution Downsample
+    def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
+        # ch_in, ch_out, kernel, stride, groups, expansion, shortcut
+        super(CrossConv, self).__init__()
+        c_ = int(c2 * e)  # hidden channels
+        self.cv1 = Conv(c1, c_, (1, k), (1, s))
+        self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
+        self.add = shortcut and c1 == c2
+
+    def forward(self, x):
+        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
+
+
+class Sum(nn.Module):
+    # Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070
+    def __init__(self, n, weight=False):  # n: number of inputs
+        super(Sum, self).__init__()
+        self.weight = weight  # apply weights boolean
+        self.iter = range(n - 1)  # iter object
+        if weight:
+            self.w = nn.Parameter(-torch.arange(1., n) / 2, requires_grad=True)  # layer weights
+
+    def forward(self, x):
+        y = x[0]  # no weight
+        if self.weight:
+            w = torch.sigmoid(self.w) * 2
+            for i in self.iter:
+                y = y + x[i + 1] * w[i]
+        else:
+            for i in self.iter:
+                y = y + x[i + 1]
+        return y
+
+
+class GhostConv(nn.Module):
+    # Ghost Convolution https://github.com/huawei-noah/ghostnet
+    def __init__(self, c1, c2, k=1, s=1, g=1, act=True):  # ch_in, ch_out, kernel, stride, groups
+        super(GhostConv, self).__init__()
+        c_ = c2 // 2  # hidden channels
+        self.cv1 = Conv(c1, c_, k, s, None, g, act)
+        self.cv2 = Conv(c_, c_, 5, 1, None, c_, act)
+
+    def forward(self, x):
+        y = self.cv1(x)
+        return torch.cat([y, self.cv2(y)], 1)
+
+
+class GhostBottleneck(nn.Module):
+    # Ghost Bottleneck https://github.com/huawei-noah/ghostnet
+    def __init__(self, c1, c2, k, s):
+        super(GhostBottleneck, self).__init__()
+        c_ = c2 // 2
+        self.conv = nn.Sequential(GhostConv(c1, c_, 1, 1),  # pw
+                                  DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(),  # dw
+                                  GhostConv(c_, c2, 1, 1, act=False))  # pw-linear
+        self.shortcut = nn.Sequential(DWConv(c1, c1, k, s, act=False),
+                                      Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity()
+
+    def forward(self, x):
+        return self.conv(x) + self.shortcut(x)
+
+
+class MixConv2d(nn.Module):
+    # Mixed Depthwise Conv https://arxiv.org/abs/1907.09595
+    def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True):
+        super(MixConv2d, self).__init__()
+        groups = len(k)
+        if equal_ch:  # equal c_ per group
+            i = torch.linspace(0, groups - 1E-6, c2).floor()  # c2 indices
+            c_ = [(i == g).sum() for g in range(groups)]  # intermediate channels
+        else:  # equal weight.numel() per group
+            b = [c2] + [0] * groups
+            a = np.eye(groups + 1, groups, k=-1)
+            a -= np.roll(a, 1, axis=1)
+            a *= np.array(k) ** 2
+            a[0] = 1
+            c_ = np.linalg.lstsq(a, b, rcond=None)[0].round()  # solve for equal weight indices, ax = b
+
+        self.m = nn.ModuleList([nn.Conv2d(c1, int(c_[g]), k[g], s, k[g] // 2, bias=False) for g in range(groups)])
+        self.bn = nn.BatchNorm2d(c2)
+        self.act = nn.LeakyReLU(0.1, inplace=True)
+
+    def forward(self, x):
+        return x + self.act(self.bn(torch.cat([m(x) for m in self.m], 1)))
+
+
+class Ensemble(nn.ModuleList):
+    # Ensemble of models
+    def __init__(self):
+        super(Ensemble, self).__init__()
+
+    def forward(self, x, augment=False):
+        y = []
+        for module in self:
+            y.append(module(x, augment)[0])
+        # y = torch.stack(y).max(0)[0]  # max ensemble
+        # y = torch.stack(y).mean(0)  # mean ensemble
+        y = torch.cat(y, 1)  # nms ensemble
+        return y, None  # inference, train output
+
+
+def attempt_load(weights, map_location=None):
+    # Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
+    model = Ensemble()
+    for w in weights if isinstance(weights, list) else [weights]:
+        attempt_download(w)
+        model.append(torch.load(w, map_location=map_location ,  weights_only=False)['model'].float().fuse().eval())  # load FP32 model
+
+    # Compatibility updates
+    for m in model.modules():
+        if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]:
+            m.inplace = True  # pytorch 1.7.0 compatibility
+        elif type(m) is Conv:
+            m._non_persistent_buffers_set = set()  # pytorch 1.6.0 compatibility
+
+    if len(model) == 1:
+        return model[-1]  # return model
+    else:
+        print('Ensemble created with %s\n' % weights)
+        for k in ['names', 'stride']:
+            setattr(model, k, getattr(model[-1], k))
+        return model  # return ensemble

+ 519 - 0
models/yolo.py

@@ -0,0 +1,519 @@
+import argparse
+import logging
+import math
+import sys
+from copy import deepcopy
+from pathlib import Path
+
+from models.common import (
+    Conv,
+    Bottleneck,
+    SPP,
+    DWConv,
+    Focus,
+    BottleneckCSP,
+    C3,
+    ShuffleV2Block,
+    Concat,
+    NMS,
+    autoShape,
+    StemBlock,
+    BlazeBlock,
+    DoubleBlazeBlock,
+)
+from models.experimental import MixConv2d, CrossConv
+from utils.autoanchor import check_anchor_order
+from utils.general import make_divisible, check_file, set_logging
+from utils.torch_utils import (
+    time_synchronized,
+    fuse_conv_and_bn,
+    model_info,
+    scale_img,
+    initialize_weights,
+    select_device,
+    copy_attr,
+)
+
+import torch
+import torch.nn as nn
+
+sys.path.append("./")  # to run '$ python *.py' files in subdirectories
+logger = logging.getLogger(__name__)
+
+
+try:
+    import thop  # for FLOPS computation
+except ImportError:
+    thop = None
+
+
+class Detect(nn.Module):
+    stride = None  # strides computed during build
+    export_cat = False  # onnx export cat output
+
+    def __init__(self, nc=80, anchors=(), ch=()):  # detection layer
+        super(Detect, self).__init__()
+        self.nc = nc  # number of classes
+        # self.no = nc + 5  # number of outputs per anchor
+        self.no = nc + 5 + 8  # number of outputs per anchor
+
+        self.nl = len(anchors)  # number of detection layers
+        self.na = len(anchors[0]) // 2  # number of anchors
+        self.grid = [torch.zeros(1)] * self.nl  # init grid
+        a = torch.tensor(anchors).float().view(self.nl, -1, 2)
+        self.register_buffer("anchors", a)  # shape(nl,na,2)
+        self.register_buffer(
+            "anchor_grid", a.clone().view(self.nl, 1, -1, 1, 1, 2)
+        )  # shape(nl,1,na,1,1,2)
+        self.m = nn.ModuleList(
+            nn.Conv2d(x, self.no * self.na, 1) for x in ch
+        )  # output conv
+
+    def forward(self, x):
+        # x = x.copy()  # for profiling
+        z = []  # inference output
+        # self.training=True
+        if self.export_cat:
+            for i in range(self.nl):
+                x[i] = self.m[i](x[i])  # conv
+                bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
+                x[i] = (
+                    x[i]
+                    .view(bs, self.na, self.no, ny, nx)
+                    .permute(0, 1, 3, 4, 2)
+                    .contiguous()
+                )
+
+                if self.grid[i].shape[2:4] != x[i].shape[2:4]:
+                    # self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
+                    self.grid[i], self.anchor_grid[i] = self._make_grid_new(nx, ny, i)
+
+                y = torch.full_like(x[i], 0)
+                y = y + torch.cat(
+                    (
+                        x[i][:, :, :, :, 0:5].sigmoid(),
+                        torch.cat(
+                            (
+                                x[i][:, :, :, :, 5:13],
+                                x[i][:, :, :, :, 13 : 13 + self.nc].sigmoid(),
+                            ),
+                            4,
+                        ),
+                    ),
+                    4,
+                )
+
+                box_xy = (
+                    y[:, :, :, :, 0:2] * 2.0 - 0.5 + self.grid[i].to(x[i].device)
+                ) * self.stride[i]  # xy
+                box_wh = (y[:, :, :, :, 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
+                # box_conf = torch.cat((box_xy, torch.cat((box_wh, y[:, :, :, :, 4:5]), 4)), 4)
+
+                landm1 = (
+                    y[:, :, :, :, 5:7] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x1 y1
+                landm2 = (
+                    y[:, :, :, :, 7:9] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x2 y2
+                landm3 = (
+                    y[:, :, :, :, 9:11] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x3 y3
+                landm4 = (
+                    y[:, :, :, :, 11:13] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x4 y4
+                prob = y[:, :, :, :, 13 : 13 + self.nc]
+                score, index_ = torch.max(prob, dim=-1, keepdim=True)
+                score = score.type(box_xy.dtype)
+                index_ = index_.type(box_xy.dtype)
+                index = torch.argmax(prob, dim=-1, keepdim=True).type(box_xy.dtype)
+                # landm5 = y[:, :, :, :, 13:13] * self.anchor_grid[i] + self.grid[i].to(x[i].device) * self.stride[i]  # landmark x5 y5
+                # landm = torch.cat((landm1, torch.cat((landm2, torch.cat((landm3, torch.cat((landm4, landm5), 4)), 4)), 4)), 4)
+                # y = torch.cat((box_conf, torch.cat((landm, y[:, :, :, :, 13:13+self.nc]), 4)), 4)
+                y = torch.cat(
+                    [
+                        box_xy,
+                        box_wh,
+                        y[:, :, :, :, 4:5],
+                        landm1,
+                        landm2,
+                        landm3,
+                        landm4,
+                        y[:, :, :, :, 13 : 13 + self.nc],
+                    ],
+                    -1,
+                )
+
+                z.append(y.view(bs, -1, self.no))
+            return torch.cat(z, 1)
+
+        for i in range(self.nl):
+            x[i] = self.m[i](x[i])  # conv
+            bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
+            x[i] = (
+                x[i]
+                .view(bs, self.na, self.no, ny, nx)
+                .permute(0, 1, 3, 4, 2)
+                .contiguous()
+            )
+
+            if not self.training:  # inference
+                if self.grid[i].shape[2:4] != x[i].shape[2:4]:
+                    self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
+
+                y = torch.full_like(x[i], 0)
+                class_range = list(range(5)) + list(range(13, 13 + self.nc))
+                y[..., class_range] = x[i][..., class_range].sigmoid()
+                y[..., 5:13] = x[i][..., 5:13]
+                # y = x[i].sigmoid()
+
+                y[..., 0:2] = (
+                    y[..., 0:2] * 2.0 - 0.5 + self.grid[i].to(x[i].device)
+                ) * self.stride[i]  # xy
+                y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
+
+                # y[..., 5:13] = y[..., 5:13] * 8 - 4
+                y[..., 5:7] = (
+                    y[..., 5:7] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x1 y1
+                y[..., 7:9] = (
+                    y[..., 7:9] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x2 y2
+                y[..., 9:11] = (
+                    y[..., 9:11] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x3 y3
+                y[..., 11:13] = (
+                    y[..., 11:13] * self.anchor_grid[i]
+                    + self.grid[i].to(x[i].device) * self.stride[i]
+                )  # landmark x4 y4
+                # y[..., 13:13] = y[..., 13:13] * self.anchor_grid[i] + self.grid[i].to(x[i].device) * self.stride[i]# landmark x5 y5
+
+                # y[..., 5:7] = (y[..., 5:7] * 2 -1) * self.anchor_grid[i]  # landmark x1 y1
+                # y[..., 7:9] = (y[..., 7:9] * 2 -1) * self.anchor_grid[i]  # landmark x2 y2
+                # y[..., 9:11] = (y[..., 9:11] * 2 -1) * self.anchor_grid[i]  # landmark x3 y3
+                # y[..., 11:13] = (y[..., 11:13] * 2 -1) * self.anchor_grid[i]  # landmark x4 y4
+                # y[..., 13:13] = (y[..., 13:13] * 2 -1) * self.anchor_grid[i]  # landmark x5 y5
+
+                z.append(y.view(bs, -1, self.no))
+
+        return x if self.training else (torch.cat(z, 1), x)
+
+    @staticmethod
+    def _make_grid(nx=20, ny=20):
+        yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)] , indexing ='ij')
+        return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()
+
+    def _make_grid_new(self, nx=20, ny=20, i=0):
+        d = self.anchors[i].device
+        if (
+            "1.10.0" in torch.__version__
+        ):  # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility
+            yv, xv = torch.meshgrid(
+                [torch.arange(ny).to(d), torch.arange(nx).to(d)], indexing="ij"
+            )
+        else:
+            yv, xv = torch.meshgrid([torch.arange(ny).to(d), torch.arange(nx).to(d) ] , indexing='ij')
+        grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float()
+        anchor_grid = (
+            (self.anchors[i].clone() * self.stride[i])
+            .view((1, self.na, 1, 1, 2))
+            .expand((1, self.na, ny, nx, 2))
+            .float()
+        )
+        return grid, anchor_grid
+
+
+class Model(nn.Module):
+    def __init__(
+        self, cfg="yolov5s.yaml", ch=3, nc=None
+    ):  # model, input channels, number of classes
+        super(Model, self).__init__()
+        if isinstance(cfg, dict):
+            self.yaml = cfg  # model dict
+        else:  # is *.yaml
+            import yaml  # for torch hub
+
+            self.yaml_file = Path(cfg).name
+            with open(cfg) as f:
+                self.yaml = yaml.load(f, Loader=yaml.FullLoader)  # model dict
+
+        # Define model
+        ch = self.yaml["ch"] = self.yaml.get("ch", ch)  # input channels
+        if nc and nc != self.yaml["nc"]:
+            logger.info(
+                "Overriding model.yaml nc=%g with nc=%g" % (self.yaml["nc"], nc)
+            )
+            self.yaml["nc"] = nc  # override yaml value
+        self.model, self.save = parse_model(
+            deepcopy(self.yaml), ch=[ch]
+        )  # model, savelist
+        self.names = [str(i) for i in range(self.yaml["nc"])]  # default names
+        # print([x.shape for x in self.forward(torch.zeros(1, ch, 64, 64))])
+
+        # Build strides, anchors
+        m = self.model[-1]  # Detect()
+        if isinstance(m, Detect):
+            s = 128  # 2x min stride
+            m.stride = torch.tensor(
+                [s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]
+            )  # forward
+            m.anchors /= m.stride.view(-1, 1, 1)
+            check_anchor_order(m)
+            self.stride = m.stride
+            self._initialize_biases()  # only run once
+            # print('Strides: %s' % m.stride.tolist())
+
+        # Init weights, biases
+        initialize_weights(self)
+        self.info()
+        logger.info("")
+
+    def forward(self, x, augment=False, profile=False):
+        if augment:
+            img_size = x.shape[-2:]  # height, width
+            s = [1, 0.83, 0.67]  # scales
+            f = [None, 3, None]  # flips (2-ud, 3-lr)
+            y = []  # outputs
+            for si, fi in zip(s, f):
+                xi = scale_img(x.flip(fi) if fi else x, si)
+                yi = self.forward_once(xi)[0]  # forward
+                # cv2.imwrite('img%g.jpg' % s, 255 * xi[0].numpy().transpose((1, 2, 0))[:, :, ::-1])  # save
+                yi[..., :4] /= si  # de-scale
+                if fi == 2:
+                    yi[..., 1] = img_size[0] - yi[..., 1]  # de-flip ud
+                elif fi == 3:
+                    yi[..., 0] = img_size[1] - yi[..., 0]  # de-flip lr
+                y.append(yi)
+            return torch.cat(y, 1), None  # augmented inference, train
+        else:
+            return self.forward_once(x, profile)  # single-scale inference, train
+
+    def forward_once(self, x, profile=False):
+        y, dt = [], []  # outputs
+        for m in self.model:
+            if m.f != -1:  # if not from previous layer
+                x = (
+                    y[m.f]
+                    if isinstance(m.f, int)
+                    else [x if j == -1 else y[j] for j in m.f]
+                )  # from earlier layers
+
+            if profile:
+                o = (
+                    thop.profile(m, inputs=(x,), verbose=False)[0] / 1e9 * 2
+                    if thop
+                    else 0
+                )  # FLOPS
+                t = time_synchronized()
+                for _ in range(10):
+                    _ = m(x)
+                dt.append((time_synchronized() - t) * 100)
+                print("%10.1f%10.0f%10.1fms %-40s" % (o, m.np, dt[-1], m.type))
+
+            x = m(x)  # run
+            y.append(x if m.i in self.save else None)  # save output
+
+        if profile:
+            print("%.1fms total" % sum(dt))
+        return x
+
+    def _initialize_biases(
+        self, cf=None
+    ):  # initialize biases into Detect(), cf is class frequency
+        # https://arxiv.org/abs/1708.02002 section 3.3
+        # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
+        m = self.model[-1]  # Detect() module
+        for mi, s in zip(m.m, m.stride):  # from
+            b = mi.bias.view(m.na, -1)  # conv.bias(255) to (3,85)
+            b.data[:, 4] += math.log(
+                8 / (640 / s) ** 2
+            )  # obj (8 objects per 640 image)
+            b.data[:, 5:] += (
+                math.log(0.6 / (m.nc - 0.99))
+                if cf is None
+                else torch.log(cf / cf.sum())
+            )  # cls
+            mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
+
+    def _print_biases(self):
+        m = self.model[-1]  # Detect() module
+        for mi in m.m:  # from
+            b = mi.bias.detach().view(m.na, -1).T  # conv.bias(255) to (3,85)
+            print(
+                ("%6g Conv2d.bias:" + "%10.3g" * 6)
+                % (mi.weight.shape[1], *b[:5].mean(1).tolist(), b[5:].mean())
+            )
+
+    # def _print_weights(self):
+    #     for m in self.model.modules():
+    #         if type(m) is Bottleneck:
+    #             print('%10.3g' % (m.w.detach().sigmoid() * 2))  # shortcut weights
+
+    def fuse(self):  # fuse model Conv2d() + BatchNorm2d() layers
+        print("Fusing layers... ")
+        for m in self.model.modules():
+            if type(m) is Conv and hasattr(m, "bn"):
+                m.conv = fuse_conv_and_bn(m.conv, m.bn)  # update conv
+                delattr(m, "bn")  # remove batchnorm
+                m.forward = m.fuseforward  # update forward
+            elif type(m) is nn.Upsample:
+                m.recompute_scale_factor = None  # torch 1.11.0 compatibility
+        self.info()
+        return self
+
+    def nms(self, mode=True):  # add or remove NMS module
+        present = type(self.model[-1]) is NMS  # last layer is NMS
+        if mode and not present:
+            print("Adding NMS... ")
+            m = NMS()  # module
+            m.f = -1  # from
+            m.i = self.model[-1].i + 1  # index
+            self.model.add_module(name="%s" % m.i, module=m)  # add
+            self.eval()
+        elif not mode and present:
+            print("Removing NMS... ")
+            self.model = self.model[:-1]  # remove
+        return self
+
+    def autoshape(self):  # add autoShape module
+        print("Adding autoShape... ")
+        m = autoShape(self)  # wrap model
+        copy_attr(
+            m, self, include=("yaml", "nc", "hyp", "names", "stride"), exclude=()
+        )  # copy attributes
+        return m
+
+    def info(self, verbose=False, img_size=640):  # print model information
+        model_info(self, verbose, img_size)
+
+
+def parse_model(d, ch):  # model_dict, input_channels(3)
+    logger.info(
+        "\n%3s%18s%3s%10s  %-40s%-30s"
+        % ("", "from", "n", "params", "module", "arguments")
+    )
+    anchors, nc, gd, gw = (
+        d["anchors"],
+        d["nc"],
+        d["depth_multiple"],
+        d["width_multiple"],
+    )
+    na = (
+        (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors
+    )  # number of anchors
+    no = na * (nc + 5)  # number of outputs = anchors * (classes + 5)
+
+    layers, save, c2 = [], [], ch[-1]  # layers, savelist, ch out
+    for i, (f, n, m, args) in enumerate(
+        d["backbone"] + d["head"]
+    ):  # from, number, module, args
+        m = eval(m) if isinstance(m, str) else m  # eval strings
+        for j, a in enumerate(args):
+            try:
+                args[j] = eval(a) if isinstance(a, str) else a  # eval strings
+            except:
+                pass
+
+        n = max(round(n * gd), 1) if n > 1 else n  # depth gain
+        if m in [
+            Conv,
+            Bottleneck,
+            SPP,
+            DWConv,
+            MixConv2d,
+            Focus,
+            CrossConv,
+            BottleneckCSP,
+            C3,
+            ShuffleV2Block,
+            StemBlock,
+            BlazeBlock,
+            DoubleBlazeBlock,
+        ]:
+            c1, c2 = ch[f], args[0]
+
+            # Normal
+            # if i > 0 and args[0] != no:  # channel expansion factor
+            #     ex = 1.75  # exponential (default 2.0)
+            #     e = math.log(c2 / ch[1]) / math.log(2)
+            #     c2 = int(ch[1] * ex ** e)
+            # if m != Focus:
+
+            c2 = make_divisible(c2 * gw, 8) if c2 != no else c2
+
+            # Experimental
+            # if i > 0 and args[0] != no:  # channel expansion factor
+            #     ex = 1 + gw  # exponential (default 2.0)
+            #     ch1 = 32  # ch[1]
+            #     e = math.log(c2 / ch1) / math.log(2)  # level 1-n
+            #     c2 = int(ch1 * ex ** e)
+            # if m != Focus:
+            #     c2 = make_divisible(c2, 8) if c2 != no else c2
+
+            args = [c1, c2, *args[1:]]
+            if m in [BottleneckCSP, C3]:
+                args.insert(2, n)
+                n = 1
+        elif m is nn.BatchNorm2d:
+            args = [ch[f]]
+        elif m is Concat:
+            c2 = sum([ch[-1 if x == -1 else x + 1] for x in f])
+        elif m is Detect:
+            args.append([ch[x + 1] for x in f])
+            if isinstance(args[1], int):  # number of anchors
+                args[1] = [list(range(args[1] * 2))] * len(f)
+        else:
+            c2 = ch[f]
+
+        m_ = (
+            nn.Sequential(*[m(*args) for _ in range(n)]) if n > 1 else m(*args)
+        )  # module
+        t = str(m)[8:-2].replace("__main__.", "")  # module type
+        np = sum([x.numel() for x in m_.parameters()])  # number params
+        m_.i, m_.f, m_.type, m_.np = (
+            i,
+            f,
+            t,
+            np,
+        )  # attach index, 'from' index, type, number params
+        logger.info("%3s%18s%3s%10.0f  %-40s%-30s" % (i, f, n, np, t, args))  # print
+        save.extend(
+            x % i for x in ([f] if isinstance(f, int) else f) if x != -1
+        )  # append to savelist
+        layers.append(m_)
+        ch.append(c2)
+    return nn.Sequential(*layers), sorted(save)
+
+
+from thop import profile
+from thop import clever_format
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--cfg", type=str, default="yolov5s.yaml", help="model.yaml")
+    parser.add_argument(
+        "--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu"
+    )
+    opt = parser.parse_args()
+    opt.cfg = check_file(opt.cfg)  # check file
+    set_logging()
+    device = select_device(opt.device)
+
+    # Create model
+    model = Model(opt.cfg).to(device)
+    stride = model.stride.max()
+    if stride == 32:
+        input = torch.Tensor(1, 3, 480, 640).to(device)
+    else:
+        input = torch.Tensor(1, 3, 512, 640).to(device)
+    model.train()
+    print(model)
+    flops, params = profile(model, inputs=(input, ))
+    flops, params = clever_format([flops, params], "%.3f")
+    print('Flops:', flops, ',Params:' ,params)

+ 47 - 0
models/yolov5l.yaml

@@ -0,0 +1,47 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 1.0  # model depth multiple
+width_multiple: 1.0  # layer channel multiple
+
+# anchors
+anchors:
+  - [4,5,  8,10,  13,16]  # P3/8
+  - [23,29,  43,55,  73,105]  # P4/16
+  - [146,217,  231,300,  335,433]  # P5/32
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [[-1, 1, StemBlock, [64, 3, 2]],  # 0-P1/2
+   [-1, 3, C3, [128]],
+   [-1, 1, Conv, [256, 3, 2]],      # 2-P3/8
+   [-1, 9, C3, [256]],
+   [-1, 1, Conv, [512, 3, 2]],      # 4-P4/16
+   [-1, 9, C3, [512]],
+   [-1, 1, Conv, [1024, 3, 2]],     # 6-P5/32
+   [-1, 1, SPP, [1024, [3,5,7]]],
+   [-1, 3, C3, [1024, False]],      # 8
+  ]
+
+# YOLOv5 head
+head:
+  [[-1, 1, Conv, [512, 1, 1]],
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 5], 1, Concat, [1]],  # cat backbone P4
+   [-1, 3, C3, [512, False]],  # 12
+
+   [-1, 1, Conv, [256, 1, 1]],
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 3], 1, Concat, [1]],  # cat backbone P3
+   [-1, 3, C3, [256, False]],  # 16 (P3/8-small)
+
+   [-1, 1, Conv, [256, 3, 2]],
+   [[-1, 13], 1, Concat, [1]],  # cat head P4
+   [-1, 3, C3, [512, False]],  # 19 (P4/16-medium)
+
+   [-1, 1, Conv, [512, 3, 2]],
+   [[-1, 9], 1, Concat, [1]],  # cat head P5
+   [-1, 3, C3, [1024, False]],  # 22 (P5/32-large)
+
+   [[16, 19, 22], 1, Detect, [nc, anchors]],  # Detect(P3, P4, P5)
+  ]

+ 60 - 0
models/yolov5l6.yaml

@@ -0,0 +1,60 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 1.0  # model depth multiple
+width_multiple: 1.0  # layer channel multiple
+
+# anchors
+anchors:
+  - [6,7,  9,11,  13,16]  # P3/8
+  - [18,23,  26,33,  37,47]  # P4/16
+  - [54,67,  77,104,  112,154]  # P5/32
+  - [174,238,  258,355,  445,568]  # P6/64
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [ [ -1, 1, StemBlock, [ 64, 3, 2 ] ],  # 0-P1/2
+    [ -1, 3, C3, [ 128 ] ],
+    [ -1, 1, Conv, [ 256, 3, 2 ] ],  # 2-P3/8
+    [ -1, 9, C3, [ 256 ] ],
+    [ -1, 1, Conv, [ 512, 3, 2 ] ],  # 4-P4/16
+    [ -1, 9, C3, [ 512 ] ],
+    [ -1, 1, Conv, [ 768, 3, 2 ] ],  # 6-P5/32
+    [ -1, 3, C3, [ 768 ] ],
+    [ -1, 1, Conv, [ 1024, 3, 2 ] ],  # 8-P6/64
+    [ -1, 1, SPP, [ 1024, [ 3, 5, 7 ] ] ],
+    [ -1, 3, C3, [ 1024, False ] ],  # 10
+  ]
+
+# YOLOv5 head
+head:
+  [ [ -1, 1, Conv, [ 768, 1, 1 ] ],
+    [ -1, 1, nn.Upsample, [ None, 2, 'nearest' ] ],
+    [ [ -1, 7 ], 1, Concat, [ 1 ] ],  # cat backbone P5
+    [ -1, 3, C3, [ 768, False ] ],  # 14
+
+    [ -1, 1, Conv, [ 512, 1, 1 ] ],
+    [ -1, 1, nn.Upsample, [ None, 2, 'nearest' ] ],
+    [ [ -1, 5 ], 1, Concat, [ 1 ] ],  # cat backbone P4
+    [ -1, 3, C3, [ 512, False ] ],  # 18
+
+    [ -1, 1, Conv, [ 256, 1, 1 ] ],
+    [ -1, 1, nn.Upsample, [ None, 2, 'nearest' ] ],
+    [ [ -1, 3 ], 1, Concat, [ 1 ] ],  # cat backbone P3
+    [ -1, 3, C3, [ 256, False ] ],  # 22 (P3/8-small)
+
+    [ -1, 1, Conv, [ 256, 3, 2 ] ],
+    [ [ -1, 19 ], 1, Concat, [ 1 ] ],  # cat head P4
+    [ -1, 3, C3, [ 512, False ] ],  # 25 (P4/16-medium)
+
+    [ -1, 1, Conv, [ 512, 3, 2 ] ],
+    [ [ -1, 15 ], 1, Concat, [ 1 ] ],  # cat head P5
+    [ -1, 3, C3, [ 768, False ] ],  # 28 (P5/32-large)
+
+    [ -1, 1, Conv, [ 768, 3, 2 ] ],
+    [ [ -1, 11 ], 1, Concat, [ 1 ] ],  # cat head P6
+    [ -1, 3, C3, [ 1024, False ] ],  # 31 (P6/64-xlarge)
+
+    [ [ 22, 25, 28, 31 ], 1, Detect, [ nc, anchors ] ],  # Detect(P3, P4, P5, P6)
+  ]
+

+ 47 - 0
models/yolov5m.yaml

@@ -0,0 +1,47 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 0.67  # model depth multiple
+width_multiple: 0.75  # layer channel multiple
+
+# anchors
+anchors:
+  - [4,5,  8,10,  13,16]  # P3/8
+  - [23,29,  43,55,  73,105]  # P4/16
+  - [146,217,  231,300,  335,433]  # P5/32
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [[-1, 1, StemBlock, [64, 3, 2]],  # 0-P1/2
+   [-1, 3, C3, [128]],
+   [-1, 1, Conv, [256, 3, 2]],      # 2-P3/8
+   [-1, 9, C3, [256]],
+   [-1, 1, Conv, [512, 3, 2]],      # 4-P4/16
+   [-1, 9, C3, [512]],
+   [-1, 1, Conv, [1024, 3, 2]],     # 6-P5/32
+   [-1, 1, SPP, [1024, [3,5,7]]],
+   [-1, 3, C3, [1024, False]],      # 8
+  ]
+
+# YOLOv5 head
+head:
+  [[-1, 1, Conv, [512, 1, 1]],
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 5], 1, Concat, [1]],  # cat backbone P4
+   [-1, 3, C3, [512, False]],  # 12
+
+   [-1, 1, Conv, [256, 1, 1]],
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 3], 1, Concat, [1]],  # cat backbone P3
+   [-1, 3, C3, [256, False]],  # 16 (P3/8-small)
+
+   [-1, 1, Conv, [256, 3, 2]],
+   [[-1, 13], 1, Concat, [1]],  # cat head P4
+   [-1, 3, C3, [512, False]],  # 19 (P4/16-medium)
+
+   [-1, 1, Conv, [512, 3, 2]],
+   [[-1, 9], 1, Concat, [1]],  # cat head P5
+   [-1, 3, C3, [1024, False]],  # 22 (P5/32-large)
+
+   [[16, 19, 22], 1, Detect, [nc, anchors]],  # Detect(P3, P4, P5)
+  ]

+ 60 - 0
models/yolov5m6.yaml

@@ -0,0 +1,60 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 0.67  # model depth multiple
+width_multiple: 0.75  # layer channel multiple
+
+# anchors
+anchors:
+  - [6,7,  9,11,  13,16]  # P3/8
+  - [18,23,  26,33,  37,47]  # P4/16
+  - [54,67,  77,104,  112,154]  # P5/32
+  - [174,238,  258,355,  445,568]  # P6/64
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [ [ -1, 1, StemBlock, [ 64, 3, 2 ] ],  # 0-P1/2
+    [ -1, 3, C3, [ 128 ] ],
+    [ -1, 1, Conv, [ 256, 3, 2 ] ],  # 2-P3/8
+    [ -1, 9, C3, [ 256 ] ],
+    [ -1, 1, Conv, [ 512, 3, 2 ] ],  # 4-P4/16
+    [ -1, 9, C3, [ 512 ] ],
+    [ -1, 1, Conv, [ 768, 3, 2 ] ],  # 6-P5/32
+    [ -1, 3, C3, [ 768 ] ],
+    [ -1, 1, Conv, [ 1024, 3, 2 ] ],  # 8-P6/64
+    [ -1, 1, SPP, [ 1024, [ 3, 5, 7 ] ] ],
+    [ -1, 3, C3, [ 1024, False ] ],  # 10
+  ]
+
+# YOLOv5 head
+head:
+  [ [ -1, 1, Conv, [ 768, 1, 1 ] ],
+    [ -1, 1, nn.Upsample, [ None, 2, 'nearest' ] ],
+    [ [ -1, 7 ], 1, Concat, [ 1 ] ],  # cat backbone P5
+    [ -1, 3, C3, [ 768, False ] ],  # 14
+
+    [ -1, 1, Conv, [ 512, 1, 1 ] ],
+    [ -1, 1, nn.Upsample, [ None, 2, 'nearest' ] ],
+    [ [ -1, 5 ], 1, Concat, [ 1 ] ],  # cat backbone P4
+    [ -1, 3, C3, [ 512, False ] ],  # 18
+
+    [ -1, 1, Conv, [ 256, 1, 1 ] ],
+    [ -1, 1, nn.Upsample, [ None, 2, 'nearest' ] ],
+    [ [ -1, 3 ], 1, Concat, [ 1 ] ],  # cat backbone P3
+    [ -1, 3, C3, [ 256, False ] ],  # 22 (P3/8-small)
+
+    [ -1, 1, Conv, [ 256, 3, 2 ] ],
+    [ [ -1, 19 ], 1, Concat, [ 1 ] ],  # cat head P4
+    [ -1, 3, C3, [ 512, False ] ],  # 25 (P4/16-medium)
+
+    [ -1, 1, Conv, [ 512, 3, 2 ] ],
+    [ [ -1, 15 ], 1, Concat, [ 1 ] ],  # cat head P5
+    [ -1, 3, C3, [ 768, False ] ],  # 28 (P5/32-large)
+
+    [ -1, 1, Conv, [ 768, 3, 2 ] ],
+    [ [ -1, 11 ], 1, Concat, [ 1 ] ],  # cat head P6
+    [ -1, 3, C3, [ 1024, False ] ],  # 31 (P6/64-xlarge)
+
+    [ [ 22, 25, 28, 31 ], 1, Detect, [ nc, anchors ] ],  # Detect(P3, P4, P5, P6)
+  ]
+

+ 46 - 0
models/yolov5n-0.5.yaml

@@ -0,0 +1,46 @@
+# parameters
+nc: 1  # number of classes
+depth_multiple: 1.0  # model depth multiple
+width_multiple: 0.5  # layer channel multiple
+
+# anchors
+anchors:
+  - [4,5,  8,10,  13,16]  # P3/8
+  - [23,29,  43,55,  73,105]  # P4/16
+  - [146,217,  231,300,  335,433]  # P5/32
+
+# YOLOv5 backbone
+backbone:
+  # [from, number, module, args]
+  [[-1, 1, StemBlock, [32, 3, 2]],    # 0-P2/4
+   [-1, 1, ShuffleV2Block, [128, 2]], # 1-P3/8
+   [-1, 3, ShuffleV2Block, [128, 1]], # 2
+   [-1, 1, ShuffleV2Block, [256, 2]], # 3-P4/16
+   [-1, 7, ShuffleV2Block, [256, 1]], # 4
+   [-1, 1, ShuffleV2Block, [512, 2]], # 5-P5/32
+   [-1, 3, ShuffleV2Block, [512, 1]], # 6
+  ]
+
+# YOLOv5 head
+head:
+  [[-1, 1, Conv, [128, 1, 1]],
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 4], 1, Concat, [1]],  # cat backbone P4
+   [-1, 1, C3, [128, False]],  # 10
+
+   [-1, 1, Conv, [128, 1, 1]],
+   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
+   [[-1, 2], 1, Concat, [1]],  # cat backbone P3
+   [-1, 1, C3, [128, False]],  # 14 (P3/8-small)
+
+   [-1, 1, Conv, [128, 3, 2]],
+   [[-1, 11], 1, Concat, [1]],  # cat head P4
+   [-1, 1, C3, [128, False]],  # 17 (P4/16-medium)
+
+   [-1, 1, Conv, [128, 3, 2]],
+   [[-1, 7], 1, Concat, [1]],  # cat head P5
+   [-1, 1, C3, [128, False]],  # 20 (P5/32-large)
+
+   [[14, 17, 20], 1, Detect, [nc, anchors]],  # Detect(P3, P4, P5)
+  ]
+          

+ 0 - 0
models/yolov5n.yaml


Some files were not shown because too many files changed in this diff