[7] Nginx 서버 셋업 - Drogon 서버 OpenCV를 활용한 Text Detection API 구축


RHEL 환경에서 Text Detection을 Drogon 서버에 연동하여 
API 형태로 추출 및 Client에 보여지는 기능 개발

주의. OCR(Optical Character Recognition, 광학 문자 인식)이 아닌 Text Detection이 목적
Detection 이후 OCR은 추후에 연동 해보는걸로..

## 0. Text Detection 프레임 워크 서치 

| 프레임워크          | 언어            | 장점                                                                                             | 단점                                                                                          |
|---------------------|-----------------|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| Tesseract OCR       | C++ (Python 등) | 오픈 소스, 다국어 지원, 커스터마이징 가능                                                         | 전처리 없이는 성능 저하 가능, 텍스트 감지 기능이 따로 없으므로 OpenCV 등과 함께 사용 필요        |
| OpenCV + EAST 모델  | C++, Python     | 빠른 텍스트 감지, CPU 및 GPU 모두 지원, OpenCV와의 통합 용이                                      | 텍스트 감지만 지원하므로 OCR은 별도 필요, 고해상도 이미지 처리 시 성능에 제약 가능               |
| EasyOCR             | Python          | PyTorch 기반으로 다양한 언어 및 스타일 지원, 설치 및 사용 용이                                    | CPU 환경에서는 느릴 수 있음, 고성능 처리에는 GPU가 필요                                       |
| Amazon Textract (API)| 다중 언어 지원   | AWS 서버리스 배포 가능, 고급 기능 (테이블 및 양식 인식), 안정적인 성능                             | 대용량 데이터 처리 시 비용 발생, 클라우드 외 사용 불가 및 보안 고려 필요                       |
| Google Vision API (API) | 다중 언어 지원 | 다양한 텍스트 감지 및 이미지 분석 기능, GCP 서버리스 배포 가능                                    | 대용량 처리 시 비용 발생, 클라우드 외 사용 불가 및 민감 데이터의 보안 문제                     |
| PaddleOCR           | Python          | 아시아 언어에 뛰어난 성능, 경량화된 모델로 CPU에서도 빠른 처리 가능                               | PaddlePaddle 기반으로 PyTorch/TensorFlow 사용자에겐 진입 장벽, Python 기반으로 타 언어와 통합 필요 |


- 외부 API를 호출하는것은 제외한다.
- Detection 목적이기에 OpenCV + EAST 모델로 진행한다
- API 기능은 이미지 내 Text의 좌표 정보와, 언어검출 문자에 프레임을 씌워 새 이미지 생성 ($filename_detection.jpg)

---

## 1. OpenCV 셋업 및 필수 Library 설치 

아래 형식으로 진행 ( shell 로 우선 구현함 ) 

```

#!/bin/sh

sudo yum update -y
sudo yum groupinstall -y "Development Tools"
sudo yum install -y epel-release
sudo yum install -y cmake git gtk2-devel boost-devel
sudo yum install -y libjpeg-turbo-devel libpng-devel libtiff-devel
sudo yum install -y libdc1394-devel gstreamer1-devel gstreamer1-plugins-base-devel
sudo yum install -y tbb-devel

# OpenCV 4.10.0
wget https://github.com/opencv/opencv/archive/refs/tags/4.10.0.tar.gz
mv 4.10.0.tar.gz opencv.tar
tar -xvf opencv.tar
mv opencv-4.10.0 opencv

# OpenCV Contrib 4.10.0
wget https://github.com/opencv/opencv_contrib/archive/refs/tags/4.10.0.tar.gz
mv 4.10.0.tar.gz opencv_contrib.tar
tar -xvf opencv_contrib.tar
mv opencv_contrib-4.10.0 opencv_contrib

# OpenCV Build
cd ./opencv
mkdir build
cd build

# CMake
cmake -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
      -DCMAKE_BUILD_TYPE=Release \
      -DBUILD_SHARED_LIBS=ON \
      -DWITH_IPP=ON \
      -DWITH_TBB=ON \
      -DWITH_OPENMP=ON \
      -DENABLE_FAST_MATH=ON \
      -DCMAKE_INSTALL_PREFIX=/usr/local \
      -DBUILD_EXAMPLES=OFF \
      -DBUILD_TESTS=OFF \
      -DBUILD_PERF_TESTS=OFF \
          ..

# 빌드 및 설치
make -j$(nproc)  # 시스템의 모든 CPU 코어를 사용하여 빌드
sudo make install
sudo ldconfig  # 라이브러리 캐시 업데이트
```

시스템 경로에 우선 설치하도록 진행함 

---

## 2. API 개발

이전에서 하던 서버에 계속 이어서 진행한다.


2-1. Drogon Ctl 연결 

```
cd /root/drogon2/drogon/build/drogon_ctl 
drogon_ctl create controller TextDetectionController
```

2-2. TextDetection 구현
소스 위치할 경로 ( /root/drogon2/drogon/build/drogon_ctl/testAPI/controllers ) 

TextDetectionController.h
<pre class="brush:c++"
class="brush:plain; gutter:true">
#pragma once
#include <drogon/HttpController.h>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>

using namespace drogon;

void initializeModel();

class TextDetectionController : public HttpController<TextDetectionController>
{
public:
    std:: string _storagePath = "/root/storage/";
    METHOD_LIST_BEGIN
    // src에 있는 이밎 파일을 text detection 하여 dst로
    ADD_METHOD_TO(TextDetectionController::handleTextDetection, "/text-detection", Get);
    METHOD_LIST_END

    // 메서드 선언
    void handleTextDetection(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
    std::vector<cv::RotatedRect> decodeBoundingBoxes(const cv::Mat& scores, const cv::Mat& geometry, float scoreThresh);
};
</pre>

---

TextDetectionController.cc
<pre class="brush:c++"
class="brush:plain; gutter:true">
#include "TextDetectionController.h"
#include <drogon/utils/Utilities.h>
#include <fstream>

using namespace cv;
using namespace cv::dnn;
namespace {
    cv::dnn::Net eastNet;
    const std::string eastModelPath = "./frozen_east_text_detection.pb";
}

// OpenCV Initialize
// Model Init이 시간이 걸리기에, 전역으로 우선 뺏다
// 이건 thread unsafe 하기 때문에, 서버 로직으로는 개선이 필요하다
void initializeModel() {
    if (eastNet.empty()) {
        eastNet = cv::dnn::readNet(eastModelPath);
        if (eastNet.empty()) {
            throw std::runtime_error("Failed to load EAST model");
        }
    }
}


std::vector<cv::RotatedRect> TextDetectionController::decodeBoundingBoxes(const cv::Mat& scores, const cv::Mat& geometry, float scoreThresh)
{
    std::vector<cv::RotatedRect> detections;
    const int numRows = scores.size[2];
    const int numCols = scores.size[3];

    for (int y = 0; y < numRows; 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 < numCols; x++) {
            float score = scoresData[x];
            if (score < scoreThresh)
                continue;

            float offsetX = x * 4.0;
            float offsetY = y * 4.0;
            float angle = anglesData[x];
            float cosA = cos(angle);
            float sinA = sin(angle);
            float h = x0_data[x] + x2_data[x];
            float w = x1_data[x] + x3_data[x];

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

    return detections;
}

void TextDetectionController::handleTextDetection(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback)
{
    Json::Value respStr;
    HttpStatusCode code = k200OK;

    do
    {
        auto filename = req->getParameter("filename");
        if (filename.empty())
        {
            code = k404NotFound;
            break;
        }

        std::string path = _storagePath.c_str() + filename;
        // OpenCV Read Image From Server Local
        Mat image = imread(path);
        if (image.empty())
        {
            code = k404NotFound;
            break ;
        }

        // 원본 이미지 크기와 변환 비율 설정
        int origH = image.rows;
        int origW = image.cols;
        int newW = 320;
        int newH = 320;
        float rW = static_cast<float>(origW) / newW;
        float rH = static_cast<float>(origH) / newH;

        // blob 생성 및 네트워크 입력 설정
        Mat blob = blobFromImage(image, 1.0, Size(newW, newH), Scalar(123.68, 116.78, 103.94), true, false);
        eastNet.setInput(blob);

        // EAST 모델의 출력 레이어 설정
        std::vector<String> outputLayers = {"feature_fusion/Conv_7/Sigmoid", "feature_fusion/concat_3"};
        std::vector<Mat> outs;
        eastNet.forward(outs, outputLayers);

        // 텍스트 감지 수행
        std::vector<RotatedRect> detections = decodeBoundingBoxes(outs[0], outs[1], 0.7);
        if ( detections.size() <= 0 )
        {
            // Detection 된 텍스트 없으면 404 return
            code = k404NotFound;
            break;
        }

        // JSON 형식으로 결과 반환
        Json::Value jsonResponse;
        for (const auto& detection : detections) {
            Rect boundingBox = detection.boundingRect();
            boundingBox.x *= rW;
            boundingBox.y *= rH;
            boundingBox.width *= rW;
            boundingBox.height *= rH;

            Json::Value box;
            box["x"] = boundingBox.x;
            box["y"] = boundingBox.y;
            box["width"] = boundingBox.width;
            box["height"] = boundingBox.height;
            jsonResponse["detections"].append(box);
        }

        // 응답 생성
        auto response = HttpResponse::newHttpJsonResponse(jsonResponse);

        // Text 영역 사각형으로 처리
        // 감지된 텍스트 영역에 사각형 그리기
        for (const auto& detection : detections)
        {
            Rect boundingBox = detection.boundingRect();
            boundingBox.x *= rW;
            boundingBox.y *= rH;
            boundingBox.width *= rW;
            boundingBox.height *= rH;

            rectangle(image, boundingBox, Scalar(0, 255, 0), 2);  // 녹색 사각형 그리기
        }
        // 이미지 저장
        std::filesystem::path outputPath = _storagePath + (std::filesystem::path(path).stem().string() + "_detection.jpg");
        imwrite(outputPath.string(), image);

        callback(response);
        return ;
    }
    while(false);


    auto resp = HttpResponse::newHttpResponse();
    resp->setStatusCode(code);
    callback(resp);
}


</pre>

---
main.cc
<pre class="brush:c++"
class="brush:plain; gutter:true">
#include <drogon/drogon.h>
#include "controllers/TextDetectionController.h"

int main() {
    try
    {
        // 서버 초기화 시 모델 로드
        initializeModel();
    }
    catch (const std::exception &e)
    {
        std::cerr << "Error initializing model: "
                << e.what() << std::endl;
        return 1;  // 모델 로드에 실패하면 서버 실행 중단
    }
    drogon::app().loadConfigFile("../config.json");

    LOG_INFO << "Server RUN";
    drogon::app().run();

    return 0;
}
</pre>

2-3. drogon CMake 수정

```
cd /root/drogon2/drogon/build/drogon_ctl/testAPI
vi CMakeLists.txt
```

- 아래 형태로, 이전에 생성한 FileController.cc 뒤에 TextDetectionController.cc를 쓴다

```
작업필요 사항
# find_package(OpenCV CONFIG REQUIRED) 추가
# target_link_libraries에 ${OpenCV_LIBS} 추가

# OpenCV 경로가 기본 위치가 아닌 경우 경로 지정 (위 내용대로 햇으면 추가할 필요는 없음)
# set(OpenCV_DIR "/path/to/opencv")  # 예: /usr/local/opencv
---

add_executable(${PROJECT_NAME} main.cc controllers/FileController.cc controllers/TextDetectionController.cc)

# ##############################################################################
# If you include the drogon source code locally in your project, use this method
# to add drogon
# add_subdirectory(drogon)
# target_link_libraries(${PROJECT_NAME} PRIVATE drogon)
#
# and comment out the following lines
find_package(Drogon CONFIG REQUIRED)
find_package(OpenCV CONFIG REQUIRED) # 추가
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon ${OpenCV_LIBS}) # 추가
```


2-4. drogon Build
- cmake 이후 make 진행
```
cd /root/drogon2/drogon/build/drogon_ctl/testAPI/build
cmake .. 
make
```

---

## 3. 실행
3-1. 실행 전 모델 다운로드

```
cd /root/drogon2/drogon/build/drogon_ctl/testAPI/build

# detection model
wget https://www.dropbox.com/s/r2ingd0l3zt8hxs/frozen_east_text_detection.tar.gz?dl=1

./testAPI
```
---

3-2. 실행 및 업로드 
- 아래와 같은 Text Image 사용 (sample image)

<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWXSLU-khBE73xhSr4ynW1XqXHyh78HqGPfbRvKdQlZCs4ceJWucduH6XXVIlZg81Ym7qmpcZwyicyRygHm3oDZGAHGkQi-3aVLR82w0ysmL1c1Yi4myoSiOjsUI11P0VTbg0SAHkiOXMUJopg8-iOPD4tfM0c1_nv8217lWRZNELJUGYUAismZHSg/s354/image_in_text.PNG" style="display: block; padding: 1em 0; text-align: center; "><img alt="drogon 업로드" border="0" data-original-height="163" data-original-width="354" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWXSLU-khBE73xhSr4ynW1XqXHyh78HqGPfbRvKdQlZCs4ceJWucduH6XXVIlZg81Ym7qmpcZwyicyRygHm3oDZGAHGkQi-3aVLR82w0ysmL1c1Yi4myoSiOjsUI11P0VTbg0SAHkiOXMUJopg8-iOPD4tfM0c1_nv8217lWRZNELJUGYUAismZHSg/s320-rw/image_in_text.PNG" width="320"/></a></div>

---

- Upload를 통해 파일을 서버로 올림 (http://127.0.0.1:10099/doUpload/) (업로드 구현은 이전 페이지 참조)

<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivWzw0L6Afo-IZCq0NA5oXuo2pFFntc83oXx3LUTKYKIogyobEqiFuiPCnKtb1IZQaA9e2mqf5UNK0scdekPSjFrzPdBHB3x4SA9ss0QMlACBgyzE6bjEmToSm4VeZdXRrbIbJn7-Br_8-7Z4cieMqIF4MiQD-yhC2thyuH-6wE00KinepZJG_C7ag/s1600/upload.PNG" style="display: block; padding: 1em 0; text-align: center; "><img alt="업로드" border="0" data-original-height="210" data-original-width="398" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivWzw0L6Afo-IZCq0NA5oXuo2pFFntc83oXx3LUTKYKIogyobEqiFuiPCnKtb1IZQaA9e2mqf5UNK0scdekPSjFrzPdBHB3x4SA9ss0QMlACBgyzE6bjEmToSm4VeZdXRrbIbJn7-Br_8-7Z4cieMqIF4MiQD-yhC2thyuH-6wE00KinepZJG_C7ag/s1600-rw/upload.PNG"/></a></div>

---

- 업로드 완료 후 list 확인 (http://127.0.0.1:10099/list/) 

<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiGrWkbxfIISSvDdKchWtjegM8KgkRYnxySjEGEkQJWu1kNc_168DBJeGEkRqB3lYR7JIIVz3mjGz3pVatE5dNqa3BzOQWFOwTqH36cYzuoe2yR71u445u6WBpzNEXah8BlqbONYwoaHSTMAetPXXUrhN4Gu_m1nWVMyF-zFZW1V8iuvs4qWvwEVIR/s1600/pre-list.PNG" style="display: block; padding: 1em 0; text-align: center; "><img alt="리스팅" border="0" data-original-height="227" data-original-width="604" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiGrWkbxfIISSvDdKchWtjegM8KgkRYnxySjEGEkQJWu1kNc_168DBJeGEkRqB3lYR7JIIVz3mjGz3pVatE5dNqa3BzOQWFOwTqH36cYzuoe2yR71u445u6WBpzNEXah8BlqbONYwoaHSTMAetPXXUrhN4Gu_m1nWVMyF-zFZW1V8iuvs4qWvwEVIR/s1600-rw/pre-list.PNG"/></a></div>


---

- Detection API 호출 (`http://127.0.0.1:10099/text-detection?filename=image_in_text.PNG`) 

<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm8ttm4U2dTgFMHdVr_Auhw1PNFt_w7MyltrAXVR1MqKSvr6Sh20YFXn2CU1GZAarS_aVkt-dla94H8_nGgFbsqFhEoy54HqUWm3gt6AwJioRxOAROpaychxS8PJFcqfC7xtXxE-CkKOH6gYsJcU3zqoPOwp3og37VwGndSzzLqPc8yCTYncJNsWGA/s1600/response.PNG" style="display: block; padding: 1em 0; text-align: center; "><img alt="opencv text detection" border="0" data-original-height="753" data-original-width="919" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm8ttm4U2dTgFMHdVr_Auhw1PNFt_w7MyltrAXVR1MqKSvr6Sh20YFXn2CU1GZAarS_aVkt-dla94H8_nGgFbsqFhEoy54HqUWm3gt6AwJioRxOAROpaychxS8PJFcqfC7xtXxE-CkKOH6gYsJcU3zqoPOwp3og37VwGndSzzLqPc8yCTYncJNsWGA/s1600-rw/response.PNG"/></a></div>

---

- Detection 완료 후 list 확인 (http://127.0.0.1:10099/list/) 

<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJCP6ToJK5xBWvRyqplWaFRdsNTUv55C5zyBkJ01xxLYZVw5ppQiuv1Mpv1LSj-7WVQZJ4SiDtQ9LnBX8V_TN1_sw1askkZlI_AtqBwSSIIVmINpTGqQcQVU4DdKwz1luknEZXMvkOQwvc9tTFpgSlNb5yasLGUWy0CZbmki42DV6wDge7ahb7Togy/s1600/post-list.PNG" style="display: block; padding: 1em 0; text-align: center; "><img alt="list" border="0" data-original-height="251" data-original-width="654" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJCP6ToJK5xBWvRyqplWaFRdsNTUv55C5zyBkJ01xxLYZVw5ppQiuv1Mpv1LSj-7WVQZJ4SiDtQ9LnBX8V_TN1_sw1askkZlI_AtqBwSSIIVmINpTGqQcQVU4DdKwz1luknEZXMvkOQwvc9tTFpgSlNb5yasLGUWy0CZbmki42DV6wDge7ahb7Togy/s1600-rw/post-list.PNG"/></a></div>

---

- Detection 된 이미지 다운로드 후 확인 (`http://127.0.0.1:10099/download/image_in_text_detection.jpg`)

<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWys4fyTDyGLQTHLIEEfyJPow4NCVzUY2zIBbQkbjxNE-DjmtZc_DhLkmdFub3xotQaGAgHC_3S0luU5E2mSNMUTqgkSzPhMekynRwYDMD8wSuJZymFSK6o0D8RuryS8jrb9LrBCFmDnW_ZBxPkZWwIaGRLTO104Ch984N_34jQAomiJj3_vQ5EqGq/s354/image_in_text_detection.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="다운로드" border="0" data-original-height="163" data-original-width="354" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWys4fyTDyGLQTHLIEEfyJPow4NCVzUY2zIBbQkbjxNE-DjmtZc_DhLkmdFub3xotQaGAgHC_3S0luU5E2mSNMUTqgkSzPhMekynRwYDMD8wSuJZymFSK6o0D8RuryS8jrb9LrBCFmDnW_ZBxPkZWwIaGRLTO104Ch984N_34jQAomiJj3_vQ5EqGq/s320-rw/image_in_text_detection.jpg" width="320"/></a></div>

---

<!-- 목록을 표시할 HTML 컨테이너 -->
<div>
    <h3>Related Links</h3>
    <ul id="label-post-list">
        <!-- 여기에 게시물 목록이 추가됩니다 -->
    </ul>
</div>

---

<!-- 목록을 표시할 HTML 컨테이너 -->
<div>
    <h3>Recommended Link</h3>
    <ul id="label-post-list-include">
        <!-- 여기에 게시물 목록이 추가됩니다 -->
    </ul>
</div>

---



댓글

이 블로그의 인기 게시물

윤석열 계엄령 선포! 방산주 대폭발? 관련주 투자 전략 완벽 분석

대통령 퇴진운동 관련주: 방송·통신·촛불수혜주 완벽 분석

키움 OPEN API MFC 개발 (1)