딥러닝 AI를 이용한 항공영상의 해상도 강화

딥러닝 신경망을 이용하여 항공영상의 해상도를 강화하는 GeoAI의 기술중 하나를 소개합니다. 흔히 슈퍼 레졸루션(Super Resolution)이라 불리며, 25cm 급 해상도의 항공영상을 통해 학습 DB를 구축하여 Super Resoultion에 대한 신경망을 학습한 후 그 결과를 정리하였습니다.

저해상도 영상을 고해상도 영상으로 만들기 위한, 신경망을 활용하기 이전의 방법은 인접 픽셀간의 보간을 통한 Bicubic Interpolation 방식, 이미지 데이터베이스에서 유사한 장면의 이미지를 선택해 그 이미지를 결과로 하는 Best Scene Match 방식, 좀더 큰 유사한 패턴을 가져와 작은 패턴 위치에 붙여 넣는 Self-Similarity 기반 방식 등이 있었고, 그 후 신경망을 활용하여 더욱 성능이 향상된, 또한 보다 범용적으로 활용할 수 있게 되었습니다.

위의 그림(출처: EXTREMETECH)은 Orignal 영상을 2배 확대했을 때, 가장 가까운 픽셀을 활용한 보간법을 통한 방법인 Nearest Neighbor 방식과 Super Resolution 방식의 결과 비교입니다. 바로 이러한 Super Resolution을 저해상도의 항공영상에 적용하여 더 높은 해상도를 가지는 영상을 생성하는데 활용할 수 있습니다.

딥러닝 신경망을 활용한 해상도 강화는 Convolutional Layer를 사용하는 CNN을 이용한 모델로 시작하여, Skip-Connection 기법(잔차 연결;Residual-Connection 또는 숏컷;Short-Cut이라고도 함)을 적용한 VDSR 모델로 발전하였고, 다음에는 적대적 생성 신경망인 GAN을 활용한 SRGAN 모델이 연구 되었습니다. VDSR 모델은 기본적으로 CNN의 사용에 Skip-Connection 기법을 추가한 것이고, SRGAN 모델은 CNN 및 VDSR에서 의미 있게 사용한 Skip-Connection 기법을 Generator에 적용한 GAN 방식으로, 이전의 기반 기술을 새로운 기술에서 보다 효과적으로 활용하고 있음을 알 수 있습니다.

Super Resolution은 저해상도의 이미지를 고해상도의 이미지로 만들어 주는 기술입니다. 이를 위한 신경망 학습에는 저해상도의 이미지와 해당 저해상도 이미지에 대한 고해상도 이미지가 필요 합니다. 이를 위해 먼저 많은 고해상도 이미지를 작은 크기로 줄여 저해상도 이미지로 만들 수 있고, 이렇게 만든 저해상도 이미지를 입력 데이터로, 원래의 고해상도 데이터를 정답인 레이블 데이터로 사용하게 됩니다. 즉, 레이블 데이터는 사람이 별도로 구축하지 않고, 학습시 자동으로 생성해 낼 수 있어 더욱 활용도가 매우 높은 기술입니다.

이 글은 적대적 생성 신경망인 GAN을 이용하여 항공영상의 해상도를 향상시키는 신경망에 대한 구현 사례에 대한 결과를 소개하는데 초점을 맞췄으며, GAN에 대한 보다 상세한 기술적 내용과 구현은 다음 글을 참고하기 바랍니다.

적대적 생성 신경망 GAN

GAN은 기본적으로 생성자(Generator)와 판별자(Discriminator)에 대한 모델 2개가 필요하며, 본 사례에서 사용한 사용한 신경망 모델은 다음과 같습니다.

위의 Generator와 Discriminator 신경망 모델의 학습에는 25cm급 항공영상을 1/8 크기로 줄여 해상도를 대폭 낮췄습니다. 즉, 25cm 급을 200cm 급으로 낮춘 것으로 샘플 중 3가지만 언급하면 다음과 같습니다.

원본 이미지에 비해 그 품질이 현격히 낮아진 것을 확인할 수 있습니다. 이처럼 낮아진 저해상도 영상을 상대적으로 더 향상시키고자 먼저 보유하고 있는 항공영상을 768×768 픽셀 크기로 잘라내어, 총 3766개의 학습 영상을 구축했습니다. 이 학습 데이터를 활용하여 총 50 번의 신경망 학습을 수행했을 때, 결과는 다음과 같았습니다. 아래의 결과는 신경망이 학습시에 한번도 보지 못한 영상에 대한 결과입니다. 각 줄의 첫번째가 입력값인 저해상도 영상이고 세번째가 저해상도 영상을 Super Resolution으로 향상시킨 결과입니다. 가운데 이미지는 저해상도 이미지를 생성해 내기 위한 원본 이미지로써 신경망 학습 시 정답으로 사용되는 레이블 데이터입니다.

위의 결과는 GAN 방식을 이용한 Super Resolution입니다. 이외에 기본적인 CNN만을 사용한 모델과 학습을 좀더 잘되게 하기 위해 Skip-Connection 기법을 적용한 VDSR 등이 있다고 앞서 언급했습니다. 이 셋 모두 저해상도 이미지를 고해상도 이미지로 변환하기 위해 이미지의 크기를 확대하기 위한 방법으로 Transposed Convolutional 방식과 Sub-Pixel 방식이 있어, 대상이 되는 이미지의 성격에 따라 어떤 모델을, 세부적으로는 모델을 구성하는 레이어를 어떻게 구성할지를 결정해야 합니다. 또한 최적의 하이퍼파라메터도 반복적인 학습 및 검증을 통해 다양하게 조정해야 합니다.

또한 구슬이 서말이라도 꿰어야 보배라고 하듯이 Super Resolution을 이용한 항공영상이나 위성영상의 해상도를 보강하는 기술이 실제 상황에 사용하기 위해서는 단순이 딥러닝의 신경망 학습과 의미있는 결과 도출만으로는 충분하지 않습니다. 이러한 신경망의 의미있는 결과를 실제 대용량의 영상에 적용하고 실제 사용자가 사용할 수 있는 효과적 UI과 성능을 갖춘 어플리케이션으로 개발되어야 할 것입니다.

다중분류를 위한 대표적인 손실함수, torch.nn.CrossEntropyLoss

딥러닝의 많은 이론 중 가장 중요한 부분이 손실함수와 역전파입니다. PyTorch에서는 다양한 손실함수를 제공하는데, 그 중 torch.nn.CrossEntropyLoss는 다중 분류에 사용됩니다. torch.nn.CrossEntropyLoss는 nn.LogSoftmax와 nn.NLLLoss의 연산의 조합입니다. nn.LogSoftmax는 신경망 말단의 결과 값들을 확률개념으로 해석하기 위한 Softmax 함수의 결과에 log 값을 취한 연산이고, nn.NLLLoss는 nn.LogSoftmax의 log 결과값에 대한 교차 엔트로피 손실 연산(Cross Entropy Loss|Error)입니다.

Softmax와 교차 엔트로피 손실 연산에 대한 각각의 설명은 아래와 같습니다.

활성화 함수(Activation Function)

손실함수(Loss Function)

참고로 nn.NLLLoss의 구현 코드는 아래와 같습니다.

import torch

def NLLLoss(logs, targets):
    out = torch.zeros_like(targets, dtype=torch.float)
    for i in range(len(targets)):
        out[i] = logs[i][targets[i]]
    return -out.sum()/len(out)

물론 PyTorch에서도 torch.nn.NLLLoss를 통해 위와 동일한 기능을 제공합니다. 결과적으로 Softmax의 Log 결과를 Cross Entropy Loss 값의 결과를 얻기 위해 3가지 방식이 존재하는데, 아래와 같습니다.

x = torch.Tensor([[0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]])
y = torch.LongTensor([1])

# Case 1
cross_entropy_loss = torch.nn.CrossEntropyLoss()
print(cross_entropy_loss(x, y)) # tensor(2.1438)

# Case 2
log_softmax = torch.nn.LogSoftmax(dim=1)
x_log = log_softmax(x)
print(NLLLoss(x_log, y)) # tensor(2.1438)

# Case 3
nll_loss = torch.nn.NLLLoss()
print(nll_loss(x_log, y)) # tensor(2.1438)

위의 세가지 방식 중 torch.nn.CrossEntropyLoss처럼 연산을 한번에 처리하는 것이 수식이 간소화되어 역전파가 더 안정적으로 이루지므로 실제 사용에 권장됩니다.

torch.nn.CrossEntropyLoss를 이용하여 손실값을 구하는 것에 초점을 맞춰보면.. 먼저 torch.nn.CrossEntropyLoss의 수식은 다음과 같습니다.

    $$loss(x,class)=-\log\biggl(\frac{\exp(x[class])}{\sum_{j}{\exp(x[j])}}\biggr)=-x[class]+\log\biggl(\sum_{j}{\exp(x[j])}}\biggr)$$

위의 수식을 살펴보면 앞서 언급한 것처럼 Softmax와 Log처리 및 Cross Entropy Loss 연산의 조합이라는 것을 알수 있습니다.

torch.nn.CrossEntropyLoss를 코드를 통해 설명하면… 예를들어 신경망 말단에서 총 10개의 값(앞서 언급한 x값)이 출력되었고, 실제 레이블 값(앞서 언급한 y 또는 class)은 1일때에 손실값을 구하는 코드는 아래와 같습니다.

import torch
import torch.nn as nn
import numpy as np

output = torch.Tensor([[0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]])
target = torch.LongTensor([1])

criterion = nn.CrossEntropyLoss()
loss = criterion(output, target)
print(loss) # tensor(2.1438)

참고로 위의 코드에서 사용된 nn.CrossEntropyLoss의 수식을 알고 있으므로 nn.CrossEntropyLoss을 사용하지 않고 직접 손실값을 계산한다면 다음과 같습니다.

import torch
import torch.nn as nn
import numpy as np

output = [0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]
target = [1]
loss = np.log(sum(np.exp(output))) - output[target[0]]
print(loss) # 2.143818427948945

손실값이 필요할 때는 신경망의 학습인데, 학습에서 데이터는 GPU 자원을 최대한 활용하기 위해 배치 단위로 처리됩니다. 즉, 앞서 언급한 것처럼 1개 단위가 아닌 2개 이상의 데이터가 한꺼번에 들어온다는 것입니다. 이에 대한 처리에 대한 예는 다음 코드와 같습니다.

import torch
import torch.nn as nn
import numpy as np

output = torch.Tensor(
    [
        [0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544],
        [0.9457, 0.0195, 0.9846, 0.3231, 0.1605, 0.3143, 0.9508, 0.2762, 0.7276, 0.4332]
    ]
)

target = torch.LongTensor([1, 5])

criterion = nn.CrossEntropyLoss()
loss = criterion(output, target)
print(loss) # tensor(2.3519)

위의 코드를 nn.CrossEntropyLoss()를 사용하지 않고 계산한다면 다음 코드와 같구요.

import torch
import torch.nn as nn
import numpy as np

output = [0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]
target = [1]
loss1 = np.log(sum(np.exp(output))) - output[target[0]]

output = [0.9457, 0.0195, 0.9846, 0.3231, 0.1605, 0.3143, 0.9508, 0.2762, 0.7276, 0.4332]
target = [5]
loss2 = np.log(sum(np.exp(output))) - output[target[0]]

print((loss1 + loss2)/2) # 2.351937720511233

즉, 배치 처리에 대한 손실값은 배치를 구성하는 각 데이터의 손실값들의 평균이라는 점을 확인할 수 있습니다.

파이썬의 matplotlib 노트

파이썬의 matplotlib는 수치 데이터를 그래프로 효과적으로 표시해주는 API입니다. 이에 대해 간단한 활용 예시에 대한 코드를 기록해 둡니다.

import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [1, 2, 3, 4, 5]

plt.scatter(x, y)

plt.show()

X축과 Y축에 대한 포인트 데이터를 표시하는 코드입니다. 결과는 다음과 같습니다.

그래프에서 포인트의 크기와 색상, 투명도를 지정하는 예제는 다음과 같습니다.

import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [1, 2, 3, 4, 5]
s = [10, 20, 30, 40, 50]

plt.scatter(x = x, y = y, s = s, c = 'red', alpha=0.5)

plt.show()

결과는 다음과 같습니다.

다음은 꺽은선 그래프입니다.

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0, 10, 0.5)
y = np.sin(x)

plt.plot(x, y)
plt.show()

x축과 y축의 데이터는 4번과 5번 코드에서 정의합니다. 결과는 다음과 같습니다.

하나의 차트에 여러개의 그래프를 동시에 표시하고, 추가적으로 제목, 축이름 등을 표시하는 코드입니다.

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0, 10, 0.5)
y1 = np.sin(x)
y2 = np.cos(x)

plt.plot(x, y1, label="sin(x)")
plt.plot(x, y2, label="cos(x)", linestyle="--")

plt.xlabel("x")
plt.xlabel("y")

plt.title("sin & cos")
plt.legend()

plt.show()

3차원 차트의 경우 먼저 X, Y축에 대한 데이터와 이 X, Y를 변수로 하여 계산된 Z 값의 함수가 정의해야 합니다. 이렇게 정의된 X, Y, Z에 대한 3차원 그래프는 아래의 예제 코드를 통해 3차원 차트로 시각화할 수 있습니다.

import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
Z = X**2 + Y**2

fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_wireframe(X, Y, Z, color='black')

plt.show()

여러개의 차트를 동시에 표시하는 경우입니다.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, np.pi * 3, 100)
fig, axes = plt.subplots(2,2)

axes[0][0].plot(x, np.sin(x))
axes[0][1].plot(x, np.arccos(x))
axes[1][0].plot(x, np.cos(x))
axes[1][1].plot(x, np.arcsin(x))

plt.show()

아래는 차트를 그리는 스타일을 지정하고 범례를 표현하는 코드입니다.

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-10,10)
y = x**2

plt.plot(x, y, 
    linewidth=2, color='green', linestyle=':', 
    marker='*', markersize=10, markerfacecolor='yellow', markeredgecolor='red', 
    label='y=x^2')
   
plt.legend()

plt.show()

결과는 다음과 같습니다.

[PyQt5] 타이머(Timer) 사용하기

PyQt에서 제공하는 타이머를 사용해 간단한 디지털 시계에 대한 UI를 구성해 보겠습니다. 디지털 시계에 대한 표현은 QLCDNumber라는 위젯을 사용합니다. 아래는 최종 실행 결과입니다.

시작 버튼을 클릭하면, 일정한 주기로 시간 표시가 변경되며 멈춤 버튼을 클릭하면 시간 표시에 대한 갱신을 중단합니다. 멈춤 상태에서 다시 시작 버튼을 클릭하면 시간 표시가 변경되기 시작합니다. 코드는 다음과 같습니다.

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QTimer, QTime

class MyWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.timer = QTimer(self)
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.timeout)

        self.setWindowTitle('QTimer')
        self.setGeometry(100, 100, 600, 280)
 
        layout = QVBoxLayout()
 
        self.lcd = QLCDNumber()
        self.lcd.display('')
        self.lcd.setDigitCount(8)

        subLayout = QHBoxLayout()
        
        self.btnStart = QPushButton("시작")
        self.btnStart.clicked.connect(self.onStartButtonClicked)
 
        self.btnStop = QPushButton("멈춤")
        self.btnStop.clicked.connect(self.onStopButtonClicked)
 
        layout.addWidget(self.lcd)
        
        subLayout.addWidget(self.btnStart)
        subLayout.addWidget(self.btnStop)
        layout.addLayout(subLayout)
 
        self.btnStop.setEnabled(False)

        self.setLayout(layout)        

    def onStartButtonClicked(self):
        self.timer.start()
        self.btnStop.setEnabled(True)
        self.btnStart.setEnabled(False)

    def onStopButtonClicked(self):
        self.timer.stop()
        self.btnStop.setEnabled(False)
        self.btnStart.setEnabled(True)

    def timeout(self):
        sender = self.sender()
        currentTime = QTime.currentTime().toString("hh:mm:ss")

        if id(sender) == id(self.timer):
            self.lcd.display(currentTime)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    sys.exit(app.exec_())

앞서 언급한 타이머의 갱신 시간 주기는 QTimer의 setInterval 함수로 지정하며 단위는 ms로써, 1000이 1초에 해당합니다. 타이머에 대한 갱신 시작과 중지는 각각 start와 stop입니다.