Как удалить дефекты выпуклости на площади Судоку?
Я делал забавный проект: решение Sudoku с входного изображения с использованием OpenCV (как в Google goggles и т.д.). И я выполнил задание, но в конце я нашел небольшую проблему, ради которой я пришел сюда.
Я программировал, используя Python API OpenCV 2.3.1.
Ниже приведено то, что я сделал:
- Прочтите изображение
- Найти контуры
- Выберите ту, которая имеет максимальную площадь (а также несколько эквивалентна квадрату).
-
Найдите угловые точки.
например. ниже:
( Обратите внимание на то, что зеленая линия правильно совпадает с истинной границей судоку, поэтому судоку можно правильно деформировать. Проверьте следующее изображение)
-
превратить изображение в идеальный квадрат
например, изображение:
-
Выполните OCR (для которого я использовал метод, который я дал в OCR OCR в OpenCV-Python)
И этот метод работал хорошо.
Проблема:
Отметьте это изображение.
Выполнение этапа 4 на этом изображении дает следующий результат:
Нарисованная красная линия - это исходный контур, который является истинным контуром границы судоку.
Нарисованная зеленая линия представляет собой аппроксимированный контур, который будет контуром искаженного изображения.
Что, конечно, есть разница между зеленой линией и красной линией на верхнем крае судоку. Поэтому, пока деформирование, я не получаю первоначальную границу судоку.
Мой вопрос:
Как я могу исказить изображение на правильной границе судоку, т.е. красную линию ИЛИ как я могу удалить разницу между красной линией и зеленой линией? Есть ли какой-либо метод для этого в OpenCV?
Ответы
Ответ 1
У меня есть решение, которое работает, но вам придется перевести его на OpenCV самостоятельно. Это написано в Mathematica.
Первым шагом является настройка яркости изображения путем деления каждого пикселя на результат операции закрытия:
src = ColorConvert[Import["http://davemark.com/images/sudoku.jpg"], "Grayscale"];
white = Closing[src, DiskMatrix[5]];
srcAdjusted = Image[ImageData[src]/ImageData[white]]
Следующий шаг - найти область sudoku, поэтому я могу игнорировать (маскировать) фон. Для этого я использую анализ связанных компонентов и выбираю компонент, который получил наибольшую выпуклую область:
components =
ComponentMeasurements[
[email protected][srcAdjusted], {"ConvexArea", "Mask"}][[All,
2]];
largestComponent = Image[SortBy[components, First][[-1, 2]]]
Заполнив это изображение, я получаю маску для сетки sudoku:
mask = FillingTransform[largestComponent]
Теперь я могу использовать фильтр производных 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];
Я снова использую компонентный анализ компонентов, чтобы извлечь линии сетки из этих изображений. Линии сетки намного длиннее цифр, поэтому я могу использовать длину суппорта для выбора только компонентов, связанных с сеткой. Сортируя их по положению, я получаю изображения маски 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]];
Далее я беру каждую пару вертикальных/горизонтальных линий сетки, расширяю их, вычисляю пиксельно-пиксельное пересечение и вычисляю центр результата. Эти точки представляют собой пересечения линий сетки:
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}];
Последний шаг - определить две функции интерполяции для отображения 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]
Все операции - это основная функция обработки изображений, поэтому это тоже возможно в OpenCV. Преобразование изображений на основе сплайна может быть сложнее, но я не думаю, что вам это действительно нужно. Вероятно, использование перспективного преобразования, которое вы используете сейчас в каждой отдельной ячейке, даст хорошие результаты.
Ответ 2
Ответ Ники решил решить мою проблему, но его ответ был в Mathematica. Поэтому я подумал, что должен дать здесь свою адаптацию OpenCV. Но после внедрения я понял, что код OpenCV намного больше, чем код математики 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)
Результат:
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)
Результат:
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()
Результат:
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()
Результат:
Конечно, это не так хорошо.
5. Поиск точек сетки
res = cv2.bitwise_and(closex,closey)
Результат:
6. Исправление дефектов
Здесь nikie делает какую-то интерполяцию, о которой у меня мало знаний. И я не мог найти никакой соответствующей функции для этого 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))
Но полученные центроиды не будут отсортированы. Посмотрите ниже изображение, чтобы увидеть их порядок:
Итак, мы сортируем их слева направо, сверху вниз.
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))
Теперь см. ниже их порядок:
Наконец, мы применяем преобразование и создаем новое изображение размером 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()
Результат:
Результат почти такой же, как у nikie, но длина кода велика. Возможно, лучшие методы доступны там, но до тех пор это работает нормально.
Отношения
ARK.
Ответ 3
Вы могли бы попытаться использовать какое-то сетевое моделирование вашего произвольного деформирования. И поскольку судоку уже является сеткой, это не должно быть слишком сложно.
Итак, вы можете попытаться определить границы каждой субрегиона 3x3, а затем деформировать каждый регион отдельно. Если обнаружение завершится успешно, это даст вам лучшее приближение.
Ответ 4
Я хочу добавить, что описанный выше метод работает только в том случае, если доска судоку стоит прямо, в противном случае проверка соотношения высоты/ширины (или наоборот), скорее всего, провалится, и вы не сможете обнаружить края судоку. (Я также хочу добавить, что если линии, которые не перпендикулярны границам изображения, операции sobel (dx и dy) будут по-прежнему работать, так как линии будут по-прежнему иметь края относительно обеих осей.)
Чтобы иметь возможность обнаруживать прямые линии, вы должны работать с контурным или пиксельным анализом, таким как contourArea/boundingRectArea, верхняя левая и нижняя правая точки...
Изменение: мне удалось проверить, образует ли набор контуров линию или нет, применив линейную регрессию и проверив ошибку. Однако линейная регрессия выполняется плохо, когда наклон линии слишком велик (т.е.> 1000) или очень близок к 0. Поэтому применение вышеуказанного критерия отношения (в ответе с наибольшим количеством голосов) перед линейной регрессией логично и сработало для меня.
Ответ 5
Для удаления незапятнанных углов я применил гамма-коррекцию со значением гаммы 0,8.
Красный круг нарисован, чтобы показать недостающий угол.
Код является:
gamma = 0.8
invGamma = 1/gamma
table = np.array([((i / 255.0) ** invGamma) * 255
for i in np.arange(0, 256)]).astype("uint8")
cv2.LUT(img, table, img)
Это в дополнение к ответу Абида Рахмана, если отсутствуют некоторые угловые точки.