Heesung Yang

스마트폰 앱은 어떻게 이미지를 보정하는 걸까?

이미지 보정

스마트폰이 필수 아이템으로 자리잡은 요즘, 스마트폰을 이용한 사진 촬영과 보정은 매우 흔한 일상이 되었다. 터치 몇 번으로 보정할 수 있는 앱들도 많이 사용되고 있다. 터치 몇 번으로 이미지가 확확 바뀌는 일들은 실제로 어떻게 이뤄지는 걸까?

이미지 밝기 조정

이어지는 내용은 (컴퓨터는 어떻게 이미지를 저장하고 표현할까) 글을 읽었다는 가정하에 작성한 내용이므로 혹시 위 글을 아직 읽지 않았다면 먼저 읽기를 추천한다.

이미지 밝게 만들기

아래 흑백 이미지를 예로 들어 보자.

cameraman

흑백 이미지는 0~255(검은색~흰색) 사이의 숫자로 표현된다. 숫자가 255에 가까울수록 흰색에 가까워 진다. 이 말은 이미지의 밝기를 밝게 하기 위해선 픽셀의 값을 일정하게 증가시키면 이미지가 밝아지고 감소시키면 이미지가 어두워진다는 얘기다.

먼저 이미지를 밝게 만들어보자. 각 픽셀의 값을 50씩 증가시켜보자.

from PIL import Image
import numpy as np

# 이미지를 읽어서 numpy 객체로 변환
img = np.array(Image.open("cameraman.png"))

# 이미지의 각 픽셀 값을 50씩 증가시킴
img = img + 50

Image.fromarray(img).show()

cameraman-brighten-overflow.png

이미지가 밝아지긴 했는데… 군데군데 이상한 점이 보인다.

대체 왜 하얀 부분이었던 곳이 검정색으로 변한거지??

왜 이런 현상이 나올까?

흑백 이미지는 각 픽셀이 0(검은색) ~ 255(흰색)값을 갖는다고 말했다. 밝기를 조정하기 전, 흰 색 픽셀의 값은 255 였을 것이고, 여기에 50을 더했으면 305가 되는게 아닌가? 잠깐, 가장 밝은 흰 색이 255 라고 했는데 305는 어떤 색이 되는거지? 흰 색보다 더 밝은 색이 있는건가?

컴퓨터와 이진수

혹시 컴퓨터는 0과 1로 모든 데이터를 표현한다는 얘기를 들어본 적 있는가? 0과 1로 표현할 수 있는 건 오직 2가지 뿐인데 이게 무슨 얘기일까? 우선 아래 그림을 보자.

binary number

  • 한 자리수일 때는 0과 1만 표현할 수 있다.
  • 두 자리를 묶어서 하나의 수를 표현할 수 있다. 즉 11 이라고 쓰고 숫자 3이라고 해석할 수 있다.
  • 세 자리를 묶어서 하나의 수를 표현할 수 있다. 즉 111 이라고 쓰고 숫자 7이라고 해석할 수 있다.
  • 네 자리를 묶어서 하나의 수를 표현할 수 있다. 즉 1111 이라고 쓰고 숫자 15이라고 해석할 수 있다.
  • 혹시 이진법/이진수 라는 단어가 떠오르지 않는가? 우리가 수학시간에 배운 이진수가 이렇게 활용되고 있다…!!!

흑백 이미지는 숫자 0 ~ 255 로 각 픽셀을 표현한다고 했다. 0 ~ 255를 0과 1로 표현하려면 몇 자리수가 필요할까? 직접 계산해보면 8자리가 필요하다는 걸 알 수 있다.

0 0 0 0 0 0 0 0 -> 0
0 0 0 0 0 0 0 1 -> 1
...
1 1 1 1 1 1 1 0 -> 254
1 1 1 1 1 1 1 1 -> 255

대부분의 흑백 이미지는 8자리만 사용해서 각 픽셀을 표현한다. (그래서 8비트 흑백 이미지라고 부르기도 한다.) 즉, 표현할 수 있는 가장 큰 숫자는 255다. 근데 255에 1을 더하면 어떻게 될까? 일단 한번 이진수로 표현해보자.

    1 1 1 1 1 1 1 1
+                 1
-------------------
  1 0 0 0 0 0 0 0 0

256을 0과 1로 표현하기 위해 한 자리가 더 필요하다! 총 아홉 자리수가 필요하다. 근데 흑백 이미지는 8자리만 사용한다고 했다. 그래서 맨 앞 한자리가 버려지고 0만 남는다.

    1 1 1 1 1 1 1 1
+                 1
-------------------
    0 0 0 0 0 0 0 0  -> 맨 앞자리 1이 버려지고 0만 남았다.

다시 0이 되었다. 즉, 검은색이 되었다는 말이다. 255(흰색)보다 큰 256이 더 밝아지기는 커녕 가장 어두운 0(검은색)이 되었다. 이게 바로 앞에서 보았던 흰색이 검은색으로 변했던 비밀이다!

255에 2를 더하면 어떻게 되는지 한번 더 살펴보자.

    1 1 1 1 1 1 1 1  -> 255
+                 1
-------------------
  1 0 0 0 0 0 0 0 0  -> 256
+                 1
-------------------
  1 0 0 0 0 0 0 0 1  -> 257

위에서 맨 앞자리 1을 버리면 00000001만 남는다. 즉, 숫자 1이 된다.
거의 검정에 가까운 값이다.
    0 0 0 0 0 0 0 1

한 바퀴 돌아 다시 제자리 느낌이지 않은가?

254 -> 254  (1 1 1 1 1 1 1 0)
255 -> 255  (1 1 1 1 1 1 1 1)
256 -> 0    (0 0 0 0 0 0 0 0)
257 -> 1    (0 0 0 0 0 0 0 1)
258 -> 2    (0 0 0 0 0 0 1 0)
259 -> 3    (0 0 0 0 0 0 1 1)
...

위 현상은 우리가 원했던 결과와 다르다. 더 이상 밝아질 수 없으면 그냥 가장 밝은 색으로 표현하면 좋겠다. 픽셀에 어떤 값을 더했는데 255(흰 색)보다 커지면 그냥 255로 만들어서 흰색으로 표현하면 좋겠다. 이를 좀 어려운 말로 Saturation 연산이라고 한다.

254 -> 254  (1 1 1 1 1 1 1 0)
255 -> 255  (1 1 1 1 1 1 1 1)
256 -> 255  (1 1 1 1 1 1 1 1)
257 -> 255  (1 1 1 1 1 1 1 1)
258 -> 255  (1 1 1 1 1 1 1 1)
259 -> 255  (1 1 1 1 1 1 1 1)
...

자, 처음 코드를 약간 수정해보자.

from PIL import Image
import numpy as np

# 이미지를 읽어서 numpy 객체로 생성
img = np.array(Image.open('cameraman.png'))

# 이 예제에서 사용한 cameraman 흑백 이미지를 읽으면
# 각 픽셀이 8자리로 구성되어 있다.(8비트 흑백 이미지)
# 때문에 255를 초과하는 값은 자동으로 0,1 과 같이 변경된다.
# 이를 방지하기 위해 자리수를 16자리로 늘린다. (16비트 흑백 영상으로 변환)
# 16자리에선 65535 까지 표현할 수 있다.
img = img.astype(np.uint16)

# 각 픽셀마다 50씩 더하기
img = img + 50

# 255를 넘는 픽셀값은 255로 설정
img[img > 255] = 255

# 16자리에서 8자리로 다시 변경(8비트 흑백 영상으로 변환)
img = img.astype(np.uint8)

# 이미지 보기
Image.fromarray(img).show()

brighten image pixel

흰색이 검은색으로 변하던 현상이 없어졌다 !!!

brighten cameraman

이미지 반전

이미지 반전은 어떻게 할 수 있을까? 반전은, 뒤바꾼다는 의미이므로 각 픽셀값을 아래와 같이 바꿔보면 어떨까?

  • 픽셀값이 0인 경우 255로(검은색 -> 흰색), 255인 경우 0으로(흰색->검은색) 바꾼다.

  • 픽셀값이 1인 경우 254로(덜 검은색 -> 덜 흰색), 254인 경우 1로 바꾼다.

  • 정리하면 아래와 같다.

    0   <-> 255
    1   <-> 254
    2   <-> 253
    ...
    125 <-> 130
    126 <-> 129
    127 <-> 128
    

inverted cameraman

와우! 예상했던대로 된다!

아래는 파이썬으로 구현한 예제 코드다.

from PIL import Image
import numpy as np

img = np.array(Image.open('cameraman.png'))

# 0을 입력하면, 255가 되어야 하고
# 1을 입력하면, 254가 되어야 하고
# 2을 입력하면, 253가 되어야 하고
# 3을 입력하면, 252가 되어야 하고
# ...
# 127을 입력하면, 128이 되어야 한다.
# 위 상황을 모두 만족하는 공식은 다음과 같다.
# y = 255 - x

# 위 공식의 x에 0을 넣으면 255가 나온다.
# 위 공식의 x에 1을 넣으면 254가 나온다.
# 위 공식의 x에 255를 넣으면 0이 나온다.
img = 255 - img

Image.fromarray(img).show()

마치며

매우 간단한 보정 방법을 살펴보았다. 사실 우리가 일상생활에서 스마트폰으로 촬영한 이미지를 보정하는 것들은 훨씬 더 복잡하다. 각 픽셀 값을 어떻게 변화시킬 것인지, 수학적으로 풀어내야 하기 때문이다. (중고등학교 때 수학은 대체 배워서 어따갖다 쓰냐며 무시했었는데…)

앞으로 필자도 좀 더 공부해서 복잡한 예제들을 하나씩 포스팅 해보겠다. (수학의 정석을 사야하나…)