본문 바로가기
Programming/Python

플로이드 스타인버그 디더링 (Floyd–Steinberg dithering) 파이썬으로 구현하기

by IN.0 2020. 11. 20.
728x90
반응형

음악 스트리밍 서비스인 스포티파이(Spotify)의 셔플 알고리즘에 대해 공부하다가

플로이드 스타인버그 디더링을 응용하여 구현하였다길래 디더링이 무엇인지 찾아보고 구현해보았다.

(설명 없이 코드만 보려면 맨 아래로..)

 

디더링?


디더링이란 제한된 색을 이용하여 음영이나 색을 나타내는 것이며, 여러 컬러의 색을 최대한 맞추는 과정이라고 한다.

찾아보니 이미지 외에 음악에도 사용되는 것 같은데, 고음질의 음원 (무손실 음원)을 최대한 오류를 줄이며

왜곡되지 않게 비트를 줄이는 것을 뜻하기도 한다.

압축과 비슷한 개념인가? 하는 생각도 들었다.

 

디더링 구현 전에 간단한 손 풀기 1 - 이미지 그레이 처리


from PIL import Image

im = Image.open("fall.jpg")
mode, size = im.mode, im.size
width, height = size[0], size[1]
pix = list(im.getdata())
im.close()


new_pix = []
for i in pix:
    maxi = max(i)
    new_pix.append((maxi, maxi, maxi))

newIm = Image.new(mode, size)
newIm.putdata(new_pix)
newIm.show()

파이썬에서 이미지를 다루기 위해 PIL(Python Image Library)을 임포트 했다.

마음에 드는 이미지를 im이라는 변수에 담고, 모드와 사이즈를 따온다.

타겟 이미지

pix라는 변수에 리스트로 im에서 픽셀 데이터를 담는다. (R, G, B) 이렇게 튜플로 구성되어 있다.

new_pix는 pix에서 이미지를 변형한 후 담을 새로운 변수이다.

pix에서 for문을 돌며 R, G, B 값 중 가장 큰 값을 maxi라고 정의하고,

new_pix에 R, G, B 값을 모두 maxi로 변경하여 담는다. 이유는 아래와 같다.

RGB 값을 보자!
RGB 값을 보자!

그레이 영역(최좌측)은 RGB 값이 모두 같다. 흰색은 (255, 255, 255), 검은색은 (0, 0, 0) 임을 생각하면

쉽게 알 수 있다.

원래 색상을 왼쪽으로 쭈욱 밀어 같은 톤의 그레이로 바꾸게 되면 모두 RGB 중에 가장 큰 값으로 변경된다.

 

모든 픽셀에 이러한 과정을 적용하면 그레이쉬한 이미지가 도출된다.

그레이 처리

 

 

디더링 구현 전에 간단한 손 풀기 2 - 이미지 흑백 처리


from PIL import Image
from math import sqrt

def index(x, y):
    return x*width + y

im = Image.open("fall.jpg")
mode, size = im.mode, im.size
width, height = size[0], size[1]
pix = list(im.getdata())
im.close()

COLORS = [(0,0,0), (255,255,255)]
for i in range(height):
    for j in range(width):
        target = pix[index(i,j)]
        diff_list = []
        for col in COLORS:
            diff = sqrt((col[0]-target[0])**2 + (col[1]-target[1])**2 + (col[2]-target[2])**2)
            diff_list.append(diff)
        if diff_list[0] < diff_list[1]:
            pix[index(i,j)] = COLORS[0]
        else:
            pix[index(i,j)] = COLORS[1]

newIm = Image.new(mode, size)
newIm.putdata(pix)
newIm.show()

손 풀기 1과 동일하게 PIL을 임포트 하여 사용했다.

그리고 픽셀마다 블랙, 화이트 좌표와의 거리를 구하기 위해 sqrt(루트를 씌워 계산)도 임포트 했다.

이미지를 가져오는 과정은 동일한데, 이번에는 new_pix를 만들지 않고 pix 값을 변경하는 방식으로 구현해 보았다.

그러기 위해서는 이미지의 width와 height를 고려한 좌표를 사용해야 한다.

index 함수가 그 역할을 하는데, 예를 들어 720*400 사이즈의 이미지에서

3번째 줄 14번째 픽셀을 선택한다면 그 픽셀의 순번은 3*720(너비) + 14 = 2174이다.

즉, pix[index(3, 14)]는 pix[2174]와 같다.

이런 식으로 x축과 y축을 i, j로 정의하여 이중 포문을 돈다.

COLORS는 [검은색, 흰색] RGB 값이 들어있으며, 타겟 픽셀이 어느 쪽에 더 가까운지 검사한다.

그리고 더 가까운(거리 값이 작은) 쪽의 RGB 값을 pix의 해당 순번에 대체한다.

결괏값을 확인하면 아래와 같다.

흑백 처리

블랙 아니면 화이트로 (극단적으로) 나뉘었기 때문에 이미지가 딱딱해 보인다.

그래서 이를 보완하기 위해 사용하는 것이 디더링이다.

 

디더링 구현


from PIL import Image
from math import sqrt

def index(x, y):
    return x*width + y

def apply_err(target, err, factor):
    r = int(target[0] + err[0]*factor)
    g = int(target[1] + err[1]*factor)
    b = int(target[2] + err[2]*factor)
    rgb_list = [r, g, b]
    for i in range(3):
        if rgb_list[i] < 0:
            rgb_list[i] = 0
        elif rgb_list[i] > 255:
            rgb_list[i] = 255
    return tuple(rgb_list)

im = Image.open("fall.jpg")
mode, size = im.mode, im.size
width, height = size[0], size[1]
pix = list(im.getdata())
im.close()

COLORS = [(0,0,0), (255,255,255)]
for i in range(height):
    for j in range(width):
        target = pix[index(i,j)]
        diff_list = []
        for col in COLORS:
            diff = sqrt((col[0]-target[0])**2 + (col[1]-target[1])**2 + (col[2]-target[2])**2)
            diff_list.append(diff)
        if diff_list[0] < diff_list[1]:
            pix[index(i,j)] = COLORS[0]
        else:
            pix[index(i,j)] = COLORS[1]

        new_target = pix[index(i,j)]
        err = (target[0]-new_target[0], target[1]-new_target[1], target[2]-new_target[2])

        if j != width-1 :
            pix[index(i,j+1)] = apply_err(pix[index(i, j+1)], err, 7/16)
        if i != height-1 :
            pix[index(i+1,j)] = apply_err(pix[index(i+1,j)], err, 5/16)
            if (j > 0):
                pix[index(i+1,j-1)] = apply_err(pix[index(i+1,j-1)], err, 3/16)
            if (j != width-1):
                pix[index(i+1,j+1)] = apply_err(pix[index(i+1,j+1)], err, 1/16)

newIm = Image.new(mode, size)
newIm.putdata(pix)
newIm.show()

에러 처리를 위한 apply_err를 제외하고 전체적인 흐름은 흑백처리와 같다.

에러 처리에는 여러 방식이 있는데, 플로이드 스타인버그 디더링 (Floyd–Steinberg dithering)에서는

커널의 모든 값을 전체 합 16으로 나누어 처리한다.

이렇게... (*)이 현재 위치

이미지 처리를 위에서 아래로, 왼쪽에서 오른쪽으로 하기 때문에 이미 처리한 픽셀은 건드리지 않고,

앞으로 처리할 픽셀에만 오차 보정을 준다.

처리할 미래 픽셀의 R, G, B 값에 현재 흑백 처리한 픽셀의 오차를 위의 수만큼 곱하여 더한다.

그럼 오차에 따라 0보다 작아지거나 255보다 커질 수 있는데, 그럴 때에는 0 또는 255로 보정을 해준다.

그리고 결과는 아래와 같다.

디더링 결과

손 풀기 2에서 한 흑백처리와 비교하면 훨씬 부드럽다.

블랙과 화이트만 사용했지만 그레이쉬한 느낌이 나면서 효율적이다.

 

이걸 어떻게 음원 셔플에 사용했는지 감은 잘 안 오지만.. 아무튼 흥미로웠고,

이미지 처리가 재미있어서 위에 설명한 것 말고 이미지 밝게 하기, 특정 색감 강조하기 등

카메라 어플에 있을법한 것도 구현해 봤는데, 나중에 써먹을 수 있을 것 같다.

728x90
반응형

댓글