2013년 1월 26일 토요일

OpenCV #7-4 Example (컴포넌트의 외곽선 추출)

 - 영상은 일반적으로 객체 표현을 포함.
 - 영상 분석의 목표 중 하나는 이런 객체를 식별하고 추출함에 있음.
 - 객체 감지/인식 애플리케이션에서 첫 번째 단계는 관심 객체가 위치했음을 보여주는 영상을 만듬.
 - 이진 맵을 어떻게 얻었는지는 관계가 없음.
 - 다음 단계는 1과 0으로 구성한 집단에 포함된 객체를 추출.
 - 다음과 같이 5장에서 다뤘던 이진 형태의 물소 영상을 들 수 있음.



 - 소스
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

int main() {
cv::Mat image= cv::imread("binary.bmp");
cv::namedWindow("Image");
cv::imshow("Image", image);

cv::Mat element5(5, 5, CV_8U, cv::Scalar(1));
// 필터 효과를 더 두드러지게 5x5 구조 요소를 사용

// 영상 닫힘과 영상 열림 (열림, 닫힘 연산자)
cv::morphologyEx(image,image,cv::MORPH_CLOSE,element5);
cv::morphologyEx(image,image,cv::MORPH_OPEN,element5);
cv::imwrite("binaryGroup.bmp",image);
cv::namedWindow("Closed and Opened Image");
cv::imshow("Closed and Opened Image",image);

cv::waitKey(0);

return 0;
}

 - 결과

 - 열림과 닫힘 형태학 필터의 애플리케이션에 의한 간단한 경계화 작업으로 위 영상을 얻음.
 - 이번 예제는 위와 같은 영상 내에서 객체를 어떻게 추출하는지 보여줌.
 - 더 구체적으로는 연결 컴포넌트를 추출.
 - 즉, 모양은 이진 영상 내에 연결된 화소 집합으로 만듬.

 - 영상 내에서 연결 컴포넌트의 외곽선을 추출하는 함수cv::findContours 함수를 사용.

  • Example
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

int main()
{
cv::Mat image= cv::imread("binaryGroup.bmp",0);

// 입력 영상은 이진 영상, 결과는 외곽선의 벡터로 각 외곽선은 cv::Point로 표현
// 이것은 결과 파라미터가 std::vectors의 std::vector로 정의된 이유
// 연결 구성요소의 외곽선 가져오기
std::vector<std::vector<cv::Point>> contours;
cv::findContours(image, 
contours, // 외곽선 벡터 
CV_RETR_EXTERNAL, // 외부 외곽선 검색
CV_CHAIN_APPROX_NONE); // 각 외곽선의 모든 화소 탐색
// 지정된 플래그 - 첫 번째는 외부 외곽선이 필요함을 나타내며 객체의 구멍을 무시
// 두 번째 플래그는 외곽선의 형태를 지정 - 현재 옵션으로 벡터는 외곽선 내의 모든 화소 목록
// CV_CHAIN_APPROX_SIMPLE 플래그로 하면 마지막 점이 수평 또는 수직, 대각선 외곽선에 포함됨
// 다른 플래그는 간결한 표현을 얻기 위해 외곽선의 정교하게 연결된 근사치를 제공

// 이전 영상으로 9개 외곽선을 contours.size()로 얻음
// drawContours() 함수는 영상 내의 각 외곽선을 그릴 수 있는 함수
// 하얀 영상 내 검은 외곽선 그리기
cv::Mat result(image.size(), CV_8U, cv::Scalar(255));
cv::drawContours(result, contours,
-1, // 모든 외곽선 그리기
cv::Scalar(0), // 검게
2); // 두께를 2로
// 세 번째 파라미터가 음수라면 모든 외곽선이 그려짐
// 반면 그려져야 하는 외곽선의 첨자를 지정할 수 있음

cv::namedWindow("Contours");
cv::imshow("Contours", result);

cv::waitKey(0);

return 0;
}

  • Result

  • 예제 분석
 - 컴포넌트를 찾을 때까지 영상을 체계적으로 조회하는 과정을 구성된 간단한 알고리즘으로 외곽선을 추출.

 - 컴포넌트 내의 출발점부터 그 외곽선을 따라가면서 외곽선 경계에 있는 화소를 표시.
 - 외곽선이 완료되면 새로운 컴포넌트를 찾을 때까지 마지막 위치부터 다시 조회.

 - 식별한 연결 컴포넌트를 개별로 분석할 수 있음.
 - 일부 사전 지식을 관심 객체 크기를 예상하는 데 사용할 수 있다면 컴포넌트의 일부를 제거할 수 있음.
 - 즉, 컴포넌트의 둘레에 대한 최소와 최대값을 사용할 수 있다.
 - 외곽선 벡터를 반복으로 조회하면서 잘못된 컴포넌트를 제거.

 - 소스
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

int main()
{
cv::Mat image= cv::imread("binaryGroup.bmp", 0);

std::vector<std::vector<cv::Point>> contours;
cv::findContours(image, 
contours, // 외곽선 벡터 
CV_RETR_EXTERNAL, // 외부 외곽선 검색
CV_CHAIN_APPROX_NONE); // 각 외곽선의 모든 화소 탐색

// 컴포넌트의 둘레에 대한 최소와 최대값 사용
// 외곽선 벡터를 반복으로 조회하면서 잘못된 컴포넌트를 제거
// 너무 짧거나 너무 긴 외곽선 제거
int cmin= 100;  // 최소 외곽선 길이
int cmax= 1000; // 최대 외곽선 길이
std::vector<std::vector<cv::Point>>::const_iterator itc= contours.begin();
while (itc!=contours.end()) {
if (itc->size() < cmin || itc->size() > cmax)
itc= contours.erase(itc);
else 
++itc;
}

// 원 영상 내 외곽선 그리기
cv::Mat original= cv::imread("group.jpg");
cv::drawContours(original,contours,
-1, // 모든 외곽선 그리기
cv::Scalar(255,255,255), // 검게
2); // 두께를 2로

cv::namedWindow("Contours on Animals");
cv::imshow("Contours on Animals",original);

cv::waitKey(0);

return 0;
}

 - 결과

 - std::vector는 O(N)이므로 반복문에서는 각 삭제 연산을 더 효율적으로 만들 수 있음.
 - 원 영상에 남아 있는 외곽선을 그리고 위 그림과 같은 결과를 얻음.

  • 부연 설명
 - cv::findContours 함수로 이진 맵 내의 모든 폐쇄 외곽선을 포함할 수 있고, 컴포넌트 내의 구멍 형태를 포함할 수 있음.
 - 이는 다른 플래그를 함수에 지정해 호출함으로 써 이루어짐.

 - 소스
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

int main()
{
cv::Mat image= cv::imread("binaryGroup.bmp", 0);

cv::Mat result(image.size(), CV_8U, cv::Scalar(255));
std::vector<std::vector<cv::Point>> contours;

// 연결 구성요소의 외곽선 가져오기
cv::findContours(image, 
contours, // 외곽선 벡터 
CV_RETR_LIST, // 외부와 내부 외곽선 탐색
CV_CHAIN_APPROX_NONE); // 각 외곽선의 모든 화소 탐색

// 하얀영상에 검은 외곽선 그리기
result.setTo(cv::Scalar(255));
cv::drawContours(result, contours,
-1, // 모든 외곽선 그리기
cv::Scalar(0), // 검게
2); // 두께를 2로

cv::namedWindow("All Contours");
cv::imshow("All Contours",result);

cv::waitKey(0);

return 0;
}

 - 결과

 - 여분의 외곽선은 배경인 숲에서 추가된 것.
 - 또한 계층 구조로 구성한 외곽선을 가질 수 있음.
 - 주 컴포넌트가 부모라면 구멍은 그 컴포넌트의 자식이고, 구멍 내에 컴포넌트가 있다면 이전 자식의 자식이 됨.
 - 계층 구조는 다음과 같이 CV_RETR_TREE 플래그를 사용해 얻게됨.

std::vector<cv::Vec4i> hierarchy;
cv::findContours (image,
contours, // 외곽선 벡터
hierarch, // 계층 구조 표현
CV_RETR_TREE // 나무 형태의 모든 외곽선 검색
CV_CHAIN_APPROX_NONE); // 각 외곽선의 모든 화소

 - 이번 경우 각 외곽선은 4개 정수의 동일한 첨자인 해당 계층 구조 요소를 갖게됨.
 - 첫 두 정수는 동일한 수준의 다음과 이전 외곽선에 대한 첨자.
 - 다음 두 정수는 첫 번째 자식과 자신 외곽선의 부모에 대한 첨자.
 - 음수 첨자는 외곽선 목록의 마지막을 가리킴.
 - CV_RETR_CCOMP 플래그는 동일하지만 2레벨의 계층 구조로 제한.

  • 참고문헌 : OpenCV 2 Computer Vision Application Programming Cookbook

OpenCV #7-3 Example (선을 점집합으로 맞추기)

 - 영상 내의 선을 감지하는 것은 매우 중요하며, 선의 위치와 방향에 대한 정확한 추정치를 얻는 것도 중요.
 - 점집합을 가장 적합하게 맞춘 선을 찾는 방법을 알아봄.

 - 영상 내의 직선을 따라 정렬한 것으로 보이는 점을 찾는 일을 먼저 한다.
 - 이전 절에서 감지했던 선 중 하나를 사용하자.
 - cv::HoughLinesP를 사용해 감지한 선에 lines 함수를 호출한 std::vector를 포함한다고 가정.

  • Example
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define PI 3.1415926

class LineFinder {
private:
cv::Mat img; // 원 영상
std::vector<cv::Vec4i> lines; // 선을 감지하기 위한 마지막 점을 포함한 벡터
double deltaRho;
double deltaTheta; // 누산기 해상도 파라미터
int minVote; // 선을 고려하기 전에 받아야 하는 최소 투표 개수
double minLength; // 선에 대한 최소 길이
double maxGap; // 선에 따른 최대 허용 간격

public:
LineFinder() : deltaRho(1), deltaTheta(PI/180), minVote(10), minLength(0.), maxGap(0.) {}
// 기본 누적 해상도는 1각도 1화소 
// 간격이 없고 최소 길이도 없음

// 해당 세터 메소드들

// 누적기에 해상도 설정
void setAccResolution(double dRho, double dTheta) {
deltaRho= dRho;
deltaTheta= dTheta;
}

// 투표 최소 개수 설정
void setMinVote(int minv) {
minVote= minv;
}

// 선 길이와 간격 설정
void setLineLengthAndGap(double length, double gap) {
minLength= length;
maxGap= gap;
}

// 허프 선 세그먼트 감지를 수행하는 메소드
// 확률적 허프 변환 적용
std::vector<cv::Vec4i> findLines(cv::Mat& binary) {
lines.clear();
cv::HoughLinesP(binary,lines,deltaRho,deltaTheta,minVote, minLength, maxGap);
return lines;
} // cv::Vec4i 벡터를 반환하고, 감지된 각 세그먼트의 시작과 마지막 점 좌표를 포함.

// 위 메소드에서 감지한 선을 다음 메소드를 사용해서 그림
// 영상에서 감지된 선을 그리기
void drawDetectedLines(cv::Mat &image, cv::Scalar color=cv::Scalar(255,255,255)) {

// 선 그리기
std::vector<cv::Vec4i>::const_iterator it2= lines.begin();

while (it2!=lines.end()) {
cv::Point pt1((*it2)[0],(*it2)[1]);
cv::Point pt2((*it2)[2],(*it2)[3]);
cv::line( image, pt1, pt2, color);
++it2;
}
}
};

int main()
{
cv::Mat image = cv::imread("road.jpg",0);

// 캐니 알고리즘 적용
cv::Mat contours;
cv::Canny(image,contours,125,350);

LineFinder ld;

// 확률적 허프변환 파라미터 설정하기
ld.setLineLengthAndGap(100,20);
ld.setMinVote(80);

std::vector<cv::Vec4i> li= ld.findLines(contours);

// 선의 첫 번째에 속한 것으로 보이는 점 집합을 추출하기 위해 다음과 같이 진행
// 검은 영상에 하얀 선을 그린 후 그 선을 감지하기 위해 사용하는 외곽선의 캐니 영상으로 함께 교차
int n=0; // 0번째 선 선택
cv::Mat oneline(image.size(),CV_8U,cv::Scalar(0)); // 검은 영상
cv::line(oneline, // 하얀 선
cv::Point(li[n][0],li[n][1]),
cv::Point(li[n][2],li[n][3]),
cv::Scalar(255),
5); //특정 두께 5를 갖는 선을 그림
cv::bitwise_and(contours, oneline, oneline);
// 외곽선과 하얀 선 간의 AND
// 결과는 지정한 선과 관련된 점만 포함

// 검은 영상과 하얀 선을 반전시킴
cv::Mat onelineInv;
cv::threshold(oneline,onelineInv,128,255,cv::THRESH_BINARY_INV);
cv::namedWindow("One line");
cv::imshow("One line", onelineInv);

// 이중 반복문으로 집합 내에 있는 점의 좌표를 cv::Points의 std::vector에 삽입
std::vector<cv::Point> points;
// 모든 점 위치를 얻기 위환 화소 조회
for( int y = 0; y < oneline.rows; y++ ) { // y행
uchar* rowPtr = oneline.ptr<uchar>(y);
for( int x = 0; x < oneline.cols; x++ ) { // x열
   // 외곽선에 있다면
if (rowPtr[x]) {
points.push_back(cv::Point(x,y));
}
}
    }

// cv::fitLine을 호출해 가장 적합한 선을 찾음
cv::Vec4f line;
cv::fitLine(cv::Mat(points),line,
CV_DIST_L2, // 거리 유형
0, // L2 거리를 사용하지 않음
0.01,0.01); // 정확도
// 단위 방향 벡터(cv::Vec4f의 첫 두개 값), 
// 선에 놓인 한 점의 좌표(cv::Vec4f의 마지막 두 값) 형태인 선 방정식의 파라미터를 제공
// 마지막 두 파라미터는 선 파라미터에 대한 요구 정확도를 지정
// 함수에서 요구 하는 std::vector 내에 포함된 입력 점은 cv::Mat로 전달

// 선 방정식은 일부 속성계산에 사용
// 올바른 선을 계산하는지 확인하기 위해 영상에 예상 선을 그림
// 200화소 길이와 3화소 두께를 갖는 임의의 검은 세그먼트를 그림
int x0= line[2]; // 선에 놓은 한 점
int y0= line[3];
int x1= x0-200*line[0]; // 200 길이를 갖는 벡터 추가
int y1= y0-200*line[1]; // 단위 벡터 사용
image= cv::imread("road.jpg",0);
cv::line(image,cv::Point(x0,y0),cv::Point(x1,y1),cv::Scalar(0),3);
cv::namedWindow("Estimated line");
cv::imshow("Estimated line",image);

cv::waitKey(0);

return 0;
}


  • Result


  • 예제 분석
 - 선을 점집합으로 맞추는 것은 수학의 고전적인 문제.
 - OpenCV 구현은 각 점에서 선 거리의 합을 최소화해 진행.
 - 여러 거리 함수가 제안됬고, 가장 빠른 옵션은 CV_DIST_L2로 지정해 유클리디안 거리를 사용.
 - 이 선택은 표준 최소 제곱 선 맞추기에 해당.
 - 영외(점이 선에 속하지 않음)에 점집합을 포함했을 때 점에 더 적은 영향을 주는 다른 거리 함수를 선택할 수 있움.
 - 최소화는 M 추정 기술에 기반을 두며, 반복적으로 선으로부터 거리에 반비례하는 가중치와 함께 가중 최소 제곱 문제를 해결.

 - 함수를 사용하면 3D 점집합에 대한 선 맞춤이 가능.
 - 입력은 이번 경우에 cv::Point3i 또는 cv::point3f의 집합이고, 결과는 std::Vec6f 이다.


  • 부연 설명
 - cv::fitEllipse 함수는 2D 점집합에 대한 타원을 맞춤.
 - 내부에 타원이 들어간 회전된 사각형(cv::RotatedRect 인스턴스)을 반환.
 - 이번 경우에는 다음과 같이 쓸 수 있다.

cv::RotatedRect rrect = cv::fitEllipse(cv::Mat(points));
cv::ellipse(image, rrect, cv::Scalar(0));

 - cv::ellipse 함수는 계산된 타원을 그리기 위해 사용하는 함수 중 하나.

  • 참고문헌 : OpenCV 2 Computer Vision Application Programming Cookbook

OpenCV #7-2 Example (허프 변환으로 영상 내 선 감지 - 부연 설명)

 - 허프 변환은 또한 다른 기하학적인 요소를 감지하기 위해 사용.
 - 확률 방정식으로 표현하는 임의 요소는 허프 변환을 위한 좋은 후보.


  • 원 감지
 - 원인 경우에 해당 확률 방정식은 다음과 같다.
 - 이 방정식은 세 가지 파라미터(원의 반지름과 중심 좌표)를 갖는데, 3차원 누산기가 필요함을 의미.
 - 그러나 일반적으로 허프 변환은 누산기의 차원이 증가하면 신뢰도가 떨어짐.
 - 이 경우에는 누산기 요소의 많은 개수가 각 점에 대해 증가하고, 결과로는 지역 봉우리의 정확한 지역화가 더욱 어려워짐.
 - 그러므로 이런 문제를 극복하기 위한 다른 전략이 제안됨.
 - 그 중 하나는 OpenCV에서 허프 원 감지를 구현할 때 두 가시 패스를 사용하는 방법.

 - 첫 번째 패스 과정에서 2차원 누산기는 후보 원 위치를 찾는 데 사용.
 - 원 둘레에 놓인 점의 기울기는 반지를 방향에 있는 점이어야 하므로, 다음 각 점에 대해 누산기 내의 각 요소는 기울기 방향에 따라 증가. (미리 정의된 최소와 최대 반지름 값)
 - 일단 가능한 원 중심을 감지하면(미리 정의한 투표 개수를 받아들이면) 두번째 패스 동안 가능한 반지름의 1차원 히스토그램을 구축.
 - 히스토그램 내 봉우리 값은 감지한 원의 반지름에 대응.

 - cv::HoughCircles 함수는 위의 전략을 구현하기 위해 캐니 감지와 허프 변환을 모두 통합.


cv::GaussianBlur(image, image, cv::Size(5,5), 1.5);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT, 
2,   // 누적기 해상도(영상크기/2)
50,  // 두 원 간의 최소 거리
200, // 캐니 최대 경계값
100, // 투표 최소 개수
25, 100); // 최소와 최대 반지름


 - 몇 가지 잘못된 원 감지를 일으킬 수 있는 영상 잡음을 제거하기 위해 cv::HoughCircles 함수를 호출하기 전에 영상을 부드럽게 할 것을 권장.
 - cv::Vec3f 인스턴스의 벡터에 감지 결과가 들어 있음.
 - 처음 두 값은 원의 중심이고, 세 번째는 반지름이다.
 - CV_HOUGH_GRADIENT 인자는 2패스 원 감지 메소드에 해당.
 - 네 번째 파라미터는 누산기 해상도를 정의.
 - 분할자를 예를 들어 2로 지정하면 영상의 절반 크기인 누산기를 만듬.
 - 다음 파라미터는 두 감지된 원 간의 화소에서 최소 거리.
 - 다른 파라미터는 캐니 에지 감지기의 높은 경계값에 해당.
 - 낮은 경계값은 이 값의 절반으로 설정.
 - 일곱 번째 파라미터는 두번째 패스에서 후보 원으로 간주하기 위해 첫 번째 패스 동안 중심 위치에서 반드시 받아야 하는 투표 최소 개수.
 - 끝으로 마지막 두 파라미터는 감지하기 위한 원에 대한 최소와 최대 반지름 값.
 - 이렇듯 이 함수는 조정하기 어렵게 만드는 많은 파라미터를 가짐.

 - 일단 감지한 원의 벡터를 얻으면 벡터를 반복해 영상 내에 그릴 수 있고, 찾은 파라미터와 함께 cv::circle 그리기 함수를 호출.


std::vector<cv::Vec3f>::const_iterator itc= circles.begin();
while (itc!=circles.end()) {
 cv::circle(image, 
 cv::Point((*itc)[0], (*itc)[1]), // 원 중심
 (*itc)[2], // 원 반지름
 cv::Scalar(255), // 컬러 
 2); // 두께
 ++itc;
}

 - 소스
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

int main()
{
// 원 감지
cv::Mat image;
image = cv::imread("chariot.jpg",0);

cv::GaussianBlur(image, image, cv::Size(5,5), 1.5);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT, 
2,   // 누적기 해상도(영상크기/2)
50,  // 두 원 간의 최소 거리
200, // 캐니 최대 경계값
100, // 투표 최소 개수
25, 100); // 최소와 최대 반지름

std::cout << "Circles: " << circles.size() << std::endl;

// 원 그리기
image= cv::imread("chariot.jpg",0);
std::vector<cv::Vec3f>::const_iterator itc= circles.begin();
while (itc!=circles.end()) {
 cv::circle(image, 
 cv::Point((*itc)[0], (*itc)[1]), // 원 중심
 (*itc)[2], // 원 반지름
 cv::Scalar(255), // 컬러 
 2); // 두께
 ++itc;
}

cv::namedWindow("Detected Circles");
cv::imshow("Detected Circles",image);

cv::waitKey(0);

return 0;
}

 - 결과



  • 일반화 허프 변환
 - 일부 모양에 대해 간결한 파라메트릭 표현을 찾기가 어려움.
 - 삼각형, 팔각형, 다각형, 객체 프로파일등을 예로 들 수 있음.
 - 영상 내에서 이런 모양의 위치에 대한 허프 변환을 사용할 수 있음.
 - 2차원 누산기는 목표 모양에 대한 모든 가능한 위치를 보여주기 위해 만들었다.
 - 모양에서 참조점을 정의해야 하고, 가능한 참조점 위치에 대해 영상 내의 각 특징점에 투표 해야 함.
 - 점은 모양의 외곽선 어디서나 존재할 수 있으므로, 가능한 모든 참조 위치로 누산기 내 관심 모양의 거울인 모양을 추적.
 - 거기에 영상 안의 같은 모양에 속하는 점은 누산기 내 모양의 위치에 대응하는 교차점에서 정점peak을 만듬.

 - 다음 그림에서 참조가 하단 왼쪽 모서리에 정의된 관심 모양이 삼각형(오른쪽에 보임)임을 보여줌.
 - 누산기는 해당 특징점을 통과하는 삼각형 참조점이 있을 법한 위치에 대응해 그려진 위치에서 모든 요소를 증가하는 특징점을 보여줌.



 - 이런 접근은 종종 일반화 허프 변환이라고 함.
 - 분명 기술 가능한 크기 변경이나 모양 회전에 대해서는 계산할 수 없음.
 - 더 높은 차원에서 검색해야함.
  • 참고문헌 : OpenCV 2 Computer Vision Application Programming Cookbook

OpenCV #7-2 Example (허프 변환으로 영상 내 선 감지)

 - 직선은 객체 인식과 영상 이해에 중요한 역할을 하기 때문에 영상 내의 특별한 특징을 감지하기 위해 사용.
 - 허프 변환 알고리즘은 영상에서 선을 감지하기 위해 개발했고, 다른 간단한 영상 구조 감지로 확장할 수 있다.

 - 허브 변환은 다음 방정식을 사용해 직선을 표현.
 - p : 직선과 영상 원점(왼쪽 상단 모서리)간의 거리.
 - Θ : 선에 대한 수직 각도.
 - 식에서 영상 내에 보이는 선은 0과 π(파이) 라디안 사이의 각도인 Θ를 갖고,p 반지름은 영상 대각선의 길이와 같은 최대값을 갖는다.

 - 다음 선 집합을 보았을 때
 - 1인 선인 수평선은 0과 동일한 Θ 각도 값을 갖고, 수평선(5인 선)은 π/2와 동일한 Θ 값을 갖음.
 - 즉, 3인 선은 π/4와 동일한 각도 Θ를 갖고, 4인 선은 0.7π에 가깝다.
 - 가능한 모든 선을 [0, π] 범위에 있는 Θ로 표현하기 위해서는 반지름 값을 음수로 만들 수 있음.
 - 2인 선의 경우에는 p가 음수 값으로서 값이 0.8π와 동일.

 - OpenCV는 선을 감지하기 위한 두 가지 허프 변환을 제공.
 - 기본 버전은 cv::HoughLines 이다.
 - 입력은 점집합(넌제로 화소로 표현)을 포함하는 이진 맵이고, 일부는 선 형태로 정렬. (에지 맵에서 캐니 연산자로 얻는다.)
 - cv::HoughLines 함수의 결과는 cv::Vec2f 요소의 벡터로, 부동소수점 값의 각 쌍은 감지된 선 (p, Θ)의 파라미터를 표현.
 - 즉, 영상 외곽선을 얻기 위해 먼저 캐니 연산자를 적용한 곳에 허프 변환을 이용해 선을 감지.


// 선 감지 위한 허프 변환
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours, lines, 
1,PI/180, // 단계별 크기
80); // 투표(vote) 최대 개수
 - 3, 4번째 파라미터는 선을 찾기 위한 단계별 크기에 해당.
 - 예제에서 이 함수는 1과 π/180에서 단계별로 가능한 모든 각도로 반지름의 선을 찾음.
 - 마지막 파라미터는 다음 절에서 설명.
 - 특정 파라미터 값으로는 이전 절의 도로 영상에서 열다섯 개의 선을 감지.

 - 이 알고리즘은 영상 내의 선을 감지할 뿐 각 선의 마지막 점이 부여되지 않은 상태에서 선을 분할할 수 없다. (따라서 영상 전체를 가로지르는 선을 그림)
 - 대부분 수직선에 대해 영상의 수평 한계(처음과 마지막 행)와 교차하는 부분을 계산하고, 두 점 간의 선을 그린다. (처음과 마지막 열을 사용)
 - cv::line 함수를 사용해 선을 그림. 
 - cv::line 함수는 영상 한계의 바깥에 있는 점 좌표로도 작동 (즉, 영상 내부에 계산된 교차점이 없는지 확인할 필요 없음)


  • Example
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define PI 3.1415926

int main()
{
cv::Mat image= cv::imread("road.jpg", 0);
cv::namedWindow("Original Image");
cv::imshow("Original Image",image);

// 캐니 알고리즘 적용
cv::Mat contours;
cv::Canny(image, contours, 125, 350);

// 선 감지 위한 허프 변환
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours, lines, 
1,PI/180, // 단계별 크기
80); // 투표(vote) 최대 개수

// 선 그리기
cv::Mat result(contours.rows, contours.cols, CV_8U, cv::Scalar(255));
std::cout << "Lines detected: " << lines.size() << std::endl;

// 선 벡터를 반복해 선 그리기
std::vector<cv::Vec2f>::const_iterator it= lines.begin();
while (it!=lines.end()) {
float rho = (*it)[0];   // 첫 번째 요소는 rho 거리
float theta = (*it)[1]; // 두 번째 요소는 델타 각도
if (theta < PI/4. || theta > 3.*PI/4.) { // 수직 행
cv::Point pt1(rho/cos(theta), 0); // 첫 행에서 해당 선의 교차점   
cv::Point pt2((rho-result.rows*sin(theta))/cos(theta), result.rows);
// 마지막 행에서 해당 선의 교차점
cv::line(image, pt1, pt2, cv::Scalar(255), 1); // 하얀 선으로 그리기

} else { // 수평 행
cv::Point pt1(0,rho/sin(theta)); // 첫 번째 열에서 해당 선의 교차점  
cv::Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
// 마지막 열에서 해당 선의 교차점
cv::line(image, pt1, pt2, cv::Scalar(255), 1); // 하얀 선으로 그리기
}
std::cout << "line: (" << rho << "," << theta << ")\n"; 
++it;
}

cv::namedWindow("Detected Lines with Hough");
cv::imshow("Detected Lines with Hough",image);

cv::waitKey(0);

return 0;
}


  • Result

 - 그림에서 처럼 허프 변환은 단순히 영상에서 정렬된 에지 화소를 찾는다.
 - 잠재적으로 예기치 않은 화소 정렬로 인해 일부 잘못된 감지를 만든거나, 여러 선이 동일한 화소 정렬을 경유한 여러 개를 감지할 수 있다.

 - 이런 문제의 일부를 극복하고 선 세그먼트를 감지(즉, 마지막 점과 함께)하기 위해서 제안된 것이 확률적 허프 변환이다.
 - 확률적 허프 변환은 cv::HoughLinesP 함수로 구현.
 - 여기에 LineFinder 클래스를 생성해 이 함수를 사용하며, 함수 파라미터를 캡슐화 함.

  • Example
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define PI 3.1415926

class LineFinder {
private:
cv::Mat img; // 원 영상
std::vector<cv::Vec4i> lines; // 선을 감지하기 위한 마지막 점을 포함한 벡터
double deltaRho;
double deltaTheta; // 누산기 해상도 파라미터
int minVote; // 선을 고려하기 전에 받아야 하는 최소 투표 개수
double minLength; // 선에 대한 최소 길이
double maxGap; // 선에 따른 최대 허용 간격

public:
LineFinder() : deltaRho(1), deltaTheta(PI/180), minVote(10), minLength(0.), maxGap(0.) {}
// 기본 누적 해상도는 1각도 1화소 
// 간격이 없고 최소 길이도 없음
// 해당 세터 메소드들

// 누적기에 해상도 설정
void setAccResolution(double dRho, double dTheta) {
deltaRho= dRho;
deltaTheta= dTheta;
}
// 투표 최소 개수 설정
void setMinVote(int minv) {
minVote= minv;
}

// 선 길이와 간격 설정
void setLineLengthAndGap(double length, double gap) {
minLength= length;
maxGap= gap;
}
// 허프 선 세그먼트 감지를 수행하는 메소드
// 확률적 허프 변환 적용
std::vector<cv::Vec4i> findLines(cv::Mat& binary) {
lines.clear();
cv::HoughLinesP(binary,lines,deltaRho,deltaTheta,minVote, minLength, maxGap);
return lines;
} // cv::Vec4i 벡터를 반환하고, 감지된 각 세그먼트의 시작과 마지막 점 좌표를 포함.
// 위 메소드에서 감지한 선을 다음 메소드를 사용해서 그림
// 영상에서 감지된 선을 그리기
void drawDetectedLines(cv::Mat &image, cv::Scalar color=cv::Scalar(255,255,255)) {
// 선 그리기
std::vector<cv::Vec4i>::const_iterator it2= lines.begin();
while (it2!=lines.end()) {
cv::Point pt1((*it2)[0],(*it2)[1]);
cv::Point pt2((*it2)[2],(*it2)[3]);
cv::line( image, pt1, pt2, color);
++it2;
}
}
};

int main()
{
cv::Mat image= cv::imread("road.jpg", 0);

// 캐니 알고리즘 적용
cv::Mat contours;
cv::Canny(image, contours, 125, 350);

LineFinder ld; // 인스턴스 생성

// 확률적 허프변환 파라미터 설정하기
ld.setLineLengthAndGap(100, 20);
ld.setMinVote(80);

// 선을 감지하고 그리기
std::vector<cv::Vec4i> li= ld.findLines(contours);
ld.drawDetectedLines(image);
cv::namedWindow("Detected Lines with HoughP");
cv::imshow("Detected Lines with HoughP",image);

cv::waitKey(0);

return 0;
}

  • Result

  • 예제 분석
 - 허프 변환의 목적은 이진 영상 내에서 충분한 개수의 점을 통과하는 모든 선을 찾는데 있음.
 - 입력 이진 맵의 각 화소점을 고려해 진행하고 통과 가능한 모든 선을 식별.
 - 동일한 선이 많은 점을 지난다면 이 선을 충분히 고려할 정도로 중요함을 의미.

 - 허프 변환은 주어진 선을 식별하기 위해 여러 번 계산하기 위한 2차원 누산기를 사용.
 - 누산기의 크기는 채택한 선 표현의 (p, Θ) 파라미터로 지정된 단계별 크기로 정의.

 - 변환의 기능을 증명하기 위해 180x200 행렬(Θ은 π/180이고, p는 1인 단계별 크기에 대응)을 만들어보면
 - cv::Mat acc(200,180,CV_8U,cv::Scalar(0)); // 허프 누산기 생성

 - 누산기는 각 (p, Θ) 값을 매핑.
 - 그러므로 행렬의 각 요소는 특정 선에 대응.
 - 한 점을 고려하는데, 예제에서는 (50, 30) 좌표로 예를 든다.
 - 그 다음 가능한 모든 각도인 Θ(π/180의 단계별 크기로)와 해당 (반올림) p 값을 계산해 반복하는 과정에서 해당 점을 지나는 모든 선을 식별할 수 있다.

 - 소스
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define PI 3.1415926

int main()
{
cv::Mat acc(200,180,CV_8U,cv::Scalar(0)); 
// 허프 누적기 생성
// uchar 영상이어야 하고, 실제로 ing이어야 함

int x=50, y=30; // 점 선택

// 모든 각도로 반복
for (int i=0; i<180; i++) { 
double theta= i*PI/180.;

// 대응하는 rho 값을 찾기
double rho= x*cos(theta)+y*sin(theta);
int j= static_cast<int>(rho+100.5);

std::cout << i << "," << j << std::endl;

acc.at<uchar>(j,i)++; // 누적기 증가
}

cv::namedWindow("Hough Accumulator 1");
cv::imshow("Hough Accumulator 1",acc*100);

x=30, y=10; // 두 번째 점 선택

// 모든 각도로 반복
for (int i=0; i<180; i++) {
double theta= i*PI/180.;
double rho= x*cos(theta)+y*sin(theta);
int j= static_cast<int>(rho+100.5);

acc.at<uchar>(j,i)++;
}

cv::namedWindow("Hough Accumulator 2");
cv::imshow("Hough Accumulator 2",acc*100);

cv::waitKey(0);

return 0;
}

 - 결과

 - 누산기의 요소는 계산된 (p, Θ) 쌍에 대응하면 증가하고, 영상의 한 지점(또는 다른 가능한 후보 선의 집합에 대한 각 점 투표)을 지나는 모든 선을 나타냄.
 - 영상(1개수가 나타나게 100을 곱함)으로 누산기를 출력해서 위와 같은 결과를 얻는다.
 - 그림에서 곡선은 고려한 점을 통과하는 모든 선의 집합을 표현.
 - (30, 10) 점에 대해 동일한 과정을 반복하면 2번째 결과와 같은 누산기를 갖는다.

 - 그림에서 보듯 두 결과 곡선은 한 점에서 교차.
 - 이 점은 두 점이 선을 통과하는 점에 해당.
 - 누산기의 해당 요소는 두 투표를 받고, 두 점이 이선을 통과했음을 나타냄.
 - 이진 맵의 모든 점에 동일한 과정을 반복하면 점이 주어진 선에 따라 정렬되고 누산기의 공통 요소가 여러 번 증가.
 - 마지막에 하나는 누산기에서 영상 안의 선(점 정렬)을 감지하기 위한 상당한 투표 개수를 받는 지역 최대치를 식별해야 함.
 - cv::HoughLines 함수 내에 지정한 마지막 파라미터는 투표 최소 개수에 대응하는 선이 감지된 것으로 간주해 받아 들여야 함.

 - 예를 들어 cv::HoughLines(contours, lines, 1, PI/180, 80); 을 60으로,
 즉, cv::HoughLines(contours, lines, 1, PI/180, 60); 으로 하면 더 많은 선이 나타난다.

 - 소스
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define PI 3.1415926

int main()
{
cv::Mat image= cv::imread("road.jpg", 0);

// 캐니 알고리즘 적용
cv::Mat contours;
cv::Canny(image, contours, 125,350);

// 선 감지 위한 허프 변환
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours, lines, 
1,PI/180, // 단계별 크기
60); // 투표(vote) 최대 개수

// 선 그리기
cv::Mat result(contours.rows, contours.cols, CV_8U, cv::Scalar(255));
std::cout << "Lines detected: " << lines.size() << std::endl;

// 선 벡터를 반복해 선 그리기
std::vector<cv::Vec2f>::const_iterator it= lines.begin();
while (it!=lines.end()) {
float rho = (*it)[0];   // 첫 번째 요소는 rho 거리
float theta = (*it)[1]; // 두 번째 요소는 델타 각도
if (theta < PI/4. || theta > 3.*PI/4.) { // 수직 행
cv::Point pt1(rho/cos(theta), 0); // 첫 행에서 해당 선의 교차점   
cv::Point pt2((rho-result.rows*sin(theta))/cos(theta), result.rows);
// 마지막 행에서 해당 선의 교차점
cv::line(image, pt1, pt2, cv::Scalar(255), 1); // 하얀 선으로 그리기

} else { // 수평 행
cv::Point pt1(0,rho/sin(theta)); // 첫 번째 열에서 해당 선의 교차점  
cv::Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
// 마지막 열에서 해당 선의 교차점
cv::line(image, pt1, pt2, cv::Scalar(255), 1); // 하얀 선으로 그리기
}
std::cout << "line: (" << rho << "," << theta << ")\n"; 
++it;
}

cv::namedWindow("Detected Lines with Hough");
cv::imshow("Detected Lines with Hough",image);

cv::waitKey(0);

return 0;
}

 - 결과

 - 확률적 허프 변환은 기본 알고리즘에 몇 가지 수정을 가했음.
 - 첫째, 체계적으로 영상을 행마다 조회하는 대신 이진 맵에서 무작위 순서로 점을 선택.
 - 누산기의 항목이 특정 최소값에 도달할 때마다 해당 선을 따라 영상을 조회하고 통과하는 모든 점을 제거(아직 투표하지 않은 경우에도) 함,
 - 또한 조회 과정에서 세그먼트의 길이를 결정하며 받아들임.
 - 이를 위해 알고리즘은 두 개의 추가 파라미터를 정의.
 - 전자는 받아들이기 위한 세그먼트의 최소 길이이고, 후자는 연속적인 세그먼트를 형성하기 위해 허용하는 최대 화소 간격.
 - 이 추가 단계는 알고리즘의 복잡도를 증가시키지만, 선을 조회하는 과정에서 일부가 사라지고, 투표 과정에서 아주 작은 점을 포함하기 때문에 어느 정도 보상할 수 있음.

  • 참고문헌 : OpenCV 2 Computer Vision Application Programming Cookbook