摘要: 照片承載了很多人在某個時刻的記憶,尤其是一些老舊的黑白照片,塵封於腦海之中,隨著時間的流逝,記憶中對當時顏色的印象也會慢慢消散,這確實有些可惜。技術的發展會解決一些現有的難題,深度學習恰好能夠解決這個問題。
人工智慧和深度學習技術逐漸在各行各業中發揮著作用,尤其是在計算機視覺領域,深度學習就像繼承了某些上帝的功能,無所不能,令人歎為觀止。照片承載了很多人在某個時刻的記憶,尤其是一些老舊的黑白照片,塵封於腦海之中,隨著時間的流逝,記憶中對當時顏色的印象也會慢慢消散,這確實有些可惜。但隨著科技的發展,這些已不再是比較難的問題。在這篇文章中,將帶領大家領略一番深度學習的強大能力——將灰度影像轉換為彩色影像。文章使用PyTorch從頭開始構建一個機器學習模型,自動將灰度影像轉換為彩色影像,並且給出了相應程式碼及影像效果圖。整篇文章都是通過iPython Notebook中實現,對效能的要求不高,讀者們可以自行動手實踐一下在各自的計算機上執行下,親身體驗下深度學習神奇的效果吧。
PS:不僅能夠對舊影像進行著色,還可以對視訊(每次對視訊進行一幀處理)進行著色哦!閒話少敘,下面直接進入正題吧。
簡介
在影像著色任務中,我們的目標是在給定灰度輸入影像的情況下生成彩色影像。這個問題是具有一定的挑戰性,因為它是多模式的——單個灰度影像可能對應許多合理的彩色影像。因此,傳統模型通常依賴於重要的使用者輸入以及輸入的灰度影像內容。
最近,深層神經網路在自動影像著色方面取得了顯著的成功——從灰度到彩色,無需額外的人工輸入。這種成功的部分原因在於深層神經網路能夠捕捉和使用語義資訊(即影像的實際內容),儘管目前還不能夠確定這些型別的模型表現如此出色的原因,因為深度學習類似於黑匣子,暫時無法弄清演算法是如何自動學習,後續會朝著可解釋性研究方向發展。
在解釋模型之前,首先以更精確地方式闡述我們所面臨的問題。
問題
我們的目的是要從灰度影像中推斷出每個畫素(亮度、飽和度和色調)具有3個值的全色影像,對於灰度圖而言,每個畫素僅具有1個值(僅亮度)。為簡單起見,我們只能處理大小為256 x 256的影像,所以我們的輸入影像大小為256 x 256 x 1(亮度通道),輸出的影像大小為256 x 256 x 2(另兩個通道)。
正如人們通常所做的那樣,我們不是用RGB格式的影像進行處理,而是使用LAB色彩空間(亮度,A和B)。該色彩空間包含與RGB完全相同的資訊,但它將使我們能夠更容易地將亮度通道與其他兩個(我們稱之為A和B)分開。在稍後會構造一個輔助函式來完成這個轉換過程。
此外將嘗試直接預測輸入影像的顏色值(即迴歸)。還有其他更有趣的分類方法,但目前堅持使用迴歸方法,因為它很簡單且效果很好。
資料
著色資料無處不在,這是由於我們可以從任何一張彩色影像中提取出灰度通道。對於本文專案,我們將使用MIT地點資料集中的一個子集,該子資料集包含地點、景觀和建築物。
# Download and unzip (2.2GB)
!wget http://data.csail.mit.edu/places/places205/testSetPlaces205_resize.tar.gz
!tar -xzf testSetPlaces205_resize.tar.gz
# Move data into training and validation directories
import os
os.makedirs('images/train/class/', exist_ok=True) # 40,000 images
os.makedirs('images/val/class/', exist_ok=True) # 1,000 images
for i, file in enumerate(os.listdir('testSet_resize')):
if i < 1000: # first 1000 will be val
os.rename('testSet_resize/' + file, 'images/val/class/' + file)
else: # others will be val
os.rename('testSet_resize/' + file, 'images/train/class/' + file)
# Make sure the images are there
from IPython.display import Image, display
display(Image(filename='images/val/class/84b3ccd8209a4db1835988d28adfed4c.jpg'))
工具
本文使用PyTorch構建和訓練搭建的模型。此外,我們還了使用torchvision工具,該工具在PyTorch中處理影像和視訊時很有用,以及使用了scikit-learn工具,用於在RGB和LAB顏色空間之間進行轉換。
# Download and import libraries
!pip install torch torchvision matplotlib numpy scikit-image pillow==4.1.1
# For plotting
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# For conversion
from skimage.color import lab2rgb, rgb2lab, rgb2gray
from skimage import io
# For everything
import torch
import torch.nn as nn
import torch.nn.functional as F
# For our model
import torchvision.models as models
from torchvision import datasets, transforms
# For utilities
import os, shutil, time
# Check if GPU is available
use_gpu = torch.cuda.is_available()
模型
模型採用卷積神經網路構建而成,與傳統的卷積神經網路模型類似,首先應用一些卷積層從影像中提取特徵,然後將反摺積層應用於高階(增加空間解析度)特徵。
具體來說,模型採用的是遷移學習的方法,基礎是ResNet-18模型,ResNet-18網路具有18層結構以及剩餘連線的影像分類網路層。我們修改了該網路的第一層,以便它接受灰度輸入而不是彩色輸入,並且切斷了第六層後面的網路結構:
現在,在程式碼中定義後續的網路模型,將從網路的後半部分開始,即上取樣層:
class ColorizationNet(nn.Module):
def __init__(self, input_size=128):
super(ColorizationNet, self).__init__()
MIDLEVEL_FEATURE_SIZE = 128
## First half: ResNet
resnet = models.resnet18(num_classes=365)
# Change first conv layer to accept single-channel (grayscale) input
resnet.conv1.weight = nn.Parameter(resnet.conv1.weight.sum(dim=1).unsqueeze(1))
# Extract midlevel features from ResNet-gray
self.midlevel_resnet = nn.Sequential(*list(resnet.children())[0:6])
## Second half: Upsampling
self.upsample = nn.Sequential(
nn.Conv2d(MIDLEVEL_FEATURE_SIZE, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.Upsample(scale_factor=2),
nn.Conv2d(128, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Upsample(scale_factor=2),
nn.Conv2d(64, 32, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 2, kernel_size=3, stride=1, padding=1),
nn.Upsample(scale_factor=2)
)
def forward(self, input):
# Pass input through ResNet-gray to extract features
midlevel_features = self.midlevel_resnet(input)
# Upsample to get colors
output = self.upsample(midlevel_features)
return output
現在通過下面的程式碼建立整個模型:
model = ColorizationNet()
訓練
損失函式
由於使用的是迴歸方法,所以使用的仍然是均方誤差損失函式:嘗試最小化預測的顏色值與真實(實際值)顏色值之間的平方距離。
criterion = nn.MSELoss()
由於問題的多形式性,上述損失函式對於著色有一點小的問題。例如,如果一件灰色的衣服可能是紅色或藍色,而模型若選擇錯誤的顏色時,則會受到嚴厲的懲罰。因此,構建的模型通常會選擇與飽和度鮮豔的顏色相比不太可能“非常錯誤”的不飽和顏色。關於這個問題已經有了重要的研究(參見Zhang等人),但是本文將堅持這種損失函式,就是這麼任性。
優化
使用Adam優化器優化選定的損失函式(標準)。
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2, weight_decay=0.0)
載入資料
使用torchtext來載入資料,由於我們需要LAB空間中的影像,所以首先必須定義一個自定義資料載入器(dataloader)來轉換影像。
class GrayscaleImageFolder(datasets.ImageFolder):
'''Custom images folder, which converts images to grayscale before loading'''
def __getitem__(self, index):
path, target = self.imgs[index]
img = self.loader(path)
if self.transform is not None:
img_original = self.transform(img)
img_original = np.asarray(img_original)
img_lab = rgb2lab(img_original)
img_lab = (img_lab + 128) / 255
img_ab = img_lab[:, :, 1:3]
img_ab = torch.from_numpy(img_ab.transpose((2, 0, 1))).float()
img_original = rgb2gray(img_original)
img_original = torch.from_numpy(img_original).unsqueeze(0).float()
if self.target_transform is not None:
target = self.target_transform(target)
return img_original, img_ab, target
接下來,對訓練資料和驗證資料定義變換。
# Training
train_transforms = transforms.Compose([transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip()])
train_imagefolder = GrayscaleImageFolder('images/train', train_transforms)
train_loader = torch.utils.data.DataLoader(train_imagefolder, batch_size=64, shuffle=True)
# Validation
val_transforms = transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224)])
val_imagefolder = GrayscaleImageFolder('images/val' , val_transforms)
val_loader = torch.utils.data.DataLoader(val_imagefolder, batch_size=64, shuffle=False)
輔助函式
在進行訓練之前,定義了輔助函式來跟蹤訓練損失並將影像轉換回RGB影像。
class AverageMeter(object):
'''A handy class from the PyTorch ImageNet tutorial'''
def __init__(self):
self.reset()
def reset(self):
self.val, self.avg, self.sum, self.count = 0, 0, 0, 0
def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count
def to_rgb(grayscale_input, ab_input, save_path=None, save_name=None):
'''Show/save rgb image from grayscale and ab channels
Input save_path in the form {'grayscale': '/path/', 'colorized': '/path/'}'''
plt.clf() # clear matplotlib
color_image = torch.cat((grayscale_input, ab_input), 0).numpy() # combine channels
color_image = color_image.transpose((1, 2, 0)) # rescale for matplotlib
color_image[:, :, 0:1] = color_image[:, :, 0:1] * 100
color_image[:, :, 1:3] = color_image[:, :, 1:3] * 255 - 128
color_image = lab2rgb(color_image.astype(np.float64))
grayscale_input = grayscale_input.squeeze().numpy()
if save_path is not None and save_name is not None:
plt.imsave(arr=grayscale_input, fname='{}{}'.format(save_path['grayscale'], save_name), cmap='gray')
plt.imsave(arr=color_image, fname='{}{}'.format(save_path['colorized'], save_name))
驗證
在驗證過程中,使用torch.no_grad()函式簡單地執行下沒有反向傳播的模型。
def validate(val_loader, model, criterion, save_images, epoch):
model.eval()
# Prepare value counters and timers
batch_time, data_time, losses = AverageMeter(), AverageMeter(), AverageMeter()
end = time.time()
already_saved_images = False
for i, (input_gray, input_ab, target) in enumerate(val_loader):
data_time.update(time.time() - end)
# Use GPU
if use_gpu: input_gray, input_ab, target = input_gray.cuda(), input_ab.cuda(), target.cuda()
# Run model and record loss
output_ab = model(input_gray) # throw away class predictions
loss = criterion(output_ab, input_ab)
losses.update(loss.item(), input_gray.size(0))
# Save images to file
if save_images and not already_saved_images:
already_saved_images = True
for j in range(min(len(output_ab), 10)): # save at most 5 images
save_path = {'grayscale': 'outputs/gray/', 'colorized': 'outputs/color/'}
save_name = 'img-{}-epoch-{}.jpg'.format(i * val_loader.batch_size + j, epoch)
to_rgb(input_gray[j].cpu(), ab_input=output_ab[j].detach().cpu(), save_path=save_path, save_name=save_name)
# Record time to do forward passes and save images
batch_time.update(time.time() - end)
end = time.time()
# Print model accuracy -- in the code below, val refers to both value and validation
if i % 25 == 0:
print('Validate: [{0}/{1}]\t'
'Time {batch_time.val:.3f} ({batch_time.avg:.3f})\t'
'Loss {loss.val:.4f} ({loss.avg:.4f})\t'.format(
i, len(val_loader), batch_time=batch_time, loss=losses))
print('Finished validation.')
return losses.avg
訓練
在訓練過程中,使用loss.backward()執行模型並進行反向傳播過程。我們首先定義了一個訓練一個epoch的函式:
def train(train_loader, model, criterion, optimizer, epoch):
print('Starting training epoch {}'.format(epoch))
model.train()
# Prepare value counters and timers
batch_time, data_time, losses = AverageMeter(), AverageMeter(), AverageMeter()
end = time.time()
for i, (input_gray, input_ab, target) in enumerate(train_loader):
# Use GPU if available
if use_gpu: input_gray, input_ab, target = input_gray.cuda(), input_ab.cuda(), target.cuda()
# Record time to load data (above)
data_time.update(time.time() - end)
# Run forward pass
output_ab = model(input_gray)
loss = criterion(output_ab, input_ab)
losses.update(loss.item(), input_gray.size(0))
# Compute gradient and optimize
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Record time to do forward and backward passes
batch_time.update(time.time() - end)
end = time.time()
# Print model accuracy -- in the code below, val refers to value, not validation
if i % 25 == 0:
print('Epoch: [{0}][{1}/{2}]\t'
'Time {batch_time.val:.3f} ({batch_time.avg:.3f})\t'
'Data {data_time.val:.3f} ({data_time.avg:.3f})\t'
'Loss {loss.val:.4f} ({loss.avg:.4f})\t'.format(
epoch, i, len(train_loader), batch_time=batch_time,
data_time=data_time, loss=losses))
print('Finished training epoch {}'.format(epoch))
接下來,我們定義一個迴圈訓練函式,即訓練100個epoch:
# Move model and loss function to GPU
if use_gpu:
criterion = criterion.cuda()
model = model.cuda()
# Make folders and set parameters
os.makedirs('outputs/color', exist_ok=True)
os.makedirs('outputs/gray', exist_ok=True)
os.makedirs('checkpoints', exist_ok=True)
save_images = True
best_losses = 1e10
epochs = 100
# Train model
for epoch in range(epochs):
# Train for one epoch, then validate
train(train_loader, model, criterion, optimizer, epoch)
with torch.no_grad():
losses = validate(val_loader, model, criterion, save_images, epoch)
# Save checkpoint and replace old best model if current model is better
if losses < best_losses:
best_losses = losses
torch.save(model.state_dict(), 'checkpoints/model-epoch-{}-losses-{:.3f}.pth'.format(epoch+1,losses))
Starting training epoch 0 ...
預訓練模型
如果你想運用預訓練模型而不想從頭開始訓練的話,我已經為你訓練了好一個模型。該模型在少量時間內接受相對少量的資料訓練,並且能夠工作正常。可以從下面的連結下載並使用它:
# Download pretrained model
!wget https://www.dropbox.com/s/kz76e7gv2ivmu8p/model-epoch-93.pth
#https://www.dropbox.com/s/9j9rvaw2fo1osyj/model-epoch-67.pth
# Load model
pretrained = torch.load('model-epoch-93.pth', map_location=lambda storage, loc: storage)
model.load_state_dict(pretrained)
# Validate
save_images = True
with torch.no_grad():
validate(val_loader, model, criterion, save_images, 0)
Validate: [0/16] Time 10.628 (10.628) Loss 0.0030 (0.0030)
Validate: [16/16] Time 0.328 ( 0.523) Loss 0.0029 (0.0029)
結果
有趣的內容到了,讓我們看看深度學習技術實現的效果吧!
# Show images
import matplotlib.image as mpimg
image_pairs = [('outputs/color/img-2-epoch-0.jpg', 'outputs/gray/img-2-epoch-0.jpg'),
('outputs/color/img-7-epoch-0.jpg', 'outputs/gray/img-7-epoch-0.jpg')]
for c, g in image_pairs:
color = mpimg.imread(c)
gray = mpimg.imread(g)
f, axarr = plt.subplots(1, 2)
f.set_size_inches(15, 15)
axarr[0].imshow(gray, cmap='gray')
axarr[1].imshow(color)
axarr[0].axis('off'), axarr[1].axis('off')
plt.show()
結論
在這篇文章中,使用PyTorch工具從頭建立了一個簡單的自動影像著色器,沒有太複雜的程式碼,只需要簡單的準備好資料並設計好合理的模型即可得到令人令人興奮的結果,此外,這僅僅只是起步,後續還有很多地方可以進行改進優化並進行推廣。
本文作者:【方向】
閱讀原文
本文為雲棲社群原創內容,未經允許不得轉載。