Как удалить дефекты выпуклости в квадрате судоку?


Я делал забавный проект: решение судоку из входного изображения с помощью OpenCV (как в Google goggles и т. д.). И я выполнил задачу, но в конце концов я нашел небольшую проблему, для которой я пришел сюда.

Я сделал Программирование с использованием Python API OpenCV 2.3.1.

вот что я сделал :

  1. читать изображения
  2. найти контуры
  3. Выберите один с максимальной площадью, (а также несколько эквивалентно квадрат.)
  4. найти угловые точки.

    например, приведенный ниже:

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

  5. деформация изображения до идеального квадрата

    например изображения:

  6. выполнить OCR ( для которого я использовал метод, который я дал простое распознавание цифр OCR в OpenCV-Python)

и метод работал хорошо.

:

проверить этот образ.

выполнение шага 4 на этом изображении дает результат ниже:

нарисованная красная линия является исходным контуром, который является истинным контуром границы судоку.

нарисованная зеленая линия является аппроксимированным контуром, который будет контур искаженного изображения.

конечно, есть разница между зеленой линией и красной линией по верхнему краю судоку. Поэтому, деформируя, я не получаю исходную границу судоку.

Мой Вопрос :

Как я могу деформировать изображение на правильной границе судоку, т. е. красной линии или как я могу удалить разницу между красной линией и зеленой линией? Есть ли какой-либо метод для этого в OpenCV?

4 161

4 ответа:

у меня есть решение, которое работает, но вам придется перевести его на OpenCV самостоятельно. Это написано в Mathematica.

первый шаг заключается в регулировке яркости изображения, разделив каждый пиксель с результатом операции закрытия:

src = ColorConvert[Import["http://davemark.com/images/sudoku.jpg"], "Grayscale"];
white = Closing[src, DiskMatrix[5]];
srcAdjusted = Image[ImageData[src]/ImageData[white]]

enter image description here

следующий шаг-найти область судоку, поэтому я могу игнорировать (маскировать) фон. Для этого я использую анализ связанных компонентов и выбираю компонент, который имеет наибольшую выпуклость площадь:

components = 
  ComponentMeasurements[
    ColorNegate@Binarize[srcAdjusted], {"ConvexArea", "Mask"}][[All, 
    2]];
largestComponent = Image[SortBy[components, First][[-1, 2]]]

enter image description here

заполнив это изображение, я получаю маску для сетки судоку:

mask = FillingTransform[largestComponent]

enter image description here

теперь я могу использовать производный фильтр 2-го порядка, чтобы найти вертикальные и горизонтальные линии в двух отдельных изображениях:

lY = ImageMultiply[MorphologicalBinarize[GaussianFilter[srcAdjusted, 3, {2, 0}], {0.02, 0.05}], mask];
lX = ImageMultiply[MorphologicalBinarize[GaussianFilter[srcAdjusted, 3, {0, 2}], {0.02, 0.05}], mask];

enter image description here

я снова использую анализ подключенных компонентов для извлечения линий сетки из этих изображений. Линии сетки намного длиннее цифр, поэтому я могу использовать длину суппорта для выбора только линий сетки-подключенные компоненты. Сортируя их по позиции, я получаю изображения маски 2x10 для каждой из вертикальных / горизонтальных линий сетки на изображении:

verticalGridLineMasks = 
  SortBy[ComponentMeasurements[
      lX, {"CaliperLength", "Centroid", "Mask"}, # > 100 &][[All, 
      2]], #[[2, 1]] &][[All, 3]];
horizontalGridLineMasks = 
  SortBy[ComponentMeasurements[
      lY, {"CaliperLength", "Centroid", "Mask"}, # > 100 &][[All, 
      2]], #[[2, 2]] &][[All, 3]];

enter image description here

далее я беру каждую пару вертикальных / горизонтальных линий сетки, расширяю их, вычисляю пиксельное пересечение и вычисляю центр результата. Эти точки являются пересечениями линий сетки:

centerOfGravity[l_] := 
 ComponentMeasurements[Image[l], "Centroid"][[1, 2]]
gridCenters = 
  Table[centerOfGravity[
    ImageData[Dilation[Image[h], DiskMatrix[2]]]*
     ImageData[Dilation[Image[v], DiskMatrix[2]]]], {h, 
    horizontalGridLineMasks}, {v, verticalGridLineMasks}];

enter image description here

последний шаг заключается в определении две функции интерполяции для отображения X/Y через эти точки и преобразования изображения с помощью этих функций:

fnX = ListInterpolation[gridCenters[[All, All, 1]]];
fnY = ListInterpolation[gridCenters[[All, All, 2]]];
transformed = 
 ImageTransformation[
  srcAdjusted, {fnX @@ Reverse[#], fnY @@ Reverse[#]} &, {9*50, 9*50},
   PlotRange -> {{1, 10}, {1, 10}}, DataRange -> Full]

enter image description here

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

ответ Ники решил мою проблему, но его ответ был в Mathematica. Поэтому я подумал, что должен дать его адаптацию OpenCV здесь. Но после реализации я мог видеть, что код OpenCV намного больше, чем код mathematica от nikie. Кроме того, я не смог найти метод интерполяции, выполненный nikie в OpenCV ( хотя это можно сделать с помощью scipy, я расскажу об этом, когда придет время.)

1. Предварительная обработка изображений ( операция закрытия )

import cv2
import numpy as np

img = cv2.imread('dave.jpg')
img = cv2.GaussianBlur(img,(5,5),0)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
mask = np.zeros((gray.shape),np.uint8)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

close = cv2.morphologyEx(gray,cv2.MORPH_CLOSE,kernel1)
div = np.float32(gray)/(close)
res = np.uint8(cv2.normalize(div,div,0,255,cv2.NORM_MINMAX))
res2 = cv2.cvtColor(res,cv2.COLOR_GRAY2BGR)

результат :

Result of closing

2. Поиск квадрата судоку и создание маски изображения

thresh = cv2.adaptiveThreshold(res,255,0,1,19,2)
contour,hier = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

max_area = 0
best_cnt = None
for cnt in contour:
    area = cv2.contourArea(cnt)
    if area > 1000:
        if area > max_area:
            max_area = area
            best_cnt = cnt

cv2.drawContours(mask,[best_cnt],0,255,-1)
cv2.drawContours(mask,[best_cnt],0,0,2)

res = cv2.bitwise_and(res,mask)

результат :

enter image description here

3. Поиск вертикальных линий

kernelx = cv2.getStructuringElement(cv2.MORPH_RECT,(2,10))

dx = cv2.Sobel(res,cv2.CV_16S,1,0)
dx = cv2.convertScaleAbs(dx)
cv2.normalize(dx,dx,0,255,cv2.NORM_MINMAX)
ret,close = cv2.threshold(dx,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
close = cv2.morphologyEx(close,cv2.MORPH_DILATE,kernelx,iterations = 1)

contour, hier = cv2.findContours(close,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for cnt in contour:
    x,y,w,h = cv2.boundingRect(cnt)
    if h/w > 5:
        cv2.drawContours(close,[cnt],0,255,-1)
    else:
        cv2.drawContours(close,[cnt],0,0,-1)
close = cv2.morphologyEx(close,cv2.MORPH_CLOSE,None,iterations = 2)
closex = close.copy()

результат :

enter image description here

4. Поиск Горизонтальных Линий

kernely = cv2.getStructuringElement(cv2.MORPH_RECT,(10,2))
dy = cv2.Sobel(res,cv2.CV_16S,0,2)
dy = cv2.convertScaleAbs(dy)
cv2.normalize(dy,dy,0,255,cv2.NORM_MINMAX)
ret,close = cv2.threshold(dy,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
close = cv2.morphologyEx(close,cv2.MORPH_DILATE,kernely)

contour, hier = cv2.findContours(close,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for cnt in contour:
    x,y,w,h = cv2.boundingRect(cnt)
    if w/h > 5:
        cv2.drawContours(close,[cnt],0,255,-1)
    else:
        cv2.drawContours(close,[cnt],0,0,-1)

close = cv2.morphologyEx(close,cv2.MORPH_DILATE,None,iterations = 2)
closey = close.copy()

результат :

enter image description here

конечно, это не так хороший.

5. Поиск Точек Сетки

res = cv2.bitwise_and(closex,closey)

результат :

enter image description here

6. Исправление дефектов

здесь Никки делает какую-то интерполяцию, о которой у меня мало знаний. И я не мог найти никакой соответствующей функции для этого OpenCV. (может быть, это там, я не знаю).

проверьте этот SOF, который объясняет, как это сделать с помощью SciPy, который я не хочу использовать : преобразование изображений в OpenCV

Итак, здесь я взял 4 угла каждого суб-квадрата и применил перспективу деформации к каждому.

для этого, сначала мы находим центроиды.

contour, hier = cv2.findContours(res,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
centroids = []
for cnt in contour:
    mom = cv2.moments(cnt)
    (x,y) = int(mom['m10']/mom['m00']), int(mom['m01']/mom['m00'])
    cv2.circle(img,(x,y),4,(0,255,0),-1)
    centroids.append((x,y))

но полученные центроиды не будут отсортированы. Проверьте ниже изображение, чтобы увидеть их порядок:

enter image description here

поэтому мы сортируем их слева направо, сверху вниз.

centroids = np.array(centroids,dtype = np.float32)
c = centroids.reshape((100,2))
c2 = c[np.argsort(c[:,1])]

b = np.vstack([c2[i*10:(i+1)*10][np.argsort(c2[i*10:(i+1)*10,0])] for i in xrange(10)])
bm = b.reshape((10,10,2))

теперь смотрите ниже их порядок :

enter image description here

наконец, мы применяем преобразование и создаем новое изображение размером 450x450.

output = np.zeros((450,450,3),np.uint8)
for i,j in enumerate(b):
    ri = i/10
    ci = i%10
    if ci != 9 and ri!=9:
        src = bm[ri:ri+2, ci:ci+2 , :].reshape((4,2))
        dst = np.array( [ [ci*50,ri*50],[(ci+1)*50-1,ri*50],[ci*50,(ri+1)*50-1],[(ci+1)*50-1,(ri+1)*50-1] ], np.float32)
        retval = cv2.getPerspectiveTransform(src,dst)
        warp = cv2.warpPerspective(res2,retval,(450,450))
        output[ri*50:(ri+1)*50-1 , ci*50:(ci+1)*50-1] = warp[ri*50:(ri+1)*50-1 , ci*50:(ci+1)*50-1].copy()

результат :

enter image description here

результат почти такой же, как у nikie, но длина кода большая. Может быть, лучшие методы доступны там, но до тех пор, это работает нормально.

С уважением КОВЧЕГ.

вы можете попробовать использовать какое-то сеточное моделирование произвольного деформирования. И поскольку судоку уже является сеткой, это не должно быть слишком сложно.

таким образом, вы можете попытаться определить границы каждого субрегиона 3x3, а затем деформировать каждый регион по отдельности. Если обнаружение пройдет успешно, это даст вам лучшее приближение.

Я хочу добавить, что выше метод работает только тогда, когда судоку доска стоит прямо, в противном случае высота/ширина (или наоборот) тест отношения, скорее всего, не удастся, и вы не сможете обнаружить края судоку. (Я также хочу добавить, что если линии, которые не перпендикулярны границам изображения, операции sobel (dx и dy) все равно будут работать, поскольку линии все равно будут иметь края относительно обеих осей.)

чтобы иметь возможность обнаруживать прямые линии, вы должны работать по контуру или по пикселям анализ, такой как contourArea/boundingRectArea, верхние левые и нижние правые точки...

Edit: мне удалось проверить, образует ли набор контуров линию или нет, применяя линейную регрессию и проверяя ошибку. Однако линейная регрессия выполняется плохо, когда наклон линии слишком велик (т. е. >1000) или очень близок к 0. Поэтому применение теста отношения выше (в большинстве опрошенных ответов) перед линейной регрессией логично и работает для меня.