※ 주의사항 ※
본 블로그는 수업 내용을 바탕으로 제가 이해한 부분을 정리한 블로그입니다.
본 내용을 참고로만 보시고, 틀린 부분이 있다면 지적 부탁드립니다!
감사합니다😁
안녕하세요!!
오늘은 아래와 같은 내용을 확인해보겠습니다.
이미지 영상처리 알고리즘
화소 점 처리
기하학 처리
▣ 화소점 처리 및 기하학 처리 시연 동영상 (C++)
▣ 화소점 처리 및 기하학 처리 시연 동영상 (C#)
지난 시간에는 Gray Scale 이미지파일을 열고 Display 까지 프로그래밍을 하였다면 이번 시간부터는 이미지 가지고 다양한 가공 및 처리를 통해 다양한 이미지 편집 효과를 부여해 보도록 하겠습니다!!
영상처리 알고리즘은 기능별로 대동소이하기 때문에 알고리즘별로 다른 코드부분만 소개하도록 하겠습니다!!
1. 이미지 영상처리 알고리즘 - ① : (화소 점 처리)
※ 화소 점 처리 : 이미지의 한점 한점(픽셀)을 처리
→ 다른 화소의 영향을 받지 않고 단순히 화소 점의 값만 변경한다.
1-1. 반전
이미지 영상처리 알고리즘의 첫 함수부이기 때문에 해당 함수의 전체 코드를 가져왔습니다.
영상처리 알고리즘의 함수부에는 출력 영상의 크기를 결정하고, 출력 영상 메모리를 할당하며, 영상 처리 알고리즘의 본문이 들어가있고 마지막엔 영상을 출력하는 함수를 호출을 합니다.
영상 처리 알고리즘의 본문(2중 for문의 코드부)을 제외하곤 모든 영상처리 함수부에서 동일하게 사용되니 밑에서부턴 생략을 하도록 하겠습니다.( 간혹 출력 영상의 크기를 결정하는 부분은 바뀌는 경우가 있으니 바뀔경우에만 다시 코드를 소개하도록 하겠습니다.)
- 화소점의 값을 MAX 값(255)에서 빼주면 반전 처리가 된다.
void reverseImage() // 반전 영상
{
// 이미지를 열지 않았으면 영상처리 불가
if (inImage == null) return;
// 중요 !! 출력 영상의 크기를 결정 --> 알고리즘에 따라 다름
outH = inH; outW = inW;
// 출력 영상 메모리 할당
outImage = new byte[outH, outW];
// ** 영상처리 알고리즘 **
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
outImage[i, k] = (byte)(255 - inImage[i, k]);
}
}
///////////////////////////
displayImage();
}
1-2. 밝게 / 어둡게
- 밝게/어둡게의 기능을 선택하면 위의 사진과 같이 사용자가 값을 입력하라는 창이 뜬다.
- 저러한 창을 만들기 위해서는 새로운 Windows Form을 만들어야 한다.
(1) 사용자의 값을 입력받는 Windows Form 만들기
- 솔루션 탐색기 → 프로젝트 우클릭 → 추가 → 새항목 → 양식(Windows Forms) 추가
- Form Name 변경 및 사용자가 입력하는 값을 받을 수 있게 적절하게 GUI 배치
→ Form Name : input1Form (값을 1개 입력하는 폼)
→ Label, NumericUpDown, Button 배치
→ NumericUpDown 및 Button을 적당한 이름으로 변경해준다.
- NumericUpDown 속성에서 Modifiers, DecimalPlaces, Increment, Maximum, Minimum 변경 ★중요
→ Modifiers의 값을 Private 에서 Public으로 변경해주지 않는다면 다른 Forms에서 값을 불러올 수 없다.
- "확인"Button과 "취소"Button을 더블 클릭해서 아래와 같은 코드 작성
→ 확인을 누르면 OK값을 반환하고 취소를 누르면 Cancel을 반환한다는 내용
→ 반환 결과는 메인Form에서 사용할 예정
(2) MainForms의 코드에서 input1Form의 값을 받는 함수를 작성한다!!
- input1Form을 "sub"라는 이름으로 선언을 하고 input1Form에서 취소 버튼을 누르면 0의 값을 반환하고 확인 버튼을 누르면 numericalUpDown의 값을 반환하여 준다.
(3) 밝게 / 어둡게 알고리즘 코드
int value = (int)getValue();
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
int px = inImage[i, k] + (int)value;
if (px > 255) px = 255;
else if (px < 0) px = 0;
outImage[i, k] = (byte)px;
}
}
- getValue() 함수를 사용해서 input1Form으로부터 받은 값을 적용하였다.
- 이미지를 밝게 하기 위해선 화소점의 값을 더해주고, 이미지를 어둡게 하기 위해선 값을 빼주면 된다.
1-3. 선명하게
- 화소점의 값에서 임의의 값을 곱해주면 이미지가 더 선명해진다.
MessageBox.Show("1.1 ~ 3.0 사이의 값을 입력해주세요. \n(1.1 ≤ value ≤ 3.0)");
float value = getValue();
if (value < 1.1 || value > 3.0)
{
MessageBox.Show("허용하는 범위내의 값을 입력해주세요!!. \n[EX] 1.1 ≤ value ≤ 3.0 ");
}
else
{
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
float px = (inImage[i, k] * value);
if (px > 255) px = 255;
else if (px < 0) px = 0;
outImage[i, k] = (byte)px;
}
}
- input1Form을 그대로 사용하였기 때문에 Maximum값은 255, Minimum값은 -255에 해당된다.
- 하지만 이미지 처리과정에 따라 곱하거나 나누는 연산은 임의의 값이 커봤자 효과가 미비하기 때문에 if를 사용해서 사용자가 일정한 범위의 값을 벗어나면 메세지를 출력하고 작업을 수행하지 않도록 한다.
1-4. 흐릿하게
- 화소점의 값에서 임의의 값을 나누어주면 이미지가 흐려진다.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
float px = (inImage[i, k] / value);
if (px > 255) px = 255;
else if (px < 0) px = 0;
outImage[i, k] = (byte)px;
}
}
1-5. 흑백처리(127 기준)
- 이미지 화소 점의 평균 값이 아닌 '127'을 기준으로 낮으면 0 높으면 255를 주어서 흑백처리를 한다.
- 이미지 화소 점의 평균을 기준으로 흑백처리를 하는 과정은 추후 "히스토그램 화소점 처리"에서 다룰예정이다.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
if (inImage[i, k] > 127) outImage[i, k] = 255;
else outImage[i, k] = 0;
}
}
1-6. 감마보정
- 감마보정을 위해 제곱 함수를 사용해야하며 C#에서는 Math 클래스의 Pow() 메소드를 사용한다.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
temp = (float)Math.Pow(inImage[i, k], (1 / value));
if (temp > 255) outImage[i, k] = 255;
else if (temp < 0) outImage[i, k] = 0;
else outImage[i, k] = (byte)temp;
}
}
1-7. 범위강조
- 0 ~ 255의 화소 점 영역에서 특정한 범위를 강조하는 알고리즘으로 사용자로부터 범위 값(시작과 끝값)을 받아서 해당 범위의 화소 점 값을 모두 255로 바꾸어 준다.
- 사용자로부터 2개의 값을 받아야 하기 때문에 return값을 2개 반환하는 Windows Form을 만들었다.
- rv.Item1 = 초기값 / rv.Item2 = 끝 값
var rv = getValue_2();
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
if (inImage[i, k] > rv.Item1 && inImage[i, k] < rv.Item2) // getValue_2()의 리턴값 2개를 Item1과 Item2로 받는다.
{
outImage[i, k] = 255;
}
else
{
outImage[i, k] = inImage[i, k];
}
}
}
(1) return 값을 2개 반환하는 Windows Form 만들기!!!
- 작성방법 및 구성은 위의 input1Form과 대동사이하기 때문에 자세한 설명은 생략
(2) MainForms의 코드에서 input2Form의 값을 받는 함수를 작성한다!!
- input 값이 2개이기 때문에 Naming을 input2Form으로 하였다..
- Tuple 함수를 사용하면 return값을 2개 뿐만이 아니라 2개 이상(3개, 4개...)을 반환할 수 있다.
1-8. 포스터라이징
- 0 ~ 255, 총 256단계의 화소 값으로 이루어진 이미지의 단계를 더욱 축소해서 나타낸다.
- 본 코드에서는 2 ~ 10단계 사이로만 표현하였다.
MessageBox.Show("2 ~ 10 사이의 값을 입력해주세요. \n(2 ≤ value ≤ 10)");
int value = (int)getValue();
int a = 255 / value;
if (value < 2 || value > 10)
{
MessageBox.Show("허용하는 범위내의 값을 입력해주세요!! \n[EX] 2 ≤ value ≤ 10");
}
else
{
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
for (int j = 0; j * a < inImage[i, k]; j++)
{
if (j * a > 255) outImage[i, k] = 255;
else if (j * a < 0) outImage[i, k] = 0;
else outImage[i, k] = (byte)(j * a);
}
}
}
1-9. 파라볼라
- 밝은 곳 혹은 어두운 곳이 입체적으로 보이는 효과를 준다.
- 본 코드에서는 어두운 곳을 입체적으로 보이게 하였으며, 밝은 곳에 대한 코드는 주석으로 처리하였다.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
// CAP 파라볼라 - 밝은 곳 입체형
//outImage[i, k] = (byte)(255.0-255.0 * Math.Pow((inImage[i, k] / 127.0 - 1.0), 2));
// CUP 파라볼라 - 어두운곳 입체형
outImage[i, k] = (byte)(255.0 * Math.Pow((inImage[i, k] / 127.0 - 1.0), 2));
}
}
2. 이미지 영상처리 알고리즘 - ② : (기하학 처리)
※ 기하학 점 처리 : 이미지의 한 점(픽셀)의 위치(배열)를 변환
→ 이미지를 구성하는 화소의 공간적 위치를 재배치하는 과정
2-1. 축소(1/2)
기하학 처리는 이미지의 한 점의 위치나 배열을 변환하기 때문에 outImage의 크기가 변경된다.
따라서 화소 점 처리와는 다르게 출력 영상의 크기를 결정하는 코드도 수정이 된다.
- 축소의 경우는 이상하게 1/2가 아닌 값을 축소하게 되면 오류가 발생해서 축소의 크기를 1/2로 정해두었습니다. 나중에 디버깅을 해서 오류를 수정하게 된다면 고쳐보겠습니다..😥
- 출력 이미지의 크기를 입력 이미지의 1/2배로 하고, 그 크기만큼 출력 이미지의 화소점 값을 받는다.
if (inImage == null) return;
int scale = 2; //2가 아닌값을 받으면 오류 발생
outH = inH / scale; outW = inW / scale;
outImage = new byte[outH, outW];
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
outImage[(i / scale), (k / scale)] = inImage[i, k];
}
}
displayImage();
2-2. 확대
- 출력 이미지의 크기를 입력 이미지의 크기에서 임의의 값(사용자 입력)만큼 곱한 값으로 한다.
- 크기가 커진 만큼 근처의 픽셀에는 값이 채워지지 않아 이미지 품질이 떨어진다.
- 사용자의 입력 값이 4배보다 더 크면 이미지를 표시하는데 시간도 오래걸리고 품질이 좋지 않아서 최대 4배로 제한시켜 두었다.
MessageBox.Show("확대하고자 하는 배율을 입력해주세요. \n [x2, x3, x4]");
int scale = (int)getValue();
outH = inH * scale; outW = inW * scale;
outImage = new byte[outH, outW];
if (scale < 2 || scale > 4)
{
MessageBox.Show("허용하는 범위내의 값을 입력해주세요!! \n[EX] 2 ≤ value ≤ 4");
}
else
{
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
outImage[(i * scale), (k * scale)] = inImage[i, k];
}
}
(1) 이미지 축소 및 확대 관련 개념
- 확대의 방법에는 크게 포워딩과 백워딩의 방법으로 나뉜다.
"2-2. 확대" 알고리즘의 경우는 위의 개념과 같이 포워딩 개념에 속하고,
"2-3. 확대(백워딩)" 알고리즘의 경우는 말그대로 백워딩 개념에 해당된다.
포워딩은 단순히 이미지만 확대하고 빈 값(홀)이 존재해서 품질이 떨어지지만 백워딩은 이미지도 확대하고 빈 값(홀)은 인근 픽셀에서 값을 채워 넣기 때문에 이미지 품질이 개선된다.
2-3. 확대(백워딩)
- 출력 이미지의 크기를 입력 이미지의 크기에서 임의의 값(사용자 입력)만큼 곱한 값으로 한다.
- 크기가 커져서 생긴 빈 공간은 근처의 화소점 값으로 채워 넣는다.
- 사용자의 입력 값이 4배보다 더 크면 이미지를 표시하는데 시간도 오래걸리고 품질이 좋지 않아서 최대 4배로 제한시켜 두었다.
MessageBox.Show("확대하고자 하는 배율을 입력해주세요. \n [x2, x3, x4]");
int scale = (int)getValue();
outH = inH * scale; outW = inW * scale;
outImage = new byte[outH, outW];
if (scale < 2 || scale > 4)
{
MessageBox.Show("허용하는 범위내의 값을 입력해주세요!! \n[EX] 2 ≤ value ≤ 4");
}
else
{
for (int i = 0; i < outH; i++)
{
for (int k = 0; k < outW; k++)
{
outImage[i, k] = inImage[(i / scale), (k / scale)];
}
}
- 2중 for문에서 Maximum 값이 기존과는 다르게 outH, outW에 해당된다.
2-4. 이미지 회전 처리 개념 및 회전(포워딩)
(1) 이미지 회전 처리 방식 (3가지)
- 빨간색 outImage의 영역안에 들어온 inImage의 범위를 보면서 이미지 회전 처리 방식의 3가지를 구분지어보겠습니다.
- 회전(포워딩, 정방향) : 이미지를 시계방향으로 회전을 하는데 회전하는 기준점이 이미지의 정중앙이 아닌 이미지의 왼쪽, 위 꼭지점(0,0)을 기준으로 회전이 되어서 outImage[outH, outW] 상에는 이미지가 다소 짤려서 보이게 됨
- 회전(백워딩, 역방향, 중앙) : 이미지를 반시계 방향으로 회전을 하는데 회전하는 기준점이 이미지의 정중앙을 기준으로 회전되어서, 회전 각도는 알맞게 회전되나 outImage[outH, outW]의 크기가 회전된 이미지의 크기가 반영되지 않아서 이미지의 귀퉁이 부분이 짤려 보이게 됨.
- 회전(백워딩, 역방향, 중앙, 확대) : 이미지를 반시계 방향으로 회전하며, 회전하는 기준점이 이미지의 정중앙이 되며, 출력 이미지 크기(outImage[outH+@, outW+@]가 회전된 이미지의 크기에 반영되어서 회전된 이미지가 정상적으로 보임
(2) 회전(포워딩)
- 사용자가 입력하는 각도를 호도법(radian)으로 변환해서 이미지 회전 변환 공식에 적용.
- 삼각함수, x축 좌표를 나타내기 위해서는 호도법으로 바꾸어 주어야 한다!!
int angle = (int)getValue();
double radian = (angle * Math.PI) / 180.0; //각도 값을 받아서 radian으로 변환해야함.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
int xd = (int)(Math.Cos(radian) * i - Math.Sin(radian) * k);
int yd = (int)(Math.Sin(radian) * i + Math.Cos(radian) * k);
// xd, yd가 outImage 범위 안에 있는 값만 골라내고 벗어나면 버리기
if ((0 <= xd && xd < outH) && (0 <= yd && yd < outW))
outImage[i, k] = inImage[xd, yd];
}
}
- if문을 사용해서 outImage의 크기를 벗어나는 값은 버리고 범위 내의 값만을 표시한다.
2-5. 회전(백워딩, 중앙)
- 이미지 중앙을 기준으로 회전하고, 표시하기 위해 outImage의 행과 열의 크기를 반으로 나눈값을 반영해준다.
if (inImage == null) return;
outH = inH; outW = inW;
outImage = new byte[outH, outW];
MessageBox.Show("회전 시키고자 하는 각도를 입력해주세요!!!");
int angle = (int)getValue();
double radian = (angle * Math.PI) / 180.0; //각도 값을 받아서 radian으로 변환해야함.
int cx = outH / 2;
int cy = outW / 2;
// 임의의 방향으로 특정한 각도(radian)만큼 회전 시키는 공식 사용
// 이미지의 중심이 cx, cy 이고, 이 중심점을 기준으로 회전하는 공식 사용
for (int i = 0; i < outH; i++)
{
for (int k = 0; k < outW; k++)
{
int newi = (int)(Math.Cos(radian) * (i - cx) + Math.Sin(radian) * (k - cy)) + cx;
int newk = (int)(-Math.Sin(radian) * (i - cx) + Math.Cos(radian) * (k - cy)) + cy;
if (((0 <= newi) && (newi < outH)) && ((0 <= newk) && (newk < outW)))
outImage[i, k] = inImage[newi, newk];
}
}
2-6. 회전(백워딩, 중앙, 확대)
- 회전된 이미지의 크기를 고려한 출력 영상의 크기 산출
int angle = (int)getValue();
double radian = (angle * Math.PI) / 180.0; //각도 값을 받아서 radian으로 변환해야함.
double radian_90 = ((90 - angle) * (Math.PI) / 180.0);
// 출력 영상의 크기 구하기
outH = (int)(inH * (Math.Cos(radian)) + inW * (Math.Cos(radian_90)));
outW = (int)(inH * (Math.Cos(radian_90)) + inW * (Math.Cos(radian)));
outImage = new byte[outH, outW];
// 임시 입력 영상 --> 출력과 크기가 같게 하고, 입력 영상을 중앙에 두기
byte[,] tmpInPut = new byte[outH, outW];
int dx = (outH - inH) / 2;
int dy = (outW - inW) / 2;
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
tmpInPut[(i + dx), (k + dy)] = inImage[i, k];
}
}
// 이미지의 중앙값 구하기
int cx = outH / 2;
int cy = outW / 2;
for (int i = 0; i < outH; i++)
{
for (int k = 0; k < outW; k++)
{
int xs = (int)(Math.Cos(radian) * (i - cx) + Math.Sin(radian) * (k - cy));
int ys = (int)(-Math.Sin(radian) * (i - cx) + Math.Cos(radian) * (k - cy));
// 회전한 이미지를 중앙으로 배치하기
xs += cx;
ys += cy;
// xd, yd가 outImage 범위 안에 있는 값만 골라내고 벗어나면 버리기
if ((0 <= xs && xs < outH) && (0 <= ys && ys < outW))
outImage[i, k] = tmpInPut[xs, ys];
}
}
2-7. 상하반전
- 출력 행 Max 값(outH)에서 열(i)의 값을 빼주면 상하 반전이 된다.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
outImage[i, k] = inImage[(outH - i - 1), k];
}
}
2-8. 좌우반전
- 출력 열 Max 값(outW)에서 행(k)의 값을 빼주면 좌우 반전이 된다.
for (int i = 0; i < inH; i++)
{
for (int k = 0; k < inW; k++)
{
outImage[i, k] = inImage[i, (outW - k - 1)];
}
}
댓글