You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python多线程+OpenCV:无法从线程类获取numpy数组问题

问题

将目标检测类放在独立线程中运行后,主线程无法获取numpy数组形式的检测结果rectangles。线程内部能打印出预期的数组,但主线程调用detect.rectangles始终返回空数组,编写getRectangles方法也无效。相关代码如下:

主方法

detect = detector.Detection()

detect.start()

while(True): 
    # 获取游戏的最新截图
    screenshot = wincap.get_screenshot()

    detect.update(screenshot) 

    detection_image = Vision.draw_rectangles(screenshot, detect.rectangles)

说明:Vision.draw_rectangles是基于OpenCV的目标绘制方法,多线程改造前可正常工作,目前仍能输出正确结果,但主线程无法从线程中获取数据。

Detector类

class Detection(object):
    # 线程相关属性
    stopped = True
    lock = None
    rectangles = []
 
    # 属性
    screenshot = None
 
    def __init__(self):
        # 创建线程锁对象
        self.lock = Lock()
    
    def update(self, screenshot):
        self.lock.acquire()
        self.screenshot = screenshot
        self.lock.release()

    def start(self):
        self.stopped = False
        t = Thread(target=self.run)
        print("Started")
        t.start()
     
    def stop(self):
        print("Stopped")
        self.stopped = True
      
    def run(self):
        vision_mineral = Vision('C:/Users/Jordan/Desktop/Wakfu_Bot/Mining_bot/Resources/Copper.JPG')
        # 初始化采矿图标检测器
        vision_mine_icon = Vision('C:/Users/Jordan/Desktop/Wakfu_Bot/Mining_bot/Resources/mineIcon.JPG')
        # 初始化加载图标检测器
        vision_loading_icon = Vision('C:/Users/Jordan/Desktop/Wakfu_Bot/Mining_bot/Resources/LoadingBar.JPG')
        
        while not self.stopped:
            if not self.screenshot is None:
                print("updating image")
                # 目标检测
                rectangles = vision_mineral.find(self.screenshot, 0.54)
                # 检测采矿图标
                mine_icon = vision_mine_icon.find(self.screenshot, 0.6)
                # 检测加载图标
                loading_icon = vision_loading_icon.find(self.screenshot, 0.6)

                rectangles = np.concatenate((rectangles, mine_icon, loading_icon))
                
                self.lock.acquire()
                self.rectangles = rectangles
                self.lock.release()

Vision类

class Vision:
    # 常量
    TRACKBAR_WINDOW = "Trackbars"

    # 属性
    img_tofind = None
    img_tofind_w = None
    img_tofind_h = None
    method = None

    def __init__(self, img_tofind_path, method=cv.TM_CCOEFF_NORMED):
        # 加载目标图像
        self.img_tofind = cv.imread(img_tofind_path)
        
        # 获取图像尺寸
        self.img_tofind_w = self.img_tofind.shape[1]
        self.img_tofind_h = self.img_tofind.shape[0]
        
        # 匹配方法可选6种:
        # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED
        method = cv.TM_CCOEFF_NORMED
        self.method = method
    
    def find(self, screen_img, threshold=0.5, max_results=10):
        # 运行OpenCV匹配算法
        result = cv.matchTemplate(screen_img, self.img_tofind, self.method)

        # 获取所有超过阈值的匹配位置
        locations = np.where(result >= threshold)
        locations = list(zip(*locations[::-1]))

        # 如果无匹配结果,返回特定形状的空数组,避免拼接报错
        if not locations:
            return np.array([], dtype=np.int32).reshape(0, 4)

        # 去除重叠矩形,使用groupRectangles()
        # 先创建[x, y, w, h]格式的矩形列表
        rectangles = []
        for loc in locations:
            rect = [int(loc[0]), int(loc[1]), self.img_tofind_w, self.img_tofind_h]
            # 每个矩形添加两次,保留非重叠的单个矩形
            rectangles.append(rect)
            rectangles.append(rect)
        # 应用矩形分组
        # groupThreshold通常设为1,设为0则不分组;设为2则需要至少3个重叠矩形才保留
        # eps是矩形合并的相对边长差异阈值,设为0.5
        rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5)
        
        return rectangles

    # 将find()返回的[x, y, w, h]矩形列表转换为矩形中心的[x, y]点击点列表
    def get_click_points(self, rectangles):
        points = []

        # 遍历所有矩形
        for (x, y, w, h) in rectangles:
            # 计算中心位置
            center_x = x + int(w/2)
            center_y = y + int(h/2)
            # 保存点
            points.append((center_x, center_y))

        return points

    # 在画布图像上绘制所有[x, y, w, h]矩形,返回绘制后的图像
    def draw_rectangles(self, screen_img, rectangles):
        # 颜色为BGR格式
        line_color = (0, 255, 0)
        line_type = cv.LINE_4

        for (x, y, w, h) in rectangles:
            # 确定矩形的左上角和右下角
            top_left = (x, y)
            bottom_right = (x + w, y + h)
            # 绘制矩形
            cv.rectangle(screen_img, top_left, bottom_right, line_color, lineType=line_type)

        return screen_img

    # 在画布图像上绘制所有[x, y]点击点为十字标记,返回绘制后的图像
    def draw_crosshairs(self, screen_img, points):
        # 颜色为BGR格式
        marker_color = (255, 0, 255)
        marker_type = cv.MARKER_CROSS

        for (center_x, center_y) in points:
            # 绘制中心点
            cv.drawMarker(screen_img, (center_x, center_y), marker_color, marker_type)

        return screen_img

解决方案

核心问题是主线程读取rectangles时未加锁,导致线程安全问题:线程更新rectangles时持有锁,但主线程直接读取可能在更新中途获取到不完整数据,或者因Python内存可见性问题无法感知到更新后的值。

修复步骤

  1. 给rectangles的读取添加锁保护,通过封装方法确保读写操作都在锁的控制下;
  2. 优化线程同步逻辑,避免线程空转浪费CPU资源。

修改后的Detector类代码

from threading import Lock, Thread, Event
import numpy as np

class Detection(object):
    stopped = True
    lock = None
    rectangles = []
    screenshot = None
    # 新增事件用于同步截图更新
    screenshot_updated = None
 
    def __init__(self):
        self.lock = Lock()
        self.screenshot_updated = Event()
    
    def update(self, screenshot):
        self.lock.acquire()
        self.screenshot = screenshot
        self.lock.release()
        # 通知线程有新截图可用
        self.screenshot_updated.set()

    def start(self):
        self.stopped = False
        t = Thread(target=self.run)
        print("Started")
        t.start()
     
    def stop(self):
        print("Stopped")
        self.stopped = True
        # 唤醒线程,避免阻塞在wait()
        self.screenshot_updated.set()
      
    # 新增带锁的获取方法
    def get_rectangles(self):
        self.lock.acquire()
        # 返回数组副本,避免外部操作影响内部数据
        res = self.rectangles.copy() if isinstance(self.rectangles, np.ndarray) else np.array([], dtype=np.int32).reshape(0,4)
        self.lock.release()
        return res
      
    def run(self):
        vision_mineral = Vision('C:/Users/Jordan/Desktop/Wakfu_Bot/Mining_bot/Resources/Copper.JPG')
        vision_mine_icon = Vision('C:/Users/Jordan/Desktop/Wakfu_Bot/Mining_bot/Resources/mineIcon.JPG')
        vision_loading_icon = Vision('C:/Users/Jordan/Desktop/Wakfu_Bot/Mining_bot/Resources/LoadingBar.JPG')
        
        while not self.stopped:
            # 等待新截图,避免空转
            self.screenshot_updated.wait()
            self.screenshot_updated.clear()
            
            self.lock.acquire()
            # 复制截图,避免主线程更新时冲突
            current_screenshot = self.screenshot.copy()
            self.lock.release()
            
            if current_screenshot is not None:
                print("updating image")
                rectangles = vision_mineral.find(current_screenshot, 0.54)
                mine_icon = vision_mine_icon.find(current_screenshot, 0.6)
                loading_icon = vision_loading_icon.find(current_screenshot, 0.6)

                rectangles = np.concatenate((rectangles, mine_icon, loading_icon))
                
                self.lock.acquire()
                self.rectangles = rectangles
                self.lock.release()

修改后的主方法代码

detect = detector.Detection()

detect.start()

while(True): 
    screenshot = wincap.get_screenshot()
    detect.update(screenshot) 
    # 使用带锁的方法获取结果
    detection_rects = detect.get_rectangles()
    detection_image = Vision.draw_rectangles(screenshot, detection_rects)

关键说明

  • 锁的双向保护:读写rectangles和screenshot都必须加锁,确保同一时间只有一个线程操作这些共享变量;
  • 数组副本返回:获取rectangles时返回副本,避免外部代码修改内部数据,同时减少锁持有时间;
  • 事件同步:用Event替代空循环等待,降低CPU占用,线程只有在有新截图时才会执行检测逻辑。

内容的提问来源于stack exchange,提问作者Plastic_Bat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 22:15:39