이미지를 저장하는 보통의 방법은 각 픽셀을 명시하는 것입니다. 이러한 포맷은 이미지 벡터의 표준 기저에 대한 표현이라고 할 수 있습니다. 하지만 다른 기저로 스파스한 표현이 (값이 0인 데이터는 저장하지 않는) 가능하다면 압축을 이룰 수 있습니다. 만약 표준 기저에 대한 표현이 스파스하지 않으면 표현된 엔트리 중 영에 가까운 것을 억제(suppress)하여 스파스하게 만들 수 있습니다.
원래의 이미지 벡터 b를 표준 기저 A에 의해 다음과 같이 표현할 수 있습니다.
Ax=b
여기서 x는 b의 표준 기저 A에 대한 좌표 표현입니다. 기저 u1,...,un에 대한 표현으로 바꾸고자 한다면 u1,...,un가 열벡터인 행렬 Q에 대해 다음과 같이 표현할 수 있습니다.
Qy=b
이 때 Q의 열벡터들이 모두 정규직교한다면, 즉 직교 행렬이라면 y를 다음과 같이 구할 수 있습니다.
y=QTb
한편 정규직교 벡터는 norm을 보존하는 좋은 성질을 가지고 있습니다. 임의의 벡터 u, v, 열-직교 행렬 Q에 대해 다음이 성립합니다.
위 기저는 V8에 대한 기저입니다. 마찬가지로 V4,V2,V1에 대한 기저를 만들 수 있습니다.
V2k를 Vk로 다운샘플링했을 때 Vk의 V2k에 대한 직교여공간을 웨이브릿 공간 Wk라 하겠습니다. 즉, 다음과 같이 쓸 수 있습니다.
V2k=Vk⊕Wk
k=8,4,2,1을 대입하면 다음을 얻습니다.
V16=V8⊕W8V8=V4⊕W4V4=V2⊕W2V2=V1⊕W1
대입을 모두 적용하면 다음을 얻습니다.
V16=V1+W1+W2+W4+W8
V16의 하나의 기저는 V1, W1W2, W4, W8의 기저의 합집합입니다. 이러한 형태를 가지는 기저를 Haar 기저라고 합니다.
V8의 직교여공간 W8의 기저부터 구해보겠습니다. W8의 기저는 V16에 속하면서 b08,b18,...,b78에 모두 직교하는 벡터입니다. 첫번째 기저는 b016의 b08,b18,...,b78에 대한 직교 투영을 구함으로서 찾을 수 있습니다. b016과 b08,b18,...,b78은 다음과 같습니다.
b016의 b08,b18,...,b78에 대한 직교 투영은 b016에서 b016의 b08,b18,...,b78에 대한 투영을 뺀 b016−b016∥V8=b016−σ0b08−σ1b18−...−σ7b78 입니다. 이 때 b016은 V8의 기저 중 b08을 제외한 다른 모든 기저에 직교하므로 b016∥V8=σ0b08−0−0−...−0=σ0b08입니다. 따라서 b016⊥V8=b016−1+11b08이고 다음과 같습니다.
w08=0.5−0.500000000000000
b116의 V8에 대한 직교 투영은 단순히 w08의 음수값을 제공합니다. b216의 직교 투영, w18은 다음과 같습니다.
16픽셀 이미지를 15개의 웨이브릿 기저와 1개의 박스 기저에 대해 표현하는 법을 알아보았습니다. 압축을 위해서는 벡터들을 정규직교 기저에 대해 표현하는 것이 좋습니다. 따라서 기저들을 각각의 norm으로 나누고 선형결합의 항등을 위해 계수들에 norm을 곱함으로서 정규화합니다. 벡터 v를 정규화해 다음과 같이 바꿔 씁니다.
예를 들어 키 (8,0)에 연관된 값은 w08에 대한 계수이고 (1,0)에 연관된 값은 w01에 대한 계수입니다. (0,0)에 대한 계수는 b01에 대한 계수이고 편의상 b01은 w00라 하겠습니다.
정규화되지 않은 웨이브릿 기저에 대한 계수를 구하는 프로시저 forward_no_normalization
defforward_no_normalization(v):
D ={(0,0):sum(v)/len(v)}whilelen(v)>1:
k =len(v)for i inrange(k //2):
D[(k //2, i)]= v[2* i]- v[2* i +1]
vnew =[(v[2* i]+ v[2* i +1])/2for i inrange(k //2)]
v = vnew
return D
프로시저 forward_no_normalization의 변수 vnew는 박스 기저에 대한 계수를 나타냈던 트리의 각각의 레이어를 뜻합니다. 예를 들어 b016,...,b1516에 대한 계수를 한 쌍씩 묶어 평균을 구한 b08,...,b78에 대한 계수가 한 번의 이터레이션에서 저장됩니다. 한 번의 이터레이션을 통해 바뀐 vnew는 다음과 같습니다.
v =[4,5,3,7,4,5,2,3,9,7,3,5,0,0,0,0]# 첫번째 이터레이션 후 v와 vnew
v = vnew =[(4+5)/2,(3+7)/2,...,(0+0)/2]
웨이브릿 기저에 대한 계수는 변수 v의 한 쌍의 엔트리의 차분값이므로 D[(k // 2, i)] = v[2 * i] - v[2 * i + 1]와 같이 작성합니다.
정규화를 위해 계수들에 norm을 곱하는 프로시저 normalize_coefficients
import math
defnormalize_coefficients(n, D):
new_D ={(0,0): D[(0,0)]* math.sqrt(n)}for k, v in D.items():if k[0]!=0:
new_D[k]= v * math.sqrt(n /(4* k[0]))return new_D
프로시저 normalize_coefficients에서 주의해야 할 것은 w01,...,w78의 norm은 4sn이지만 w00의 norm은 n이라는 것입니다.
forward_no_normalization와 normalize_coefficients를 이용해 정규화된 웨이브릿 기저에 대한 계수를 구하는 프로시저 forward
프로시저 forward를 이용해 표준 기저에 대해 표현한 이미지 벡터를 웨이브릿 기저에 대해 표현할 수 있게 되었습니다. 이미지 압축의 첫 단계가 끝난 것입니다. 이제 새로운 기저에 대해 표현된 계수들 중 작은 값들을 억제하는 프로시저를 작성해 보겠습니다.
억제 압축 프로시저 suppress
defsuppress(D, threshold):return{
k: v ifabs(v)> threshold else0for k, v in D.items()}
간단히 딕셔너리 형태로 저장된 계수들 중 threshold보다 작은 값을 0으로 만듦으로서 억제 압축을 구현합니다. 이 때 0으로 만드는 것은 벡터를 스파스하게 표현하는 것입니다.
스파스의 정도를 측정하는 프로시저 sparsity
defsparsity(D):returnlen([ k for k in D if D[k]!=0])/len(D)
만약 D에 0이 하나도 없다면 1의 sparsity를, 10개의 엔트리 중 2개의 0을 가지면 0.8의 sparsity를 갖게 됩니다.
프로시저 suppress를 이용해 웨이브릿 기저에 대한 이미지 벡터 표현을 압축할 수 있게 되었습니다. 이제 압축된 이미지 벡터를 다시 표준 기저에 대한 표현으로 바꾸는 과정을 구현해 보겠습니다.
계수들을 대응하는 웨이브릿 기저의 norm으로 나누는 프로시저 unnormalize_coefficients
import math
defunnormalize_coefficients(n, D):
new_D ={}
new_D[(0,0)]= D[(0,0)]/ math.sqrt(n)for k, v in D.items():if k[0]!=0:
new_D[k]= v / math.sqrt(n /(4* k[0]))return new_D
단순히 프로시저 normalize_coefficients와 반대의 연산을 진행합니다.
엔트리의 평균과 차분을 이용해 표준 기저에 대한 계수를 구하는 프로시저 backward_no_normalization
defbackward_no_normalization(D):
n =len(D)
v =[ D[(0,0)]]whilelen(v)< n:
k =len(v)
vnew =[
v[i //2]+(-1)**i * D[(k, i //2)]/2for i inrange(k *2)]
v = vnew
k *=2return v
1-벡터를 2-벡터로, 2-벡터를 4-벡터로, ..., 8-벡터를 16-벡터로 되돌리는 방법은 한 쌍의 엔트리들의 평균과 차분을 이용해 역연산할 수 있습니다. 이는 v[i // 2] + (-1)**i * D[(k, i // 2)] / 2에 잘 구현되어 있습니다.
unnormalize_coefficients, backward_no_normalization를 이용해 표준 기저에 대한 계수를 구하는 프로시저 backward
짐작하셨다시피 프로시저 forward, backward는 표준기저에 대한 계수와 웨이브릿 기저에 대한 계수 사이에서 서로 반대되는 변환 역할을 합니다.
2차원이미지로의 확장
웨이브릿 기저에 대한 이해와 1차원 이미지에 대한 프로시저를 바탕으로 2차원 이미지를 위한 프로시저를 작성해보겠습니다. 압축의 과정은 1차원 이미지의 경우와 같습니다.
표준기저에 대한 계수를 웨이브릿 기저에 대한 계수로 바꾸는 프로시저 forward2d
defdictlist_helper(dlist, k):return[ d[k]for d in dlist ]defforward2d(listlist):
D_list =[ forward(li)for li in listlist ]
L_dict ={}for k in D_list[0]:
L_dict[k]= dictlist_helper(D_list, k)
D_dict ={ k: forward(v)for k, v in L_dict.items()}return D_dict
리스트의 리스트로 받은 인자 listlist를 딕셔너리의 리스트 형태로 바꿉니다. 이 때 forward 프로시저를 이용해 각각의 표준기저에 대한 계수 리스트를 웨이브릿 기저에 대한 계수 딕셔너리로 바꿉니다.
딕셔너리의 리스트를 리스트의 딕셔너리 형태로 바꿉니다. 이 때 프로시저 dictlist_helper를 보조 프로시저로 활용합니다. 이는 행벡터로 이루어진 행렬을 열벡터로 이루어진 행렬로 바꾸는 것과 같습니다.
리스트의 딕셔너리를 딕셔너리의 딕셔너리 형태로 바꿉니다. 이 때 리스트를 딕셔너리로 바꾸는 작업은 forward로 이루어집니다.
2차원 이미지를 억제 압축하는 프로시저 suppress2d
defsuppress2d(D_dict, threshold):for d in D_dict:
D_dict[d]={ k: v ifabs(v)> threshold else0for k, v in D_dict[d].items()}return D_dict
2차원 이미지의 sparsity를 구하는 프로시저 sparsity2d
defsparsity2d(D_dict):
first =[ v for k, v in D_dict.items()][0]returnlen([ v for km, d in D_dict.items()for kn, v in d.items()if v !=0])/(len(D_dict)*len(first.keys()))
웨이브릿 기저에 대한 계수로 부터 표준 기저에 대한 계수를 구하는 프로시저 backward2d
deflistdict2dict(L_dict, i):return{ k: L_dict[k][i]for k in L_dict }deflistdict2dictlist(L_dict):
li =[ v for k, v in L_dict.items()][0]
D_list =[]for i, v inenumerate(li):
D_list.append(listdict2dict(L_dict, i))return D_list
defbackward2d(dictdict):
listdict ={ k: backward(v)for k, v in dictdict.items()}
dictlist = listdict2dictlist(listdict)
listlist =[ backward(d)for d in dictlist ]return listlist