opencv

스캔 이미지 중 여러 영수증 분리하기

꼰대코더 2025. 2. 15. 23:56

과제

아래와 같이 영수증을 모아 스캔한 이미지가 있고 영수증 영역을 각각 분리하고자 한다.

 

아이디어

1. 위에 4장은 세로로 길고 아래 한장은 옆으로 눕혀있다.

2. 위의 4장은 세로이므로 흑백 이미지로 변환한 후 세로로 길게 (3 x 40) 모펄러지 필터를 적용하자

3. 2의 필터로 문자들간에 거리가 40픽셀 이하는 하나로 묶여지게 되었고 큰 덩어리로 서로 분리 되었다.
    큰 틀안에 독립된 덩어리들도 존재하지만 5번에서 처리.

4. findContours 실시하여 먼저 덩어리의 면적 순으로 내림정렬를 시킨다. 

5. 작은 덩어리들은 큰 덩어리 내 혹은 겹치지 않는지 조사하여 합체 시키고, 외에 존재하는 덩어리들은 한 묶음으로 하기 위해 시작점과 끝점을 벡터에 등록한다.

6. 큰 덩어리는 빨간색으로 작은 덩어리들의 좌표들은 boundingRect 으로 바운더리를 구한 후 녹색으로 영역을 표시

 

흑백 이미지로 변환 ( 문자는 백, 배경은 흑)

    cv::Mat img = cv::imread("d:\\image.jpg");

    cv::Mat gray;
    cv::cvtColor(img, gray, CV_BGR2GRAY);
    cv::GaussianBlur(gray, gray, cv::Size(3, 3), 0.0);
    cv::Mat thresh;
    //cv::adaptiveThreshold(gray, thresh, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 11, 2);
    cv::threshold(gray, thresh, 0, 255, cv::THRESH_OTSU | cv::THRESH_BINARY_INV);

 

모펄러지 필터 적용

    cv::Mat kern = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 40 + 1));
    cv::morphologyEx(thresh, thresh, cv::MORPH_DILATE, kern);

 

findContours 실시 후 면적으로 내림 차순 정렬

    std::vector< std::vector< cv::Point > > contours;
    std::vector< cv::Vec4i > hierarchy;
    cv::findContours(thresh, contours, hierarchy, cv::RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);

    int receiptMinSize = 80;

    std::vector receiptRects;
    std::vector blobs;

    sort(contours.begin(), contours.end(), [](const std::vector& c1, const std::vector& c2) {
        return cv::contourArea(c1, false) > cv::contourArea(c2, false);
        });

 

영수증의 최소 사이즈를 정의하고 큰 덩어리는 receiptRects 벡터에 넣고
작은 덩어리들은 기존 receiptRects 에 등록된 Rect와 곂치면 합치고 그 외에는 따로 blobs 벡터에 포이터를 등록

    int receiptMinSize = 80;

    std::vector< cv::Rect > receiptRects;
    std::vector< cv::Point > blobs;

    for (size_t i = 0; i < contours.size(); i++) {
        cv::Rect rect = cv::boundingRect(contours[i]);

        if (std::min(rect.width, rect.height) > receiptMinSize) {
            receiptRects.push_back(rect);
        }
        else {
            bool isInside = false;
            for (cv::Rect& rt : receiptRects) {
                if ((rt & rect).area() > 0) {
                    rt = rt | rect;
                    isInside = true;
                    break;
                }
            }

            if (isInside) continue;

            blobs.push_back(cv::Point(rect.x, rect.y));
            blobs.push_back(cv::Point(rect.x + rect.width, rect.y + rect.height));
        }
    }

 

큰 덩어리들은 청색으로 작은 덩어리들은 하나로 합친 후 녹색으로 표시

    for (cv::Rect& rt : receiptRects) {
        cv::rectangle(img, rt, cv::Scalar(255, 0, 0), 2);
    }

    cv::Rect rect = cv::boundingRect(blobs);
    cv::rectangle(img, rect, cv::Scalar(0, 255, 0), 2);

    cv::imwrite("d:\\result.png", img);

    return 0;

 

'opencv' 카테고리의 다른 글

Barcode 읽기  (0) 2025.02.23
close contours (convexHull 사용)  (0) 2025.02.16
자동차 번호판 추출하기  (1) 2025.02.15
Color를 Grayscale 로 변환 (커스텀 비율)  (0) 2025.02.14
C# 와 C++/CLI Dll 사이의 이미지 전달  (0) 2025.02.10