Extraiga ilustraciones de la imagen de la tarjeta de juego de mesa con OpenCV

10

Escribí un pequeño script en python donde trato de extraer o recortar la parte de la tarjeta de juego que representa solo la obra de arte, eliminando todo el resto. He estado probando varios métodos de umbralización pero no pude llegar allí. También tenga en cuenta que no puedo simplemente grabar manualmente la posición de la obra de arte porque no siempre está en la misma posición o tamaño, sino siempre en una forma rectangular donde todo lo demás es solo texto y bordes.

ingrese la descripción de la imagen aquí

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 salida actual es lo más parecido que podría obtener. Podría estar en el camino correcto e intentar un poco más de discusión para dibujar un rectángulo alrededor de las partes blancas, pero no creo que sea un método sostenible:

Salida de corriente

Como última nota, vea las tarjetas a continuación, no todos los marcos son exactamente del mismo tamaño o posición, pero siempre hay una obra de arte con solo texto y bordes a su alrededor. No tiene que cortarse de forma súper precisa, pero claramente el arte es una "región" de la tarjeta, rodeada de otras regiones que contienen texto. Mi objetivo es tratar de capturar la región de la obra de arte lo mejor que pueda.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Waroulolz
fuente
¿Qué tipo de salida esperas de la tarjeta "Narcomoeba"? Ni siquiera tiene un límite de forma regular. Además, no creo que haya una solución sin asistencia del usuario.
Burak
Lo mejor que puede hacer es hacer clic en los puntos delimitadores, mejorar esos puntos haciendo coincidirlos con la esquina detectada más cercana, luego encontrar la forma en función de los bordes entre los puntos. Todavía dudo que una buena implementación de este algoritmo se logre la mayoría de las veces. Ajustar el umbral de detección de bordes y dar una pista sobre la curvatura de la línea entre puntos (clic izquierdo: derecho, clic derecho: ¿curvado, tal vez?) En tiempo real puede aumentar las posibilidades de éxito.
Burak
1
Agregué un mejor ejemplo a la tarjeta Narcomoeba. Como puede ver, estoy interesado en la región de ilustraciones de la tarjeta, no tiene que ser 100% precisa. En mi opinión, debe haber algunas transformaciones que me permitan dividir una tarjeta en diferentes 'regiones', por así decirlo.
Waroulolz
Creo que primero puede recortar imágenes a 2 tipos (¿tal vez 4 tipos? como se proporciona información, la imagen se mostrará en la parte superior o derecha) y usar opencv para verificar si tiene texto en la imagen. Por lo tanto, recortar -> filtro -> resultado -> cortar el borde si es necesario es más fácil para que opencv obtenga un mejor resultado.
elprup

Respuestas:

3

Utilicé la transformación de línea de Hough para detectar partes lineales de la imagen. Los cruces de todas las líneas se usaron para construir todos los rectángulos posibles, que no contienen otros puntos de cruce. Dado que la parte de la tarjeta que está buscando siempre es el más grande de esos rectángulos (al menos en las muestras que proporcionó), simplemente elegí el más grande de esos rectángulos como ganador. El script funciona sin interacción del usuario.

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()

Estos son los resultados con las muestras que proporcionó:

Imagen1

Imagen2

Imagen3

El código para encontrar cruces de línea se puede encontrar aquí: encuentre el punto de intersección de dos líneas dibujadas usando líneas de corte opencv

Puede leer más sobre Hough Lines aquí .

M. Martin
fuente
2
Gracias por el arduo trabajo. Tu respuesta es lo que estaba buscando. Sabía que Hough Lines jugaría un gran papel aquí. Intenté usarlo un par de veces, pero no pude acercarme a su solución. Como comentaste, hay que hacer algunos ajustes en los parámetros para generalizar el enfoque, pero la lógica es excelente y poderosa.
Waroulolz
1
Creo que es una gran solución para este tipo de problema, no se necesita la intervención del usuario. ¡¡Bravo!!
Meto
@Meto: agradezco el trabajo realizado aquí, pero no estoy de acuerdo con la parte de no entrada de usuario . Es solo un alias si ingresa en tiempo de ejecución o cambia el umbral después de buscar el resultado.
Burak
1
@Burak: pude ejecutar todas las muestras que se suministraron con la misma configuración, así que supongo que la mayoría de las otras tarjetas funcionarían también. Por lo tanto, la configuración de theshold solo debe hacerse una vez.
M. Martin
0

Sabemos que las tarjetas tienen límites rectos a lo largo de los ejes x e y. Podemos usar esto para extraer partes de la imagen. El siguiente código implementa la detección de líneas horizontales y verticales en la imagen.

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()

Solo necesita hacer clic en dos áreas para incluir. Un área de clic de muestra y el resultado correspondiente son los siguientes:

líneas resultado_de_líneas

Resultados de otras imágenes:

resultado_2 resultado_3

Burak
fuente
0

No creo que sea posible recortar automáticamente el ROI de la obra de arte utilizando técnicas de procesamiento de imágenes tradicionales debido a la naturaleza dinámica de los colores, dimensiones, ubicaciones y texturas para cada tarjeta. Tendría que buscar en la máquina / aprendizaje profundo y entrenar a su propio clasificador si desea hacerlo automáticamente. En cambio, aquí hay un enfoque manual para seleccionar y recortar un ROI estático de una imagen.

La idea es usar cv2.setMouseCallback()controladores de eventos para detectar si se ha hecho clic o liberado el mouse. Para esta implementación, puede extraer el ROI de la ilustración manteniendo presionado el botón izquierdo del mouse y arrastrándolo para seleccionar el ROI deseado. Una vez que haya seleccionado el ROI deseado, presione cpara recortar y guardar el ROI. Puede restablecer el ROI con el botón derecho del mouse.

ROI de obras de arte guardadas

Código

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
fuente