본문으로 바로가기

재미있고 응용의 폭이 넓은 Tk의 photo 이미지를 C언어로 처리하는 방법에 대해서 알아보도록 하겠습니다.

photo handle과 photo image block

C언어로 제어할 때에 사용할 두 가지의 데이터 타입을 소개합니다.
Tk_PhotoHandle 구조체는, Tk의 photo 이미지 데이터를 식별하는 것으로 그 내용을 보통 알아둘 필요는 없지만, 일반적으로 image create photo ImageName의 ImageName에 대해 Tk_PhotoHandle 이 할당된다고 생각하시면 됩니다.

다음 Tk_PhotoImageBlock은 구조체이며, 다음과 같이 정의 되어 있습니다.

typedef struct {
        unsigned char *pixelPtr;
        int width;
        int height;
        int pitch;
        int pixelSize;
        int offset[3]; // (*1)
} Tk_PhotoImageBlock;

pixelPtr은 이미지 데이타가데이터가 들어가는 영역입니다. 이미지 위에서 아래까지, 각 픽셀의 RGB 데이터가 연속으로 들어 있습니다. width는 이미지 데이터 폭, 즉 가로 방향의 픽셀수입니다. height는 이미지 데이터의 높이, 즉 세로 방향의 픽셀수입니다. pitch는 가로 방향의 픽셀 전부를 표현하는데 필요한 바이트 수, pixelSize는 하나의 픽셀을 표현하는데 필요한 바이트의 수입니다. 이 값은 보통 3이나 4입니다. 마지막으로 offset[]은 순서대로 R, G, B의 컬러 정보가 저장되어 있습니다. 보통은 offset[0]=0, offset[1]=1, offset[2]=2, 즉 이미지 데이터의 RGBRGBRGB... 와 같습니다.

Tcl/Tk 8.3부터는, RGB 이외에 투명도를 나타내는 레이어가 추가 되어 졌기 때문에 offset[4]로 되어 있습니다.

여기까지 정리한 것을 바탕으로 Tk_PhotoImageBlock 타입의 변수 b 내부의 x, y에 있는 픽셀의 RGB컬러의 값을 구하는 방법을 구현해 보겠습니다.

void putRGB(Tk_PhotoImageBlock* b, int x, int y){
        unsigned char* pixelp;
        unsigned char  red, green, blue;
 
        if(x < 0 || x >= b->width || y < 0 || y >= b->height){
                printf("범위를 초과 했습니다.\n");
                return;
        }
        pixelp = (b->pixelPtr) + y * (b->pitch) + x * (b->pixelSize);
        red   = * (pixelp+(b->offset[0]));
        green = * (pixelp+(b->offset[1]));
        blue  = * (pixelp+(b->offset[2]));
        printf("%d,%d의 RGB컬러는, %d,%d,%d입니다.\n",
                   x, y, red, green, blue);
}

Tk 라이브러리 함수

주로 사용하는 함수는 이미지를 읽는 함수, 이미지를 쓰는 함수라 생각해도 될것입니다.

Tk_PhotoHandle Tk_FindPhoto(Tcl_Interp* interp, char* imageName);

Tk_FindPhoto 함수는 image create photo ImageName로 ImageName에 지정되었던 이름 ImageName의 photo handle을 얻는 함수입니다. 이미 존재하는 이미지 데이터의 값을 처리할 때에 반드시 사용되어야 할 함수입니다.

void Tk_PhotoGetImage(Tk_PhotoHandle handle, Tk_PhotoImageBlock* blockPtr);

Tk_PhotoGetImage 함수는 존재하는 이미지 데이타를 얻기 위한 함수입니다. 특별히 초기화할 필요가 없는 Tk_PhotoImageBlock 타입의 변수를 두 번째 인자로 포인터를 넘겨주면 데이터가 들어가게 됩니다. 추가로 image create photo로 읽었던 데이터를 이 함수로 분석해 보면 대부분,

  • pixelSize는 3 (Tcl/Tk 8.3부터는 투명도를 나타내는 채널 레이어가 더해져 기본으로 4가 됩니다.)
  • offset[]의 순서로 0, 1, 2
  • pitch는 pixelSize에 width를 곱한 값

로 되어 있습니다. 가장 알기 쉬운 구조로 되어 있는 것이죠.

Tk_PhotoPutBlock(Tk_PhotoHandle handle,
	Tk_PhotoImageBlock* blockPtr, int x, int y, int width, int height, int compRule);

Tk_PhotoPutBlock 함수는, photo handle을 갖는 이미지 데이터의 x, y를 왼쪽 상단 위에 width, height의 영역에, blockPtr에 들어 있는 데이터로 덮어쓰는 함수입니다. compRule은 다음과 같습니다.

Specifies the compositing rule used when combining transparent 
pixels in a block of data with a photo image. Must be one of 
TK_PHOTO_COMPOSITE_OVERLAY (which puts the block of data over 
the top of the existing photo image, with the previous contents 
showing through in the transparent bits) or 
TK_PHOTO_COMPOSITE_SET (which discards the existing photo image 
contents in the rectangle covered by the data block.) 

 

여기까지 정리하면, 이미지를 처리하기 위한 순서는 다음과 같을 것입니다.

  • 읽은 이미지, 기록할 이미지의 photo handle를 각각 Tk_FindPhoto로 찾는다. (여기서는 hsrc, hdest)
  • 초기화가 필요없는 Tk_PhotoImageBlock 구조체를 하나 준비하여, Tk_PhotoGetImage(hsrc, &b)로 hsrc의 이미지 데이터를 얻어온다.
  • 구조체 b의 데이타를 처리한다.
  • Tk_PhotoPutBlock(hdest, &b, ...)로 hdest에 쓴다.

필터(Filter) 커맨드 제작

위의 설명대로 전형적인 안티 알리어싱 처리 필터인 Tcl 커맨드 cmfg를 소개합니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
 
#include "tcl.h"
#include "tk.h"
 
#define RERROR \
{Tcl_SetResult(interp, ereason, TCL_VOLATILE);\
return TCL_ERROR;}
 
/*-----------------------------------------------------------------------*
* 이미지 데이타의 왼쪽상단 위에서 좌표 x,y의 점의 컬러 정보를 rgb[0]~[2]에
* 저장합니다. 이미지 범위를 넘어섰다면 제로를, 범위내라면 1을 돌려줍니다.
* [0]에 적색, [1]에 녹색, [2]에 청색이 들어갑니다. *
*-----------------------------------------------------------------------*/
static int getp(Tk_PhotoImageBlock* b, int x, int y, int* rgb){
        unsigned char* pixelp;
 
        if(x < 0 || x >= b->width || y < 0 || y >= b->height){
                return 0;
        }
        pixelp = (b->pixelPtr) + y * (b->pitch) + x * (b->pixelSize);
        * (rgb+0) = * (pixelp+(b->offset[0]));
        * (rgb+1) = * (pixelp+(b->offset[1]));
        * (rgb+2) = * (pixelp+(b->offset[2]));
        return 1;
}
 
/*-----------------------------------------------------------------------*
* 역으로 셋팅하는 함수
*-----------------------------------------------------------------------*/
static int setp(Tk_PhotoImageBlock* b, int x, int y, int* rgb){
        unsigned char* pixelp;
 
        if(x < 0 || x >= b->width || y < 0 || y >= b->height){
                return 0;
        }
        pixelp = b->pixelPtr + y * (b->pitch) + x * (b->pixelSize);
        * (pixelp+(b->offset[0])) = (unsigned char)(* (rgb + 0));
        * (pixelp+(b->offset[1])) = (unsigned char)(* (rgb + 1));
        * (pixelp+(b->offset[2])) = (unsigned char)(* (rgb + 2));
        return 1;
}
 
/*-----------------------------------------------------------------------*
* 안티 알리아싱(Anti Aliasing) 필터를 처리
*-----------------------------------------------------------------------*/
static void filtering3x3(Tk_PhotoImageBlock* db, Tk_PhotoImageBlock* sb){
        static int m[3][3] = {
        {1, 1, 1,},
        {1, 1, 1,},
        {1, 1, 1,},
        };
        int x, y, sx, sy, i;
 
        /* db의 멤버를 초기화 */
        db->width  = sb->width;
        db->height = sb->height;
        db->pitch  = sb->pitch;
        db->pixelSize = sb->pixelSize;
        db->pixelPtr = (unsigned char* ) Tcl_Alloc (db->pitch * db->height);
        for(i=0; i<3; i++){
                db->offset[i] = sb->offset[i];
        }
        for(y=0; y<sb->height; y++){
                for(x=0; x<sb->width; x++){
                        int r, g, b;
                        int ctr;
                        int rgb[3];
                        r=0; g=0; b=0; ctr=0;
                        for(sy=0; sy<3; sy++){
                                for(sx=0; sx<3; sx++){
                                        if(getp(sb, x+sx-1, y+sy-1, rgb) == 1){
                                                r += m[sy][sx] * rgb[0];
                                                g += m[sy][sx] * rgb[1];
                                                b += m[sy][sx] * rgb[2];
                                                ctr++;
                                        }
                                }
                        }
                        rgb[0] = r / ctr;
                        rgb[1] = g / ctr;
                        rgb[2] = b / ctr;
                        setp(db, x, y, rgb);
                }
        }
}
 
int cmfgFiltering(ClientData clientData, Tcl_Interp* interp, int argc, char* argv[]){
        char ereason[256];
        char* srcImageName, * destImageName;
        Tk_PhotoHandle phsrc, phdest;
        Tk_PhotoImageBlock sb, db;
        /**********************************************
        * Tk_PhotoImageBlock은 다음과 같은 형태를 하고 있습니다.
        typedef struct {
        unsigned char *pixelPtr;
        int width;
        int height;
        int pitch;
        int pixelSize;
        int offset[3];
        } Tk_PhotoImageBlock;
        * Tk 8.3 이후부터는 offset[4] 입니다.
        ***********************************************/
 
        if(argc != 3){
                sprintf(ereason,
                "Ootto! illegal number of arguments.\nusage:cmfg destImage srcImage.");
                RERROR;
        }
        destImageName = argv[1];
        srcImageName  = argv[2];
        if((phsrc  = Tk_FindPhoto(interp, srcImageName)) == NULL){
                sprintf(ereason, "Ootto! photo image %s not found.", srcImageName);
                RERROR;
        }
        if((phdest = Tk_FindPhoto(interp, destImageName)) == NULL){
                sprintf(ereason, "Ootto! photo image %s not found.", destImageName);
                RERROR;
        }
        Tk_PhotoGetImage(phsrc, & sb);
 
        fprintf(stderr, "sb:\n");
        fprintf(stderr, "width=%d height=%d pitch=%d pixelSize=%d offset=[%d][%d][%d]",
        sb.width, sb.height, sb.pitch, sb.pixelSize,
        sb.offset[0], sb.offset[1], sb.offset[2]);
 
        filtering3x3(& db, & sb);
        Tk_PhotoPutBlock(phdest, & db, 0, 0, sb.width, sb.height, TK_PHOTO_COMPOSITE_OVERLAY);
        Tcl_Free(db.pixelPtr);
        return TCL_OK;
}
 
DLLEXPORT int Cmfg_Init(Tcl_Interp* interp){
        Tcl_CreateCommand(interp, "cmfg", cmfgFiltering, NULL, NULL);
        return TCL_OK;
}

다음의 방법으로 공유 라이브러리로 빌드합니다. 다음과 같은 방법으로 빌드합니다.

Unix 계열

INCTCLDIR       = /usr/local/include
CC              = gcc
CFLAGS          = -g -Wall -I$(INCTCLDIR)
SHFLAGS         = -fPIC
SHLD            = gcc -shared
.c.o:
        $(CC) $(CFLAGS) $(SHFLAGS) -c $*.c
libcmfg.so: cmfg.o
        $(SHLD) -o $@ $(CFLAGS) $?

Windows 

TCLROOT = C:\Progra~1\Tcl
TCLVER  = 84
CC      = cl
SHLD    = cl /LD
CFLAGS  = /O2 "-I$(TCLROOT)\include"
LIBS    = "$(TCLROOT)\lib\tcl$(TCLVER).lib" \
        "$(TCLROOT)\lib\tk$(TCLVER).lib"
OBJS    = cmfg.obj
.c.obj:
        $(CC) $(CFLAGS) /o $@ /c $*.c
libcmfg.dll: $(OBJS)
        $(SHLD) /o $@ $(OBJS) $(LIBS)


빌드한 cmfg 커맨드를 사용해 안티 알리어싱 테스트를 해봅니다.

# CMFG.TCL
 
load libcmfg[info sharedlibextension]
 
wm title . "Example Filter"
. configure -width 300 -height 280
canvas .can -bg white -width 300 -height 280
pack .can
image create photo ImageSrc -format gif -file image.gif
image create photo ImageDest
.can create image 0 0 -image ImageSrc -anc nw
.can create image 0 135 -image ImageDest -anc nw
cmfg ImageDest ImageSrc

#ImageDest write output.ppm -format ppm

아래의 그림은 안티알리어싱 처리를 하기 전과 후의 결과입니다.