컬러 영상 처리

OpenCV 에서 컬러 영상 표현 방법

  • 파랑(Blue), 초록(Green), 빨강(Red)색 성분을 각각 256 단계로 표현
  • CV_8UC3 타입
  • OpenCV에서는 RGB 순서가 아닌 BGR 순서를 기본으로 사용
Mat img = imread("lenna.bmp", IMREAD_COLOR);
Mat img2(rows,cols,CV_8UC3);

Mat img3 = imread("leanna.bmp", IMREAD_GRAYSCALE);
Mat img4;
cvtColor(img3, img4, COLOR_GRAY2BGR); // img4 영상의 각 픽셀은 B,G,R 색 성분 값이 모두 같게 설정됨

컬러 영상 반전

  • ”-“ 연산자 오버로딩 사용 방법
void ColorOp1()
{
	Mat src = imread("mandrill.bmp")
	
#if 0
	Mat dst = Scalar(255, 255, 255) - src;
#else 
	Mat dst(src.rows, src.cols, CV_8UC3);
	
	for (int y = 0; y< src.rows; y++){
		for (int x = 0; x < src.cols; x++) {

			dst.at<Vec3b>(y, x) = Vec3b(255, 255, 255) - src.at<Vec3b>(y, x);
			
			/*
            Vec3b& p1 = src.at<Vec3b>(y, x);
            Vec3b& p2 = dst.at<Vec3b>(y, x); // 참조인 &를 붙여야 p2의 픽셀 값이 바뀔때 dst의 픽셀 값도 바뀜      
			p2 = Vec3b(255, 255, 255) - p1; */
			
			
		}
	}	
#endif

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
	destroyAllWindows();
}

image

컬러 영상을 그레이스케일로 변환

  • 다음과 같은 수식을 사용하여 그레이스케일로 변환함
    • 대략적으로 R G B 색상 성분이 3 : 6 : 1을 따름

image

  • 장점 : 데이터 저장 용량 감소, 연산 처리 속도 향상
  • 단점 : 색상 정보 손실
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0);
  • src : 입력 영상
  • dst : 출력 영상
  • code : 색 변환 코드

image

  • dstCn : 결과 영상의 채널 수. 0이면 자동 결정
void ColorOp2()
{
	Mat src = imread("mandrill.bmp");

	if (src.empty()) {
		cerr << "Image laod failed!" << endl;
		return;
	}

#if 1
	Mat dst;
	cvtColor(src, dst, COLOR_BGR2GRAY);
#else
	Mat dst(src.rows, src.cols, CV_8UC1);

	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			uchar b = src.at<Vec3b>(y, x)[0];
			uchar g = src.at<Vec3b>(y, x)[1];
			uchar r = src.at<Vec3b>(y, x)[2];

			//uchar gray = (uchar)(0.299 * r + 0.587 * g + 0.114 * b + 0.5);
			//dst.at<uchar>(y, x) = gray;

			dst.at<uchar>(y, x) = (uchar)RGB2GRAY(r, g, b);
		}
	}
#endif

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
	destroyAllWindows();
}

image

다양한 색 공간

  • RGB 색 공간
    • 빛의 삼원색인 빨간색(R), 녹색(G), 파란색(B)를 혼합하여 색상을 표현
    • 카메라 센서 Bayer 필터, TV & 모니터, 비트맵

image

  • HSV 색 공간
    • Hue : 색상, 색의 종류, 0도 ~ 360도 사이의 각도로 표현 (0~179)
    • Saturation : 채도, 색의 탁하고 선명한 정도 (0~255)
    • Value : 명도, 빛의 밝기 (0~255)
    • 일반적인 8비트 영상의 경우, Hue 값의 범위를 0~360로 표현할 수 없으므로 Hue/2를 대신 사용

image

  • YCrCb 색 공간
    • 휘도(Luminance)와 색차(Chrominance) 성분을 이용하여 색을 표현하는 방법
      • Y : 휘도, 밝기 정보(Luma) (0~255)
      • Cr, Cb : 색차(Chroma) (0~255)
    • 유사한 색 공간으로 YIQ, YUV, YPbPr 등이 있음

image

채널 분리

void split(const Mat& src, Mat * mvbegin);
void split(InputArray src, OutputArrayOfArrays mv);
  • src : (입력) 다채널 행렬
  • mvbegin : (출력) Mat 배열의 주소
  • mv : (출력) 행렬의 벡터
Mat src = imread("lenna.bmp");

vector<Mat> planes;
split(src, planes);

image

채널 병합

void merge(const Mat * mv, size_t count, OutputArray dst);
void merge(InputArrayOfArrays mv, OutputArray dst);
  • mv : 1채널 Mat 배열 또는 행렬의 벡터
  • count : Mat 배열의 크기
  • dst : 다채널 행렬
Mat src = imread("lenna.bmp");

vector<Mat> planes;
split(src, planes);

swap(planes[0], planes[2]);

Mat dst;
merge(planes, dst);

image

  • 컬러 영상의 색상 평면 나누기
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

void split_bgr();
void split_hsv();
void split_ycrcb();

int main()
{
	split_bgr();
	split_hsv();
	split_ycrcb();
}

void split_bgr()  // RGB 색상 평면 나누기
{
	Mat src = imread("candies.png", IMREAD_COLOR);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return;
	}

	vector<Mat> bgr_planes;
	split(src, bgr_planes);

	imshow("src", src);
	imshow("B plane", bgr_planes[0]);
	imshow("G plane", bgr_planes[1]);
	imshow("R plane", bgr_planes[2]);

	waitKey();
	destroyAllWindows();
}

void split_hsv()  // HSV 색상 평면 나누기
{
	Mat src = imread("candies.png", IMREAD_COLOR);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return;
	}

	Mat src_hsv;
	cvtColor(src, src_hsv, COLOR_BGR2HSV);

	vector<Mat> hsv_planes;
	split(src_hsv, hsv_planes);

	imshow("src", src);
	imshow("H plane", hsv_planes[0]);
	imshow("S plane", hsv_planes[1]);
	imshow("V plane", hsv_planes[2]);

	waitKey();
	destroyAllWindows();
}

void split_ycrcb() // YCrCb 색상 평면 나누기
{
	Mat src = imread("candies.png", IMREAD_COLOR);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return;
	}

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

	vector<Mat> ycrcb_planes;
	split(src_ycrcb, ycrcb_planes);

	imshow("src", src);
	imshow("Y plane", ycrcb_planes[0]);
	imshow("Cr plane", ycrcb_planes[1]);
	imshow("Cb plane", ycrcb_planes[2]);

	waitKey();
	destroyAllWindows();
}

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

  • 입력 컬러 영상의 R, G, B를 따로 히스토그램 평활화를 진행한 후에 합치기
    • 변환 함수의 형태가 R, G, B 3개의 색상 성분에 대해 다른 형태로 적용이 되므로 색감 자체가 변경이 되는 문제 발생

image

  • 정상적인 컬러 영상의 히스토그램 평활화를 수행 방법
    • 밝기 성분에 대해서만 히스토그램 평활화를 수행함

image

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("mandrill.bmp", IMREAD_COLOR);

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

	Mat src_ycrcb;
	cvtColor(src, src_ycrcb, COLOR_BGR2YCrCb); // BGR 에서 YCrCb로 변경하면서 src_ycrcb는 CV_8UC3의 형태를 가짐

	vector<Mat> planes;
	split(src_ycrcb, planes); // planes안에 3개의 Mat 객체가 들어있는 형태로 변경됨 planes[0] : y, planes[1] : Cr, planes[2] : Cb

	equalizeHist(planes[0], planes[0]); // planes[0]만 평활화

	Mat dst_ycrcb;
	merge(planes, dst_ycrcb);

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

	imshow("src", src);
	imshow("dst", dst); // imshow 함수는 무조건 BGR 색상 성분의 Mat 객체를 전달받아야함 따라서 위의 dst_ycrcb를 YCrCb 성분의 형태를 BGR로 변환해야함
	waitKey();
}
  • 히스토그램 평활화에 의해 명암비가 증가된 결과 영상을 확인할 수 있음

image

image

컬러 영상의 색감 바꾸기

  • R, G, B 색상 채널의 커브를 변경하여 컬러 톤을 변경

image

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

uchar curve1[256] = {
	0, 2, 3, 4, 7, 8, 9, 11,
	12, 14, 15, 16, 18, 19, 21, 22,
	24, 25, 27, 28, 30, 31, 33, 34,
	36, 37, 39, 40, 42, 43, 45, 46,
	47, 49, 50, 52, 53, 55, 56, 58,
	59, 61, 62, 63, 65, 66, 68, 69,
	71, 72, 73, 75, 76, 78, 79, 80,
	82, 83, 85, 86, 87, 89, 90, 91,
	93, 94, 95, 97, 98, 100, 101, 102,
	104, 105, 106, 107, 109, 110, 111, 113,
	114, 115, 117, 118, 119, 120, 122, 123,
	124, 125, 127, 128, 129, 130, 131, 133,
	134, 135, 136, 137, 139, 140, 141, 142,
	143, 144, 145, 146, 148, 149, 150, 151,
	152, 153, 154, 155, 156, 157, 158, 159,
	160, 161, 162, 164, 165, 165, 166, 167,
	168, 169, 170, 171, 172, 173, 174, 175,
	176, 177, 178, 179, 180, 180, 181, 182,
	183, 184, 185, 186, 186, 187, 188, 189,
	190, 191, 191, 192, 193, 194, 195, 195,
	196, 197, 198, 198, 199, 200, 201, 201,
	202, 203, 204, 204, 205, 206, 207, 207,
	208, 209, 209, 210, 211, 211, 212, 213,
	213, 214, 215, 215, 216, 217, 217, 218,
	219, 219, 220, 221, 221, 222, 222, 223,
	224, 224, 225, 226, 226, 227, 227, 228,
	229, 229, 230, 230, 231, 231, 232, 233,
	233, 234, 234, 235, 235, 236, 237, 237,
	238, 238, 239, 239, 240, 241, 241, 242,
	242, 243, 243, 244, 244, 245, 246, 246,
	247, 247, 248, 248, 249, 249, 250, 250,
	251, 252, 252, 253, 254, 254, 255, 255
};

uchar curve2[256] = {
	0, 0, 1, 1, 1, 2, 3, 3,
	4, 4, 5, 6, 6, 7, 7, 8,
	8, 9, 9, 10, 10, 11, 11, 12,
	12, 13, 13, 14, 14, 15, 15, 16,
	17, 17, 18, 18, 19, 19, 20, 20,
	21, 21, 22, 23, 23, 24, 24, 25,
	25, 26, 27, 27, 28, 28, 29, 30,
	30, 31, 31, 32, 33, 33, 34, 35,
	35, 36, 37, 37, 38, 38, 39, 40,
	40, 41, 42, 43, 43, 44, 45, 45,
	46, 47, 47, 48, 49, 50, 50, 51,
	52, 53, 53, 54, 55, 56, 57, 57,
	58, 59, 60, 61, 61, 62, 63, 64,
	65, 66, 67, 68, 68, 69, 70, 71,
	72, 73, 74, 75, 76, 77, 78, 79,
	80, 81, 82, 82, 83, 85, 86, 87,
	88, 89, 90, 91, 92, 93, 94, 95,
	96, 97, 98, 99, 100, 102, 103, 104,
	105, 106, 107, 108, 110, 111, 112, 113,
	114, 115, 117, 118, 119, 120, 121, 123,
	124, 125, 126, 128, 129, 130, 131, 133,
	134, 135, 136, 138, 139, 140, 141, 143,
	144, 145, 147, 148, 149, 151, 152, 153,
	155, 156, 157, 159, 160, 161, 163, 164,
	165, 167, 168, 169, 171, 172, 174, 175,
	176, 178, 179, 180, 182, 183, 185, 186,
	187, 189, 190, 192, 193, 195, 196, 197,
	199, 200, 202, 203, 205, 206, 207, 209,
	210, 212, 213, 215, 216, 217, 219, 220,
	222, 223, 225, 226, 228, 229, 230, 232,
	233, 235, 236, 238, 239, 241, 242, 244,
	245, 246, 248, 249, 250, 252, 253, 255
};

int main()
{
	Mat src = imread("girl.jpg", IMREAD_COLOR);

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

	vector<Mat> channels;
	split(src, channels);

	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			channels[2].at<uchar>(y, x) = curve1[channels[2].at<uchar>(y, x)]; // 빨간색 성분을 강화
			channels[0].at<uchar>(y, x) = curve2[channels[0].at<uchar>(y, x)]; // 파랑색 성분을 낮춰줌
		}
	}

	Mat dst;
	merge(channels, dst);

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

  • 빨간색 성분을 강화하고, 파랑색 성분을 낮춰서 따듯해진 dst 영상을 확인할 수 있음

image

특정 색상 영역 추출

  • RGB, HSV, YCrCb 등의 색 공간에서 각 색상 성분의 범위를 지정하여 특성 색상 영역 추출
    • 조명으로 인해 RGB의 색공간을 사용했을 때 녹색을 제대로 찾지 못하는 결과가 나옴
    • HSV, YCrCb 색공간에서는 V를 무시하고 Y를 빼서 HS만 사용하거나 CrCb만 사용해서 색상을 검출할 때 조명의 밝기를 크게 받지 않음

image

범위 선택 함수

void inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst);
  • src : 입력 행렬
  • lowerb : 하한 값(Mat or Scalar)
  • upperb : 상한 값(Mat or Scalar)
  • dst : 입력 영상과 동일 크기(CV_8UC1). 범위 안에 들어가는 픽셀만 255로 설정됨
  • 그레이스케일 단일채널에서는 0번 채널에 대해서만 비교해서 생성을하고, 컬러영상 다중채널에서는 각각의 채널값에 대해서 AND 연산을 수행 했을때 모두 참인 결과에 대해서만 255로 설정이 됨

image

색상 범위 지정 기능을 이용한 컬러 필터 만들기

image

#include <iostream>
#include <algorithm>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int pos_hue1 = 50, pos_hue2 = 80, pos_sat1 = 150, pos_sat2 = 255;
Mat src, src_hsv, dst, mask;

void on_hsv_changed(int, void*) // 트랙바를 움직일때 자동으로 호출됨
{
	Scalar lowerb(pos_hue1, pos_sat1, 0); // 하한값 value의 상한값은 0으로 지정
	Scalar upperb(pos_hue2, pos_sat2, 255); // 상한값 value의 상한값은 255로 지정 value는 어떤값이든 상관없이 H와 S의 값만 만족하면 찾겠다 라는 의미
	inRange(src_hsv, lowerb, upperb, mask); // 위에서 지정한 범위에 만족하는 색상은 흰색255 로 설정되고 그렇지 않은 부분은 검정색 0 으로 설정한 것을 mask에 저장

	cvtColor(src, dst, COLOR_BGR2GRAY); // 컬러영상 src 영상을 그레이영상 dst로 만듬
	cvtColor(dst, dst, COLOR_GRAY2BGR); 
	// 그레이영상 dst를 다시 BGR로 바꾼다해서 이미 손상된 컬러 정보가 회복되진 않음.
	// 따라서 그레이로 보이는 것은 똑같은데 dst가 가진 채널 개수가 1개에서 3개로 늘어남(CV_8UC3 타입으로 변환)
	src.copyTo(dst, mask); // mask 영상에서 검정색이 아닌 부분(픽셀값이 0이 아닌부분)에 대해서 src영상을 dst 영상으로 Copy

	imshow("mask", mask);
	imshow("dst", dst);
}

int main()
{
	src = imread("candies.png", IMREAD_COLOR);

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

	cvtColor(src, src_hsv, COLOR_BGR2HSV);

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

	imshow("src", src);

	createTrackbar("Lower Hue", "dst", &pos_hue1, 179, on_hsv_changed);
	createTrackbar("Upper Hue", "dst", &pos_hue2, 179, on_hsv_changed);
	createTrackbar("Lower Sat", "dst", &pos_sat1, 255, on_hsv_changed);
	createTrackbar("Upper Sat", "dst", &pos_sat2, 255, on_hsv_changed); 
	// Trackbar 4개를 붙임
	
	on_hsv_changed(0, 0);
	// 프로그램이 실행되자마자 on_hsv_changed가 실행될 수 있게 강제로 작성함

	waitKey();
}

image

히스토그램 역투영

  • 주어진 히스토그램 모델에 영상의 픽셀들이 얼마나 일치하는지를 검사하는 방법
  • 임의의 색상 영역을 검출할 때 효과적임
  • 특정 색상 영역 추출 방법 처럼조명의 밝기 변화의 영향을 줄이기 위해 보통 HSV 색 공간에서 HS 성분만 사용하거나, YCrCb 색 공간에서 CrCb 성분만 사용

  • 아래 이미지의 들판을 CrCb 성분으로 나타내고 그리고 전체 영상에 대해 이 히스토그램을 만족하는 픽셀을 모두 찾아 표현
void calcBackProject(const Mat* images, int nimages, const int* channels, InputArray hist, OutputArray backProject, const float ** ranges, double scale = 1, bool uniform = true);
  • images : 입력 영상 배열 주소 및 입력 영상 주소
  • nimages : 입력 영상 개수
  • channels : 역투영 계산에 사용할 채널 목록
  • hist : 입력 히스토그램
  • backProject : (출력) 히스토그램 역투영 결과 행렬. 입력 영상과 동일 크기, CV_8U
  • ranges : 히스토그램 빈 경계값 배열의 배열
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	// Calculate CrCb histogram from a reference image

	Mat src = imread("cropland.png", IMREAD_COLOR);

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

	Rect rc = selectROI(src); // 마우스를 이용해서 특정 영역을 선택후 스페이스나 엔터키를 누르면 사용자가 선택한 영역이 rc로 전달 

	Mat src_ycrcb;
	cvtColor(src, src_ycrcb, COLOR_BGR2YCrCb); // src를 ycrcb로 변환한 것을 src_ycrcb로

	Mat crop = src_ycrcb(rc); // src_ycrcb의 ROI 지정한 rc에 해당하는 부분을 crop에 저장

	Mat hist;
	int channels[] = {1, 2};
	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};

	// 부분 영상에 대한 히스토그램 계산
	calcHist(&crop, 1, channels, Mat(), hist, 2, histSize, ranges);
	
	// 전체 영상에 대해 히스토그램 역투영
	Mat backproj;
	calcBackProject(&src_ycrcb, 1, channels, hist, backproj, ranges);

	Mat dst = Mat::zeros(src.rows, src.cols, CV_8UC3);
	src.copyTo(dst, backproj);

	//imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

image

  • 히스토그램 역투영을 이용한 살색 검출 예제
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	// Calculate CrCb histogram from a reference image

	Mat ref, ref_ycrcb, mask;
	ref = imread("ref.png", IMREAD_COLOR);
	mask = imread("mask.bmp", IMREAD_GRAYSCALE);
	cvtColor(ref, ref_ycrcb, COLOR_BGR2YCrCb);

	Mat hist;
	int channels[] = { 1, 2 };
	int cr_bins = 128; int cb_bins = 128; // cr, cb를 원래의 255이 아닌 128개로 나눈 것으로 설정
	int histSize[] = { cr_bins, cb_bins };
	float cr_range[] = { 0, 256 };
	float cb_range[] = { 0, 256 };
	const float* ranges[] = { cr_range, cb_range };

	calcHist(&ref_ycrcb, 1, channels, mask, hist, 2, histSize, ranges); // mask 이미지를 가지고 Mat 클래스 타입 hist에 저장

#if 1
	Mat hist_norm;
	normalize(hist, hist_norm, 0, 255, NORM_MINMAX, CV_8UC1);
	imshow("hist_norm", hist_norm);
#endif

	Mat src, src_ycrcb;
	src = imread("kids.png", IMREAD_COLOR);
	cvtColor(src, src_ycrcb, COLOR_BGR2YCrCb);

	Mat backproj;
	calcBackProject(&src_ycrcb, 1, channels, hist, backproj, ranges);
	
	GaussianBlur(backproj, backproj, Size(), 1.0); // 가우시안 블러딩 결과를 약간 스무딩함, 간혹 튀는 값인 노이즈를 조금 상쇄시키기 위함
	backproj = backproj > 50; // 50 보다 작은 것은 무시

	Mat dst = Mat::zeros(src.rows, src.cols, CV_8UC3);
	src.copyTo(dst, backproj); // src 영상을 backproj에서 흰색으로 되어있는 부분만 dst에로 저장

	imshow("ref", ref);
	imshow("mask", mask);
	imshow("src", src);
	imshow("backproj", backproj);
	imshow("dst", dst);
	waitKey();
}
  • 가우시안 블러를 적용하기 전 backproj 영상

image

  • 가우시안 블러를 적용한 backproj 영상

image

  • src 영상을 backproj에서 흰색으로 되어있는 부분만 dst로 저장한 영상

image