From b87a26d3863634cb0a5d7c7a49e631348755c1b3 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 29 Oct 2025 18:11:25 +0800 Subject: [PATCH] =?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)