딥러닝을 사용한 Image Inpainting 소개
- image inpainting 소개
- image inpainting을 수행하는 다양한 방법
- 기존의 컴퓨터 비전 기반 접근법
- 딥러닝 기반 접근법 (Vanilla 오토인코더, Partial convolutions(부분 합성곱))
- 향후 방향 및 엔딩 노트
image inpainting이란 무엇인가요?

image inpainting 수행하기: 전통적인 방식
- 주변 영역에 누락된 부분을 채우기에 적합한 정보가 (픽셀 읽기) 없을 수 있습니다.
- 누락된 영역은 렌더링할 객체의 속성을 추론하기 위해 인페인팅 시스템이 필요합니다.
- Navier-Stokes(나비에-스토크스) 방법: 이는 2021년 (논문) 거슬러 올라갑니다. 이 방법은 유체역학 및 편미분 방정식(partial differential equations) 개념을 통합하고 있습니다. 이는 이미지의 가장자리는 본질적으로 연속적이어야 한다는 사실에 기초하고 있습니다. 다음 그림을 살펴보시기 바랍니다 -

- 연속성 제약(continuity constraint)과 함께 (가장자리와 같은 특징을 유지하는 방법을 말하는 다른 방식), 저자들은 인페인팅이 필요한 가장자리의 주변 영역에서 색 정보를 가져왔습니다.
- Fast marching method(패스트 마칭 방법): 2004년, Alexandru Telesa는 이 아이디어를 이 논문에 제시했으며, 다음의 사항을 제안했습니다:
- 누락된 픽셀 추정을 위해, 픽셀 주변(neighborhood)에서 정규화된 가중치 합(normalized weighted sum of pixels)을 가져옵니다. 이 주변은 boundary(경계)에 의해 매개변수화(parameterize)되고, 픽셀 세트가 인페인팅되면 이 경계가 업데이트됩니다.
- 픽셀의 색을 추정하기 위해, 주변 픽셀의 gradients(경사)가 사용됩니다. 이 두 가지 방법이 생성할 수 있는 결과를 확인하고 싶으시면, 이 기사를 참조하시기 바랍니다. 이제 기존의 image inpainting 방식에 익숙해졌으므로, 현대적인 방식, 즉, 딥러닝을 활용한 방식으로 어떻게 이를 수행하는지 살펴보겠습니다.
image inpainting 수행하기: 현대적 방식
CIFA10 데이터세트를 포함한 간단한 image inpainting 모델
간단한 데이터세트를 선택한 이유는 무엇인가요?
데이터 준비

class createAugment(keras.utils.Sequence):# Generates masked_image, masks, and target images for trainingdef __init__(self, X, y, batch_size=32, dim=(32, 32),n_channels=3, shuffle=True):# Initialize the constructorself.batch_size = batch_sizeself.X = Xself.y = yself.dim = dimaself.n_channels = n_channelsself.shuffle = shuffleself.on_epoch_end()def __len__(self):# Denotes the number of batches per epochreturn int(np.floor(len(self.X) / self.batch_size))def __getitem__(self, index):# Generate one batch of dataindexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]# Generate dataX_inputs, y_output = self.__data_generation(indexes)return X_inputs, y_outputdef on_epoch_end(self):# Updates indexes after each epochself.indexes = np.arange(len(self.X))if self.shuffle:np.random.shuffle(self.indexes)
def __data_generation(self, idxs):# Masked_images is a matrix of masked images used as inputMasked_images = np.empty((self.batch_size, self.dim[0], self.dim[1], self.n_channels)) # Masked image# Mask_batch is a matrix of binary masks used as inputMask_batch = np.empty((self.batch_size, self.dim[0], self.dim[1], self.n_channels)) # Binary Masks# y_batch is a matrix of original images used for computing error from reconstructed imagey_batch = np.empty((self.batch_size, self.dim[0], self.dim[1], self.n_channels)) # Original image## Iterate through random indexesfor i, idx in enumerate(idxs):image_copy = self.X[idx].copy()## Get mask associated to that imagemasked_image, mask = self.__createMask(image_copy)## Append and scale down.Masked_images[i,] = masked_image/255Mask_batch[i,] = mask/255y_batch[i] = self.y[idx]/255return [Masked_images, Mask_batch], y_batch
아키텍처
Vanilla Convolutional 오토인코더

def dice_coef(y_true, y_pred):y_true_f = keras.backend.flatten(y_true)y_pred_f = keras.backend.flatten(y_pred)intersection = keras.backend.sum(y_true_f * y_pred_f)return (2. * intersection) / (keras.backend.sum(y_true_f + y_pred_f))
여러 epoch 또는 단계에 걸쳐 모델이 누락된 구멍을 채우는 방법을 어떻게 학습하는지를 지켜보는 것은 흥미롭지 않습니까?
class PredictionLogger(tf.keras.callbacks.Callback):def __init__(self):super(PredictionLogger, self).__init__()# The callback will be executed after an epoch is completeddef on_epoch_end(self, logs, epoch):# Pick a batch, and sample the masked images, masks, and the labelssample_idx = 54[masked_images, masks], sample_labels = testgen[sample_idx]# Initialize empty lists store intermediate resultsm_images = []binary_masks = []predictions = []labels = []# Iterate over the batchfor i in range(32):# Our inpainting model accepts masked imaged and masks as its inputs,# then use perform inferenceinputs = [B]impainted_image = model.predict(inputs)# Append the results to the respective listsm_images.append(masked_images[i])binary_masks.append(masks[i])predictions.append(impainted_image.reshape(impainted_image.shape[1:]))labels.append(sample_labels[i])# Log the results on wandb run page and voila!wandb.log({"masked_images": [wandb.Image(m_image)for m_image in m_images]})wandb.log({"masks": [wandb.Image(mask)for mask in binary_masks]})wandb.log({"predictions": [wandb.Image(inpainted_image)for inpainted_image in predictions]})wandb.log({"labels": [wandb.Image(label)for label in labels]})
Partial convolutions(부분 합성곱)
이제 Vanilla CNN의 강력한 대안으로 Image Inpainting for Irregular Holes Using Partial Convolutions(부분 합성곱을 사용하여 불규칙한 이미지 인페인팅)에 대해 이야기해보겠습니다. 이미지의 구멍과 같은 누락된 데이터를 채우기 위해 Partial convolution이 제안되었습니다. 원래의 공식은 다음과 같습니다 – X는 현재 슬라이딩 (컨볼루션) 윈도우의 특징값이고, M은 해당 이진 마스크(binary mask)라고 가정합니다. 구멍은 0으로, 구멍이 아닌 것은 1로 나타낸다고 합시다. 수학적으로 partial convolution은 다음과 같이 나타낼 수 있습니다:
scaling factor sum(1)/sum(M)은 유효한 (마스킹 되지 않은) 입력의 다양한 양을 조정하기 위해 적절한 스케일링을 적용합니다. 각 partial convolution operation (부분 합성곱 연산) 후, 다음과 같이 마스크를 업데이트합니다: convolition이 하나 이상의 유효한 입력 (특징) 값에 대한 출력을 조정할 수 있는 경우, 해당 위치가 유효현 것으로 표시합니다. 이는 다음과 같이 표현할 수 있습니다:
입력이 유효한 픽셀을 포함하고 있는 경우, 여러 partial convolution 레이어를 통해 모든 마스크는 결국 모두 1이 됩니다. image inpainting 작업에서 Vanilla CNN을 partial convolution 레이어로 대체하려면 동일한 구현이 필요합니다.
안타깝게도, TensorFlow 및 Pytorch에는 공식적인 구현이 없기 때문에, 커스텀 레이어를 직접 구현해야 합니다. 이 커스텀 레이어를 구축하는 방법을 설명하고 있는 TensorFlow 튜토리얼이 바로 그 훌륭한 시작점입니다. 다행히도 저는 여기서 partial convolution의 Keras 구현을 찾을 수 있었습니다. 이 코드베이스는 제가 TF 2.x를 사용하도록 업그레이드한 Keras 백엔드로 TF 1.x을 사용했습니다. 저희는 이 블로그 게시물에 대한 GitHub repo와 함께 이 업그레이드된 구현을 제공해왔습니다. 여기서 PConv2D layer를 확인하시기 바랍니다.
코드로 모델을 구현하고, CIFAR 10 데이터세트에서 훈련해보겠습니다. 저희는 inpaintingModel 클래스를 구현했습니다. 모델을 빌드하려면 prepare_model()
방법을 호출해야 합니다.
def prepare_model(self, input_size=(32,32,3)):
input_image = keras.layers.Input(input_size)
input_mask = keras.layers.Input(input_size)
conv1, mask1, conv2, mask2 = self.__encoder_layer(32, input_image, input_mask)
conv3, mask3, conv4, mask4 = self.__encoder_layer(64, conv2, mask2)
conv5, mask5, conv6, mask6 = self.__encoder_layer(128, conv4, mask4)
conv7, mask7, conv8, mask8 = self.__encoder_layer(256, conv6, mask6)
conv9, mask9, conv10, mask10 = self.__decoder_layer(256, 128, conv8, mask8, conv7, mask7)
conv11, mask11, conv12, mask12 = self.__decoder_layer(128, 64, conv10, mask10, conv5, mask5)
conv13, mask13, conv14, mask14 = self.__decoder_layer(64, 32, conv12, mask12, conv3, mask3)
conv15, mask15, conv16, mask16 = self.__decoder_layer(32, 3, conv14, mask14, conv1, mask1)
outputs = keras.layers.Conv2D(3, (3, 3), activation='sigmoid', padding='same')(conv16)
return keras.models.Model(inputs=[input_image, input_mask], outputs=[outputs])
오토인코더이기 때문에, 이 아키텍처는 두 가지 요소 즉, 이미 논의한 인코더와 디코더를 갖고 있습니다. 이 인코더 및 디코더 conv 블록을 재사용하기 위해 저희는 간단한 효용함수(utility function) 2개, encoder_layer
와 decoder_layer
를 빌드했습니다.
def __encoder_layer(self, filters, in_layer, in_mask):
conv1, mask1 = PConv2D(32, (3,3), strides=1, padding='same')([in_layer, in_mask])
conv1 = keras.activations.relu(conv1)
conv2, mask2 = PConv2D(32, (3,3), strides=2, padding='same')([conv1, mask1])
conv2 = keras.layers.BatchNormalization()(conv2, training=True)
conv2 = keras.activations.relu(conv2)
return conv1, mask1, conv2, mask2
def __decoder_layer(self, filter1, filter2, in_img, in_mask, share_img, share_mask):
up_img = keras.layers.UpSampling2D(size=(2,2))(in_img)
up_mask = keras.layers.UpSampling2D(size=(2,2))(in_mask)
concat_img = keras.layers.Concatenate(axis=3)([share_img, up_img])
concat_mask = keras.layers.Concatenate(axis=3)([share_mask, up_mask])
conv1, mask1 = PConv2D(filter1, (3,3), padding='same')([concat_img, concat_mask])
conv1 = keras.activations.relu(conv1)
conv2, mask2 = PConv2D(filter2, (3,3), padding='same')([conv1, mask1])
conv2 = keras.layers.BatchNormalization()(conv2)
conv2 = keras.activations.relu(conv2)
return conv1, mask1, conv2, mask2
오토인코더 구현의 본질은 Upsampling2D
및 Concatenate
레이어에 있습니다. 이것에 대한 대안은 바로 Conv2DTranspose
레이어를 사용하는 것입니다.
저희는 default parameter인 mean_square_error를 손실로, dice_coef를 메트릭으로 사용하여 Adam optimizer를 통해 모델을 컴파일했습니다. model.fit()
을 사용하여 모델을 훈련했고, WandbCallback
및 PredictionLogger
콜백을 사용하여 모델의 결과를 로그했습니다.