
SSIM (Structural Similarity Index)
2022, Feb 17
- 참조 : https://en.wikipedia.org/wiki/Structural_similarity
- 참조 : https://medium.com/srm-mic/all-about-structural-similarity-index-ssim-theory-code-in-pytorch-6551b455541e
- 참조 : https://bskyvision.com/878
- 참조 : https://walkaroundthedevelop.tistory.com/m/56
- 참조 : https://nate9389.tistory.com/2067
- 참조 : https://medium.com/@sanari85/image-reconstruction-%EC%97%90%EC%84%9C-ssim-index%EC%9D%98-%EC%9E%AC%EC%A1%B0%EB%AA%85-b3ca26434fb1
- 이번 글에서는 두 이미지를 비교하는 지표인
SSIM
에 대하여 다루어 보도록 하겠습니다. SSIM
은Structural Similarity Index
의 약어로 사용되며 주어진 2개의 이미지의similarity(유사도)
를 계산하는 측도로 사용됩니다.SSIM
은 두 이미지의 단순 유사도를 측정하는데 사용하기도 하지만 풀고자 하는 문제가 두 이미지가 유사해지도록 만들어야 되는 문제일 때SSIM
을 Loss Function 형태로 사용하기도 합니다. 왜냐하면SSIM
이 gradient-based로 구현되어 있기 때문입니다.- 딥러닝에서 두 이미지를 유사하게 만드는 문제나 depth estimation시 disparity를 구하기 위하여 이미지를 복원할 때, 두 이미지 또는 두 패치의 유사도를 측정하여 Loss Function을 사용하는 방법이 많이 사용됩니다.
- 따라서 이번 챕터에서는
SSIM
의 원리에 대하여 먼저 알아보고 Pytorch의 구현 방법을 통하여 학습에 사용하는 방법과 skimage를 이용하여 단순히 이미지의 유사도를 측정하는 방법에 대하여 살펴보도록 하겠습니다.
목차
SSIM의 정의
SSIM
은 Structural Similarity Index Measure의 약어로 두 이미지의 유사도를luminance
,contrast
,structure
3가지 요소를 이용하여 비교하는 방법을 의미합니다. 이와 같은 요소를 이용하여 이미지를 비교하는 이유는 실제 인간의 시각 기관도 이와 같은 방법으로 인식하기 때문입니다.SSIM
의 최종 결과는 0 ~ 1 사이이며 1에 가까울수록 두 이미지가 유사함을 의미합니다. 그러면luminance
,contrast
,structure
가 각각 어떻게 계산되어서 하나로 합쳐지는 지 살펴보도록 하곘습니다. 입력값의 범위에 따라서 -1 ~ 1 사이의 값을 가질수도 있으며 1에 가까울수록 두 이미지가 유사한 것은 동일합니다.
Luminance
luminance
는 한글로 휘도라고 하며 빛의 밝기를 나타내는 양입니다. SSIM에서 계산할 때, 별도 빛의 밝기 성분을 추출해서 사용하지는 않고 이미지의 픽셀값을 이용합니다. (픽셀 값이 클수록 밝음을 이용함) grayscale 이미지에서는 각 픽셀의 값을 의미하며 RGB 이미지에서는 R, G, B 각 채널 별 픽셀 값을 의미합니다.
- μx=1NN∑i=1xiμx=1NN∑i=1xi
-
xixi : 각 픽셀의 값 (밝기 값을 의미함)
-
NN : 전체 픽셀의 갯수
- μxμx : 이미지의 평균
luminance
- 두 이미지의
luminance
가 얼마나 다른 지 비교하기 위해 μxμx 값을 이용합니다. 두 이미지를 x,yx,y 라고 할 때 두 이미지의luminance
를 비교하기 위한 식은 다음과 같습니다.
- l(x,y)=2μxμy+C1μ2x+μ2y+C1l(x,y)=2μxμy+C1μ2x+μ2y+C1(1)
- 식 (1)의 C1C1 을 제외하고 살펴보면 2μxμyμ2x+μ2y2μxμyμ2x+μ2y 가 되며 μx,μyμx,μy 각 같으면 1이 되고 두 값이 차이가 많이 날수록 0에 가까워 집니다. 이와 같은 성질을 이용하여 두 값의 차이에 따른 값의 범위를 0 ~ 1로 계산될 수 있도록 합니다.
- 식 (1)에서 C1C1 의 사용 용도는 분모에 0이 되는 것을 방지하기 위하여 안정성을 위해 추가하였고 값의 정의 방법은 아래 식을 따르는 것으로 알려져 있습니다. (하지만 크게 중요하지 않으니 적당한 상수값을 사용하여도 무관합니다.)
- C1=(K1L)2C1=(K1L)2
- 위 식에서 K1K1 는 일반 상수이며 보통 0.01을 많이 사용합니다. LL 은 픽셀값의 범위를 입력하며 일반적으로 8비트 값을 사용하여 0 ~ 255의 픽셀 값을 사용하므로 255를 LL 로 사용합니다.
- 따라서 C1=(0.01×255)2=6.5025C1=(0.01×255)2=6.5025 를 사용합니다.
Contrast
contrast
는 한글로 대조라고 하며 이미지 내에서 빛의 밝기가 바뀌는 정도를 나타내는 양입니다. 이 값은 픽셀 간의 값이 얼마나 차이가 나는 지 통하여 정량화 할 수 있으므로 표준 편차를 사용합니다.
- σx=(1N−1N∑i=1(xi−μx)2)1/2σx=(1N−1N∑i=1(xi−μx)2)1/2
-
N−1N−1 : 표본의 표준 편차를 구하기 때문에 표본의 표준편차가 모표준 편차가 될 수 있도록 하기 위해 N−1N−1을 사용합니다.
- σxσx : 이미지의 픽셀 간 표준편차로
contrast
를 의미합니다.
- 두 이미지의
contrast
성분을 비교하기 위해서는 σxσx 를 사용합니다. 상세식은luminance
의 l(x,y)l(x,y) 와 동일합니다.
- c(x,y)=2σxσy+C2σ2x+σ2y+C2c(x,y)=2σxσy+C2σ2x+σ2y+C2(2)
- 위 식에서도
luminance
의 경우와 동일하게 두 이미지의contrast
성분이 같을 수록 1에 가깝고 다를수록 0에 가까워집니다. - 식 (2)의 C2C2 의 경우 C2=(K2L)2C2=(K2L)2 로 구하며 K2=0.03K2=0.03 을 주로 사용하여 다음과 같은 값을 가집니다.
- C2=(K2L)2=(0.03×255)2=58.5225C2=(K2L)2=(0.03×255)2=58.5225
Structure
- 마지막으로
structure
는 픽셀값의 구조적인 차이점을 나타내며 정성적으로 성분을 확인 시, edge를 나타냅니다.sturucture
를 구하기 위하여luminance
을 평균,contrast
를 표준 편차로 이용하여 Normalized된 픽셀 값의 분포에서 픽셀 값을 다시 정의합니다.
- (X−μx)/σx(X−μx)/σx
- XX : 입력 이미지
- 두 이미지의
structure
성분의 유사성을 확인하는 것은 두 이미지의correlation
을 이용하것과 같은 의미를 지닙니다.
- corr(X,Y)=σxyσxσy=E[(x−μx)(y−μy)]σxσy=E[(x−μx)(y−μy)σxσy]corr(X,Y)=σxyσxσy=E[(x−μx)(y−μy)]σxσy=E[(x−μx)(y−μy)σxσy]
- =E[(x−μx)σx(y−μy)σy]=E[(x−μx)σx(y−μy)σy]
- 따라서 두 이미지의 correlation을 구하는 것은 각 이미지의
structure
성분의 곱의 평균을 구하는 것과 같고structure
성분이 같은 방향으로 커지면 1에 가까워지는 성질을 이용하여 앞선luminance
,contrast
와 동일하게 이용할 수 있습니다. - 따라서 두 이미지의
structure
를 비교하는 함수를 다음과 같이 정의할 수 있습니다.
- s(x,y)=σxy+C3σxσy+C3s(x,y)=σxy+C3σxσy+C3(3)
- σxy=1N−1N∑i=1(xi−μx)(yi−μy)
- 이 때, C3 는 식의 편의상 C2/2 로 사용합니다. 그 이유는
SSIM
은 l(x,y),c(x,y),s(x,y) 의 곱으로 정의되는 데 C3=C2/2 로 정의하면 식을 간편화 할 수 있습니다. 이는 이후 식을 전개하면서 보여드리겠습니다.
- 이와 같이
luminance
,contrast
,strucrue
를 모두 반영한 이미지의 유사도를 결정하는SSIM
은 다음과 같이 정의됩니다.
- SSIM(x,y)=l(x,y)α⋅c(x,y)β⋅s(x,y)γ
- 식 (4)에서 α,β,γ>0 이면
luminance
,contrast
,structure
에 상대적인 중요도를 설정할 수 있습니다. - 앞에서 설명한 대로 식을 간소화 하기 위하여 α=β=γ=1 로 두고 C3=C2/2 로 하여 식을 전개해 보겠습니다.
- SSIM(x,y)=l(x,y)⋅c(x,y)⋅s(x,y)=2μxμy+C1μ2x+μ2y+C12σxσy+C2σ2x+σ2y+C2σxy+C2/2σxσy+C2/2
- =(2μxμy+C1)(2σxy+C2)(μ2x+μ2y+C1)(σ2x+σ2y+C2)
SSIM
은 symmetry 성질은 만족하여 x, y 이미지의 순서를 바꿔도 됩니다. 하지만 triangle inequality ( |a+b|≤|a|+|b| ) 를 만족하지 않아서 distance를 구하기 위한 함수로는 사용할 수 없습니다.SSIM
을 좀 더 효과적으로 사용하기 위해서는 이미지 전체를 한번에 비교하기 보다는 N x N 윈도우를 이용하여 (ex. 8 X 8, 11 X 11) 지역적으로 비교하여 사용하는 것이 효과적입니다. 왜냐하면 이미지의 왜곡이나 통계적 특성이 이미지 전반에 걸쳐서 나타나는 경우보다 지역적으로 나타나는 경우가 많고 지역적으로 더 다양한 특성을 분석할 수 있기 때문입니다.- 이와 같은 방법으로 지역적으로
SSIM
을 구하려면 윈도우를 이용하여 슬라이딩 윈도우를 적용하여 각 부분의 값을 구해야 합니다. 이와 관련된 구현 내용으로 아래SSIM의 Pytorch에서의 사용법 (locally)
을 참조해 주시기 바랍니다.
- 만약
SSIM
을 Loss로 사용하려면 아래와 같이 식을 변경해서 사용하면 됩니다.
- LSSIM=1−SSIM(x,y)
SSIM
은 미분 가능하므로 Loss로 사용 가능하며 0 ~ 1 사이의 스코어 값을 1에서 빼주면 두 이미지의 유사도가 낮을수록 값이 커지기 때문에 Loss로 사용할 수 있습니다.
- 만약 RGB 이미지에서 SSIM을 적용해야 한다면 각 채널 별로 SSIM을 구한 후 모두 합해주면 됩니다. 식으로 나타내면 아래와 같습니다.
- SSIMrgb=wrSSIMr(I1,I2)+wgSSIMg(I1,I2)+wbSSIMb(I1,I2)
- 특정 채널에 대하여 더 가중치를 줄 수도 있으며 일반적으로
RGB
에 1/3 씩 균등하게 가중치를 주어서 사용합니다. YCrCb
에서는Y
에 0.8,Cr
,Cb
에 각각 0.1을 주어서 사용하기도 합니다. (링크 참조)
SSIM의 Pytorch에서의 사용법 (globally)
- 먼저 이미지 전체에서 한번에
SSIM
을 구하는 방법을 Pytorch을 이용해서 구해보도록 하곘습니다. 이와 같은 방법으로SSIM
을 구하면 간단하게 전체 이미지에 대하여SSIM
의 Score 또는SSIM
을 이용한Loss
를 구할 수 있습니다.
class SSIM(nn.Module):
"""Layer to compute the SSIM loss between a pair of images
"""
def __init__(self):
super(SSIM, self).__init__()
self.mu_x_pool = nn.AvgPool2d(3, 1)
self.mu_y_pool = nn.AvgPool2d(3, 1)
self.sig_x_pool = nn.AvgPool2d(3, 1)
self.sig_y_pool = nn.AvgPool2d(3, 1)
self.sig_xy_pool = nn.AvgPool2d(3, 1)
# 입력 경계의 반사를 사용하여 상/하/좌/우에 입력 텐서를 추가로 채웁니다.
self.refl = nn.ReflectionPad2d(1)
self.C1 = 0.01 ** 2
self.C2 = 0.03 ** 2
def forward(self, x, y):
# shape : (xh, xw) -> (xh + 2, xw + 2)
x = self.refl(x)
# shape : (yh, yw) -> (yh + 2, yw + 2)
y = self.refl(y)
mu_x = self.mu_x_pool(x)
mu_y = self.mu_y_pool(y)
sigma_x = self.sig_x_pool(x ** 2) - mu_x ** 2
sigma_y = self.sig_y_pool(y ** 2) - mu_y ** 2
sigma_xy = self.sig_xy_pool(x * y) - mu_x * mu_y
SSIM_n = (2 * mu_x * mu_y + self.C1) * (2 * sigma_xy + self.C2)
SSIM_d = (mu_x ** 2 + mu_y ** 2 + self.C1) * (sigma_x + sigma_y + self.C2)
# SSIM score
return torch.clamp((SSIM_n / SSIM_d) / 2, 0, 1)
# Loss function
# return torch.clamp((1 - SSIM_n / SSIM_d) / 2, 0, 1)
SSIM의 Pytorch에서의 사용법 (locally)
- 이번에는
convolution
연산을 이용하여 로컬한 영역에서SSIM
을 구하는 방법에 대하여 Pytorch 코드를 통해 알아보도록 하겠습니다. - 앞에서 설명 드렸듯이, 이미지의 특성상 이미지 전체를 한번에 계산하는 것 보다 local한 영역을 기준으로 계산하는 것이 이미지의 각 부분 별 특성을 비교할 수 있습니다.
import torch
import torch.nn.functional as F
import numpy as np
import math
import cv2
def gaussian(window_size, sigma):
"""
Generates a list of Tensor values drawn from a gaussian distribution with standard
diviation = sigma and sum of all elements = 1.
Length of list = window_size
"""
gauss = torch.Tensor([math.exp(-(x - window_size//2)**2/float(2*sigma**2)) for x in range(window_size)])
return gauss/gauss.sum()
def create_window(window_size, channel=1):
# Generate an 1D tensor containing values sampled from a gaussian distribution
# _1d_window : (window_size, 1)
# sum of _1d_window = 1
_1d_window = gaussian(window_size=window_size, sigma=1.5).unsqueeze(1)
# Converting to 2D : _1d_window (window_size, 1) @ _1d_window.T (1, window_size)
# _2d_window : (window_size, window_size)
# sum of _2d_window = 1
_2d_window = _1d_window.mm(_1d_window.t()).float().unsqueeze(0).unsqueeze(0)
# expand _2d_window to window size
# window : (channel, 1, window_size, window_size)
window = torch.Tensor(_2d_window.expand(channel, 1, window_size, window_size).contiguous())
return window
def ssim(img1, img2, window_size=11, val_range=255, window=None, size_average=True, full=False):
# L is the dynamic range of the pixel values (255 for 8-bit grayscale images),
L = val_range
try:
_, channels, height, width = img1.size()
except:
channels, height, width = img1.size()
# if window is not provided, init one
if window is None:
# window should be at least 11x11
real_size = min(window_size, height, width)
window = create_window(real_size, channel=channels).to(img1.device)
# calculating the mu parameter (locally) for both images using a gaussian filter
# calculates the luminosity params
pad = window_size//2
mu1 = F.conv2d(img1, window, padding=pad, groups=channels)
mu2 = F.conv2d(img2, window, padding=pad, groups=channels)
mu1_sq = mu1 ** 2
mu2_sq = mu2 ** 2
mu12 = mu1 * mu2
# now we calculate the sigma square parameter
# Sigma deals with the contrast component
sigma1_sq = F.conv2d(img1 * img1, window, padding=pad, groups=channels) - mu1_sq
sigma2_sq = F.conv2d(img2 * img2, window, padding=pad, groups=channels) - mu2_sq
sigma12 = F.conv2d(img1 * img2, window, padding=pad, groups=channels) - mu12
# Some constants for stability
C1 = (0.01 ) ** 2 # NOTE: Removed L from here (ref PT implementation)
C2 = (0.03 ) ** 2
contrast_metric = (2.0 * sigma12 + C2) / (sigma1_sq + sigma2_sq + C2)
contrast_metric = torch.mean(contrast_metric)
numerator1 = 2 * mu12 + C1
numerator2 = 2 * sigma12 + C2
denominator1 = mu1_sq + mu2_sq + C1
denominator2 = sigma1_sq + sigma2_sq + C2
ssim_score = (numerator1 * numerator2) / (denominator1 * denominator2)
if size_average:
ret = ssim_score.mean()
else:
ret = ssim_score.mean(1).mean(1).mean(1)
if full:
return ret, contrast_metric
return ret
- 위 코드에서
gaussian
함수는 가우시안 분포를 출력합니다. 다음과 같습니다.
gauss_dis = gaussian(11, 1.5)
print("Distribution: ", gauss_dis)
# Distribution: tensor([0.0010, 0.0076, 0.0360, 0.1094, 0.2130, 0.2660, 0.2130, 0.1094, 0.0360, 0.0076, 0.0010])
print("Sum of Gauss Distribution:", torch.sum(gauss_dis))
# Sum of Gauss Distribution: tensor(1.)
- 위 가우시안 분포를 이용하여 이미지의 local 영역을 순회하는
window
를 생성해야 합니다. 이 때,create_window
함수를 사용합니다. create_window
함수를 보면 동일한 값의 1D gaussian distribution (N) 을 cross product하여 (N, N) 크기의 2D로 window로 만들고 channel 수 만큼 복사하여 확장합니다.- 아래 코드에서는 (11, 11) 크기의 window 를 channel 방향으로 3 만큼 복사합니다.
window = create_window(11, 3)
print(window.shape)
# torch.Size([3, 1, 11, 11])
F.conv2d
연산을 보면 생성된 window를 순회하면서 local 영역의 평균과 표준편차를 구하도록 되어있습니다.F.conv2d
의 첫번째 인자는 입력으로(B, C, H, W)
의 크기를 가지고 두번째 인자는 convolution 연산을 위한 weight로(#num_filter, channel, H, W)
의 크기를 가집니다.window = create_window(11, 3)
으로 window를 생성하면 사이즈가torch.Size([3, 1, 11, 11])
가 되므로 (channel=1, H=11, W=11) 크기의 필터가 3개 있다는 뜻입니다.- 이와 같은 입력과 필터의 연산으로 평균과 표준편차를 local 영역 단위로 구하여 전체 SSIM을 구하게 됩니다.

- 위 이미지의 가장 왼쪽부터 첫번째 이미지가 원본이고 2 ~ 4번째 이미지는 원본 이미지에 노이즈를 추가한 케이스 입니다.
- 위 코드를 이용하여 (원본, 두번째 이미지), (원본, 세번째 이미지), (원본, 네번째 이미지) 간의 SSIM을 구하면 아래와 같습니다.
load_images = lambda path, h, w: cv2.resize(cv2.cvtColor(cv2.imread(path, cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), ((w, h)))
tensorify = lambda x: torch.Tensor(x.transpose((2, 0, 1))).unsqueeze(0).float().div(255.0)
img_tensor = tensorify(load_images("origin.png", 400, 300))
noise1_tensor = tensorify(load_images("noise1.png", 400, 300))
noise2_tensor = tensorify(load_images("noise2.png", 400, 300))
noise3_tensor = tensorify(load_images("noise3.png", 400, 300))
print(ssim(img_tensor, noise1_tensor), ssim(img_tensor, noise2_tensor), ssim(img_tensor, noise3_tensor))
# tensor(0.2894) tensor(0.7756) tensor(0.8177)
- 위 코드 결과 원본 이미지와 가장 마지막 이미지가 SSIM 기준으로 가장 유사한 것을 확인할 수 있습니다.
SSIM의 skimage에서의 사용법
- 참조 : https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.structural_similarity
- 마지막으로
skimage
를 이용하여 구하는 방법에 대하여 알아보도록 하겠습니다. 학습에 사용이 아니라 단순히SSIM
스코어를 구하는 데 사용하려면 아래 코드와 같이 간단히 사용해도 무방합니다.

- 위 그림과 같이 4개의 이미지가 있고 가장 왼쪽이 원본 이미지인
origin
, 그리고 그 다음 이미지 부터는 노이즈가 섞여있는 이미지로 각각 noise1, noise2, noise3 이라고 해보겠습니다. - 아래 코드에서
ssim
함수에서channel_axis
는 RGB와 같이 채널이 여러개 존재할 때, 그 채널의 axis 인덱스를 명시하는 것이며full
은 SSIM 스코어 뿐 아니라 평균을 구하기 이전의 픽셀 단위의 계산 결과도 모두 포함하여 출력하도록 합니다. 이 중간 출력 결과를 통해 두 이미지에 어떤 차이가 있는 지 시각화 해서 볼 수 있습니다. win_size
옵션을 추가로 입력하면 로컬 영역에서 SSIM을 구하도록 할 수 있습니다.
import cv2
import matplotlib.pyplot as plt
from skimage.metrics import structural_similarity as ssim
origin = cv2.cvtColor(cv2.imread("origin.png"), cv2.COLOR_BGR2RGB)
noise1 = cv2.cvtColor(cv2.imread("noise1.png"), cv2.COLOR_BGR2RGB)
noise2 = cv2.cvtColor(cv2.imread("noise2.png"), cv2.COLOR_BGR2RGB)
noise3 = cv2.cvtColor(cv2.imread("noise3.png"), cv2.COLOR_BGR2RGB)
ssim_1, diff1 = ssim(origin, noise1, channel_axis=2, full=True)
diff1 = (diff1 * 255).astype("uint8")
# plt.imshow(diff1)
ssim_2, diff2 = ssim(origin, noise2, channel_axis=2, full=True)
diff2 = (diff2 * 255).astype("uint8")
ssim_3, diff3 = ssim(origin, noise3, channel_axis=2, full=True)
diff3 = (diff3 * 255).astype("uint8")
print(ssim_1, ssim_2, ssim_3)
# 0.21075336301148573 0.6888119020545118 0.7808179172891382
ssim_1, diff1 = ssim(origin, noise1, channel_axis=2, win_size=11, full=True)
diff1 = (diff1 * 255).astype("uint8")
ssim_2, diff2 = ssim(origin, noise2, channel_axis=2, win_size=11, full=True)
diff2 = (diff2 * 255).astype("uint8")
ssim_3, diff3 = ssim(origin, noise3, channel_axis=2, win_size=11, full=True)
diff3 = (diff3 * 255).astype("uint8")
print(ssim_1, ssim_2, ssim_3)
# 0.23226598957553168 0.7078116166774144 0.7831195478428952
- 위 SSIM의 결과를 보면 원본 이미지와 노이즈 추가 이미지3의 SSIM이 0.78로 가장 유사하며 원본 이미지와 노이즈 추가 이미지1의 SSIm이 0.21(0.23)으로 가장 차이가 있는 것을 확인할 수 있습니다.