글을 작성하면서 조금 아쉽게 느껴졌던 게 "픽셀에 접근하는 방법을 알겠지만 이를 어떻게 응용할 수 있는 것인가?" 에
대한 질문을 나에게 던지면 막상 떠오르는 것이 없었던 것이였습니다. 그래서 며칠을 생각한 결과, 입력받은 텍스트를
이미지 속 픽셀값에 변조시켜 이미지에 텍스트를 숨길 수 있는 방법에 대하여 소개하려 합니다.
먼저 암호화 시키는 기술에 대해 간단히 알아보도록 하겠습니다.
전달하려는 정보를 이미지나 MP3 파일 등에 암호화 하여 숨기는 기술을 스테가노그래피[Steganographt]이라고 합니다.
이 기술은 크게 삽입 기법과 수정 기법으로 나뉘어집니다.
삽입 기법은 이미지 파일 데이터를 변경하지 않고 추가 데이터를 파일 앞이나 뒤에 붙이는 방식입니다.
다시 말해, jpg, png 등의 파일에서 데이터의 시작을 알리는 SOI(Start Of Image) 이전에 또는 끝을 알리는
EOI(End Of Image) 뒤에 전달하려는 정보를 추가하는 것입니다.
이러한 방법은 정보를 숨기려는 이미지에 가시적인 영향을 주진 않지만 이미지의 크기가 커지게 됩니다.
수정 기법은 이미지 파일에서 RGB 값의 최하위 비트(LSB)를 수정하여 추가 데이터를 입력하는 기법입니다.
이는 육안으로 알아차리기 힘들기 때문에 많이 이용되는 방식입니다.
다시 말해, 픽셀 값의 표현 가능한 수의 범위인 8비트(0~255)에서 특정 값이 255일 경우 최하위비트를 수정해 254 변조시켜도 우리 눈에는 255과 254가 거의 동일하게 보인다는 것입니다.
아래 코드는 스테가노그래피의 수정 기법을 이용한 방법입니다.
* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.
< Image Encording >
#include <opencv2/opencv.hpp>
#include <iostream>
#include <time.h>
#include <math.h>
using namespace cv;
using namespace std;
int main(int argc, char *argv[]) {
// 이미지 입력
/*
if (argc < 2) {
printf("이미지 경로를 입력하세요.\n");
printf("파일 이름, 이미지 경로\n\n");
exit(0);
}
else if (argc > 2) {
printf("인자를 초과하여 작성하였습니다.\n");
printf("파일 이름, 이미지 경로\n\n");
exit(0);
}
*/
Mat img = imread("C:\\test\\bridge.bmp");
int max_text = img.cols*img.rows; // 텍스트 작성 최대 범위 변수
int t_length; // 입력 텍스트 길이
int t_length_clone = 0;
int inf_lo = 0; // 위치 정보 변수
int data_text = 0;
int data_start = 0;
int x = 0, y = 0; // 좌표
int cnt = 0;
char str[10000];
char e_bit_text[8] = { 0, };
char e_bit_lo[8] = { 0, };
char text_length[14] = { 0, };
if (max_text < 50 * 50) {
printf("50x50 크기 이상의 이미지를 입력하세요.\n");
exit(0);
}
// 최대 텍스트 입력 수
if (((max_text - 14) / 25) > 10000)
max_text = 10000;
else
max_text = (max_text - 14) / 25;
max_text = max_text - 1; // 널문자 제외
do {
printf("입력 가능 텍스트 수(띄어쓰기 포함) : %d \n\n", max_text);
printf("------------------------------------------------------------------------\n");
printf("TEXT : ");
fgets(str, sizeof(str), stdin);
t_length = strlen(str);
printf("\n\nText length : %d\n", t_length - 1);
printf("------------------------------------------------------------------------\n");
if (t_length == 1) {
printf("텍스트를 입력하지 않았습니다.\n\n");
printf("다시 입력해주세요.\n\n\n\n");
}
else if (t_length > max_text + 1) {
printf("입력 가능 텍스트 수를 초과하였습니다.\n\n");
printf("다시 입력해주세요.\n\n\n\n");
}
} while ((t_length == 1) || (t_length > max_text + 1));
///////////////////////////////////////
// 14bit 내, 위치 정보 저장 (범위 0~37,777) //
//////////////////////////////////////
t_length = t_length - 1; // 널문자 제외
t_length_clone = t_length; // 복사
// 입력 텍스트 길이 2진수 14비트로 저장
for (int t = 0; t < 14; t++) {
if (t_length_clone > 0) {
text_length[13 - t] = t_length_clone % 2;
t_length_clone = t_length_clone / 2;
}
else
text_length[13 - t] = 0;
}
// 입력 텍스트 길이 데이터 좌표에 입력
for (int t = 0; t < 14; t++) {
if (img.at<Vec3b>(y, x)[0] % 2 == 1) {
if (text_length[x] == 0)
img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] - 1;
}
else if (img.at<Vec3b>(y, x)[0] % 2 == 0) {
if (text_length[x] == 1)
img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] + 1;
}
x++;
if (x == img.cols) {
x = 0;
y++;
}
}
srand(time(NULL)); // 랜덤값
while (cnt < t_length)
{
// 첫 번째 정보(텍스트 정보가 Y축에 어느 X축 위치인지에 대한 정보
inf_lo = rand();
while (inf_lo > 10)
inf_lo = inf_lo / 10;
//inf_lo = inf_lo % 10;
inf_lo = 9;
//printf("난수 %d \n", inf_lo);
// 위치 정보와 텍스트 데이터 위치 사이 좌표 거리
data_start = inf_lo;
// 문자 1개 변수에 대입
data_text = str[cnt];
// 위치 정보, 문자 8비트로 표현
for (int t = 0; t < 8; t++)
{
// 위치
if (inf_lo > 0) {
e_bit_lo[7 - t] = inf_lo % 2;
inf_lo = inf_lo / 2;
}
else
e_bit_lo[7 - t] = 0;
// 문자
if (data_text > 0) {
e_bit_text[7 - t] = data_text % 2;
data_text = data_text / 2;
}
else
e_bit_text[7 - t] = 0;
}
// 위치 정보 입력 // (y,0)을 기준으로 8픽셀 맨 하위 비트에 입력
for (int q = 0; q < 8; q++) {
if (img.at<Vec3b>(y, x)[0] % 2 == 1) {
if (e_bit_lo[q] == 0)
img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] - 1;
}
else if (img.at<Vec3b>(y, x)[0] % 2 == 0) {
if (e_bit_lo[q] == 1)
img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] + 1;
}
x++;
if (x == img.cols) {
x = 0;
y++;
}
}
x += data_start;
if (x >= img.cols) {
x = x - img.cols;
y++;
}
// 데이터 정보 입력 // (y,8+위치정보)를 기준으로 8픽셀 맨 하위 비트에 입력
for (int q = 0; q < 8; q++) {
if (img.at<Vec3b>(y, x)[0] % 2 == 1) {
if (e_bit_text[q] == 0)
img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] - 1;
}
else if (img.at<Vec3b>(y, x)[0] % 2 == 0) {
if (e_bit_text[q] == 1)
img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] + 1;
}
x++;
if (x == img.cols) {
x = 0;
y++;
if (y == img.rows)
printf("텍스트 입력 가능 범위 초과\n");
}
}
cnt++;
}
imwrite("Encording_image.bmp", img);
printf("텍스트 인코딩 완료!!!\n\n");
}
< Image Decording>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
int main() {
// 데이터 입력
Mat img = imread("Encording_image.bmp");
if (img.empty() == 1) {
printf("이미지를 로드하지 못했습니다.\n\n");
exit(0);
}
int text_length[14] = { 0, };
int lo_inf[8] = { 0, };
int text_inf[8] = { 0, };
int length = 0;
int cnt = 0;
int x = 0, y = 0;
int text = 0;
int lo = 0;
int data_start = 0;
char str[10000] = { 0, };
// 텍스트 길이 정보 비트 추출
for (int t = 0; t < 14; t++) {
if (img.at<Vec3b>(0, t)[0] % 2 == 1)
text_length[t] = 1;
else if (img.at<Vec3b>(0, t)[0] % 2 == 0)
text_length[t] = 0;
}
// 텍스트 길이 값
for (int t = 0; t < 14; t++)
length += text_length[t] * pow(2, 13 - t);
// 텍스트 길이가 포함된 14비트 길이
x = 14;
while (cnt < length)
{
// 위치 정보 비트 추출
for (int t = 0; t < 8; t++) {
if (img.at<Vec3b>(y, x)[0] % 2 == 1)
lo_inf[t] = 1;
else if (img.at<Vec3b>(y, x)[0] % 2 == 0)
lo_inf[t] = 0;
x++;
if (x == img.cols) {
x = 0;
y++;
}
}
// 위치 정보 값
for (int t = 0; t < 8; t++)
lo += lo_inf[t] * pow(2, 7 - t);
x += lo;
if (x >= img.cols) {
x = x - img.cols;
y++;
}
// 텍스트 정보 비트 추출
for (int t = 0; t < 8; t++) {
if (img.at<Vec3b>(y, x)[0] % 2 == 1)
text_inf[t] = 1;
else if (img.at<Vec3b>(y, x)[0] % 2 == 0)
text_inf[t] = 0;
x++;
if (x == img.cols) {
x = 0;
y++;
if (y == img.rows)
printf("디코딩 오류..\n");
}
}
//텍스트 정보 값
for (int t = 0; t < 8; t++)
text += text_inf[t] * pow(2, 7 - t);
// 정보 저장
str[cnt] = text;
//초기화
lo = 0;
text = 0;
cnt++;
}
printf("받은 텍스트 : ");
for (int i = 0; i < length; i++)
printf("%c", str[i]);
printf("\n\n");
}
코드를 살펴보면, Image Encording을 수행할 때 가장 먼저 텍스트를 입력받고 텍스트의 길이와 각각의 글자들을
바이너리로 표현한다. 이후 아래 그림과 같이 바이너리 값을 원본 이미지의 픽셀에 입력시켜 변조합니다.
아래 그림은 이미지의 픽셀단위로 표현한 것이며, 위 결과의 입력 텍스트인 "I am Groot!!!"에서 일부인 글자 I와
띄어쓰기가 입력된 바이너리 값을 나타 것입니다. 아래 그림을 참고하면 이해 하는 데 도움이 될 것입니다.
파란색은 입력 받은 텍스트의 길이 정보(14비트)
노란색은 조금의 보안성을 높일 수 있도록 텍스트 데이터를 입력시킬 픽셀의 위치 정보(8비트)
* 위치 정보는 랜덤값을 호출하여 나타냄
초록색은 텍스트 데이터의 정보(8비트)
아래 구글 드라이브 올라간 파일은 위 코드의 exe파일입니다
위 코드를 작성하여 확인하는 것 보다 실행파일을 받아 하는 게 더 빠르게 확인이 가능할 거 같아 올렸습니다.
cmd창에서 실행 파일 경로로 들어가 [Image Encording.exe] [입력시킬 bmp 이미지] 순으로 작성하여 실행해
텍스트를 입력한다. 그러면 새로운 Encording_image.bmp 파일이 생성됩니다.
그리고 다시 cmd창에서 Image Decording.exe을 실행하면 같은 폴더에 있는 Encording_image.bmp를 입력받아
픽셀이란 디지털 영상에서 더이상 쪼갤 수 없는 최소 단위를 뜻하며 픽셀 또는 화소로 얘기합니다.
이미지의 가로/세로 사이즈를 줄인다는 말에서 사이즈가 바로 픽셀을 말하는 거죠.
아래 사진을 예로 보시게 되시면 640x480 크기의 이미지가 있으며. 픽셀의 수는 640x480 = 307,200 개가 되네요.
우리가 흔하게 보는 이미지는 대게 RGB인 3개의 채널의 혼합으로 이루어진 이미지 입니다.
여기서 RGB는 색상인 Red, Green, Blue를 뜻합니다.
이미지속 한 픽셀마다 RGB의 3가지 값이 혼합되어있습니다.
이 내용은 아래의 앵무새 그림을 보고 이해하시면 됩니다.
그렇다면 오늘의 주제인 이미지 픽셀에 접근하는 방법을 코드를 통해 알아보도록 하겠습니다.
* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main() {
Mat img = imread("D:\\학교\\티스토리\\color\\mountain.jpg");
Mat only_r = img.clone();
Mat only_g = img.clone();
Mat only_b = img.clone();
for (int y = 0; y < img.rows; y++) {
for (int x = 0; x < img.cols; x++) {
// B Channel => img.at<Vec3b>(y,x)[0]
// G Channel => img.at<Vec3b>(y,x)[1]
// R Channel => img.at<Vec3b>(y,x)[2]
// Only Red
only_r.at<Vec3b>(y, x)[0] = 0; // zero B
only_r.at<Vec3b>(y, x)[1] = 0; // zero G
// Only Green
only_g.at<Vec3b>(y, x)[0] = 0; // zero B
only_g.at<Vec3b>(y, x)[2] = 0; // zero R
// Only Blue
only_b.at<Vec3b>(y, x)[1] = 0; // zero G
only_b.at<Vec3b>(y, x)[2] = 0; // zero R
}
}
imshow("원본 이미지", img);
imshow("Only Red", only_r);
imshow("Only Green", only_g);
imshow("Only Blue", only_b);
waitKey(0);
}
위 코드는 한 이미지를 불러와 RGB로 혼합되어 이루어진 이미지를 각각의 채널만을 남기고 나머지 채널은
0 값을 준 것입니다. 이렇게 코드를 돌려보면 위에 앵무새 그림과 동일하게 나오게 됩니다.
추가적으로 코드 내 Vec3b는 3 Byte로 된 벡터값을 의미합니다.
코드의 결과는 아래와 같습니다.
< 정리글 >
오늘은 이미지 속 픽셀의 RGB 채널에 접근하는 방법을 알아보았습니다.
이 흐름을 이용하면 어떠한 영역에 접근하여 색상을 바꾸어 주거나 이미지 전체의 밝기를 조절할 수 있죠.
한가지 확실히 하고 넘어가야 되는건 위 코드의 결과는 RGB에서 1가지의 채널을 제외한 나머지 값을 0으로 만들어 나온
결과이며, 만약 각각의 채널을 따로 분리하여 1개 채널로 이미지를 보게 되면 회색 계열로 보이게 됩니다.
만약 아파트 입구에 설치된 주차 차단바의 상황라 가정하고 차량이 카메라 영역 속 특정 위치에 멈춰야 한다면
이미지 전체를 탐색하지 않고 두 번째 그림과 같이 이미지 속 특정 영역을 탐색해여 차량 번호판을 검출할 수 있으며,
처리 속도는 전체를 탐색하는 것 보다 월등히 차이가 나게 되죠.
바로 코드를 통해 Range() 함수를 사용해보겠습니다.
* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(){
Mat img = imread("D:\\티스토리\\range\\벚꽃.jpg"); // 1000x1000 이미지
Mat rect_img;
// 영역 좌표
int y1 = 300;
int y2 = 600;
int x1 = 250;
int x2 = 750;
rect_img = img(Range(y1, y2), Range(x1, x2));
imshow("원본 이미지", img);
imshow("결과 이미지", rect_img);
waitKey(0);
}
< 정리글 >
오늘은 Range() 함수에 대해서 알아보았습니다.
이 함수만을 이해하고 사용하시는 데 큰 어려움은 없을거라 생각합니다.
영상처리를 이용한 프로젝트를 하시게 되면 상황에 맞는 사용 이유와 응용 방법에 대해
잘 이해하실 수 있을거라 생각합니다
* 겨울이 다 지나고 벚꽃의 계절이 찾아온거 같네요.
코로나가 잠잠해지고 있는 것처럼 느껴지지만 이럴 시기일 수록 더욱 조심해야되지 않을까 싶습니다.