이 글은 EBS 수학과 함께하는 AI 기초를 바탕으로 작성되었습니다.
이번 실습에 사용된 사진은 평소처럼 USC SIPI Institute Database에서 가져왔습니다.
글 발췌나 라이선스에 관련한 안내를 찾아볼 수 없었지만 아무리 무료로 배포된다하더라도 엄연한 출판물이므로 교재 내용을 막 퍼오기는 어렵다고 판단하였다.
사실 교재 내용도 꽤 친절하여 내가 뭐 기록을 남길만한 부분도 많지 않다. 다만 알게된 내용 몇가지를 정리하거나, 제공된 소스를 일부 바꾸어 보는 것도 괜찮을 듯하여 글로 남기고자 한다.
1. 사진작가가 꿈인 민지
이번 단원에선 민지의 꿈이 사진작가란다. 사진을 찍고 수정을 하다보니 이미지 데이터를 어떻게 저장하나 궁금해졌단다.
별수있나. 함께 알아봐야지.
2. 이미지 데이터 표현하기
2-1. Pixel Data Container : numpy.ndarray
파이썬 기본 자료형만으로 Pixel Array Data를 담기엔 한계가 있다. 뭐 2차원의 리스트를 쓰면 되겠지만, 강력한 행렬연산을 지원하는 numpy
를 쓰지 않을 이유가 없다. 앞으로 자주 쓸 matplotlib
나 pandas
, sympy
, scipy
와의 호환성도 생각하면 무조건 써야하겠다.
numpy 홈페이지를 들어가보라. 벌써부터 가슴이 웅장해진다.
어쨌든, 해보자.
import numpy as np
myImg = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]])
yrImg = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]])
result = myImg-yrImg
result *= 2
print(result)
# result
#[[0 0 0 0 0 0 0 0]
# [0 2 2 2 0 0 0 0]
# [2 2 0 2 2 0 0 0]
# [2 2 0 2 2 0 0 0]
# [0 2 2 2 0 0 0 0]
# [0 0 0 0 0 0 0 0]
# [0 0 0 0 0 0 0 0]
# [0 0 0 0 0 0 0 0]]
numpy
를 import후 저렇게 리스트 형태로 넘겨주면 numpy.ndarray
형태로 반환이 된다. 그럼 보다시피 저렇게 행렬 연산을 표현하듯 연산을 명령하면 알아서 행렬 뺄샘과 상수배 계산을 진행한다. 세상에.
2-2. Pixel Data 표현하기 : Python Turtle Graphics (2-2-10.py)

교재에서 굉장히 재밌는 친구를 들고왔다. Turtle Graphics는 원래 프로그래밍 언어 'Logo'에서 처음 개발되었단다.
거북이(또는 화살표, 원)가 명령어에 따라 이동하면서 선을 그리고 색칠을 한다.
뭐 검색해보면 굉장히 다양한 걸 그릴 수 있는 친구라는 걸 알 수 있다만, 뭐 그런거까지는 관심없고 교재에서는 픽셀들을 그리는 용도로 사용했다. 적어도 toASCII보다는 훨씬 나아보인다. 하지만 굉장히 느려서 큰 사진을 그리면 속 터진다.
자세한 사용법은 문서를 참고하라.
교재에서는 아래와 같은 함수를 정의하여 픽셀 하나를 그릴 수 있도록 하였다.
import turtle
# (x,y) 위치에 pSize 크기의 픽셀을 pCol 색으로 그리는 함수
def putPixel(x, y, pSize, pCol): # 메인 소스 코드에서 호출하는 픽셀 채우기 함수
turtle.penup() # 좌표 이동을 위해 펜기능을 비활성화
turtle.goto(x*pSize,(-1)*y*pSize) # 주어진 좌표로 이동 (y좌표에 유의)
turtle.pendown() # 펜기능을 다시 활성화
turtle.begin_fill() # 다각형을 그릴 때 내부를 채우기
turtle.fillcolor(pCol) # 다각형의 채움색 설정하기
turtle.setheading(45) # 시작 각도
turtle.circle(pSize/2, steps = 4) # 정사각형 픽셀 도출하기
turtle.end_fill() # 채우기 끝
turtle.penup
은 선그리기(또는 채우기)를 임시 비활성화 하는 기능이다. turtle.goto
를 통해 다음 픽셀 위치로 이동하면서 선을 그리면 안되기 때문에 사용하였고, turtle.pendown
을 통해서 다시 펜 기능을 활성화 한다. turtle의 좌표계는 x축 양의 방향이 오른쪽, y축 양의 방향이 위쪽인, 수학에서 사용하는 2차원 직교좌표계와 동일한 좌표를 사용하므로 y좌표를 수정하지 않으면 상하반전이 되어 출력됨에 유의해야한다.
turtle.begin_fill
을 실행한 이후 turtle.end_fill
가 실행될때까지, 다각형을 그리면 그 내부는 설정된 색으로 색칠된다. turtle.fillcolor
로 색을 설정할 수 있다. ('red'
, 'green'
또는 HexColor와 같은 colorstring
도 되고, 각 R
,G
,B
색상 채널값을 줄 수도 있다. 역시 파이썬 다형성은…)
turtle.circle
로 원을 그릴 수 있는데, 특이하게도 steps
속성에 3이상의 자연수 값을 주면 정다각형이 나온다. 원을 그리는 과정도 진짜 원이 아닌 무한에 가까운 정n각형을 그리는 것이기 때문에 몇단계에 걸쳐 원을 그리냐는 steps
속성을 활성화 시켜 정다각형을 그린다는 건 어찌보면 매우 자연스러운 확장이다.
예제에서는 좌표이동을 pSize
를 기준으로 하기때문에 정다각형의 반지름을 pSize/2
로 주었다.
turtle.setheading
은 정다각형 그리기의 시작각도를 설정하는데, 시작각도를 0도로 놓고 그리면 우리가 기대한 것과 다른 결과가 나온다.
turtle.circle
로 그리기 시작할때 \(-\frac{\pi}{2}\), 또는 \(-90^\circ\)에서부터 시작해 양의 방향으로 그려나가기 시작한다. 때문에 heading
이 0인 상태에서 시작하면 제일 아래 꼭지점에서 시작해서 \(45^\circ\) 우상향으로 그려나가기 시작한다. heading
을 45로 놓고 시작하면 정사각형의 각 변이 수직 수평방향과 평행하도록 그릴 수 있다.
for y in range (0, 8):
for x in range (0, 8):
if (result[y][x] > 0):
putPixel(x,y,10, "blue")
else:
putPixel(x,y,10, "white")
이제 이렇게 순회만 해주면 아래와 같은 결과를 확인할 수 있겠다.
이제 이렇게 픽셀을 간접적으로나마 출력이 가능하다. 어느세월에…
2-3. Open Image File : using Pillow (2-2-11.py)
PIL(Python Imaging Library)의 Fork인데 PIL에 image라이브러리를 추가한 듯 하다. 어쨌든,
> pip install pillow
와 같이 설치한 후,
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
img = Image.open("parrots.png")
pix = np.array(img)
print(pix.shape)
plt.imshow(pix)
plt.axis("off")
plt.show()
와 같이 실행시켜보자.
matplotlib.pyplot.imshow
에 pixel data를 numpy.ndarray
형태로 넘겨주면 pixel data를 읽어서 출력해준다!
더불어, numpy.ndarray.shape
는 행렬의 크기를 tuple로 반환하는데, 예시로 사용한 parrots.png
의 경우
(1085,1080,4)
값을 가진다. 음… 3차원 array이다. 1085는 세로픽셀, 즉 행의 개수이고 1080은 가로 픽셀, 즉 열의 개수인데… 4는 채널수이다. 그러니 이 array는
pix[y][x][0] = red_value
pix[y][x][1] = green_value
pix[y][x][2] = blue_value
pix[y][x][3] = alpha_value
아래와 같이 접근해야겠다. 참고로 plt.axis
를 건드리지 않고 축을 출력해보면 알겠지만 다행히 PIL.Image
라이브러리의 배려로, y좌표가 반전되어 저장되어 있음을 고려하여 픽셀 데이터를 가져오므로 이제 이에 구애받지 않고 그대로 접근 가능하다!
여기를 참고했는데 matplotlib.pyplot.subplot
을 통해 여러 그래프를 표시하기 용이하다. 물론 우리가 지금 사용하는 이미지 데이터도 말이다. 가령,
plt.subplot(4,2,3)
과 같이 설정하는 건, 전체영역을 4행 2열로 쪼갠 후, 그중 3번째 영역(행우선)에 이어지는 명령에 따라 그래프든 뭐든간에 출력을 하겠다 라고 설정하는 것과 같다.
이야기가 길어지니 2-2-11.py를 참고하자.
2-4. Save Image File with Pillow
import numpy as np
from PIL import Image
img = Image.open("parrots.png")
pix = np.array(img)
# Processing...
out = Image.fromarray(pix.astype(np.uint8))
out.save("parrots_processed.png")
PIL.Image.fromarray
를 통해 ndarray
에 저장되어 있는 픽셀데이터를 이미지파일로 바꿀 수 있다. 한 채널당 8비트이므로 array의 각 원소는 uint8
로 읽어와야한다. (사실 astype(np.uint8)
은 사족에 가까웠다. 그냥 넘겨도 알아서 인식한다.)
어쨌든, 이런 식으로 처리한 이미지 데이터를 저장할 수도 있다.
3. Processing (2-2-12.py)
배경사진 하나에 다른 이미지 사진을 섞는 blending을 할건데, 두 이미지를 이어붙인 이미지를 배경사진에 맞게 resizing후 blending을 하겠다.
3-1. manipulating image : convert to RGB / resizing
# image file 읽어오기
imgBG = Image.open("lake.jpg") # 배경 이미지 열기
imgFG1 = Image.open("lena.png") # 사진 이미지1 열기
imgFG2 = Image.open("mandrill.png") # 사진 이미지2 열기
imgFG2 = imgFG2.convert('RGB')
우선 이 파일들을 사용할 예정이다. 배경사진은 여기에서 가져왔다.
그런데 다짜고짜 convert
부터 했다. lake.jpg
야 jpeg형식이니 그냥 RGB 채널이 있는 Indexed color table을 가질 것이고, lena.png
는 24bit RGB이나, mandrill.png
가 32bit RGBA 형식이다. 쓸데없이 알파채널까지 지원해서… 이대로 연산하면 보기좋게 exception을 토해낼 것이다. 당연히 Array 크기가 다르니 호환이 될리가 없다. 과감히 RGB형식으로 통일하자.
그런데 그냥 함수하나로 이미지 파일의 형식이 변환됐다. 방대한 라이브러리의 위력을 직접 느끼는 중이다.
pix1 = np.array(imgBG)
# 사진을 이어붙이기 위해 배경에 맞추어 변경할 크기 계산하기
# 만약 배경 화면의 가로 크기가 홀수이면 첫번째 이미지의 가로 크기를 반올림하기
resize1 = resize2 = pix1.shape[1]//2 # 우선 절반
if (pix1.shape[1] % 2 > 0) : # 홀수인지 체크
resize1 += 1
# 사진 2장을 나란히 붙이기 위해 배경 이미지의 절반씩 차지하도록 크기 변경하기
imgFG1 = imgFG1.resize((resize1, pix1.shape[0])) # 첫번째 사진 크기 변경
imgFG2 = imgFG2.resize((resize2, pix1.shape[0])) # 두번째 사진 크기 변경
pix2 = np.array(imgFG1)
pix3 = np.array(imgFG2)
배경 이미지 하나에 blending할 이미지 두개를 우겨넣어야 하기 때문에 사이즈를 조정하는 작업을 해야한다. 우선 imgBG
로부터 ndarray
를 읽어들이고, 그 사이즈를 바탕으로 가로길이를 결정한다. 이후 그 가로길이와 세로길이를 이용해 resizing을 진행한다.
PIL.Image
에서는 resizing도 함수 하나로 해결된다. 눈물난다
3-2. Concatenating
# 사진 2개를 옆으로 나란히 붙이기(axis값을 0으로 하면 세로로 설정됨.)
pix4 = np.concatenate((pix2, pix3), axis = 1) # 두 사진을 가로 방향으로 붙이기
이것도 numpy.concatenate
함수로 한번에 해결된다.
3-3. Blending
# 이미지를 블렌딩하기 위해 각 픽셀의 RGB 값을 (0~1)의 실수 범위로 정규화(normalize)
pix1 = (1/255)*pix1
pix4 = (1/255)*pix4
weight = 0.7 # 가중치 정하기
pix5 = pix4 * weight + pix1 * (1-weight)
# 가중치를 적용하기 위해 원본 이미지 행렬에 가중치를 실수배하여 합하기
pix5 = pix5*255
뭐 사실 소스로 쓰는 사진들이야 각 채널이 8비트임을 잘 알기때문에 굳이 정규화를 할 필요도 없겠다만, 두 이미지 채널 크기가 다르다면 통일시켜줘야한다.
간단하게 가중치를 정한 후, blending하는 두 이미지의 행렬에 적절히 곱해서 더해준다.
끗. 결과는 2-2-12.py를 직접 실행하여 확인해라.
4. Bitmap 프로젝트의 연장…?
나중에… 나중에 해보겠다…