Skip to content

opencv

更新: 6/26/2025 字数: 0 字 时长: 0 分钟


1. 基本用法

python
import cv2
import numpy as np

def show(img):
    ''' 显示一个图片 '''
    cv2.imshow('image', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def imread(filename):
    ''' 
    Like cv2.imread
    This function will make sure filename exists 
    '''
    im = cv2.imread(filename)
    if im is None:
        raise RuntimeError("file: '%s' not exists" % filename)
    return im

def find_template(im_source, im_search, threshold=0.5, rgb=False, bgremove=False):
    '''
    @return find location
    if not found; return None
    '''
    result = find_all_template(im_source, im_search, threshold, 1, rgb, bgremove)
    return result[0] if result else None

def find_all_template(im_source, im_search, threshold=0.5, maxcnt=0, rgb=False, bgremove=False):
    '''
    Locate image position with cv2.templateFind

    Use pixel match to find pictures.

    Args:
        im_source(string): 图像、素材
        im_search(string): 需要查找的图片
        threshold: 阈值,当相识度小于该阈值的时候,就忽略掉

    Returns:
        A tuple of found [(point, score), ...]

    Raises:
        IOError: when file read error
    '''
    # method = cv2.TM_CCORR_NORMED
    # method = cv2.TM_SQDIFF_NORMED
    method = cv2.TM_CCOEFF_NORMED

    if rgb:
        s_bgr = cv2.split(im_search) # Blue Green Red
        i_bgr = cv2.split(im_source)
        weight = (0.3, 0.3, 0.4)
        resbgr = [0, 0, 0]
        for i in range(3): # bgr
            resbgr[i] = cv2.matchTemplate(i_bgr[i], s_bgr[i], method)
        res = resbgr[0]*weight[0] + resbgr[1]*weight[1] + resbgr[2]*weight[2]
    else:
        s_gray = cv2.cvtColor(im_search, cv2.COLOR_BGR2GRAY)
        i_gray = cv2.cvtColor(im_source, cv2.COLOR_BGR2GRAY)
        # 边界提取(来实现背景去除的功能)
        if bgremove:
            s_gray = cv2.Canny(s_gray, 100, 200)
            i_gray = cv2.Canny(i_gray, 100, 200)

        res = cv2.matchTemplate(i_gray, s_gray, method)
    w, h = im_search.shape[1], im_search.shape[0]

    result = []
    while True:
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
        if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
            top_left = min_loc
        else:
            top_left = max_loc
        if DEBUG: 
            print('templmatch_value(thresh:%.1f) = %.3f' %(threshold, max_val)) # not show debug
        if max_val < threshold:
            break
        # calculator middle point
        middle_point = (top_left[0]+w/2, top_left[1]+h/2)
        result.append(dict(
            result=middle_point,
            rectangle=(top_left, (top_left[0], top_left[1] + h), (top_left[0] + w, top_left[1]), (top_left[0] + w, top_left[1] + h)),
            confidence=max_val
        ))
        if maxcnt and len(result) >= maxcnt:
            break
        # floodfill the already found area
        cv2.floodFill(res, None, max_loc, (-1000,), max_val-threshold+0.1, 1, flags=cv2.FLOODFILL_FIXED_RANGE)
    return result


def _sift_instance(edge_threshold=100):
    if hasattr(cv2, 'SIFT'):
        return cv2.SIFT(edgeThreshold=edge_threshold)
    return cv2.xfeatures2d.SIFT_create(edgeThreshold=edge_threshold)


def sift_count(img):
    sift = _sift_instance()
    kp, des = sift.detectAndCompute(img, None)
    return len(kp)

def find_sift(im_source, im_search, min_match_count=4):
    '''
    SIFT特征点匹配
    '''
    res = find_all_sift(im_source, im_search, min_match_count, maxcnt=1)
    if not res:
        return None
    return res[0]
    

FLANN_INDEX_KDTREE = 0

def find_all_sift(im_source, im_search, min_match_count=4, maxcnt=0):
    '''
    使用sift算法进行多个相同元素的查找
    Args:
        im_source(string): 图像、素材
        im_search(string): 需要查找的图片
        threshold: 阈值,当相识度小于该阈值的时候,就忽略掉
        maxcnt: 限制匹配的数量

    Returns:
        A tuple of found [(point, rectangle), ...]
        A tuple of found [{"point": point, "rectangle": rectangle, "confidence": 0.76}, ...]
        rectangle is a 4 points list
    '''
    sift = _sift_instance()
    flann = cv2.FlannBasedMatcher({'algorithm': FLANN_INDEX_KDTREE, 'trees': 5}, dict(checks=50))

    kp_sch, des_sch = sift.detectAndCompute(im_search, None)
    if len(kp_sch) < min_match_count:
        return None

    kp_src, des_src = sift.detectAndCompute(im_source, None)
    if len(kp_src) < min_match_count:
        return None

    h, w = im_search.shape[1:]

    result = []
    while True:
        # 匹配两个图片中的特征点,k=2表示每个特征点取2个最匹配的点
        matches = flann.knnMatch(des_sch, des_src, k=2)
        good = []
        for m, n in matches:
            # 剔除掉跟第二匹配太接近的特征点
            if m.distance < 0.9 * n.distance:
                good.append(m)

        if len(good) < min_match_count:
            break

        sch_pts = np.float32([kp_sch[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
        img_pts = np.float32([kp_src[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) 

        # M是转化矩阵
        M, mask = cv2.findHomography(sch_pts, img_pts, cv2.RANSAC, 5.0)
        matches_mask = mask.ravel().tolist()

        # 计算四个角矩阵变换后的坐标,也就是在大图中的坐标
        h, w = im_search.shape[:2]
        pts = np.float32([[0, 0], [0, h-1], [w-1, h-1], [w-1, 0]]).reshape(-1, 1, 2)
        dst = cv2.perspectiveTransform(pts, M)

        # trans numpy arrary to python list
        # [(a, b), (a1, b1), ...]
        pypts = []
        for npt in dst.astype(int).tolist():
            pypts.append(tuple(npt[0]))

        lt, br = pypts[0], pypts[2]
        middle_point = (lt[0] + br[0]) / 2, (lt[1] + br[1]) / 2

        result.append(dict(
            result=middle_point,
            rectangle=pypts,
            confidence=(matches_mask.count(1), len(good)) #min(1.0 * matches_mask.count(1) / 10, 1.0)
        ))

        if maxcnt and len(result) >= maxcnt:
            break
        
        # 从特征点中删掉那些已经匹配过的, 用于寻找多个目标
        qindexes, tindexes = [], []
        for m in good:
            qindexes.append(m.queryIdx) # need to remove from kp_sch
            tindexes.append(m.trainIdx) # need to remove from kp_img

        def filter_index(indexes, arr):
            r = np.ndarray(0, np.float32)
            for i, item in enumerate(arr):
                if i not in qindexes:
                    r = np.append(r, item)
            return r
        kp_src = filter_index(tindexes, kp_src)
        des_src = filter_index(tindexes, des_src)

    return result

def find_all(im_source, im_search, maxcnt=0):
    '''
    优先Template,之后Sift
    @ return [(x,y), ...]
    '''
    result = find_all_template(im_source, im_search, maxcnt=maxcnt)
    if not result:
        result = find_all_sift(im_source, im_search, maxcnt=maxcnt)
    if not result:
        return []
    return [match["result"] for match in result]

def find(im_source, im_search):
    '''
    Only find maximum one object
    '''
    r = find_all(im_source, im_search, maxcnt=1)
    return r[0] if r else None

def brightness(im):
    '''
    Return the brightness of an image
    Args:
        im(numpy): image

    Returns:
        float, average brightness of an image
    '''
    im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
    h, s, v = cv2.split(im_hsv) 
    height, weight = v.shape[:2]
    total_bright = 0
    for i in v:
        total_bright = total_bright+sum(i)
    return float(total_bright)/(height*weight)


def main():
    print(cv2.IMREAD_COLOR)
    print(cv2.IMREAD_GRAYSCALE)
    print(cv2.IMREAD_UNCHANGED)
    imsrc = imread('testdata/1s.png')
    imsch = imread('testdata/1t.png')
    print(brightness(imsrc))
    print(brightness(imsch))

    pt = find(imsrc, imsch)
    #mark_point(imsrc, pt)
    #show(imsrc)
    imsrc = imread('testdata/2s.png')
    imsch = imread('testdata/2t.png')
    result = find_all_template(imsrc, imsch)
    print(result)
    pts = []
    for match in result:
        pt = match["result"]
        #mark_point(imsrc, pt)
        pts.append(pt)
    # pts.sort()
    #show(imsrc)
    # print pts
    # print sorted(pts, key=lambda p: p[0])

    imsrc = imread('yl/bg_half.png')
    imsch = imread('yl/q_small.png')
    print(result)
    print('SIFT count=', sift_count(imsch))
    print(find_sift(imsrc, imsch))
    print(find_all_sift(imsrc, imsch))
    print(find_all_template(imsrc, imsch))
    print(find_all(imsrc, imsch))


if __name__ == '__main__':
    main()

2. 结合adb使用

python
"""ADB Auto Player Template Matching Module."""
 
from enum import StrEnum, auto
from pathlib import Path
from typing import NamedTuple
 
import cv2
import numpy as np
 
 
class MatchMode(StrEnum):
    """Match mode as a sting-based enum."""
 
    BEST = auto()
    TOP_LEFT = auto()
    TOP_RIGHT = auto()
    BOTTOM_LEFT = auto()
    BOTTOM_RIGHT = auto()
    LEFT_TOP = auto()
    LEFT_BOTTOM = auto()
    RIGHT_TOP = auto()
    RIGHT_BOTTOM = auto()
 
 
class CropRegions(NamedTuple):
    """Crop named tuple."""
 
    left: float = 0  # Percentage to crop from the left side
    right: float = 0  # Percentage to crop from the right side
    top: float = 0  # Percentage to crop from the top side
    bottom: float = 0  # Percentage to crop from the bottom side
 
 
template_cache: dict[str, np.ndarray] = {}
 
 
def load_image(
    image_path: Path,
    image_scale_factor: float = 1.0,
    grayscale: bool = False,
) -> np.ndarray:
    """Loads an image from disk or returns the cached version if available.
 
    Resizes the image if needed and stores it in the global template_cache.
 
    Args:
        image_path: Path to the template image.
        image_scale_factor: Scale factor for resizing the image.
        grayscale: Whether to convert the image to grayscale.
 
    Returns:
        np.ndarray
    """
    if image_path.suffix == "":
        image_path = image_path.with_suffix(".png")
 
    cache_key = f"{image_path}_{image_scale_factor}_grayscale={grayscale}"
    if cache_key in template_cache:
        return template_cache[cache_key]
 
    image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR)
    if image is None:
        raise FileNotFoundError(f"Failed to load image from path: {image_path}")
 
    if image_scale_factor != 1.0:
        new_width = int(image.shape[1] * image_scale_factor)
        new_height = int(image.shape[0] * image_scale_factor)
        image = cv2.resize(
            image, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4
        )
 
    if grayscale:
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
 
    template_cache[cache_key] = image
    return image
 
 
def crop_image(image: np.ndarray, crop: CropRegions) -> tuple[np.ndarray, int, int]:
    """Crop an image based on percentage values for each side.
 
    Args:
        image (np.ndarray): The input image to be cropped.
        crop (Crop): The crop percentage values for each edge.
 
    Returns:
        Cropped image.
        Number of pixels cropped from the left.
        Number of pixels cropped from the top.
 
    Raises:
        ValueError: If any crop percentage is negative,
            the sum of left and right crop percentages is 1.0 or greater
            or the sum of top and bottom crop percentages is 1.0 or greater.
            :rtype: tuple[np.ndarray, int, int]
    """
    if all(v == 0 for v in (crop.left, crop.right, crop.top, crop.bottom)):
        return image, 0, 0
 
    if any(v < 0 for v in (crop.left, crop.right, crop.top, crop.bottom)):
        raise ValueError("Crop percentages cannot be negative.")
    if crop.left + crop.right >= 1.0:
        raise ValueError("left + right must be less than 1.0.")
    if crop.top + crop.bottom >= 1.0:
        raise ValueError("top + bottom must be less than 1.0.")
 
    height, width = image.shape[:2]
    left_px = int(width * crop.left)
    right_px = int(width * (1 - crop.right))
    top_px = int(height * crop.top)
    bottom_px = int(height * (1 - crop.bottom))
 
    cropped_image = image[top_px:bottom_px, left_px:right_px]
    return cropped_image, left_px, top_px
 
 
def similar_image(
    base_image: np.ndarray,
    template_image: np.ndarray,
    threshold: float = 0.9,
    grayscale: bool = False,
) -> bool:
    """
    判断两个图像是否相似。
    通过比较模板图像与基础图像之间的匹配程度来判断两者是否相似。使用OpenCV库进行图像处理和匹配。
 
    :param base_image: 基础图像,作为背景进行匹配。
    :param template_image: 模板图像,将在基础图像中寻找相似的模式。
    :param threshold: 匹配度阈值,默认为0.9。匹配结果中的最大值必须大于等于此值才能认为图像相似。
    :param grayscale: 是否将图像转换为灰度图像进行处理,默认为False。灰度处理可以提高匹配的鲁棒性。
    :return: 如果模板图像与基础图像的某部分匹配度达到或超过阈值,则返回True,否则返回False。
    """
    base_cv, template_cv = _prepare_images_for_processing(
        base_image=base_image,
        template_image=template_image,
        threshold=threshold,
        grayscale=grayscale,
    )
 
    result = cv2.matchTemplate(base_cv, template_cv, method=cv2.TM_CCOEFF_NORMED)
    return np.max(result) >= threshold
 
 
def find_template_match(
    base_image: np.ndarray,
    template_image: np.ndarray,
    match_mode: MatchMode = MatchMode.BEST,
    threshold: float = 0.9,
    grayscale: bool = False,
) -> tuple[int, int] | None:
    """
    在基础图像中寻找模板图像的匹配位置。
 
    :param base_image: 基础图像数组。
    :param template_image:  模板图像数组。
    :param match_mode: 匹配模式,默认为最佳匹配。
    :param threshold: 匹配阈值,默认为0.9。
    :param grayscale: 是否转换图像为灰度,默认为False。
    :return: 匹配位置的中心坐标,如果没有找到匹配则返回None。
    """
 
    base_cv, template_cv = _prepare_images_for_processing(
        base_image=base_image,
        template_image=template_image,
        threshold=threshold,
        grayscale=grayscale,
    )
 
    result = cv2.matchTemplate(base_cv, template_cv, cv2.TM_CCOEFF_NORMED)
    if match_mode == MatchMode.BEST:
        _, max_val, _, max_loc = cv2.minMaxLoc(result)
        if max_val >= threshold:
            template_height, template_width = template_cv.shape[:2]
            center_x = max_loc[0] + template_width // 2
            center_y = max_loc[1] + template_height // 2
            return center_x, center_y
        return None
 
    match_locations = np.where(result >= threshold)
    if len(match_locations[0]) == 0:
        return None
 
    template_height, template_width = template_cv.shape[:2]
    matches = list(zip(match_locations[1], match_locations[0]))  # x, y coordinates
 
    key_functions = {
        MatchMode.TOP_LEFT: lambda loc: (loc[1], loc[0]),
        MatchMode.TOP_RIGHT: lambda loc: (loc[1], -loc[0]),
        MatchMode.BOTTOM_LEFT: lambda loc: (-loc[1], loc[0]),
        MatchMode.BOTTOM_RIGHT: lambda loc: (-loc[1], -loc[0]),
        MatchMode.LEFT_TOP: lambda loc: (loc[0], loc[1]),
        MatchMode.LEFT_BOTTOM: lambda loc: (loc[0], -loc[1]),
        MatchMode.RIGHT_TOP: lambda loc: (-loc[0], loc[1]),
        MatchMode.RIGHT_BOTTOM: lambda loc: (-loc[0], -loc[1]),
    }
 
    selected_match = min(matches, key=key_functions[match_mode])
    center_x = selected_match[0] + template_width // 2
    center_y = selected_match[1] + template_height // 2
 
    return center_x, center_y
 
 
def find_all_template_matches(
    base_image: np.ndarray,
    template_image: np.ndarray,
    threshold: float = 0.9,
    grayscale: bool = False,
    min_distance: int = 10,
) -> list[tuple[int, int]]:
    """
    在基础图像中查找所有与模板图像匹配的位置。
 
    :param base_image: 基础图像数组。
    :param template_image: 模板图像数组。
    :param threshold: 匹配度阈值,默认为0.9。
    :param grayscale: 是否转换图像为灰度,默认为False。
    :param min_distance: 最小匹配间距,默认为10。
    :return: 匹配位置的中心坐标列表。
    """
 
    base_cv, template_cv = _prepare_images_for_processing(
        base_image=base_image,
        template_image=template_image,
        threshold=threshold,
        grayscale=grayscale,
    )
 
    result = cv2.matchTemplate(base_cv, template_cv, cv2.TM_CCOEFF_NORMED)
    match_locations = np.where(result >= threshold)
 
    template_height, template_width = template_cv.shape[:2]
    centers = []
 
    for x, y in zip(match_locations[1], match_locations[0]):
        center_x = x + template_width // 2
        center_y = y + template_height // 2
        centers.append((center_x, center_y))
 
    if centers:
        centers = _suppress_close_matches(centers, min_distance)
 
    return centers
 
 
def find_worst_template_match(
    base_image: np.ndarray,
    template_image: np.ndarray,
    grayscale: bool = False,
) -> tuple[int, int] | None:
    """
 
    :param base_image: 基础图像数组。
    :param template_image: 模板图像数组。
    :param grayscale: 是否转换图像为灰度,默认为False。
    :return:
    """
    base_cv, template_cv = _prepare_images_for_processing(
        base_image=base_image, template_image=template_image, grayscale=grayscale
    )
 
    diff_map = cv2.matchTemplate(base_cv, template_cv, cv2.TM_SQDIFF)
 
    _, max_val, _, max_diff_loc = cv2.minMaxLoc(diff_map)
    min_difference_threshold = 10000
    if max_val < min_difference_threshold:
        return None
    max_diff_x, max_diff_y = max_diff_loc
 
    template_height, template_width = template_cv.shape[:2]
    center_x = max_diff_x + template_width // 2
    center_y = max_diff_y + template_height // 2
 
    return center_x, center_y
 
 
def _suppress_close_matches(
    matches: list[tuple[int, int]], min_distance: int
) -> list[tuple[int, int]]:
    """Suppresses closely spaced matches to return distinct results.
 
    Uses a simple clustering method based on minimum distance.
    """
    if not matches:
        return []
 
    matches_array = np.array(matches)
    suppressed: list[tuple[int, int]] = []  # type: ignore
    dimension = 2
 
    for match in matches_array:
        match_tuple = tuple(match)
        if len(match_tuple) == dimension and all(
            np.linalg.norm(match_tuple - np.array(s)) >= min_distance
            for s in suppressed
        ):
            suppressed.append(match_tuple)  # type: ignore
    return suppressed
 
 
def _validate_threshold(threshold: float) -> None:
    """Validate the threshold value.
 
    Raises:
        ValueError: If the threshold is less than 0 or greater than 1.
    """
    if threshold < 0.0 or threshold > 1.0:
        raise ValueError(f"Threshold must be between 0 and 1, got {threshold}")
 
 
def _validate_template_size(base_image: np.ndarray, template_image: np.ndarray) -> None:
    """Validate that the template image is smaller than the base image.
 
    Args:
        base_image: The base Image as ndarray
        template_image: The template Image as ndarray
 
    Raises:
        ValueError: If the template is larger than the base image in any dimension
    """
    base_height, base_width = base_image.shape[:2]
    template_height, template_width = template_image.shape[:2]
 
    if template_height > base_height or template_width > base_width:
        cv2.imwrite("debug/validate_template_size_base_image.png", base_image)
        cv2.imwrite("debug/validate_template_size_template_image.png", template_image)
        raise ValueError(
            f"Template must be smaller than the base image. "
            f"Base size: ({base_width}, {base_height}), "
            f"Template size: ({template_width}, {template_height})"
        )
 
 
def _prepare_images_for_processing(
    base_image, template_image, threshold=None, grayscale=True
):
    """Validates inputs and prepares images for template matching.
 
    Args:
        base_image (np.ndarray): The base image.
        template_image (np.ndarray): The template image.
        threshold (float, optional): Matching threshold to validate.
        grayscale (bool): Whether to convert images to grayscale.
 
    Returns:
        Tuple[np.ndarray, np.ndarray]: Prepared base and template images.
    """
    if threshold is not None:
        _validate_threshold(threshold)
 
    _validate_template_size(base_image=base_image, template_image=template_image)
 
    if grayscale:
        return convert_to_grayscale(base_image), convert_to_grayscale(template_image)
 
    return base_image, template_image
 
 
_NUM_COLORS_IN_RGB = 3
 
 
def convert_to_grayscale(image: np.ndarray) -> np.ndarray:
    """Convert np.ndarray to grayscale."""
    if len(image.shape) == _NUM_COLORS_IN_RGB:
        return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    return image

Released under the MIT License.