Извлечение иллюстраций из изображения настольной игровой карты с помощью OpenCV

10

Я написал небольшой скрипт на python, где я пытаюсь извлечь или обрезать ту часть игральной карты, которая представляет только художественные работы, удаляя все остальное. Я пробовал различные методы пороговых значений, но не смог туда добраться. Также обратите внимание, что я не могу просто записать вручную положение обложки, потому что оно не всегда находится в одинаковом положении или размере, но всегда в прямоугольной форме, где все остальное - просто текст и границы.

введите описание изображения здесь

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

Токовый выход - самая близкая вещь, которую я мог получить. Я мог бы быть на правильном пути и попытаться еще немного спорить, чтобы нарисовать прямоугольник вокруг белых частей, но я не думаю, что это устойчивый метод:

Токовый выход

Как последнее примечание, смотрите карты ниже, не все рамки имеют одинаковые размеры или позиции, но всегда есть произведение искусства с только текстом и рамками вокруг него. Это не должно быть супер точно вырезано, но ясно, что искусство - это «область» карты, окруженная другими областями, содержащими некоторый текст. Моя цель - постараться как можно лучше охватить область искусства.

введите описание изображения здесь

введите описание изображения здесь

Waroulolz
источник
Какой выход ты ждешь с карты "Наркомоэба"? У него даже нет границы правильной формы. Кроме того, я не думаю, что есть решение без помощи пользователя.
Бурак
Лучшее, что вы можете сделать, - это щелкнуть ограничивающие точки, улучшить их, сопоставив их с ближайшим обнаруженным углом, а затем выяснить форму на основе ребер между точками. Я все еще сомневаюсь, что хорошая реализация этого алгоритма будет успешной в большинстве случаев. Регулировка порога обнаружения края и подсказка о кривизне линии между точками (щелчок левой кнопкой мыши: прямой, щелчок правой кнопкой мыши: изогнутый, может быть?) В реальном времени может увеличить вероятность успеха.
Бурак
1
Я добавил лучший пример к карточке Наркомоэбы. Как вы видите, меня интересует область рисунка карты, она не должна быть на 100% точной. По моему мнению, должны быть какие-то преобразования, которые позволят мне разделить карту на различные «регионы», так сказать.
Waroulolz
Я думаю, что вы можете сначала обрезать изображения до 2 типов (может быть, 4 типа? в качестве предоставленной информации, изображение будет отображаться сверху или справа) и использовать opencv для проверки наличия текста на изображении. Так что обрезать -> фильтр -> результат -> обрезать края, если это необходимо, для opencv легче добиться лучшего результата.
Elprup

Ответы:

3

Я использовал преобразование линии Хафа для обнаружения линейных частей изображения. Пересечения всех линий были использованы для построения всех возможных прямоугольников, которые не содержат других точек пересечения. Так как та часть карты, которую вы ищете, всегда является самой большой из этих прямоугольников (по крайней мере, в предоставленных вами образцах), я просто выбрал самый большой из этих прямоугольников в качестве победителя. Скрипт работает без взаимодействия с пользователем.

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

Вот результаты с предоставленными вами образцами:

Изображение1

Image2

Image3

Код для нахождения пересечения линий можно найти здесь: найти точку пересечения двух линий, нарисованных с помощью houghlines opencv

Вы можете прочитать больше о Hough Lines здесь .

М. Мартин
источник
2
Спасибо за тяжелую работу. Ваш ответ - то, что я искал. Я знал, что Hough Lines сыграет здесь большую роль. Я пытался использовать его пару раз, но не смог подобраться к вашему решению. Как вы прокомментировали, необходимо сделать несколько настроек параметров, чтобы обобщить подход, но логика велика и мощна.
Waroulolz
1
Я думаю, что это отличное решение для такого рода проблем, пользовательский ввод не требуется. Браво!!
Мето
@ Meto - я ценю проделанную здесь работу, но я не согласен с тем, что пользователь не вводит . Это просто псевдоним, вводите ли вы во время выполнения или изменяете порог после просмотра результата.
Бурак
1
@Burak - мне удалось запустить все сэмплы с одинаковыми настройками, поэтому я предполагаю, что большинство других карт также будет работать. Таким образом, настройки должны быть выполнены только один раз.
М. Мартин
0

Мы знаем, что карты имеют прямые границы по осям X и Y. Мы можем использовать это, чтобы извлечь части изображения. Следующий код реализует обнаружение горизонтальных и вертикальных линий на изображении.

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

Вам просто нужно нажать на две области, чтобы включить. Пример области щелчка и соответствующий результат:

линии result_of_lines

Результаты из других изображений:

result_2 result_3

Бурак
источник
0

Я не думаю, что можно автоматически обрезать ROI художественных работ с использованием традиционных методов обработки изображений из-за динамического характера цветов, размеров, местоположений и текстур для каждой карты. Вы должны изучить машинное / глубокое обучение и обучить свой собственный классификатор, если хотите сделать это автоматически. Вместо этого, вот ручной подход, чтобы выбрать и обрезать статическую область интереса на изображении.

Идея состоит в том, чтобы использовать cv2.setMouseCallback()и обработчики событий, чтобы определить, была ли нажата или отпущена мышь. Для этой реализации вы можете извлечь графическую область интереса, удерживая левую кнопку мыши и перетаскивая ее, чтобы выбрать нужную область интереса. После того, как вы выбрали нужный ROI, нажмите, cчтобы обрезать и сохранить ROI. Вы можете сбросить ROI, используя правую кнопку мыши.

Сохраненные графические объекты

Код

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
источник