[OpenCV] 2주차 실습

haeryong·2022년 12월 7일
0

1일차

이동 변환과 전단 변환

이동 변환

 cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, CV_8UC1);

    for (int y = 0; y < src.rows; y++)
    {
        for(int x = 0; x < src.cols; x++)
        {
            int x_ = x - 200;
            int y_ = y + 100;
            if(x_ < 0 || x_ >= dst.cols) continue;
            if(y_ < 0 || y_ >= dst.rows) continue;
            dst.at<uchar>(y_, x_) = src.at<uchar>(y, x);
        }
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

전단 변환

    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst(src.rows, src.cols*3/2, src.type(), cv::Scalar(0));

    double m(0.5);
    
    for (int y = 0; y < src.rows; y++)
    {
        for(int x = 0; x < src.cols; x++)
        {
            int nx = int(x + m*y);
            int ny = y;
            dst.at<uchar>(ny, nx) = src.at<uchar>(y, x);
        }
    }
    //cv::Mat aff = (cv::Mat_<float>(2, 3) << 1, 0.5, 0, 0, 1, 0);  // 같은 결과
    //cv::warpAffine(src, dst, aff, cv::Size(src.cols*3/2, src.rows));
    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

영상의 크기 변환, 보간법

영상 크기 변환

backward mapping

    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst= cv::Mat::zeros(src.rows * 2, src.cols * 2, src.type());

    // backward mapping
    for(int y_ = 0; y_ < dst.rows; y_++)
    {
        for(int x_ = 0; x_ < dst.cols; x_++)
        {
            int x = x_ / 2;
            int y = y_ / 2;
            dst.at<uchar>(y_, x_) = src.at<uchar>(y, x);
        }
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

보간법

양선형 보간법 구현

    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst;
    
    dst.create(src.rows*3/2, src.cols*3/2, CV_8U);

    int x1, y1, x2, y2;
    double rx, ry, p, q, value;

    // scaling factor
    double sx = static_cast<double>(src.cols - 1) / (dst.cols - 1);
    double sy = static_cast<double>(src.rows - 1) / (dst.rows - 1);

    for (int y = 0; y < dst.rows; y++)
    {
        for(int x = 0; x < dst.cols; x++)
        {
            rx = sx * x;
            ry = sy * y;
            x1 = cvFloor(rx);
            y1 = cvFloor(ry);
            x2 = x1 + 1; if(x2 == src.cols) x2 = src.cols - 1;
            y2 = y1 + 1; if(y2 == src.rows) y2 = src.rows - 1;
            p  = rx - x1;
            q  = ry - y1;

            value = (1. - p) * (1. - q) * src.at<uchar>(y1, x1)
                + p * (1. - q) * src.at<uchar>(y1, x2)
                + (1. - p) * q * src.at<uchar>(y2, x1)
                + p * q * src.at<uchar>(y2, x2);
            
            dst.at<uchar>(y, x) = static_cast<uchar>(value + .5); 
        }
    }


    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

cv::resize()


    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst;
    cv::resize(src, dst, cv::Size(), 1.5, 1.5, cv::INTER_NEAREST);
    // INTER_LINEAR, INTER_CUBIC, INTER_LANCZOS4, INTER_AREA(축소 시 사용)

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

영상의 회전, 대칭 변환

영상의 회전 변환

    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    float degree = 45.; // 45도 회전
    cv::Point2f pt(src.cols / 2.f, src.rows / 2.f); // center 기준으로 회전.

    cv::Mat rot = cv::getRotationMatrix2D(pt, degree, 1.0); 

    cv::Mat dst;
    cv::warpAffine(src, dst, rot, cv::Size());

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

대칭 변환

    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst;

    cv::flip(src, dst, 1); // 0 = 상하대칭, -1 = 좌우&상하대칭

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

어파인 변환, 투시 변환

어파인 변환

    cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    std::vector<cv::Point2f> src_pts = {cv::Point2f(0, 0), 
    cv::Point2f(0, src.rows-1), cv::Point2f(src.cols-1, src.rows-1)};

    std::vector<cv::Point2f> dst_pts = {cv::Point2f(0, 0), 
    cv::Point2f(0, src.rows-1), cv::Point2f(src.cols/2, src.rows/2)};

    cv::Mat mat = cv::getAffineTransform(src_pts, dst_pts);


    cv::Mat dst;
    cv::warpAffine(src, dst, mat, cv::Size());


    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

투시 변환

    cv::Mat src = cv::imread("lane01.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    std::vector<cv::Point2f> src_pts(4), dst_pts(4);

    int w = 500, h = 260;

    src_pts[0] = cv::Point2f(470, 400);
    src_pts[1] = cv::Point2f(710, 400);
    src_pts[2] = cv::Point2f(860, 530);
    src_pts[3] = cv::Point2f(370, 530);

    dst_pts[0] = cv::Point2f(0, 0);
    dst_pts[1] = cv::Point2f(w-1, 0);
    dst_pts[2] = cv::Point2f(w-1, h-1);
    dst_pts[3] = cv::Point2f(0, h-1);

    cv::Mat mat = cv::getPerspectiveTransform(src_pts, dst_pts);

    cv::Mat dst;
    cv::warpPerspective(src, dst, mat, cv::Size(w, h));

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

리매핑

    cv::Mat src = cv::imread("tekapo.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat map_x = cv::Mat::zeros(src.rows, src.cols, CV_32FC1);
    cv::Mat map_y = cv::Mat::zeros(src.rows, src.cols, CV_32FC1);

    for (int y = 0; y < src.rows; y++)
    {
        for(int x = 0; x < src.cols; x++)
        {
            map_x.at<float>(y, x) = (float)x;
            map_y.at<float>(y, x) = (float)src.rows -1 - y; // 상하대칭
        }
    }

    cv::Mat dst;
    cv::remap(src, dst, map_x, map_y, cv::INTER_LINEAR);


    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

2일차

컬러 영상

컬러 영상 반전

산술 연산

    cv::Mat src = cv::imread("tekapo.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst = cv::Scalar(255, 255, 255) - src;
    //cv::Mat dst = ~src;
    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

픽셀 값 접근

    cv::Mat src = cv::imread("tekapo.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst(src.rows, src.cols, CV_8UC3);

    for(int y = 0; y < src.rows; y++)
    {
        for(int x = 0; x < src.cols; x++)
        {
            cv::Vec3b& p = src.at<cv::Vec3b>(y, x);
            cv::Vec3b& q = dst.at<cv::Vec3b>(y, x);

            // p[0], p[1] .. 으로 접근하거나
            q = cv::Vec3b(255, 255, 255) - p;
        }
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

컬러 -> 그레이스케일 변환

구현

    cv::Mat src = cv::imread("tekapo.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst(src.rows, src.cols, CV_8UC1);

    for(int y = 0; y < src.rows; y++)
    {
        for(int x = 0; x < src.cols; x++)
        {
            cv::Vec3b& p = src.at<cv::Vec3b>(y, x);
            //dst.at<uchar>(y, x) = (uchar)((299*p[2] + 587*p[1] + 114*p[0]) / 1000);
            dst.at<uchar>(y, x) = (4899*p[2]+9617*p[1]+1868*p[0]) >> 14;
        }
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

cv::cvtColor()
COLOR_BGR2GRAY, COLOR_BGR2HSV, COLOR_BGR2YCrCb 등..

    cv::Mat src = cv::imread("tekapo.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }    

    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY);

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

컬러 영상 히스토그램 평활화

YCrCb로 변환해 밝기 성분에 해당하는 Y만 평활화한 후 다시 BGR로 변환.

    cv::Mat src = cv::imread("peppers.bmp");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }    

    cv::Mat src_ycrcb;
    cv::cvtColor(src, src_ycrcb, cv::COLOR_BGR2YCrCb);

    std::vector<cv::Mat> planes;
    cv::split(src_ycrcb, planes);

    cv::equalizeHist(planes[0], planes[0]);

    cv::Mat dst_ycrcb;
    cv::merge(planes, dst_ycrcb);

    cv::Mat dst;
    cv::cvtColor(dst_ycrcb, dst, cv::COLOR_YCrCb2BGR);

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

특정 색상 영역 추출

색상 범위 지정

HSV: h(0 ~ 179), s(0 ~ 255), v(0 ~ 255)
트랙바를 이용해 Hue, Saturation threshold 조절.

int h_1(50), h_2(80), s_1(150), s_2(255);
cv::Mat src_, hsv_, mask_, dst_;

void callback(int, void*)
{
    // mask 생성
    cv::Scalar lowerb(h_1, s_1, 0);
    cv::Scalar upperb(h_2, s_2, 255);
    cv::inRange(hsv_, lowerb, upperb, mask_);

    cv::cvtColor(src_, dst_, cv::COLOR_BGR2GRAY);
    cv::cvtColor(dst_, dst_, cv::COLOR_GRAY2BGR);
    src_.copyTo(dst_, mask_);

    cv::imshow("mask", mask_);
    cv::imshow("dst", dst_);
}


int main()
{
    src_ = cv::imread("candies.png");

    if (src_.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }    

    cv::cvtColor(src_, hsv_, cv::COLOR_BGR2HSV);

    cv::namedWindow("src");
    cv::namedWindow("mask");
    cv::namedWindow("dst");

    cv::imshow("src", src_);

    cv::createTrackbar("Lower Hue", "dst", &h_1, 179, callback);
    cv::createTrackbar("Upper Hue", "dst", &h_2, 179, callback);
    cv::createTrackbar("Lower Sat", "dst", &s_1, 255, callback);
    cv::createTrackbar("Upper Sat", "dst", &s_2, 255, callback);


    while(cv::waitKey(0) != 'q')
        continue;

    return 0;
}

히스토그램 역투영

마우스로 선택한 영역과 유사한 색상 영역들을 검출.

    cv::Mat src = cv::imread("cropland.png");

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }   

    cv::Rect rect = cv::selectROI(src);

    cv::Mat src_ycrcb;
    cv::cvtColor(src, src_ycrcb, cv::COLOR_BGR2YCrCb);

    cv::Mat crop = src_ycrcb(rect);

    cv::Mat hist;
    int channels[] = {1, 2}; // cr, cb channel
    int cr_bins = 128;
    int cb_bins = 128;
    int histSize[] = {cr_bins, cb_bins};
    float cr_range[] = {0, 256};
    float cb_range[] = {0, 256};
    const float* ranges[] = {cr_range, cb_range};

    cv::calcHist(&crop, 1, channels, cv::Mat(), hist, 2, histSize, ranges);

    // mask 생성
    cv::Mat backproj;
    cv::calcBackProject(&src_ycrcb, 1, channels, hist, backproj, ranges);

    //cv::GaussianBlur(backproj, backproj, cv::Size(), 1.0);
    //backproj = backproj > 50;

    cv::Mat dst;
    src.copyTo(dst, backproj);

    //cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

3일차

에지 검출과 소벨 필터

	cv::Mat src = cv::imread("lenna.bmp", cv::IMREAD_GRAYSCALE);
    
    cv::Mat dx, dy;
    cv::Sobel(src, dx, CV_32FC1, 1, 0); // x방향 gradient
    cv::Sobel(src, dy, CV_32FC1, 0, 1); // y방향 gradient

    cv::Mat mag; // magnitude
    cv::magnitude(dx, dy, mag);
    mag.convertTo(mag, CV_8UC1);

    cv::Mat dst = mag > 120; // threshold

    cv::imshow("src", src);
    cv::imshow("mag", mag);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

케니 에지 검출기

    cv::Mat src = cv::imread("circuit.bmp", cv::IMREAD_GRAYSCALE);

    cv::Mat dst;
    cv::Canny(src, dst, 50, 150);    

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

허프 직선, 원 검출

houghline, houghlinesP

    cv::Mat src = cv::imread("building.jpg", cv::IMREAD_GRAYSCALE);

    cv::Mat edge;
    cv::Canny(src, edge, 50, 150);

    std::vector<cv::Vec2f> lines1;
    cv::HoughLines(edge, lines1, 1, CV_PI / 180, 250);

    // draw hough lines

    cv::Mat dst1;
    cv::cvtColor(edge, dst1, cv::COLOR_GRAY2BGR);

    for(const cv::Vec2f& line : lines1)
    {
        float r = line[0], t = line[1];
        double cos = std::cos(t), sin = std::sin(t);
        double x = r * cos, y = r * sin;
        double alpha = 1000;

        cv::Point pt1(cvRound(x + alpha * (-sin)), cvRound(y + alpha * cos));
        cv::Point pt2(cvRound(x - alpha * (-sin)), cvRound(y - alpha * cos));
        cv::line(dst1, pt1, pt2, cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
    }

    std::vector<cv::Vec4i> lines2;
    cv::HoughLinesP(edge, lines2, 1, CV_PI / 180, 160, 50, 5);

    cv::Mat dst2;
    cv::cvtColor(edge, dst2, cv::COLOR_GRAY2BGR);

    for(const cv::Vec4i& line : lines2)
    {
        cv::line(dst2, cv::Point(line[0], line[1]), cv::Point(line[2], line[3])
            , cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
    }


    cv::imshow("src", src);
    cv::imshow("dst1", dst1);
    cv::imshow("dst2", dst2);
    
    while(cv::waitKey(0) != 'q')
        continue;

hough circles

    cv::Mat src = cv::imread("coins.jpg", cv::IMREAD_GRAYSCALE);

    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

    std::vector<cv::Vec3f> circles;
    cv::HoughCircles(src, circles, cv::HOUGH_GRADIENT_ALT, 1.5, 10, 300, 0.9, 20, 50);

    for(const cv::Vec3f& circle : circles)
    {
        cv::Vec3i c = circle;
        cv::circle(dst, cv::Point(c[0], c[1]), c[2], cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);
    
    
    while(cv::waitKey(0) != 'q')
        continue;

코너 검출

good features to track / FAST corner detection

    cv::Mat src = cv::imread("building.jpg", cv::IMREAD_GRAYSCALE);

    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    // GFTT

    std::vector<cv::Point2f> corners;
    cv::goodFeaturesToTrack(src, corners, 400, 0.01, 10);

    cv::Mat dst1;
    cv::cvtColor(src, dst1, cv::COLOR_GRAY2BGR);

    for(const auto& corner : corners)
    {
        cv::circle(dst1, corner, 5, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
    }

    // FAST

    std::vector<cv::KeyPoint> kps;
    cv::FAST(src, kps, 60);

    cv::Mat dst2;
    cv::cvtColor(src, dst2, cv::COLOR_GRAY2BGR);

    for(const auto& kp : kps)
    {
        cv::circle(dst2, cv::Point(kp.pt.x, kp.pt.y), 5, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
    }

    cv::imshow("src", src);
    cv::imshow("GFTT", dst1);
    cv::imshow("FAST", dst2);
    
    while(cv::waitKey(0) != 'q')
        continue;

4일차

영상의 이진화

int t_val = 128;
cv::Mat src_, dst_;

void on_trackbar_threshold(int, void*)
{
    cv::threshold(src_, dst_, t_val, 255, cv::THRESH_BINARY_INV);
    cv::imshow("dst", dst_);
}

int main()
{
s
    src_ = cv::imread("neutrophils.png", cv::IMREAD_GRAYSCALE);

    if(src_.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::namedWindow("src");
    cv::imshow("src", src_);

    cv::namedWindow("dst");
    cv::createTrackbar("Threshold", "dst", &t_val, 255, on_trackbar_threshold);
    on_trackbar_threshold(0, 0);
    
    while(cv::waitKey(0) != 'q')
        continue;


    return 0;
}

자동 임계값 결정

OTSU 이진화
히스토그램 분포가 bimodal 형태인 경우 사용.

    cv::Mat src = cv::imread("rice.png", cv::IMREAD_GRAYSCALE);
    
    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat dst;
    double threshold = cv::threshold(src, dst, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); // 0, 255는 사용되지 않는다. BINARY_INV를 대신 넣어주면 반전됨.

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

지역 이진화

영상의 조명이 균일하지 않을 때 효과적인 기법.
원본 이미지를 16개로 나눠 각각 OTSU 이진화 수행.

    cv::Mat src = cv::imread("rice.png", cv::IMREAD_GRAYSCALE);
    
    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    int w = src.cols / 4;
    int h = src.rows / 4;

    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, CV_8UC1);
    double threshold = cv::threshold(src, dst, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

    for(int y = 0; y < 4; y++)
    {
        for(int x = 0; x < 4; x++)
        {
            cv::Mat src_ = src(cv::Rect(x*w, y*h, w, h));
            cv::Mat dst_ = dst(cv::Rect(x*w, y*h, w, h));
            cv::threshold(src_, dst_, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
        }
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

adaptive threshold

각각의 픽셀마다 다른 threshold 사용.
blocksize(6번째 인자) 크기의 window를 생성. window 내부 픽셀 값의 평균값을 사용.
평균값에서 C(7번째 인자)값을 뺀 값이 threshold가 됨.
4번째 인자

  • ADAPTIVE_THRESH_MEAN_C
  • ADAPTIVE_THRESH_GAUSSIAN_C
int block_size = 51;
cv::Mat src_, dst_;

void on_trackbar(int, void*)
{
    int bsize = block_size;
    std::cout << bsize << std::endl;
    if((bsize & 1) == 0)
        bsize--;
    
    if(bsize < 3)
        bsize = 3;
    
    std::cout << "->" << bsize << std::endl;

    cv::adaptiveThreshold(src_, dst_, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, 
        cv::THRESH_BINARY, bsize, 5);
    cv::imshow("dst", dst_);
}



int main()
{
    src_ = cv::imread("sudoku.jpg", cv::IMREAD_GRAYSCALE);

    if(src_.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::namedWindow("src");
    cv::imshow("src", src_);

    cv::namedWindow("dst");
    cv::createTrackbar("Block size", "dst", &block_size, 201, on_trackbar);
    on_trackbar(0, 0);
    
    while(cv::waitKey(0) != 'q')
        continue;

    return 0;
}

모폴로지

침식(erode) 연산: 작은 크기의 잡음 제거 효과.
팽창(dilation) 연산: 객체 외곽을 확대시키는 효과.

cv::erode(src, dst, cv::Mat());
cv::dilate(src, dst, cv::Mat());

// 3번째 인자를 직접 입력할 경우
// cv::getStructuringElement() 함수로 kernel을 생성 가능.

열기(opening) 연산: 침식 -> 팽창
닫기(closing) 연산: 팽창 -> 침식

    cv::Mat src = cv::imread("rice.png", cv::IMREAD_GRAYSCALE);
    
    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }
	
    // local threshold
    
    int w = src.cols / 4;
    int h = src.rows / 4;

    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, CV_8UC1);

    for(int y = 0; y < 4; y++)
    {
        for(int x = 0; x < 4; x++)
        {
            cv::Mat src_ = src(cv::Rect(x*w, y*h, w, h));
            cv::Mat dst_ = dst(cv::Rect(x*w, y*h, w, h));
            cv::threshold(src_, dst_, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
        }
    }

    cv::Mat dst2;
    cv::morphologyEx(dst, dst2, cv::MORPH_OPEN, cv::Mat()); // MORPH_CLOSE ...

    cv::imshow("src", src);
    cv::imshow("dst", dst);
    cv::imshow("dst2", dst2);
    while(cv::waitKey(0) != 'q')
        continue;

레이블링, 외곽선 검출

레이블링

레이블링 함수

cv::Mat labels;
int N = cv::connectedComponents(src, labels);
// N = 객체+배경 갯수 (0은 배경, 1~N-1은 객체)

객체 정보를 함께 반환하는 레이블링 함수

cv::Mat labels, stats, centroids; 
// stats -> 객체별 바운딩박스 + 픽셀 개수 행렬(N by 5)
// centroids -> 객체별 무게중심 위치 행렬(N by 2)
int N = connectedComponentsWithStats(src, labels, stats, centroids);

예제

    uchar data[] = {
		0, 0, 1, 1, 0, 0, 0, 0,
		1, 1, 1, 1, 0, 0, 1, 0,
		1, 1, 1, 1, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 1, 1, 0,
		0, 0, 0, 1, 1, 1, 1, 0,
		0, 0, 1, 1, 0, 0, 1, 0,
		0, 0, 1, 1, 1, 1, 1, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
	};

    cv::Mat src(8, 8, CV_8UC1, data);

    cv::Mat labels, stats, centroids;
    int num_labels = cv::connectedComponentsWithStats(src, labels, stats, centroids);

    std::cout << "src:\n" << src << std::endl;
    std::cout << "num of labels: " << num_labels<< std::endl;
    std::cout << "labels:\n" << labels << std::endl;
    std::cout << "stats:\n" << stats << std::endl;
    std::cout << "centroids:\n" << centroids << std::endl;
output:
src:
[  0,   0,   1,   1,   0,   0,   0,   0;
   1,   1,   1,   1,   0,   0,   1,   0;
   1,   1,   1,   1,   0,   0,   0,   0;
   0,   0,   0,   0,   0,   1,   1,   0;
   0,   0,   0,   1,   1,   1,   1,   0;
   0,   0,   1,   1,   0,   0,   1,   0;
   0,   0,   1,   1,   1,   1,   1,   0;
   0,   0,   0,   0,   0,   0,   0,   0]
num of labels: 4
labels:
[0, 0, 1, 1, 0, 0, 0, 0;
 1, 1, 1, 1, 0, 0, 2, 0;
 1, 1, 1, 1, 0, 0, 0, 0;
 0, 0, 0, 0, 0, 3, 3, 0;
 0, 0, 0, 3, 3, 3, 3, 0;
 0, 0, 3, 3, 0, 0, 3, 0;
 0, 0, 3, 3, 3, 3, 3, 0;
 0, 0, 0, 0, 0, 0, 0, 0]
stats:
[0, 0, 8, 8, 39;
 0, 0, 4, 3, 10;
 6, 1, 1, 1, 1;
 2, 3, 5, 4, 14]
centroids:
[3.615384615384615, 3.692307692307693;
 1.7, 1.2;
 6, 1;
 4.285714285714286, 4.785714285714286]

키보드 영상에서 문자 영역 분할 예제

    cv::Mat src = cv::imread("keyboard.bmp", cv::IMREAD_GRAYSCALE);
    
    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat src_bin;
    cv::threshold(src, src_bin, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

    cv::Mat labels, stats, centroids;
    int cnt = cv::connectedComponentsWithStats(src_bin, labels, stats, centroids);

    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

    for(int i = 1; i < cnt; i++)
    {
        int* p = stats.ptr<int>(i);

        if (p[4] < 20) continue; // pixel 개수가 20 미만이면(너무 작은 객체) continue
        cv::rectangle(dst, cv::Rect(p[0], p[1], p[2], p[3]), cv::Scalar(0, 255, 255));
    }


    cv::imshow("src", src);
    cv::imshow("dst", dst);
    while(cv::waitKey(0) != 'q')
        continue;s

외곽선 검출

외곽선 검출 함수

void cv::findContours(src, vector<vector<Point>> contours, vector<Vec4i> hierarchy, int mode, int method);

// hierarchy[i] : next, prev, child, parent를 담고 있음.
// mode : 외곽선 검출모드 RETR_EXTERNAL, RETR_LIST, RETR_CCOMP, RETR_TREE 중 선택.
// method : 외곽선 근사화방법 CHAIN_APPROX_NONE or CHAIN_APPROX_SIMPLE.

외곽선 그리기 함수

void cv::drawContours(src, contours, -1, color, thickness, LINE_8, hierarchy);

외곽선 검출, 그리기 예제

    cv::Mat src = cv::imread("milkdrop.bmp", cv::IMREAD_GRAYSCALE);
    
    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat src_bin;
    cv::threshold(src, src_bin, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); // binarization

    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(src_bin, contours, cv::RETR_LIST, cv::CHAIN_APPROX_NONE);

    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, CV_8UC3);
    for(int i = 0; i < contours.size(); i++)
    {
        cv::Scalar color(rand() & 255, rand() & 255, rand() & 255);
        cv::drawContours(dst, contours, i, color, 2, cv::LINE_8);
    }

    cv::imshow("src", src);
    cv::imshow("src_bin", src_bin);
    cv::imshow("dst", dst);
    while(cv::waitKey(0) != 'q')
        continue;

외곽선 검출 및 계층정보 기반 그리기 예제

    cv::Mat src = cv::imread("contours.bmp", cv::IMREAD_GRAYSCALE);
    
    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(src, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_NONE);

    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, CV_8UC3);
    for(int i = 0; i >= 0; i = hierarchy[i][0])
    {
        cv::Scalar color(rand() & 255, rand() & 255, rand() & 255);
        cv::drawContours(dst, contours, i, color, 2, cv::LINE_8, hierarchy);
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);
    while(cv::waitKey(0) != 'q')
        continue;

외곽선 관련 함수들

  • arcLength() : 외곽선 길이
  • boundingRect() : 주어진 점을 감싸는 최소 크기 사각형(바운딩박스)
  • contourArea() : 외곽선이 감싸는 영역의 면적
  • approxPolyDP() : 외곽선 근사화
  • isContourConvex() : convex인지 검사

다각형 판별 예제
영상에서 삼각형, 사각형, 원을 찾는 예제.

  1. 이진화
  2. 외곽선 찾기
  3. 외곽선 근사화
  4. 크기가 작거나 non convex인 객체 제외
  5. 꼭지점 개수 확인(삼각형, 사각형) + 원 판별

void setLabel(cv::Mat& img, const std::vector<cv::Point>& pts, const cv::String& label)
{
    cv::Rect rc = cv::boundingRect(pts);
    cv::rectangle(img, rc, cv::Scalar(0, 0, 255), 1);
    cv::putText(img, label, rc.tl(), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255));
}


int main()
{
    cv::Mat src = cv::imread("polygon.bmp", cv::IMREAD_COLOR);

    if(src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    cv::Mat gray;
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);

    cv::Mat bin;
    cv::threshold(gray, bin, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);

    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(bin, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);

    for(const std::vector<cv::Point>& pts : contours)
    {
        // 작은 contour 제외
        if(cv::contourArea(pts) < 400)
            continue;
        
        // contour 근사화
        std::vector<cv::Point> approx;
        cv::approxPolyDP(pts, approx, cv::arcLength(pts, true)*0.02, true);

        // non convex 객체 제외
        if(!cv::isContourConvex(approx))
            continue;
        
        int vtc = (int)approx.size(); // 꼭지점 개수

        if(vtc == 3)
        {
            setLabel(src, pts, "TRI");
        }
        else if(vtc == 4)
        {
            setLabel(src, pts, "RECT");
        }
        else
        {
            // 원 판별
            double len   = cv::arcLength(pts, true);
            double area  = cv::contourArea(pts);
            double ratio = 4. * CV_PI * area / (len * len);

            if (ratio > 0.85)
                setLabel(src, pts, "CIR");
        }
    }

    cv::imshow("src", src);
    while(cv::waitKey(0) != 'q')
        continue;

    return 0;
}

5일차

템플릿 매칭

  • 입력 영상에서 부분 영상(템플릿)의 위치를 찾는 기법.
  • 회전, 크기 변환에 취약하다.

템플릿 매칭 비교 방법

  • TM_SQDIFF : square diff
  • TM_SQDIFF_NORMED : + normalize
  • TM_CCORR : correlation
  • TM_CCORR_NORMED
  • TM_CCOEFF : 평균 보정 후 correlation
  • TM_CCOEFF_NORMED
    cv::Mat src = cv::imread("circuit.bmp", cv::IMREAD_GRAYSCALE);
    cv::Mat tmpl = cv::imread("crystal.bmp", cv::IMREAD_GRAYSCALE);

    if (src.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    src = src + 50;

#if 1

    // add random noise (optional)
    cv::Mat noise(src.size(), CV_32S); 
    cv::randn(noise, 0, 10); // mean=0, stddev = 10
    cv::add(src, noise, src, cv::noArray(), CV_8U);


    // remove noise using gaussianblur (optional)
    cv::GaussianBlur(src, src, cv::Size(), 1);

#endif

    cv::Mat res, res_norm;
    cv::matchTemplate(src, tmpl, res, cv::TM_CCOEFF_NORMED);
    cv::normalize(res, res_norm, 0, 255, cv::NORM_MINMAX, CV_8U);

    double maxVal;
    cv::Point maxLoc;

    cv::minMaxLoc(res, 0, &maxVal, 0, &maxLoc);

    std::cout << "maxVal: " << maxVal << std::endl;
    std::cout << "max location: " << maxLoc << std::endl;

    cv::Mat dst;
    cv::cvtColor(src ,dst, cv::COLOR_GRAY2BGR);
    cv::rectangle(dst, cv::Rect(maxLoc.x, maxLoc.y, tmpl.cols, tmpl.rows), 
        cv::Scalar(0, 0, 255), 2);
    



    // cv::imshow("src", src);
    cv::imshow("template", tmpl);
    cv::imshow("res_norm", res_norm);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;
output:
maxVal: 0.945132
max location: [558, 323]

여러 개의 템플릿 매칭

템플릿이 여러개인 경우.

  1. matchTemplate를 통해 matching image 획득.
  2. 값이 충분히 큰 영역만을 threshold를 통해 검출.
  3. 레이블링을 통해 각 영역별로 local maxima 검출.

    cv::Mat src = cv::imread("cookierun.png");
    cv::Mat tmpl = cv::imread("item.png");

    if (src.empty() || tmpl.empty())
    {
        std::cerr << "Image load failed" << std::endl;
        return -1;
    }

    // 템플릿 매칭
    cv::Mat res, res_norm;
    cv::matchTemplate(src, tmpl, res, cv::TM_CCOEFF_NORMED);
    cv::normalize(res, res_norm, 0, 255, cv::NORM_MINMAX, CV_8UC1);

    // 값이 큰 것만 남김.
    cv::Mat local_max = res_norm > 220;

    // 레이블링
    cv::Mat labels;
    int num = cv::connectedComponents(local_max, labels);

    cv::Mat dst = src.clone();

    for(int i = 1; i < num; i++)
    {
        cv::Point maxLoc;
        cv::Mat mask = (labels == i);
        cv::minMaxLoc(res, 0, 0, 0, &maxLoc, mask);

        cv::rectangle(dst, cv::Rect(maxLoc.x, maxLoc.y, tmpl.cols, tmpl.rows), 
            cv::Scalar(0, 255, 0), 2);

    }

    // cv::imshow("src", src);
    cv::imshow("template", tmpl);
    cv::imshow("local_max", local_max);
    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

템플릿 매칭을 이용한 숫자 인식

  • 폰트, 글씨체가 변할 경우 인식이 힘들다.
  1. 이진화 + 레이블링을 통해 src에서 숫자 부분 영상을 획득.
  2. 각 부분영상은 템플릿 사이즈와 동일하게 resize
  3. 부분영상마다 0부터 9까지의 템플릿 매칭.
  4. 매칭 결과 중 결과값이 가장 큰 템플릿의 숫자로 구분.

cv::Mat img_digits[10];

bool load_digits()
{
    for(int i = 0; i < 10; i++)
    {
        cv::String filename = cv::format("./digits/digit%d.bmp", i);
        img_digits[i] = cv::imread(filename, cv::IMREAD_GRAYSCALE);
        if(img_digits[i].empty())
            return false;
    }

    return true;
}

void set_label(cv::Mat& img, int digit, const std::vector<cv::Point>& contour)
{
    int font = cv::FONT_HERSHEY_SIMPLEX;
    double font_scale = 1.5;
    int thickness = 3;
    int baseline = 0;
    cv::String text = cv::format("%d", digit);
    
    cv::Size text_sz = cv::getTextSize(text, font, font_scale, thickness, &baseline);
    cv::Rect bbox = cv::boundingRect(contour);

    cv::Point text_pt(bbox.x + ((bbox.width - text_sz.width) / 2), 
        bbox.y - ((bbox.height + text_sz.height) / 2));
    cv::putText(img, text, text_pt, font, font_scale, 
                cv::Scalar(255, 0, 0), thickness, cv::LINE_AA);
}

int find_digit(const cv::Mat& img)
{
    int max_idx = -1;
    float max_ccoeff = -1;

    for(int i = 0; i < 10; i++)
    {
        cv::Mat src, res;
        // 템플릿과 같은 크기로 resize
        cv::resize(img, src, cv::Size(100, 150));
        cv::matchTemplate(src, img_digits[i], res, cv::TM_CCOEFF_NORMED);

        // res는 1 by 1 matrix이다.
        float ccoeff = res.at<float>(0, 0);

        if(ccoeff > max_ccoeff)
        {
            max_idx = i;
            max_ccoeff = ccoeff;
        }
    }

    return max_idx;
}

int main()
{
    cv::Mat src = cv::imread("digits.png");
    
    if(src.empty() || !load_digits())
    {
        std::cerr << "Image load failed!" << std::endl;
        return -1;
    }

    cv::Mat src_gray, src_bin;
    cv::cvtColor(src, src_gray, cv::COLOR_BGR2GRAY);

    // 이진화
    cv::threshold(src_gray, src_bin, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);

    // 외곽선 검출
    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(src_bin, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);

    cv::Mat dst = src.clone();

    for(int i = 0; i < contours.size(); i++)
    {   
        // 객체의 크기가 작은 경우 continue
        if (cv::contourArea(contours[i]) < 1000)
            continue;

        cv::Rect rect = cv::boundingRect(contours[i]);
        int digit     = find_digit(src_gray(rect));

        cv::drawContours(dst, contours, i, cv::Scalar(0, 255, 255), 1, cv::LINE_AA);
        set_label(dst, digit, contours[i]);

    }


    cv::imshow("dst", dst);

    while(cv::waitKey(0) != 'q')
        continue;

    return 0;
}

서브픽셀 정확도 에지 위치 검출

subpixel accuracy: 실수 단위로 특정 좌표를 기술하는 기법.

  • 정수 단위 에지 위치 검출

    • 가우시안 블러 + ROI를 임의로 지정.
    • 해당 ROI 영역에서 dx 방향 gradient를 구하고 최대, 최소값 위치를 찾음.(sobel)
  • 실수 단위 에지 위치 검출

    • 1차 미분 최대 or 최소값 위치 주변 3개 픽셀 위치에서의 미분값을 이용해 2차 다항식 근사화.
    • 근사화된 2차 다항식의 최대 또는 최소값 위치를 찾음.

1개의 댓글

comment-user-thumbnail
2024년 11월 2일

subpixel에 대해서 궁금한게 있어요.

cv::minMaxLoc로 근사치값을 받을때 약간의 미동이라도 감지하려면 픽셀 가지고 안되서요.

혹시 가능한 방법이 있을까요? 정수 픽셀이 아닌 float 형의 픽셀값?은 받을수 없을까요?

답글 달기