Python을 이용한 비트맵 그래픽

1. 문제 개요

- Python과 Tkinter를 이용한 프로그래밍으로, 비트맵 그래픽의 효과를 구현한다.

- http://www.python.org 에서 Python 프로그램을 다운로드 할 수 있다.

- http://www.activestate.com/activetcl/ 의 다운로드 프로그램 중 ActivePython을

  다운로드 한다.

- Python 은 3.0 버전이 있으나 일부 기능의 변화와(print "Hello" 의 문법은 오류가

  발생하고 print("Hello") 로 구현해주어야 한다.) ActivePython과의 원활한 동작을 위

  해 2.6.2 버전으로 통일하였다.


2. 문제 분석(알고리즘 및 해결방안)

  채점하지 않는 과제 1은 캔버스의 크기를 지정하여 생성한 뒤 반복문을 통해

그린 캔버스 내부를 검은 사각형과 하얀 사각형들로 번갈아 표현하는 것이 관건이다.

물론, 이는 전체 크기인 800 x 600 size 와 10 x 10 인 사각형들의 구성 관계를 연상해보면 일련의 계산을 통해 이중 반복문으로 해결할 수 있다.

  2 x 2 사각형들의 모임으로 베지어 곡선을 표현하는데, 알고리즘은 다음과 같다.

     1) 사다리꼴 형태로 보았을 때 좌측부터 p1,2,3,4 로 정의한다.

     2) p1과 p2의 중점, p2와 p3의 중점, p3와 p4의 중점을 구한다. 이를 p12, p23,

     p34 라 정의하자.

     3) p12와 p23의 중점, p23과 p34의 중점을 각각 p1223, p2334로 정의하자.

     4) p1223과 p2334의 중점을 구한다. 이 때 중점을 중심으로 내부에도 또 다른

     사다리꼴 형상의 구조가 만들어지는데 이에 대해서도 각각 재귀적으로 중점을 구할

     수 있다.

     5) 최종적으로 구한 점들을 모두 찍어보면 그 점들을 이었을 때 베지어 곡선이라

     할 수 있으며, 이 장에선 직선관련 함수의 내용보다 사각형을 직관적으로 볼 수 있

     게 2 x 2 size 로 정의된 것으로 보아 선이 아닌 점들로 표현하도록 하자.

  과제 2는 256 x 256 크기의 캔버스에 2 x 2 size 의 사각형들로 RGB 그러데이션을 보이는 것이다. 채점하지 않는 과제 1 과 마찬가지로 계산을 통하여 이중 반복문 구조로 해결할 수 있으나, 배열을 사용하는 조건이 있다. 하지만 C 언어와는 달리 python은 기존의 생성한 배열에 추가적으로 원소들을 더해줄 수 있음을 이용하여, 배열을 추가하여 사용하도록 한다.

  과제 3은 비트맵 이미지의 블러링 필터를 적용하는 문제로, 각 픽셀별로 접근하여 해당 값을 계산, 적용을 반복하여야 한다. 문제 조건에서 가장자리의 경우는 무시하여도 된다는 조건이 있으므로 가장 외곽에 위치한 픽셀들은 이 대상에서 제외하여도 무방하며(단, 외곽 바로 안쪽에 위치한 픽셀들은 이를 참조하여야 한단 것은 누락해선 안 된다.)내부 픽셀들에 차례로 접근하여 계산하는 과정을 거치도록 한다.

  과제 4는 45도의 회전을 적용하는 것으로, PIL 라이브러리에서의 rotate 를 직접 구현하는 문제다. 수업시간 때의 python 과 과제를 위한 예제 pdf 파일의 내용만으로는 삼각함수를 표현하는데 무리가 있다고 판단, math 함수를 사용하였으며, 이를 이용한 구현은 bitmap 수업 시간 때의 이론을 적용하여 표현하도록 한다.

3. 결과(소스 및 스크린샷)

<<채점하지 않는 과제 1>> ($ 표시 부분은 한 줄이라는 의미로 정의하자.)

from Tkinter import *       #Tkinter 라이브러리를 import

root = Tk()                  #root 로 지정

c = Canvas(root, height=600, width=800)     #c에 높이 600, 너비 800 Canvas 지정

c.pack()                                     #지정한 Canvas를 화면에 출력

for i in range(1,61):     #이중 반복문. 바깥쪽을 세로로 할 예정이므로 1~60까지

     for j in range(1,81): #안쪽은 너비에 맞춰 1~80까지

         if(i/2 != (i+1)/2):       #홀수줄이고

             if(j/2 != (j+1)/2):   #홀수칸이라면

                 c.create_rectangle(j*10-9,i*10-9,j*10,i*10, width=0, fill=$

"#%02x%02x%02x"%(255,255,255))  #하얀색의 네모를 그림

             else:               #홀수줄이지만 짝수칸이라면

                 c.create_rectangle(j*10-9,i*10-9,j*10,i*10, width=0, fill=$

"#%02x%02x%02x"%(0,0,0))     #검은색의 네모를 그림

         else:                   #짝수줄이고,

             if(j/2 != (j+1)/2):   #홀수칸이라면

                 c.create_rectangle(j*10-9,i*10-9,j*10,i*10, width=0, fill=$

"#%02x%02x%02x"%(0,0,0))     #검은색의 네모를 그림

             else:               #짝수줄, 동시에 짝수칸이라면

                 c.create_rectangle(j*10-9,i*10-9,j*10,i*10, width=0, fill=$

"#%02x%02x%02x"%(255,255,255))  #하얀색의 네모 그림

 - 구조는 위와 같다. 2중 반복문 내부에 2중의 조건문이 삽입된 형태로, 반복문은

총 800 x 600 의 캔버스에 채우기 위해, 조건문은 각 라인별로 검정색(모두 0)과

하얀색의(모두 255) 출력이 서로 어긋나게 하기 위함이다. 조건문은 홀수인지 짝수인지를 묻는 구문으로 mod 연산 없이 수행했다. (덧붙여서, 10inch 세로 길이 600 으로 제한된 넷북 사양에서 편집한 화면으로 아랫부분이 일정량 편집되었다.)


<<채점하는 과제 1>> ($ 표시 부분은 한 줄이라는 의미로 정의하자.)

from Tkinter import *

root = Tk()

c = Canvas(root, height=600, width=800)

c.pack()


def midp((x1, y1), (x2, y2)):     #두 개의 좌표로부터 중점을 구하는 정의 함수

 return ((x1+x2)/2, (y1+y2)/2)     #중점 좌표를 되돌린다


def beziercurves(p1, p2, p3, p4):   #베지어 곡선을 그리는 함수

 if ((p4[0]-p1[0]) > 3) :             #끝점과 시작점 사이의 x 축 거리가 4 이상일 때

     leftp1 = p1                     #왼쪽 첫 번째 점에 첫 점을 저장

     rightp4 = p4                   #오른쪽 끝 점에 마지막 점을 저장

     leftp2 = midp(p1, p2)  #왼쪽 두 번째 점은 첫 점과 두 번째 점 사이의 중점을

     rightp3 = midp(p3, p4) #오른쪽 세 번째 점은 세 번째 점과 네 번째 사이의 중점을

     temp = midp(p2, p3)   #임시로 두 번째 점과 세 번째 점 사이의 중점을 저장

     leftp3 = midp(leftp2, temp)     #왼쪽 세 번째에 임시점과 두 번째 점간의 중점을

     rightp2 = midp(rightp3, temp) #오른쪽 두 번째에 임시점과 오른쪽 세 번째점간의 중점을

     leftp4 = midp(leftp3, rightp2) #왼쪽 네 번째에 왼쪽 세 번째와 오른쪽 두 번째 중점을

     rightp1 = leftp4         #최종적으로 오른쪽 첫 번째 점을 왼쪽 끝 점과 같게함

     beziercurves(leftp1, leftp2, leftp3, leftp4) #왼쪽 점들을 인수로 베지어 함수 호출

     c.create_rectangle(rightp1[0], rightp1[1], rightp1[0]+2, rightp1[1]+2, $

width=0, fill="black")     #2 x 2 사이즈의 사각형을 위 점들을 기준으로 그림

     beziercurves(rightp1, rightp2, rightp3, rightp4)#오른쪽 점들을 인수로 베지어 호출

beziercurves((20,500),(360,100),(580,100),(690,500)) #베지어 함수 초기 호출값

(단위를 더 좁히면 자연스러운 베지어 곡선이 나타난다.)

- 2번에 명시한 알고리즘을 설계한 것이다. 정의한 midp 함수는 중점을 구하는 사용자정의 함수이며 베지어 곡선의 점을 찍는 함수는 재귀적 용법으로 호출된다. 이 재귀적 호출은 첫 점과 사다리꼴형의 반대쪽 끝점간의 x 축 거리가 4 이상일 때까지 시행하며 제한 조건에 도달했을 때 오른쪽 기준의 점들부터 찍는다. 차이에 관련해서 사실상 오른쪽 기준의 점이든 왼쪽 기준의 점이든 찍히는 모양은 같은데 이는 바로 아랫줄의 right point 들을 인수로 다시 재귀적 호출을 하기 때문이다. 이 구조로 인해 베지어 곡선이 완성되며 순서를 바꾸어도 같은 결과를 출력해낸다. 단, 두 번의 재귀호출을 한 번으로 줄이면 엉성하게 지극히 셀 수 있는 개수로 왼쪽에 점이 찍힘을 볼 수 있다.(첫 재귀 호출을 오른쪽으로 바꾸면 오른쪽에만 찍힌다.)


<<채점하는 과제 2>> ($ 표시 부분은 한 줄이라는 의미로 정의하자.)

from Tkinter import *

root = Tk()

c = Canvas(root, height=256, width=256)

c.pack()

rgbarray = [[0,0]]  #list 를 선언하고 [0,0]을 초기 값으로 지정

for i in range(128):

     for j in range(128):     #이중 반복문

         if j!=0 or i!=0:      #위의 초기 지정 값을 제외하고 반복

             rgbarray.append([2*i,2*j])   #list 에 배열을 변화하여 추가

         c.create_rectangle(i*2, j*2, i*2+2, j*2+2, width=0, fill= $

"#%02x%02x%02x"%(rgbarray[(128*i)+j][0],rgbarray[(128*i)+j][1],128))

#그리고 배열의 값에 맞춰 RGB 조절 후 사각형을 그림

- 채점하지 않는 과제 1 과 마찬가지로 이중 반복문 구조를 통해 canvas 전체에 표현한 결과다. 세로로 한 줄씩 표현되어 다음으로 이동하는 구조로 처음이 R,G,B 순으로

0,0,128 임을 참조하면, 파란색으로 표현되고 그 라인 가장 아래쪽은 0, 254, 128 임을 생각한다면 올바르게 표현되었다. 2차원 배열과 같은 형태이나 128 * 128 개의 좌표쌍 배열들의 집합으로 구성되어 접근방법이 외부 반복문의 변수에 128 을 곱하여 접근한다.


<<채점하는 과제 3>> ($ 표시 부분은 한 줄이라는 의미로 정의하자.)

from Tkinter import *

root = Tk()

c = Canvas(root, height=256, width=256)

c.pack()

rgbarray = [[0,0]]

for i in range(128):

     for j in range(128):

         if j!=0 or i!=0:

             rgbarray.append([2*i,2*j])           #위 내용과 동일

     

def filtering(n):     #블러링을 적용할 필터링이란 이름의 사용자 정의 함수

 temp1 = 0         #임시로 누적 합산 및 배정 용도로 쓰일 변수 선언 및 초기화

 temp2 = 0

 for x in range(-1,2):         #9칸이므로 -1 ~ 1 까지

     for y in range(-1,2):     #역시 -1 ~ 1 까지의 범위

         temp1 = temp1 + rgbarray[n+(128*x)+y][0] #R값들을 누적

         temp2 = temp2 + rgbarray[n+(128*x)+y][1] #G값들을 누적

 temp1 = temp1/9   #9로 나눔(평균을 구함)

 temp2 = temp2/9

 for x in range(-1,2):         #위에서 계산했었던 해당 9 칸에

     for y in range(-1,2):

         rgbarray[n+(128*x)+y][0] = temp1  #R 평균값으로 적용

         rgbarray[n+(128*x)+y][1] = temp2  #G 평균값으로 적용

     

for i in range(1,127):

     for j in range(1,127):   #모서리는 제외하므로 1~126까지의 이중 반복문

         filtering(128*i+j)     #필터링 함수를 호출


for i in range(128):

     for j in range(128):

         c.create_rectangle(i*2, j*2, i*2+2, j*2+2, width=0, fill= $

"#%02x%02x%02x"%(rgbarray[(128*i)+j][0],rgbarray[(128*i)+j][1],128))

#필터링 적용 후 해당 값들을 사각형으로 출력

- 각 RGB 성분 중 RG 의 값을 성분으로 갖는 배열들을 추가하여 만든 뒤 순차적으로 접근하여 해당 성분으로 사각형을 만드는 과정을 거치는 것은 위의 문제와 동일하고 모서리는 제외하므로 1~126 의 범위로 접근하여 사각형에 대한 색을 블러링 한다. 순차적으로 접근하는데 원본 그림이 이미 순차적인 RG 성분의 변화에 따른 그림인 탓에 결과화면상으로는 확연한 차이를 알기 힘들다. 이는 느낀점 항목에서 다른 소스에 적용하여 효과를 확인하도록 한다.


<<채점하는 과제 4>> ($ 표시 부분은 한 줄이라는 의미로 정의하자.)

from Tkinter import *

root = Tk()

c = Canvas(root, height=256, width=256)

c.pack()

import math

rgbarray = [[0,0]]

for i in range(128):

     for j in range(128):

         if j!=0 or i!=0:

             rgbarray.append([2*i,2*j])   #위 내용과 동일

         

def calcdistance(x,y):           #Rotate 를 실현할 함수

     rex = int(x*math.cos(math.radians(45)) - y*math.sin(math.radians(45)))

     rey = int(x*math.sin(math.radians(45)) + y*math.cos(math.radians(45)))

     #45도 변환이므로 삼각함수를 이용하여 새로운 좌표 rex, rey 를 도출

     if rex>-1 and rex<128 and rey>-1 and rey<128: #새로운 좌표가 유효하면

         c.create_rectangle(rex*2, rey*2, rex*2+4, rey*2+4, width=0, fill= $

"#%02x%02x%02x"%(rgbarray[(128*x)+y][0],rgbarray[(128*x)+y][1],128))

 #새로운 좌표의 위치에 현재 위치의 RG 값을 적용하여 사각형을 그림

for i in range(128):

     for j in range(128):

         calcdistance(i, j)     #함수 호출

- Bitmap 시간 때 배웠던 Rotate 하는 과정에 필요한 수식을 이용하여 구현했다. 이를 위해 math 를 import 하였으며, 정수여야 하기 때문에 int 를 적용하였다.(혹은 round를 이용하여 두 번째 인수에 0 으로 설정해도 무방하다.) 길이를 2로 설정하지 않은 것은 45도 회전에 따른 일부 공간의 표현이 막히기 때문이다. 정수로만 존재하는 좌표탓에 실수로서는 분명히 다른 좌표가 동일화되는 문제가 발생한다. 그 결과는 아래의 그림과 같으며, +4의 효과는 보간법을 적용한 결과와 유사한 효과를 지니게 된다.


 4. 배운점(느낀점)

  블러링 효과가 유효한 내용이었는지 다른 예제에 적용한 결과를 통하여 파악하자.


이는 기존의 문제에서 사각형의 크기를 넓혀서 표현한 그림이다. 커진 변화 탓에 앨리어싱 현상이 보임을 알 수 있다. 그림에서 보이듯 왼쪽의 그림보다 오른쪽의 그림이 색상간의 변화가 무뎌졌음을 통해 이 알고리즘은 색상에 대한 블러링 효과가 유효하단 결론을 얻을 수 있다.

  채점하는 과제 3은 블러링이 적용되는지 알아보기가 힘든 탓에 몇 번이고 알고리즘을 재적용하는 일이 있었던 반면에, 채점하는 과제 4는 표현 결과 때문에 많은 고생을 했다. 처음의 45도 결과 화면은 현재의 결과 화면과 반대방향으로 표현되는 것과 함께 실제 그림이 돌아간 것이 아닌 돌아간 형태만 유지, 색상은 원본상의 위치와 동일하게 표현되는 문제가 있었다. 다시 알고리즘을 파악하다보니 구조에서, 새로운 좌표를 얻어 그 좌표의 값에 해당하는 x, y 를 그대로 출력했기 때문에 생긴 실수였으며 이를 파악했더니 구멍이 숭숭 뚫린 그림 6과 같은 결과를 얻고 역시 한참을 고민했다. 배웠던 interpolation 은 Nearest neighbor, Bilinear, Bicubic 이었는데 Nearest 를 직접 구현하지 않고 이와 유사한 효과를 얻을 수 있는 방법을 고민하다가 차례로 색을 적용해나가는 알고리즘 상에 미리 그려놔도 이후에 겹쳐지는 영역은 이후의 그림이 덮어쓴다는 점을 이용하여 구현하였다.

  배열을 이용하여 그림을 표현할 때 처음엔 튜플을 추가하는 방법으로 표현하였다.(리스트내의 튜플로)다음 문제에서 직접 내부 배열의 값을 변경하는 과정에서 에러가 발생하였고 다시 튜플이 아닌 리스트로 수정하는 시행착오도 있었다.

  과제 수행과는 직접적인 연관이 없는 건으로, 프로그램의 코딩을 따로 파일로 생성하여 실행하는데에서도 꽤 많은 어려움이 있었다. 더블클릭하여 실행하면 프로그램이 바로 종료되는 탓에 프롬프트로 직접 접근하여 실행해보기도 하는 등의 시도 끝에 폴더 옵션에서 E:\PYTHON22\python.exe -i "%1" %* 로 -i 가 추가되면 바로 종료되지 않음을 인터넷을 통해 알 수 있었다.

  sin, cos 부분은 모두 45도이므로 math.sqrt(2)/2 로 대체하여 같은 결과를 얻을 수 있다. math 없이 루트 2 의 값을 (약)1.4142136 를 곱하여 표현한다면 math 없이 표현하는 것도 가능하다.

  가장 흥미 있었던 과제였다. 알고 있는 범위 속에서 문제를 풀어내는 과정이 마치 퍼즐을 풀어내는 것 같았으며 여러 번의 시도 끝에 결과를 얻어냈을 때의 기분은 무척 좋았다.