
【機械学習】CNNで低解像度な画像を高解像度に変換してみる
2021.12.20
目次
CNNで低解像度な画像を高解像度に変換してみたい!
前回は、「畳み込みニューラルネットワーク(Convolutional Neural Network:CNN)」を用いて、顔画像のカラー復元を行いました。
小さな画像サイズで実験を行いましたが、実際の場面では限られたリソース(計算資源)の中で、大きな画像サイズを取り扱う場面も多くあります。
そこで今回は、CNNを用いて、前回行ったカラー化と画像の高解像度化を組み合わせた実験をしてみたいと思います。
前回の記事はこちら
低解像度画像と高解像度画像
一般的にボケている画像のことを「低解像度な画像」と呼びます。
それに対し「高解像度な画像」とは、ボケの少ない画像のことをいいます。
そもそもボケというのは、画像のピクセル数が減少することで起こります。
低解像度化のイメージ

例えば、左下の画像のように適当なブロック分けを行い、その一ブロック内の全画素値の平均値を新たな画素値とすることを、画像全体に行うことでピクセル数を減少させることができます。
低解像度化は簡単
右の画像は、元画像に対してピクセル数が減少しているため「低解像度な画像」と言えます。
このように、「高解像度の画像」から「低解像度な画像」を作ることは比較的容易にできます。
低解像度画像から高解像度画像への変換
では逆のパターンで、「低解像度な画像」から「高解像度な画像」を生成することを考えてみましょう。
先ほどのブロックに分けた例で言うと、「低解像度な画像」の高解像度化は、一つのピクセルから複数のピクセルの値を決定するということが必要になります。
高解像度化のイメージ

「高解像度化」の作業は、下図のように解が一意に決まらず、様々なパターンが存在することが容易に想像できます。
高解像度化は難しい
言うなれば、先ほどの「高解像度な画像」もその解の一つであるものの、正解かどうかは分かりません。
そのため、「高解像度化」を人間が手探りで行うのは途方もなく難しい作業なのです。
そこで、あり得る様々なパターンから最も自然な画像になるパターンをニューラルネットワークに出力してもらうと言うのが、CNN で「高解像度化」をするモチベーションです。
高解像度化を用いるメリット
では、「高解像度化」をすることでどんなメリットがあるのでしょう。
機械学習のボトルネック
これは前回も話したとおり、機械学習の研究が進んだ現在、ボトルネックとなるのは計算資源です。
「高解像度な画像」を使って機械学習を行うことができればそれでいいのですが、そうはいきません。
ただでさえ制限があるメモリに、ニューラルネットワークのパラメータを多くメモリに保存しなければなりません。
そのため、画像サイズでその足りないメモリの帳尻合わせをする必要があるのです。
ボトルネック解消のために
そのため病理画像など、「高解像度な画像」に対しては、かなり小さい画像でしか学習ができません。
そんな中で低解像度で学習したものを「高解像度化」できれば、メモリの節約にもなり、学習結果もより向上するのではないかと言うのが私の考えるメリットです。
高解像度化のための学習モデル
では実際に、「学習モデル」について話していきます。

U-net のような構造にしました。
また、白黒画像のカラー化のときと同じように、深い層で浅い層の情報を渡し、細部の画像情報と全体的な情報を両方とも加味できるようにしました。
私の中でこれが一番損失関数が小さくなったので、このモデルを選びました。
カラー化と高解像度化の併用実験
検証用データに対する精度
「低解像度画像」に比べて生成画像はボケが小さい、つまりは「高解像度」になっていることがわかります。
低解像度画像

生成画像

私自身、かなり精度が良くてびっくりしています。
SRGAN を使わなくてもある程度は精度が出るようです。
白黒画像のカラー化との併用実験
次に、白黒画像の「カラー化」と画像の「高解像度化」を併用して実験してみました。
併用実験の方法
併用実験の方法としては、以下のように行いました。

- 64×64の白黒画像をカラー化する
- 変換した64×64のカラー画像を128×128に高解像度化
こうすることで、実際の実験環境ではメモリの影響で学習できなかった画像サイズでも、カラー画像の変換ができるようになります。
実験!
実際に、以下の変換画像の比較を行ってみましょう!
- 「64×64のサイズ」で学習したモデルで「128×128の画像」をカラー化した場合
- 「64×64の画像」をカラー化した後で「128×128に高解像度化」した場合
カラー化のみ

カラー化+高解像度化

実験結果
見てわかるように、「カラー化のみ」の結果では、色が正しく塗れていない部分があり、ムラがあります。
しかし、「カラー化+高解像度化」の結果は、前者と比較してムラが少なく、「64×64のサイズ」で学習したモデルが「128×128の画像サイズ」にも適用できていることがわかります。
このように、高解像度化は他の実験に併用することも有効であると言えます。
様々なタスクへの応用
それは白黒画像のカラー化だけでなく、画像のセグメンテーションなど様々なタスクに応用することができます。
学習時の画像サイズに限界を感じている機械学習エンジニアの方は、ぜひ参考にしてみてくださ!
さいごに ~次回予告~
さて、画像の「カラー化」と「高解像度化」と行ってきました。
しかし、白黒画像を「カラー化」したモデルでは、よく見ると肌色を顔の近くに塗っているだけで、背景も肌色に塗られていることがわかります。
正直言って、自然な画像であるとは言えません。
さらに、「カラー化」との併用実験では、画像の「高解像度化」も、実際の画像の解像度に比べてボケていることがわかります。
これは、「損失関数による問題」です。
ピクセル単位での二乗誤差をとることで、画像が自然なパターンにならずに全体的にボヤッとした見た目になるからです。
そこで次回からは、ピクセル単位での誤差に加えて、「GAN」による「Adversarial Loss」を加えることで、画像のボケ除去や色の多様性の追加を行っていきたいと思います!
こちらの記事もオススメ!
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | import torch import torchvision import torchvision.datasets as dset from torch import nn from torch.autograd import Variable from torch.utils.data import DataLoader from torchvision import transforms from torchvision.utils import save_image from mpl_toolkits.mplot3d import axes3d from torchvision.datasets import MNIST import os import math import pylab import matplotlib.pyplot as plt num_epochs = 1 #エポック数 batch_size = 100 #バッチサイズ learning_rate = 1e-3 #学習率 train = True#学習を行うかどうかのフラグ pretrained =False#事前に学習したモデルがあるならそれを使う save_img = True #ネットワークによる生成画像を保存するかどうのフラグ def to_img(x): x = 0.5 * (x + 1) x = x.clamp(0, 1) x = x.view(x.size(0), 3, x.shape[2], x.shape[3]) return x def to_img_mono(x): x = 0.5 * (x + 1) x = x.clamp(0, 1) x = x.view(x.size(0), 3,x.shape[2], x.shape[3]) return x #データセットを調整する関数 transform = transforms.Compose( [transforms.ToTensor(), transforms.Normalize((0.5, ), (0.5, ))]) #訓練用データセット dataset = dset.ImageFolder(root='./drive/My Drive/face/', transform=transforms.Compose([ transforms.RandomResizedCrop(64, scale=(1.0, 1.0), ratio=(1., 1.)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.05, contrast=0.05, saturation=0.05, hue=0.05), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ])) #データセットをdataoaderで読み込み dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True) #低解像度画像を高解像度化するニューラルネットワーク class SizeDecoder(nn.Module): def __init__(self): super(SizeDecoder, self).__init__() nch_g = 64 self.layer1 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.Conv2d(3, nch_g , 3, 2, 1), nn.BatchNorm2d(nch_g), nn.ReLU() ), # (1, 64, 64) -> (64, 32, 32) }) self.layer2 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.Conv2d(nch_g , nch_g*2 , 3, 2, 1), nn.BatchNorm2d(nch_g*2), nn.ReLU() ), # (64, 32, 32) -> (128, 16, 16) }) self.layer3 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.Conv2d(nch_g*2 , nch_g*4 , 3, 2, 1), nn.BatchNorm2d(nch_g*4), nn.ReLU() ), # (128, 16, 16) -> (256, 8, 8) }) self.layer4= nn.ModuleDict({ 'layer0': nn.Sequential( nn.Conv2d(nch_g*4 , nch_g*8 , 3, 2, 1), nn.BatchNorm2d(nch_g*8), nn.ReLU() ), # (256, 8, 8) -> (512, 4, 4) }) self.layer7 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.ConvTranspose2d(nch_g*8 , nch_g*4 , 4, 2, 1), nn.BatchNorm2d(nch_g*4), nn.ReLU() ), # (512, 4, 4) -> (256, 8, 8) }) self.layer8 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.ConvTranspose2d(nch_g*4 , nch_g*2 , 4, 2, 1), nn.BatchNorm2d(nch_g*2), nn.ReLU() ), # (256, 8,8) -> (128, 16, 16) }) self.layer9= nn.ModuleDict({ 'layer0': nn.Sequential( nn.ConvTranspose2d(nch_g*2 , nch_g , 4, 2, 1), nn.BatchNorm2d(nch_g), nn.ReLU() ), # (128, 16, 16) -> (64, 32, 32) }) self.layer10 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.ConvTranspose2d(nch_g,int(nch_g/2) , 4, 2, 1), nn.BatchNorm2d(int(nch_g/2)), nn.Tanh() ), # (64, 32, 32) -> (32, 64, 64) }) self.layer11 = nn.ModuleDict({ 'layer0': nn.Sequential( nn.ConvTranspose2d(int(nch_g/2) , 3 , 4, 2, 1), nn.BatchNorm2d(3), nn.Tanh() ), # (32, 64, 64) -> (3, 128, 128) }) def forward(self, z): for layer in self.layer1.values(): z = layer(z) z1 =z for layer in self.layer2.values(): z = layer(z) z2 =z for layer in self.layer3.values(): z = layer(z) z3=z for layer in self.layer4.values(): z = layer(z) for layer in self.layer7.values(): z = layer(z) z =z+z3 for layer in self.layer8.values(): z = layer(z) z =z+z2 for layer in self.layer9.values(): z = layer(z) z =z+z1 for layer in self.layer10.values(): z = layer(z) for layer in self.layer11.values(): z = layer(z) return z def main(): #もしGPUがあるならGPUを使用してないならCPUを使用 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") #ネットワークを呼び出し model = SizeDecoder().to(device) #事前に学習しているモデルがあるならそれを読み込む if pretrained: param = torch.load('./Size_Decoder.pth') model.load_state_dict(param) #誤差関数には二乗誤差を使用 criterion = nn.MSELoss() #更新式はAdamを適用 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5) loss_train_list = [] loss_test_list= [] for epoch in range(num_epochs): print(epoch) for data in dataloader: img, num = data #img --> [batch_size,1,32,32] #imgは元画像 #imgをGPUに載せる img = Variable(img).to(device) # ===================forward===================== #_imgは高解像度画像を低解像度に変換した画像 _img = (img[:,:,::2, ::2] + img[:,:,1::2, ::2] + img[:,:,::2, 1::2] + img[:,:,1::2, 1::2])/4 _img =_img.to(device) #ネットワークの出力結果 output = model(_img) print(output.shape) #もし学習するなら if train: #ネットワークの出力と高解像度画像との誤差を損失として学習 # ===================backward==================== loss = criterion(output, img) print(loss) #勾配を初期化 optimizer.zero_grad() #微分値を計算 loss.backward() #パラメータを更新 optimizer.step() else:#学習しないなら break # ===================log======================== if train == True: #モデルを保存 torch.save(model.state_dict(), './Size_Decoder.pth') #もし生成画像を保存するなら if save_img: value = int(math.sqrt(batch_size)) pic = to_img(img.cpu().data) pic = torchvision.utils.make_grid(pic,nrow = value) save_image(pic, './real_image_{}.png'.format(epoch)) #元画像を保存 pic = to_img_mono(output.cpu().data) pic = torchvision.utils.make_grid(pic,nrow = value) save_image(pic, './image_{}.png'.format(epoch)) #生成画像 if __name__ == '__main__': main() |
書いた人はこんな人

- 「好きなことを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もりは大歓迎!
また、WEBエンジニアとモバイルエンジニアも積極採用中です!
ご応募をお待ちしております!
ITエンタメ2022.07.06高水準言語『FORTRAN』を開発したジョン・バッカス氏
ITエンタメ2022.06.22IntelliJ IDEAとkotlinを送り出したJetBrains創業物語
ITエンタメ2022.06.15【アタリ創業者】スティーブ・ジョブズを雇った男「ノーラン・ブッシュネル」
ITエンタメ2022.06.13プログラミングに飽きてPHPを開発したラスマス・ラードフ