영상의 픽셀에 접근하여 데이터를 처리하는 방법에 대해서 알아보겠습니다.
- at( int x, int y )
각 원소에 접근하기 위한 방법으로 Mat의 at 메소드를 이용할 수 있습니다. at은 컴파일 시 타입을 필요로 하는데요. 이는 처리할 영상이 컬러인지 그레이인지, 어떤 데이터가 들어있는지를 알아야 하기 때문입니다. 이러한 타입 설정은
image.at<uchar>(j,i) = 255;
image.at<cv::Vec3b>(j,i)[channel] = 255;
와 같이 코드로 표현할 수 있습니다. uchar의 경우 그레이(흑백) 이미지, cv::Vec3b의 경우 컬러 이미지에 사용되며 3개의 unsigned char를 가지는 벡터가 됩니다. 컬러이미지의 경우 RGB 3개의 채널이 있기 때문에 어떤 채널에 접근할 것인지를 추가적으로 입력해주어야 합니다.
※ 참고 : 벡터 타입을 사용할 때 숫자 설정으로 2채널 4채널을 선택할 수 있으며(Vec2b, Vec4b), 외에 마지막 문자를 각 형의 첫 글자로 수정하여 short, int, float, double을 저장하도록 할 수 있습니다. (b는 byte를 의미합니다.)
Mat 클래스의 at 메소드를 사용하면 템플릿 인자를 반드시 지정하기 때문에 반복작업시 번거로울 수 있습니다.
이러한 번거로움을 Mat의 템플릿 하위 클래스인 Mat_으로 해결할 수 있습니다. 행렬의 타입을 알고 있을때 초기에 참조시에만 설정을 해주면 이후에는 인자를 설정하지 않아도 됩니다.
cv::Mat_<uchar> im2 = img; //im2는 img를 참조
im2(1,2) = 0; //1행 2열에 해당하는 픽셀 접근
다만 코드가 길어지고 다양한 변수들이 존재하게 되면 타입을 한눈에 확인하기 어렵기 때문에 가독성이 떨어질 수 있으며 디버깅에 어려움이 있을수 있습니다.
- 결과
※ 참고 : 영상에 흰색, 검은색 노이즈가 위와 같은 형태로 있을 때, 이러한 노이즈를 소금과 후추를 뿌려놓은것 같다고 하여 Salt and pepper noise(점잡음)이라고 부릅니다.
제공되는 colorReduce.cpp 예제에는 무려 14가지의 픽셀 접근 방법이 설명되어 있습니다. 저는 이중에서 ptr을 이용한 방법 2가지를 설명할 것이고 추가적인 방법이 필요하신분들은 소스 코드를 참고하시면 좋을 것 같습니다.
- ptr<T>(int i0)
포인터를 이용해 픽셀에 접근할 때 사용하는 방법입니다. T에는 타입을 i0에는 행의 값을 넣어주게 됩니다. 이후 []나 * ++를 통해 열 단위값을 선택, 픽셀에 접근하게 됩니다.
굳이 at이 있는데도 ptr을 설명드리는 이유는 연산 속도의 차이 때문입니다. at은 접근하고 데이터를 읽고 쓰는 과정에서 추가적인 연산을 통해 안정성을 확보하지만 ptr은 그렇지 않기 때문에 연산속도에서 차이가 발생한다고 합니다.
- 결과
왼쪽 영상은 입력영상에 대해 컬러감축을 수행한 결과이며 오른쪽 영상은 이를 20회 반복한 후 소요된 시간의 평균 값을 구한 것입니다. 빨간색 밑줄은 ptr연산에 걸린 시간이며 파란색 밑줄은 at을 이용하여 연산하였을 때 걸린 시간 입니다. 약 50배 정도의 연산속도 차이를 보임을 확인할 수 있습니다. 이러한 특징 때문에 초기 개발 단계에서는 at을 사용하여 코드를 진행하고 이후 최적화 단계에서 ptr로 수정하는 경우가 많습니다.
※ 참고 : opencv 3.0 버전 이후로는 이러한 at 연산에 소요되는 시간이 획기적으로 감소하였다고 합니다.
글 작성을 위해 테스트하는 과정을 3.1버전에서 하였는데 안타깝게도 아직 차이가 많이 있는 것 같습니다.
위의 연산시간은 Debug 모드를 기준하였으며 혹시나 해서 Release 모드로도 수행하여 보았습니다. 결과는 약 2배정도 차이가 있는 것을 확인하였습니다.
- 코드 첨부
saltImage.cpp
/*------------------------------------------------------------------------------------------*\
This file contains material supporting chapter 2 of the cookbook:
Computer Vision Programming using the OpenCV Library.
by Robert Laganiere, Packt Publishing, 2011.
This program is free software; permission is hereby granted to use, copy, modify,
and distribute this source code, or portions thereof, for any purpose, without fee,
subject to the restriction that the copyright notice may not be removed
or altered from any source or altered source distribution.
The software is released on an as-is basis and without any warranties of any kind.
In particular, the software is not guaranteed to be fault-tolerant or free from failure.
The author disclaims all warranties with regard to this software, any use,
and any consequent failure, is purely the responsibility of the user.
Copyright (C) 2010-2011 Robert Laganiere, www.laganiere.name
\*------------------------------------------------------------------------------------------*/
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
void salt(cv::Mat &image, int n) {
int i,j;
for (int k=0; k<n; k++) {
// rand()는 MFC 임의 숫자 생성기이다
i= rand()%image.cols;
j= rand()%image.rows;
if (image.channels() == 1) { // 명암도 영상
image.at<uchar>(j,i)= 255;
} else if (image.channels() == 3) { // 컬러 영상
image.at<cv::Vec3b>(j,i)[0]= 255;
image.at<cv::Vec3b>(j,i)[1]= 255;
image.at<cv::Vec3b>(j,i)[2]= 255;
}
}
}
int main()
{
srand(cv::getTickCount()); // 임의 숫자 생성기 초기화
cv::Mat image= cv::imread("boldt.jpg",0);
salt(image,3000);
cv::namedWindow("Image");
cv::imshow("Image",image);
cv::imwrite("salted.bmp",image);
cv::waitKey(5000);
return 0;
}
colorReduce.cpp
/*------------------------------------------------------------------------------------------*\
This file contains material supporting chapter 2 of the cookbook:
Computer Vision Programming using the OpenCV Library.
by Robert Laganiere, Packt Publishing, 2011.
This program is free software; permission is hereby granted to use, copy, modify,
and distribute this source code, or portions thereof, for any purpose, without fee,
subject to the restriction that the copyright notice may not be removed
or altered from any source or altered source distribution.
The software is released on an as-is basis and without any warranties of any kind.
In particular, the software is not guaranteed to be fault-tolerant or free from failure.
The author disclaims all warranties with regard to this software, any use,
and any consequent failure, is purely the responsibility of the user.
Copyright (C) 2010-2011 Robert Laganiere, www.laganiere.name
\*------------------------------------------------------------------------------------------*/
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
// .ptr와 [] 사용하기
void colorReduce0(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols * image.channels(); // 각 행에 있는 원소의 총 개수
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
data[i]= data[i]/div*div + div/2;
// 각 화소 처리 끝 ----------------
} // end of line
}
}
// .ptr와 * ++ 사용하기
void colorReduce1(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols * image.channels(); // 각 행에 있는 원소의 총 개수
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
*data++= *data/div*div + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// .ptr 와 * ++ 그리고 나머지 사용하기
void colorReduce2(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols * image.channels(); // 각 행에 있는 원소의 총 개수
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
int v= *data;
*data++= v - v%div + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// .ptr와 * ++ 그리고 비트별 연산
void colorReduce3(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols * image.channels(); // 각 행에 있는 원소의 총 개수
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, div=16이면 mask= 0xF0
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
*data++= *data&mask + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// 저수준 포인터 산술 연산
void colorReduce4(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols * image.channels(); // 각 행에 있는 원소의 총 개수
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
int step= image.step; // 유효 너비
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
// 영상 버퍼에 대한 포인터 가져오기
uchar *data= image.data;
for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
*(data+i)= *data&mask + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
data+= step; // 다음 행
}
}
// image.cols * image.channels()로 .ptr와 * ++ 그리고 비트별 연산
void colorReduce5(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<image.cols * image.channels(); i++) {
// 각 화소에 대한 처리 ---------------------
*data++= *data&mask + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// .ptr와 * ++ 그리고 비트별 연산 (계속)
void colorReduce6(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols * image.channels(); // 각 행에 있는 원소의 총 개수
if (image.isContinuous()) {
// 더 이상 여분 화소가 없다면
nc= nc*nl;
nl= 1; // 지금 1D 배열임
}
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
*data++= *data&mask + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// .ptr와 * ++ 그리고 비트별 연산 (계속+채널)
void colorReduce7(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols ; // 열 개수
if (image.isContinuous()) {
// 더 이상 여분 화소가 없다면
nc= nc*nl;
nl= 1; // 지금 1D 배열임
}
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
*data++= *data&mask + div/2;
*data++= *data&mask + div/2;
*data++= *data&mask + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// Mat_ 반복자 사용
void colorReduce8(cv::Mat &image, int div=64) {
// 반복자 가져오기
cv::Mat_<cv::Vec3b>::iterator it= image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::iterator itend= image.end<cv::Vec3b>();
for ( ; it!= itend; ++it) {
// 각 화소에 대한 처리 ---------------------
(*it)[0]= (*it)[0]/div*div + div/2;
(*it)[1]= (*it)[1]/div*div + div/2;
(*it)[2]= (*it)[2]/div*div + div/2;
// 각 화소 처리 끝 ----------------
}
}
// Mat_ iterator와 비트별 연산 사용
void colorReduce9(cv::Mat &image, int div=64) {
// 2의 지수승으로 반드시 나눠야 함
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
// 반복자 가져오기
cv::Mat_<cv::Vec3b>::iterator it= image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::iterator itend= image.end<cv::Vec3b>();
// 모든 화소 조회
for ( ; it!= itend; ++it) {
// 각 화소에 대한 처리 ---------------------
(*it)[0]= (*it)[0]&mask + div/2;
(*it)[1]= (*it)[1]&mask + div/2;
(*it)[2]= (*it)[2]&mask + div/2;
// 각 화소 처리 끝 ----------------
}
}
// MatIterator_ 사용하기
void colorReduce10(cv::Mat &image, int div=64) {
// 반복자 가져오기
cv::Mat_<cv::Vec3b> cimage= image;
cv::Mat_<cv::Vec3b>::iterator it=cimage.begin();
cv::Mat_<cv::Vec3b>::iterator itend=cimage.end();
for ( ; it!= itend; it++) {
// 각 화소에 대한 처리 ---------------------
(*it)[0]= (*it)[0]/div*div + div/2;
(*it)[1]= (*it)[1]/div*div + div/2;
(*it)[2]= (*it)[2]/div*div + div/2;
// 각 화소 처리 끝 ----------------
}
}
void colorReduce11(cv::Mat &image, int div=64) {
int nl= image.rows; // 행 개수
int nc= image.cols; // 열 개수
for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
image.at<cv::Vec3b>(j,i)[0]= image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[1]= image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[2]= image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// 입력/결과 영상과 함께
void colorReduce12(const cv::Mat &image, // 입력 영상
cv::Mat &result, // 결과 영상
int div=64) {
int nl= image.rows; // 열 개수
int nc= image.cols ; // 행 개수
// 필요 시 결과 영상 할당
result.create(image.rows,image.cols,image.type());
// 여분 화소를 갖지 않는 영상 생성
nc= nc*nl;
nl= 1; // 지금 1D 배열임
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
for (int j=0; j<nl; j++) {
uchar* data= result.ptr<uchar>(j);
const uchar* idata= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 각 화소에 대한 처리 ---------------------
*data++= (*idata++)&mask + div/2;
*data++= (*idata++)&mask + div/2;
*data++= (*idata++)&mask + div/2;
// 각 화소 처리 끝 ----------------
} // 행 끝
}
}
// 오버로드한 연산자 사용
void colorReduce13(cv::Mat &image, int div=64) {
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
// 화소값을 반올림하기 위해 사용하는 마스크
uchar mask= 0xFF<<n; // 예, for div=16이면 mask= 0xF0
// 컬러 감축 수행
image=(image&cv::Scalar(mask,mask,mask))+cv::Scalar(div/2,div/2,div/2);
}
#define NTESTS 14
#define NITERATIONS 20
int main()
{
int64 t[NTESTS],tinit;
cv::Mat image1;
cv::Mat image2;
// 타이머를 0으로 설정
for (int i=0; i<NTESTS; i++)
t[i]= 0;
// 여러 번 실험 반복
int n=NITERATIONS;
for (int k=0; k<n; k++) {
std::cout << k << " of " << n << std::endl;
image1= cv::imread("../image.jpg");
if (!image1.data)
return 0;
// .ptr와 [] 사용하기
tinit= cv::getTickCount();
colorReduce0(image1);
t[0]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// .ptr와 * ++ 사용하기
tinit= cv::getTickCount();
colorReduce1(image1);
t[1]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// .ptr와 * ++ 그리고 나머지 사용하기
tinit= cv::getTickCount();
colorReduce2(image1);
t[2]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// .ptr와* ++ 그리고 비트별 연산
tinit= cv::getTickCount();
colorReduce3(image1);
t[3]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// 저수준 포인터 산술 연산 사용하기
tinit= cv::getTickCount();
colorReduce4(image1);
t[4]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// image.cols * image.channels()로 .ptr와 * ++ 그리고 비트별 연산
tinit= cv::getTickCount();
colorReduce5(image1);
t[5]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// .ptr와 * ++ 그리고 비트별 연산 사용하기(계속)
tinit= cv::getTickCount();
colorReduce6(image1);
t[6]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// .ptr와 * ++ 그리고 비트별 연산(계속+채널)
tinit= cv::getTickCount();
colorReduce7(image1);
t[7]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// Mat_ 반복자 사용하기
tinit= cv::getTickCount();
colorReduce8(image1);
t[8]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// Mat_ 반복자와 비트별 연산 사용하기
tinit= cv::getTickCount();
colorReduce9(image1);
t[9]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// Mat_ 반복자 사용하기
tinit= cv::getTickCount();
colorReduce10(image1);
t[10]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// 여기서 사용
tinit= cv::getTickCount();
colorReduce11(image1);
t[11]+= cv::getTickCount()-tinit;
image1= cv::imread("../image.jpg");
// 입력/결과 영상 사용하기
tinit= cv::getTickCount();
cv::Mat result;
colorReduce12(image1, result);
t[12]+= cv::getTickCount()-tinit;
image2= result;
image1= cv::imread("../image.jpg");
// 입력/결과 영상 사용하기
tinit= cv::getTickCount();
colorReduce13(image1);
t[13]+= cv::getTickCount()-tinit;
//------------------------------
}
cv::namedWindow("Result");
cv::imshow("Result",image2);
cv::namedWindow("Image Result");
cv::imshow("Image Result",image1);
// print average execution time
std::cout << std::endl << "-------------------------------------------" << std::endl << std::endl;
std::cout << "using .ptr and [] =" << 1000.*t[0]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using .ptr and * ++ =" << 1000.*t[1]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using .ptr and * ++ and modulo =" << 1000.*t[2]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using .ptr and * ++ and bitwise =" << 1000.*t[3]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using direct pointer arithmetic =" << 1000.*t[4]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using .ptr and * ++ and bitwise with image.cols * image.channels() =" << 1000.*t[5]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using .ptr and * ++ and bitwise (continuous) =" << 1000.*t[6]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using .ptr and * ++ and bitwise (continuous+channels) =" << 1000.*t[7]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using Mat_ iterator =" << 1000.*t[8]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using Mat_ iterator and bitwise =" << 1000.*t[9]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using MatIterator_ =" << 1000.*t[10]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using at =" << 1000.*t[11]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using input/output images =" << 1000.*t[12]/cv::getTickFrequency()/n << "ms" << std::endl;
std::cout << "using overloaded operators =" << 1000.*t[13]/cv::getTickFrequency()/n << "ms" << std::endl;
cv::waitKey();
return 0;
}
'영상처리 > OpenCV 및 영상처리 이론' 카테고리의 다른 글
OpenCV 기초 - 6. 모폴로지 연산 (2) - 엣지, 코너 검출 (0) | 2016.09.28 |
---|---|
OpenCV 기초 - 5. 모폴로지(morphology) 연산 (1) - 침식, 팽창 (0) | 2016.09.26 |
OpenCV 기초 - 4. 영상 이진화 + 관심 영역(ROI) 정의 (0) | 2016.09.23 |
OpenCV 기초 - 2. 영상처리 준비(2) (2) | 2016.09.22 |
OpenCV 기초 - 1. 영상처리 준비 (1) + Image Watch 설치 (3) | 2016.09.20 |