OpenCV를 이용해 OCR 기능을 구현해봅시다. - slog(완료)

OpenCV(Open Source Computer Vision)은 오픈소스로 컴퓨터 비전을 위해 인텔이 개발하였던 것이 오픈소스화 되면서 보편적으로 사용하게 된 라이브러리 입니다. Windows 및 Linux등 다양한 운영체제에서 지원하며, OpenCV 딥러닝 모듈을 이용해 머신러닝을 통한 영상 인식에도 사용되고 있습니다.

OpenCV - 위키백과, 우리 모두의 백과사전 (wikipedia.org)

이번 공부의 골은

  • ML.NET을 이용해 영상 학습을 한 결과를 OpenCV 모듈을 이용해 라즈베리파이에서 OCR 인식을 하도록 함.

관련해서 경험이 전혀 없는데요, 이번 경험을 통해 ML.NET에 대해 익숙해지고, 학습한 결과가 라즈베리파이를 통해 잘 동작하는지를 경험하는게 목적이 되겠습니다.

관련해서 알아야 할 지식은,

  • .NET 5, C# 9
    • .NET 5를 arm64환경에서 C# 9 문법으로 빠른 결과물을 낼 수 있기 위한 목표로 진행합니다.
  • ML.NET
    • ML.NET은 UI 도구를 통해 내가 만들고자 하는 기본 구성을 알아서 만들어주는데요, 첫번째 그 접근으로 해보고, 좀 더 상세 접근은 관련 문서를 통해 하나씩 알아고자 합니다.
  • OpenCV Sharp
    • OpenCV를 C#에서 이용할 수 있는 래핑 라이브러리가 오래전 부터 존재하고 있습니다. 해당 라이브러리를 이용해서 OpenCV 경험을 하면서 최종적으로 라즈베리파이에서 OCR 기능을 구현하는게 목표입니다.
3 Likes

.NET 5, C# 9

Visual Studio 2019 Preview 환경을 구축합니다. ML.NET Preview를 이용하기 위해서는 Visual Studio 2019 Preview 환경을 이용해야 한다고 합니다.

Visual Studio Preview (microsoft.com)

1 Like

ML.NET

Visual Studio 2019 Preview를 설치하면 ML.NET Model BuilderGPU Support를 바로 사용할 수 있습니다.

image

image

OpenCV Sharp

NuGet을 보면 상당히 많은 OpenCV 관련 패키지가 눈에 보입니다. 가장 최신의 릴리즈어야 하기 때문에, OpenCvSharp4를 선택합니다.

image

1 Like

OCR - Tesseract 4이용

음. ML.NET Builder에 OCR은 없네요. 그래서 OCR 인식용으로 Tesseract 4를 이용해야 할 것 같습니다.
Tesseract 4는 LSTM 네트워크를 이용한 딥 러닝 기술이 포함되어서 Tesseract 3보다 비정형 데이터에 대해 더 나은 성능을 제공한다고 합니다.

tesseract-ocr/tesseract: Tesseract Open Source OCR Engine (main repository) (github.com)

.NET 용으로는

charlesw/tesseract: A .Net wrapper for tesseract-ocr (github.com)

가장 최신의 4.1.1 에 대응하고 .NET Standard 2.0 을 지원하므로 .NET 5에서 사용하는데 문제는 없어 보입니다.

  1. ML.NET으로 텍스트 개체 검색 및 영역 목록화
  2. 영역 별 Tesseract 4를 이용해 텍스트화

음 그런데 ML.NET Builder에서의 개체 검색은 템플릿이 Azure ML만 됩니다. 결국엔… 빌더의 도움은 얻지 못하겠군요 T_T

1 Like

ML.NET 샘플

dotnet/machinelearning-samples: Samples for ML.NET, an open source and cross-platform machine learning framework for .NET. (github.com)

다양한 샘플들이 있습니다. 이곳 중 컴퓨터 비젼의 Object Detecction이 필요합니다. 슬슬 현타가 옵니다.

1 Like

OCR 전처리 관련

OCR의 인식률을 높이기 위해 이미지를 전처리해야 합니다. 비단 OCR뿐만 아니라 머신러닝에서 사용하는 이미지도 마찬가지입니다. 다양한 사이즈의 이미지의 스케일을 맞추어야 하며, 색깊이를 낮추고 통일시켜야 합니다.

[Tesseract & OpenCV]를 이용한 OCR-2-1 전처리(pre-processing) (tistory.com)

2 Likes

image

ONNX | Home는 프레임워크 간의 상호 운용성을 지원합니다. 학습한 프레임워크와 상관없이 ONNX를 지원한다면 적용 프레임워크에서 학습데이터를 그대로 사용할 수 있습니다.

Open Neural Network Exchange - Wikipedia

1 Like

우리는 신경망 알고리즘 자체를 연구하고 개발할 수는 없습니다. 워낙 이 영역이 전문적이기도 하고 프로그래머의 영역은 아니기 때문입니다. 우리는 이미 존재하는 신경망 알고리즘이 무엇에 효과적이고 정확하며 어떠한 프레임워크가 잘 지원하는지를 파악하고 알아야 합니다. 그런 이후에는 목적에따라 적절한 신경망알고리즘과 프레임워크는 선정해서 사용하는 것을 익숙해지는게 필요한 것 같습니다.

이후에는 복수개의 신경망을 조합하여 사용하는게 필요할 것 같습니다.

1 Like

글자 영역을 객체감지가 아닌 외곽선 감지로 잡아낼 경우 OpenCV의 기능을 이용하면 됩니다

1 Like

신경망을 통해 텍스트를 감지하려면 다음의 글을 참고하면 될 것 같습니다

1 Like

ML.NET에서의 EAST Text Detector 관련

1 Like
1 Like

1차 정리

  • 딥러닝으로 OCR을 하기 위해선 두가지로 접근해야 한다.

    1. 글자 영역 검출 EAST Text Detector
    2. 글자 인식 Tesseract 4
    3. 기타 전처리 기법
  • .NET 환경에 맞게 적절한 딥러닝 프레임워크 및 엔진, 신경망알고리즘을 찾아 적용한다.

1 Like

OpenCV : DNN 신경망 및 Text Detection Model - EAST

https://docs.opencv.org/master/d6/d0f/group__dnn.html

https://docs.opencv.org/master/d8/ddc/classcv_1_1dnn_1_1TextDetectionModel__EAST.html

1 Like

Tesseract (.NET) Samples

charlesw/tesseract-samples: Samples for the Tesseract.Net wrapper (github.com)

tessdata

1 Like

Tesseract 테스트

레가시 방식과 LSTM 방식 모두 지원하고 Default가 LSTM이라고 하는데 생각보다 인식이 좋지는 않네요.

using System;
using System.IO;
using System.Net.Http;

using Tesseract;

// Tesseract를 테스트하는 간단한 예제를 작성합니다.
// 1. Tesseract .NET 패키지 NuGet 설치 - Install-Package Tesseract
// 2. 샘플코드 작성

var a1 = "http://cdn.011st.com/11dims/resize/600x600/quality/75/11src/pd/20/3/7/2/6/0/6/iOLuU/2722372606_B.jpg"; // 이런건 안됨
var a2 = "https://image.chosun.com/sitedata/image/202008/13/2020081303153_0.jpg"; // 잘됨. 단, 상단 로고도 한글로 변환하는것으로 텍스트 영역지정이 필요해 보임
var a3 = "https://www.ibookpark.com/wp-content/uploads/2020/03/Screen-Shot-2019-07-08-at-3.56.10-PM.jpg"; // 이런건 안됨
var a4 = "https://mblogthumb-phinf.pstatic.net/MjAxOTEyMjBfNDkg/MDAxNTc2ODI0NTMwNjA0.r2AOGK4g4ssSCUsiJjImlLRVTpkQg9bWOWXlaEwvqNQg.Mv9Fz2c6tc6FV1Z4kMiaigyU4RqUWxKb8LX0ch9SvBkg.JPEG.feublot/SE-dc86b9b0-807a-4a7c-a54d-17c5e5981123.jpg?type=w800"; // 이런것도 안됨
var a5 = "https://www.computertechreviews.com/wp-content/uploads/2019/11/image-result-for-link-building-min-1200x675.jpg";
var imgUri = a5;
var c = new HttpClient(); 
 var imgData = await c.GetByteArrayAsync(imgUri);
imgData = File.ReadAllBytes(@"w:\input.png");

using var engine = new TesseractEngine("./tessdata", "eng", EngineMode.Default);
using var img = Pix.LoadFromMemory(imgData);
using var page = engine.Process(img);
var text = page.GetText();
Console.WriteLine(text);
1 Like

감사합니다. 아직은 구현 전이라 잘 읽어 본 후 코멘트 드리겠습니다.

2 Likes

Tesseract - tessdata 선택

tessdatatesseract-ocr (github.com)tessdata레파지토리에서 필요한 언어에 맞게 다운로드 받아 사용하면 됩니다.

목적에 따라 tessdata_fast, tessdata, tessdata_best를 선택할 수 있습니다. fast는 빠른대신 인식률이 떨어지고, best는 인식률이 좋은대신 속도가 느립니다.

1 Like

OpenCV - EAST Text Detection 테스트

관련 소스코드를 참조하여 코딩했습니다.
OpenCvSharp 패키지를 설치하고 Models/frozen_east_text_detection.pb가 있어야 합니다
EAST Text Detection Model 다운로드

image
image
image

using OpenCvSharp;
using OpenCvSharp.Dnn;

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Net.Http;

//var imageUri = "https://ganpaneasy.co.kr/uploads/cmallitem/2020/01/1d9a6d44adfe9b20bcbfb964b822be98.jpg";
var imageUri = "https://t1.daumcdn.net/thumb/R720x0/?fname=http://t1.daumcdn.net/brunch/service/user/3FXy/image/sqSmOMklFK34ylouQhXl08CPenw.png";

using var net = CvDnn.ReadNet("./Models/frozen_east_text_detection.pb");

using var c = new HttpClient();
var imageData = await c.GetByteArrayAsync(imageUri);
using var frame = Mat.FromImageData(imageData);
var (newW, newH) = (320, 320);
var rW = (float)frame.Width / newW;
var rH = (float)frame.Height / newH;
var newFrame = frame.Resize(new Size(newW, newH));
//Window.ShowImages(newFrame);
using var blob = CvDnn.BlobFromImage(newFrame, 1.0, new Size(newFrame.Width, newFrame.Height), new Scalar(123.68, 116.78, 103.94), swapRB: true, crop: false);

var outputLayers = new[] { "feature_fusion/Conv_7/Sigmoid", "feature_fusion/concat_3" };
net.SetInput(blob);
var output = new List<Mat>() { new(), new() };
net.Forward(output, outputLayers);

var scores = output[0];
var geometry = output[1];

var numRows = scores.Rows;
var numCols = scores.Cols;

var confThreshold = 0.5f;
Decode(scores, geometry, confThreshold, out var boxes, out var confidences);

var nmsThreshold = 0.4f;
CvDnn.NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, out var indices);

// Render detections.
var ratio = new Point2f(rW, rH);
for (var i = 0; i < indices.Length; ++i)
{
    RotatedRect box = boxes[indices[i]];

    Point2f[] vertices = box.Points();

    for (int j = 0; j < 4; ++j)
    {
        vertices[j].X *= ratio.X;
        vertices[j].Y *= ratio.Y;
    }

    for (int j = 0; j < 4; ++j)
    {
        Cv2.Line(frame, (int)vertices[j].X, (int)vertices[j].Y, (int)vertices[(j + 1) % 4].X, (int)vertices[(j + 1) % 4].Y, new Scalar(0, 255, 0), 3);
    }
}

// Optional - Save detections
var fileName = "output.jpg";
frame.SaveImage(Path.Combine(Path.GetDirectoryName(fileName), $"{Path.GetFileNameWithoutExtension(fileName)}_east.jpg"));

// -------------

static unsafe void Decode(Mat scores, Mat geometry, float confThreshold, out IList<RotatedRect> boxes, out IList<float> confidences)
{
    boxes = new List<RotatedRect>();
    confidences = new List<float>();

    if ((scores == null || scores.Dims != 4 || scores.Size(0) != 1 || scores.Size(1) != 1) ||
        (geometry == null || geometry.Dims != 4 || geometry.Size(0) != 1 || geometry.Size(1) != 5) ||
        (scores.Size(2) != geometry.Size(2) || scores.Size(3) != geometry.Size(3)))
    {
        return;
    }

    int height = scores.Size(2);
    int width = scores.Size(3);

    for (int y = 0; y < height; ++y)
    {
        var scoresData = new ReadOnlySpan<float>((void*)scores.Ptr(0, 0, y), height);
        var x0Data = new ReadOnlySpan<float>((void*)geometry.Ptr(0, 0, y), height);
        var x1Data = new ReadOnlySpan<float>((void*)geometry.Ptr(0, 1, y), height);
        var x2Data = new ReadOnlySpan<float>((void*)geometry.Ptr(0, 2, y), height);
        var x3Data = new ReadOnlySpan<float>((void*)geometry.Ptr(0, 3, y), height);
        var anglesData = new ReadOnlySpan<float>((void*)geometry.Ptr(0, 4, y), height);

        for (int x = 0; x < width; ++x)
        {
            var score = scoresData[x];
            if (score >= confThreshold)
            {
                float offsetX = x * 4.0f;
                float offsetY = y * 4.0f;
                float angle = anglesData[x];
                float cosA = (float)Math.Cos(angle);
                float sinA = (float)Math.Sin(angle);
                float x0 = x0Data[x];
                float x1 = x1Data[x];
                float x2 = x2Data[x];
                float x3 = x3Data[x];
                float h = x0 + x2;
                float w = x1 + x3;
                Point2f offset = new Point2f(offsetX + (cosA * x1) + (sinA * x2), offsetY - (sinA * x1) + (cosA * x2));
                Point2f p1 = new Point2f((-sinA * h) + offset.X, (-cosA * h) + offset.Y);
                Point2f p3 = new Point2f((-cosA * w) + offset.X, (sinA * w) + offset.Y);
                RotatedRect r = new RotatedRect(new Point2f(0.5f * (p1.X + p3.X), 0.5f * (p1.Y + p3.Y)), new Size2f(w, h), (float)(-angle * 180.0f / Math.PI));
                boxes.Add(r);
                confidences.Add(score);
            }
        }
    }
}
1 Like