본문 바로가기
opencv

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

by 꼰대코더 2025. 2. 15.

과제

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

 

아이디어

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;