이미지를 활용한 메뉴를 구현하면서 선택한 메뉴를 제외한 나머지 이미지 메뉴들을 비활성화되는 것처럼 보이게 하기 위해 흑백 처리를 해야 하는 기능이 있었다.
위 동영상에서 보는 것과 같이 사용자가 선택한 이미지를 제외한 나머지 이미지들은 모두 흑백 처리를 해야 하는 문제였다. 구글링을 해보니 이미지를 흑백처리 하는 것과 관련해서 다양한 방법들이 많이 존재했는데, 이 방법을 C#에서 적용해 보는 것을 정리해보려고 한다.
이미지를 흑백 처리로 변환하는 방법은 여러 가지가 존재하겠지만, 이번 포스팅에서는 크게 2가지 위주로 정리하고 개인적으로 더 서칭 해서 찾은 더 빠르게 흑백처리를 변환하는 방법을 정리를 해보겠다.
흑백처리 변환의 2가지 방법은 다음과 같다.
- RGB 평균값으로 Gray 만들기
- YUV 값을 이용해 Gray 만들기
위 두 가지 방법을 각각 정리를 할 건데, 그에 앞서 일단 전체적인 코드 틀을 먼저 살펴보고 각각의 방법에 대해서 적용해 보는 코드를 함께 보면 될 것 같다.
이미지 변환 전체 코드 틀
public static Bitmap ConvertColorImage(Bitmap org)
{
Bitmap convertBitmap = new Bitmap(org.Width, org.Height);
for (int i = 0; i < org.Width; i++)
{
for (int j = 0; j < org.Height; j++)
{
Color orgColor = org.GetPixel(i, j);
int grayScale = 25;
Color newColor = Color.FromArgb(grayScale, grayScale, grayScale);
convertBitmap.SetPixel(i, j, newColor);
}
}
return convertBitmap;
}
이미지 변환을 위한 코드를 위와 같이 메서드로 만들어 보았다. 원본 이미지 정보를 Bitmap 객체로 전달받아, 똑같은 크기의 빈 Bitmap 객체를 생성하여 주고, 원본 이미지에 있는 색상 정보를 Pixel 단위로 읽어와서 흑백 처리를 위한 Gray Scale 값을 계산해서 새롭게 생성한 Bitmap 객체에 SetPixel 메서드를 사용해서 Pixel 단위로 하나하나 색값을 입력해 주는 원리다.
이렇다 보니 이미지 원본이 정보가 매우 큰 경우에는 코드 실행이 좀 오래 걸릴 수 있다는 단점이 존재한다. (모든 Pixel 정보를 가지고 작업을 수행하기 때문)
하지만, 우선 일단 이 방법이 코드를 이해하는 데는 매우 간단하기 때문에 이 코드 틀 위주로 정리를 진행할 예정이다. 위 코드에서 바뀌는 것은 int grayScale = 25;라고 되어있는 이 부분만 변경이 될 것이다. 어떤 방식으로 GrayScale 값을 계산하느냐에 따라 흑백처리가 변경되는 것이기 때문이다.
RGB 평균값으로 Gray 만들기
private Image ConvertGrayScaleByAvg(Image img)
{
Bitmap bmp = new Bitmap(img.Width, img.Height);
Bitmap orgBmp = new Bitmap(img, img.Width, img.Height);
Graphics g = Graphics.FromImage(bmp);
for (int y = 0; y < img.Height; y++)
{
for (int x = 0; x < img.Width; x++)
{
Color orgColor = orgBmp.GetPixel(x, y);
int avgVal = (int)((orgColor.R + orgColor.G + orgColor.B) / 3);
bmp.SetPixel(x, y, Color.FromArgb(avgVal, avgVal, avgVal));
}
}
return bmp;
}
int avgVal = (int)((orgColor.R + orgColor.G + orgColor.B) / 3);
GetPixel로 해당 Pixel에 있는 색상 정보를 Color 객체로 받아올 수 있다. 이를 사용해서 R, G, B 값을 각각 추출한 후 더하고 3으로 나눠서 평균값을 사용해서 흑백 이미지로 전환하는 방법이다. 적용해 본 결과는 다음과 같다.
어느 정도 흑백처리가 되어서 큰 고민 없이 이렇게 처리만 해도 될 것 같다는 생각이 든다.
YUV 값을 이용해 Gray 만들기
사실 이번 내용을 정리하면서 새롭게 알게 된 내용이었다. 색을 표현하는 것에 있어 YUV 체계로도 표현할 수 있고, 이를 구하는 방법도 RGB를 활용해서 할 수 있다는 것을 말이다. 우선 YUV가 무엇인지 정의를 살펴보자. 자료는 위키백과 자료를 참고했다. (https://ko.wikipedia.org/wiki/YUV)
YUV는 과거 흑백 TV를 보던 시기에 컬러 TV가 출시가 되었고, 두 TV에 모두 영상을 송출하기 위한 방법으로 탄생하게 된 것이 YUV 신호다. YUV 신호는 휘도(Y), 청색 색차(U), 적색 색차(V) 정보로 구성되어 있다. 그래서 컬러 TV들은 YUV 신호를 모두 받아서 사용해 색을 표시하고, 흑백 TV는 Y 신호만을 사용해서 흑백으로 영상을 보여주게끔 처리가 된 것이다.
현재는 흑백 TV가 쓰이고 있지 않지만 YUV 신호는 계속 사용되고 있다고 한다. 이유는 영상이나 사진 등의 압축에 용이하기 때문이다. 여하튼 이 정도로 마무리하고, 이 YUV를 사용해서 흑백으로 변환하는 코드를 살펴보자.
private Image ConvertGrayScaleByLumaCoding(Image img)
{
Bitmap bmp = new Bitmap(img.Width, img.Height);
Bitmap orgBmp = new Bitmap(img, img.Width, img.Height);
Graphics g = Graphics.FromImage(bmp);
for (int y = 0; y < img.Height; y++)
{
for (int x = 0; x < img.Width; x++)
{
Color orgColor = orgBmp.GetPixel(x, y);
int YValue = (int)((orgColor.R * 0.299 + orgColor.G * 0.587 + orgColor.B * 0.114));
bmp.SetPixel(x, y, Color.FromArgb(YValue, YValue, YValue));
}
}
return bmp;
}
int YValue = (int)((orgColor.R * 0.299 + orgColor.G * 0.587 + orgColor.B * 0.114));
구글에 RGB to YUV로 검색을 하게 되면 공식이 우수수 쏟아진다. 그중에 결과론 적으로 찾은 공식은 위와 같다. R 값에 0.299 값을 곱하고, G 값에 0.587 값을 곱하고, B 값에 0.114을 곱해서 다 더한 값을 사용하면 YUV 중에서도 Y 휘도값을 구할 수 있다.
원래 공식은 Y = (0.257 * R) + (0.504 * G) + (0.098 * B) + 16 공식인데, 이 공식은 휘도 값의 범위가 16 ~ 235까지 밖에 표현할 수가 없어서 대체된 방법으로 Y = (0.299 * R) + (0.587 * G) + (0.114 * B) 공식이 나오게 됐다고 한다.
그리하여 이 방법을 적용한 흑백 이미지는 어떨까? RGB 평균치로 흑백 처리한 이미지와 함께 살펴보자.
이미지 처리 속도 향상 하기
위에서 했던 방법들을 사용해서 진행을 하게 되면 Pixel 단위로 작업을 다 수행하기 때문에, 매우 엄청난 시간이 걸린다. 그래서 AI를 사용해서 이미지 처리 속도를 높일 수 있는 방법을 물어봤는데 다음과 같이 좋은 솔루션을 내주었다. LockBits를 사용해서 메모리를 직접 접근하여 처리를 하는 방식이다. (이 방법으로 변경해서 실행을 해보니 진짜 GPT느님의 말대로 수십 배는 속도가 더 향상이 되었음을 체감할 수 있었다.)
private Image ConvertGrayScaleByLumaCoding(Image img)
{
Bitmap bmp = new Bitmap(img.Width, img.Height);
Bitmap orgBmp = new Bitmap(img, img.Width, img.Height);
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
BitmapData orgData = orgBmp.LockBits(new Rectangle(0, 0, orgBmp.Width, orgBmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
int stride = bmpData.Stride;
IntPtr scan0 = bmpData.Scan0;
IntPtr orgScan0 = orgData.Scan0;
unsafe
{
byte* p = (byte*)(void*)scan0;
byte* pOrg = (byte*)(void*)orgScan0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
int idx = y * stride + x * 3;
byte b = pOrg[idx];
byte g = pOrg[idx + 1];
byte r = pOrg[idx + 2];
byte gray = (byte)(r * 0.299 + g * 0.587 + b * 0.114);
p[idx] = gray;
p[idx + 1] = gray;
p[idx + 2] = gray;
}
}
}
bmp.UnlockBits(bmpData);
orgBmp.UnlockBits(orgData);
return bmp;
}
'C# > Winform (.Net Framework)' 카테고리의 다른 글
C# Bitmap 이미지 자르기 (Graphics.DrawImage 활용) (2) | 2025.06.11 |
---|---|
C# Winform - Pagination 구현하기 (여러 페이지 보여주기) (1) | 2025.04.21 |
C# Winform - Delegate 활용 열려있는 모든 Form, UserControl 디자인 변경하기 (Pub-Sub Design Pattern 활용) (0) | 2025.04.09 |
C# Winform - Delegate 정리해보기 (Event, Callback) (2) | 2025.04.09 |
C# - Winform PictureBox 2개 사용해서 가로 슬라이드 애니메이션 적용 (0) | 2025.04.07 |