使用?Python?和?OpenCV?构建?SET?求解器
set 是一种游戏,玩家在指定的时间竞相识别出十二张独特纸牌中的三张纸牌(或 set)的模式。每张 set 卡都有四个属性:形状、阴影/填充、颜色和计数。下面是一个带有一些卡片描述的十二张卡片布局示例。带有一些卡片描述的标准十二张卡片布局请注意,卡片的四个属性中的每一个都可以通过三个变体之一来表达。因为没有两张牌是重复的,所以一副套牌包含 3? = 81 张牌(每个属性 3 个变体,4 个属性)。一个有效的 set 由三张卡片组成,对于四个属性中的每一个,要么全部共享相同的变量,要么都具有不同的变量。为了直观地演示,以下是三个有效 set 示例:(1) 形状:全部不同 (2) 阴影:全部不同 (3) 颜色:全部不同 (4) 计数:全部相同(1) 形状:全部不同 (2) 阴影:全部相同 (3) 颜色:全部不同 (4) 计数:全部相同(1) 形状:全部相同 (2) 阴影:全部不同 (3) 颜色:全部相同 (4) 计数:全部不同构建一个 set 求解器:一个计算机程序,该程序获取 set 卡的图像并返回所有有效的 set,我们使用 opencv(一个开源计算机视觉库)和 python。为了使自己熟悉,我们可以浏览图书馆的文档并和观看一系列教程。此外,我们还可以阅读一些类似项目的博客文章和 github 存储库。我们将项目分解为四项任务: 在输入图像中定位卡片 (cardextractor.py) 识别每张卡片的唯一属性 (card.py) 评估已识别的 set 卡 (setevaluator.py) 向用户显示 set (set_utils.display_sets) 我们为前三个任务中的每一个创建了一个专用类,我们可以在下面的类型提示 main 方法中看到。
在输入图像中定位卡片
识别卡片属性作为第一步,一种名为process_card的静态方法应用了上述相同的预处理技术,以及对重构后的卡片图像进行二进制膨胀和腐蚀。简要说明和示例:
- import cv2
- # main method takes path to input image of cards and displays sets
- def main():
- input_image = 'path_to_image'
- original_image = cv2.imread(input_image)
- extractor: cardextractor = cardextractor(original_image)
- cards: list[card] = extractor.get_cards()
- evaluator: setevaluator = setevaluator(cards)
- sets: list[list[card]] = evaluator.get_sets()
- display_sets(sets, original_image)
- cv2.destroyallwindows()
1. 图像预处理
在导入opencv和numpy(开源数组和矩阵操作库)之后,定位卡片的第一步是应用图像预处理技术来突出卡片的边界。具体来说,这种方法涉及将图像转换为灰度,应用高斯模糊并对图像进行阈值处理。简要地:- 转换为灰度可通过仅保留每个像素的强度或亮度(rgb 色彩通道的加权总和)来消除图像的着色。
- 对图像应用高斯模糊会将每个像素的强度值转换为该像素邻域的加权平均值,权重由以当前像素为中心的高斯分布确定。这样可以消除噪声并 “平滑” 图像。经过实验后,我们决定高斯核大小设定 (3,3) 。
- 阈值化将灰度图像转换为二值图像——一种新矩阵,其中每个像素具有两个值(通常是黑色或白色)之一。为此,使用恒定值阈值来分割像素。因为我们预计输入图像具有不同的光照条件,所以我们使用 cv2.thresh_otsu 标志来估计运行时的最佳阈值常数。
- # convert input image to greyscale, blurs, and thresholds using otsu's binarization
- def preprocess_image(image):
- greyscale_image = cv2.cvtcolor(image, cv2.color_bgr2gray)
- blurred_image = cv2.gaussianblur(greyscale_image, (3, 3), 0)
- _, thresh = cv2.threshold(blurred_image, 0, 255, cv2.thresh_otsu)
- return thresh 原始 → 灰度和模糊 → 阈
2. 查找卡片轮廓
接下来,我使用 opencv 的 findcontours() 和 approxpolydp() 方法来定位卡片。利用图像的二进制值属性,findcontours() 方法可以找到 “ 连接所有具有相同颜色或强度的连续点(沿边界)的曲线。”2 第一步是对预处理图像使用以下函数调用:- contours, hierarchy = cv2.findcontours(processed_image, cv2.retr_tree, cv2.chain_approx_simple)
3. 重构卡片图像
识别轮廓后,必须重构卡片的边界以标准化原始图像中卡片的角度和方向。这可以通过仿射扭曲变换来完成,仿射扭曲变换是一种几何变换,可以保留图像上线条之间的共线点和平行度。我们可以在示例图像中看到下面的代码片段。- # performs an affine transformation and crop to a set of card vertices
- def refactor_card(self, bounding_box, width, height):
- bounding_box = cv2.umat(np.array(bounding_box, dtype=np.float32))
- frame = [[449, 449], [0, 449], [0, 0], [449, 0]]
- if height > width:
- frame = [[0, 0], [0, 449], [449, 449], [449, 0]]
- affine_frame = np.array(frame, np.float32)
- affine_transform = cv2.getperspectivetransform(bounding_box, affine_frame)
- refactored_card = cv2.warpperspective(self.original_image, affine_transform, (450, 450))
- cropped_card = refactored_card[15:435, 15:435]
- return cropped_card
- class card:
- def __init__(self, card_image, original_coord):
- self.image = card_image
- self.processed_image = self.process_card(card_image)
- self.processed_contours = self.processed_contours()
- self.original_coord = reorient_coordinates(original_coord) #standardize coordinate orientation
- self.count = self.get_count()
- self.shape = self.get_shape()
- self.shade = self.get_shade()
- self.color = self.get_color()
- 膨胀是其中像素 p 的值变成像素 p 的 “邻域” 中最大像素的值的操作。腐蚀则相反:像素 p 的值变成像素 p 的 “邻域” 中最小像素的值。
- 该邻域的大小和形状(或“内核”)可以作为输入传递给 opencv(默认为 3x3 方阵)。
- 对于二值图像,腐蚀和膨胀的组合(也称为打开和关闭)用于通过消除落在相关像素 “范围” 之外的任何像素来去除噪声。在下面的例子中可以看到这一点。
- #close card image (dilate then erode)
- dilated_card = cv2.dilate(binary_card, kernel=(x,x), iterations=y)
- eroded_card = cv2.erode(dilated_card, kernel=(x,x), iterations=y)
形状
- 为了识别卡片上显示的符号的形状,我们使用卡片最大轮廓的面积。这种方法假设最大的轮廓是卡片上的一个符号——这一假设在排除非极端照明的情况下几乎总是正确的。
阴影
- 识别卡片阴影或 “填充” 的方法使用卡片最大轮廓内的像素密度。
颜色
- 识别卡片颜色的方法包括评估三个颜色通道 (rgb) 的值并比较它们的比率。
计数
- 为了识别卡片上的符号数量,我们首先找到了四个最大的轮廓。尽管实际上计数从未超过三个,但我们选择了四个,然后进行了错误检查以排除非符号。在使用 cv2.drawcontours 填充轮廓后,为了避免重复计算后,我们需要检查一下轮廓区域的值以及层次结构(以确保轮廓没有嵌入到另一个轮廓中)。
方法一:所有可能的组合
至少有两种方法可以评估卡的数组表示形式是否为有效集。第一种方法需要评估所有可能的三张牌组合。例如,当显示 12 张牌时,有 ??c? =(12!)/(9!)(3!) = 660 种可能的三张牌组合。使用 python 的 itertools 模块,可以按如下方式计算- import itertools set_combinations = list(combinations(cards: list[card], 3))
- # takes 3 card objects and returns boolean: true if set, false if not set
- @staticmethod
- def is_set(trio):
- count_sum = sum([card.count for card in trio])
- shape_sum = sum([card.shape for card in trio])
- shade_sum = sum([card.shade for card in trio])
- color_sum = sum([card.color for card in trio])
- set_values_mod3 = [count_sum % 3, shape_sum % 3, shade_sum % 3, color_sum % 3]
- return set_values_mod3 == [0, 0, 0, 0]
方法 2:验证 set key
请注意,对于一副牌中的任意两张牌,只有一张牌(并且只有一张牌)可以完成 set,我们称这第三张卡为set key。方法 1 的一种更有效的替代方法是迭代地选择两张卡片,计算它们的 set 密钥,并检查该密钥是否出现在剩余的卡片中。在 python 中检查 set() 结构的成员资格的平均时间复杂度为 o (1)。这将算法的时间复杂度降低到 o( n2),因为它减少了需要评估的组合数量。考虑到只有少量 n 次输入的事实(在游戏中有12 张牌在场的 set 有 96.77% 的机会,15 张牌有 99.96% 的机会,16 张牌有 99.9996% 的机会?),效率并不是最重要的。使用第一种方法,我在我的中端笔记本电脑上对程序计时,发现它在我的测试输入上平均运行 1.156 秒(渲染最终图像)和 1.089 秒(不渲染)。在一个案例中,程序在 1.146 秒内识别出七个独立的集合。向用户显示 sets
最后,我们跟随 piratefsh 和 nicolas hahn 的引导,通过在原始图像上用独特的颜色圈出各自 set 的卡片,向用户展示 set。我们将每张卡片的原始坐标列表存储为一个实例变量,该变量用于绘制彩色轮廓。- # takes list[list[card]] and original image. draws colored bounding boxes around sets.
- def display_sets(sets, image, wait_key=true):
- for index, set_ in enumerate(sets):
- set_card_boxes = set_outline_colors.pop()
- for card in set_:
- card.boundary_count = 1
- expanded_coordinates = np.array(expand_coordinates(card.original_coord, card.boundary_count), dtype=np.int64)
- cv2.drawcontours(image, [expanded_coordinates], 0, set_card_boxes, 20)
属于多个 set 的卡片需要多个轮廓。为了避免在彼此之上绘制轮廓,expanded_coordinates() 方法根据卡片出现的 set 数量迭代扩展卡片的轮廓。这是使用 cv2.imshow() 的操作结果: 就是这样——一个使用 python 和 opencv 的 set 求解器!这个项目很好地介绍了 opencv 和计算机视觉基础知识。特别是,我们了解到:
- 图像处理、降噪和标准化技术,如高斯模糊、仿射变换和形态学运算。
- otsu 的自动二元阈值方法。
- 轮廓和 canny 边缘检测。
- opencv 库及其一些用途。
- piratefsh’s set-solver on github was particularly informative. after finding that her approach to color identification very accurate, i ended up simply copying the method. arnab nandi’s card game identification project was also a useful starting point, and nicolas hahn’s set-solver also proved useful. thank you sherr, arnab, and nicolas, if you are reading this!
- here’s a basic explanation of contours and how they work in opencv. i initially implement the program with canny edge detection, but subsequently removed it because it did not improve card identification accuracy for test cases.
- you can find a more detailed description of morphological transformations on the opencv site here.
- some interesting probabilities related to the game set.
相关新闻