使用?Python?和?OpenCV?構建?SET?求解器
編輯:廣州人工智能解決方案_APP開發公司_小程序開發公司_歌莫信息 來源: 日期:2024-9-26 11:04:42 人氣: 標簽:
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.
相關新聞