스도쿠 광장에서 볼록 결함을 제거하는 방법? 스도쿠의 실제 경계와 정확하게 일치하므로 스도쿠가

재미있는 프로젝트를하고있었습니다 : OpenCV를 사용하여 입력 이미지에서 스도쿠를 해결하십시오 (Google 고글 등). 그리고 나는 과제를 완수했지만 결국 나는 여기에 왔던 작은 문제를 발견했습니다.

OpenCV 2.3.1의 Python API를 사용하여 프로그래밍을 수행했습니다.

아래는 내가 한 일입니다.

  1. 이미지를 읽으십시오
  2. 윤곽선 찾기
  3. 최대 면적을 가진 것을 선택하십시오 (그리고 정사각형과 다소 동등합니다).
  4. 코너 포인트를 찾으십시오.

    예를 들면 다음과 같습니다.

    여기에 이미지 설명을 입력하십시오

    ( 여기에서 녹색 선은 스도쿠의 실제 경계와 정확하게 일치하므로 스도쿠가 올바르게 뒤 틀릴 수 있습니다 . 다음 이미지 확인)

  5. 이미지를 완벽한 정사각형으로 워프

    예 : 이미지 :

  6. OCR 수행 ( OpenCV-Python의 Simple Digit Recognition OCR에서 제공 한 방법을 사용했습니다 )

그리고 그 방법은 잘 작동했습니다.

문제:

이 이미지를 확인하십시오 .

이 이미지에서 4 단계를 수행하면 다음과 같은 결과가 나타납니다.

그려진 빨간 선은 스도쿠 경계의 실제 윤곽 인 원래 윤곽입니다.

그려진 녹색 선은 뒤틀린 이미지의 윤곽이 될 대략적인 윤곽입니다.

물론 스도쿠 상단 가장자리에 녹색 선과 빨간색 선이 다릅니다. 따라서 뒤틀리는 동안 스도쿠의 원래 경계를 얻지 못했습니다.

내 질문 :

스도쿠의 올바른 경계 (예 : 빨간색 선)에서 이미지를 왜곡하는 방법 또는 빨간색 선과 녹색 선의 차이를 어떻게 제거 할 수 있습니까? OpenCV에 이에 대한 방법이 있습니까?



답변

작동하는 솔루션이 있지만 OpenCV로 직접 변환해야합니다. Mathematica로 작성되었습니다.

첫 번째 단계는 각 픽셀을 닫기 작업의 결과로 나누어 이미지의 밝기를 조정하는 것입니다.

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

다음 단계는 스도쿠 영역을 찾는 것이므로 배경을 무시 (마스크 아웃) 할 수 있습니다. 이를 위해 연결된 구성 요소 분석을 사용하고 가장 큰 볼록 영역이있는 구성 요소를 선택합니다.

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

이 이미지를 채워 스도쿠 그리드 마스크를 얻습니다.

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];

연결된 컴포넌트 분석을 다시 사용하여이 이미지에서 그리드 선을 추출합니다. 눈금 선이 자릿수보다 훨씬 길기 때문에 캘리퍼 길이를 사용하여 눈금 선으로 연결된 구성 요소 만 선택할 수 있습니다. 위치별로 정렬하면 이미지의 세로 / 가로 그리드 선 각각에 대해 2×10 마스크 이미지가 나타납니다.

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에서도 가능합니다. 스플라인 기반 이미지 변환이 더 어려울 수 있지만 실제로 필요한 것은 아닙니다. 아마도 각 개별 셀에서 지금 사용하는 원근 변환을 사용하면 충분한 결과를 얻을 수 있습니다.


답변

Nikie의 대답은 내 문제를 해결했지만 그의 대답은 Mathematica에있었습니다. 그래서 여기에 OpenCV를 적용해야한다고 생각했습니다. 그러나 구현 후 OpenCV 코드가 nikie의 수학 코드보다 훨씬 큽니다. 또한 OpenCV에서 nikie가 수행 한 보간 방법을 찾을 수 없었습니다 (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에 해당하는 기능을 찾을 수 없습니다. (있을 수도 있습니다, 모르겠습니다).

사용하고 싶지 않은 SciPy를 사용하여이를 수행하는 방법을 설명하는이 SOF를 확인하십시오 : 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))

이제 아래 순서를 참조하십시오.

마지막으로 변환을 적용하고 450×450 크기의 새 이미지를 만듭니다.

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×3 하위 지역의 경계를 감지 한 다음 각 지역을 개별적으로 왜곡 할 수 있습니다. 탐지에 성공하면 더 나은 근사치를 얻을 수 있습니다.


답변

위의 방법은 스도쿠 보드가 똑바로 서있을 때만 작동한다는 것을 추가하고 싶습니다. 그렇지 않으면 높이 / 너비 (또는 그 반대) 비율 테스트가 실패하고 스도쿠의 가장자리를 감지 할 수 없습니다. (또한 이미지 테두리에 수직이 아닌 선이 선이 여전히 두 축에 대해 가장자리를 가지므로 sobel 작업 (dx 및 dy)이 계속 작동한다고 덧붙이고 싶습니다.)

직선을 감지하려면 contourArea / boundingRectArea, 왼쪽 상단 및 오른쪽 하단 점과 같은 등고선 또는 픽셀 별 분석 작업을 수행해야합니다.

편집 : 선형 회귀를 적용하고 오류를 확인하여 윤곽 세트가 선을 형성하는지 여부를 확인할 수있었습니다. 그러나 선의 기울기가 너무 크거나 (예 :> 1000) 0에 매우 가까운 경우 선형 회귀가 제대로 수행되지 않았습니다. 따라서 선형 회귀 전에 위의 비율 테스트를 적용하는 것이 논리적이며 저에게 효과적이었습니다.


답변

미결정 모서리를 제거하기 위해 감마 값 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)

이것은 일부 코너 포인트가 누락 된 경우 Abid Rahman의 답변에 추가됩니다.


답변

나는 이것이 ARK의 훌륭한 게시물이자 훌륭한 해결책이라고 생각했다. 잘 정리하고 설명했습니다.

나는 비슷한 문제를 겪고 있었고 전체를 만들었습니다. 일부 변경 사항 (예 : xrange to range, cv2.findContours의 인수)이 있었지만 즉시 작동해야합니다 (Python 3.5, Anaconda).

이것은 누락 된 코드 중 일부 (예 : 포인트 레이블)가 추가 된 위의 요소를 편집 한 것입니다.

'''

/programming/10196198/how-to-remove-convexity-defects-in-a-sudoku-square

'''

import cv2
import numpy as np

img = cv2.imread('test.png')

winname="raw image"
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.moveWindow(winname, 100,100)


img = cv2.GaussianBlur(img,(5,5),0)

winname="blurred"
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.moveWindow(winname, 100,150)

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
mask = np.zeros((gray.shape),np.uint8)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

winname="gray"
cv2.namedWindow(winname)
cv2.imshow(winname, gray)
cv2.moveWindow(winname, 100,200)

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)

winname="res2"
cv2.namedWindow(winname)
cv2.imshow(winname, res2)
cv2.moveWindow(winname, 100,250)

 #find elements
thresh = cv2.adaptiveThreshold(res,255,0,1,19,2)
img_c, 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)

winname="puzzle only"
cv2.namedWindow(winname)
cv2.imshow(winname, res)
cv2.moveWindow(winname, 100,300)

# vertical lines
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)

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

winname="vertical lines"
cv2.namedWindow(winname)
cv2.imshow(winname, img_d)
cv2.moveWindow(winname, 100,350)

# find horizontal lines
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)

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

winname="horizontal lines"
cv2.namedWindow(winname)
cv2.imshow(winname, img_e)
cv2.moveWindow(winname, 100,400)


# intersection of these two gives dots
res = cv2.bitwise_and(closex,closey)

winname="intersections"
cv2.namedWindow(winname)
cv2.imshow(winname, res)
cv2.moveWindow(winname, 100,450)

# text blue
textcolor=(0,255,0)
# points green
pointcolor=(255,0,0)

# find centroids and sort
img_f, 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))

# sorting
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 range(10)])
bm = b.reshape((10,10,2))

# make copy
labeled_in_order=res2.copy()

for index, pt in enumerate(b):
    cv2.putText(labeled_in_order,str(index),tuple(pt),cv2.FONT_HERSHEY_DUPLEX, 0.75, textcolor)
    cv2.circle(labeled_in_order, tuple(pt), 5, pointcolor)

winname="labeled in order"
cv2.namedWindow(winname)
cv2.imshow(winname, labeled_in_order)
cv2.moveWindow(winname, 100,500)

# create final

output = np.zeros((450,450,3),np.uint8)
for i,j in enumerate(b):
    ri = int(i/10) # row index
    ci = i%10 # column index
    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()

winname="final"
cv2.namedWindow(winname)
cv2.imshow(winname, output)
cv2.moveWindow(winname, 600,100)

cv2.waitKey(0)
cv2.destroyAllWindows()