본문 바로가기
개발 공부/Language

[Bitmap] C# 비트맵 픽셀 처리

by whatisthisblog 2021. 8. 11.
반응형

1. Bitmap 픽셀 접근 메서드

 

Bitmap 객체는 픽셀을 편하게 읽고 쓰기 위해 다음과 같은 메서드를 제공합니다.

 

public System.Drawing.Color GetPixel (int x, int y);			// 지정된 픽셀의 색을 가져옴
public void SetPixel (int x, int y, System.Drawing.Color color);	// 지정된 픽셀의 색을 설정

 

그러나, 위의 방법은 매번 메서드를 호출해야하므로 고속 연산이 필요한 이미지처리에 적합하지 않습니다.

이를 해결하기 위해 unsafe 키워드와 Bitmap 클래스의 LockBits 메서드를 사용해 C/C++ 의 포인터 처럼 픽셀 배열에 직접 접근하여 처리할 수 있습니다.

 

1.1 unsafe/fixed 키워드

 

C#에서는 CLR의 메모리 관리 시 메모리에 비정상적인 참조 등의 문제를 방지하기 위해 포인터를 사용하지 못 하도록 합니다. C#에서 unsafe 키워드는 포인터와 관련된 모든 작업에 필요한 안전하지 않은 컨텍스트를 나타냅니다.

unsafe 키워드 블록을 사용하면, 해당 블록 내에서 포인터 접근과 같은 안전하지 않은 코드를 사용할 수 있습니다.

unsafe 블럭을 컴파일하기 위해서는 빌드 옵션에서 unsafe code 사용을 체크해줘야 합니다.

 

또한 C#에서 CLR의 GC 의해 포인터를 통해 참조 중이던 메모리가 이동될 수 있기 때문에, fixed 지시어를 통해 해당 메모리를 잠궈 GC 대상에서 제외시켜줘야 합니다.

 

class Point 
{ 
    public int x;
    public int y; 
}

unsafe private static void ModifyFixedStorage()
{
    // Variable pt is a managed variable, subject to garbage collection.
    Point pt = new Point();

    // Using fixed allows the address of pt members to be taken,
    // and "pins" pt so that it is not relocated.

    fixed (int* p = &pt.x)
    {
        *p = 1;
    }
}

 

Bitmap 클래스에서는 LockBits 메서드가 이와 비슷한 역할을 수행합니다.

 

1.2 Bitmap.LockBits 메서드

 

Bitmap 객체는 LockBits 메서드를 통해 설정된 메모리 영역을 잠그는 역할을 하며, Bitmap 이미지의 특성들이 정의되어 있는 BitmapData 클래스를 반환합니다. 

 

public System.Drawing.Imaging.BitmapData LockBits (System.Drawing.Rectangle rect, System.Drawing.Imaging.ImageLockMode flags, System.Drawing.Imaging.PixelFormat format);
public System.Drawing.Imaging.BitmapData LockBits (System.Drawing.Rectangle rect, System.Drawing.Imaging.ImageLockMode flags, System.Drawing.Imaging.PixelFormat format, System.Drawing.Imaging.BitmapData bitmapData);

 

1.3 Bitmap 픽셀 데이터 접근

 

위의 Unsafe 키워드와 Bitmap.LockBits 메서드를 통해 픽셀 데이터에 접근하여 32bppARGB Color Image를 8bpp Gray Image로 변환하는 작업을 해보겠습니다.

 

public Bitmap ConvertColorToGray(Bitmap colorBitmap)
{
    int W = colorBitmap.Width;
    int H = colorBitmap.Height;

    BitmapData cBd = colorBitmap.LockBits(new Rectangle(0, 0, W, H), 
                                          ImageLockMode.ReadWirte, colorBitmap.PixelFormat);
    int cStride = colorBitmap.Stride;
    int cOffset = cStride - W * Bitmap.GetPixelFormatSize(colorBitmap.PixelFormat) / 8;
    
    Bitmap grayBitmap = new Bitmap();
    BitmapData gBd = grayBitmap.LockBits(new Rectangle(0, 0, W, H), 
                                          ImageLockMode.ReadWirte, PixelFormat.Format8bppIndexed);
    int gStride = grayBitmap.Stride;
    int gOffset = gStride - W * Bitmap.GetPixelFormatSize(grayBitmap.PixelFormat) / 8;
    
    unsafe
    {
        byte* cPtr = colorBitmap.Scan0;	
    	byte* gPtr = grayBitmap.Scan0;
        
        for(int y = 0; y < H; y++)
        {
        	for(int x = 0; x < W; x++)
            {
            	byte b = cPtr[0];
                byte g = cPtr[1];
                byte r = cPtr[2];
                // byte a = cPtr[3];
                
                gPtr[0] = (byte)((r + g + b) / 3);	// Color Pixel을 Gray Pixel 값으로 변환
                
                cPtr += 4;	// 다음 픽셀 위치로 이동
                gPtr += 1;	// 다음 픽셀 위치로 이동
            }
            
            cPtr += cOffset;	// 다음 라인으로 이동
            gPtr += gOffset;	// 다음 라인으로 이동
        }
        
        colorBitmap.UnlockBits(cBd);
        grayBitmap.UnlockBits(gBd);   
    }
    
    return grayBitmap;
}

 

위의 코드에서 LockBits 메서드는 Bitmap 클래스의 데이터 메모리에서 원하는 영역 (new Rectangle(0, 0, W, H)) 만큼을 설정하여 메모리를 잠급니다.

 

이후 Scan0 속성을 통해 메모리 첫 위치의 주소를 반환하며, 해당 위치에서 부터 포인터를 이동시켜 각 픽셀 데이터를 가져옵니다.

입력 이미지가 32bpp ARGB 포맷이기 때문에 한 픽셀의 메모리는 4byte 크기이며 B, G, R, A 순서로 byte 값이 저장되어 있습니다.

따라서 포인터 위치에서부터 각각 cPtr[0] : B, cPtr[1] : G, cPtr[2] : R, cPtr[3] : A 가 저장되어 있습니다.

 

byte b = cPtr[0];
byte g = cPtr[1];
byte r = cPtr[2];
byte a = cPtr[3];

 

첫 width 크기만큼 진행하고 나면, 그 다음 줄로 포인터를 이동시켜야 하는데요, 이때 주의해야 할 점이 있습니다.

Bitmap은 효율적인 데이터 관리를 위해 영상의 가로 크기를 4의 배수 형태로 저장하기 때문에 픽셀 데이터가 존재하지 않는 Padding 영역이 존재합니다.

만약 이미지의 가로 길이가 3이라면, 실제 메모리의 가로 크기는 4로 저장되는 것 입니다.

 

따라서 포인터를 이미지의 다음 줄로 이동시키기 위해서는 해당 Offset 크기 만큼 더해주어야 합니다.

Offset의 크기를 구하는 방법은 이미지의 Stride에서 이미지 Width를 빼주면 됩니다. 

cPtr += cOffset;
gPtr += gOffset;

 

 

모든 픽셀 연산이 끝나고 나면, UnlockBIts 메서드를 통해 잠긴 메모리 영역을 다시 풀어주고 unsafe 블록을 빠져나오면 됩니다.

반응형

댓글