Extrair arte da imagem do cartão de jogo de mesa com OpenCV

10

Eu escrevi um pequeno script em python, onde estou tentando extrair ou cortar a parte do baralho que representa apenas a arte, removendo todo o resto. Eu tenho tentado vários métodos de limiar, mas não consegui chegar lá. Observe também que não posso simplesmente gravar manualmente a posição do trabalho artístico, porque ele nem sempre está na mesma posição ou tamanho, mas sempre em uma forma retangular, onde todo o resto é apenas texto e bordas.

insira a descrição da imagem aqui

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

A saída atual é a coisa mais próxima que pude obter. Eu poderia estar no caminho certo e tentar algumas discussões adicionais para desenhar um retângulo em torno das partes brancas, mas não acho que seja um método sustentável:

Saída atual

Como última observação, veja os cartões abaixo, nem todos os quadros têm exatamente os mesmos tamanhos ou posições, mas sempre há uma obra de arte com apenas texto e bordas ao redor. Não precisa ser super precisamente cortado, mas claramente a arte é uma "região" do cartão, cercada por outras regiões contendo algum texto. Meu objetivo é tentar capturar a região da obra de arte o melhor que puder.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Waroulolz
fonte
Que tipo de saída você espera do cartão "Narcomoeba"? Nem sequer tem um limite de forma regular. Além disso, não acho que exista uma solução sem a assistência do usuário.
Burak
O melhor que você pode fazer é clicar nos pontos delimitadores, aprimorar esses pontos combinando-os com o canto detectado mais próximo e descobrir a forma com base nas arestas entre os pontos. Eu ainda duvido que uma boa implementação desse algoritmo seja realizada na maioria das vezes. Ajustar o limite de detecção de arestas e dar dicas sobre a curvatura da linha entre os pontos (clique esquerdo: reto, clique direito: curvado, talvez?) Em tempo real pode aumentar a chance de sucesso.
Burak
11
Adicionei um exemplo melhor ao cartão Narcomoeba. Como você pode ver, estou interessado na região do trabalho artístico do cartão, não precisa ser 100% preciso. Na minha opinião, deve haver algumas transformações que me permitam dividir um cartão em diferentes "regiões", por assim dizer.
Waroulolz 25/03
Eu acho que você pode primeiro cortar imagens para 2 tipos (talvez 4 tipos? conforme informações fornecidas, a imagem será exibida no lado superior ou direito) e usar o opencv para verificar se há texto na imagem. Portanto, recorte -> filtro -> resultado -> borda de corte, se necessário, é mais fácil para o opencv obter melhores resultados.
elprup 25/03

Respostas:

3

Usei a transformação de linha Hough para detectar partes lineares da imagem. Os cruzamentos de todas as linhas foram usados ​​para construir todos os retângulos possíveis, que não contêm outros pontos de cruzamento. Como a parte do cartão que você está procurando é sempre o maior desses retângulos (pelo menos nas amostras que você forneceu), eu simplesmente escolhi o maior desses retângulos como vencedor. O script funciona sem a interação do usuário.

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

Estes são os resultados com as amostras que você forneceu:

Image1

Image2

Image3

O código para encontrar cruzamentos de linha pode ser encontrado aqui: encontre o ponto de interseção de duas linhas desenhadas usando linhas abreviadas opencv

Você pode ler mais sobre as linhas Hough aqui .

M. Martin
fonte
2
Obrigado pelo trabalho duro. Sua resposta é o que eu estava procurando. Eu sabia que a Hough Lines teria um grande papel aqui. Eu tentei algumas vezes usá-lo, mas não consegui chegar perto da sua solução. Como você comentou, alguns ajustes precisam ser feitos nos parâmetros para generalizar a abordagem, mas a lógica é ótima e poderosa.
Waroulolz 25/03
11
Eu acho que é uma ótima solução para esse tipo de problema, sem a necessidade de entrada do usuário. Bravo!!
Meto 26/03
@Meto - Agradeço o trabalho feito aqui, mas não concordo com a parte de entrada do usuário . É apenas um alias, se você digita em tempo de execução ou altera o limite depois de pesquisar o resultado.
Burak
11
@Burak - Consegui executar todas as amostras que foram fornecidas com as mesmas configurações, portanto, estou assumindo que a maioria das outras placas funcionaria também. Portanto, as configurações de limite precisam ser feitas apenas uma vez.
M. Martin
0

Sabemos que as cartas têm limites retos ao longo dos eixos x e y. Podemos usar isso para extrair partes da imagem. O código a seguir implementa a detecção de linhas horizontais e verticais na imagem.

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

Você só precisa clicar em duas áreas para incluir. Uma área de clique de amostra e o resultado correspondente são os seguintes:

linhas result_of_lines

Resultados de outras imagens:

result_2 result_3

Burak
fonte
0

Não acho que seja possível cortar automaticamente o ROI da arte finala usando técnicas tradicionais de processamento de imagem devido à natureza dinâmica das cores, dimensões, locais e texturas de cada cartão. Você precisaria examinar o aprendizado de máquina / profundo e treinar seu próprio classificador, se quiser fazê-lo automaticamente. Em vez disso, aqui está uma abordagem manual para selecionar e cortar um ROI estático de uma imagem.

A idéia é usar cv2.setMouseCallback()manipuladores de eventos para detectar se o mouse foi clicado ou liberado. Para esta implementação, você pode extrair o ROI do trabalho artístico mantendo pressionado o botão esquerdo do mouse e arrastando para selecionar o ROI desejado. Depois de selecionar o ROI desejado, pressione cpara cortar e salvar o ROI. Você pode redefinir o ROI usando o botão direito do mouse.

ROIs de obras de arte salvas

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
fonte