diff --git a/busisness/blls.py b/busisness/blls.py index 432cd84..f4f8153 100644 --- a/busisness/blls.py +++ b/busisness/blls.py @@ -20,11 +20,46 @@ class ArtifactBll: def get_artifact_task(self) -> List[ArtifactInfoModel]: """获取官片任务数据""" - return self.dal.get_top_artifact(5, "ArtifactID asc") + return self.dal.get_top_artifact(5, "ID desc") + def update_artifact_task(self, artifact_id: str, status: int) -> bool: + """更新管片任务状态""" + return self.dal.update_artifact(artifact_id, {"Status": status}) + + def finish_artifact_task(self, artifact_id: str,beton_volume) -> bool: + """完成管片任务""" + return self.dal.update_artifact(artifact_id, {"Status": 3,"BetonVolume":beton_volume,"EndTime":datetime.now()}) + + def insert_artifact_task(self,model:ArtifactInfoModel) -> bool: + + """插入管片任务""" + if self.dal.exists_by_id(model.ArtifactID): + return False + return self.dal.insert_artifact({ + "ArtifactID": model.ArtifactID, + "ArtifactActionID": model.ArtifactActionID, + "ArtifactIDVice1": model.ArtifactIDVice1, + "ProduceRingNumber": model.ProduceRingNumber, + "MouldCode": model.MouldCode, + "SkeletonID": model.SkeletonID, + "RingTypeCode": model.RingTypeCode, + "SizeSpecification": model.SizeSpecification, + "BuriedDepth": model.BuriedDepth, + "BlockNumber": model.BlockNumber, + "BetonVolume": model.BetonVolume, + "BetonTaskID": model.BetonTaskID, + "HoleRingMarking": model.HoleRingMarking, + "GroutingPipeMarking": model.GroutingPipeMarking, + "PolypropyleneFiberMarking": model.PolypropyleneFiberMarking, + "PStatus":1, + "Status": 2, + "Source": model.Source, + "BeginTime": model.BeginTime, + "OptTime": datetime.now(), + }) def get_artifacting_task(self) -> ArtifactInfoModel: - """获取正在进行的官片任务数据""" + """获取正在进行的管片任务数据""" loc_item= self.dal.get_top_artifact(1,"","Status=2") if loc_item: return loc_item[0] diff --git a/busisness/dals.py b/busisness/dals.py index 28ff0c9..f203d41 100644 --- a/busisness/dals.py +++ b/busisness/dals.py @@ -42,7 +42,7 @@ class ArtifactDal(BaseDal): print(f"获取所有构件任务失败: {e}") return [] - def get_top_artifact(self, top: int,desc:str="ArtifactID asc",where:str="1=1") -> List[ArtifactInfoModel]: + def get_top_artifact(self, top: int,desc:str="ID desc",where:str="1=1") -> List[ArtifactInfoModel]: """获取top条数数据,根据ArtifactID升序""" try: # 确保top为正整数 @@ -59,6 +59,8 @@ class ArtifactDal(BaseDal): # 保证row的变量和模板变量一致 artifact = ArtifactInfoModel() artifact.ArtifactID=row["ArtifactID"] + artifact.ArtifactActionID=row["ArtifactActionID"] + artifact.ArtifactIDVice1=row["ArtifactIDVice1"] artifact.ProduceRingNumber=row["ProduceRingNumber"] artifact.MouldCode=row["MouldCode"] artifact.SkeletonID=row["SkeletonID"] @@ -68,6 +70,9 @@ class ArtifactDal(BaseDal): artifact.BlockNumber=row["BlockNumber"] artifact.BetonVolume=row["BetonVolume"] artifact.BetonTaskID=row["BetonTaskID"] + artifact.HoleRingMarking=row["HoleRingMarking"] + artifact.GapRingMarking=row["GroutingPipeMarking"] + artifact.PolypropyleneFiberMarking=row["PolypropyleneFiberMarking"] artifact.Status=row["Status"] artifact.BeginTime=row["BeginTime"] artifacts.append(artifact) @@ -78,6 +83,21 @@ class ArtifactDal(BaseDal): return [] + def exists_by_id(self, artifact_id: int) -> bool: + """根据构件ID获取构件任务""" + try: + sql = "SELECT count(1) FROM ArtifactTask WHERE ArtifactID = ?" + results = self.db_dao.execute_read(sql, (artifact_id,)) + + rows = list(results) + if rows[0][0] == 1: + return True + + return False + except Exception as e: + print(f"根据ID获取构件任务失败: {e}") + return False + def get_by_id(self, artifact_id: int) -> Optional[ArtifactInfoModel]: """根据构件ID获取构件任务""" try: @@ -108,7 +128,7 @@ class ArtifactDal(BaseDal): """更新构件任务记录""" try: # 构建WHERE条件 - where_condition = {"ArtifactID": artifact_id} + where_condition = f"ArtifactID='{artifact_id}'" # 使用update方法更新数据 affected_rows = self.db_dao.update("ArtifactTask", update_data, where_condition) return affected_rows > 0 diff --git a/config/settings.py b/config/settings.py index 528c404..f86cc91 100644 --- a/config/settings.py +++ b/config/settings.py @@ -7,9 +7,11 @@ class Settings: self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 网络继电器配置 - self.relay_host = '192.168.0.18' + self.relay_host = '192.168.250.62' self.relay_port = 50000 + self.debug_feeding=True + # 摄像头配置 self.camera_type = "ip" self.camera_ip = "192.168.1.51" @@ -18,6 +20,25 @@ class Settings: self.camera_password = "XJ123456" self.camera_channel = 1 + self.camera_configs = { + 'cam1': { + 'type': 'ip', + 'ip': '192.168.250.60', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + }, + 'cam2': { + 'type': 'ip', + 'ip': '192.168.250.61', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + } + } + # 下料控制参数 self.min_required_weight = 500 # 模具车最小需要重量(kg) self.target_vehicle_weight = 5000 # 目标模具车重量(kg) @@ -36,13 +57,13 @@ class Settings: self.frequencies = [220.0, 230.0, 240.0] # 下料阶段频率(Hz) # 模型路径配置 - self.models_dir = os.path.join(self.project_root, 'vision', 'models') - self.angle_model_path = os.path.join(self.models_dir, 'angle.pt') - self.overflow_model_path = os.path.join(self.models_dir, 'overflow.pt') - self.alignment_model_path = os.path.join(self.models_dir, 'alig.pt') + self.models_dir = os.path.join(self.project_root, 'vision') + self.angle_model_path = os.path.join(self.models_dir, 'obb_angle_model', 'obb.rknn') + self.overflow_model_path = os.path.join(self.models_dir,'overflow_model', 'yiliao_cls.rknn') + # self.alignment_model_path = os.path.join(self.models_dir, 'align_model', 'yolov11_cls_640v6.rknn') # ROI路径配置 - self.roi_file_path = os.path.join(self.project_root, 'vision', 'roi_coordinates', '1_rois.txt') + self.roi_file_path = os.path.join(self.models_dir, 'overflow_model', 'roi_coordinates', '1_rois.txt') # 系统控制参数 self.visual_check_interval = 1.0 # 视觉检查间隔(秒) @@ -60,6 +81,7 @@ class Settings: # self.block_numbers=['B1','B2','B3','L1','L2','F'] #需核实上下位漏斗容量 self.max_upper_volume = 2.4 # 上料斗容量(方) + #下料到下料斗最大下到多少,并非最大容量 self.max_lower_volume = 2.4 # 下料斗容量(方) diff --git a/controller/main_controller.py b/controller/main_controller.py index c74e8aa..1eb4045 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -1,22 +1,29 @@ from PySide6.QtCore import QTimer, Signal, QObject # 导入Qt核心类 from PySide6.QtWidgets import QApplication # 用于获取主线程 import threading -from hardware import transmitter from view.main_window import MainWindow from .camera_controller import CameraController from .bottom_control_controller import BottomControlController from .hopper_controller import HopperController from service.msg_recorder import MessageRecorder +from config.settings import Settings +from core.system import FeedingControlSystem +from opc.opcua_server import SimpleOPCUAServer class MainController: def __init__(self): # 主界面 self.main_window = MainWindow() - + self.settings = Settings() + self.system = FeedingControlSystem(self.settings) + self.system.initialize() + self.system.state.state_updated.connect(self.update_ui_notify) + self.opcua_server = SimpleOPCUAServer(self.system.state) + self.opcua_server.start() self.msg_recorder = MessageRecorder() self.msg_recorder.normal_record("开始自动智能浇筑系统") - + # 初始化子界面和控制器 self._initSubViews() self._initSubControllers() @@ -53,7 +60,10 @@ class MainController: def _initSubViews(self): pass - + + def update_ui_notify(self, prop:str,value): + """更新UI状态""" + print(f"更新UI状态: {prop} = {value}") def __connectSignals(self): self.main_window.about_to_close.connect(self.handleMainWindowClose) # 处理主界面关闭 diff --git a/core/state.py b/core/state.py index 6914504..58d4425 100644 --- a/core/state.py +++ b/core/state.py @@ -7,12 +7,32 @@ class SystemState(QObject): state_updated=Signal(str,object) def __init__(self): super().__init__() + # + self._watched_props = [] + self.lock = threading.RLock() + # 系统运行状态 - self.running = False + self.running = True - # 下料控制相关 + # 上料斗控制相关 self._upper_door_position = 'default' # default(在搅拌楼下接料), over_lower(在下料斗上方), returning(返回中) + # 是否破拱 + self._upper_is_arch_=False + self._upper_door_closed=True + self._upper_weight=0 + self._upper_volume=0.0 + + #下料斗状态想着 self._lower_feeding_stage = 0 # 0:未下料, 1:第一阶段, 2:第二阶段, 3:第三阶段, 4:等待模具车对齐 + self._lower_is_arch_=False + self._lower_weight=0 + self._lower_angle=0.0 + + #模具车状态 + self._mould_weight=0 + self._mould_frequency=220 + self._mould_vibrate_status=0 #1振动中0未振动 + self.lower_feeding_cycle = 0 # 下料斗下料循环次数 self.upper_feeding_count = 0 # 上料斗已下料次数 self.upper_feeding_max = 2 #上料斗最大下料次数 @@ -21,7 +41,11 @@ class SystemState(QObject): self.last_upper_weight = 0 self.last_lower_weight = 0 self.last_weight_time = 0 - self.need_total_weight=0 + #需要下料的总重量 + self._mould_need_weight=0 + #完成下料的总重量 + self._mould_finish_weight=0 + self.initial_upper_weight=0 self.initial_lower_weight=0 @@ -31,7 +55,8 @@ class SystemState(QObject): # 视觉系统状态 self.angle_control_mode = "normal" # 角度控制模式: normal, reducing, maintaining, recovery - self.overflow_detected = False # 堆料检测 + self.overflow_detected = "0" # 堆料检测 + self.current_finish_status=False # 当前是否完成浇筑满 self.door_opening_large = False # 夹角 self.vehicle_aligned = False # 模具车是否对齐 self.last_angle = None # 上次检测角度 @@ -42,16 +67,13 @@ class SystemState(QObject): #当前生产的管片 self.current_artifact=None #当前生产状态 - self.feed_status=FeedStatus.FNone + self._feed_status=FeedStatus.FNone #每方重量 self.density=2500 - - # 记录需要监听的属性名(筛选掉不需要发信号的内部变量) - #是否破拱 - self._upper_is_arch_=False - self._lower_is_arch_=False - self.lock = threading.RLock() + + + # self._watched_props = [k for k in self.__dict__ if k.startswith('_')] def __setattr__(self, name, value): diff --git a/core/system.py b/core/system.py index 18d452a..d8bea07 100644 --- a/core/system.py +++ b/core/system.py @@ -8,7 +8,7 @@ from hardware.relay import RelayController from hardware.inverter import InverterController from hardware.transmitter import TransmitterController from hardware.RFID.rfid_service import rfid_service -from vision.camera import CameraController +from vision.camera import DualCameraController from vision.detector import VisionDetector from feeding.controller import FeedingController @@ -28,9 +28,15 @@ class FeedingControlSystem: self.transmitter_controller = TransmitterController(self.relay_controller) # 初始化视觉系统 - self.camera_controller = CameraController() + self.camera_controller = DualCameraController(settings.camera_configs) + self.vision_detector = VisionDetector(settings) + # 初始化RFID控制器 + self.rfid_controller = rfid_service( + host=settings.rfid_host, + port=settings.rfid_port + ) # 初始化下料控制器 self.feeding_controller = FeedingController( self.relay_controller, @@ -43,11 +49,7 @@ class FeedingControlSystem: settings ) - # 初始化RFID控制器 - self.rfid_controller = rfid_service( - host=settings.rfid_host, - port=settings.rfid_port - ) + # 线程管理 self.monitor_thread = None @@ -60,39 +62,38 @@ class FeedingControlSystem: print("初始化控制系统...") # 设置摄像头配置 - self.camera_controller.set_config( - camera_type=self.settings.camera_type, - ip=self.settings.camera_ip, - port=self.settings.camera_port, - username=self.settings.camera_username, - password=self.settings.camera_password, - channel=self.settings.camera_channel - ) + # self.camera_controller.set_config( + # camera_type=self.settings.camera_type, + # ip=self.settings.camera_ip, + # port=self.settings.camera_port, + # username=self.settings.camera_username, + # password=self.settings.camera_password, + # channel=self.settings.camera_channel + # ) - # 初始化摄像头 - if not self.camera_controller.setup_capture(): - raise Exception("摄像头初始化失败") + # # 初始化摄像头 + # if not self.camera_controller.setup_capture(): + # raise Exception("摄像头初始化失败") # 加载视觉模型 - if not self.vision_detector.load_models(): - raise Exception("视觉模型加载失败") + # if not self.vision_detector.load_models(): + # raise Exception("视觉模型加载失败") + if not self.settings.debug_feeding: + if not self.check_device_connectivity(): + raise Exception("设备连接失败") + self.camera_controller.start_cameras() + # 启动系统监控(要料,破拱)线程 + self.start_monitoring() - if not self.check_device_connectivity(): - raise Exception("设备连接失败") + # 启动视觉控制(角度、溢出)线程 + self.start_visual_control() - # 启动系统监控 - self.start_monitoring() + # 启动对齐检查线程 + self.start_alignment_check() - # 启动视觉控制 - self.start_visual_control() - - # 启动对齐检查 - self.start_alignment_check() - - # 启动下料轮询线程 + # 启动下料线程 self.start_lower_feeding() - print("控制系统初始化完成") def start_monitoring(self): @@ -126,7 +127,8 @@ class FeedingControlSystem: """视觉控制循环""" while self.state.running: try: - current_frame = self.camera_controller.capture_frame() + print('visual_control') + current_frame = self.camera_controller.get_single_latest_frame() if current_frame is not None: # 执行视觉控制逻辑 self.feeding_controller.visual_control(current_frame) @@ -148,13 +150,15 @@ class FeedingControlSystem: while self.state.running: try: if self.state._lower_feeding_stage == 4: # 等待对齐阶段 - current_frame = self.camera_controller.capture_frame() + current_frame = self.camera_controller.get_single_latest_frame() if current_frame is not None: self.state.vehicle_aligned = self.vision_detector.detect_vehicle_alignment(current_frame) if self.state.vehicle_aligned: print("检测到模具车对齐") else: print("模具车未对齐") + else: + print('未检测到图像') time.sleep(self.settings.alignment_check_interval) except Exception as e: print(f"对齐检查循环错误: {e}") diff --git a/db/messages.db b/db/messages.db new file mode 100644 index 0000000..20aab85 Binary files /dev/null and b/db/messages.db differ diff --git a/db/three.db b/db/three.db index 6681482..047574e 100644 Binary files a/db/three.db and b/db/three.db differ diff --git a/db/three.db-shm b/db/three.db-shm new file mode 100644 index 0000000..fe9ac28 Binary files /dev/null and b/db/three.db-shm differ diff --git a/db/three.db-wal b/db/three.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/doc/~$ble表设计.doc b/doc/~$ble表设计.doc new file mode 100644 index 0000000..f600800 Binary files /dev/null and b/doc/~$ble表设计.doc differ diff --git a/doc/~$宝-生产信息接口文档-20250911.doc b/doc/~$宝-生产信息接口文档-20250911.doc new file mode 100644 index 0000000..7018ce2 Binary files /dev/null and b/doc/~$宝-生产信息接口文档-20250911.doc differ diff --git a/doc/~$系统对接接口文档-20251020.doc b/doc/~$系统对接接口文档-20251020.doc new file mode 100644 index 0000000..702be87 Binary files /dev/null and b/doc/~$系统对接接口文档-20251020.doc differ diff --git a/doc/控制程序对接文档.docx b/doc/控制程序对接文档.docx new file mode 100644 index 0000000..e2292dd Binary files /dev/null and b/doc/控制程序对接文档.docx differ diff --git a/feeding/__pycache__/__init__.cpython-39.pyc b/feeding/__pycache__/__init__.cpython-39.pyc index 28e57b6..386b49b 100644 Binary files a/feeding/__pycache__/__init__.cpython-39.pyc and b/feeding/__pycache__/__init__.cpython-39.pyc differ diff --git a/feeding/__pycache__/controller.cpython-39.pyc b/feeding/__pycache__/controller.cpython-39.pyc index e56c404..ff4f143 100644 Binary files a/feeding/__pycache__/controller.cpython-39.pyc and b/feeding/__pycache__/controller.cpython-39.pyc differ diff --git a/feeding/__pycache__/process.cpython-39.pyc b/feeding/__pycache__/process.cpython-39.pyc index cf4b481..07f329e 100644 Binary files a/feeding/__pycache__/process.cpython-39.pyc and b/feeding/__pycache__/process.cpython-39.pyc differ diff --git a/feeding/controller.py b/feeding/controller.py index dd31d53..cb7e9fa 100644 --- a/feeding/controller.py +++ b/feeding/controller.py @@ -1,5 +1,6 @@ # feeding/controller.py import time +from core.state import FeedStatus from feeding.process import FeedingProcess from busisness.blls import ArtifactBll @@ -49,10 +50,11 @@ class FeedingController: self.state._upper_door_position = 'over_lower' self.state.upper_weight_error_count = 0 # 判断是否需要要料:当前重量 < 目标重量 + 缓冲重量 - if current_weight < (self.settings.single_batch_weight + self.settings.min_required_weight): - print("上料斗重量不足,通知搅拌楼要料") - self.request_material_from_mixing_building() # 请求搅拌楼下料 - return True + if self.state.feed_status != FeedStatus.FUpperToLower: + if current_weight < (self.settings.min_required_weight): + print("上料斗重量不足,通知搅拌楼要料") + self.request_material_from_mixing_building() # 请求搅拌楼下料 + return True return False def request_material_from_mixing_building(self): @@ -115,68 +117,89 @@ class FeedingController: 视觉控制主逻辑 """ # 检测是否溢料 - overflow = self.vision_detector.detect_overflow(current_frame) - - # 获取当前角度 + self.state.overflow_detected = self.vision_detector.detect_overflow(current_frame) + overflow=self.state.overflow_detected in ["大堆料", "小堆料"] current_angle = self.vision_detector.detect_angle(image=current_frame) - if current_angle is None: print("无法获取当前角度,跳过本次调整") return - - print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 控制模式: {self.state.angle_control_mode}") - - # 状态机控制逻辑 - if self.state.angle_control_mode == "normal": - # 正常模式 - if overflow and current_angle > self.settings.angle_threshold: - # 检测到堆料且角度过大,进入角度减小模式 - print("检测到堆料且角度过大,关闭出砼门开始减小角度") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') - self.state.angle_control_mode = "reducing" - else: - # 保持正常开门 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - - elif self.state.angle_control_mode == "reducing": - # 角度减小模式 - if current_angle <= self.settings.target_angle + self.settings.angle_tolerance: - # 角度已达到目标范围 - if overflow: - # 仍有堆料,进入维持模式 - print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") - self.state.angle_control_mode = "maintaining" - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') # 先打开门 - else: - # 无堆料,恢复正常模式 - print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - self.state.angle_control_mode = "normal" - - elif self.state.angle_control_mode == "maintaining": - # 维持模式 - 使用脉冲控制 - if not overflow: - # 堆料已消除,恢复正常模式 - print("堆料已消除,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - self.state.angle_control_mode = "normal" - else: - # 继续维持角度控制 - self.pulse_control_door_for_maintaining() - - elif self.state.angle_control_mode == "recovery": - # 恢复模式 - 逐步打开门 - if overflow: - # 又出现堆料,回到角度减小模式 - print("恢复过程中又检测到堆料,回到角度减小模式") - self.state.angle_control_mode = "maintaining" - else: - # 堆料已消除,恢复正常模式 - print("堆料已消除,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - self.state.angle_control_mode = "normal" - self.state.last_angle = current_angle + print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 溢料返回: {self.state.overflow_detected}") + return + if self.state.overflow_detected!="浇筑满": + if current_angle is None: + print("无法获取当前角度,跳过本次调整") + return + + print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 控制模式: {self.state.angle_control_mode}") + + # 状态机控制逻辑 + if self.state.angle_control_mode == "normal": + # 正常模式大于self.settings.angle_threshold=60度 + if overflow and current_angle > self.settings.angle_threshold: + # 检测到堆料且角度过大,进入角度减小模式 + print("检测到堆料且角度过大,关闭出砼门开始减小角度") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + self.state.angle_control_mode = "reducing" + else: + # 保持正常开门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + if current_angle >self.settings.target_angle: + # 角度已降至目标范围,关闭出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + + elif self.state.angle_control_mode == "reducing": + # 角度减小模式 + if current_angle <= self.settings.target_angle + self.settings.angle_tolerance: + # 角度已达到目标范围 + if overflow: + # 仍有堆料,进入维持模式 + print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") + self.state.angle_control_mode = "maintaining" + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') # 先打开门 + else: + # 无堆料,恢复正常模式 + print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.state.angle_control_mode = "normal" + + elif self.state.angle_control_mode == "maintaining": + # 维持模式 - 使用脉冲控制 + if not overflow: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.state.angle_control_mode = "normal" + else: + # 继续维持角度控制 + self.pulse_control_door_for_maintaining() + + elif self.state.angle_control_mode == "recovery": + # 恢复模式 - 逐步打开门 + if overflow: + # 又出现堆料,回到角度减小模式 + print("恢复过程中又检测到堆料,回到角度减小模式") + self.state.angle_control_mode = "maintaining" + else: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.state.angle_control_mode = "normal" + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(5) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + def pulse_control_door_for_maintaining(self): """ @@ -185,8 +208,10 @@ class FeedingController: """ print("执行维持脉冲控制") # 关门1秒 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') time.sleep(1.0) # 开门1秒 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') time.sleep(1.0) diff --git a/feeding/process.py b/feeding/process.py index 63815bd..6da32ef 100644 --- a/feeding/process.py +++ b/feeding/process.py @@ -3,6 +3,8 @@ from core.state import FeedStatus from service.mould_service import MouldService from busisness.blls import ArtifactBll from busisness.models import ArtifactInfoModel +import time +from datetime import datetime class FeedingProcess: def __init__(self, relay_controller, inverter_controller, @@ -10,6 +12,7 @@ class FeedingProcess: camera_controller, state, settings): self.relay_controller = relay_controller self.artifact_bll = ArtifactBll() + self.mould_service = MouldService() self.inverter_controller = inverter_controller self.transmitter_controller = transmitter_controller self.vision_detector = vision_detector @@ -17,6 +20,8 @@ class FeedingProcess: self.state = state self.state.feed_status = FeedStatus.FNone + #标志位用,是否是第一次运行 + self.is_first_flag=True self.settings = settings def start_feeding(self): @@ -26,19 +31,32 @@ class FeedingProcess: loc_state.feed_status = FeedStatus.FCheckM return elif loc_state.feed_status == FeedStatus.FCheckM: + """开始生产管片""" loc_state._lower_feeding_stage = 4 - self.wait_for_vehicle_alignment() - loc_state.feed_status = FeedStatus.FStart - print("生产已检查模车") + + if self.settings.debug_feeding: + loc_state.feed_status = FeedStatus.FApiCheck + if self.state.vehicle_aligned: + loc_state.feed_status = FeedStatus.FCheckGB + print("检查模车") return elif loc_state.feed_status == FeedStatus.FApiCheck: print("生产已开始") time.sleep(2) - loc_modules = MouldService.get_not_pour_artifacts() + loc_modules = self.mould_service.get_not_pour_artifacts() if loc_modules: # 取第一个未浇筑的管片 + #后续放入队列处理 loc_module = loc_modules[0] + #API + loc_module.Source = 1 + loc_module.BeginTime=datetime.now() + + self.artifact_bll.insert_artifact_task(loc_module) self.state.current_artifact = loc_module + + # self.artifact_bll.finish_artifact_task(loc_state.current_artifact.ArtifactID,1.92) + self.state.feed_status = FeedStatus.FCheckGB else: #未读取到AIP接口数据. self.state.current_artifact = None @@ -71,7 +89,7 @@ class FeedingProcess: time.sleep(10) loc_state.feed_status = FeedStatus.FUpperToLower #计算本次生产需要的总重量 - print(f"本次生产需要的总重量:{self.state.need_total_weight}") + print(f"本次生产需要的总重量:{self.state._mould_need_weight}") return elif loc_state.feed_status == FeedStatus.FUpperToLower: print("上料斗向下料斗转移") @@ -80,13 +98,13 @@ class FeedingProcess: #下料斗重量 loc_state.initial_lower_weight=self.transmitter_controller.read_data(2) #需要的总重量 - loc_state.need_total_weight=loc_state.current_artifact.BetonVolume*loc_state.density - if loc_state.need_total_weight > loc_state.initial_upper_weight + loc_state.initial_lower_weight: + loc_state._mould_need_weight=loc_state.current_artifact.BetonVolume*loc_state.density + if loc_state._mould_need_weight > loc_state.initial_upper_weight + loc_state.initial_lower_weight: # 等待上料斗重量增加(多久不够报警,可能出现F块不足的情况) print('重量不够,需要增加') return - if loc_state.need_total_weight>loc_state.initial_lower_weight: + if loc_state._mould_need_weight>loc_state.initial_lower_weight: if self.state._upper_door_position != 'over_lower': #是否需要等待上料斗下料,如果下料斗够重量,则不需要等待 return @@ -94,32 +112,43 @@ class FeedingProcess: # 需要等待上料斗下料 # 最后一块进行尾数控制 # 最后一块F块,前面多要0.25,0.3,F块直接下料(先多下0.3后续) - loc_FWeight=0.3*loc_state.density - loc_feed_weight=loc_state.need_total_weight-loc_state.initial_lower_weight-loc_FWeight - self.transfer_material_from_upper_to_lower(loc_state.initial_upper_weight,loc_state.initial_lower_weight,loc_feed_weight) - - self.state.feed_status = FeedStatus.FFeed1 + # loc_FWeight=0.3*loc_state.density + # loc_feed_weight=loc_state.need_total_weight-loc_state.initial_lower_weight-loc_FWeight + self.transfer_material_from_upper_to_lower(loc_state.initial_upper_weight,loc_state.initial_lower_weight) + #完成了上料斗重量转移才进入下料斗 + loc_state.feed_status = FeedStatus.FFeed1 # time.sleep(10) return - elif self.state.feed_status == FeedStatus.FFeed1: + elif loc_state.feed_status == FeedStatus.FFeed1: #下料 # self._start_feeding_stage() + self.feeding_stage_one(loc_state) print("下料1") return - elif self.state.feed_status == FeedStatus.FFeed2: + elif loc_state.feed_status == FeedStatus.FFeed2: #上料 # self._start_feeding_stage() + self.feeding_stage_two(loc_state) print("下料2") return - elif self.state.feed_status == FeedStatus.FFeed3: + elif loc_state.feed_status == FeedStatus.FFeed3: #下料 # self._start_feeding_stage() + self.feeding_stage_three(loc_state) print("下料3") return - elif self.state.feed_status == FeedStatus.FFinished: - print("生产已完成") - self.state.feed_status = FeedStatus.FCheckM - return + elif loc_state.feed_status == FeedStatus.FFinished: + """完成当前批次下料""" + print("当前批次下料完成,关闭出砼门") + + if loc_state.overflow_detected=="浇筑满": + self.inverter_controller.control('stop') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') + #更新数据库状态 + self.artifact_bll.finish_artifact_task(loc_state.current_artifact.ArtifactID,loc_state._mould_finish_weight/loc_state.density) + loc_state.feed_status = FeedStatus.FCheckM + return def _start_feeding_stage(self): """启动指定下料阶段""" @@ -127,7 +156,7 @@ class FeedingProcess: print("开始分步下料过程") self.transfer_material_from_upper_to_lower() - def transfer_material_from_upper_to_lower(self,initial_upper_weight,initial_lower_weight,feed_weight): + def transfer_material_from_upper_to_lower(self,initial_upper_weight,initial_lower_weight): """target_upper_weight:转移后剩下的上料斗重量""" # 如果低于单次,全部卸掉 @@ -186,118 +215,110 @@ class FeedingProcess: break time.sleep(self.settings.alignment_check_interval) - def feeding_stage_one(self): + def feeding_stage_one(self,loc_state): """第一阶段下料:下料斗向模具车下料(低速)""" print("开始第一阶段下料:下料斗低速下料") - self.inverter_controller.set_frequency(self.settings.frequencies[0]) - self.inverter_controller.control('start') + if self.is_first_flag: + self.inverter_controller.set_frequency(self.settings.frequencies[0]) + self.inverter_controller.control('start') - # 确保上料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') - # 打开下料斗出砼门 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') + # 确保上料斗出砼门关闭 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + # 打开下料斗出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') + loc_cur_weight = self.transmitter_controller.read_data(2) + if loc_cur_weight is None: + #报警处理 + print("无法获取初始重量,取消下料") + # self.finish_feeding_process() + return + loc_state.initial_lower_weight=loc_cur_weight + self.is_first_flag=False - import time + start_time = time.time() - initial_weight = self.transmitter_controller.read_data(2) - if initial_weight is None: - print("无法获取初始重量,取消下料") - self.finish_feeding_process() + + current_weight = self.transmitter_controller.read_data(2) + if current_weight is None: + #报警处理 + print("无法获取当前重量,取消下料") + # self.finish_feeding_process() return + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + target_weight = loc_state._mould_need_weight/3 - target_weight = initial_weight + self.settings.single_batch_weight - - while self.state._lower_feeding_stage == 1: - current_weight = self.transmitter_controller.read_data(2) - if current_weight is None: - self.state.lower_weight_error_count += 1 - if self.state.lower_weight_error_count >= self.settings.max_error_count: - print("下料斗传感器连续读取失败,停止下料") - self.finish_feeding_process() - return - else: - self.state.lower_weight_error_count = 0 - - if (current_weight is not None and current_weight >= target_weight) or (time.time() - start_time) > 30: - self.state._lower_feeding_stage = 2 - self.feeding_stage_two() - break - time.sleep(2) + if (current_weight is not None and loc_state._mould_finish_weight >= target_weight) or (time.time() - start_time) > 30: + loc_state.feed_status = FeedStatus.FFeed2 + loc_state._lower_feeding_stage = 2 + self.is_first_flag=True + return + else: + time.sleep(1) def feeding_stage_two(self): """第二阶段下料:下料斗向模具车下料(中速)""" - print("开始第二阶段下料:下料斗中速下料") - self.inverter_controller.set_frequency(self.settings.frequencies[1]) + if self.is_first_flag: + print("开始第二阶段下料:下料斗中速下料") + self.inverter_controller.set_frequency(self.settings.frequencies[1]) - # 保持下料斗出砼门打开 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - # 确保上料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + # 保持下料斗出砼门打开 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') + # 确保上料斗出砼门关闭 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + # loc_cur_weight = self.transmitter_controller.read_data(2) + # if loc_cur_weight is None: + # #报警处理 + # print("无法获取初始重量,取消下料") + # # self.finish_feeding_process() + # return + # loc_state.initial_lower_weight=loc_cur_weight + self.is_first_flag=False - import time start_time = time.time() - initial_weight = self.transmitter_controller.read_data(2) - if initial_weight is None: - print("无法获取初始重量,取消下料") - self.finish_feeding_process() + current_weight = self.transmitter_controller.read_data(2) + if current_weight is None: + #报警处理 + print("无法获取当前重量,取消下料") + # self.finish_feeding_process() return - - target_weight = initial_weight + self.settings.single_batch_weight - - while self.state._lower_feeding_stage == 2: - current_weight = self.transmitter_controller.read_data(2) - if current_weight is None: - self.state.lower_weight_error_count += 1 - if self.state.lower_weight_error_count >= self.settings.max_error_count: - print("下料斗传感器连续读取失败,停止下料") - self.finish_feeding_process() - return - else: - self.state.lower_weight_error_count = 0 - - if (current_weight is not None and current_weight >= target_weight) or (time.time() - start_time) > 30: - self.state._lower_feeding_stage = 3 - self.feeding_stage_three() - break - time.sleep(2) - + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + target_weight = (loc_state._mould_need_weight/3)*2 + + if (current_weight is not None and loc_state._mould_finish_weight >= target_weight) or (time.time() - start_time) > 30: + loc_state.feed_status = FeedStatus.FFeed3 + loc_state._lower_feeding_stage = 3 + self.is_first_flag=True + return + else: + time.sleep(1) def feeding_stage_three(self): """第三阶段下料:下料斗向模具车下料(高速)""" - print("开始第三阶段下料:下料斗高速下料") - self.inverter_controller.set_frequency(self.settings.frequencies[2]) - - # 保持下料斗出砼门打开 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - # 确保上料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') - - import time - start_time = time.time() - initial_weight = self.transmitter_controller.read_data(2) - if initial_weight is None: - print("无法获取初始重量,取消下料") - self.finish_feeding_process() + if self.is_first_flag: + print("开始第三阶段下料:下料斗高速下料") + self.inverter_controller.set_frequency(self.settings.frequencies[2]) + # 保持下料斗出砼门打开 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') + # 确保上料斗出砼门关闭 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + self.is_first_flag=False + + current_weight = self.transmitter_controller.read_data(2) + if current_weight is None: + #报警处理 + print("无法获取当前重量,取消下料") + # self.finish_feeding_process() return - - target_weight = initial_weight + self.settings.single_batch_weight - - while self.state._lower_feeding_stage == 3: - current_weight = self.transmitter_controller.read_data(2) - if current_weight is None: - self.state.lower_weight_error_count += 1 - if self.state.lower_weight_error_count >= self.settings.max_error_count: - print("下料斗传感器连续读取失败,停止下料") - self.finish_feeding_process() - return - else: - self.state.lower_weight_error_count = 0 - - if (current_weight is not None and current_weight >= target_weight) or (time.time() - start_time) > 30: - self.state._lower_feeding_stage = 4 - self.finish_current_batch() - break - time.sleep(2) - + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + target_weight = loc_state._mould_need_weight + + if (current_weight is not None and loc_state._mould_finish_weight >= target_weight): + loc_state.feed_status = FeedStatus.FFinished + loc_state._lower_feeding_stage = 5 + self.is_first_flag=True + return + else: + time.sleep(1) + def finish_current_batch(self): """完成当前批次下料""" print("当前批次下料完成,关闭出砼门") @@ -341,5 +362,5 @@ class FeedingProcess: def return_upper_door_to_default(self): """上料斗回到默认位置(搅拌楼下接料位置)""" print("上料斗回到默认位置") - self.relay_controller.control(self.relay_controller.DOOR_UPPER, 'close') + self.relay_controller.control(self.relay_controller.UPPER_TO_JBL, 'open') self.state._upper_door_position = 'default' diff --git a/hardware/relay.py b/hardware/relay.py index 049942e..39f6a5e 100644 --- a/hardware/relay.py +++ b/hardware/relay.py @@ -3,44 +3,66 @@ import socket import binascii from pymodbus.client import ModbusTcpClient from pymodbus.exceptions import ModbusException +from config.settings import Settings class RelayController: # 继电器映射 - DOOR_UPPER = 'door_upper' # DO0 - 上料斗滑动 - DOOR_LOWER_1 = 'door_lower_1' # DO1 - 上料斗出砼门 - DOOR_LOWER_2 = 'door_lower_2' # DO2 - 下料斗出砼门 - BREAK_ARCH_UPPER = 'break_arch_upper' # DO3 - 上料斗破拱 - BREAK_ARCH_LOWER = 'break_arch_lower' # DO4 - 下料斗破拱 + FAST_STOP = 'fast_stop' # DO1 - 急停 + UPPER_TO_JBL = 'upper_to_jbl' # DO2 - 上料斗到搅拌楼 + UPPER_TO_ZD = 'upper_to_zd' # DO3 - 上料斗到振捣室 + # DOOR_UPPER = 'door_upper' # DO0 - 上料斗滑动 + DOOR_LOWER_OPEN = 'door_lower_open' # DO1 - 下料斗出砼门开角度 + DOOR_LOWER_CLOSE = 'door_lower_close' # DO2 - 下料斗出砼门关角度(角度在7.5以下可关闭信号) + DOOR_UPPER_OPEN = 'door_upper_open' # DO3 - 上料斗开 + DOOR_UPPER_CLOSE = 'door_upper_close' # DO4 - 上料斗关 + BREAK_ARCH_UPPER = 'break_arch_upper' # DO3 - 上料斗震动 + BREAK_ARCH_LOWER = 'break_arch_lower' # DO4 - 下料斗震动 + DIRECT_LOWER_FRONT = 'direct_lower_front' # DO5 - 下料斗前 + DIRECT_LOWER_BEHIND = 'direct_lower_behind' # DO6 - 下料斗后 + DIRECT_LOWER_LEFT = 'direct_lower_left' # DO7 - 下料斗左 + DIRECT_LOWER_RIGHT = 'direct_lower_right' # DO8 - 下料斗右 - def __init__(self, host='192.168.0.18', port=50000): + def __init__(self, host='192.168.250.62', port=50000): self.host = host self.port = port self.modbus_client = ModbusTcpClient(host, port=port) - +#遥1 DO 7 左 DO8 右 角度 摇2:DO 12上 13下 14 往后 15往前 # 继电器命令(原始Socket) self.relay_commands = { - self.DOOR_UPPER: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'}, - self.DOOR_LOWER_1: {'open': '00000000000601050001FF00', 'close': '000000000006010500010000'}, - self.DOOR_LOWER_2: {'open': '00000000000601050002FF00', 'close': '000000000006010500020000'}, - self.BREAK_ARCH_UPPER: {'open': '00000000000601050003FF00', 'close': '000000000006010500030000'}, - self.BREAK_ARCH_LOWER: {'open': '00000000000601050004FF00', 'close': '000000000006010500040000'} + self.FAST_STOP: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'}, + self.UPPER_TO_JBL: {'open': '00000000000601050001FF00', 'close': '000000000006010500010000'}, + self.UPPER_TO_ZD: {'open': '00000000000601050002FF00', 'close': '000000000006010500020000'}, + self.DOOR_LOWER_OPEN: {'open': '00000000000601050006FF00', 'close': '00000000000601050006FF00'}, + self.DOOR_LOWER_CLOSE: {'open': '00000000000601050007FF00', 'close': '000000000006010500070000'}, + self.DOOR_UPPER_OPEN: {'open': '00000000000601050003FF00', 'close': '000000000006010500030000'}, + self.DOOR_UPPER_CLOSE: {'open': '00000000000601050004FF00', 'close': '000000000006010500040000'}, + self.BREAK_ARCH_UPPER: {'open': '0000000000060105000AFF00', 'close': '0000000000060105000A0000'}, + self.BREAK_ARCH_LOWER: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'}, + self.DIRECT_LOWER_FRONT: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'}, + self.DIRECT_LOWER_BEHIND: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'}, + self.DIRECT_LOWER_LEFT: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'}, + self.DIRECT_LOWER_RIGHT: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'} } + self.settings = Settings() # 读取状态命令 self.read_status_command = '000000000006010100000008' # 设备位映射 self.device_bit_map = { - self.DOOR_UPPER: 0, - self.DOOR_LOWER_1: 1, - self.DOOR_LOWER_2: 2, + self.FAST_STOP: 0, + self.UPPER_TO_JBL: 1, + self.UPPER_TO_ZD: 2, self.BREAK_ARCH_UPPER: 3, self.BREAK_ARCH_LOWER: 4 } def send_command(self, command_hex): """发送原始Socket命令""" + if not self.settings.debug_feeding: + return None + try: byte_data = binascii.unhexlify(command_hex) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: diff --git a/hardware/transmitter.py b/hardware/transmitter.py index d22b033..09e2b66 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -82,6 +82,8 @@ class TransmitterController: TIMEOUT = 2 # 超时时间为 2秒 BUFFER_SIZE= 1024 weight = None + if self.relay_controller.settings.debug_feeding: + return 0 import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -114,7 +116,48 @@ class TransmitterController: # 成功返回重量(int),失败返回None return weight - + + elif transmitter_id == 2: + # 上料斗变送器的信息: + IP = "192.168.250.66" + PORT = 8234 + TIMEOUT = 2 # 超时时间为 2秒 + BUFFER_SIZE= 1024 + weight = None + if self.relay_controller.settings.debug_feeding: + return 0 + import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) + s.connect((IP, PORT)) + # print(f"连接上料斗变送器 {IP}:{PORT} 成功") + + # 接收数据(变送器主动推送,recv即可获取数据) + data = s.recv(BUFFER_SIZE) + if data: + # print(f"收到原始数据:{data}") + + # 提取出完整的一个数据包 (\r\n结尾) + packet = self.get_latest_valid_packet(data) + if not packet: + print("未获取到有效数据包!!") + return None + # 解析重量 + weight = self.parse_weight(packet) + else: + print("未收到设备数据") + + except ConnectionRefusedError: + print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据") + except Exception as e: + print(f"读取异常:{e}") + + # 成功返回重量(int),失败返回None + return weight + def get_latest_valid_packet(self, raw_data): """ 解决TCP粘包: diff --git a/main.py b/main.py index e323843..ee63ec2 100644 --- a/main.py +++ b/main.py @@ -10,16 +10,34 @@ def main(): # 初始化系统 system = FeedingControlSystem(settings) + # system.camera_controller.start_cameras() + + # time.sleep(2) + # system._alignment_check_loop() + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'close') + + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(5) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'close') + # system._visual_control_loop() + try: # 系统初始化 - system.initialize() + # system.initialize() print("系统准备就绪,5秒后开始下料...") time.sleep(5) # 启动下料流程 - system.start_lower_feeding() + # system.start_lower_feeding() # 保持运行 while True: diff --git a/opc/opcua_client_test.py b/opc/opcua_client_test.py new file mode 100644 index 0000000..303b193 --- /dev/null +++ b/opc/opcua_client_test.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +OPC UA 客户端测试脚本 +用于连接和测试 OPC UA 服务器 +""" + +from opcua import Client +import time +import sys + +class OPCUAClientTest: + def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"): + """ + 初始化 OPC UA 客户端 + + Args: + server_url: 服务器URL地址 + """ + self.client = Client(server_url) + self.connected = False + + def connect(self): + """连接到服务器""" + try: + self.client.connect() + self.connected = True + print(f"成功连接到 OPC UA 服务器: {self.client.server_url}") + return True + except Exception as e: + print(f"连接服务器失败: {e}") + return False + + def disconnect(self): + """断开连接""" + if self.connected: + self.client.disconnect() + self.connected = False + print("已断开与 OPC UA 服务器的连接") + + def browse_nodes(self): + """浏览服务器节点结构""" + if not self.connected: + print("请先连接到服务器") + return + + try: + # 获取根节点 + root = self.client.get_root_node() + print(f"根节点: {root}") + + # 获取对象节点 + objects = self.client.get_objects_node() + print(f"对象节点: {objects}") + + # 浏览 IndustrialDevice 节点 + upper_device = objects.get_child("2:upper") + print(f"\n工业设备节点: {upper_device}") + + # 获取传感器节点 + lower_device = objects.get_child("2:lower") + print(f"传感器节点: {lower_device}") + + + print(f"温度传感器: {upper_device}") + print(f"压力传感器: {lower_device}") + + # 获取变量值 + print("\n=== 当前传感器数据 ===") + self.read_sensor_values(upper_device, lower_device) + + except Exception as e: + print(f"浏览节点时出错: {e}") + + def read_sensor_values(self, upper_device, lower_device): + """读取传感器数值""" + try: + # 读取温度 + temp_value = upper_device.get_child("2:upper_weight").get_value() + temp_unit = upper_device.get_child("2:lower_weight").get_value() + print(f"温度: {temp_value} {temp_unit}") + + + except Exception as e: + print(f"读取传感器数据时出错: {e}") + + def monitor_data(self, duration=30): + """监控数据变化""" + if not self.connected: + print("请先连接到服务器") + return + + print(f"\n开始监控数据变化,持续 {duration} 秒...") + + try: + # 获取传感器节点 + objects = self.client.get_objects_node() + upper_device = objects.get_child("2:upper") + lower_device = objects.get_child("2:lower") + + + + + start_time = time.time() + while time.time() - start_time < duration: + print(f"\n--- {time.strftime('%H:%M:%S')} ---") + self.read_sensor_values(upper_device, lower_device) + time.sleep(5) # 每5秒读取一次 + + except KeyboardInterrupt: + print("\n监控被用户中断") + except Exception as e: + print(f"监控数据时出错: {e}") + + + +def main(): + """主函数""" + # 创建客户端 + client = OPCUAClientTest("opc.tcp://localhost:4840/zjsh_feed/server/") + + try: + # 连接到服务器 + if not client.connect(): + return + + # 浏览节点结构 + client.browse_nodes() + + # 监控数据变化 + client.monitor_data(duration=30) + + # 测试写入数据 + # client.write_test_data() + + # 继续监控 + print("\n继续监控数据...") + client.monitor_data(duration=15) + + except KeyboardInterrupt: + print("\n客户端被用户中断") + finally: + # 断开连接 + client.disconnect() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + # 支持自定义服务器地址 + server_url = sys.argv[1] + client = OPCUAClientTest(server_url) + else: + client = OPCUAClientTest() + + try: + main() + except Exception as e: + print(f"客户端运行错误: {e}") + sys.exit(1) \ No newline at end of file diff --git a/opc/opcua_server.py b/opc/opcua_server.py new file mode 100644 index 0000000..98cbcff --- /dev/null +++ b/opc/opcua_server.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +简单的OPC UA服务器示例 +用于工业自动化数据通信 +""" + +from opcua import Server, ua +import time +import random +import threading +from datetime import datetime +from core.system import SystemState + +class SimpleOPCUAServer: + def __init__(self, state, endpoint="opc.tcp://0.0.0.0:4840/zjsh_feed/server/", name="Feed_Server"): + """ + 初始化OPC UA服务器 + + Args: + endpoint: 服务器端点地址 + name: 服务器名称 + """ + self.server = Server() + self.server.set_endpoint(endpoint) + self.server.set_server_name(name) + self.state = state + + # 设置服务器命名空间 + self.namespace = self.server.register_namespace("Feed_Control_System") + + # 获取对象节点 + self.objects = self.server.get_objects_node() + + # 创建自定义对象 + self.create_object_structure() + + # 运行标志 + self.running = False + + def create_object_structure(self): + """创建OPC UA对象结构""" + # 创建上料斗对象 + self.upper = self.objects.add_object(self.namespace, "upper") + self.lower=self.objects.add_object(self.namespace, "lower") + self.sys=self.objects.add_object(self.namespace, "sys") + + # 创建变量 + self.create_variables() + + def create_variables(self): + """创建OPC UA变量""" + # 上料斗重量变量 + self.upper_weight = self.upper.add_variable(self.namespace, "upper_weight", 0.0) + self.lower_weight = self.lower.add_variable(self.namespace, "lower_weight", 0.0) + + # 设置变量为可写 + self.upper_weight.set_writable() + self.lower_weight.set_writable() + + def setup_state_listeners(self): + """设置状态监听器 - 事件驱动更新""" + if hasattr(self.state, 'state_updated'): + self.state.state_updated.connect(self.on_state_changed) + print("状态监听器已设置 - 事件驱动模式") + + def on_state_changed(self, property_name, value): + """状态变化时的回调函数""" + try: + # 根据属性名更新对应的OPC UA变量 + if property_name == "upper_weight": + self.upper_weight.set_value(value) + elif property_name == "lower_weight": + self.lower_weight.set_value(value) + + # 可以在这里添加更多状态映射 + print(f"状态更新: {property_name} = {value}") + + except Exception as e: + print(f"状态更新错误: {e}") + + def start(self): + """启动服务器""" + try: + self.server.start() + self.running = True + print(f"OPC UA服务器启动成功!") + print(f"服务器端点: opc.tcp://0.0.0.0:4840/freeopcua/server/") + print(f"命名空间: {self.namespace}") + + # 初始化当前值 + if self.state: + self.upper_weight.set_value(self.state._upper_weight) + self.lower_weight.set_value(self.state._lower_weight) + print("已同步初始状态值") + + # 设置状态监听器 - 关键步骤! + self.setup_state_listeners() + + # # 只有在没有状态系统时才使用模拟线程 + # if not self.state: + # print("使用模拟数据模式") + # self.simulation_thread = threading.Thread(target=self.simulate_data) + # self.simulation_thread.daemon = True + # self.simulation_thread.start() + + except Exception as e: + print(f"启动服务器失败: {e}") + + def stop(self): + """停止服务器""" + self.running = False + if hasattr(self, 'simulation_thread'): + self.simulation_thread.join(timeout=2) + self.server.stop() + print("OPC UA服务器已停止") + + # 断开状态监听器 + if hasattr(self.state, 'state_updated'): + try: + self.state.state_updated.disconnect(self.on_state_changed) + except: + pass + + def simulate_data(self): + """模拟数据更新""" + while self.running: + try: + # 更新变量值 + self.upper_weight.set_value(self.state.upper_weight) + self.lower_weight.set_value(self.state.lower_weight) + + # 模拟延迟 + time.sleep(1) + + except Exception as e: + print(f"数据更新错误: {e}") + continue + + +def main(): + """主函数""" + # 创建系统状态实例 + state = SystemState() + + # 创建并启动服务器 + server = SimpleOPCUAServer( + state=state, + endpoint="opc.tcp://0.0.0.0:4840/freeopcua/server/", + name="工业自动化 OPC UA 服务器" + ) + + try: + server.start() + + + print("服务器正在运行,按 Ctrl+C 停止...") + + # 保持服务器运行 + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n正在停止服务器...") + server.stop() + + except Exception as e: + print(f"服务器运行错误: {e}") + server.stop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/resources/resources_rc.py b/resources/resources_rc.py index b661162..a07b775 100644 --- a/resources/resources_rc.py +++ b/resources/resources_rc.py @@ -14370,139 +14370,139 @@ qt_resource_struct = b"\ \x00\x00\x00\x10\x00\x02\x00\x00\x00C\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xb8\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x16\ -\x00\x00\x01\x9a>\xc4\x05p\ +\x00\x00\x01\x9a\x80)\xd9\x84\ \x00\x00\x04\x02\x00\x00\x00\x00\x00\x01\x00\x01\xfb\xa6\ -\x00\x00\x01\x9a>\xc4\x05s\ +\x00\x00\x01\x9a\x80)\xd3W\ \x00\x00\x05(\x00\x00\x00\x00\x00\x01\x00\x02\xbc.\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xd8a\ \x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00+8\ -\x00\x00\x01\x9a>\xc4\x05s\ +\x00\x00\x01\x9a\x80)\xdc9\ \x00\x00\x00T\x00\x00\x00\x00\x00\x01\x00\x00\x11r\ -\x00\x00\x01\x9a>\xc4\x05v\ +\x00\x00\x01\x9a\x80)\xd8\xb7\ \x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\xddk\ -\x00\x00\x01\x9a>\xc4\x05x\ +\x00\x00\x01\x9a\x80)\xda\x98\ \x00\x00\x02\xa6\x00\x00\x00\x00\x00\x01\x00\x01\x08\xf0\ -\x00\x00\x01\x9a>\xc4\x05x\ +\x00\x00\x01\x9a\x80)\xd9b\ \x00\x00\x00:\x00\x00\x00\x00\x00\x01\x00\x00\x01\x05\ -\x00\x00\x01\x9a>\xc4\x05u\ +\x00\x00\x01\x9a\x80)\xd6\x9c\ \x00\x00\x02\xd8\x00\x00\x00\x00\x00\x01\x00\x01\x1d/\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbb\ +\x00\x00\x01\x9a\x80)\xdc\x8a\ \x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x84\x83\ -\x00\x00\x01\x9a>\xc4\x05\x81\ +\x00\x00\x01\x9a\x80)\xdd\x04\ \x00\x00\x03\x8e\x00\x00\x00\x00\x00\x01\x00\x01N/\ -\x00\x00\x01\x9a>\xc4\x05r\ +\x00\x00\x01\x9a\x80)\xd8\xfc\ \x00\x00\x04\x84\x00\x00\x00\x00\x00\x01\x00\x02\x92\xa4\ -\x00\x00\x01\x9a>\xc4\x05r\ +\x00\x00\x01\x9a\x80)\xd9\xff\ \x00\x00\x01$\x00\x00\x00\x00\x00\x01\x00\x00-\x82\ -\x00\x00\x01\x9a\x14\xe2\x1b\xb9\ +\x00\x00\x01\x9a\x80)\xdaB\ \x00\x00\x02x\x00\x00\x00\x00\x00\x01\x00\x00\xf5\xe3\ -\x00\x00\x01\x9a>\xc4\x05\x83\ +\x00\x00\x01\x9a\x80)\xda]\ \x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x00c\xfb\ -\x00\x00\x01\x9a>\xc4\x05|\ +\x00\x00\x01\x9a\x80)\xdb\xf3\ \x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x01\x02\xaa\ -\x00\x00\x01\x9a>\xc4\x05|\ +\x00\x00\x01\x9a\x80)\xd4\x9b\ \x00\x00\x03\x02\x00\x00\x00\x00\x00\x01\x00\x01%\xbb\ -\x00\x00\x01\x9a>\xc4\x05\x85\ +\x00\x00\x01\x9a\x80)\xda\xf5\ \x00\x00\x05p\x00\x00\x00\x00\x00\x01\x00\x02\xc7\x9f\ -\x00\x00\x01\x9a>\xc4\x05~\ +\x00\x00\x01\x9a\x80)\xd3\xfd\ \x00\x00\x03\xae\x00\x00\x00\x00\x00\x01\x00\x01Q\xcb\ -\x00\x00\x01\x9a>\xc4\x05\x86\ +\x00\x00\x01\x9a\x80)\xd7\x04\ \x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9a>\xc4\x05~\ +\x00\x00\x01\x9a\x80)\xd8A\ \x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x02\x96\x82\ -\x00\x00\x01\x9a>\xc4\x05w\ +\x00\x00\x01\x9a\x80)\xddw\ \x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00 \x01\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xd67\ \x00\x00\x06\x9a\x00\x00\x00\x00\x00\x01\x00\x03Yo\ -\x00\x00\x01\x9a>\xc4\x05}\ +\x00\x00\x01\x9a\x80)\xdbu\ \x00\x00\x04\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x94\xf7\ -\x00\x00\x01\x9a>\xc4\x05v\ +\x00\x00\x01\x9a\x80)\xd9\x1e\ \x00\x00\x03^\x00\x00\x00\x00\x00\x01\x00\x010\x95\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xda|\ \x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00((\ -\x00\x00\x01\x9a>\xc4\x05w\ +\x00\x00\x01\x9a\x80)\xd4Y\ \x00\x00\x06l\x00\x00\x00\x00\x00\x01\x00\x036\xa7\ -\x00\x00\x01\x9a>\xc4\x05\x80\ +\x00\x00\x01\x9a\x80)\xdcp\ \x00\x00\x02\x18\x00\x00\x00\x00\x00\x01\x00\x00\xe1c\ -\x00\x00\x01\x9a>\xc4\x05h\ +\x00\x00\x01\x9a\x80)\xdd;\ \x00\x00\x02`\x00\x00\x00\x00\x00\x01\x00\x00\xf4\xe4\ -\x00\x00\x01\x9a>\xc4\x05~\ +\x00\x00\x01\x9a\x80)\xd4\xba\ \x00\x00\x03\xc4\x00\x00\x00\x00\x00\x01\x00\x01\xa8}\ -\x00\x00\x01\x9a>\xc4\x05\x7f\ +\x00\x00\x01\x9a\x80)\xd5\x9f\ \x00\x00\x03>\x00\x00\x00\x00\x00\x01\x00\x01/\xe2\ -\x00\x00\x01\x9a>\xc4\x05k\ +\x00\x00\x01\x9a\x80)\xd9\xa3\ \x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\xe0\xd7\ -\x00\x00\x01\x9a>\xc4\x05l\ +\x00\x00\x01\x9a\x80)\xdd\x8f\ \x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x00z\x9d\ -\x00\x00\x01\x9a>\xc4\x05l\ +\x00\x00\x01\x9a\x80)\xdb\xb6\ \x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x02\xeb\xec\ -\x00\x00\x01\x9a>\xc4\x05m\ +\x00\x00\x01\x9a\x80)\xd3\x96\ \x00\x00\x06T\x00\x00\x00\x00\x00\x01\x00\x032L\ -\x00\x00\x01\x9a>\xc4\x05\x86\ +\x00\x00\x01\x9a\x80)\xd5|\ \x00\x00\x06\x86\x00\x00\x00\x00\x00\x01\x00\x03?\x19\ -\x00\x00\x01\x9a>\xc4\x05{\ +\x00\x00\x01\x9a\x80)\xd4\xfa\ \x00\x00\x04h\x00\x00\x00\x00\x00\x01\x00\x02\x8a\xb0\ -\x00\x00\x01\x9a>\xc4\x05\x84\ +\x00\x00\x01\x9a\x80)\xd7t\ \x00\x00\x05>\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd1\ -\x00\x00\x01\x9a>\xc4\x05\x85\ +\x00\x00\x01\x9a\x80)\xda#\ \x00\x00\x05\x0c\x00\x00\x00\x00\x00\x01\x00\x02\xb9\x8e\ -\x00\x00\x01\x9a>\xc4\x05t\ +\x00\x00\x01\x9a\x80)\xd4;\ \x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00#\xb3\ -\x00\x00\x01\x9a>\xc4\x05t\ +\x00\x00\x01\x9a\x80)\xdc\x17\ \x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x00!\x14\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbb\ +\x00\x00\x01\x9a\x80)\xd6z\ \x00\x00\x06\xb2\x00\x00\x00\x00\x00\x01\x00\x03_2\ -\x00\x00\x01\x9a>\xc4\x05{\ +\x00\x00\x01\x9a\x80)\xda\xda\ \x00\x00\x01`\x00\x00\x00\x00\x00\x01\x00\x00rC\ -\x00\x00\x01\x9a>\xc4\x05\x84\ +\x00\x00\x01\x9a\x80)\xd9?\ \x00\x00\x064\x00\x00\x00\x00\x00\x01\x00\x03)\xfc\ -\x00\x00\x01\x9a>\xc4\x05\x83\ +\x00\x00\x01\x9a\x80)\xdb\x19\ \x00\x00\x05\xa6\x00\x00\x00\x00\x00\x01\x00\x02\xd7\xf5\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xddX\ \x00\x00\x01\xa0\x00\x00\x00\x00\x00\x01\x00\x00{\x07\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xd5\xc2\ \x00\x00\x03\x1e\x00\x00\x00\x00\x00\x01\x00\x01&\xcb\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xdb\xd3\ \x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x02\x98\x0d\ -\x00\x00\x01\x9a>\xc4\x05\x7f\ +\x00\x00\x01\x9a\x80)\xdcT\ \x00\x00\x06\x0a\x00\x00\x00\x00\x00\x01\x00\x02\xecg\ -\x00\x00\x01\x9a>\xc4\x05x\ +\x00\x00\x01\x9a\x80)\xd8\x9b\ \x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x84.\ -\x00\x00\x01\x9a>\xc4\x05i\ +\x00\x00\x01\x9a\x80)\xdbU\ \x00\x00\x04\xf2\x00\x00\x00\x00\x00\x01\x00\x02\x9c\x95\ -\x00\x00\x01\x9a>\xc4\x05q\ +\x00\x00\x01\x9a\x80)\xd7O\ \x00\x00\x04L\x00\x00\x00\x00\x00\x01\x00\x02h\xd9\ -\x00\x00\x01\x9a>\xc4\x05j\ +\x00\x00\x01\x9a\x80)\xd7*\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x01\xaa\x0a\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xd4\x1c\ \x00\x00\x00n\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x5c\ -\x00\x00\x01\x9a>\xc4\x05\x85\ +\x00\x00\x01\x9a\x80)\xd3\xdc\ \x00\x00\x05\x88\x00\x00\x00\x00\x00\x01\x00\x02\xc8\xac\ -\x00\x00\x01\x9a>\xc4\x05\x80\ +\x00\x00\x01\x9a\x80)\xd6\xdf\ \x00\x00\x02\xea\x00\x00\x00\x00\x00\x01\x00\x01\x22\x08\ -\x00\x00\x01\x9a>\xc4\x05t\ +\x00\x00\x01\x9a\x80)\xd8\x03\ \x00\x00\x06 \x00\x00\x00\x00\x00\x01\x00\x03&\x04\ -\x00\x00\x01\x9a>\xc4\x05q\ +\x00\x00\x01\x9a\x80)\xd6\x06\ \x00\x00\x020\x00\x00\x00\x00\x00\x01\x00\x00\xea\x99\ -\x00\x00\x01\x9a>\xc4\x05}\ +\x00\x00\x01\x9a\x80)\xd5\x1a\ \x00\x00\x05Z\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x8e\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xd6\xbe\ \x00\x00\x04\x22\x00\x00\x00\x00\x00\x01\x00\x01\xffL\ -\x00\x00\x01\x9a>\xc4\x05y\ +\x00\x00\x01\x9a\x80)\xda\xbe\ \x00\x00\x01:\x00\x00\x00\x00\x00\x01\x00\x00.\xdc\ -\x00\x00\x01\x9a>\xc4\x05z\ +\x00\x00\x01\x9a\x80)\xdd\x22\ \x00\x00\x03\xf0\x00\x00\x00\x00\x00\x01\x00\x01\xabj\ -\x00\x00\x01\x9a>\xc4\x05z\ +\x00\x00\x01\x9a\x80)\xd5\xe3\ \x00\x00\x03v\x00\x00\x00\x00\x00\x01\x00\x01I\x0f\ -\x00\x00\x01\x9a>\xc4\x05|\ +\x00\x00\x01\x9a\x80)\xd9\xc9\ \x00\x00\x02H\x00\x00\x00\x00\x00\x01\x00\x00\xf4\x8f\ -\x00\x00\x01\x9a>\xc4\x05n\ +\x00\x00\x01\x9a\x80)\xd8\xda\ \x00\x00\x02\xc0\x00\x00\x00\x00\x00\x01\x00\x01\x1c\xda\ -\x00\x00\x01\x9a>\xc4\x05n\ +\x00\x00\x01\x9a\x80)\xd4x\ \x00\x00\x044\x00\x00\x00\x00\x00\x01\x00\x02h\x84\ -\x00\x00\x01\x9a>\xc4\x05p\ +\x00\x00\x01\x9a\x80)\xd7\x99\ \x00\x00\x00\xb4\x00\x00\x00\x00\x00\x01\x00\x00#^\ -\x00\x00\x01\x9a>\xc4\x05o\ +\x00\x00\x01\x9a\x80)\xd57\ " def qInitResources(): diff --git a/service/mould_service.py b/service/mould_service.py index 115f7ee..86263c4 100644 --- a/service/mould_service.py +++ b/service/mould_service.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from common.sqlite_handler import SQLiteHandler from typing import Optional, List -from api_http_client import api_http_client +from .api_http_client import api_http_client from busisness.models import ArtifactInfo, TaskInfo, LoginRequest from config.ini_manager import ini_manager diff --git a/tests/Investor485test.py b/tests/Investor485test.py new file mode 100644 index 0000000..8952bea --- /dev/null +++ b/tests/Investor485test.py @@ -0,0 +1,259 @@ +import serial +import time +import struct + + +class InovanceMD520: + def __init__(self, port='COM4', baudrate=9600, timeout=1): + """ + 初始化汇川MD520变频器通信 + :param port: 串口名称,Windows为COMx,Linux为/dev/ttyUSBx + :param baudrate: 波特率,默认9600 + :param timeout: 超时时间,秒 + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.ser = None + + def connect(self): + """连接串口""" + try: + self.ser = serial.Serial( + port=self.port, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=self.timeout + ) + print(f"成功连接到串口 {self.port}") + return True + except serial.SerialException as e: + print(f"连接串口失败: {e}") + return False + + def disconnect(self): + """断开串口连接""" + if self.ser and self.ser.is_open: + self.ser.close() + print("串口连接已关闭") + + def calculate_crc(self, data): + """ + 计算Modbus CRC16校验码 + :param data: 字节数据 + :return: CRC校验码(低位在前,高位在后) + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc = crc >> 1 + return struct.pack('> 8) & 0xFF, # 寄存器地址高字节 + register_addr & 0xFF, # 寄存器地址低字节 + (register_count >> 8) & 0xFF, # 寄存器数量高字节 + register_count & 0xFF # 寄存器数量低字节 + ]) + + # 计算CRC + crc = self.calculate_crc(cmd_data) + full_cmd = cmd_data + crc + + print(f"发送读取指令: {full_cmd.hex().upper()}") + + try: + self.ser.reset_input_buffer() + self.ser.write(full_cmd) + time.sleep(0.01) + + # 计算预期响应长度 + expected_length = 5 + 2 * register_count # 地址1 + 功能码1 + 字节数1 + 数据2*N + CRC2 + response = self.ser.read(expected_length) + + if len(response) < expected_length: + print(f"响应数据长度不足: {len(response)} 字节,期望 {expected_length} 字节") + return None + + print(f"收到响应: {response.hex().upper()}") + + # 验证CRC + received_crc = response[-2:] + calculated_crc = self.calculate_crc(response[:-2]) + if received_crc != calculated_crc: + print("CRC校验失败") + return None + + # 解析数据 + data_length = response[2] + data_bytes = response[3:3 + data_length] + + results = [] + for i in range(0, len(data_bytes), 2): + value = (data_bytes[i] << 8) | data_bytes[i + 1] + results.append(value) + + return results + + except Exception as e: + print(f"通信错误: {e}") + return None + + +def main(): + # 创建变频器对象 + inverter = InovanceMD520(port='COM4', baudrate=9600) + + # 连接串口 + if not inverter.connect(): + return + + try: + while True: + print("\n" + "=" * 50) + print("汇川MD520变频器频率查询") + print("=" * 50) + + # 查询运行频率 + frequency = inverter.query_frequency(slave_addr=0x01) + + if frequency is not None: + print(f"✅ 当前运行频率: {frequency:.2f} Hz") + else: + print("❌ 频率查询失败") + + # 可选:读取其他监控参数 + print("\n--- 其他监控参数 ---") + + # 读取母线电压 (地址1002H) + voltage_data = inverter.read_register(0x01, 0x1002) + if voltage_data: + voltage = voltage_data[0] / 10.0 # 单位0.1V + print(f"母线电压: {voltage:.1f} V") + + # 读取输出电压 (地址1003H) + output_voltage_data = inverter.read_register(0x01, 0x1003) + if output_voltage_data: + output_voltage = output_voltage_data[0] # 单位1V + print(f"输出电压: {output_voltage} V") + + # 读取输出电流 (地址1004H) + current_data = inverter.read_register(0x01, 0x1004) + if current_data: + current = current_data[0] / 100.0 # 单位0.01A + print(f"输出电流: {current:.2f} A") + + # 等待5秒后再次查询 + print("\n等待5秒后继续查询...") + time.sleep(5) + + except KeyboardInterrupt: + print("\n用户中断查询") + finally: + # 断开连接 + inverter.disconnect() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/hardware_test.py b/tests/hardware_test.py index ec501f3..63ddc6b 100644 --- a/tests/hardware_test.py +++ b/tests/hardware_test.py @@ -20,11 +20,12 @@ def test_relay_controls(relay): # 测试上料斗滑动门 print("测试上料斗滑动门开启...") - relay.control(relay.DOOR_UPPER, 'open') + relay.control(relay.UPPER_TO_JBL, 'open') + time.sleep(1) + relay.control(relay.UPPER_TO_JBL, 'close') time.sleep(1) - print("测试上料斗滑动门关闭...") - relay.control(relay.DOOR_UPPER, 'close') + relay.control(relay.UPPER_TO_ZD, 'open') time.sleep(1) # 测试上料斗出砼门 @@ -137,13 +138,13 @@ def main(): # 初始化控制器 print("初始化控制器...") relay = RelayController(host='192.168.0.18', port=50000) - inverter = InverterController(relay_controller=relay) - transmitter = TransmitterController(relay_controller=relay) + # inverter = InverterController(relay_controller=relay) + # transmitter = TransmitterController(relay_controller=relay) # 执行各项测试 test_relay_controls(relay) test_inverter_controls(inverter) - test_transmitter_reading(transmitter) + # test_transmitter_reading(transmitter) print("\n所有测试完成!") diff --git a/tests/test_relay_controller.py b/tests/test_relay_controller.py index ca030e8..5816aa4 100644 --- a/tests/test_relay_controller.py +++ b/tests/test_relay_controller.py @@ -13,7 +13,7 @@ class TestRelayController(unittest.TestCase): def setUp(self): """测试前的准备工作""" - self.relay_host = '192.168.0.18' + self.relay_host = '192.168.250.62' self.relay_port = 50000 self.relay = RelayController(host=self.relay_host, port=self.relay_port) @@ -24,7 +24,8 @@ class TestRelayController(unittest.TestCase): self.assertIsNotNone(self.relay.modbus_client) # 检查设备映射 - self.assertIn(RelayController.DOOR_UPPER, self.relay.device_bit_map) + self.assertIn(RelayController.UPPER_TO_JBL, self.relay.device_bit_map) + self.assertIn(RelayController.UPPER_TO_ZD, self.relay.device_bit_map) self.assertIn(RelayController.DOOR_LOWER_1, self.relay.device_bit_map) self.assertIn(RelayController.DOOR_LOWER_2, self.relay.device_bit_map) self.assertIn(RelayController.BREAK_ARCH_UPPER, self.relay.device_bit_map) @@ -62,7 +63,7 @@ class TestRelayController(unittest.TestCase): """测试控制有效设备和动作""" with patch.object(self.relay, 'send_command') as mock_send: # 测试打开上料斗门 - self.relay.control(RelayController.DOOR_UPPER, 'open') + self.relay.control(RelayController.UPPER_TO_JBL, 'open') mock_send.assert_called_once() def test_control_invalid_device(self): @@ -76,7 +77,7 @@ class TestRelayController(unittest.TestCase): """测试控制无效动作""" with patch.object(self.relay, 'send_command') as mock_send: # 测试无效动作 - self.relay.control(RelayController.DOOR_UPPER, 'invalid_action') + self.relay.control(RelayController.UPPER_TO_JBL, 'invalid_action') mock_send.assert_not_called() @patch.object(RelayController, 'send_command') @@ -89,7 +90,8 @@ class TestRelayController(unittest.TestCase): # 验证返回的状态字典 self.assertIsInstance(status, dict) - self.assertIn(RelayController.DOOR_UPPER, status) + self.assertIn(RelayController.UPPER_TO_JBL, status) + self.assertIn(RelayController.UPPER_TO_ZD, status) mock_send_command.assert_called_once_with(self.relay.read_status_command) @patch.object(RelayController, 'send_command') diff --git a/tests/test_vision.py b/tests/test_vision.py new file mode 100644 index 0000000..b17c457 --- /dev/null +++ b/tests/test_vision.py @@ -0,0 +1,65 @@ +import os +import sys +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import unittest +from unittest.mock import MagicMock +from vision.camera import CameraController + +# from core.vision import Vision + +class TestVision(unittest.TestCase): + + def setUp(self): + self.jj=2 + # self.testclass2 = MagicMock() + # self.testclass = TestClass(self.testclass2) + + def test_capture_frame(self): + # 测试capture_frame方法 + camera=CameraController() + result = camera.capture_frame() + self.assertIsNone(result, msg="capture_frame方法测试失败") + camera.capture_frame_exec.assert_called_once() + + # def test_first(self): + # 测试TestClass的add方法 + # mock_testclass2 = MagicMock() + # # mock_testclass2.i = 1 + # # mock_testclass2.j = 2 + + # test_class = TestClass(mock_testclass2) + # result = test_class.add() + + # # 验证结果 + # self.assertEqual(result, 3, msg="add方法测试失败") + + # def test_second(self): + # 测试TestClass2的mock行为 + # mock_testclass2 = MagicMock(spec=TestClass2) + # mock_testclass2.sub.return_value = 1 + # result = mock_testclass2.sub() + + # # 测试返回值 + # self.assertEqual(result, 1, msg="sub方法测试失败") + # mock_testclass2.sub.assert_called_once() + + +class TestClass: + def __init__(self,testclass2): + self.testclass2 = testclass2 + pass + def add(self): + return self.testclass2.i + self.testclass2.j + +class TestClass2: + def __init__(self): + self.i = 1 + self.j = 2 + pass + + def sub(self): + return self.j - self.i + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/vision/.py b/vision/.py new file mode 100644 index 0000000..7c4da50 --- /dev/null +++ b/vision/.py @@ -0,0 +1,137 @@ +# vision/camera.py +import cv2 + + +class CameraController: + def __init__(self): + self.camera = None + self.camera_type = "ip" + self.camera_ip = "192.168.1.51" + self.camera_port = 554 + self.camera_username = "admin" + self.camera_password = "XJ123456" + self.camera_channel = 1 + + def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1): + """ + 设置摄像头配置 + """ + self.camera_type = camera_type + if ip: + self.camera_ip = ip + if port: + self.camera_port = port + if username: + self.camera_username = username + if password: + self.camera_password = password + self.camera_channel = channel + + def setup_capture(self, camera_index=0): + """ + 设置摄像头捕获 + """ + try: + rtsp_url = f"rtsp://{self.camera_username}:{self.camera_password}@{self.camera_ip}:{self.camera_port}/streaming/channels/{self.camera_channel}01" + self.camera = cv2.VideoCapture(rtsp_url) + + if not self.camera.isOpened(): + print(f"无法打开网络摄像头: {rtsp_url}") + return False + print(f"网络摄像头初始化成功,地址: {rtsp_url}") + return True + except Exception as e: + print(f"摄像头设置失败: {e}") + return False + + def capture_frame_exec(self): + """捕获当前帧并返回numpy数组,设置5秒总超时""" + try: + if self.camera is None: + print("摄像头未初始化") + return None + + # 设置总超时时间为5秒 + total_timeout = 5.0 # 5秒总超时时间 + start_time = time.time() + + # 跳20帧,获取最新图像 + frames_skipped = 0 + while frames_skipped < 20: + # 检查总超时 + if time.time() - start_time > total_timeout: + print("捕获图像总超时") + return None + self.camera.grab() + time.sleep(0.05) # 稍微增加延迟,确保有新帧到达 + frames_skipped += 1 + + # 尝试读取帧,使用同一超时计时器 + read_attempts = 0 + max_read_attempts = 3 + if self.camera.grab(): + while read_attempts < max_read_attempts: + # 使用同一个超时计时器检查 + if time.time() - start_time > total_timeout: + print("捕获图像总超时") + return None + + ret, frame = self.camera.retrieve() + if ret: + return frame + else: + print(f"尝试读取图像帧失败,重试 ({read_attempts+1}/{max_read_attempts})") + read_attempts += 1 + # 短暂延迟后重试 + time.sleep(0.05) + + print("多次尝试后仍无法捕获有效图像帧") + return None + except Exception as e: + print(f"图像捕获失败: {e}") + return None + + def capture_frame(self): + """捕获当前帧并返回numpy数组""" + try: + if self.camera is None: + # self.set_config() + self.setup_capture() + + + frame = self.capture_frame_exec() + if frame is not None: + return frame + else: + print("无法捕获图像帧") + return None + except Exception as e: + print(f"图像捕获失败: {e}") + return None + + def capture_frame_bak(self): + """捕获当前帧并返回numpy数组""" + try: + if self.camera is None: + print("摄像头未初始化") + return None + + ret, frame = self.camera.read() + if ret: + return frame + else: + print("无法捕获图像帧") + return None + except Exception as e: + print(f"图像捕获失败: {e}") + return None + + def release(self): + """释放摄像头资源""" + if self.camera is not None: + self.camera.release() + self.camera = None + + def __del__(self): + """析构函数,确保资源释放""" + self.release() diff --git a/vision/align_model/__init__.py b/vision/align_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vision/align_model/aligment_inference.py b/vision/align_model/aligment_inference.py new file mode 100644 index 0000000..39b1695 --- /dev/null +++ b/vision/align_model/aligment_inference.py @@ -0,0 +1,167 @@ +import cv2 +import numpy as np +import platform +from .labels import labels # 确保这个文件存在 + + + +# ------------------- 核心:全局变量存储RKNN模型实例(确保只加载一次) ------------------- +# 初始化为None,首次调用时加载模型,后续直接复用 +_global_rknn_instance = None + +# device tree for RK356x/RK3576/RK3588 +DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible' + +def get_host(): + # get platform and device type + system = platform.system() + machine = platform.machine() + os_machine = system + '-' + machine + if os_machine == 'Linux-aarch64': + try: + with open(DEVICE_COMPATIBLE_NODE) as f: + device_compatible_str = f.read() + if 'rk3562' in device_compatible_str: + host = 'RK3562' + elif 'rk3576' in device_compatible_str: + host = 'RK3576' + elif 'rk3588' in device_compatible_str: + host = 'RK3588' + else: + host = 'RK3566_RK3568' + except IOError: + print('Read device node {} failed.'.format(DEVICE_COMPATIBLE_NODE)) + exit(-1) + else: + host = os_machine + return host + +def get_top1_class_str(result): + """ + 从推理结果中提取出得分最高的类别,并返回字符串 + + 参数: + result (list): 模型推理输出结果(格式需与原函数一致,如 [np.ndarray]) + 返回: + str:得分最高类别的格式化字符串 + 若推理失败,返回错误提示字符串 + """ + if result is None: + print("Inference failed: result is None") + return + + # 解析推理输出(与原逻辑一致:展平输出为1维数组) + output = result[0].reshape(-1) + + # 获取得分最高的类别索引(np.argmax 直接返回最大值索引,比排序更高效) + top1_index = np.argmax(output) + + # 处理标签(确保索引在 labels 列表范围内,避免越界) + if 0 <= top1_index < len(labels): + top1_class_name = labels[top1_index] + else: + top1_class_name = "Unknown Class" # 应对索引异常的边界情况 + + # 5. 格式化返回字符串(包含索引、得分、类别名称,得分保留6位小数) + return top1_class_name + +def preprocess(raw_image, target_size=(640, 640)): + """ + 读取图像并执行预处理(BGR转RGB、调整尺寸、添加Batch维度) + + 参数: + image_path (str): 图像文件的完整路径(如 "C:/test.jpg" 或 "/home/user/test.jpg") + target_size (tuple): 预处理后图像的目标尺寸,格式为 (width, height),默认 (640, 640) + 返回: + img (numpy.ndarray): 预处理后的图像 + 异常: + FileNotFoundError: 图像路径不存在或无法读取时抛出 + ValueError: 图像读取成功但为空(如文件损坏)时抛出 + """ + # img = cv2.cvtColor(raw_image, cv2.COLOR_BGR2RGB) + # 调整尺寸 + img = cv2.resize(raw_image, target_size) + img = np.expand_dims(img, 0) # 添加batch维度 + + return img + +# ------------------- 新增:模型初始化函数(控制只加载一次) ------------------- +def init_rknn_model(model_path): + """ + 初始化RKNN模型(全局唯一实例): + - 首次调用:加载模型+初始化运行时,返回模型实例 + - 后续调用:直接返回已加载的全局实例,避免重复加载 + """ + from rknnlite.api import RKNNLite + + global _global_rknn_instance # 声明使用全局变量 + + # 若模型未加载过,执行加载逻辑 + if _global_rknn_instance is None: + # 1. 创建RKNN实例(关闭内置日志) + rknn_lite = RKNNLite(verbose=False) + + # 2. 加载RKNN模型 + ret = rknn_lite.load_rknn(model_path) + if ret != 0: + print(f'[ERROR] Load CLS_RKNN model failed (code: {ret})') + exit(ret) + + # 3. 初始化运行时(绑定NPU核心0) + ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + print(f'[ERROR] Init CLS_RKNN runtime failed (code: {ret})') + exit(ret) + + # 4. 将加载好的实例赋值给全局变量 + _global_rknn_instance = rknn_lite + print(f'[INFO] CLS_RKNN model loaded successfully (path: {model_path})') + + return _global_rknn_instance + +def yolov11_cls_inference(model_path, raw_image, target_size=(640, 640)): + """ + 根据平台进行推理,并返回最终的分类结果 + + 参数: + model_path (str): RKNN模型文件路径 + image_path (str): 图像文件的完整路径(如 "C:/test.jpg" 或 "/home/user/test.jpg") + target_size (tuple): 预处理后图像的目标尺寸,格式为 (width, height),默认 (640, 640) + """ + rknn_model = model_path + + img = preprocess(raw_image, target_size) + + rknn = init_rknn_model(rknn_model) + if rknn is None: + return None, img + outputs = rknn.inference([img]) + + # Show the classification results + class_name = get_top1_class_str(outputs) + + # rknn_lite.release() + + return class_name + +if __name__ == '__main__': + + # 调用yolov11_cls_inference函数(target_size使用默认值640x640,也可显式传参如(112,112)) + image_path = "/userdata/reenrr/inference_with_lite/cover_ready.jpg" + bgr_image = cv2.imread(image_path) + if bgr_image is None: + print(f"Failed to read image from {image_path}") + exit(-1) + + rgb_frame = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB) + print(f"Read image from {image_path}, shape: {rgb_frame.shape}") + + result = yolov11_cls_inference( + # model_path="/userdata/PyQt_main_test/app/view/yolo/yolov11_cls.rknn", + model_path="/userdata/chuyiwen/Feeding_control_system/vision/align_model/yolov11_cls_640v6.rknn", + raw_image=rgb_frame, + target_size=(640, 640) + ) + # 打印最终结果 + print(f"\n最终分类结果:{result}") + diff --git a/vision/align_model/labels.py b/vision/align_model/labels.py new file mode 100644 index 0000000..4ed38b9 --- /dev/null +++ b/vision/align_model/labels.py @@ -0,0 +1,6 @@ +# the labels come from synset.txt, download link: https://s3.amazonaws.com/onnx-model-zoo/synset.txt + +labels = \ +{0: 'cover_noready', + 1: 'cover_ready' +} \ No newline at end of file diff --git a/vision/align_model/yolo11_main.py b/vision/align_model/yolo11_main.py new file mode 100644 index 0000000..578ea9b --- /dev/null +++ b/vision/align_model/yolo11_main.py @@ -0,0 +1,93 @@ +# yolo11_main.py +import cv2 +import numpy as np +from collections import deque +import os + +# 导入模块(不是函数) +from .aligment_inference import yolov11_cls_inference + +# 模型路径 +CLS_MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "yolov11_cls_640v6.rknn") + +class ClassificationStabilizer: + """分类结果稳定性校验器,处理瞬时噪声帧""" + + def __init__(self, window_size=5, switch_threshold=2): + self.window_size = window_size # 滑动窗口大小(缓存最近N帧结果) + self.switch_threshold = switch_threshold # 状态切换需要连续N帧一致 + self.result_buffer = deque(maxlen=window_size) # 缓存最近结果 + self.current_state = "盖板未对齐" # 初始状态 + self.consecutive_count = 0 # 当前状态连续出现的次数 + + def stabilize(self, current_frame_result): + """ + 输入当前帧的分类结果,返回经过稳定性校验的结果 + Args: + current_frame_result: 当前帧的原始分类结果(str) + Returns: + str: 经过校验的稳定结果 + """ + # 1. 将当前帧结果加入滑动窗口 + self.result_buffer.append(current_frame_result) + + # 2. 统计窗口内各结果的出现次数(多数投票基础) + result_counts = {} + for res in self.result_buffer: + result_counts[res] = result_counts.get(res, 0) + 1 # 使用 result_counts 字典记录每个元素出现的总次数。 + + # 3. 找到窗口中出现次数最多的结果(候选结果) + candidate = max(result_counts, key=result_counts.get) + + # 4. 状态切换校验:只有候选结果连续出现N次才允许切换 + if candidate == self.current_state: + # 与当前状态一致,重置连续计数 + self.consecutive_count = 0 + else: + # 与当前状态不一致,累计连续次数 + self.consecutive_count += 1 + # 连续达到阈值,才更新状态 + if self.consecutive_count >= self.switch_threshold: + self.current_state = candidate + self.consecutive_count = 0 + + return self.current_state + +# 初始化稳定性校验器(全局唯一实例,确保状态连续) +cls_stabilizer = ClassificationStabilizer( + window_size=5, # 缓存最近5帧 + switch_threshold=2 # 连续2帧一致才切换状态 +) + +# ====================== 分类接口(可选,保持原逻辑) ====================== +def run_yolo_classification(rgb_frame): + """ + YOLO 图像分类接口函数 + Args: + rgb_frame: numpy array (H, W, 3), RGB 格式 + Returns: + str: 分类结果("盖板对齐" / "盖板未对齐" / "异常") + """ + if not isinstance(rgb_frame, np.ndarray): + print(f"[ERROR] 输入类型错误:需为 np.ndarray,当前为 {type(rgb_frame)}") + return "异常" + + try: + cover_cls = yolov11_cls_inference(CLS_MODEL_PATH, rgb_frame, target_size=(640, 640)) + except Exception as e: + print(f"[WARN] 分类推理失败: {e}") + cover_cls = "异常" + + raw_result = "盖板未对齐" # 默认值 + # 结果映射 + if cover_cls == "cover_ready": + raw_result = "盖板对齐" + elif cover_cls == "cover_noready": + raw_result = "盖板未对齐" + else: + raw_result = "异常" + # 通过稳定性校验器处理,返回最终结果 + stable_result = cls_stabilizer.stabilize(raw_result) + print("raw_result, stable_result:",raw_result, stable_result) + return stable_result + diff --git a/vision/align_model/yolov11_cls_640v6.rknn b/vision/align_model/yolov11_cls_640v6.rknn new file mode 100644 index 0000000..4d4f55f Binary files /dev/null and b/vision/align_model/yolov11_cls_640v6.rknn differ diff --git a/vision/alignment_detector.py b/vision/alignment_detector.py index b535c34..2ee0998 100644 --- a/vision/alignment_detector.py +++ b/vision/alignment_detector.py @@ -1,30 +1,35 @@ # vision/alignment_detector.py -def detect_vehicle_alignment(image_array, alignment_model): +from vision.align_model.yolo11_main import run_yolo_classification +def detect_vehicle_alignment(image_array): """ 通过图像检测模具车是否对齐 """ try: # 检查模型是否已加载 - if alignment_model is None: - print("对齐检测模型未加载") - return False - if image_array is None: print("输入图像为空") return False # 直接使用模型进行推理 - results = alignment_model(image_array) - pared_probs = results[0].probs.data.cpu().numpy().flatten() + # results = alignment_model(image_array) + # pared_probs = results[0].probs.data.cpu().numpy().flatten() - # 类别0: 未对齐, 类别1: 对齐 - class_id = int(pared_probs.argmax()) - confidence = float(pared_probs[class_id]) + # # 类别0: 未对齐, 类别1: 对齐 + # class_id = int(pared_probs.argmax()) + # confidence = float(pared_probs[class_id]) + + # # 只有当对齐且置信度>95%时才认为对齐 + # if class_id == 1 and confidence > 0.95: + # return True + # return False + + # 使用yolov11_cls_inference函数进行推理 + results = run_yolo_classification(image_array) + if results=="盖板对齐": + return True + else: + return False - # 只有当对齐且置信度>95%时才认为对齐 - if class_id == 1 and confidence > 0.95: - return True - return False except Exception as e: print(f"对齐检测失败: {e}") return False diff --git a/vision/anger_caculate.py b/vision/anger_caculate.py index 2a82dd3..997eba6 100644 --- a/vision/anger_caculate.py +++ b/vision/anger_caculate.py @@ -1,88 +1,235 @@ import cv2 -import os import numpy as np -from ultralytics import YOLO +import math +from shapely.geometry import Polygon +from rknnlite.api import RKNNLite +import os -def predict_obb_best_angle(model=None, model_path=None, image=None, image_path=None, save_path=None): +# ------------------- 全局配置变量 ------------------- +# 模型相关 +CLASSES = ['clamp'] +nmsThresh = 0.4 +objectThresh = 0.35 + +# 可视化与保存控制(全局变量,可外部修改) +DRAW_RESULT = True # 是否在输出图像上绘制旋转框 +SAVE_PATH = None # 保存路径,如 "./result.jpg";设为 None 则不保存 + +# RKNN 单例 +_rknn_instance = None + +# ------------------- RKNN 管理函数 ------------------- +def init_rknn(model_path): + """只加载一次 RKNN 模型""" + global _rknn_instance + if _rknn_instance is None: + _rknn_instance = RKNNLite(verbose=False) + ret = _rknn_instance.load_rknn(model_path) + if ret != 0: + print(f"[ERROR] Failed to load RKNN model: {ret}") + return None + ret = _rknn_instance.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + print(f"[ERROR] Failed to init runtime: {ret}") + return None + return _rknn_instance + +def release_rknn(): + """释放 RKNN 对象""" + global _rknn_instance + if _rknn_instance: + _rknn_instance.release() + _rknn_instance = None + +# ------------------- 工具函数 ------------------- +def letterbox_resize(image, size, bg_color=114): + target_width, target_height = size + image_height, image_width, _ = image.shape + scale = min(target_width / image_width, target_height / image_height) + new_width, new_height = int(image_width * scale), int(image_height * scale) + image_resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) + canvas = np.ones((target_height, target_width, 3), dtype=np.uint8) * bg_color + offset_x, offset_y = (target_width - new_width) // 2, (target_height - new_height) // 2 + canvas[offset_y:offset_y + new_height, offset_x:offset_x + new_width] = image_resized + return canvas, scale, offset_x, offset_y + +class DetectBox: + def __init__(self, classId, score, xmin, ymin, xmax, ymax, angle): + self.classId = classId + self.score = score + self.xmin = xmin + self.ymin = ymin + self.xmax = xmax + self.ymax = ymax + self.angle = angle + +def rotate_rectangle(x1, y1, x2, y2, a): + cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + cos_a, sin_a = math.cos(a), math.sin(a) + pts = [(x1, y1), (x1, y2), (x2, y2), (x2, y1)] + return [[int(cx + (xx - cx) * cos_a - (yy - cy) * sin_a), + int(cy + (xx - cx) * sin_a + (yy - cy) * cos_a)] for xx, yy in pts] + +def intersection(g, p): + g = Polygon(np.array(g).reshape(-1, 2)) + p = Polygon(np.array(p).reshape(-1, 2)) + if not g.is_valid or not p.is_valid: + return 0 + inter = g.intersection(p).area + union = g.area + p.area - inter + return 0 if union == 0 else inter / union + +def NMS(detectResult): + predBoxs = [] + sort_detectboxs = sorted(detectResult, key=lambda x: x.score, reverse=True) + for i in range(len(sort_detectboxs)): + if sort_detectboxs[i].classId == -1: + continue + p1 = rotate_rectangle(sort_detectboxs[i].xmin, sort_detectboxs[i].ymin, + sort_detectboxs[i].xmax, sort_detectboxs[i].ymax, + sort_detectboxs[i].angle) + predBoxs.append(sort_detectboxs[i]) + for j in range(i + 1, len(sort_detectboxs)): + if sort_detectboxs[j].classId == sort_detectboxs[i].classId: + p2 = rotate_rectangle(sort_detectboxs[j].xmin, sort_detectboxs[j].ymin, + sort_detectboxs[j].xmax, sort_detectboxs[j].ymax, + sort_detectboxs[j].angle) + if intersection(p1, p2) > nmsThresh: + sort_detectboxs[j].classId = -1 + return predBoxs + +def sigmoid(x): + x = np.clip(x, -709, 709) # 防止 exp 溢出 + return np.where(x >= 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x))) + +def softmax(x, axis=-1): + exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True)) + return exp_x / np.sum(exp_x, axis=axis, keepdims=True) + +def process(out, model_w, model_h, stride, angle_feature, index, scale_w=1, scale_h=1): + class_num = len(CLASSES) + angle_feature = angle_feature.reshape(-1) + xywh = out[:, :64, :] + conf = sigmoid(out[:, 64:, :]) + conf = conf.reshape(-1) + boxes = [] + for ik in range(model_h * model_w * class_num): + if conf[ik] > objectThresh: + w = ik % model_w + h = (ik % (model_w * model_h)) // model_w + c = ik // (model_w * model_h) + xywh_ = xywh[0, :, (h * model_w) + w].reshape(1, 4, 16, 1) + data = np.arange(16).reshape(1, 1, 16, 1) + xywh_ = softmax(xywh_, 2) + xywh_ = np.sum(xywh_ * data, axis=2).reshape(-1) + xywh_add = xywh_[:2] + xywh_[2:] + xywh_sub = (xywh_[2:] - xywh_[:2]) / 2 + angle = (angle_feature[index + (h * model_w) + w] - 0.25) * math.pi + cos_a, sin_a = math.cos(angle), math.sin(angle) + xy = xywh_sub[0] * cos_a - xywh_sub[1] * sin_a, xywh_sub[0] * sin_a + xywh_sub[1] * cos_a + xywh1 = np.array([xy[0] + w + 0.5, xy[1] + h + 0.5, xywh_add[0], xywh_add[1]]) + xywh1 *= stride + xmin = (xywh1[0] - xywh1[2] / 2) * scale_w + ymin = (xywh1[1] - xywh1[3] / 2) * scale_h + xmax = (xywh1[0] + xywh1[2] / 2) * scale_w + ymax = (xywh1[1] + xywh1[3] / 2) * scale_h + boxes.append(DetectBox(c, conf[ik], xmin, ymin, xmax, ymax, angle)) + return boxes + +# ------------------- 主推理函数 ------------------- +def detect_two_box_angle(model_path, rgb_frame): """ - 输入: - model: 预加载的YOLO模型实例(可选) - model_path: YOLO 权重路径(当model为None时使用) - image: 图像数组(numpy array) - image_path: 图片路径(当image为None时使用) - save_path: 可选,保存带标注图像 - 输出: - angle_deg: 置信度最高两个框的主方向夹角(度),如果检测少于两个目标返回 None - annotated_img: 可视化图像 + 输入模型路径和 RGB 图像(numpy array),输出夹角和结果图像。 + 可视化和保存由全局变量 DRAW_RESULT 和 SAVE_PATH 控制。 """ - # 1. 使用预加载的模型或加载新模型 - if model is not None: - loaded_model = model - elif model_path is not None: - loaded_model = YOLO(model_path) - else: - raise ValueError("必须提供model或model_path参数") + global _rknn_instance, DRAW_RESULT, SAVE_PATH - # 2. 读取图像(优先使用传入的图像数组) - if image is not None: - img = image - elif image_path is not None: - img = cv2.imread(image_path) - if img is None: - print(f"无法读取图像: {image_path}") - return None, None - else: - raise ValueError("必须提供image或image_path参数") + if not isinstance(rgb_frame, np.ndarray) or rgb_frame is None: + print(f"[ERROR] detect_two_box_angle 接收到错误类型: {type(rgb_frame)}") + return None, np.zeros((640, 640, 3), np.uint8) - # 3. 推理 OBB - results = loaded_model(img, save=False, imgsz=640, conf=0.5, mode='obb') - result = results[0] + # 注意:输入是 BGR(因为 cv2.imread 返回 BGR),但内部会转为 RGB 给模型 + img = rgb_frame.copy() + img_resized, scale, offset_x, offset_y = letterbox_resize(img, (640, 640)) + infer_img = np.expand_dims(cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB), 0) - # 4. 可视化 - annotated_img = result.plot() - if save_path: - os.makedirs(os.path.dirname(save_path), exist_ok=True) - cv2.imwrite(save_path, annotated_img) - print(f"推理结果已保存至: {save_path}") + try: + rknn = init_rknn(model_path) + if rknn is None: + return None, img + results = rknn.inference([infer_img]) + except Exception as e: + print(f"[ERROR] RKNN 推理失败: {e}") + return None, img - # 5. 提取旋转角度和置信度 - boxes = result.obb - if boxes is None or len(boxes) < 2: - print("检测到少于两个目标,无法计算夹角。") - return None, annotated_img + outputs = [] + for x in results[:-1]: + index, stride = 0, 0 + if x.shape[2] == 20: + stride, index = 32, 20*4*20*4 + 20*2*20*2 + elif x.shape[2] == 40: + stride, index = 16, 20*4*20*4 + elif x.shape[2] == 80: + stride, index = 8, 0 + feature = x.reshape(1, 65, -1) + outputs += process(feature, x.shape[3], x.shape[2], stride, results[-1], index) - box_info = [] - for box in boxes: - conf = box.conf.cpu().numpy()[0] - cx, cy, w, h, r_rad = box.xywhr.cpu().numpy()[0] - direction = r_rad if w >= h else r_rad + np.pi/2 - direction = direction % np.pi - box_info.append((conf, direction)) + predbox = NMS(outputs) + print(f"[DEBUG] 检测到 {len(predbox)} 个框") - # 6. 取置信度最高两个框 - box_info = sorted(box_info, key=lambda x: x[0], reverse=True) - dir1, dir2 = box_info[0][1], box_info[1][1] + if len(predbox) < 2: + print("检测少于两个目标,无法计算夹角。") + return None, img - # 7. 计算夹角(最小夹角,0~90°) + predbox = sorted(predbox, key=lambda x: x.score, reverse=True) + box1, box2 = predbox[:2] + + output_img = img.copy() if DRAW_RESULT else img # 若不绘制,则直接用原图 + + if DRAW_RESULT: + for box in [box1, box2]: + xmin = int((box.xmin - offset_x) / scale) + ymin = int((box.ymin - offset_y) / scale) + xmax = int((box.xmax - offset_x) / scale) + ymax = int((box.ymax - offset_y) / scale) + points = rotate_rectangle(xmin, ymin, xmax, ymax, box.angle) + cv2.polylines(output_img, [np.array(points, np.int32)], True, (0, 255, 0), 2) + + def main_direction(box): + w, h = (box.xmax - box.xmin)/scale, (box.ymax - box.ymin)/scale + direction = box.angle if w >= h else box.angle + np.pi/2 + return direction % np.pi + + dir1 = main_direction(box1) + dir2 = main_direction(box2) diff = abs(dir1 - dir2) diff = min(diff, np.pi - diff) angle_deg = np.degrees(diff) - print(f"置信度最高两个框主方向夹角: {angle_deg:.2f}°") - return angle_deg, annotated_img + # 保存结果(如果需要) + if SAVE_PATH: + save_dir = os.path.dirname(SAVE_PATH) + if save_dir: # 非空目录才创建 + os.makedirs(save_dir, exist_ok=True) + cv2.imwrite(SAVE_PATH, output_img) + return angle_deg, output_img -# ------------------- 测试 ------------------- +# ------------------- 示例调用 ------------------- # if __name__ == "__main__": -# weight_path = r'angle.pt' -# image_path = r"./test_image/3.jpg" -# save_path = "./inference_results/detected_3.jpg" -# -# #angle_deg, annotated_img = predict_obb_best_angle(weight_path, image_path, save_path) -# angle_deg,_ = predict_obb_best_angle(model_path=weight_path, image_path=image_path, save_path=save_path) -# annotated_img = None -# print(angle_deg) -# if annotated_img is not None: -# cv2.imshow("YOLO OBB Prediction", annotated_img) -# cv2.waitKey(0) -# cv2.destroyAllWindows() \ No newline at end of file + # MODEL_PATH = "./obb.rknn" + # IMAGE_PATH = "./11.jpg" + + # # === 全局控制开关 === + # DRAW_RESULT = True # 是否绘制框 + # SAVE_PATH = "./result11.jpg" # 保存路径,设为 None 则不保存 + + # frame = cv2.imread(IMAGE_PATH) + # if frame is None: + # print(f"[ERROR] 无法读取图像: {IMAGE_PATH}") + # else: + # angle_deg, output_image = detect_two_box_angle(MODEL_PATH, frame) + # if angle_deg is not None: + # print(f"检测到的角度差: {angle_deg:.2f}°") + # else: + # print("未能成功检测到目标或计算角度差") \ No newline at end of file diff --git a/vision/anger_caculate_old.py b/vision/anger_caculate_old.py new file mode 100644 index 0000000..af12cb0 --- /dev/null +++ b/vision/anger_caculate_old.py @@ -0,0 +1,88 @@ +import cv2 +import os +import numpy as np +from ultralytics import YOLO + +def predict_obb_best_angle(model=None, model_path=None, image=None, image_path=None, save_path=None): + """ + 输入: + model: 预加载的YOLO模型实例(可选) + model_path: YOLO 权重路径(当model为None时使用) + image: 图像数组(numpy array) + image_path: 图片路径(当image为None时使用) + save_path: 可选,保存带标注图像 + 输出: + angle_deg: 置信度最高两个框的主方向夹角(度),如果检测少于两个目标返回 None + annotated_img: 可视化图像 + """ + # 1. 使用预加载的模型或加载新模型 + if model is not None: + loaded_model = model + elif model_path is not None: + loaded_model = YOLO(model_path) + else: + raise ValueError("必须提供model或model_path参数") + + # 2. 读取图像(优先使用传入的图像数组) + if image is not None: + img = image + elif image_path is not None: + img = cv2.imread(image_path) + if img is None: + print(f"无法读取图像: {image_path}") + return None, None + else: + raise ValueError("必须提供image或image_path参数") + + # 3. 推理 OBB + results = loaded_model(img, save=False, imgsz=640, conf=0.5, mode='obb') + result = results[0] + + # 4. 可视化 + annotated_img = result.plot() + if save_path: + os.makedirs(os.path.dirname(save_path), exist_ok=True) + cv2.imwrite(save_path, annotated_img) + print(f"推理结果已保存至: {save_path}") + + # 5. 提取旋转角度和置信度 + boxes = result.obb + if boxes is None or len(boxes) < 2: + print("检测到少于两个目标,无法计算夹角。") + return None, annotated_img + + box_info = [] + for box in boxes: + conf = box.conf.cpu().numpy()[0] + cx, cy, w, h, r_rad = box.xywhr.cpu().numpy()[0] + direction = r_rad if w >= h else r_rad + np.pi/2 + direction = direction % np.pi + box_info.append((conf, direction)) + + # 6. 取置信度最高两个框 + box_info = sorted(box_info, key=lambda x: x[0], reverse=True) + dir1, dir2 = box_info[0][1], box_info[1][1] + + # 7. 计算夹角(最小夹角,0~90°) + diff = abs(dir1 - dir2) + diff = min(diff, np.pi - diff) + angle_deg = np.degrees(diff) + + print(f"置信度最高两个框主方向夹角: {angle_deg:.2f}°") + return angle_deg, annotated_img + + +# ------------------- 测试 ------------------- +# if __name__ == "__main__": +# weight_path = r'angle.pt' +# image_path = r"./test_image/3.jpg" +# save_path = "./inference_results/detected_3.jpg" +# +# #angle_deg, annotated_img = predict_obb_best_angle(weight_path, image_path, save_path) +# angle_deg,_ = predict_obb_best_angle(model_path=weight_path, image_path=image_path, save_path=save_path) +# annotated_img = None +# print(angle_deg) +# if annotated_img is not None: +# cv2.imshow("YOLO OBB Prediction", annotated_img) +# cv2.waitKey(0) +# cv2.destroyAllWindows() \ No newline at end of file diff --git a/vision/angle_detector.py b/vision/angle_detector.py index bb3e571..9f89989 100644 --- a/vision/angle_detector.py +++ b/vision/angle_detector.py @@ -1,12 +1,12 @@ # vision/angle_detector.py import sys import os -from vision.anger_caculate import predict_obb_best_angle +from vision.obb_angle_model.obb_angle import detect_two_box_angle # 添加项目根目录到Python路径 -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +# sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -def get_current_door_angle(model=None, image=None, image_path=None): +def get_current_door_angle(model,image=None, image_path=None): """ 通过视觉系统获取当前出砼门角度 :param model: 模型实例 @@ -16,10 +16,10 @@ def get_current_door_angle(model=None, image=None, image_path=None): """ try: # 调用实际的角度检测函数 - angle_deg, _ = predict_obb_best_angle( - model=model, - image=image, - image_path=image_path + angle_deg, _ = detect_two_box_angle( + model_path=model, + rgb_frame=image + # ,image_path=image_path ) return angle_deg except Exception as e: diff --git a/vision/camera.py b/vision/camera.py index a26f0e5..540a780 100644 --- a/vision/camera.py +++ b/vision/camera.py @@ -1,67 +1,486 @@ # vision/camera.py import cv2 +import threading +import queue +import time +import numpy as np +from datetime import datetime +from typing import Optional, Tuple, Dict, Any -class CameraController: - def __init__(self): - self.camera = None - self.camera_type = "ip" - self.camera_ip = "192.168.1.51" - self.camera_port = 554 - self.camera_username = "admin" - self.camera_password = "XJ123456" - self.camera_channel = 1 - - def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1): - """ - 设置摄像头配置 - """ - self.camera_type = camera_type - if ip: - self.camera_ip = ip - if port: - self.camera_port = port - if username: - self.camera_username = username - if password: - self.camera_password = password - self.camera_channel = channel - - def setup_capture(self, camera_index=0): - """ - 设置摄像头捕获 - """ - try: - rtsp_url = f"rtsp://{self.camera_username}:{self.camera_password}@{self.camera_ip}:{self.camera_port}/streaming/channels/{self.camera_channel}01" - self.camera = cv2.VideoCapture(rtsp_url) - - if not self.camera.isOpened(): - print(f"无法打开网络摄像头: {rtsp_url}") - return False - print(f"网络摄像头初始化成功,地址: {rtsp_url}") +class DualCameraController: + """双摄像头控制器 - 支持多线程捕获和同步帧获取""" + + def __init__(self, camera_configs: Dict[str, Dict[str, Any]], max_queue_size: int = 10, sync_threshold_ms: float = 50.0): + # 摄像头配置 + self.camera_configs = camera_configs + + # 摄像头对象和队列 + self.cameras: Dict[str, cv2.VideoCapture] = {} + self.frame_queues: Dict[str, queue.Queue] = {} + self.capture_threads: Dict[str, threading.Thread] = {} + + # 线程控制 + self.stop_event = threading.Event() + self.max_queue_size = max_queue_size + self.sync_threshold_ms = sync_threshold_ms + self.last_sync_pair: Tuple[Optional[np.ndarray], Optional[np.ndarray]] = (None, None) + + # 摄像头状态 + self.is_running = False + + def set_camera_config(self, camera_id: str, ip: str, username: str = "admin", + password: str = "XJ123456", port: int = 554, channel: int = 1): + """设置指定摄像头的配置""" + if camera_id in ['cam1', 'cam2']: + self.camera_configs[camera_id].update({ + 'ip': ip, + 'username': username, + 'password': password, + 'port': port, + 'channel': channel + }) + print(f"摄像头 {camera_id} 配置已更新: IP={ip}") + else: + raise ValueError(f"无效的摄像头ID: {camera_id}") + + def _build_rtsp_url(self, camera_id: str) -> str: + """构建RTSP URL""" + config = self.camera_configs[camera_id] + return f"rtsp://{config['username']}:{config['password']}@{config['ip']}:{config['port']}/Streaming/Channels/{config['channel']}01" + + def _capture_thread(self, camera_id: str): + """摄像头捕获线程""" + cap = self.cameras[camera_id] + q = self.frame_queues[camera_id] + rtsp_url = self._build_rtsp_url(camera_id) + + print(f"启动 {camera_id} 捕获线程") + + while not self.stop_event.is_set(): + try: + # print('aaaaa') + ret, frame = cap.read() + if ret and frame is not None: + # 使用高精度时间戳 + timestamp = time.time() + # 检查队列是否已满 + if q.qsize() >= self.max_queue_size: + # 队列已满,丢弃最旧帧(FIFO) + try: + q.get_nowait() # 移除最旧帧 + q.put_nowait((timestamp, frame)) + except queue.Empty: + # 理论上不会发生,但安全处理 + pass + else: + # 队列未满,直接添加 + q.put_nowait((timestamp, frame)) + else: + print(f"{camera_id} 读取失败,重连中...") + time.sleep(1) + cap.open(rtsp_url) + + except Exception as e: + print(f"{camera_id} 捕获异常: {e}") + time.sleep(1) + + print(f"{camera_id} 捕获线程已停止") + + def start_cameras(self) -> bool: + """启动双摄像头""" + if self.is_running: + print("摄像头已在运行中") return True - except Exception as e: - print(f"摄像头设置失败: {e}") - return False - - def capture_frame(self): - """捕获当前帧并返回numpy数组""" + try: - if self.camera is None: - print("摄像头未初始化") - return None - - ret, frame = self.camera.read() - if ret: - return frame - else: - print("无法捕获图像帧") - return None + # 初始化摄像头和队列 + for camera_id in ['cam1', 'cam2']: + rtsp_url = self._build_rtsp_url(camera_id) + cap = cv2.VideoCapture(rtsp_url) + + if not cap.isOpened(): + print(f"无法打开摄像头 {camera_id}: {rtsp_url}") + # 清理已打开的摄像头 + self.release() + return False + + self.cameras[camera_id] = cap + self.frame_queues[camera_id] = queue.Queue(maxsize=self.max_queue_size) + print(f"摄像头 {camera_id} 初始化成功: {rtsp_url}") + + # 启动捕获线程 + self.stop_event.clear() + for camera_id in ['cam1', 'cam2']: + thread = threading.Thread( + target=self._capture_thread, + args=(camera_id,), + daemon=True + ) + self.capture_threads[camera_id] = thread + thread.start() + + self.is_running = True + print("双摄像头系统启动成功") + return True + except Exception as e: - print(f"图像捕获失败: {e}") + print(f"启动摄像头失败: {e}") + self.release() + return False + + def get_latest_frames(self, sync_threshold_ms: Optional[float] = None) -> Optional[Tuple[np.ndarray, np.ndarray]]: + """获取最新的同步帧对""" + if not self.is_running: + print("摄像头未运行") + return None + + sync_threshold = sync_threshold_ms or self.sync_threshold_ms + sync_threshold_sec = sync_threshold / 1000.0 + + # 检查队列是否有数据 + if (self.frame_queues['cam1'].empty() or + self.frame_queues['cam2'].empty()): + return None + + try: + # 获取最新帧 + ts1, f1 = self.frame_queues['cam1'].queue[-1] + ts2, f2 = self.frame_queues['cam2'].queue[-1] + + dt = abs(ts1 - ts2) + + if dt < sync_threshold_sec: + # 时间差在阈值内,认为是同步的 + frame1, frame2 = f1.copy(), f2.copy() + self.last_sync_pair = (frame1, frame2) + return (frame1, frame2) + else: + # 搜索最近5帧找最小时间差 + min_dt = float('inf') + best_pair = None + + # 获取最近5帧 + cam1_frames = list(self.frame_queues['cam1'].queue)[-5:] + cam2_frames = list(self.frame_queues['cam2'].queue)[-5:] + + for t1_local, f1_local in cam1_frames: + for t2_local, f2_local in cam2_frames: + dt_local = abs(t1_local - t2_local) + if dt_local < min_dt and dt_local < sync_threshold_sec * 2: # 更宽松的阈值 + min_dt = dt_local + best_pair = (f1_local.copy(), f2_local.copy()) + + if best_pair: + self.last_sync_pair = best_pair + return best_pair + else: + # 没找到同步帧,返回最新非同步帧 + return (f1.copy(), f2.copy()) + + except Exception as e: + print(f"获取帧对失败: {e}") + return None + + def get_single_frame(self, camera_id: str) -> Optional[np.ndarray]: + """获取单个摄像头的最新帧""" + if not self.is_running: + print("摄像头未运行") + return None + + if camera_id not in self.frame_queues: + print(f"无效的摄像头ID: {camera_id}") + return None + + try: + if not self.frame_queues[camera_id].empty(): + _, frame = self.frame_queues[camera_id].queue[-1] + return frame.copy() + return None + except Exception as e: + print(f"获取单帧失败: {e}") return None + def get_single_latest_frame(self) -> Optional[np.ndarray]: + """获取单个摄像头的最新帧""" + if not self.is_running: + print("摄像头未运行") + return None + + try: + frame_latest = None + dt_t1 = None + + # 获取cam1的最新帧 + if not self.frame_queues['cam1'].empty(): + dt_t1, frame_latest = self.frame_queues['cam1'].queue[-1] + + # 获取cam2的最新帧,选择时间戳更新的那个 + if frame_latest is None: + if not self.frame_queues['cam2'].empty(): + dt_t2, frame2 = self.frame_queues['cam2'].queue[-1] + if dt_t1 is None or dt_t2 > dt_t1: + frame_latest = frame2 + + # 返回最新帧的副本(如果找到) + return frame_latest.copy() if frame_latest is not None else None + + except Exception as e: + print(f"获取单帧失败: {e}") + return None + + def get_single_latest_frame2(self) -> Optional[np.ndarray]: + """获取单个摄像头的最新帧""" + if not self.is_running: + print("摄像头未运行") + return None + + try: + frame_latest = None + dt_t1 = None + + # 获取cam1的最新帧 + if not self.frame_queues['cam2'].empty(): + dt_t1, frame_latest = self.frame_queues['cam2'].queue[-1] + + # 获取cam2的最新帧,选择时间戳更新的那个 + if frame_latest is None: + if not self.frame_queues['cam1'].empty(): + dt_t2, frame2 = self.frame_queues['cam1'].queue[-1] + if dt_t1 is None or dt_t2 > dt_t1: + frame_latest = frame2 + + # 返回最新帧的副本(如果找到) + return frame_latest.copy() if frame_latest is not None else None + + except Exception as e: + print(f"获取单帧失败: {e}") + return None + + def get_notification_frame(self, camera_id: str = None, use_sync: bool = True) -> Optional[np.ndarray]: + """根据通知参数获取最近的帧 + + Args: + camera_id: 摄像头ID ('cam1', 'cam2'),如果为None则根据use_sync决定 + use_sync: 是否使用同步帧对,如果为True则返回同步帧对,否则返回指定摄像头的单帧 + + Returns: + 单帧图像或同步帧对 + """ + if not self.is_running: + print("摄像头未运行") + return None + + if use_sync: + # 获取同步帧对,返回拼接后的图像 + frames = self.get_latest_frames() + if frames: + frame1, frame2 = frames + # 调整大小并拼接 + h, w = 480, 640 + frame1_resized = cv2.resize(frame1, (w, h)) + frame2_resized = cv2.resize(frame2, (w, h)) + combined = np.hstack((frame1_resized, frame2_resized)) + + # 添加时间戳信息 + ts1 = time.time() + cv2.putText(combined, f"Sync: {datetime.fromtimestamp(ts1).strftime('%H:%M:%S.%f')[:-3]}", + (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + return combined + return None + else: + # 获取指定摄像头的单帧 + if camera_id is None: + camera_id = 'cam1' # 默认返回cam1 + return self.get_single_frame(camera_id) + + def display_live_feed(self): + """实时显示双摄像头画面(调试用)""" + if not self.is_running: + print("请先启动摄像头") + return + + print("按 'q' 退出显示,按 's' 保存同步帧") + + while True: + frame = self.get_notification_frame(use_sync=True) + if frame is not None: + cv2.imshow("Dual Camera Feed", frame) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('s'): + # 保存同步帧 + sync_frames = self.get_latest_frames() + if sync_frames: + frame1, frame2 = sync_frames + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:20] + cv2.imwrite(f"cam1_{timestamp}.jpg", frame1) + cv2.imwrite(f"cam2_{timestamp}.jpg", frame2) + print(f"✅ 保存同步帧: cam1_{timestamp}.jpg & cam2_{timestamp}.jpg") + + cv2.destroyAllWindows() + def release(self): """释放摄像头资源""" - if self.camera is not None: - self.camera.release() + print("正在释放摄像头资源...") + + # 停止捕获线程 + if self.is_running: + self.stop_event.set() + # 等待线程结束 + for camera_id, thread in self.capture_threads.items(): + if thread.is_alive(): + thread.join(timeout=2) + print(f"{camera_id} 捕获线程已停止") + + self.capture_threads.clear() + self.is_running = False + + # 释放摄像头 + for camera_id, cap in self.cameras.items(): + if cap is not None: + cap.release() + print(f"摄像头 {camera_id} 已释放") + + self.cameras.clear() + self.frame_queues.clear() + print("摄像头资源释放完成") + + def __del__(self): + """析构函数,确保资源释放""" + self.release() + + # 类方法:快速创建和启动 + @classmethod + def create_and_start(cls, camera_configs: Dict[str, Dict[str, Any]]) -> Optional['DualCameraController']: + """快速创建并启动双摄像头控制器""" + controller = cls(camera_configs) + if controller.start_cameras(): + return controller + else: + return None + + +# 向后兼容的单摄像头控制器 +class CameraController: + """单摄像头控制器 - 向后兼容""" + + def __init__(self): + self.dual_controller = DualCameraController() + self.default_camera = 'cam1' + + def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1): + """设置摄像头配置 - 兼容旧接口""" + self.dual_controller.set_camera_config( + 'cam1', ip or "192.168.1.51", username or "admin", + password or "XJ123456", port or 554, channel + ) + + def setup_capture(self, camera_index=0): + """设置摄像头捕获 - 兼容旧接口""" + return self.dual_controller.start_cameras() + + def capture_frame(self): + """捕获当前帧 - 兼容旧接口""" + return self.dual_controller.capture_frame(self.default_camera) + + def capture_frame_bak(self): + """捕获当前帧(备用) - 兼容旧接口""" + return self.dual_controller.capture_frame_bak(self.default_camera) + + def release(self): + """释放摄像头资源""" + self.dual_controller.release() + + def __del__(self): + """析构函数""" + self.release() + + +# 使用示例和测试 +if __name__ == "__main__": + # 创建双摄像头控制器 + camera_configs = { + 'cam1': { + 'type': 'ip', + 'ip': '192.168.250.60', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + }, + 'cam2': { + 'type': 'ip', + 'ip': '192.168.250.61', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + } + } + controller = DualCameraController.create_and_start(camera_configs) + + if controller: + print("双摄像头系统启动成功!") + + # 示例1:获取同步帧对 + print("\n=== 获取同步帧 ===") + while True: + single_frame = controller.get_single_latest_frame() + + if single_frame is not None: + print(f"获取到帧形状: {single_frame.shape}") + cv2.imshow("Single Camera Frame", single_frame) + else: + print("未获取到帧") + key = cv2.waitKey(1) & 0xFF + if key == ord('s') and single_frame is not None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:20] + cv2.imwrite(f"single_frame_{timestamp}.jpg", single_frame) + print(f"✅ 保存单帧: single_frame_{timestamp}.jpg") + if key == ord('q'): + break + time.sleep(1) + + # controller.get_single_latest_frame('cam2') + # sync_frames = controller.get_latest_frames(sync_threshold_ms=50) + # if sync_frames: + # frame1, frame2 = sync_frames + # print(f"获取到同步帧对 - 帧1形状: {frame1.shape}, 帧2形状: {frame2.shape}") + # else: + # print("未获取到同步帧对") + + # 示例2:根据通知参数获取帧 + # print("\n=== 根据通知参数获取帧 ===") + + # # 获取同步拼接帧(用于显示) + # combined_frame = controller.get_notification_frame(use_sync=True) + # if combined_frame is not None: + # print(f"获取到同步拼接帧,形状: {combined_frame.shape}") + # cv2.imshow("Sync Frame", combined_frame) + # cv2.waitKey(1000) # 显示1秒 + # cv2.destroyAllWindows() + + # # 获取单个摄像头帧 + # single_frame = controller.get_notification_frame(camera_id='cam1', use_sync=False) + # if single_frame is not None: + # print(f"获取到cam1单帧,形状: {single_frame.shape}") + + # # 示例3:实时显示 + # print("\n=== 实时显示模式 ===") + # print("按 'q' 退出显示,按 's' 保存同步帧") + # # controller.display_live_feed() # 取消注释以启用实时显示 + + # # 示例4:兼容性测试 + # print("\n=== 兼容性测试 ===") + # old_frame = controller.capture_frame('cam1') + # if old_frame is not None: + # print(f"旧接口兼容 - 帧形状: {old_frame.shape}") + + # 清理 + controller.release() + print("\n摄像头资源已释放") + else: + print("双摄像头系统启动失败!") diff --git a/vision/detector.py b/vision/detector.py index 2f982c8..d1568f0 100644 --- a/vision/detector.py +++ b/vision/detector.py @@ -1,6 +1,6 @@ # vision/detector.py import os -from ultralytics import YOLO +import cv2 from vision.angle_detector import get_current_door_angle from vision.overflow_detector import detect_overflow_from_image from vision.alignment_detector import detect_vehicle_alignment @@ -9,16 +9,14 @@ from vision.alignment_detector import detect_vehicle_alignment class VisionDetector: def __init__(self, settings): self.settings = settings - - # 模型实例 - self.angle_model = None - self.overflow_model = None - self.alignment_model = None - + #model路径在对应的模型里面 + # self.alignment_model = os.path.join(current_dir, "align_model/yolov11_cls_640v6.rknn") + def load_models(self): """ 加载所有视觉检测模型 """ + from ultralytics import YOLO success = True # 加载夹角检测模型 @@ -59,28 +57,44 @@ class VisionDetector: return success - def detect_angle(self, image=None, image_path=None): + def detect_angle(self, image=None): """ 通过视觉系统获取当前出砼门角度 """ + # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + image=cv2.flip(image, 0) return get_current_door_angle( - model=self.angle_model, - image=image, - image_path=image_path + model=self.settings.angle_model_path, + image=image ) def detect_overflow(self, image_array): """ 通过图像检测是否溢料 """ + # image_array=cv2.flip(image_array, 0) + # cv2.imwrite('test.jpg', image_array) + cv2.namedWindow("Alignment", cv2.WINDOW_NORMAL) + cv2.resizeWindow("Alignment", 640, 480) + cv2.imshow("Alignment", image_array) + cv2.waitKey(1) + print('path:', self.settings.overflow_model_path) + print('roi:', self.settings.roi_file_path) return detect_overflow_from_image( - image_array, - self.overflow_model, - self.settings.roi_file_path + self.settings.overflow_model_path, + self.settings.roi_file_path, + image_array ) def detect_vehicle_alignment(self, image_array): """ 通过图像检测模具车是否对齐 """ - return detect_vehicle_alignment(image_array, self.alignment_model) + image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB) + image_array=cv2.flip(image_array, 0) + # cv2.namedWindow("Alignment", cv2.WINDOW_NORMAL) + # cv2.resizeWindow("Alignment", 640, 480) + # cv2.imshow("Alignment", image_array) + # cv2.waitKey(1) + + return detect_vehicle_alignment(image_array) diff --git a/vision/obb_angle_model/obb.rknn b/vision/obb_angle_model/obb.rknn new file mode 100644 index 0000000..746872b Binary files /dev/null and b/vision/obb_angle_model/obb.rknn differ diff --git a/vision/obb_angle_model/obb_angle.py b/vision/obb_angle_model/obb_angle.py new file mode 100644 index 0000000..9b3dd46 --- /dev/null +++ b/vision/obb_angle_model/obb_angle.py @@ -0,0 +1,236 @@ +import cv2 +import numpy as np +import math +from shapely.geometry import Polygon +import os + +# ------------------- 全局配置变量 ------------------- +# 模型相关 +CLASSES = ['clamp'] +nmsThresh = 0.4 +objectThresh = 0.35 + +# 可视化与保存控制(全局变量,可外部修改) +DRAW_RESULT = True # 是否在输出图像上绘制旋转框 +SAVE_PATH = None # 保存路径,如 "./result.jpg";设为 None 则不保存 + +# RKNN 单例 +_rknn_instance = None + +# ------------------- RKNN 管理函数 ------------------- +def init_rknn(model_path): + from rknnlite.api import RKNNLite + + """只加载一次 RKNN 模型""" + global _rknn_instance + if _rknn_instance is None: + _rknn_instance = RKNNLite(verbose=False) + ret = _rknn_instance.load_rknn(model_path) + if ret != 0: + print(f"[ERROR] Failed to load RKNN model: {ret}") + return None + ret = _rknn_instance.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + print(f"[ERROR] Failed to init runtime: {ret}") + return None + return _rknn_instance + +def release_rknn(): + """释放 RKNN 对象""" + global _rknn_instance + if _rknn_instance: + _rknn_instance.release() + _rknn_instance = None + +# ------------------- 工具函数 ------------------- +def letterbox_resize(image, size, bg_color=114): + target_width, target_height = size + image_height, image_width, _ = image.shape + scale = min(target_width / image_width, target_height / image_height) + new_width, new_height = int(image_width * scale), int(image_height * scale) + image_resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) + canvas = np.ones((target_height, target_width, 3), dtype=np.uint8) * bg_color + offset_x, offset_y = (target_width - new_width) // 2, (target_height - new_height) // 2 + canvas[offset_y:offset_y + new_height, offset_x:offset_x + new_width] = image_resized + return canvas, scale, offset_x, offset_y + +class DetectBox: + def __init__(self, classId, score, xmin, ymin, xmax, ymax, angle): + self.classId = classId + self.score = score + self.xmin = xmin + self.ymin = ymin + self.xmax = xmax + self.ymax = ymax + self.angle = angle + +def rotate_rectangle(x1, y1, x2, y2, a): + cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + cos_a, sin_a = math.cos(a), math.sin(a) + pts = [(x1, y1), (x1, y2), (x2, y2), (x2, y1)] + return [[int(cx + (xx - cx) * cos_a - (yy - cy) * sin_a), + int(cy + (xx - cx) * sin_a + (yy - cy) * cos_a)] for xx, yy in pts] + +def intersection(g, p): + g = Polygon(np.array(g).reshape(-1, 2)) + p = Polygon(np.array(p).reshape(-1, 2)) + if not g.is_valid or not p.is_valid: + return 0 + inter = g.intersection(p).area + union = g.area + p.area - inter + return 0 if union == 0 else inter / union + +def NMS(detectResult): + predBoxs = [] + sort_detectboxs = sorted(detectResult, key=lambda x: x.score, reverse=True) + for i in range(len(sort_detectboxs)): + if sort_detectboxs[i].classId == -1: + continue + p1 = rotate_rectangle(sort_detectboxs[i].xmin, sort_detectboxs[i].ymin, + sort_detectboxs[i].xmax, sort_detectboxs[i].ymax, + sort_detectboxs[i].angle) + predBoxs.append(sort_detectboxs[i]) + for j in range(i + 1, len(sort_detectboxs)): + if sort_detectboxs[j].classId == sort_detectboxs[i].classId: + p2 = rotate_rectangle(sort_detectboxs[j].xmin, sort_detectboxs[j].ymin, + sort_detectboxs[j].xmax, sort_detectboxs[j].ymax, + sort_detectboxs[j].angle) + if intersection(p1, p2) > nmsThresh: + sort_detectboxs[j].classId = -1 + return predBoxs + +def sigmoid(x): + x = np.clip(x, -709, 709) # 防止 exp 溢出 + return np.where(x >= 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x))) + +def softmax(x, axis=-1): + exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True)) + return exp_x / np.sum(exp_x, axis=axis, keepdims=True) + +def process(out, model_w, model_h, stride, angle_feature, index, scale_w=1, scale_h=1): + class_num = len(CLASSES) + angle_feature = angle_feature.reshape(-1) + xywh = out[:, :64, :] + conf = sigmoid(out[:, 64:, :]) + conf = conf.reshape(-1) + boxes = [] + for ik in range(model_h * model_w * class_num): + if conf[ik] > objectThresh: + w = ik % model_w + h = (ik % (model_w * model_h)) // model_w + c = ik // (model_w * model_h) + xywh_ = xywh[0, :, (h * model_w) + w].reshape(1, 4, 16, 1) + data = np.arange(16).reshape(1, 1, 16, 1) + xywh_ = softmax(xywh_, 2) + xywh_ = np.sum(xywh_ * data, axis=2).reshape(-1) + xywh_add = xywh_[:2] + xywh_[2:] + xywh_sub = (xywh_[2:] - xywh_[:2]) / 2 + angle = (angle_feature[index + (h * model_w) + w] - 0.25) * math.pi + cos_a, sin_a = math.cos(angle), math.sin(angle) + xy = xywh_sub[0] * cos_a - xywh_sub[1] * sin_a, xywh_sub[0] * sin_a + xywh_sub[1] * cos_a + xywh1 = np.array([xy[0] + w + 0.5, xy[1] + h + 0.5, xywh_add[0], xywh_add[1]]) + xywh1 *= stride + xmin = (xywh1[0] - xywh1[2] / 2) * scale_w + ymin = (xywh1[1] - xywh1[3] / 2) * scale_h + xmax = (xywh1[0] + xywh1[2] / 2) * scale_w + ymax = (xywh1[1] + xywh1[3] / 2) * scale_h + boxes.append(DetectBox(c, conf[ik], xmin, ymin, xmax, ymax, angle)) + return boxes + +# ------------------- 主推理函数 ------------------- +def detect_two_box_angle(model_path, rgb_frame): + """ + 输入模型路径和 RGB 图像(numpy array),输出夹角和结果图像。 + 可视化和保存由全局变量 DRAW_RESULT 和 SAVE_PATH 控制。 + """ + global _rknn_instance, DRAW_RESULT, SAVE_PATH + + if not isinstance(rgb_frame, np.ndarray) or rgb_frame is None: + print(f"[ERROR] detect_two_box_angle 接收到错误类型: {type(rgb_frame)}") + return None, np.zeros((640, 640, 3), np.uint8) + + # 注意:输入是 BGR(因为 cv2.imread 返回 BGR),但内部会转为 RGB 给模型 + img = rgb_frame.copy() + img_resized, scale, offset_x, offset_y = letterbox_resize(img, (640, 640)) + infer_img = np.expand_dims(cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB), 0) + + try: + rknn = init_rknn(model_path) + if rknn is None: + return None, img + results = rknn.inference([infer_img]) + except Exception as e: + print(f"[ERROR] RKNN 推理失败: {e}") + return None, img + + outputs = [] + for x in results[:-1]: + index, stride = 0, 0 + if x.shape[2] == 20: + stride, index = 32, 20*4*20*4 + 20*2*20*2 + elif x.shape[2] == 40: + stride, index = 16, 20*4*20*4 + elif x.shape[2] == 80: + stride, index = 8, 0 + feature = x.reshape(1, 65, -1) + outputs += process(feature, x.shape[3], x.shape[2], stride, results[-1], index) + + predbox = NMS(outputs) + print(f"[DEBUG] 检测到 {len(predbox)} 个框") + + if len(predbox) < 2: + print("检测少于两个目标,无法计算夹角。") + return None, img + + predbox = sorted(predbox, key=lambda x: x.score, reverse=True) + box1, box2 = predbox[:2] + + output_img = img.copy() if DRAW_RESULT else img # 若不绘制,则直接用原图 + + if DRAW_RESULT: + for box in [box1, box2]: + xmin = int((box.xmin - offset_x) / scale) + ymin = int((box.ymin - offset_y) / scale) + xmax = int((box.xmax - offset_x) / scale) + ymax = int((box.ymax - offset_y) / scale) + points = rotate_rectangle(xmin, ymin, xmax, ymax, box.angle) + cv2.polylines(output_img, [np.array(points, np.int32)], True, (0, 255, 0), 2) + + def main_direction(box): + w, h = (box.xmax - box.xmin)/scale, (box.ymax - box.ymin)/scale + direction = box.angle if w >= h else box.angle + np.pi/2 + return direction % np.pi + + dir1 = main_direction(box1) + dir2 = main_direction(box2) + diff = abs(dir1 - dir2) + diff = min(diff, np.pi - diff) + angle_deg = np.degrees(diff) + + # 保存结果(如果需要) + if SAVE_PATH: + save_dir = os.path.dirname(SAVE_PATH) + if save_dir: # 非空目录才创建 + os.makedirs(save_dir, exist_ok=True) + cv2.imwrite(SAVE_PATH, output_img) + + return angle_deg, output_img + +# ------------------- 示例调用 ------------------- +if __name__ == "__main__": + MODEL_PATH = "./obb.rknn" + IMAGE_PATH = "./11.jpg" + + # # === 全局控制开关 === + # DRAW_RESULT = True # 是否绘制框 + # SAVE_PATH = "./result11.jpg" # 保存路径,设为 None 则不保存 + + # frame = cv2.imread(IMAGE_PATH) + # if frame is None: + # print(f"[ERROR] 无法读取图像: {IMAGE_PATH}") + # else: + # angle_deg, output_image = detect_two_box_angle(MODEL_PATH, frame) + # if angle_deg is not None: + # print(f"检测到的角度差: {angle_deg:.2f}°") + # else: + # print("未能成功检测到目标或计算角度差") \ No newline at end of file diff --git a/vision/overflow_detector.py b/vision/overflow_detector.py index 359e8be..2b257a9 100644 --- a/vision/overflow_detector.py +++ b/vision/overflow_detector.py @@ -1,47 +1,33 @@ # vision/overflow_detector.py import sys import os -from vision.resize_tuili_image_main import classify_image_weighted, load_global_rois, crop_and_resize +from typing import Optional +from vision.overflow_model.yiliao_main_rknn import classify_frame_with_rois # 添加项目根目录到Python路径 -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +# sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -def detect_overflow_from_image(image_array, overflow_model, roi_file_path): +def detect_overflow_from_image(overflow_model,roi_file_path,image_array)->Optional[str]: """ 通过图像检测是否溢料 :param image_array: 图像数组 :param overflow_model: 溢料检测模型 :param roi_file_path: ROI文件路径 - :return: 是否检测到溢料 (True/False) + :return: 检测到的溢料类别 (未堆料、小堆料、大堆料、未浇筑满、浇筑满) 或 None """ try: - # 检查模型是否已加载 - if overflow_model is None: - print("堆料检测模型未加载") - return False + outputs = classify_frame_with_rois(overflow_model, image_array, roi_file_path) + print("溢料检测结果:", outputs) + for res in outputs: - # 加载ROI区域 - rois = load_global_rois(roi_file_path) - - if not rois: - print(f"没有有效的ROI配置: {roi_file_path}") - return False - - if image_array is None: - print("输入图像为空") - return False - - # 裁剪和调整图像大小 - crops = crop_and_resize(image_array, rois, 640) - - # 对每个ROI区域进行分类检测 - for roi_resized, _ in crops: - final_class, _, _, _ = classify_image_weighted(roi_resized, overflow_model, threshold=0.4) - if "大堆料" in final_class or "小堆料" in final_class: - print(f"检测到溢料: {final_class}") - return True - - return False + return res["class"] + # if "大堆料" in res["class"] or "小堆料" in res["class"]: + # print(f"检测到溢料: {res['class']}") + # return True + + # return False + return None except Exception as e: print(f"溢料检测失败: {e}") - return False + return None + diff --git a/vision/overflow_detector_old.py b/vision/overflow_detector_old.py new file mode 100644 index 0000000..359e8be --- /dev/null +++ b/vision/overflow_detector_old.py @@ -0,0 +1,47 @@ +# vision/overflow_detector.py +import sys +import os +from vision.resize_tuili_image_main import classify_image_weighted, load_global_rois, crop_and_resize + +# 添加项目根目录到Python路径 +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def detect_overflow_from_image(image_array, overflow_model, roi_file_path): + """ + 通过图像检测是否溢料 + :param image_array: 图像数组 + :param overflow_model: 溢料检测模型 + :param roi_file_path: ROI文件路径 + :return: 是否检测到溢料 (True/False) + """ + try: + # 检查模型是否已加载 + if overflow_model is None: + print("堆料检测模型未加载") + return False + + # 加载ROI区域 + rois = load_global_rois(roi_file_path) + + if not rois: + print(f"没有有效的ROI配置: {roi_file_path}") + return False + + if image_array is None: + print("输入图像为空") + return False + + # 裁剪和调整图像大小 + crops = crop_and_resize(image_array, rois, 640) + + # 对每个ROI区域进行分类检测 + for roi_resized, _ in crops: + final_class, _, _, _ = classify_image_weighted(roi_resized, overflow_model, threshold=0.4) + if "大堆料" in final_class or "小堆料" in final_class: + print(f"检测到溢料: {final_class}") + return True + + return False + except Exception as e: + print(f"溢料检测失败: {e}") + return False diff --git a/vision/overflow_model/README.md b/vision/overflow_model/README.md new file mode 100644 index 0000000..269efdf --- /dev/null +++ b/vision/overflow_model/README.md @@ -0,0 +1,78 @@ +# RKNN 堆料分类推理系统 README + +本项目用于在 RK3588 平台上运行 RKNN 分类模型,对多个 ROI 区域进行堆料状态分类,包括: + +未堆料 0 +小堆料 1 +大堆料 2 +未浇筑满 3 +浇筑满 4 + +项目中支持 多 ROI 裁剪、模型推理、加权判断(小/大堆料) 和分类结果输出。 + +## 目录结构 + +project/ +│── yiliao_cls.rknn # RKNN 模型 +│── best.pt # pt 模型 +│── roi_coordinates/ # ROI 坐标文件目录 +│ └── 1_rois.txt +│── test_image/ # 测试图片目录 +│ └── 1.jpg + └── 2.jpg + └── 3.jpg +│── yiliao_main_rknn.py # RKNN主推理脚本 +│── yiliao_main_pc.py # PC推理脚本 +│── README.md + + +## 配置(略) +## 安装依赖(略) + + +## 调用示例 +单张图片推理调用示例 + +```bash + +from yiliao_main_rknn import classify_frame_with_rois + +if __name__ == "__main__": + model_path = "yiliao_cls.rknn" + roi_file = "./roi_coordinates/1_rois.txt" + + frame = cv2.imread("./test_image/2.jpg") + + outputs = classify_frame_with_rois(model_path, frame, roi_file) + + for res in outputs: + print(res) + + +``` + +##小堆料 / 大堆料加权判定说明 + +模型原始输出中,小堆料(class 1)与大堆料(class 2)相比时容易出现概率接近的情况。 + +通过加权机制: + +✔ 可以避免因整体概率偏低导致分类不稳定 +✔ 优先放大“大堆料 的可能性”(因为 w2 > w1) +✔ score 更能反映堆料大小的趋势,而不是绝对概率 + +为提高判断稳定性,采用了加权评分方式:(这些参数都可以根据实际情况在文件中对weighted_small_large中参数进行修改) +score = (0.3 * p1 + 0.7 * p2) / (p1 + p2) +score ≥ 0.4 → 大堆料 +score < 0.4 → 小堆料 + +p1:小堆料概率 +p2:大堆料概率 +score 越接近 1 越倾向于大堆料 +score 越接近 0 越倾向于小堆料 + + + + + + diff --git a/vision/overflow_model/best.pt b/vision/overflow_model/best.pt new file mode 100644 index 0000000..3fd6d73 Binary files /dev/null and b/vision/overflow_model/best.pt differ diff --git a/vision/overflow_model/roi_coordinates/1_rois.txt b/vision/overflow_model/roi_coordinates/1_rois.txt new file mode 100644 index 0000000..bb8f71d --- /dev/null +++ b/vision/overflow_model/roi_coordinates/1_rois.txt @@ -0,0 +1 @@ +859,810,696,328 diff --git a/vision/overflow_model/test_image/1.jpg b/vision/overflow_model/test_image/1.jpg new file mode 100644 index 0000000..2882cd5 Binary files /dev/null and b/vision/overflow_model/test_image/1.jpg differ diff --git a/vision/overflow_model/test_image/2.jpg b/vision/overflow_model/test_image/2.jpg new file mode 100644 index 0000000..dcd5369 Binary files /dev/null and b/vision/overflow_model/test_image/2.jpg differ diff --git a/vision/overflow_model/test_image/3.jpg b/vision/overflow_model/test_image/3.jpg new file mode 100644 index 0000000..8df7feb Binary files /dev/null and b/vision/overflow_model/test_image/3.jpg differ diff --git a/vision/overflow_model/yiliao_cls.rknn b/vision/overflow_model/yiliao_cls.rknn new file mode 100644 index 0000000..ebd4825 Binary files /dev/null and b/vision/overflow_model/yiliao_cls.rknn differ diff --git a/vision/overflow_model/yiliao_main_pc.py b/vision/overflow_model/yiliao_main_pc.py new file mode 100644 index 0000000..539ff30 --- /dev/null +++ b/vision/overflow_model/yiliao_main_pc.py @@ -0,0 +1,168 @@ +import os +from pathlib import Path +import cv2 +import numpy as np +from ultralytics import YOLO + +# --------------------------- +# 类别映射 +# --------------------------- +CLASS_NAMES = { + 0: "未堆料", + 1: "小堆料", + 2: "大堆料", + 3: "未浇筑满", + 4: "浇筑满" +} + +# --------------------------- +# 加载 ROI 列表 +# --------------------------- +def load_global_rois(txt_path): + rois = [] + if not os.path.exists(txt_path): + print(f"❌ ROI 文件不存在: {txt_path}") + return rois + with open(txt_path, 'r') as f: + for line in f: + s = line.strip() + if s: + try: + x, y, w, h = map(int, s.split(',')) + rois.append((x, y, w, h)) + except Exception as e: + print(f"无法解析 ROI 行 '{s}': {e}") + return rois + +# --------------------------- +# 裁剪并 resize ROI +# --------------------------- +def crop_and_resize(img, rois, target_size=640): + crops = [] + h_img, w_img = img.shape[:2] + for i, (x, y, w, h) in enumerate(rois): + if x < 0 or y < 0 or x + w > w_img or y + h > h_img: + continue + roi = img[y:y+h, x:x+w] + roi_resized = cv2.resize(roi, (target_size, target_size), interpolation=cv2.INTER_AREA) + crops.append((roi_resized, i)) + return crops + +# --------------------------- +# class1/class2 加权判断 +# --------------------------- +def weighted_small_large(pred_probs, threshold=0.4, w1=0.3, w2=0.7): + p1 = float(pred_probs[1]) + p2 = float(pred_probs[2]) + total = p1 + p2 + if total > 0: + score = (w1 * p1 + w2 * p2) / total + else: + score = 0.0 + final_class = "大堆料" if score >= threshold else "小堆料" + return final_class, score, p1, p2 + +# --------------------------- +# 单张图片推理函数 +# --------------------------- +def classify_image_weighted(image, model, threshold=0.4): + results = model(image) + pred_probs = results[0].probs.data.cpu().numpy().flatten() + class_id = int(pred_probs.argmax()) + confidence = float(pred_probs[class_id]) + class_name = CLASS_NAMES.get(class_id, f"未知类别({class_id})") + + # class1/class2 使用加权得分 + if class_id in [1, 2]: + final_class, score, p1, p2 = weighted_small_large(pred_probs, threshold=threshold) + else: + final_class = class_name + score = confidence + p1 = float(pred_probs[1]) + p2 = float(pred_probs[2]) + + return final_class, score, p1, p2 + +# --------------------------- +# 批量推理主函数 +# --------------------------- +def batch_classify_images(model_path, input_folder, output_root, roi_file, target_size=640, threshold=0.5): + # 加载模型 + model = YOLO(model_path) + + # 确保输出根目录存在 + output_root = Path(output_root) + output_root.mkdir(parents=True, exist_ok=True) + + # 为所有类别创建目录 + class_dirs = {} + for name in CLASS_NAMES.values(): + d = output_root / name + d.mkdir(exist_ok=True) + class_dirs[name] = d + + rois = load_global_rois(roi_file) + if not rois: + print("❌ 没有有效 ROI,退出") + return + + # 遍历图片 + for img_path in Path(input_folder).glob("*.*"): + if img_path.suffix.lower() not in ['.jpg', '.jpeg', '.png', '.bmp', '.tif']: + continue + try: + img = cv2.imread(str(img_path)) + if img is None: + continue + + crops = crop_and_resize(img, rois, target_size) + + for roi_resized, roi_idx in crops: + final_class, score, p1, p2 = classify_image_weighted(roi_resized, model, threshold=threshold) + + # 文件名中保存 ROI、类别、加权分数、class1/class2 置信度 + suffix = f"_roi{roi_idx}_{final_class}_score{score:.2f}_p1{p1:.2f}_p2{p2:.2f}" + dst_path = class_dirs[final_class] / f"{img_path.stem}{suffix}{img_path.suffix}" + cv2.imwrite(dst_path, roi_resized) + print(f"{img_path.name}{suffix} -> {final_class} (score={score:.2f}, p1={p1:.2f}, p2={p2:.2f})") + + except Exception as e: + print(f"处理失败 {img_path.name}: {e}") + + +# --------------------------- +# 单张图片使用示例(保留 ROI,不保存文件) +# --------------------------- +if __name__ == "__main__": + model_path = r"best.pt" + image_path = r"./test_image/2.jpg" # 单张图片路径 + roi_file = r"./roi_coordinates/1_rois.txt" + target_size = 640 + threshold = 0.4 #加权得分阈值可以根据大小堆料分类结果进行调整 + + # 加载模型 + model = YOLO(model_path) + + # 读取 ROI + rois = load_global_rois(roi_file) + if not rois: + print("❌ 没有有效 ROI,退出") + exit(1) + + # 读取图片 + img = cv2.imread(image_path) + if img is None: + print(f"❌ 无法读取图片: {image_path}") + exit(1) + + # 注意:必须裁剪 ROI 并推理,因为训练的时候输入的图像是经过resize的 + crops = crop_and_resize(img, rois, target_size) + for roi_resized, roi_idx in crops: + #final_class, score, p1, p2 = classify_image_weighted(roi_resized, model, threshold=threshold) + final_class,_,_,_ = classify_image_weighted(roi_resized, model, threshold=threshold) + # 只输出信息,不保存文件 + #print(f"ROI {roi_idx} -> 类别: {final_class}, 加权分数: {score:.2f}, " + #f"class1 置信度: {p1:.2f}, class2 置信度: {p2:.2f}") + print(f"类别: {final_class}") + + diff --git a/vision/overflow_model/yiliao_main_rknn.py b/vision/overflow_model/yiliao_main_rknn.py new file mode 100644 index 0000000..1b840f8 --- /dev/null +++ b/vision/overflow_model/yiliao_main_rknn.py @@ -0,0 +1,185 @@ +import os +from pathlib import Path +import cv2 +import numpy as np +import platform + + +# --------------------------- +# 类别映射 +# --------------------------- +CLASS_NAMES = { + 0: "未堆料", + 1: "小堆料", + 2: "大堆料", + 3: "未浇筑满", + 4: "浇筑满" +} + +# --------------------------- +# RKNN 全局实例(只加载一次) +# --------------------------- +_global_rknn = None +DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible' + + +# ===================================================== +# RKNN MODEL +# ===================================================== +def init_rknn_model(model_path): + from rknnlite.api import RKNNLite + + global _global_rknn + if _global_rknn is not None: + return _global_rknn + + rknn = RKNNLite(verbose=False) + + ret = rknn.load_rknn(model_path) + if ret != 0: + raise RuntimeError(f"Load RKNN failed: {ret}") + + ret = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + raise RuntimeError(f"Init runtime failed: {ret}") + + _global_rknn = rknn + print(f"[INFO] RKNN 模型加载成功: {model_path}") + return rknn + + +# --------------------------- +# 图像预处理(统一 640×640) +# --------------------------- +def preprocess(img, size=(640, 640)): + img = cv2.resize(img, size) + img = np.expand_dims(img, 0) + return img + + +# --------------------------- +# 单次 RKNN 分类 +# --------------------------- +def rknn_classify(img_resized, model_path): + rknn = init_rknn_model(model_path) + input_tensor = preprocess(img_resized) + outs = rknn.inference([input_tensor]) + + pred = outs[0].reshape(-1) + class_id = int(np.argmax(pred)) + return class_id, pred.astype(float) + + +# ===================================================== +# ROI 逻辑 +# ===================================================== +def load_rois(txt_path): + rois = [] + if not os.path.exists(txt_path): + print(f"❌ ROI 文件不存在: {txt_path}") + return rois + + with open(txt_path) as f: + for line in f: + s = line.strip() + if s: + try: + x, y, w, h = map(int, s.split(',')) + rois.append((x, y, w, h)) + except: + print("ROI 格式错误:", s) + return rois + + +def crop_and_resize(img, rois, target_size=640): + crops = [] + h_img, w_img = img.shape[:2] + + for idx, (x, y, w, h) in enumerate(rois): + if x < 0 or y < 0 or x + w > w_img or y + h > h_img: + continue + roi = img[y:y + h, x:x + w] + roi_resized = cv2.resize(roi, (target_size, target_size), interpolation=cv2.INTER_AREA) + crops.append((roi_resized, idx)) + return crops + + +# ===================================================== +# class1/class2 加权分类增强 +# ===================================================== +def weighted_small_large(pred, threshold=0.4, w1=0.3, w2=0.7): + p1 = float(pred[1]) + p2 = float(pred[2]) + total = p1 + p2 + + score = (w1 * p1 + w2 * p2) / total if total > 0 else 0.0 + final_class = "大堆料" if score >= threshold else "小堆料" + + return final_class, score, p1, p2 + + +# ===================================================== +# ⭐ 高复用:一行完成 ROI + 推理 ⭐ +# ===================================================== +def classify_frame_with_rois(model_path, frame, roi_file, threshold=0.4): + """ + 输入: + - frame: BGR 图像 (numpy array) + - model_path: RKNN 模型路径 + - roi_file: ROI 的 txt 文件 + - threshold: class1/class2 小/大堆料判断阈值 + + 输出: + [ + { "roi": idx, "class": 类别, "score": 0.93, "p1": 0.22, "p2": 0.71 }, + ... + ] + """ + + if frame is None or not isinstance(frame, np.ndarray): + raise RuntimeError("❌ classify_frame_with_rois 传入的 frame 无效") + + rois = load_rois(roi_file) + if not rois: + raise RuntimeError("ROI 文件为空") + + crops = crop_and_resize(frame, rois) + + results = [] + + for roi_img, idx in crops: + class_id, pred = rknn_classify(roi_img, model_path) + class_name = CLASS_NAMES.get(class_id, f"未知类别({class_id})") + + if class_id in [1, 2]: + final_class, score, p1, p2 = weighted_small_large(pred, threshold) + else: + final_class = class_name + score = float(pred[class_id]) + p1, p2 = float(pred[1]), float(pred[2]) + + results.append({ + "roi": idx, + "class": final_class, + "score": round(score, 4), + "p1": round(p1, 4), + "p2": round(p2, 4) + }) + + return results + + +# ===================================================== +# 示例调用 +# ===================================================== +if __name__ == "__main__": + model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "yiliao_cls.rknn") + roi_file = "./roi_coordinates/1_rois.txt" + + frame = cv2.imread("./test_image/2.jpg") + + outputs = classify_frame_with_rois(model_path, frame, roi_file) + + for res in outputs: + print(res) +