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 기능을 구현하는게 목표입니다.
좋아요 2

.NET 5, C# 9

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

Visual Studio Preview (microsoft.com)

ML.NET

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

image

OpenCV Sharp

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

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

ML.NET 샘플

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

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

OCR 전처리 관련

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

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

좋아요 1

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

Open Neural Network Exchange - Wikipedia

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

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

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

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

ML.NET에서의 EAST Text Detector 관련

1차 정리

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

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

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

Tesseract (.NET) Samples

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

tessdata

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

Tesseract - tessdata 선택

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

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

OpenCV - EAST Text Detection 테스트

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

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);
            }
        }
    }
}