From ed636f68f64c84f6e0ffc23a4d0da40c637a7307 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 29 Oct 2025 17:42:27 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E9=87=87=E9=9B=86=E5=8D=A1bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preview.py | 72 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/preview.py b/preview.py index da87660..ca89278 100644 --- a/preview.py +++ b/preview.py @@ -161,6 +161,10 @@ class PreviewWindow: def create_grid_window(self): """创建网格窗口""" + # 重新加载配置,确保获取最新值 + config_manager.load_config() + self.config = config_manager.config + # 获取显示配置 display = self.config.get('display', {}) preview_width = display.get('preview_width', 1000) @@ -168,6 +172,20 @@ class PreviewWindow: columns = display.get('preview_columns', 2) rows = display.get('preview_rows', 2) + # 验证配置值是否有效(防止读取到无效的小值) + if preview_width < 100 or preview_height < 100: + print(f"⚠️ 警告: 配置中的预览尺寸无效 ({preview_width}x{preview_height}),使用默认值") + preview_width = 1000 + preview_height = 700 + + if columns < 1 or columns > 10: + columns = 2 + if rows < 1 or rows > 10: + rows = 2 + + # 调试输出配置值 + print(f"📐 预览窗口配置: {preview_width}x{preview_height}, 网格: {columns}x{rows}") + root = tk.Tk() root.title("采集卡预览 - 点击放大") root.geometry(f"{preview_width}x{preview_height}") @@ -227,6 +245,10 @@ class PreviewWindow: texts_to_draw = [] frame_idx = 0 + # 确保至少有一些采集卡数据 + if not self.caps: + texts_to_draw.append((canvas_width // 2, canvas_height // 2, "未找到采集卡", 'red')) + for idx in self.caps.keys(): row = frame_idx // columns col = frame_idx % columns @@ -306,26 +328,52 @@ class PreviewWindow: canvas.delete("all") # 先绘制所有图像(底层) - if images_to_draw and self.debug_count <= 3: - print(f"✅ 准备绘制 {len(images_to_draw)} 个图像到画布 ({canvas_width}x{canvas_height})") + if self.debug_count <= 3: + print(f"📊 绘制状态: {len(images_to_draw)} 个图像, {len(texts_to_draw)} 个文本, 画布={canvas_width}x{canvas_height}") - for photo, x, y in images_to_draw: - try: - canvas.create_image(x, y, image=photo, anchor='center') - except Exception as e: - if "pyimage" not in str(e).lower(): - print(f"绘制图像错误: {e}") + if images_to_draw: + for i, (photo, x, y) in enumerate(images_to_draw): + try: + # 确保坐标在画布范围内 + if 0 <= x <= canvas_width and 0 <= y <= canvas_height: + canvas.create_image(x, y, image=photo, anchor='center') + if self.debug_count <= 3 and i == 0: + print(f" 绘制图像 #{i} 到位置 ({x}, {y})") + else: + if self.debug_count <= 3: + print(f" ⚠️ 图像 #{i} 位置 ({x}, {y}) 超出画布范围") + except Exception as e: + print(f" 绘制图像 #{i} 错误: {e}") + import traceback + traceback.print_exc() + else: + # 如果没有图像,至少显示一些提示 + if self.debug_count <= 3: + print(" ⚠️ 没有图像可绘制") + # 在画布中心显示提示 + canvas.create_text( + canvas_width // 2, + canvas_height // 2, + text="等待画面...\n\n如果长时间无画面,请检查:\n1. 采集卡是否正常工作\n2. 配置是否正确", + fill='yellow', + font=('Arial', 14), + justify=tk.CENTER + ) # 再绘制所有文本(上层) for x, y, text, color in texts_to_draw: try: - if color == 'white': - canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10, 'bold')) - else: - canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10)) + if 0 <= x <= canvas_width and 0 <= y <= canvas_height: + if color == 'white': + canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10, 'bold')) + else: + canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10)) except Exception as e: print(f"绘制文本错误: {e}") + # 强制更新画布显示 + canvas.update_idletasks() + # 绘制分割线,区分不同的采集卡窗口 try: # 绘制垂直分割线(列之间的分割线) From 754501b933410b90fa3c21238e709586da1772f1 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 29 Oct 2025 17:50:33 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E9=87=87=E9=9B=86=E5=8D=A1bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preview.py | 80 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/preview.py b/preview.py index ca89278..68482c8 100644 --- a/preview.py +++ b/preview.py @@ -194,8 +194,9 @@ class PreviewWindow: canvas = Canvas(root, bg='black', width=preview_width, height=preview_height) canvas.pack(fill=tk.BOTH, expand=True) - # 存储图像对象 - self.photo_objects = {} + # 存储图像对象(使用列表保存所有PhotoImage引用,防止GC) + self.photo_objects_list = [] + self.photo_objects = {} # 按索引映射 # 用于控制调试输出(只打印前几次) self.debug_count = 0 @@ -219,10 +220,27 @@ class PreviewWindow: root.after(33, update_frames_once) return + # 重新读取配置,确保获取最新值(修复配置读取问题) + config_manager.load_config() + display = config_manager.config.get('display', {}) + current_preview_width = display.get('preview_width', 1000) + current_preview_height = display.get('preview_height', 700) + current_columns = display.get('preview_columns', 2) + current_rows = display.get('preview_rows', 2) + + # 验证配置值是否有效 + if current_preview_width < 100 or current_preview_height < 100: + current_preview_width = 1000 + current_preview_height = 700 + if current_columns < 1 or current_columns > 10: + current_columns = 2 + if current_rows < 1 or current_rows > 10: + current_rows = 2 + # 计算每个预览窗口的位置和大小 - # 直接使用配置值作为画布尺寸(macOS上窗口尺寸可能在显示前返回默认值) - canvas_width = preview_width - canvas_height = preview_height + # 直接使用配置值作为画布尺寸 + canvas_width = current_preview_width + canvas_height = current_preview_height # 尝试获取实际的窗口尺寸,如果有效则使用(大于配置值说明可能被手动调整了) try: @@ -237,8 +255,8 @@ class PreviewWindow: except: pass # 如果获取失败,使用配置值 - cell_width = max(10, canvas_width // columns) - cell_height = max(10, canvas_height // rows) + cell_width = max(10, canvas_width // current_columns) + cell_height = max(10, canvas_height // current_rows) # 先收集所有需要显示的图像 images_to_draw = [] @@ -250,8 +268,8 @@ class PreviewWindow: texts_to_draw.append((canvas_width // 2, canvas_height // 2, "未找到采集卡", 'red')) for idx in self.caps.keys(): - row = frame_idx // columns - col = frame_idx % columns + row = frame_idx // current_columns + col = frame_idx % current_columns x = col * cell_width y = row * cell_height center_x = x + cell_width // 2 @@ -289,7 +307,7 @@ class PreviewWindow: self.debug_count += 1 if self.debug_count <= 3: # 只打印前3次 print(f"🔍 预览调试 #{self.debug_count}:") - print(f" 配置尺寸: {preview_width}x{preview_height}") + print(f" 配置尺寸: {current_preview_width}x{current_preview_height}") print(f" 实际画布: {canvas_width}x{canvas_height}") print(f" 单元格大小: {cell_width}x{cell_height}") print(f" 原始帧: {w}x{h}") @@ -303,8 +321,10 @@ class PreviewWindow: resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(resized_frame) photo = ImageTk.PhotoImage(image=pil_image) - # 保持引用,避免被GC - self.photo_objects[idx] = photo + + # 多重引用保护,防止被GC回收 + self.photo_objects[idx] = photo # 按索引保存 + self.photo_objects_list.append(photo) # 在列表中保存(防止GC) images_to_draw.append((photo, center_x, center_y)) texts_to_draw.append((center_x, y + 15, name, 'white')) @@ -324,21 +344,26 @@ class PreviewWindow: texts_to_draw.append((center_x, center_y, f"{name}\n等待画面...", 'gray')) frame_idx += 1 - # 清空画布 + # 清空画布(保留photo引用,只清空画布内容) canvas.delete("all") # 先绘制所有图像(底层) if self.debug_count <= 3: print(f"📊 绘制状态: {len(images_to_draw)} 个图像, {len(texts_to_draw)} 个文本, 画布={canvas_width}x{canvas_height}") + print(f" 当前保存的照片对象数量: {len(self.photo_objects_list)}") if images_to_draw: + # 创建一个临时列表保存所有photo引用,防止在绘制时被GC + photos_to_keep = [] for i, (photo, x, y) in enumerate(images_to_draw): try: # 确保坐标在画布范围内 if 0 <= x <= canvas_width and 0 <= y <= canvas_height: + # 在绘制前保存引用 + photos_to_keep.append(photo) canvas.create_image(x, y, image=photo, anchor='center') if self.debug_count <= 3 and i == 0: - print(f" 绘制图像 #{i} 到位置 ({x}, {y})") + print(f" 绘制图像 #{i} 到位置 ({x}, {y}), photo id: {id(photo)}") else: if self.debug_count <= 3: print(f" ⚠️ 图像 #{i} 位置 ({x}, {y}) 超出画布范围") @@ -346,6 +371,13 @@ class PreviewWindow: print(f" 绘制图像 #{i} 错误: {e}") import traceback traceback.print_exc() + + # 确保照片对象不被回收(保存引用到实例变量) + # 只保留最近的一批照片对象,避免内存泄漏 + self.photo_objects_list = photos_to_keep[:] # 复制列表 + # 限制列表大小,只保留最近的20个对象 + if len(self.photo_objects_list) > 20: + self.photo_objects_list = self.photo_objects_list[-20:] else: # 如果没有图像,至少显示一些提示 if self.debug_count <= 3: @@ -377,7 +409,7 @@ class PreviewWindow: # 绘制分割线,区分不同的采集卡窗口 try: # 绘制垂直分割线(列之间的分割线) - for col in range(1, columns): + for col in range(1, current_columns): x = col * cell_width canvas.create_line( x, 0, @@ -388,7 +420,7 @@ class PreviewWindow: ) # 绘制水平分割线(行之间的分割线) - for row in range(1, rows): + for row in range(1, current_rows): y = row * cell_height canvas.create_line( 0, y, @@ -409,14 +441,20 @@ class PreviewWindow: def on_canvas_click(event): """点击画布事件""" - window_width = root.winfo_width() - window_height = root.winfo_height() - cell_width = window_width // columns - cell_height = window_height // rows + # 重新读取配置以获取最新的columns和rows + config_manager.load_config() + display = config_manager.config.get('display', {}) + click_columns = display.get('preview_columns', 2) + click_rows = display.get('preview_rows', 2) + + window_width = root.winfo_width() if root.winfo_width() > 1 else preview_width + window_height = root.winfo_height() if root.winfo_height() > 1 else preview_height + cell_width = window_width // click_columns + cell_height = window_height // click_rows col = int(event.x // cell_width) row = int(event.y // cell_height) - index = row * columns + col + index = row * click_columns + col # 找到对应的配置 if index < len(self.caps): From 94fa69043b441a4c8403f7c73aef068a87974203 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 29 Oct 2025 17:55:54 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E9=87=87=E9=9B=86=E5=8D=A1bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preview.py | 64 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/preview.py b/preview.py index 68482c8..179be11 100644 --- a/preview.py +++ b/preview.py @@ -322,11 +322,11 @@ class PreviewWindow: pil_image = Image.fromarray(resized_frame) photo = ImageTk.PhotoImage(image=pil_image) - # 多重引用保护,防止被GC回收 - self.photo_objects[idx] = photo # 按索引保存 - self.photo_objects_list.append(photo) # 在列表中保存(防止GC) + # 保存引用(按索引保存,方便查找) + self.photo_objects[idx] = photo - images_to_draw.append((photo, center_x, center_y)) + # 添加到绘制列表(在列表中保存引用,防止GC) + images_to_draw.append((photo, center_x, center_y, idx)) texts_to_draw.append((center_x, y + 15, name, 'white')) frame_idx += 1 @@ -353,31 +353,57 @@ class PreviewWindow: print(f" 当前保存的照片对象数量: {len(self.photo_objects_list)}") if images_to_draw: - # 创建一个临时列表保存所有photo引用,防止在绘制时被GC - photos_to_keep = [] - for i, (photo, x, y) in enumerate(images_to_draw): + # 先收集所有photo对象到列表中,确保引用不丢失 + photos_current_frame = [] + + for i, item in enumerate(images_to_draw): + if len(item) == 4: + photo, x, y, idx = item + else: + # 兼容旧格式(如果还有的话) + photo, x, y = item[:3] + idx = None + try: # 确保坐标在画布范围内 if 0 <= x <= canvas_width and 0 <= y <= canvas_height: - # 在绘制前保存引用 - photos_to_keep.append(photo) + # 先保存引用到列表(防止GC) + photos_current_frame.append(photo) + + # 然后绘制 canvas.create_image(x, y, image=photo, anchor='center') + if self.debug_count <= 3 and i == 0: - print(f" 绘制图像 #{i} 到位置 ({x}, {y}), photo id: {id(photo)}") + print(f" 绘制图像 #{i} (idx={idx}) 到位置 ({x}, {y})") else: if self.debug_count <= 3: print(f" ⚠️ 图像 #{i} 位置 ({x}, {y}) 超出画布范围") except Exception as e: - print(f" 绘制图像 #{i} 错误: {e}") - import traceback - traceback.print_exc() + error_msg = str(e).lower() + print(f" ❌ 绘制图像 #{i} 时出错: {type(e).__name__}: {e}") + + if "pyimage" in error_msg: + # PhotoImage 对象被 GC 了,尝试恢复 + if self.debug_count <= 5: + print(f" ⚠️ PhotoImage 对象丢失,尝试从字典恢复 (idx={idx})") + if idx is not None and idx in self.photo_objects: + try: + photo = self.photo_objects[idx] + photos_current_frame.append(photo) + canvas.create_image(x, y, image=photo, anchor='center') + if self.debug_count <= 5: + print(f" ✅ 已从字典恢复并重新绘制") + except Exception as e2: + if self.debug_count <= 5: + print(f" ❌ 恢复失败: {e2}") + else: + # 其他类型的错误,打印完整堆栈 + import traceback + traceback.print_exc() - # 确保照片对象不被回收(保存引用到实例变量) - # 只保留最近的一批照片对象,避免内存泄漏 - self.photo_objects_list = photos_to_keep[:] # 复制列表 - # 限制列表大小,只保留最近的20个对象 - if len(self.photo_objects_list) > 20: - self.photo_objects_list = self.photo_objects_list[-20:] + # 保存当前帧的所有photo引用(替换旧的列表,只保留当前帧) + # 这样确保正在显示的photo对象不会被GC + self.photo_objects_list = photos_current_frame[:] # 复制列表 else: # 如果没有图像,至少显示一些提示 if self.debug_count <= 3: From b87a26d3863634cb0a5d7c7a49e631348755c1b3 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 29 Oct 2025 18:11:25 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E9=87=87=E9=9B=86=E5=8D=A1bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preview.py | 228 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 165 insertions(+), 63 deletions(-) diff --git a/preview.py b/preview.py index 179be11..835c5fe 100644 --- a/preview.py +++ b/preview.py @@ -126,8 +126,17 @@ class PreviewWindow: print(f"✅ 成功加载 {loaded_count} 个采集卡") def capture_frames(self): - """捕获帧""" + """每5秒截取一张图""" + import time + first_capture = True # 第一次立即截取 while self.running: + if first_capture: + first_capture = False + # 第一次立即截取,不等待 + pass + else: + # 之后每5秒截取一次 + time.sleep(5.0) for idx, data in self.caps.items(): try: ret, frame = data['cap'].read() @@ -145,19 +154,18 @@ class PreviewWindow: # 转换颜色空间 frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.frames[idx] = frame_rgb + print(f"📸 采集卡 {idx} 截图已更新") else: print(f"⚠️ 采集卡 {idx} 裁剪参数无效") else: # 读取失败,清除旧帧 if idx in self.frames: self.frames[idx] = None + print(f"⚠️ 采集卡 {idx} 读取失败") except Exception as e: print(f"捕获帧 {idx} 错误: {e}") import traceback traceback.print_exc() - - import time - time.sleep(0.01) # 避免CPU占用过高 def create_grid_window(self): """创建网格窗口""" @@ -187,7 +195,7 @@ class PreviewWindow: print(f"📐 预览窗口配置: {preview_width}x{preview_height}, 网格: {columns}x{rows}") root = tk.Tk() - root.title("采集卡预览 - 点击放大") + root.title("采集卡预览(每5秒更新)- 点击放大") root.geometry(f"{preview_width}x{preview_height}") root.update_idletasks() # 立即更新窗口尺寸 @@ -197,6 +205,7 @@ class PreviewWindow: # 存储图像对象(使用列表保存所有PhotoImage引用,防止GC) self.photo_objects_list = [] self.photo_objects = {} # 按索引映射 + self.canvas_image_items = {} # 保存canvas中的图像项ID,用于更新而不是删除重建 # 用于控制调试输出(只打印前几次) self.debug_count = 0 @@ -344,23 +353,31 @@ class PreviewWindow: texts_to_draw.append((center_x, center_y, f"{name}\n等待画面...", 'gray')) frame_idx += 1 - # 清空画布(保留photo引用,只清空画布内容) - canvas.delete("all") + # 先收集所有photo对象到列表中,确保引用不丢失 + photos_current_frame = [] # 先绘制所有图像(底层) if self.debug_count <= 3: print(f"📊 绘制状态: {len(images_to_draw)} 个图像, {len(texts_to_draw)} 个文本, 画布={canvas_width}x{canvas_height}") - print(f" 当前保存的照片对象数量: {len(self.photo_objects_list)}") if images_to_draw: - # 先收集所有photo对象到列表中,确保引用不丢失 - photos_current_frame = [] + # 删除旧的文本和分割线,但保留图像项(这样PhotoImage引用不会被释放) + # 只删除文本项和线条项 + for item_id in list(canvas.find_all()): + item_tags = canvas.gettags(item_id) + item_type = canvas.type(item_id) + # 删除文本和线条,保留图像 + if item_type in ['text', 'line']: + try: + canvas.delete(item_id) + except: + pass + # 现在更新或创建图像项 for i, item in enumerate(images_to_draw): if len(item) == 4: photo, x, y, idx = item else: - # 兼容旧格式(如果还有的话) photo, x, y = item[:3] idx = None @@ -370,45 +387,58 @@ class PreviewWindow: # 先保存引用到列表(防止GC) photos_current_frame.append(photo) - # 然后绘制 - canvas.create_image(x, y, image=photo, anchor='center') - - if self.debug_count <= 3 and i == 0: - print(f" 绘制图像 #{i} (idx={idx}) 到位置 ({x}, {y})") + # 更新或创建图像项 + if idx in self.canvas_image_items: + # 更新现有图像项 + try: + item_id = self.canvas_image_items[idx] + canvas.coords(item_id, x, y) + canvas.itemconfig(item_id, image=photo) + if self.debug_count <= 3 and i == 0: + print(f" 更新图像 #{i} (idx={idx}) 到位置 ({x}, {y})") + except: + # 如果更新失败,删除旧的并创建新的 + try: + canvas.delete(self.canvas_image_items[idx]) + except: + pass + item_id = canvas.create_image(x, y, image=photo, anchor='center') + self.canvas_image_items[idx] = item_id + if self.debug_count <= 3 and i == 0: + print(f" 重新创建图像 #{i} (idx={idx}) 到位置 ({x}, {y})") + else: + # 创建新图像项 + item_id = canvas.create_image(x, y, image=photo, anchor='center') + self.canvas_image_items[idx] = item_id + if self.debug_count <= 3 and i == 0: + print(f" 创建图像 #{i} (idx={idx}) 到位置 ({x}, {y})") else: if self.debug_count <= 3: print(f" ⚠️ 图像 #{i} 位置 ({x}, {y}) 超出画布范围") except Exception as e: error_msg = str(e).lower() print(f" ❌ 绘制图像 #{i} 时出错: {type(e).__name__}: {e}") - - if "pyimage" in error_msg: - # PhotoImage 对象被 GC 了,尝试恢复 - if self.debug_count <= 5: - print(f" ⚠️ PhotoImage 对象丢失,尝试从字典恢复 (idx={idx})") - if idx is not None and idx in self.photo_objects: - try: - photo = self.photo_objects[idx] - photos_current_frame.append(photo) - canvas.create_image(x, y, image=photo, anchor='center') - if self.debug_count <= 5: - print(f" ✅ 已从字典恢复并重新绘制") - except Exception as e2: - if self.debug_count <= 5: - print(f" ❌ 恢复失败: {e2}") - else: - # 其他类型的错误,打印完整堆栈 - import traceback - traceback.print_exc() + import traceback + traceback.print_exc() - # 保存当前帧的所有photo引用(替换旧的列表,只保留当前帧) - # 这样确保正在显示的photo对象不会被GC - self.photo_objects_list = photos_current_frame[:] # 复制列表 + # 删除不再存在的图像项 + current_indices = set(idx for item in images_to_draw if len(item) >= 4 for idx in [item[3]]) + for idx in list(self.canvas_image_items.keys()): + if idx not in current_indices: + try: + canvas.delete(self.canvas_image_items[idx]) + del self.canvas_image_items[idx] + except: + pass + + # 保存当前帧的所有photo引用 + self.photo_objects_list = photos_current_frame[:] else: - # 如果没有图像,至少显示一些提示 + # 如果没有图像,清空所有并显示提示 + canvas.delete("all") + self.canvas_image_items.clear() if self.debug_count <= 3: print(" ⚠️ 没有图像可绘制") - # 在画布中心显示提示 canvas.create_text( canvas_width // 2, canvas_height // 2, @@ -429,6 +459,20 @@ class PreviewWindow: except Exception as e: print(f"绘制文本错误: {e}") + # 绘制更新时间提示(右上角) + try: + import datetime + update_time = datetime.datetime.now().strftime("%H:%M:%S") + canvas.create_text( + canvas_width - 10, 15, + text=f"最后更新: {update_time}", + fill='gray', + font=('Arial', 8), + anchor='ne' + ) + except: + pass + # 强制更新画布显示 canvas.update_idletasks() @@ -462,8 +506,8 @@ class PreviewWindow: print(f"更新帧错误: {e}") import traceback traceback.print_exc() - # 约30fps - root.after(33, update_frames_once) + # 每1秒检查一次是否有新截图(截图在另一个线程每5秒更新) + root.after(1000, update_frames_once) def on_canvas_click(event): """点击画布事件""" @@ -516,52 +560,109 @@ class PreviewWindow: root.mainloop() def show_large_window(self, idx): - """显示大窗口""" + """显示大窗口(实时显示指定采集卡)""" if self.large_window is not None and self.large_window.winfo_exists(): self.large_window.destroy() self.large_window = tk.Toplevel() - self.large_window.title(f"放大视图 - {self.caps[idx]['name']}") + self.large_window.title(f"放大视图 - {self.caps[idx]['name']} (实时)") self.large_window.geometry("1280x720") canvas = Canvas(self.large_window, bg='black') canvas.pack(fill=tk.BOTH, expand=True) photo_obj = {} + canvas_image_item = None # 保存canvas中的图像项ID def update_large_once(): if not self.running or not self.large_window.winfo_exists(): return try: + # 从采集卡实时读取帧(不依赖截图) + if idx not in self.caps: + canvas.delete("all") + canvas.create_text( + 640, 360, + text="采集卡已断开", + fill='red', + font=('Arial', 16) + ) + self.large_window.after(1000, update_large_once) + return + + cap = self.caps[idx]['cap'] + ret, frame = cap.read() + # 获取窗口大小 window_width = self.large_window.winfo_width() if self.large_window.winfo_width() > 1 else 1280 window_height = self.large_window.winfo_height() if self.large_window.winfo_height() > 1 else 720 - if idx in self.frames and self.frames[idx] is not None: - # 先处理图像,再清空画布 - frame = self.frames[idx] - h, w = frame.shape[:2] + if ret and frame is not None: + # 裁剪到配置的区域 + height, width = frame.shape[:2] + crop_top = 30 + crop_bottom = min(crop_top + 720, height) + crop_width = min(1280, width) + + # 确保裁剪范围有效 + if crop_bottom > crop_top and crop_width > 0: + frame = frame[crop_top:crop_bottom, 0:crop_width] + + # 转换颜色空间 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + h, w = frame_rgb.shape[:2] - # 调整到窗口大小 - scale = min(window_width / w, window_height / h) - new_w = int(w * scale) - new_h = int(h * scale) + # 调整到窗口大小 + scale = min(window_width / w, window_height / h) + new_w = int(w * scale) + new_h = int(h * scale) - resized_frame = cv2.resize(frame, (new_w, new_h)) - # 确保颜色通道正确(BGR -> RGB) - if len(resized_frame.shape) == 3 and resized_frame.shape[2] == 3: - resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB) - pil_image = Image.fromarray(resized_frame) - photo = ImageTk.PhotoImage(image=pil_image) - # 保存引用到字典,确保不被GC - photo_obj['img'] = photo + resized_frame = cv2.resize(frame_rgb, (new_w, new_h)) + pil_image = Image.fromarray(resized_frame) + photo = ImageTk.PhotoImage(image=pil_image) + # 保存引用到字典,确保不被GC + photo_obj['img'] = photo - # 清空并绘制新图像 - canvas.delete("all") - canvas.create_image(window_width // 2, window_height // 2, image=photo, anchor='center') + # 更新或创建图像项 + if canvas_image_item is not None: + try: + # 更新现有图像项 + canvas.coords(canvas_image_item, window_width // 2, window_height // 2) + canvas.itemconfig(canvas_image_item, image=photo) + except: + # 如果更新失败,删除旧的并创建新的 + try: + canvas.delete(canvas_image_item) + except: + pass + canvas_image_item = canvas.create_image( + window_width // 2, + window_height // 2, + image=photo, + anchor='center' + ) + else: + # 创建新图像项 + canvas.delete("all") # 首次创建时清空 + canvas_image_item = canvas.create_image( + window_width // 2, + window_height // 2, + image=photo, + anchor='center' + ) + else: + canvas.delete("all") + canvas.create_text( + window_width // 2, + window_height // 2, + text="裁剪参数无效", + fill='red', + font=('Arial', 16) + ) else: # 显示等待提示 canvas.delete("all") + canvas_image_item = None canvas.create_text( window_width // 2, window_height // 2, @@ -572,6 +673,7 @@ class PreviewWindow: except Exception as e: if "pyimage" not in str(e).lower(): # 忽略pyimage错误,避免刷屏 print(f"更新大窗口错误: {e}") + # 约30fps,实时显示 self.large_window.after(33, update_large_once) self.large_window.after(33, update_large_once) From 2399f87d57981bfc3155f4710e7d2ae7becd8071 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 29 Oct 2025 18:15:12 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=89=8D=E7=9A=84?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/huojv.iml | 2 +- .idea/misc.xml | 2 +- preview.py | 228 +++++++++++++++++++++++++++++++++++------------- 3 files changed, 167 insertions(+), 65 deletions(-) diff --git a/.idea/huojv.iml b/.idea/huojv.iml index 8b74e97..8388dbc 100644 --- a/.idea/huojv.iml +++ b/.idea/huojv.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b02dc17..5f01a00 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/preview.py b/preview.py index 179be11..835c5fe 100644 --- a/preview.py +++ b/preview.py @@ -126,8 +126,17 @@ class PreviewWindow: print(f"✅ 成功加载 {loaded_count} 个采集卡") def capture_frames(self): - """捕获帧""" + """每5秒截取一张图""" + import time + first_capture = True # 第一次立即截取 while self.running: + if first_capture: + first_capture = False + # 第一次立即截取,不等待 + pass + else: + # 之后每5秒截取一次 + time.sleep(5.0) for idx, data in self.caps.items(): try: ret, frame = data['cap'].read() @@ -145,19 +154,18 @@ class PreviewWindow: # 转换颜色空间 frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.frames[idx] = frame_rgb + print(f"📸 采集卡 {idx} 截图已更新") else: print(f"⚠️ 采集卡 {idx} 裁剪参数无效") else: # 读取失败,清除旧帧 if idx in self.frames: self.frames[idx] = None + print(f"⚠️ 采集卡 {idx} 读取失败") except Exception as e: print(f"捕获帧 {idx} 错误: {e}") import traceback traceback.print_exc() - - import time - time.sleep(0.01) # 避免CPU占用过高 def create_grid_window(self): """创建网格窗口""" @@ -187,7 +195,7 @@ class PreviewWindow: print(f"📐 预览窗口配置: {preview_width}x{preview_height}, 网格: {columns}x{rows}") root = tk.Tk() - root.title("采集卡预览 - 点击放大") + root.title("采集卡预览(每5秒更新)- 点击放大") root.geometry(f"{preview_width}x{preview_height}") root.update_idletasks() # 立即更新窗口尺寸 @@ -197,6 +205,7 @@ class PreviewWindow: # 存储图像对象(使用列表保存所有PhotoImage引用,防止GC) self.photo_objects_list = [] self.photo_objects = {} # 按索引映射 + self.canvas_image_items = {} # 保存canvas中的图像项ID,用于更新而不是删除重建 # 用于控制调试输出(只打印前几次) self.debug_count = 0 @@ -344,23 +353,31 @@ class PreviewWindow: texts_to_draw.append((center_x, center_y, f"{name}\n等待画面...", 'gray')) frame_idx += 1 - # 清空画布(保留photo引用,只清空画布内容) - canvas.delete("all") + # 先收集所有photo对象到列表中,确保引用不丢失 + photos_current_frame = [] # 先绘制所有图像(底层) if self.debug_count <= 3: print(f"📊 绘制状态: {len(images_to_draw)} 个图像, {len(texts_to_draw)} 个文本, 画布={canvas_width}x{canvas_height}") - print(f" 当前保存的照片对象数量: {len(self.photo_objects_list)}") if images_to_draw: - # 先收集所有photo对象到列表中,确保引用不丢失 - photos_current_frame = [] + # 删除旧的文本和分割线,但保留图像项(这样PhotoImage引用不会被释放) + # 只删除文本项和线条项 + for item_id in list(canvas.find_all()): + item_tags = canvas.gettags(item_id) + item_type = canvas.type(item_id) + # 删除文本和线条,保留图像 + if item_type in ['text', 'line']: + try: + canvas.delete(item_id) + except: + pass + # 现在更新或创建图像项 for i, item in enumerate(images_to_draw): if len(item) == 4: photo, x, y, idx = item else: - # 兼容旧格式(如果还有的话) photo, x, y = item[:3] idx = None @@ -370,45 +387,58 @@ class PreviewWindow: # 先保存引用到列表(防止GC) photos_current_frame.append(photo) - # 然后绘制 - canvas.create_image(x, y, image=photo, anchor='center') - - if self.debug_count <= 3 and i == 0: - print(f" 绘制图像 #{i} (idx={idx}) 到位置 ({x}, {y})") + # 更新或创建图像项 + if idx in self.canvas_image_items: + # 更新现有图像项 + try: + item_id = self.canvas_image_items[idx] + canvas.coords(item_id, x, y) + canvas.itemconfig(item_id, image=photo) + if self.debug_count <= 3 and i == 0: + print(f" 更新图像 #{i} (idx={idx}) 到位置 ({x}, {y})") + except: + # 如果更新失败,删除旧的并创建新的 + try: + canvas.delete(self.canvas_image_items[idx]) + except: + pass + item_id = canvas.create_image(x, y, image=photo, anchor='center') + self.canvas_image_items[idx] = item_id + if self.debug_count <= 3 and i == 0: + print(f" 重新创建图像 #{i} (idx={idx}) 到位置 ({x}, {y})") + else: + # 创建新图像项 + item_id = canvas.create_image(x, y, image=photo, anchor='center') + self.canvas_image_items[idx] = item_id + if self.debug_count <= 3 and i == 0: + print(f" 创建图像 #{i} (idx={idx}) 到位置 ({x}, {y})") else: if self.debug_count <= 3: print(f" ⚠️ 图像 #{i} 位置 ({x}, {y}) 超出画布范围") except Exception as e: error_msg = str(e).lower() print(f" ❌ 绘制图像 #{i} 时出错: {type(e).__name__}: {e}") - - if "pyimage" in error_msg: - # PhotoImage 对象被 GC 了,尝试恢复 - if self.debug_count <= 5: - print(f" ⚠️ PhotoImage 对象丢失,尝试从字典恢复 (idx={idx})") - if idx is not None and idx in self.photo_objects: - try: - photo = self.photo_objects[idx] - photos_current_frame.append(photo) - canvas.create_image(x, y, image=photo, anchor='center') - if self.debug_count <= 5: - print(f" ✅ 已从字典恢复并重新绘制") - except Exception as e2: - if self.debug_count <= 5: - print(f" ❌ 恢复失败: {e2}") - else: - # 其他类型的错误,打印完整堆栈 - import traceback - traceback.print_exc() + import traceback + traceback.print_exc() - # 保存当前帧的所有photo引用(替换旧的列表,只保留当前帧) - # 这样确保正在显示的photo对象不会被GC - self.photo_objects_list = photos_current_frame[:] # 复制列表 + # 删除不再存在的图像项 + current_indices = set(idx for item in images_to_draw if len(item) >= 4 for idx in [item[3]]) + for idx in list(self.canvas_image_items.keys()): + if idx not in current_indices: + try: + canvas.delete(self.canvas_image_items[idx]) + del self.canvas_image_items[idx] + except: + pass + + # 保存当前帧的所有photo引用 + self.photo_objects_list = photos_current_frame[:] else: - # 如果没有图像,至少显示一些提示 + # 如果没有图像,清空所有并显示提示 + canvas.delete("all") + self.canvas_image_items.clear() if self.debug_count <= 3: print(" ⚠️ 没有图像可绘制") - # 在画布中心显示提示 canvas.create_text( canvas_width // 2, canvas_height // 2, @@ -429,6 +459,20 @@ class PreviewWindow: except Exception as e: print(f"绘制文本错误: {e}") + # 绘制更新时间提示(右上角) + try: + import datetime + update_time = datetime.datetime.now().strftime("%H:%M:%S") + canvas.create_text( + canvas_width - 10, 15, + text=f"最后更新: {update_time}", + fill='gray', + font=('Arial', 8), + anchor='ne' + ) + except: + pass + # 强制更新画布显示 canvas.update_idletasks() @@ -462,8 +506,8 @@ class PreviewWindow: print(f"更新帧错误: {e}") import traceback traceback.print_exc() - # 约30fps - root.after(33, update_frames_once) + # 每1秒检查一次是否有新截图(截图在另一个线程每5秒更新) + root.after(1000, update_frames_once) def on_canvas_click(event): """点击画布事件""" @@ -516,52 +560,109 @@ class PreviewWindow: root.mainloop() def show_large_window(self, idx): - """显示大窗口""" + """显示大窗口(实时显示指定采集卡)""" if self.large_window is not None and self.large_window.winfo_exists(): self.large_window.destroy() self.large_window = tk.Toplevel() - self.large_window.title(f"放大视图 - {self.caps[idx]['name']}") + self.large_window.title(f"放大视图 - {self.caps[idx]['name']} (实时)") self.large_window.geometry("1280x720") canvas = Canvas(self.large_window, bg='black') canvas.pack(fill=tk.BOTH, expand=True) photo_obj = {} + canvas_image_item = None # 保存canvas中的图像项ID def update_large_once(): if not self.running or not self.large_window.winfo_exists(): return try: + # 从采集卡实时读取帧(不依赖截图) + if idx not in self.caps: + canvas.delete("all") + canvas.create_text( + 640, 360, + text="采集卡已断开", + fill='red', + font=('Arial', 16) + ) + self.large_window.after(1000, update_large_once) + return + + cap = self.caps[idx]['cap'] + ret, frame = cap.read() + # 获取窗口大小 window_width = self.large_window.winfo_width() if self.large_window.winfo_width() > 1 else 1280 window_height = self.large_window.winfo_height() if self.large_window.winfo_height() > 1 else 720 - if idx in self.frames and self.frames[idx] is not None: - # 先处理图像,再清空画布 - frame = self.frames[idx] - h, w = frame.shape[:2] + if ret and frame is not None: + # 裁剪到配置的区域 + height, width = frame.shape[:2] + crop_top = 30 + crop_bottom = min(crop_top + 720, height) + crop_width = min(1280, width) + + # 确保裁剪范围有效 + if crop_bottom > crop_top and crop_width > 0: + frame = frame[crop_top:crop_bottom, 0:crop_width] + + # 转换颜色空间 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + h, w = frame_rgb.shape[:2] - # 调整到窗口大小 - scale = min(window_width / w, window_height / h) - new_w = int(w * scale) - new_h = int(h * scale) + # 调整到窗口大小 + scale = min(window_width / w, window_height / h) + new_w = int(w * scale) + new_h = int(h * scale) - resized_frame = cv2.resize(frame, (new_w, new_h)) - # 确保颜色通道正确(BGR -> RGB) - if len(resized_frame.shape) == 3 and resized_frame.shape[2] == 3: - resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB) - pil_image = Image.fromarray(resized_frame) - photo = ImageTk.PhotoImage(image=pil_image) - # 保存引用到字典,确保不被GC - photo_obj['img'] = photo + resized_frame = cv2.resize(frame_rgb, (new_w, new_h)) + pil_image = Image.fromarray(resized_frame) + photo = ImageTk.PhotoImage(image=pil_image) + # 保存引用到字典,确保不被GC + photo_obj['img'] = photo - # 清空并绘制新图像 - canvas.delete("all") - canvas.create_image(window_width // 2, window_height // 2, image=photo, anchor='center') + # 更新或创建图像项 + if canvas_image_item is not None: + try: + # 更新现有图像项 + canvas.coords(canvas_image_item, window_width // 2, window_height // 2) + canvas.itemconfig(canvas_image_item, image=photo) + except: + # 如果更新失败,删除旧的并创建新的 + try: + canvas.delete(canvas_image_item) + except: + pass + canvas_image_item = canvas.create_image( + window_width // 2, + window_height // 2, + image=photo, + anchor='center' + ) + else: + # 创建新图像项 + canvas.delete("all") # 首次创建时清空 + canvas_image_item = canvas.create_image( + window_width // 2, + window_height // 2, + image=photo, + anchor='center' + ) + else: + canvas.delete("all") + canvas.create_text( + window_width // 2, + window_height // 2, + text="裁剪参数无效", + fill='red', + font=('Arial', 16) + ) else: # 显示等待提示 canvas.delete("all") + canvas_image_item = None canvas.create_text( window_width // 2, window_height // 2, @@ -572,6 +673,7 @@ class PreviewWindow: except Exception as e: if "pyimage" not in str(e).lower(): # 忽略pyimage错误,避免刷屏 print(f"更新大窗口错误: {e}") + # 约30fps,实时显示 self.large_window.after(33, update_large_once) self.large_window.after(33, update_large_once) From 7623e22e5c9dd726ab3c222bd90fb36a60b0f85e Mon Sep 17 00:00:00 2001 From: ray <1416431931@qq.com> Date: Thu, 30 Oct 2025 23:39:27 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E5=A4=9A=E5=8F=B0=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.json | 19 ++- config.py | 4 +- gui_config.py | 44 +++++-- main.py | 14 +- main_multi.py | 83 ++++++------ preview.py | 320 +++++++++++++++++++++++++++++---------------- utils/get_image.py | 28 ++-- utils/logger.py | 26 ++++ utils/shizi.py | 2 +- 9 files changed, 350 insertions(+), 190 deletions(-) create mode 100644 utils/logger.py diff --git a/config.json b/config.json index ef0b1af..df339b4 100644 --- a/config.json +++ b/config.json @@ -2,12 +2,22 @@ "groups": [ { "name": "配置组1", - "serial_port": "COM6", + "serial_port": "COM10", "serial_baudrate": 9600, "camera_index": 0, "camera_width": 1920, "camera_height": 1080, "move_velocity": 470, + "active": false + }, + { + "name": "火炬游戏", + "serial_port": "COM11", + "serial_baudrate": 9600, + "camera_index": 1, + "camera_width": 1920, + "camera_height": 1080, + "move_velocity": 470, "active": true } ], @@ -16,7 +26,8 @@ "preview_height": 700, "preview_columns": 2, "preview_rows": 2, - "show_preview": true + "show_preview": true, + "preview_multi_window": false, + "preview_use_all_groups": true } -} - +} \ No newline at end of file diff --git a/config.py b/config.py index f04390d..15baa65 100644 --- a/config.py +++ b/config.py @@ -23,7 +23,9 @@ class ConfigManager: "preview_height": 700, "preview_columns": 2, "preview_rows": 2, - "show_preview": True + "show_preview": True, + "preview_multi_window": False, + "preview_use_all_groups": True } } self.config = self.load_config() diff --git a/gui_config.py b/gui_config.py index 49baa55..9bdf9e7 100644 --- a/gui_config.py +++ b/gui_config.py @@ -14,6 +14,7 @@ class ConfigGUI: self.root.minsize(900, 600) # 设置最小尺寸 self.selected_index = 0 + self.preview_thread = None # 添加预览线程引用 self.setup_ui() self.load_current_config() @@ -96,6 +97,24 @@ class ConfigGUI: self.create_entry(preview_frame, "preview_height", "预览高度:", prefix="display") self.create_entry(preview_frame, "preview_columns", "预览列数:", prefix="display") self.create_entry(preview_frame, "preview_rows", "预览行数:", prefix="display") + + # 多窗口预览开关 + multi_row = ttk.Frame(preview_frame) + multi_row.pack(fill=tk.X, pady=3) + ttk.Label(multi_row, text="多窗口预览:", width=15).pack(side=tk.LEFT) + self.preview_multi_window_var = tk.BooleanVar(value=False) + multi_cb = ttk.Checkbutton(multi_row, variable=self.preview_multi_window_var) + multi_cb.pack(side=tk.LEFT, padx=5) + self.config_vars["display_preview_multi_window"] = self.preview_multi_window_var + + # 预览全部配置组开关 + all_row = ttk.Frame(preview_frame) + all_row.pack(fill=tk.X, pady=3) + ttk.Label(all_row, text="预览全部配置组:", width=15).pack(side=tk.LEFT) + self.preview_use_all_groups_var = tk.BooleanVar(value=True) + all_cb = ttk.Checkbutton(all_row, variable=self.preview_use_all_groups_var) + all_cb.pack(side=tk.LEFT, padx=5) + self.config_vars["display_preview_use_all_groups"] = self.preview_use_all_groups_var # 保存按钮 save_frame = ttk.Frame(right_frame) @@ -145,6 +164,12 @@ class ConfigGUI: for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']: if key in display: self.config_vars[f"display_{key}"].set(str(display[key])) + # 多窗口预览 + if 'preview_multi_window' in display: + self.preview_multi_window_var.set(bool(display['preview_multi_window'])) + # 预览全部配置组 + if 'preview_use_all_groups' in display: + self.preview_use_all_groups_var.set(bool(display['preview_use_all_groups'])) # 初次自动扫描一次采集卡和串口 try: self.scan_cameras() @@ -384,6 +409,10 @@ class ConfigGUI: display[key] = int(var.get()) except: pass + # 保存多窗口预览布尔值 + display['preview_multi_window'] = bool(self.preview_multi_window_var.get()) + # 保存预览全部配置组布尔值 + display['preview_use_all_groups'] = bool(self.preview_use_all_groups_var.get()) config_manager.config['display'] = display # 保存到文件 @@ -397,20 +426,13 @@ class ConfigGUI: return False def start_preview(self): - """启动预览窗口""" - # 保存配置(不显示消息框,静默保存) + """启动预览窗口(主线程,弹窗模式)""" if not self.save_config_silent(): messagebox.showerror("错误", "配置保存失败,无法启动预览") return - - # 重新加载配置(从文件读取最新配置) config_manager.load_config() - - # 启动预览窗口 from preview import PreviewWindow - preview_thread = threading.Thread(target=lambda: PreviewWindow().run()) - preview_thread.daemon = True - preview_thread.start() + PreviewWindow().run(self.root) # 以主窗口为父Toplevel弹出独立预览窗 def save_config_silent(self): """静默保存配置(不显示消息框)""" @@ -462,6 +484,10 @@ class ConfigGUI: display[key] = int(var.get()) except: pass + # 保存多窗口预览布尔值(静默) + display['preview_multi_window'] = bool(self.preview_multi_window_var.get()) + # 保存预览全部配置组布尔值(静默) + display['preview_use_all_groups'] = bool(self.preview_use_all_groups_var.get()) config_manager.config['display'] = display # 保存到文件 diff --git a/main.py b/main.py index a70e9e3..01c1bf3 100644 --- a/main.py +++ b/main.py @@ -252,7 +252,7 @@ while True: time.sleep(0.8) mouse_gui.send_data_absolute(left + 706, top + 454, may=1) continue - elif panduan: # 图内情况 + elif panduan: # 图内情况 print("在图内") if shizi.shuzi(im_opencv[0]): boss_pd = True @@ -260,15 +260,9 @@ while True: if shizi.fuhuo(im_opencv[0]): print('点击复活') mouse_gui.send_data_absolute(left + 536, top + 627, may=1) - mouse_gui.send_data_absolute(rw[0], rw[1], may=0) - continue - if detections['zhaozi'] is not None: - move_to(rw, detections['zhaozi']) - continue - if len(detections['daojv']) != 0: - move_to(rw, process_points(detections['daojv'])) - for i in range(3 + len(detections['daojv'])): - keyboard.send_data("AA") + + + time.sleep(0.15) keyboard.release() continue diff --git a/main_multi.py b/main_multi.py index c421d33..41b9efd 100644 --- a/main_multi.py +++ b/main_multi.py @@ -6,6 +6,10 @@ import multiprocessing import sys from config import config_manager from main_single import run_automation_for_group +from utils.logger import logger, throttle +import logging +import time + def main(): """主函数""" @@ -16,44 +20,44 @@ def main(): groups = config_manager.config.get('groups', []) if not groups: - print("❌ 没有找到任何配置组") - print("请先运行 gui_config.py 创建配置组") + logger.error("❌ 没有找到任何配置组") + logger.info("请先运行 gui_config.py 创建配置组") return # 询问要启动哪些配置组 - print("=" * 60) - print("🔥 多配置组启动器") - print("=" * 60) - print("\n可用配置组:") + logger.info("=" * 60) + logger.info("🔥 多配置组启动器") + logger.info("=" * 60) + logger.info("\n可用配置组:") for i, group in enumerate(groups): active_mark = "✓" if group.get('active', False) else " " - print(f" [{i}] {active_mark} {group['name']}") - print(f" 串口: {group['serial_port']} | 采集卡: {group['camera_index']}") + logger.info(f" [{i}] {active_mark} {group['name']}") + logger.info(f" 串口: {group['serial_port']} | 采集卡: {group['camera_index']}") - print("\n选择启动方式:") - print(" 1. 启动所有活动配置组") - print(" 2. 启动所有配置组") - print(" 3. 选择特定配置组") - print(" 0. 退出") + logger.info("\n选择启动方式:") + logger.info(" 1. 启动所有活动配置组") + logger.info(" 2. 启动所有配置组") + logger.info(" 3. 选择特定配置组") + logger.info(" 0. 退出") choice = input("\n请选择 (0-3): ").strip() selected_indices = [] if choice == "0": - print("👋 退出") + logger.info("👋 退出") return elif choice == "1": # 启动所有活动配置组 selected_indices = [i for i, g in enumerate(groups) if g.get('active', False)] if not selected_indices: - print("❌ 没有活动的配置组") + logger.error("❌ 没有活动的配置组") return - print(f"\n✅ 将启动 {len(selected_indices)} 个活动配置组") + logger.info(f"\n✅ 将启动 {len(selected_indices)} 个活动配置组") elif choice == "2": # 启动所有配置组 selected_indices = list(range(len(groups))) - print(f"\n✅ 将启动所有 {len(selected_indices)} 个配置组") + logger.info(f"\n✅ 将启动所有 {len(selected_indices)} 个配置组") elif choice == "3": # 选择特定配置组 indices_input = input("请输入要启动的配置组索引(用逗号分隔,如: 0,1,2): ").strip() @@ -62,34 +66,33 @@ def main(): # 验证索引有效性 selected_indices = [i for i in selected_indices if 0 <= i < len(groups)] if not selected_indices: - print("❌ 没有有效的配置组索引") + logger.error("❌ 没有有效的配置组索引") return - print(f"\n✅ 将启动 {len(selected_indices)} 个配置组") + logger.info(f"\n✅ 将启动 {len(selected_indices)} 个配置组") except ValueError: - print("❌ 输入格式错误") + logger.error("❌ 输入格式错误") return else: - print("❌ 无效选择") + logger.error("❌ 无效选择") return # 显示将要启动的配置组 - print("\n将要启动的配置组:") + logger.info("\n将要启动的配置组:") for idx in selected_indices: group = groups[idx] - print(f" • {group['name']} (串口:{group['serial_port']}, 采集卡:{group['camera_index']})") + logger.info(f" • {group['name']} (串口:{group['serial_port']}, 采集卡:{group['camera_index']})") confirm = input("\n确认启动? (y/n): ").strip().lower() if confirm != 'y': - print("❌ 取消启动") + logger.info("❌ 取消启动") return # 启动多进程 - print("\n🚀 开始启动多个配置组...") processes = [] for idx in selected_indices: group = groups[idx] - print(f"启动进程: {group['name']}...") + logger.info(f"启动进程: {group['name']}...") process = multiprocessing.Process( target=run_automation_for_group, args=(idx,), @@ -97,12 +100,12 @@ def main(): ) process.start() processes.append((idx, group['name'], process)) - print(f"✅ {group['name']} 已启动 (PID: {process.pid})") + logger.info(f"✅ {group['name']} 已启动 (PID: {process.pid})") - print(f"\n✅ 成功启动 {len(processes)} 个配置组进程") - print("\n" + "=" * 60) - print("运行状态:") - print("=" * 60) + logger.info(f"\n✅ 成功启动 {len(processes)} 个配置组进程") + logger.info("\n" + "=" * 60) + logger.info("运行状态:") + logger.info("=" * 60) # 监控进程状态 try: @@ -112,32 +115,32 @@ def main(): if proc.is_alive(): alive_count += 1 else: - print(f"⚠️ {name} 进程已退出 (退出码: {proc.exitcode})") + logger.warning(f"⚠️ {name} 进程已退出 (退出码: {proc.exitcode})") if alive_count == 0: - print("\n所有进程已退出") + logger.info("\n所有进程已退出") break - import time time.sleep(2) # 打印存活状态 alive_names = [name for idx, name, proc in processes if proc.is_alive()] if alive_names: - print(f"\r📊 运行中: {', '.join(alive_names)} ({alive_count}/{len(processes)})", end='', flush=True) + # 每2秒节流一次状态行 + throttle("multi_alive", 2.0, logging.INFO, f"📊 运行中: {', '.join(alive_names)} ({alive_count}/{len(processes)})") except KeyboardInterrupt: - print("\n\n🛑 收到停止信号,正在关闭所有进程...") + logger.warning("\n\n🛑 收到停止信号,正在关闭所有进程...") for idx, name, proc in processes: if proc.is_alive(): - print(f"正在停止 {name}...") + logger.info(f"正在停止 {name}...") proc.terminate() proc.join(timeout=5) if proc.is_alive(): - print(f"强制停止 {name}...") + logger.warning(f"强制停止 {name}...") proc.kill() - print(f"✅ {name} 已停止") - print("\n👋 所有进程已停止") + logger.info(f"✅ {name} 已停止") + logger.info("\n👋 所有进程已停止") if __name__ == "__main__": multiprocessing.freeze_support() # Windows下需要 diff --git a/preview.py b/preview.py index da87660..5348504 100644 --- a/preview.py +++ b/preview.py @@ -11,6 +11,7 @@ from config import config_manager # 抑制OpenCV的警告信息(兼容不同版本) import sys import io +import time as _time os.environ['OPENCV_LOG_LEVEL'] = 'SILENT' os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '0' @@ -26,13 +27,25 @@ try: except Exception: pass +# 简单日志限流 +_last_log_times = {} +def log_throttle(key: str, interval_sec: float, text: str): + now = _time.time() + last = _last_log_times.get(key, 0) + if now - last >= interval_sec: + _last_log_times[key] = now + try: + print(text) + except Exception: + pass + class PreviewWindow: """采集卡预览窗口""" def __init__(self): self.config = config_manager.config self.caps = {} self.frames = {} - self.large_window = None + self.large_windows = {} # idx -> Toplevel self.running = True def init_cameras(self): @@ -40,11 +53,17 @@ class PreviewWindow: print("🔧 开始初始化采集卡...") loaded_count = 0 - # 如果没有活动配置,加载所有配置 - active_groups = [g for g in self.config['groups'] if g.get('active', True)] - if not active_groups: - print("⚠️ 没有活动的配置组,将尝试加载所有配置组") - active_groups = self.config['groups'] + # 根据配置选择加载哪些组 + display = self.config.get('display', {}) + use_all = bool(display.get('preview_use_all_groups', True)) + if use_all: + target_groups = self.config['groups'] + print(f"📷 预览全部配置组,共 {len(target_groups)} 组") + else: + target_groups = [g for g in self.config['groups'] if g.get('active', False)] + print(f"📷 仅预览活动配置组,共 {len(target_groups)} 组") + if not target_groups: + print("⚠️ 没有活动的配置组,将不显示任何采集卡。可在配置中启用‘预览全部配置组’或设置活动组") # 重定向stderr来抑制OpenCV的错误输出 old_stderr = sys.stderr @@ -53,7 +72,7 @@ class PreviewWindow: try: sys.stderr = suppressed_output - for i, group in enumerate(active_groups): + for i, group in enumerate(target_groups): try: cam_idx = group['camera_index'] print(f" 尝试打开采集卡 {cam_idx} ({group['name']})...") @@ -125,6 +144,40 @@ class PreviewWindow: else: print(f"✅ 成功加载 {loaded_count} 个采集卡") + def grab_once(self): + """抓取每個采集卡當前單幀(裁剪後,存入 self.frames),抓取结果增强debug输出""" + for idx, data in self.caps.items(): + try: + cap = data['cap'] + frm_name = data.get('name', str(idx)) + if cap is None: + print(f"[抓取] 采集卡{idx}({frm_name}) cap为None") + self.frames[idx] = None + continue + ret, frame = cap.read() + if not ret or frame is None: + print(f"[抓取] 采集卡{idx}({frm_name}) 没抓到帧 ret={ret} frame=None") + self.frames[idx] = None + continue + height, width = frame.shape[:2] + crop_top = 30 + crop_bottom = min(crop_top + 720, height) + crop_width = min(1280, width) + if crop_bottom <= crop_top or crop_width <= 0: + print(f"[抓取] 采集卡{idx}({frm_name}) 尺寸异常: H={height} W={width} 裁剪失败") + self.frames[idx] = None + continue + frame = frame[crop_top:crop_bottom, 0:crop_width] + # 额外检查:像素全黑? + if np.mean(frame) < 3: + print(f"[抓取] 采集卡{idx}({frm_name}) 帧抓到但内容接近全黑 shape={frame.shape}") + else: + print(f"[抓取] 采集卡{idx}({frm_name}) 成功 shape={frame.shape} 类型={frame.dtype}") + self.frames[idx] = frame + except Exception as e: + print(f"[抓取] 采集卡{idx}({data.get('name', str(idx))}) 抓取异常: {e}") + self.frames[idx] = None + def capture_frames(self): """捕获帧""" while self.running: @@ -159,19 +212,21 @@ class PreviewWindow: import time time.sleep(0.01) # 避免CPU占用过高 - def create_grid_window(self): - """创建网格窗口""" - # 获取显示配置 + def create_grid_window(self, master=None): + """创建网格窗口,可挂载在主Tk下作为Toplevel弹窗""" display = self.config.get('display', {}) preview_width = display.get('preview_width', 1000) preview_height = display.get('preview_height', 700) columns = display.get('preview_columns', 2) rows = display.get('preview_rows', 2) - - root = tk.Tk() + + if master is not None: + root = tk.Toplevel(master) + else: + root = tk.Tk() root.title("采集卡预览 - 点击放大") root.geometry(f"{preview_width}x{preview_height}") - root.update_idletasks() # 立即更新窗口尺寸 + root.update_idletasks() canvas = Canvas(root, bg='black', width=preview_width, height=preview_height) canvas.pack(fill=tk.BOTH, expand=True) @@ -183,11 +238,10 @@ class PreviewWindow: self.debug_count = 0 def update_frames_once(): - """在主线程中更新一帧(使用after循环)""" + """在主线程中每5秒刷新画面(直接显示高频采集线程生成的帧,去除grab_once)""" if not self.running: return try: - # 如果没有加载任何采集卡,显示提示 if not self.caps: canvas.delete("all") canvas.create_text( @@ -198,7 +252,7 @@ class PreviewWindow: font=('Arial', 14), justify=tk.CENTER ) - root.after(33, update_frames_once) + root.after(5000, update_frames_once) return # 计算每个预览窗口的位置和大小 @@ -228,78 +282,74 @@ class PreviewWindow: frame_idx = 0 for idx in self.caps.keys(): + frame = self.frames.get(idx) + has_frame = frame is not None + print(f'[DEBUG] update UI idx={idx}, has_frame={has_frame}, frame_info={getattr(frame, "shape", None)}') + name = self.caps[idx]['name'] row = frame_idx // columns col = frame_idx % columns x = col * cell_width y = row * cell_height center_x = x + cell_width // 2 center_y = y + cell_height // 2 - name = self.caps[idx]['name'] - - # 检查帧数据 - has_frame = idx in self.frames and self.frames[idx] is not None - - if has_frame: - # 调整图像大小 + # 如果没拿到frame,临时主动再抓一次,并DEBUG + if not has_frame: + print(f'[DEBUG] UI未获得frame,尝试临时抓取采集卡{idx}...') + try: + cap_test = self.caps[idx]['cap'] + ret, fr2 = cap_test.read() + print(f'[DEBUG] 临时抓取ret={ret}, shape={getattr(fr2, "shape", None)}') + if ret and fr2 is not None: + frame = fr2 + has_frame = True + else: + frame = None + has_frame = False + except Exception as e: + print(f'[DEBUG] 临时抓取异常:{e}') + frame = None + has_frame = False + if has_frame: try: - frame = self.frames[idx] h, w = frame.shape[:2] - - # 确保尺寸有效 if w <= 0 or h <= 0: - texts_to_draw.append((center_x, center_y, f"{name}\n尺寸无效", 'red')) + detail = f"采集卡{idx}:{name}\n抓到帧但图像尺寸异常 ({w}x{h})" + texts_to_draw.append((center_x, center_y, detail, 'red')) frame_idx += 1 continue - - # 计算缩放比例,留一些边距 scale = min(cell_width / w, cell_height / h, 1.0) * 0.85 new_w = max(1, int(w * scale)) new_h = max(1, int(h * scale)) - - # 确保缩放后的尺寸有效 if new_w <= 0 or new_h <= 0: - texts_to_draw.append((center_x, center_y, f"{name}\n缩放失败", 'red')) + detail = f"采集卡{idx}:{name}\n缩放计算异常 ({w}x{h})" + texts_to_draw.append((center_x, center_y, detail, 'red')) frame_idx += 1 continue - - # 调试输出(仅前几次,避免刷屏) - if frame_idx == 0 and len(images_to_draw) == 0: - self.debug_count += 1 - if self.debug_count <= 3: # 只打印前3次 - print(f"🔍 预览调试 #{self.debug_count}:") - print(f" 配置尺寸: {preview_width}x{preview_height}") - print(f" 实际画布: {canvas_width}x{canvas_height}") - print(f" 单元格大小: {cell_width}x{cell_height}") - print(f" 原始帧: {w}x{h}") - print(f" 缩放后: {new_w}x{new_h}") - - resized_frame = cv2.resize(frame, (new_w, new_h)) - - # 转换为PIL图像 - # 确保颜色通道正确(BGR -> RGB) - if len(resized_frame.shape) == 3 and resized_frame.shape[2] == 3: - resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB) + display_frame = frame + if len(display_frame.shape) == 3 and display_frame.shape[2] == 3: + display_frame = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB) + resized_frame = cv2.resize(display_frame, (new_w, new_h)) pil_image = Image.fromarray(resized_frame) photo = ImageTk.PhotoImage(image=pil_image) - # 保持引用,避免被GC - self.photo_objects[idx] = photo - + if not hasattr(self, 'photo_refs'): + self.photo_refs = {} + self.photo_refs[idx] = photo + canvas.create_image(center_x, center_y, image=photo, anchor='center') + canvas.update() # 保证实时刷新 + print(f'[DEBUG] UI idx={idx}, 绘制photo id={id(photo)}, photo_refs={len(self.photo_refs)}') images_to_draw.append((photo, center_x, center_y)) texts_to_draw.append((center_x, y + 15, name, 'white')) - frame_idx += 1 except Exception as e: - # 忽略pyimage相关错误,避免刷屏 - if "pyimage" not in str(e).lower(): - print(f"处理帧 {idx} 错误: {e}") - import traceback - traceback.print_exc() - # 如果处理失败,显示等待提示 - texts_to_draw.append((center_x, center_y, f"{name}\n处理失败", 'red')) + errstr = str(e) + detail = f"采集卡{idx}:{name}\n帧处理异常:{errstr}" + print(f'[DEBUG] idx={idx}, 帧处理异常:{errstr}') + texts_to_draw.append((center_x, center_y, detail, 'red')) frame_idx += 1 else: - # 显示等待提示 - texts_to_draw.append((center_x, center_y, f"{name}\n等待画面...", 'gray')) + detail = f"采集卡{idx}:{name}\n抓取失败/暂无画面" + print(f'[DEBUG] idx={idx} 依然无可用帧,最终显示错误提示') + texts_to_draw.append((center_x, center_y, detail, 'red')) frame_idx += 1 # 清空画布 @@ -307,14 +357,20 @@ class PreviewWindow: # 先绘制所有图像(底层) if images_to_draw and self.debug_count <= 3: - print(f"✅ 准备绘制 {len(images_to_draw)} 个图像到画布 ({canvas_width}x{canvas_height})") + log_throttle("draw_prepare", 2.0, f"✅ 准备绘制 {len(images_to_draw)} 个图像到画布 ({canvas_width}x{canvas_height})") for photo, x, y in images_to_draw: try: + # PhotoImage强引用,防止被GC + if not hasattr(self, 'photo_refs'): + self.photo_refs = {} + self.photo_refs[idx] = photo canvas.create_image(x, y, image=photo, anchor='center') + canvas.update() # 保证实时刷新 + print(f'[DEBUG] idx={idx}, photo id={id(photo)}, photo refs={len(self.photo_refs)}') except Exception as e: if "pyimage" not in str(e).lower(): - print(f"绘制图像错误: {e}") + log_throttle("draw_image_error", 2.0, f"绘制图像错误: {e}") # 再绘制所有文本(上层) for x, y, text, color in texts_to_draw: @@ -324,7 +380,7 @@ class PreviewWindow: else: canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10)) except Exception as e: - print(f"绘制文本错误: {e}") + log_throttle("draw_text_error", 2.0, f"绘制文本错误: {e}") # 绘制分割线,区分不同的采集卡窗口 try: @@ -350,14 +406,14 @@ class PreviewWindow: dash=(5, 5) # 虚线效果 ) except Exception as e: - print(f"绘制分割线错误: {e}") + log_throttle("draw_grid_error", 2.0, f"绘制分割线错误: {e}") except Exception as e: - print(f"更新帧错误: {e}") + log_throttle("update_frame_error", 1.0, f"更新帧错误: {e}") import traceback traceback.print_exc() - # 约30fps - root.after(33, update_frames_once) + # 每5秒更新一次 + root.after(5000, update_frames_once) def on_canvas_click(event): """点击画布事件""" @@ -401,86 +457,128 @@ class PreviewWindow: root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) - root.mainloop() + # 仅在独立窗口(无master)时进入事件循环 + if master is None: + root.mainloop() def show_large_window(self, idx): - """显示大窗口""" - if self.large_window is not None and self.large_window.winfo_exists(): - self.large_window.destroy() + """显示指定采集卡的大窗口(可多实例)""" + # 如果已存在该索引窗口,先销毁再创建,确保刷新 + if idx in self.large_windows and self.large_windows[idx].winfo_exists(): + try: + self.large_windows[idx].destroy() + except Exception: + pass - self.large_window = tk.Toplevel() - self.large_window.title(f"放大视图 - {self.caps[idx]['name']}") - self.large_window.geometry("1280x720") + win = tk.Toplevel() + win.title(f"放大视图 - {self.caps[idx]['name']}") + win.geometry("1280x720") + self.large_windows[idx] = win - canvas = Canvas(self.large_window, bg='black') + canvas = Canvas(win, bg='black') canvas.pack(fill=tk.BOTH, expand=True) photo_obj = {} def update_large_once(): - if not self.running or not self.large_window.winfo_exists(): + if not self.running or not win.winfo_exists(): return try: - # 获取窗口大小 - window_width = self.large_window.winfo_width() if self.large_window.winfo_width() > 1 else 1280 - window_height = self.large_window.winfo_height() if self.large_window.winfo_height() > 1 else 720 + window_width = win.winfo_width() if win.winfo_width() > 1 else 1280 + window_height = win.winfo_height() if win.winfo_height() > 1 else 720 if idx in self.frames and self.frames[idx] is not None: - # 先处理图像,再清空画布 frame = self.frames[idx] h, w = frame.shape[:2] - - # 调整到窗口大小 scale = min(window_width / w, window_height / h) new_w = int(w * scale) new_h = int(h * scale) - resized_frame = cv2.resize(frame, (new_w, new_h)) - # 确保颜色通道正确(BGR -> RGB) if len(resized_frame.shape) == 3 and resized_frame.shape[2] == 3: resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(resized_frame) photo = ImageTk.PhotoImage(image=pil_image) - # 保存引用到字典,确保不被GC photo_obj['img'] = photo - - # 清空并绘制新图像 canvas.delete("all") canvas.create_image(window_width // 2, window_height // 2, image=photo, anchor='center') else: - # 显示等待提示 canvas.delete("all") + # 显示更详细的调试信息 + debug_text = f"等待画面...\n采集卡: {idx}\n帧状态: {idx in self.frames}\n" + if idx in self.frames: + debug_text += f"帧数据: {self.frames[idx] is not None}" + else: + debug_text += "帧数据: 未初始化" canvas.create_text( - window_width // 2, - window_height // 2, - text="等待画面...", - fill='gray', - font=('Arial', 16) + window_width // 2, + window_height // 2, + text=debug_text, + fill='gray', + font=('Arial', 12), + justify='center' ) except Exception as e: - if "pyimage" not in str(e).lower(): # 忽略pyimage错误,避免刷屏 - print(f"更新大窗口错误: {e}") - self.large_window.after(33, update_large_once) + if "pyimage" not in str(e).lower(): + log_throttle("large_update_error", 1.0, f"更新大窗口错误: {e}") + # 放大視窗維持原本高頻刷新 + win.after(33, update_large_once) - self.large_window.after(33, update_large_once) + win.after(33, update_large_once) + + def on_close(): + try: + if idx in self.large_windows: + del self.large_windows[idx] + finally: + win.destroy() + win.protocol("WM_DELETE_WINDOW", on_close) - def run(self): - """运行预览""" + def run(self, master=None): + """运行预览,支持主Tk弹窗模式""" self.init_cameras() - + if not self.caps: - # 如果没有加载任何采集卡,仍然显示窗口并显示错误信息 print("⚠️ 没有可用的采集卡,将显示错误提示窗口") - - # 启动捕获线程 + + display = self.config.get('display', {}) + multi_window = bool(display.get('preview_multi_window', False)) + if self.caps: capture_thread = threading.Thread(target=self.capture_frames, daemon=True) capture_thread.start() - - # 创建并显示网格窗口 - import time - time.sleep(0.5) # 等待几帧 - self.create_grid_window() + import time + time.sleep(0.5) + + if multi_window: + root = tk.Toplevel(master) if master else tk.Tk() + root.title("采集卡预览 - 多窗口模式") + root.geometry("300x80") + tk.Label(root, text="已打开多窗口预览,可单独拖动/调整大小").pack(pady=20) + print("等待采集卡初始化...") + max_wait = 50 + wait_count = 0 + while wait_count < max_wait and not any(frame is not None for frame in self.frames.values()): + time.sleep(0.1) + wait_count += 1 + if wait_count >= max_wait: + print("⚠️ 采集卡初始化超时,可能无法显示画面") + for idx in list(self.caps.keys()): + print(f"打开采集卡 {idx} 的预览窗口...") + self.show_large_window(idx) + def on_root_close(): + self.running = False + for data in self.caps.values(): + data['cap'].release() + try: + root.destroy() + except Exception: + pass + root.protocol("WM_DELETE_WINDOW", on_root_close) + # 仅在独立窗口(无master)时进入事件循环 + if master is None: + root.mainloop() + else: + self.create_grid_window(master) if __name__ == "__main__": preview = PreviewWindow() diff --git a/utils/get_image.py b/utils/get_image.py index cefd3b2..9b1ef83 100644 --- a/utils/get_image.py +++ b/utils/get_image.py @@ -2,6 +2,8 @@ import time from PIL import Image import cv2 +from utils.logger import logger, throttle +import logging # class GetImage: # def __init__(self, cam_index=0, width=1920, height=1080): # self.cap = cv2.VideoCapture(cam_index,cv2.CAP_DSHOW) @@ -63,7 +65,7 @@ except Exception: class GetImage: def __init__(self, cam_index=0, width=1920, height=1080): - print(f"🔧 正在初始化采集卡 {cam_index}...") + logger.info(f"🔧 正在初始化采集卡 {cam_index}...") self.cap = None self.frame = None self.running = True @@ -96,7 +98,7 @@ class GetImage: # 测试读取一帧 ret, test_frame = self.cap.read() if ret and test_frame is not None: - print(f"✅ 采集卡 {cam_index} 打开成功") + logger.info(f"✅ 采集卡 {cam_index} 打开成功") break else: self.cap.release() @@ -114,12 +116,8 @@ class GetImage: sys.stderr = old_stderr if self.cap is None or not self.cap.isOpened(): - print(f"❌ 无法打开采集卡 {cam_index}") - print("请检查:") - print(" 1. 采集卡是否正确连接") - print(" 2. 采集卡索引是否正确(尝试扫描采集卡)") - print(" 3. 采集卡驱动是否安装") - print(" 4. 采集卡是否被其他程序占用") + logger.error(f"❌ 无法打开采集卡 {cam_index}") + logger.error("请检查:\n 1. 采集卡是否正确连接\n 2. 采集卡索引是否正确(尝试扫描采集卡)\n 3. 采集卡驱动是否安装\n 4. 采集卡是否被其他程序占用") self.cap = None return @@ -130,9 +128,9 @@ class GetImage: # 实际获取设置后的分辨率 actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - print(f" 分辨率设置: {width}x{height} -> 实际: {actual_width}x{actual_height}") + logger.info(f" 分辨率设置: {width}x{height} -> 实际: {actual_width}x{actual_height}") except Exception as e: - print(f"⚠️ 设置分辨率失败: {e}") + logger.warning(f"⚠️ 设置分辨率失败: {e}") # 启动更新线程 threading.Thread(target=self.update, daemon=True).start() @@ -140,7 +138,7 @@ class GetImage: # 等待几帧确保采集卡正常工作 import time time.sleep(1.0) - print(f"✅ 采集卡 {cam_index} 初始化完成") + logger.info(f"✅ 采集卡 {cam_index} 初始化完成") def update(self): while self.running and self.cap is not None: @@ -148,12 +146,14 @@ class GetImage: ret, frame = self.cap.read() if ret and frame is not None: self.frame = frame + # 限制读取频率,避免占满CPU + time.sleep(0.008) else: # 读取失败时不打印,避免刷屏 - pass + time.sleep(0.02) except Exception as e: # 只在异常时打印错误 - print(f"⚠️ 采集卡 {self.cam_index} 读取异常: {e}") + throttle(f"cap_read_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 采集卡 {self.cam_index} 读取异常: {e}") import time time.sleep(0.1) # 出错时短暂延迟 @@ -166,7 +166,7 @@ class GetImage: im_PIL = Image.fromarray(im_opencv) return [im_opencv, im_PIL] except Exception as e: - print(f"⚠️ 图像处理错误: {e}") + throttle(f"img_proc_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 图像处理错误: {e}") return None def release(self): diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..4979ca8 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,26 @@ +import logging +import sys +import time +from typing import Dict + +# 基础logger配置(控制台输出) +_logger = logging.getLogger("huojv") +if not _logger.handlers: + _logger.setLevel(logging.INFO) + handler = logging.StreamHandler(sys.stdout) + fmt = logging.Formatter(fmt='[%(asctime)s] %(levelname)s %(message)s', datefmt='%H:%M:%S') + handler.setFormatter(fmt) + _logger.addHandler(handler) + +# 简单的节流打印:同一个key在interval秒内只打印一次 +_last_log_times: Dict[str, float] = {} + +def throttle(key: str, interval_sec: float, level: int, msg: str): + now = time.time() + last = _last_log_times.get(key, 0.0) + if now - last >= interval_sec: + _last_log_times[key] = now + _logger.log(level, msg) + +# 对外暴露 +logger = _logger diff --git a/utils/shizi.py b/utils/shizi.py index 60bd8f5..ac8f3a4 100644 --- a/utils/shizi.py +++ b/utils/shizi.py @@ -45,7 +45,7 @@ def tiaozhan(image): def tuichu(image): - image = image[24:58, 569:669] + image = image[36:58, 560:669] image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) # 将裁剪后的图像编码成二进制格式 _, img_encoded = cv2.imencode('.png', image) From eb896fc31edfe53fc63c07e2d1fb33dea3ceb262 Mon Sep 17 00:00:00 2001 From: ray <1416431931@qq.com> Date: Fri, 31 Oct 2025 00:24:30 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E5=A4=9A=E5=8F=B0=E6=8E=A7=E5=88=B6bug?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/get_image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/get_image.py b/utils/get_image.py index 9b1ef83..6366d8f 100644 --- a/utils/get_image.py +++ b/utils/get_image.py @@ -154,7 +154,6 @@ class GetImage: except Exception as e: # 只在异常时打印错误 throttle(f"cap_read_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 采集卡 {self.cam_index} 读取异常: {e}") - import time time.sleep(0.1) # 出错时短暂延迟 def get_frame(self):