1. 개요
- https://dean83.tistory.com/68 에서 설명한 문서인식하는 방법과
- https://dean83.tistory.com/69?category=1088148 에서 설명한 카메라 띄우는 방법을 활용하였다.
2. 소감
- 사실 문서 인식 앱을 만드는건 어렵지 않다. 하지만, 성능을 내게끔 하는것이 어렵다
- 어떤 기능을 이용하여 얼만큼 이미지를 튜닝할 것이냐가 그 1번이다.
- 이것저것 기능을 많이 써서 튜닝을 해봤는데 결국 그 조합이 중요하다.
- 예 : 흑백으로 변환 -> 바이너리 변환 -> edge 인식은 그 결과가 대체로 안좋았다.
- 실질적으론 딥러닝 데이터들을 연계하여야만 좋은 성능을 낼것으로 보인다.
- 스터디 목적이므로 안드로이드 스토어 출시는 하지 않았고, 기타 다른 기능 추가도 하지 않았다.
- 기타 다른기능 추가 및 디자인 등 요소를 고려하는데 시간을 쏟는 시간에 다른 스터디를 하는게 이득이라
판단했다.
- 테스트 목적이라 변수명, 클래스명 등이 엉망이다.
3. 간략 설명
- 카메라로부터 실시간으로 이미지를 받아 화면에 띄운다
- 버튼을 클릭하여 이미지를 찍고, 문서 판별을 위해 이미지 튜닝을 한다
- 이미지 흑백 변환
- 노이즈 제거 (median blur)
- edge 인식
- Contour 를 찾아내기
- ConvexHull을 통해 면을 구하기
- 문서 인식 부분을 AdaptiveThreshold로 Binary 이미지로 만들기
- 인식된 문서를 화면에 보여주고, 저장버튼 클릭시 갤러리에 jpg로 저장한다
4. 상세
- 카메라로부터 실시간 이미지를 받아 화면에 띄우기
- Start 함수에서 SetCamera 함수를 호출하고, Update에서 화면에 표시한다
private void SetCamera()
{
// 사실, 전면카메라를 배제하는 기능을 넣으면 좋지만 넣지 않았다.
// devices[i].isFrontFacing 변수를 보면 알 수 있다.
string cameraName = "";
if (WebCamTexture.devices.Length > 0)
cameraName = WebCamTexture.devices[0].name;
camTexture = new WebCamTexture(cameraName);
//height와 width를 설정치 않으면 640*480으로 나온다.
camTexture.requestedHeight = Screen.height;
camTexture.requestedWidth = Screen.width;
//화면을 그나마 부드럽게 보여준다.
camTexture.filterMode = FilterMode.Trilinear;
camTexture.Play();
}
//카메라가 동작중인지 확인.
// 사실 didThisFrameUpdate 도 사용했었는데, 별 차이가 없어서 지웠다.
private bool IsCamTextureReady()
{
if (camTexture == null || camTexture.isPlaying == false)
return false;
return true;
}
void Update()
{
if (IsCamTextureReady() == false || isJobFinished)
return;
rawImage.texture = camTexture;
//이미지가 보통 돌아가 있다. 때문에 회전을 해줘야 한다.
rawImage.GetComponent<RectTransform>().localEulerAngles = new Vector3(0, 0, -camTexture.videoRotationAngle);
rawImage.GetComponent<RectTransform>().sizeDelta = new Vector2(camTexture.width, camTexture.height);
}
- 문서 스캔 동작 클래스 변수 생성 및 호출 (대리자 설정)
- 문서 스캔 동작 완료시, 호출할 대리자를 설정한다.
* 원본 이미지부터. 원래 다른 문서를 썼으나 개인정보가 있어 이 문서로 바꿨다.
public class ScannerScript
{
//ScanFinished 라는 대리자 변수를 추가한다.
private ScanFinished scanFinished;
//생성자에 대리자 설정을 한다
public ScannerScript(ScanFinished finished)
{
scanFinished = finished;
}
....
}
- WebCamTexture Mat으로 변환 및 이미지 흑백 전환
* 흑백 전환 이미지. 원본이랑 큰차이가 없네... 다른 문서로 했을땐 차이가 뚜렷했다.
Mat source = OpenCvSharp.Unity.TextureToMat(texture);
//원본 이미지를 하나 더 둔다. 나중에 문서 좌표를 구한 후, 원본에서 자를것이다.
Mat origin = new Mat();
source.CopyTo(origin);
//흑백 전환
source = source.CvtColor(ColorConversionCodes.BGR2GRAY);
- Median Blur를 이용한 노이즈 제거
* 결과 이미지 : 확실히 블러 효과가 크다. 안의 내용은 없어져도 된다. 문서 외각선만 따면 되니까.
//이부분은 거의 Demo 에 있는 기능을 갖다 썼다. 값 설정이 매직넘버인듯 하다.
int medianKernel = 11;
double kernelScale = 0.33;
kernelScale *= Math.Max(source.Width, source.Height) / 512.0;
// apply scale
medianKernel = (int)(medianKernel * kernelScale + 0.5);
medianKernel = medianKernel - (medianKernel % 2) + 1;
if (medianKernel > 1)
source = source.MedianBlur(medianKernel);
- 윤곽선 찾기 (Canny 알고리즘 및 Contour 찾기)
* Canny 알고리즘 결과 : 희미하게 문서의 선이 보인다.
//Canny 알고리즘을 이용해 외각선을 찾는다.
source = source.AdaptiveEdges();
//Contour (윤곽선)을 찾고 해당 좌표값을 받아온다
Point[][] contours;
HierarchyIndex[] hierarchy;
Cv2.FindContours(source, out contours, out hierarchy, RetrievalModes.List, ContourApproximationModes.ApproxNone, null);
List<Point> approxResult = new List<Point>();
foreach (Point[] data in contours)
{
double length = Cv2.ArcLength(data, true);
//적어도 선의 길이가 너비의 40% 이상 되는 것만 선별한다.
if (length <= source.Width * 0.4)
continue;
//ApproxPolyDP는 윤곽선을 그릴때 필요한 좌표값을 최소화 시킨다.
//두번째 인자값인 엡실론의 경우, 얼마나 단순화 시킬지를 결정하는 값이다
//값이 작을수록 원본에 가깝게 표현을 한다. 여기서 ArcLength의 값을 쓰는 이유는, 원본 길이의
//몇 퍼센트 단순화 시킬지를 의미한다. 즉, 0.01을 곱하면 1% 오차범위로 단순화 한다는 이야기 이다.
//따라서 원본 길이값을 알아야 하므로 ArcLength 값을 사용한다.
Point[] approx = Cv2.ApproxPolyDP(data, length * 0.01, true);
approxResult.AddRange(approx);
}
- ConvexHull 을 이용하여, 윤곽선 점들을 내포하는 면 찾기
- 해당 면의 꼭지점 개수가 몇개인지 판단하여 작업하기
- 4개가 아닌, 2개이상의 좌표를 발견하였을 경우, 최소한의 4각형 좌표를 임의로 만들고
인접한 실측좌표를 배정하여 임의의 4각형을 만든다.
//모든점을 내접하는 면을 만들고, 좌표값을 단순화 한다
Point[] hull = Cv2.ConvexHull(approxResult.ToArray());
Point[] hullContour = Cv2.ApproxPolyDP(hull, Cv2.ArcLength(hull, true) * 0.01, true);
Texture2D resultItem = null;
Mat croppedMat = null;
Point[] cropPoint = new Point[4];
//문서를 정확히 찾았을 경우. 꼭지점 4개, 면의 너비가 이미지의 50% 이상 차지할경우 찾은것으로 간주한다.
if (hullContour.Length == 4 && Cv2.ContourArea(hullContour) >= (source.Width * source.Height) * 0.5)
{
cropPoint = hullContour;
}
else if(hullContour.Length > 2)
{
//적어도 2개의 점이라도 있다면
//모든 점을 감싸는 최소한의 사각형을 만든다.
//좌표를 int 좌표로 변환한다.
RotatedRect bounds = Cv2.MinAreaRect(approxResult);
Point2f[] points = bounds.Points();
Point[] intPoints = Array.ConvertAll(points, p => new Point(Math.Round(p.X), Math.Round(p.Y)));
// 최소한의 사각형 좌표와 가장 근접하는 실측 좌표값을 배정하여, 사각형을 만든다.
Point[] rectPoint = new Point[4];
for (int i = 0; i < intPoints.Length; i++)
rectPoint[i] = approxResult.ToArray().ClosestElement(intPoints[i], (Point x, Point y) => Point.Distance(x, y));
cropPoint = rectPoint;
}
- 앞에서 구한 사각형 좌표값을 이용해 원본 이미지에서 문서만 자르고, 바이너리 이미지로 만든다.
WarpPerspective를 이용하여 이미지 회절 및 자르기를 한다.
* 최종 결과 이미지 : 양옆 파란색은 그림판에서 잘라내다보니 깔끔히 못잘라서 그렇다.
//좌상단, 우상단, 우하단, 좌하단 순으로 좌표를 정렬해야만
//WarpPerspective를 이용한 이미지 회절 및 자르기가 가능하다.
SortRectEdgePoints(ref cropPoint);
//기울어진 이미지를 바로잡기 위해 point2f 로 변경한다.
Point2f[] warpDestPoints = new Point2f[4];
warpDestPoints = Array.ConvertAll(cropPoint, p => new Point2f(p.X, p.Y));
//원본에서 이미지를 자를것이므로, 흑백 변환을 한다.
croppedMat = origin.CvtColor(ColorConversionCodes.BGR2GRAY);
//AdaptiveThreshold를 이용해 바이너리 이미지로 변경한다. 촬영한 원본은 흑백이어도
//그림자 등이 있어 색이 고르지 못하다.
//33 : 블록사이즈로서, 홀수로 지정해야한다. 값이 클수록 넒은범위를 대상으로 삼는다. 결과물이 크게 차이나진 않는다
//10 : 보정값이다. 평균낸 값에서 이 값을 보정을 한다. 사실상 매직넘버. 상황에 따라 좋은값일수도, 아닐수도 있다. 결과물이 크게 차이난다
croppedMat = croppedMat.AdaptiveThreshold(255, AdaptiveThresholdTypes.MeanC, ThresholdTypes.Binary, 33, 10);
////기울어진 이미지를 WarpPerspective를 이용하여 보정하고 자른다.
croppedMat = croppedMat.UnwrapShape(warpDestPoints);
....
//4각형 4개의 좌표를 각 좌상단, 우상단, 우하단, 좌하단 순으로 정렬한다.
private void SortRectEdgePoints(ref Point[] points)
{
if (points == null || points.Length < 4)
return;
points = (from item in points
orderby item.Y
select item).ToArray();
//좌상단, 우상단 순으로 정렬
if (points[0].X > points[1].X)
points.Swap(0, 1);
//우하단, 좌하단 순으로 정렬
if (points[2].X < points[3].X)
points.Swap(2, 3);
}
- 이미지를 Texture2d로 변환하고 대리자를 통해 전달한다.
if (croppedMat != null)
resultItem = OpenCvSharp.Unity.MatToTexture(croppedMat);
if (scanFinished != null)
scanFinished(resultItem);
5. 문제점 혹은 개선 필요점
- 테스트 결과 나름 문서 인식을 잘 한다.
- 문서 검출을 위한 사각형 좌표에서, 총 너비의 50% 길이인 Contour만 대상으로 삼는데, 가끔 오동작 한다. 개선필요
- 문서보다 더 넒은범위를 문서로 인식해 버리는 문제가 종종 있다.
- AdaptiveThreshold에 매직넘버가 사용된다. 별 문젠 없는데... 상황에 따라 결과가 이상할 수 있다.
이 부분은 딥러닝 데이터를 써야 하지 않을까?
- pdf 저장, 이메일로 보내기, 웹으로 다운로드 받기 등등 기능추가는 생략했지만.... 된다면 출시를 해도 될거 같다.
- 문서가 조금은 구겨져 있으므로, 수동으로 크롭 할 수 있는 기능이 있으면 좋겠다.
- 조금 구겨진 부분때문에 일그러짐은 회복이 안된다. 이 부분 또한 딥러닝 연계가 필요할듯 보인다.
'OpenCV' 카테고리의 다른 글
OpenCV+Unity Gaussian, Median Blur와 Canny 내용 재정리 (0) | 2022.04.14 |
---|---|
OpenCV+Unity 실시간으로 영상에 필터효과 적용하기 (0) | 2022.04.14 |
[Unity] Unity+OpenCV 문서 인식 하기 (0) | 2022.04.01 |
[Unity] OpenCV+Unity 도형 감별하기 (0) | 2022.03.30 |
[Unity] OpenCV+Unity 사진에서 인물 감별 (0) | 2022.03.29 |