행복한 하루

[도서 실습] Qt 5 and OpenCV 4 Computer Vision (Literacy – EAST detector와 tesseract과 이용한 text 추출) with Raspberry Pi 본문

Programming/Qt

[도서 실습] Qt 5 and OpenCV 4 Computer Vision (Literacy – EAST detector와 tesseract과 이용한 text 추출) with Raspberry Pi

변화의 물결 2022. 6. 28. 14:06

 

 

안녕하세요.

 

 이전 내용에 추가해서 전체 이미지상에서 문자열을 바로 추출하는 것이 아니라, 텍스트 영역을 검색하는 detector를 추가해서 좀 더 정확한 문자열을 추출해봅니다.


1. EAST detector 학습된 자료 다운로드

  - OpenCV EAST(Efficient and Accurate Scene Text) text detector는 novel architecture training pattern 바탕으로 하는 deep learning model입니다. 그래서그래서 학습을 시키는 과정이 필요한데, 여기서는 미리 학습한 데이터를 이용하는 것으로 합니다.

 

  - 이전 내용 LiteracyW_day3에 폴더에 학습된 데이터를 다운로드합니다. (이 글에서는 day4로 새로 디렉터리를 생성해서 하기에 day4입니다.)  학습된 데이터는 약 97MB 정도 됩니다.

pi@raspberrypi:~/Qt_project/LiteracyW_day4 $ curl -O http://depot.kdr2.com/books/Qt-5-and-OpenCV-4-Computer-Vision-Projects/trained-model/frozen_east_text_detection.pb
pi@raspberrypi:~/Qt_project/LiteracyW_day4 $ ls -l

2. LiteracyW.pro 수정

  - 이전 LiteracyW.pro 파일 내용 하단에 opencv 헤더 파일 경로와 라이브러리 경로를 추가해줍니다. 여기서 EAST 알고리즘(deep neural network)을 사용하기 위해서 dnn 모듈을 추가합니다.

# config opencv
unix: {
    INCLUDEPATH += /usr/local/include/opencv4
    LIBS += -L/usr/local/lib/arm-linux-gnueabihf -lopencv_core -lopencv_imgproc -lopencv_dnn
}

3. mainwindow.h 수정

  - 문자 영역을 감지하기 위해서 deep neural network 인스턴스 cv::dnn::Net net 변수를 만듭니다. 문자 영역 감지 알고리즘을 사용할지 여부를 나타내는 체크박스 변수도 추가해줍니다.

 

  - detectTextAreas 함수는 OpenCV로 영역을 감지하기 위한 방법이며, decode 함수는 보조 방법으로 구성됩니다.

텍스트 영역을 감지 후에 이미지에 사각 박스를 그립니다. 따라서 이미지는 cv::Mat의 인스턴스로 리턴 값으로 받으므로 업데이트된 이미지를 UI에 표시하기 위해서는 cv::Mat의 인스턴스를 인자로 받는 showImage 함수를 오버로드해서 추가로 만듭니다.

    // ...
    #include <QCheckBox>
    // ...
    #include "opencv2/opencv.hpp"
    #include "opencv2/dnn.hpp"

    class MainWindow : public QMainWindow
    {
        // ...
    private:
        // ...
        void showImage(cv::Mat);
        // ...
        void decode(const cv::Mat& scores, const cv::Mat& geometry, float scoreThresh,
            std::vector<cv::RotatedRect>& detections, std::vector<float>& confidences);
        cv::Mat detectTextAreas(QImage &image, std::vector<cv::Rect>&);
        // ...
    private:
        // ...
        QCheckBox *detectAreaCheckBox;
        // ...
        cv::dnn::Net net;
    };

4. mainwindow.cpp 수정

  - 내용이 조금 많기 때문에 조금 나누어서 설명합니다. 이 부분에 많은 변수를 정의하고 있고 신경망에 대한 내용이 있습니다. 그리고 EAST AI 모델에 대해 이해가 필요하므로 최소 설명으로 진행하겠습니다.

 

  - 2개의 임계값(Threshold)은 신뢰도와 비최대 억제 알고리즘(NMS)에 관한 값이며, AI 모델의 탐지 결과를 필터링하는 데 사용됩니다. EAST 모델에서 이미지의 너비와 높이가 32 배수여야 하므로 크기를 320으로 정의합니다.

 

  - ReadNet 함수를 통해서 훈련된 모델 데이터를 불러옵니다. cv에서 여러 개의 모델 데이터 파일을 지원합니다.

*.caffemodel (Caffe)

*.pb (TensorFlow)

*.t7 or *.net (Torch)

*.weights (Darknet)

*.bin (DLDT)

  여기서는 TensorFlow 모델을 사용한 것을 알 수 있습니다. 

cv::Mat MainWindow::detectTextAreas(QImage &image, std::vector<cv::Rect> &areas)
{
        float confThreshold = 0.5;
        float nmsThreshold = 0.4;
        int inputWidth = 320;
        int inputHeight = 320;
        std::string model = "./frozen_east_text_detection.pb";
        // Load DNN network.
        if (net.empty()) {
            net = cv::dnn::readNet(model);
        }

  

   - 입력된 이미지를 모델로 보내서 텍스트 감지를 수행합니다.

  - 모델의 출력 레이어를 저장하기 위해 cv::Mat 벡터를 정의합니다. 그런 다음 DNN 모델에서 추출해야 하는 두 레이어의 이름을 layerNames 변수인 문자열 벡터에 넣습니다.

 

  - feature_fusion/Conv_7/Sigmoid는 Sigmoid 활성화의 출력 레이어입니다. 이 레이어의 데이터에는 주어진 영역에 텍스트가 포함되어 있는지 여부의 확률이 포함됩니다.

  - feature_fusion/concat_3은 특징 맵의 출력 레이어입니다. 이 레이어의 데이터는 이미지의 위치를 포함합니다. 나중에 이 레이어의 데이터를 디코딩하여 많은 경계 상자를 얻을 것입니다.

 

- 입력 이미지를 QImage에서 cv::Mat로 변환한 다음 DNN 모델의 입력으로 사용할 수 있는 4차원 Blob로 변환합니다. 즉, 입력층을 변환합니다. 그리고 blobFromImage 함수를 호출하여 중앙에서 이미지 크기 조정 및 자르기, 평균값 빼기, 스케일 값, R 및 B 채널 교체와 같은 많은 작업이 실행됩니다.

 

- blobFromImage 인자 설명

첫 번째 인자는 입력 이미지입니다.

두 번째 인자는 출력 이미지입니다.

세 번째 인자는 각 픽셀 값의 배율입니다. 여기에서 픽셀의 크기를 조정할 필요가 없기 때문에 1.0을 사용합니다.

네 번째 인자는 출력 이미지의 공간 크기입니다. 이 크기의 너비와 높이는 32의 배수여야 하며 여기에서 정의한 변수와 함께 320 x 320을 사용합니다.

다섯 번째 인수는 모델을 훈련하는 동안 사용되었기 때문에 각 이미지에서 빼야 하는 평균입니다. 여기서 사용된 평균은 (123.68, 116.78, 103.94)입니다. (모든 딥 러닝 아키텍처에서 mean subtraction과 스케일을 수행하는 것은 아닙니다. 그래서 이과정이 필요한지 확인하고 사용할 수 있는 사전 학습된 모델의 값이 무엇인지 확인해야 합니다.

여섯 번째 인수는 R 및 B 채널을 교환할지 여부입니다. 이것은 OpenCV가 BGR 형식을 사용하고 TensorFlow가 RGB 형식을 사용하기 때문에 필요합니다.

마지막 인수는 이미지를 자르고 중앙 자르기를 원하는지 여부입니다. 이 경우 false를 지정합니다.

 

  - 함수 호출이 반환된 후 DNN 모델의 입력으로 사용할 수 있는 blob을 얻습니다. 그런 다음 이를 신경망에 전달하고 모델의 setInput 함수와 forward 함수를 호출하여 출력 레이어를 가져오기 위해 전달 라운드를 수행합니다. 전달이 완료되면 원하는 두 개의 출력 레이어는 우리가 정의한 outs 벡터에 저장됩니다. 그 후에 이러한 출력 레이어를 처리하여 텍스트 영역을 가져오는 것입니다. (여기서, blob는 동일한 방식으로 전처리된 동일한 너비, 높이 및 채널 수를 가진 하나 이상의 이미지) 

        std::vector<cv::Mat> outs;
        std::vector<std::string> layerNames(2);
        layerNames[0] = "feature_fusion/Conv_7/Sigmoid";
        layerNames[1] = "feature_fusion/concat_3";

        cv::Mat frame = cv::Mat(
            image.height(),
            image.width(),
            CV_8UC3,
            image.bits(),
            image.bytesPerLine()).clone();

		cv::Mat blob;
        cv::dnn::blobFromImage(
            frame, blob,
            1.0, cv::Size(inputWidth, inputHeight),
            cv::Scalar(123.68, 116.78, 103.94), true, false        );
        net.setInput(blob);
        net.forward(outs, layerNames);

   

  - outs 벡터의 첫 번째 요소는 점수이고 두 번째 요소는 위치정보입니다. 그런 다음 MainWindow 클래스의 또 다른 메서드인 decode를 호출하여 방향과 함께 텍스트 상자의 위치를 알아냅니다. 이 디코딩 프로세스를 통해 후보 텍스트 영역을 cv::RotatedRect로 만들고 사각박스 변수에 저장합니다.

 

  - 텍스트 박스에 대해 많은 후보를 얻을 수 있으므로 가장 보기 좋은 텍스트 박스를 필터링해야 합니다. 이것은 최대가 아닌 최소화해야 합니다. 그래서 NMSBoxes 함수를 사용합니다. 이 함수 호출해서 디코딩된 상자, 신뢰도, 신뢰도 최소화하기 위한 임계값을 제공하고 제거되지 않은 상자의 마지막 색인을 저장합니다.

 

  - 디코딩 방법은 출력 레이어에서 신뢰도 및 상자 정보를 추출하는 데 사용됩니다.

구현은 https://github.com/opencv/opencv/blob/master/samples/dnn/text_detection.cpp에서 찾을 수 있습니다. 이를 이해하려면 DNN 모델의 데이터 구조, 특히 출력 계층의 데이터 구조를 이해해야 합니다. 그것은 이 현재 테스트의 범위를 넘어서기 때문에 관심이 있으면 https://arxiv.org/abs/1704.03155v2에서 EAST와 관련된 문서를 참조하면 됩니다. 또한 https://github.com/argman/EAST에서 Tensorflow를 사용한 구현 중 하나를 참조하면 됩니다.

 

  - 이제 모든 텍스트 영역을 cv::RotatedRect 인스턴스로 가져와 원본 입력 이미지에 매핑해야 합니다.

 

        cv::Mat scores = outs[0];
        cv::Mat geometry = outs[1];

        std::vector<cv::RotatedRect> boxes;
        std::vector<float> confidences;

        decode(scores, geometry, confThreshold, boxes, confidences);
        
        std::vector<int> indices;
        cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);

 

  - 텍스트 영역을 원본 이미지에 매핑하려면 DNN 모델로 보내기 전의 이미지 크기로 조정해주어야 합니다.

따라서 너비와 높이로 크기 조정 비율을 계산한 다음 cv::Point2f 비율로 저장합니다. 그런 다음 저장한 인덱스를 반복하여 각 cv::RotatedRect 객체를 가져옵니다.

  - 이러한 영역이 어떻게 나타나는지 보여주기 위해 원본 이미지에 사각 영역을 그리고 각 직사각형의 오른쪽 상단 모서리에 숫자를 나타냅니다.

 

        cv::Point2f ratio((float)frame.cols / inputWidth, (float)frame.rows / inputHeight);
        cv::Scalar green = cv::Scalar(0, 255, 0);

        for (size_t i = 0; i < indices.size(); ++i) {
            cv::RotatedRect& box = boxes[indices[i]];
            cv::Rect area = box.boundingRect();
            area.x *= ratio.x;
            area.width *= ratio.x;
            area.y *= ratio.y;
            area.height *= ratio.y;
            areas.push_back(area);
            cv::rectangle(frame, area, green, 1);
            QString index = QString("%1").arg(i);
            cv::putText(
                frame, index.toStdString(), cv::Point2f(area.x, area.y - 2),
                cv::FONT_HERSHEY_SIMPLEX, 0.5, green, 1
            );
        }
        return frame;
}

 

  - 기본적인 텍스트 영역 detect 하는 것이 끝났다면 UI 부분을 설정해줍니다.  MainWindow::createActions() 함수에서 체크박스를 만들어서 텍스트 영역 검색 기능을 사용할지를 선택할 수 있게 합니다. 

        detectAreaCheckBox = new QCheckBox("Detect Text Areas", this);
        fileToolBar->addWidget(detectAreaCheckBox);

  

  - “Detect Text Areas” 체크박스를 선택하면 detectTextAreas 함수를 호출하여 텍스트 영역을 감지합니다. 그러면 이미지를 텍스트 영역과 인덱스가 그려진 cv::Mat의 인스턴스로 반환한 다음 이 이미지와 함께 showImage 메서드를 호출하여 ImageView에 표시합니다.

 

  - 그런 다음 텍스트 영역 개수만큼 반복하면서 SetRectangle 함수를 호출하여 텍스트 영역을 Tesseract API에 보내 이 사각형 안에 있는 문자만 인식하도록 합니다. 그런 다음 인식된 텍스트를 가져와 에디터 창에 추가하고 추출한 문자열을 메모리를 해제합니다. 체크하지 않을 경우 기존 방식처럼 전체 이미지에 대해서 문자열을 추출하는 형식을 그대로 사용합니다.

 

         tesseractAPI->SetImage(image.bits(), image.width(), image.height(),
            3, image.bytesPerLine());

        if (detectAreaCheckBox->checkState() == Qt::Checked) {
            std::vector<cv::Rect> areas;
            cv::Mat newImage = detectTextAreas(image, areas);
            showImage(newImage);
            editor->setPlainText("");
            for(cv::Rect &rect : areas) {
                tesseractAPI->SetRectangle(rect.x, rect.y, rect.width, rect.height);
                char *outText = tesseractAPI->GetUTF8Text();
                editor->setPlainText(editor->toPlainText() + outText);
                delete [] outText;
            }
        } else {
            char *outText = tesseractAPI->GetUTF8Text();
            editor->setPlainText(outText);
            delete [] outText;
        }

   

  - extractText() 함수에서 tesseractAPI 할당되어 있는지 확인해서 오류가 발생할 수 있는 부분을 최소화합니다.

if (tesseractAPI == nullptr) {
        tesseractAPI = new tesseract::TessBaseAPI();
        // Initialize tesseract-ocr with English, with specifying tessdata path
        if (tesseractAPI->Init(TESSDATA_PREFIX, "eng")) {
            QMessageBox::information(this, "Error", "Could not initialize tesseract.");
            return;
        }
    }

  

  - mainwindow 소멸 함수에서 동적 생성한 TesseractAPI 있다면 사용을 끝났음을 알려주고 해제합니다.

    MainWindow::~MainWindow()
    {
        // Destroy used object and release memory
        if(tesseractAPI != nullptr) {
            tesseractAPI->End();
            delete tesseractAPI;
        }
    }

  

  - 마지막으로 cv:Mat 타입을 이미지로 출력해주기 위한 변환 showImage() 함수를 오버로드하여 구현해줍니다.

    void MainWindow::showImage(cv::Mat mat)
    {
        QImage image(
            mat.data,
            mat.cols,
            mat.rows,
            mat.step,
            QImage::Format_RGB888);

        QPixmap pixmap = QPixmap::fromImage(image);
        imageScene->clear();
        imageView->resetMatrix();
        currentImage = imageScene->addPixmap(pixmap);
        imageScene->update();
        imageView->setSceneRect(pixmap.rect());
    }

  

  - 도서 내용에 decode() 함수 부분이 구현 부분이 빠져 있기 때문에 소스를 보고 추가해야 합니다.

void MainWindow::decode(const cv::Mat& scores, const cv::Mat& geometry, float scoreThresh,
    std::vector<cv::RotatedRect>& detections, std::vector<float>& confidences)
{
    CV_Assert(scores.dims == 4); CV_Assert(geometry.dims == 4);
    CV_Assert(scores.size[0] == 1); CV_Assert(scores.size[1] == 1);
    CV_Assert(geometry.size[0] == 1);  CV_Assert(geometry.size[1] == 5);
    CV_Assert(scores.size[2] == geometry.size[2]);
    CV_Assert(scores.size[3] == geometry.size[3]);

    detections.clear();
    const int height = scores.size[2];
    const int width = scores.size[3];
    for (int y = 0; y < height; ++y) {
        const float* scoresData = scores.ptr<float>(0, 0, y);
        const float* x0_data = geometry.ptr<float>(0, 0, y);
        const float* x1_data = geometry.ptr<float>(0, 1, y);
        const float* x2_data = geometry.ptr<float>(0, 2, y);
        const float* x3_data = geometry.ptr<float>(0, 3, y);
        const float* anglesData = geometry.ptr<float>(0, 4, y);
        for (int x = 0; x < width; ++x) {
            float score = scoresData[x];
            if (score < scoreThresh)
                continue;

            // Decode a prediction.
            // Multiple by 4 because feature maps are 4 time less than input image.
            float offsetX = x * 4.0f, offsetY = y * 4.0f;
            float angle = anglesData[x];
            float cosA = std::cos(angle);
            float sinA = std::sin(angle);
            float h = x0_data[x] + x2_data[x];
            float w = x1_data[x] + x3_data[x];

            cv::Point2f offset(offsetX + cosA * x1_data[x] + sinA * x2_data[x],
                offsetY - sinA * x1_data[x] + cosA * x2_data[x]);
            cv::Point2f p1 = cv::Point2f(-sinA * h, -cosA * h) + offset;
            cv::Point2f p3 = cv::Point2f(-cosA * w, sinA * w) + offset;
            cv::RotatedRect r(0.5f * (p1 + p3), cv::Size2f(w, h), -angle * 180.0f / (float)CV_PI);
            detections.push_back(r);
            confidences.push_back(score);
        }
    }
}

5. 실행하기

  - 실행 시에 아래와 같이 오류가 발생한다면, 학습 데이터 경로 문제로 빌드하는 경로에 pb파일을 복사해줍니다.

 

pi@raspberrypi:~/Qt_project/LiteracyW_day4 $ cp frozen_east_text_detection.pb ../build-LiteracyW-Qt_5_15_2_in_PATH_qt5-Debug/.

 

  - 샘플 이미지 복사해옵니다.

pi@raspberrypi:~/Qt_project/LiteracyW_day4 $ sudo cp /usr/local/share/opencv4/samples/text/scenetext_segmented_word03.jpg .
pi@raspberrypi:~/Qt_project/LiteracyW_day4 $ sudo cp /usr/local/share/opencv4/samples/text/scenetext05.jpg .

 

  - TesseractAPI 만 사용했을 경우 Service를 못 찾았던 부분도 인식하였습니다. 그런데, Private 앞쪽선을 기호로 인식하였습니다.

 

 

  - 다른 것도 테스트해보았는데, 글자 자체는 잘 인식하였지만 양쪽 옆을 기호로 인식하는 오류가 있었습니다. 그래서 좀 더 정확도를 높이려면 글자 부분만 잘라 내는 전처리가 필요해 보였습니다.

 

감사합니다.

 

 

<참고 사이트>

1. Qt 5 and OpenCV 4 Computer Vision github

https://github.com/PacktPublishing/Qt-5-and-OpenCV-4-Computer-Vision-Projects

2. [ OCR ] 문자 추출 및 인식 (EAST text Detector Model) - Python

https://yunwoong.tistory.com/75

3. [DL] NMS, Non-Maximum Suppression 파헤치기

https://velog.io/@bolero2/DL-Principle-of-NMS

4. Deep learning: How OpenCV’s blobFromImage works

https://pyimagesearch.com/2017/11/06/deep-learning-opencvs-blobfromimage-works/

5. [이론] OpenCV와 딥러닝 기초-5 DNN 프로세스

https://m.blog.naver.com/tommybee/222067664722

LiteracyW_day4.zip
0.20MB

 

 

Comments