OpenCV를 사용하여 컬러 영상(butterfly.jpg)을 불러온 뒤, 각 픽셀에 직접 접근하여 B(Blue), G(Green), R(Red) 세 개의 단일 채널 영상으로 분리하는 과정
#include <opencv2/opencv.hpp>
#include <string>
int main() {
// 1. 영상 로드 및 준비
cv::Mat src = cv::imread("butterfly.jpg", cv::IMREAD_COLOR); // 이미지를 BGR 3채널 컬러 형태로 읽어온다.
if (src.empty()) {
printf("이미지를 불러올 수 없습니다.\n");
return -1;
}
cv::imshow("SRC", src); // src는 채널3
cv::Mat B(src.size(), CV_8UC1); // src 에서 Blue 만 빼옴, 원본과 같은 크기의 메모리 공간을 생성한다.
cv::Mat G(src.size(), CV_8UC1); // src 에서 Green 만 빼옴, 원본과 같은 크기의 메모리 공간을 생성한다.
cv::Mat R(src.size(), CV_8UC1); // src 에서 Red 만 빼옴, 원본과 같은 크기의 메모리 공간을 생성한다.
// CV_8UC1은 8비트(0~255) 부호 없는 정수(Unsigned Char) 1채널을 의미한다. 즉, 흑백 이미지 저장용이다.
// 2. 픽셀 직접 접근 및 채널 분리
for (int i = 0; i < src.rows; ++i) {
for (int j = 0; j < src.cols; ++j) {
// OpenCV 는 <B, G, R>
// reference 연산자 사용
// 원본에서 B, G, R 빼기
cv::Vec3b& p1 = src.at<cv::Vec3b>(i, j); // src 영상의 (i, j) 위치에 있는 픽셀 데이터에 접근한다.
uchar& p_b = B.at<uchar>(i, j); // &(참조 연산자)를 사용하여 해당 위치의 실제 데이터에 직접 접근하므로 속도가 빠르다.
p_b = p1[0]; // Blue
uchar& p_g = G.at<uchar>(i, j); // cv::Vec3b는 3개의 uchar를 가진 벡터 데이터 타입이다.
p_g = p1[1]; // Green
uchar& p_r = R.at<uchar>(i, j);
p_r = p1[2]; // Red
}
}
// 코드 최적화 (내장함수 사용)
// std::vector<cv::Mat> channels;
// cv::split(src, channels);
// 3. 결과 시각화
// cv::imshow: 분리된 B, G, R 영상을 각각 새 창에 띄운다.
// B, G, R 영상은 각각 1채널(CV_8UC1)이므로, 화면에는 그레이스케일(흑백) 형태로 출력된다.
cv::imshow("Blue channel", B);
cv::imshow("Green channel", G);
cv::imshow("Red channel", R);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}


알파 채널(Alpha Channel) 처리. cv::split 함수를 사용하여 채널을 분리.
- IMREAD_UNCHANGED: PNG 파일에 포함된 투명도 채널(Alpha)을 유지하기 위해 필수적인 플래그다. 만약 IMREAD_COLOR로 읽었다면 Alpha 채널이 무시되고 강제로 3채널(BGR)로 변환되었을 것이다.
- cv::split: 이전 코드처럼 for문을 돌리지 않아도 내부적으로 최적화된 연산을 통해 채널을 분리해주므로 훨씬 빠르고 간결하다.
- 채널 개수 확인: std::size(bgr_planes)를 통해 파일이 실제로 3채널(BGR)인지 4채널(BGRA)인지 동적으로 확인할 수 있다.
#include <opencv2/opencv.hpp>
#include <string>
int main() {
cv::Mat src = cv::imread("candies.png", cv::IMREAD_UNCHANGED); // IMREAD_UNCHANGED: PNG 파일에 포함된 투명도 채널(Alpha)을 유지하기 위해 필수적인 플래그
std::vector<cv::Mat> bgr_planes;
cv::split(src, bgr_planes);
std::cout << std::size(bgr_planes) << "\r\n"; // 채널 몇개인지?
cv::imshow("SRC", src);
cv::imshow("Blue", bgr_planes[0]);
cv::imshow("Green", bgr_planes[1]);
cv::imshow("Red", bgr_planes[2]);
// A 채널없어서 bgr_pllanes[3]이 없다
cv::waitKey();
cv::destroyAllWindows();
return 0;
}

트랙바를 활용하여 실시간으로 HSV 색상 임계값을 조절하고 물체를 필터링
1. Hue(색상)란 무엇인가?
Hue는 우리가 흔히 말하는 '색의 종류'(빨강, 노랑, 초록, 파랑 등)를 의미한다.
- 각도로 표현: 원래 색상환(Color Wheel)에서 0도~360도 사이의 각도로 표현된다.
- OpenCV에서의 범위: 메모리 효율을 위해 360도를 반으로 나눠 0~179 사이의 값으로 표현한다.
- 특징: 빛의 밝기(V)나 진하기(S)와 독립적이기 때문에, 조명이 변해도 색상 값(H)은 비교적 일정하게 유지되어 물체 검출에 유리하다.
#include <opencv2/opencv.hpp>
#include <string>
// static: 여러 .cpp 파일을 함께 컴파일할 때 동일한 이름의 함수나 변수가 다른 파일에 있더라도 이름 충돌을 방지한다.
static void on_hue_change(int, void*); // 함수의 proto type
static cv::Mat src, src_hsv, mask;
static int lower_bound = 0, upper_bound = 0;
int main() {
src = cv::imread("candies.png");
cv::cvtColor(src, src_hsv, cv::COLOR_BGR2HSV); // bgr 로 읽어온걸 hsv로 찾을 것이다
cv::namedWindow("SRC");
// cv::createTrackbar: 사용자가 마우스로 값을 조절할 때마다 on_hue_change 콜백 함수를 호출한다. 이는 실시간 피드백을 가능하게 한다.
cv::createTrackbar("LOWER HUE", "SRC", &lower_bound, 179, on_hue_change); // hue 가 0 ~ 179
cv::createTrackbar("UPPER HUE", "SRC", &upper_bound, 179, on_hue_change); // hue 가 0 ~ 179
cv::imshow("CANDIES", src);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}
void on_hue_change(int, void*) {
cv::Scalar lower(lower_bound, 150, 0); // H(Hue)는 트랙바로 조절하고, S(Saturation)는 150 이상으로 고정하여 진한 색상만 추출하도록 설계했다.
cv::Scalar upper(upper_bound, 255, 255); // V(Value)는 0~255 전체를 허용하여 밝기에 관계없이 색을 찾으려 시도했다.
cv::inRange(src_hsv, lower, upper, mask); // cv::inRange: 입력 영상(src_hsv)에서 lower와 upper 사이의 값은 **255(흰색)**로, 나머지는 **0(검은색)**으로 변환하여 mask 영상을 만든다.
cv::imshow("SRC", mask); // mask: 어떤걸 걸러낸다, 막겠다, 필터링
}
| 목표 색상 | LOWER HUE 설정값 | UPPER HUE 설정값 | 비고 |
| 초록색 | 50 | 80 | 가장 안정적으로 검출됨 |
| 파란색 | 100 | 130 | 파란 사탕 검출 시 사용 |
| 빨간색 (1) | 0 | 10 | 주황색에 가까운 빨강까지 포함 |
| 빨간색 (2) | 160 | 179 | 자주색에 가까운 빨강까지 포함 |




컬러영상의 화질을 개선할 때 사용하는 표준적인 방법
#include <opencv2/opencv.hpp>
#include <string>
int main() {
cv::Mat src = cv::imread("pepper.bmp", cv::IMREAD_COLOR);
if (src.empty()) {
std::cerr << "Image load failed!" << std::endl;
return -1;
}
cv::Mat src_ycrb;
cv::cvtColor(src, src_ycrb, cv::COLOR_BGR2YCrCb); // BGR 이미지를 밝기($Y$)와 색차($Cr, Cb$) 성분으로 분리하는 YCrCb 모델로 변환한다.
std::vector<cv::Mat> ycrcb_planes;
split(src_ycrb, ycrcb_planes); // 3개의 채널을 각각의 평면으로 나눈다.
/*
ycrcb_planes[0]: Y (휘도/밝기)
ycrcb_planes[1]: Cr (붉은색 색차)
ycrcb_planes[2]: Cb (푸른색 색차)
*/
// equalizeHist(ycrcb_planes[0], ycrcb_planes[0]); // 밝기 성분인 Y 채널에 대해서만 히스토그램 이퀄라이제이션을 수행한다.
// equalizeHist 대신 사용 가능
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(4.0); // 대비 제한 수치
clahe->apply(ycrcb_planes[0], ycrcb_planes[0]);
// merge & cvtColor: 변환된 Y와 원래의 Cr, Cb를 다시 합치고, 최종적으로 출력을 위해 BGR로 되돌린다.
cv::Mat dst_ycrcb;
merge(ycrcb_planes, dst_ycrcb);
cv::Mat dst;
cvtColor(dst_ycrcb, dst, cv::COLOR_YCrCb2BGR);
imshow("src", src);
imshow("dst", dst);
cv::waitKey();
return 0;
}

히스토그램 역투영(Histogram Backprojection) 기법을 사용하여 영상 내에서 특정 색상 영역(이 경우 사람의 피부색)을 검출하는 과정
왜 YCrCb 색 공간을 사용하는가?
코드에서 COLOR_BGR2YCrCb를 사용하고 채널 {1, 2}(Cr, Cb)를 선택한 이유는 매우 전략적이다.
- 조명 변화 대응: 피부색은 조명 밝기에 따라 RGB 값이 크게 변하지만, 밝기 정보인 $Y$를 제외한 **색차 정보(Cr, Cb)**는 상대적으로 일정하게 유지된다.
- 효율성: 3개 채널을 모두 쓰는 것보다 색상 정보를 담은 2개 채널만 사용함으로써 연산량을 줄이면서도 정확도를 높일 수 있다.
#include <opencv2/opencv.hpp>
#include <string>
int main() {
cv::Mat ref, ref_ycrb, mask;
ref = imread("ref.png", cv::IMREAD_COLOR);
mask = imread("mask.bmp", cv::IMREAD_GRAYSCALE);
cvtColor(ref, ref_ycrb, cv::COLOR_BGR2YCrCb);
cv::Mat hist;
int channels[] = { 1,2 }; // Y 채널(0번)을 무시하고 Cr과 Cb 채널만 사용하여 히스토그램을 만든다.
int cr_bins = 128; int cb_bins = 128;
int histSize[] = { cr_bins, cb_bins }; // 0 ~ 255 단계를 128개의 빈(Bin)으로 압축하여 계산했다. 빈의 개수가 너무 많으면 미세한 조명 차이에도 검출이 안 될 수 있고, 너무 적으면 다른 색상까지 검출될 수 있다.
float cr_range[] = { 0,256 };
float cb_range[] = { 0, 256 };
const float* ranges[] = { cr_range, cb_range };
calcHist(&ref_ycrb, 1, channels, mask, hist, 2, histSize, ranges); // ref.png 영상에서 mask.bmp가 가리키는 영역(보통 피부색 영역)의 Cr, Cb 채널 히스토그램을 계산
cv::Mat src, src_ycrcb;
src = imread("kids.png", cv::IMREAD_COLOR);
cvtColor(src, src_ycrcb, cv::COLOR_BGR2YCrCb);
cv::Mat backproj;
calcBackProject(&src_ycrcb, 1, channels, hist, backproj, ranges, 1, true); // 입력 영상(kids.png)의 모든 픽셀을 순회하며, 해당 픽셀의 Cr, Cb 값이 학습한 히스토그램에서 어느 정도의 빈도를 가졌는지 확인한다. 빈도가 높을수록 결과 영상(backproj)에서 밝게(흰색) 나타난다.
imshow("src", src);
imshow("backproj", backproj);
cv::waitKey();
return 0;
}

이진화(Thresholding)는 영상을 검은색(0)과 흰색(255)으로 분리하여 물체의 형태를 명확히 만드는 과정이다. 입력한 내용처럼 적절한 **임계값(Threshold)**을 설정하는 것이 시스템의 성능을 결정짓는 핵심이다.
자동 임계값 결정 방식
영상의 밝기 분포(히스토그램)를 분석하여 최적의 임계값을 스스로 찾아내는 방식이다.
- THRESH_OTSU (오츠 알고리즘): 가장 대중적으로 사용되는 방식이다. 배경과 객체 두 집단 간의 분산이 최대가 되는 지점을 임계값으로 잡는다. 즉, 히스토그램이 두 개의 봉우리(Bimodal)를 가질 때 가장 효과적이다.
- THRESH_TRIANGLE (삼각형 알고리즘): 히스토그램에서 가장 높은 봉우리와 양 끝점을 잇는 선을 긋고, 그 선에서 히스토그램 곡선까지의 거리가 가장 먼 곳을 임계값으로 설정한다.
- 사용 조건: 이 자동 방식들은 반드시 CV_8UC1(8비트 단일 채널 그레이스케일) 영상에만 적용할 수 있으며, 컬러 영상에는 직접 사용할 수 없다.
전역 이진화 vs 적응형 이진화
CNN 입력 전 물체의 특징을 명확히 하기 위해 이진화를 사용한다.
- 전역 이진화 (Global Thresholding): 영상 전체에 동일한 임계값을 적용한다. 조명이 균일한 환경에서 빠르고 효율적이다.
- 적응형 이진화 (Adaptive Thresholding): 각 픽셀 주변의 작은 블록 영역마다 서로 다른 임계값을 계산하여 적용한다. 그림자가 지거나 조명이 불균일한 영상에서 객체를 분리할 때 필수적이다.
모폴로지(Morphology) 연산
이진 영상에서 노이즈를 제거하거나 끊어진 선을 연결하는 후처리 기법이다. 좌표 이동이 아닌, 주변 픽셀 값에 따른 필터링 연산이다.
- 침식 (Erosion): 커널이 객체(흰색) 영역 내부에 완전히 포함되지 않으면 중심 픽셀을 0으로 만든다. 작은 돌출부나 노이즈가 제거되며, 객체 내의 구멍은 더 커지는 효과가 있다.
- 팽창 (Dilation): 커널 영역 내에 흰색 픽셀이 하나라도 있으면 중심 픽셀을 255로 만든다. 객체의 크기가 확장되며, 내부의 작은 구멍이나 끊어진 부분이 메워진다.
열기(Opening)와 닫기(Closing) 연산
침식과 팽창을 조합하여 객체의 전체적인 크기는 유지하면서 노이즈만 효과적으로 제거하는 기법이다.
- 열기 (Opening): 침식 → 팽창
- 작고 불필요한 돌기나 미세한 노이즈를 제거하는 데 유리하다.
- 서로 살짝 붙어 있는 객체들을 떨어뜨리는 효과가 있다.
- 닫기 (Closing): 팽창 → 침식
- 객체 내부의 작은 구멍(Hole)을 메우는 데 유리하다.
- 끊어져 있는 선이나 분리된 객체를 하나로 연결하는 효과가 있다.
- 특징: morphologyEx 함수를 사용하며, 침식과 팽창을 같은 횟수만큼 반복하기 때문에 객체의 본래 크기가 거의 변하지 않는다.
변환 기법: Warp와 Morph
- Warp (워핑): 좌표축을 변경하는 기법이다. Projective 변환 등을 위해 행렬(Matrix)을 던져주면 이미지의 구도를 바꾼다. 픽셀 값 자체가 변하는 것이 아니라 픽셀의 위치가 재배치되는 것이다.
- Morph (모핑): 'Morphology'가 형태학적 연산이라면, 모핑은 한 형태에서 다른 형태로 매끄럽게 변화시키는 기법을 의미한다.
객체 분석: 레이블링과 외곽선 검출
이진화와 모폴로지로 깨끗한 영상을 만들었다면, 이제 각 객체를 개별적으로 인식해야 한다.
레이블링 (Labeling)
- 개념: 이진 영상에서 연결된 흰색 객체들에 고유 번호(1, 2, 3...)를 부여하는 기법이다.
- 결과: Label Map이 생성되며, 이를 통해 영상 내에 객체가 몇 개 있는지, 각 객체의 크기(픽셀 수)가 얼마인지 파악할 수 있다.
- 함수: connectedComponentsWithStats를 쓰면 객체의 위치, 크기, 중심점 정보를 한 번에 얻을 수 있다.
외곽선 검출 (Contour Detection)
- 개념: 객체의 테두리 좌표를 순서대로 추출하는 기법이다.
- 특징: 추출된 좌표 순서를 분석하여 해당 객체가 사각형인지, 원형인지 등 도형의 형태를 체크할 수 있다.
- Bounding Box: 검출된 외곽선을 감싸는 최소 크기의 사각형을 씌워 객체의 범위를 표시한다.
트랙바를 사용하여 실시간으로 이진화 임계값(Threshold)을 조절
#include <opencv2/opencv.hpp>
#include <string>
static void on_thread(int, void*);
int main() {
cv::Mat src = cv::imread("neutrophils.png", cv::IMREAD_GRAYSCALE);
const std::string winName = "dst";
cv::namedWindow("src");
cv::namedWindow(winName);
cv::imshow("src", src);
cv::createTrackbar(
"Threshold", // 트랙바 이름
winName, // DST 창에 붙는다
nullptr, // 가리킬 정수 변수가 없다면 nullptr 가능
255, // 최댓값
on_thread, // 이벤트 호출됨: callback
(void*)(&src) // neutrophils.png 조작함 -> 주소를 던져준다
);
/*
cv::setTrackbarPos: 프로그램 실행 시 초기 임계값을 설정한다.
128로 설정한 이유는 8비트 그레이스케일 범위(0 ~ 255)의 딱 중간값이며,
일반적인 이미지에서 이진화를 시작하기에 가장 중립적인 지점이기 때문이다.
*/
cv::setTrackbarPos("Threshold", winName, 128);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}
/*
void* userdata 활용:
createTrackbar에서 (void*)(&src)로 원본 영상의 주소를 전달했다.
콜백 함수인 on_thread에서는 이를 다시 static_cast<cv::Mat*>로 형변환하여 원본 데이터에 접근한다.
이는 전역 변수 사용을 피하고 객체 지향적인 구조를 유지하기 위한 좋은 방법이다.
*/
void on_thread(int position, void* userdata) { // userdata: 주소가 void 타입
// 넘겨온 값 형변환 해야함
cv::Mat src = *(static_cast<cv::Mat*>(userdata));
if (src.empty()) return;
cv::Mat dst;
/*
cv::THRESH_BINARY: 가장 기본적인 이진화 방식이다.
픽셀값이 임계값(position)보다 크면 **최댓값(255, 흰색)**으로, 작거나 같으면 **0(검은색)**으로 바꾼다.
*/
cv::threshold(src, dst, position, 255, cv::THRESH_BINARY); // threshold 가 원본을 dst로 바꿔줌
imshow("dst", dst);
}

적응형 이진화(cv::adaptiveThreshold)를 사용하여 조명이 불균일한 스도쿠 영상에서 객체를 분리하는 코드
#include <opencv2/opencv.hpp>
#include <string>
// void* userdata 활용: main 함수의 src 주소를 on_trackbar로 안전하게 넘겨주어 전역 변수 없이 원본 영상을 조작했다. static_cast<cv::Mat*>를 통한 타입 변환도 정확하다.
static void on_trackbar(int position, void* userdata) {
cv::Mat* src = (static_cast<cv::Mat*>(userdata)); // 이 상태는 주소
// cv::Mat src = *(static_cast<cv::Mat*>(userdata));
int block_size = position;
if (block_size % 2 == 0) {
--block_size; // block size는 홀수가 좋다, 필터할 때 홀수 만듦
}
if (block_size < 3) {
block_size = 3; // block size는 최소 3 이상이어야 한다
}
cv::Mat dst;
cv::adaptiveThreshold(
*src, // *src: 주소로 가세요
dst,
255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C, // cv::ADAPTIVE_THRESH_GAUSSIAN_C: 기준점 주변의 픽셀 값들에 가우시안 분포에 따른 가중치를 부여하여 평균을 계산한다. 조명 변화가 심한 영상에서 일반 평균 방식보다 자연스러운 결과를 얻을 수 있다.
cv::THRESH_BINARY,
block_size, // 홀수(Odd Number) 필수: 모든 필터 기반 연산(Convolution 등)은 중심 픽셀을 기준으로 대칭을 이루어야 하므로 블록 크기는 반드시 홀수여야 한다.
5.0 // 계산된 평균값에서 이 상수만큼을 뺀 값을 최종 임계값으로 사용한다. 노이즈를 제거하고 객체를 더 뚜렷하게 만드는 역할을 한다.
);
cv::imshow("DST", dst);
}
int main() {
cv::Mat src = cv::imread("sudoku.jpg", cv::IMREAD_GRAYSCALE);
cv::imshow("SUDOKU", src);
cv::namedWindow("DST"); // 창 이름을 DST 로 만든다
// 트랙바를 움직일 때마다 블록 크기가 변하며 이진화 결과가 즉시 업데이트되므로, 스도쿠 판의 숫자가 가장 잘 보이는 최적의 파라미터를 찾기에 용이하다.
cv::createTrackbar("Blocksize", "DST", 0, 200, on_trackbar, (void*)(&src));
cv::waitKey();
cv::destroyAllWindows();
return 0;
}

우유 방울 이미지(milkdrop.bmp)를 이진화한 뒤, **열기(Opening)**와 닫기(Closing) 연산을 반복 적용하여 영상의 노이즈를 제거하고 형태를 정제하는 과정
#include <opencv2/opencv.hpp>
#include <string>
int main() {
const auto src = cv::imread("milkdrop.bmp", cv::IMREAD_GRAYSCALE); // 이진화 위해 그레이 스케일, auto 쓸 때 const 쓰는게 좋음
// 모폴로지 하려면 반드시 이진화를 해야함 -> threshold;
cv::Mat binary_image;
// 우유 방울처럼 배경과 객체의 밝기 차이가 뚜렷한 영상에서 오츠 알고리즘은 최적의 분리 기준을 찾아낸다.
cv::threshold(src, binary_image, 0.0, 255.0, cv::THRESH_BINARY | cv::THRESH_OTSU); // cv::THRESH_OTSU를 사용하여 임계값을 자동으로 결정
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(11, 11)); // cv::getStructuringElement: cv::Mat()을 쓰면 기본 $3 \times 3$ 커널이 사용되지만, 명시적으로 Size(3, 3)이나 Size(11, 11)을 정의하면 연산이 미치는 범위를 직접 조절할 수 있다.
cv::Mat dst_open;
cv::Mat temp = binary_image.clone();
/*
열기 (Opening: 침식 → 팽창)
목적: 아주 작은 파편이나 돌기 같은 노이즈를 제거할 때 사용한다.
코드 특징: iterations 인자에 3을 넣었는데, 이는 침식 3번 후 팽창 3번을 수행한다는 의미다. 반복 횟수가 많아질수록 본체에서 떨어져 나간 작은 점들이 더 확실히 사라진다.
결과: 우유 방울 주변의 자잘한 물방울들이 제거되어 깔끔한 본체만 남게 된다.
*/
//cv::morphologyEx(
// binary_image,
// dst_open,
// cv::MORPH_OPEN, // 침식 -> 팽창 * 픽셀의 크기는 변하지 않는다
// //kernel
// cv::Mat() // 기본이 3*3 matrix
//); // op: 침식하고 팽창
cv::morphologyEx(
binary_image,
dst_open,
cv::MORPH_OPEN,
cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)),
cv::Point(-1, -1), // 커널의 중심점(Anchor)을 정중앙으로 설정한다는 표준적인 표현이다.
3 // 여기에 반복 횟수를 써주면 5번 반복함
);
// int open_cnt = 5;
// for(int i = 0; i < open_cnt; ++i) {
// cv::morphologyEx(temp, temp, cv::MORPH_OPEN, cv::Mat());
// }
// dst_open = temp;
cv::Mat dst_close;
/*
닫기 (Closing: 팽창 → 침식)
목적: 객체 내부의 작은 구멍(Hole)을 메우거나, 끊어진 부분을 연결할 때 사용한다.
코드 특징: 마찬가지로 3회 반복을 설정했다. 팽창이 먼저 일어나므로 객체 내부의 어두운 빈틈이 먼저 채워진다.
결과: 우유 방울 내부에 빛 반사 등으로 생긴 검은 구멍들이 흰색으로 메워져 꽉 찬 형태가 된다.
*/
//cv::morphologyEx(
// binary_image,
// dst_close,
// cv::MORPH_CLOSE, // 팽창 -> 침식
// //kernel
// cv::Mat()
//);
cv::morphologyEx(
binary_image,
dst_close,
cv::MORPH_CLOSE,
cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)),
cv::Point(-1, -1),
3 // 여기에 반복 횟수를 써주면 5번 반복함
);
// int close_cnt = 5;
// cv::Mat temp2 = binary_image.clone();
// for (int i = 0; i < close_cnt; ++i) {
// cv::morphologyEx(temp2, temp2, cv::MORPH_CLOSE, cv::Mat());
// }
// dst_close = temp2;
cv::imshow("SRC", src);
cv::imshow("Binary", binary_image);
cv::imshow("OPEN", dst_open);
cv::imshow("CLOSE", dst_close);
cv::imshow("SRC", src);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}

레이블링(Labeling)의 핵심 원리인 Connected Component (연결된 구성 요소) 분석을 시각적으로 가장 잘 보여주는 예시
#include <opencv2/opencv.hpp>
#include <string>
int main() {
uchar data[] = { // connect component -> 객체화 시켜줌
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, 0, 3, 0, 0, 3, 0,
0, 0, 3, 3, 3, 3, 3, 0,
0, 0, 0, 0, 0, 0, 0, 0
};
cv::Mat src(8, 8, CV_8UC1, data);
cv::Mat dst = src * 255; // src * 255: connectedComponents 함수는 보통 이진 영상(0과 255)을 입력으로 받는다. 1, 2, 3으로 되어 있는 데이터를 255를 곱해 명확한 이진 형태로 변환했다.
cv::Mat label;
int count = cv::connectedComponents(dst, label); // cv::connectedComponents: 연결된 흰색(255) 픽셀들을 찾아 각각에 고유 번호를 매긴다.
std::cout << "Source : \r" << src << "\n";
std::cout << "DST: \r" << dst << "\n";
std::cout << "LABEL: \r" << label << "\n";
std::cout << "찾은 객체 수: " << count << "\n";
cv::imshow("SRC", dst);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}

이미지 내 객체의 **외곽선(Contour)**을 추출하고, 추출된 각 외곽선을 무작위 색상으로 그려서 시각화하는 전형적인 객체 탐지 알고리즘
#include <opencv2/opencv.hpp>
#include <string>
int main() {
const auto src = cv::imread("contours.bmp", cv::IMREAD_GRAYSCALE);
std::vector<std::vector<cv::Point>> contours; // 외곽선은 여러 개일 수 있고, 각 외곽선은 여러 개의 점(cv::Point)으로 구성되므로 2차원 벡터 구조를 사용
// cv::findContours: 이진화된 이미지에서 같은 밝기를 가진 연결된 경계선을 찾는 핵심 함수
cv::findContours(
src,
contours,
cv::RETR_LIST, // 계층 구조를 무시하고 모든 외곽선을 나열하는 모드
cv::CHAIN_APPROX_NONE // 외곽선을 구성하는 모든 점의 좌표를 저장한다. (메모리는 많이 쓰지만 정확함)
); // 이진화된 이미지를 넣어야 함, 배열타입 -> vector 사용해야한다
cv::Mat dst;
cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);
int counting = 0;
// contours.begin(): 시작
// it != contours.end(): 끝이 아니다
for (auto it = contours.begin(); it != contours.end(); ++it) {
cv::Scalar c(rand() % 255, rand() % 255, rand() % 255); // &FF = 11111111, 하위비트만 갖겠다라는 뜻(상위비트와 하위비트 중)
cv::drawContours(dst, contours, counting++, c, 2); // cv::drawContours: 찾은 외곽선 좌표를 기반으로 이미지 위에 선을 그린다. counting++를 인덱스로 사용하여 각 외곽선을 하나씩 그려낸다.
}
/*
for (int i = 0; i < contours.size(); i++) {
cv::drawContours(dst, contours, i, cv::Scalar(rand() % 255, rand() % 255, rand() % 255));
}
std::for_each(contours.begin(), contours.end(), [&](const std::vector<cv::Point>& contour) {
cv::drawContours(dst, contours, counting++, cv::Scalar(rand() % 255, rand() % 255, rand() % 255)))
});
*/
std::cout << "찾은 개수 : " << counting << "\r\n";
cv::imshow("SRC", src);
cv::imshow("DST", dst);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}

'OpenCV' 카테고리의 다른 글
| OpenCV - Mat, 간단한 영상 처리 (0) | 2025.12.19 |
|---|---|
| OpenCV - 입문 (2) | 2025.12.18 |