Extraire l'illustration de l'image de la carte de jeu de table avec OpenCV

10

J'ai écrit un petit script en python où j'essaie d'extraire ou de recadrer la partie de la carte à jouer qui représente uniquement l'illustration, en supprimant tout le reste. J'ai essayé différentes méthodes de seuillage mais je n'ai pas pu y arriver. Notez également que je ne peux pas simplement enregistrer manuellement la position de l'illustration car elle n'est pas toujours dans la même position ou taille, mais toujours dans une forme rectangulaire où tout le reste n'est que du texte et des bordures.

entrez la description de l'image ici

from matplotlib import pyplot as plt
import cv2

img = cv2.imread(filename)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

ret,binary = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY)

binary = cv2.bitwise_not(binary)
kernel = np.ones((15, 15), np.uint8)

closing = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

plt.imshow(closing),plt.show()

La sortie actuelle est la chose la plus proche que j'ai pu obtenir. Je pourrais être sur la bonne voie et essayer d'autres disputes pour dessiner un rectangle autour des parties blanches, mais je ne pense pas que ce soit une méthode durable:

Sortie courant

Comme dernière note, voir les cartes ci-dessous, tous les cadres n'ont pas exactement les mêmes tailles ou positions, mais il y a toujours une œuvre d'art avec uniquement du texte et des bordures autour. Il n'a pas besoin d'être coupé avec une précision extrême, mais il est clair que l'art est une "région" de la carte, entourée d'autres régions contenant du texte. Mon objectif est d'essayer de capturer la région de l'œuvre d'art du mieux que je peux.

entrez la description de l'image ici

entrez la description de l'image ici

Waroulolz
la source
Quel type de sortie attendez-vous de la carte "Narcomoeba"? Il n'a même pas de bordure de forme régulière. D'ailleurs, je ne pense pas qu'il existe de solution sans assistance utilisateur.
Burak
Le mieux que vous puissiez faire est de cliquer sur les points de délimitation, d'améliorer ces points en les faisant correspondre au coin détecté le plus proche, puis de découvrir la forme en fonction des bords entre les points. Je doute encore qu'une bonne implémentation de cet algorithme puisse accomplir la plupart du temps. Ajuster le seuil de détection des bords et donner un indice sur la courbure de la ligne entre les points (clic gauche: droit, clic droit: courbe, peut-être?) En temps réel peut augmenter les chances de succès.
Burak
1
J'ai ajouté un meilleur exemple à la carte Narcomoeba. Comme vous pouvez le voir, je m'intéresse à la zone d'illustration de la carte, elle n'a pas besoin d'être précise à 100%. À mon avis, il doit y avoir des transformations qui me permettent de diviser une carte en différentes «régions» pour ainsi dire.
Waroulolz
Je pense que vous pouvez d'abord recadrer les images en 2 types (peut-être 4 types? comme les informations fournies, l'image apparaîtra en haut ou à droite) et utiliser opencv pour vérifier s'il y a du texte dans l'image. Donc, recadrer -> filtrer -> résultat -> couper le bord si nécessaire est plus facile pour l'opencv de faire un meilleur résultat.
elprup

Réponses:

3

J'ai utilisé la transformation de ligne de Hough pour détecter des parties linéaires de l'image. Les croisements de toutes les lignes ont été utilisés pour construire tous les rectangles possibles, qui ne contiennent pas d'autres points de croisement. Étant donné que la partie de la carte que vous recherchez est toujours la plus grande de ces rectangles (au moins dans les échantillons que vous avez fournis), j'ai simplement choisi la plus grande de ces rectangles comme gagnante. Le script fonctionne sans interaction de l'utilisateur.

import cv2
import numpy as np
from collections import defaultdict

def segment_by_angle_kmeans(lines, k=2, **kwargs):
    #Groups lines based on angle with k-means.
    #Uses k-means on the coordinates of the angle on the unit circle 
    #to segment `k` angles inside `lines`.

    # Define criteria = (type, max_iter, epsilon)
    default_criteria_type = cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER
    criteria = kwargs.get('criteria', (default_criteria_type, 10, 1.0))
    flags = kwargs.get('flags', cv2.KMEANS_RANDOM_CENTERS)
    attempts = kwargs.get('attempts', 10)

    # returns angles in [0, pi] in radians
    angles = np.array([line[0][1] for line in lines])
    # multiply the angles by two and find coordinates of that angle
    pts = np.array([[np.cos(2*angle), np.sin(2*angle)]
                    for angle in angles], dtype=np.float32)

    # run kmeans on the coords
    labels, centers = cv2.kmeans(pts, k, None, criteria, attempts, flags)[1:]
    labels = labels.reshape(-1)  # transpose to row vec

    # segment lines based on their kmeans label
    segmented = defaultdict(list)
    for i, line in zip(range(len(lines)), lines):
        segmented[labels[i]].append(line)
    segmented = list(segmented.values())
    return segmented

def intersection(line1, line2):
    #Finds the intersection of two lines given in Hesse normal form.
    #Returns closest integer pixel locations.
    #See https://stackoverflow.com/a/383527/5087436

    rho1, theta1 = line1[0]
    rho2, theta2 = line2[0]

    A = np.array([
        [np.cos(theta1), np.sin(theta1)],
        [np.cos(theta2), np.sin(theta2)]
    ])
    b = np.array([[rho1], [rho2]])
    x0, y0 = np.linalg.solve(A, b)
    x0, y0 = int(np.round(x0)), int(np.round(y0))
    return [[x0, y0]]


def segmented_intersections(lines):
    #Finds the intersections between groups of lines.

    intersections = []
    for i, group in enumerate(lines[:-1]):
        for next_group in lines[i+1:]:
            for line1 in group:
                for line2 in next_group:
                    intersections.append(intersection(line1, line2)) 
    return intersections

def rect_from_crossings(crossings):
    #find all rectangles without other points inside
    rectangles = []

    # Search all possible rectangles
    for i in range(len(crossings)):
        x1= int(crossings[i][0][0])
        y1= int(crossings[i][0][1])

        for j in range(len(crossings)):
            x2= int(crossings[j][0][0])
            y2= int(crossings[j][0][1])

            #Search all points
            flag = 1
            for k in range(len(crossings)):
                x3= int(crossings[k][0][0])
                y3= int(crossings[k][0][1])

                #Dont count double (reverse rectangles)
                if (x1 > x2 or y1 > y2):
                    flag = 0
                #Dont count rectangles with points inside   
                elif ((((x3 >= x1) and (x2 >= x3))and (y3 > y1) and (y2 > y3) or ((x3 > x1) and (x2 > x3))and (y3 >= y1) and (y2 >= y3))):    
                    if(i!=k and j!=k):    
                        flag = 0

            if flag:
                rectangles.append([[x1,y1],[x2,y2]])

    return rectangles

if __name__ == '__main__':
    #img = cv2.imread('TAJFp.jpg')
    #img = cv2.imread('Bj2uu.jpg')
    img = cv2.imread('yi8db.png')

    width = int(img.shape[1])
    height = int(img.shape[0])

    scale = 380/width
    dim = (int(width*scale), int(height*scale))
    # resize image
    img = cv2.resize(img, dim, interpolation = cv2.INTER_AREA) 

    img2 = img.copy()
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray,(5,5),cv2.BORDER_DEFAULT)

    # Parameters of Canny and Hough may have to be tweaked to work for as many cards as possible
    edges = cv2.Canny(gray,10,45,apertureSize = 7)
    lines = cv2.HoughLines(edges,1,np.pi/90,160)

    segmented = segment_by_angle_kmeans(lines)
    crossings = segmented_intersections(segmented)
    rectangles = rect_from_crossings(crossings)

    #Find biggest remaining rectangle
    size = 0
    for i in range(len(rectangles)):
        x1 = rectangles[i][0][0]
        x2 = rectangles[i][1][0]
        y1 = rectangles[i][0][1]
        y2 = rectangles[i][1][1]

        if(size < (abs(x1-x2)*abs(y1-y2))):
            size = abs(x1-x2)*abs(y1-y2)
            x1_rect = x1
            x2_rect = x2
            y1_rect = y1
            y2_rect = y2

    cv2.rectangle(img2, (x1_rect,y1_rect), (x2_rect,y2_rect), (0,0,255), 2)
    roi = img[y1_rect:y2_rect, x1_rect:x2_rect]

    cv2.imshow("Output",roi)
    cv2.imwrite("Output.png", roi)
    cv2.waitKey()

Voici les résultats avec les échantillons que vous avez fournis:

Image1

Image2

Image3

Le code pour trouver les croisements de lignes peut être trouvé ici: trouver le point d'intersection de deux lignes tracées à l'aide de lignes de repère opencv

Vous pouvez en savoir plus sur Hough Lines ici .

M. Martin
la source
2
Merci pour le dur travail. Votre réponse est ce que je cherchais. Je savais que Hough Lines jouerait un grand rôle ici. Je me suis essayé plusieurs fois pour l'utiliser, mais je n'ai pas pu me rapprocher de votre solution. Comme vous l'avez commenté, quelques ajustements doivent être effectués sur les paramètres pour généraliser l'approche mais la logique est grande et puissante.
Waroulolz
1
Je pense que c'est une excellente solution pour ce type de problème, aucune intervention de l'utilisateur n'est nécessaire. Bravo!!
Meto
@Meto - J'apprécie le travail accompli ici, mais je ne suis pas d'accord avec la partie sans entrée d'utilisateur . C'est juste un alias si vous saisissez au moment de l'exécution ou modifiez le seuil après avoir recherché le résultat.
Burak
1
@Burak - J'ai pu exécuter tous les échantillons fournis avec les mêmes paramètres, je suppose donc que la plupart des autres cartes fonctionneraient également. Par conséquent, les paramètres de verrouillage ne doivent être définis qu'une seule fois.
M. Martin
0

Nous savons que les cartes ont des limites droites le long des axes x et y. Nous pouvons l'utiliser pour extraire des parties de l'image. Le code suivant implémente la détection des lignes horizontales et verticales dans l'image.

import cv2
import numpy as np

def mouse_callback(event, x, y, flags, params):
    global num_click
    if num_click < 2 and event == cv2.EVENT_LBUTTONDOWN:
        num_click = num_click + 1
        print(num_click)
        global upper_bound, lower_bound, left_bound, right_bound
        upper_bound.append(max(i for i in hor if i < y) + 1)
        lower_bound.append(min(i for i in hor if i > y) - 1)
        left_bound.append(max(i for i in ver if i < x) + 1)
        right_bound.append(min(i for i in ver if i > x) - 1)

filename = 'image.png'
thr = 100  # edge detection threshold
lined = 50  # number of consequtive True pixels required an axis to be counted as line
num_click = 0  # select only twice
upper_bound, lower_bound, left_bound, right_bound = [], [], [], []
winname = 'img'

cv2.namedWindow(winname)
cv2.setMouseCallback(winname, mouse_callback)

img = cv2.imread(filename, 1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
bw = cv2.Canny(gray, thr, 3*thr)

height, width, _ = img.shape

# find horizontal lines
hor = []
for i in range (0, height-1):
    count = 0
    for j in range (0, width-1):
        if bw[i,j]:
            count = count + 1
        else:
            count = 0
        if count >= lined:
            hor.append(i)
            break

# find vertical lines
ver = []
for j in range (0, width-1):
    count = 0
    for i in range (0, height-1):
        if bw[i,j]:
            count = count + 1
        else:
            count = 0
        if count >= lined:
            ver.append(j)
            break

# draw lines
disp_img = np.copy(img)
for i in hor:
    cv2.line(disp_img, (0, i), (width-1, i), (0,0,255), 1)
for i in ver:
    cv2.line(disp_img, (i, 0), (i, height-1), (0,0,255), 1)

while num_click < 2:
    cv2.imshow(winname, disp_img)
    cv2.waitKey(10)
disp_img = img[min(upper_bound):max(lower_bound), min(left_bound):max(right_bound)]
cv2.imshow(winname, disp_img)
cv2.waitKey()   # Press any key to exit
cv2.destroyAllWindows()

Il vous suffit de cliquer sur deux zones à inclure. Un exemple de zone de clic et le résultat correspondant sont les suivants:

lignes result_of_lines

Résultats d'autres images:

result_2 result_3

Burak
la source
0

Je ne pense pas qu'il soit possible de recadrer automatiquement le ROI de l'illustration en utilisant des techniques de traitement d'image traditionnelles en raison de la nature dynamique des couleurs, des dimensions, des emplacements et des textures de chaque carte. Vous devrez vous pencher sur la machine / l'apprentissage en profondeur et former votre propre classificateur si vous voulez le faire automatiquement. Au lieu de cela, voici une approche manuelle pour sélectionner et recadrer un retour sur investissement statique à partir d'une image.

L'idée est d'utiliser cv2.setMouseCallback()et des gestionnaires d'événements pour détecter si la souris a été cliquée ou relâchée. Pour cette implémentation, vous pouvez extraire le ROI de l'illustration en maintenant le bouton gauche de la souris enfoncé et en faisant glisser pour sélectionner le ROI souhaité. Une fois que vous avez sélectionné le retour sur investissement souhaité, appuyez sur cpour recadrer et enregistrer le retour sur investissement. Vous pouvez réinitialiser le retour sur investissement en utilisant le bouton droit de la souris.

Retour sur investissement des illustrations enregistrées

Code

import cv2

class ExtractArtworkROI(object):
    def __init__(self):
        # Load image
        self.original_image = cv2.imread('1.png')
        self.clone = self.original_image.copy()
        cv2.namedWindow('image')
        cv2.setMouseCallback('image', self.extractROI)
        self.selected_ROI = False

        # ROI bounding box reference points
        self.image_coordinates = []

    def extractROI(self, event, x, y, flags, parameters):
        # Record starting (x,y) coordinates on left mouse button click
        if event == cv2.EVENT_LBUTTONDOWN:
            self.image_coordinates = [(x,y)]

        # Record ending (x,y) coordintes on left mouse button release
        elif event == cv2.EVENT_LBUTTONUP:
            # Remove old bounding box
            if self.selected_ROI:
                self.clone = self.original_image.copy()

            # Draw rectangle 
            self.selected_ROI = True
            self.image_coordinates.append((x,y))
            cv2.rectangle(self.clone, self.image_coordinates[0], self.image_coordinates[1], (36,255,12), 2)

            print('top left: {}, bottom right: {}'.format(self.image_coordinates[0], self.image_coordinates[1]))
            print('x,y,w,h : ({}, {}, {}, {})'.format(self.image_coordinates[0][0], self.image_coordinates[0][1], self.image_coordinates[1][0] - self.image_coordinates[0][0], self.image_coordinates[1][1] - self.image_coordinates[0][1]))

        # Clear drawing boxes on right mouse button click
        elif event == cv2.EVENT_RBUTTONDOWN:
            self.selected_ROI = False
            self.clone = self.original_image.copy()

    def show_image(self):
        return self.clone

    def crop_ROI(self):
        if self.selected_ROI:
            x1 = self.image_coordinates[0][0]
            y1 = self.image_coordinates[0][1]
            x2 = self.image_coordinates[1][0]
            y2 = self.image_coordinates[1][1]

            # Extract ROI
            self.cropped_image = self.original_image.copy()[y1:y2, x1:x2]

            # Display and save image
            cv2.imshow('Cropped Image', self.cropped_image)
            cv2.imwrite('ROI.png', self.cropped_image)
        else:
            print('Select ROI before cropping!')

if __name__ == '__main__':
    extractArtworkROI = ExtractArtworkROI()
    while True:
        cv2.imshow('image', extractArtworkROI.show_image())
        key = cv2.waitKey(1)

        # Close program with keyboard 'q'
        if key == ord('q'):
            cv2.destroyAllWindows()
            exit(1)

        # Crop ROI
        if key == ord('c'):
            extractArtworkROI.crop_ROI()
nathancy
la source