#1. はじめに
__機械学習__を進める上で、必要不可欠なのが学習データの準備です。学習データは通常は数百から数千、数万も必要な場合があり、基本的には多ければ多いほど良いとされています。
オンライン上で様々な機械学習用のデータセットがダウンロードできますが、いざ実際の業務になり、データセットを一から作ろうと思うと、その膨大なデータ数の取得と、アノテーション作業の困難さに断念した人も多いはずです。特に画像系の機械学習は、データセットの数や精度が精度に大きく関わってきます。
実は私は、CGの仕事をしていまして、CGの手法を使えば簡単に機械学習用のデータセットを構築できるのではないかと考えました。
これから数回にわたって、CGを使った学習データの手法について実験してみたいと思います。
#2. 概要
Synthesized Data Augment__とは、"合成されたデータ拡張"__という意味です。Data Augmentというのは、機械学習で一般的に使われている手法で、画像学習データのサイズや色や位置などをランダムに変更して、データ数を多く水増しして学習するというものです。
Synthesized Data Augmentは、それにCGの合成技術を使って、よりアグレッシブなデータの水増しを行おうという方法です。(ちなみに検索しても、ほとんどヒットはありませんが、僕が作った言葉でありません)
合成の方法は、__Photoshop__などを使われている人は、おなじみの方法で、まず機械学習で目的となる像の部分、人物のセグメンテーションなら人物の部分をマスクで切り抜きます。次に切り抜かれた後の背景の部分をインペイント(再構築)します。そして先ほど切り抜いた人物マスクを、自由な位置に置き直せば新しい画像データの出来上がりになります。
しかし、この一連の作業を一枚一枚Photoshopで行うと、それは膨大な作業量になってしまいますので、そこをAdobeの__Action Script__を使ったり、プログラムで処理したりするのです。
#3. 元になる学習データの取得
CGを使うからと言って、何もないところからデータを作れるわけではないので、まずは元になるデータセットを探すことになります。今回は、セマンティックセグメンテーションを目的としているので、教師データにセグメンテーション(部位ごとに塗り分けがされている画像データ)が含まれているデータセットを探します。
Clothing Co-Parsing (CCP) Dataset
https://github.com/bearpaw/clothing-co-parsing
人物の服ごとに塗り分けられたセグメンテーションが含まれているデータセットになります。今回はこちらを使っていきます。ダウンロードすると、trainフォルダの中に1004枚の人物画像と、
中には見慣れない".mat"というファイルが入っています。とりあえず検索してみると、Scipyで開くことができるようなので中身を見てみましょう。
import scipy.io
import matplotlib.pyplot as plt
import cv2
mat_pc = scipy.io.loadmat("./0005.mat")
print(mat_pc)
>>>{'__globals__': [],
'__header__': b'MATLAB 5.0 MAT-file, Platform: PCWIN64, Created on: Tue Aug 19 15:20:32 2014',
'__version__': '1.0',
'groundtruth': array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], dtype=uint8)}
どうやら中身は辞書型になっているようで、'groundtruth'を抜き出して表示させてみます。
gt_data = mat_pc["groundtruth"]
fig, ax = plt.subplots()
ax.imshow(gt_data, cmap='gray')
plt.show()
セグメンテーション画像が見つかったのですが、今回は人物部分を切り出して、ほかの画像と合成したいので、このデータからマスク画像を作成します。
t = 1
th1 = gt_data.copy()
th1[gt_data < t] = 0
th1[gt_data >= t] = 255
fig, ax = plt.subplots()
ax.imshow(th1, cmap='gray')
plt.show()
背景部分のピクセルが'1'で塗られているのを確認し、スレッショルドで二諧調に変更しました。
下記は全ての画像を変換するコードです。
folder_path = './pixel-level'
save_path = './png/'
image_files = os.listdir(folder_path)
image_files.sort()
for i, image_file in enumerate(image_files):
mat_pc = scipy.io.loadmat(folder_path + os.sep + image_file)
gt_data = mat_pc["groundtruth"]
t = 1
th1 = gt_data.copy()
th1[gt_data < t] = 0
th1[gt_data >= t] = 255
name = image_file.split(".")
cv2.imwrite(save_path + name[0] + '.png', th1)
これで後作業で必要になる人物のマスクが出来上がりました。
#3. Photoshopなどの手作業の手順
Photoshopは今さら説明することもないと思いますが、写真などの画像を修正するアプリケーションです。最初にPhtoshopを使って、作業の工程を理解します。
まず、Photosohopで画像とマスク画像を表示します。画像をクイックマスクの状態にし、マスク画像をコピー&ペーストすると、画像の部分にだけ選択されます。これをコピー&ペーストすると、人物レイヤーができ移動させることが可能になります。次に元の画像の人物のところだけを再選択します。そして少し選択範囲を広げてから、'塗りつぶし→コンテンツに応じて'を指定すると、人物の部分をそれ以外のピクセルで埋めてくれます。
このような指定領域を塗りつぶすことをインペイントといいます。インペイントされた部分は、見た目上は少しおかしなところもありますが、経験上これくらいは問題がないと思っています。
これで人物が自由に移動できる画像が出来上がりました。この過程をプログラムに置き換えれば、ランダムの人物の位置が変わる画像を拡張することができます。さらにもう一工夫します。
<画僧 マスク インペイント>
今度は別の画像の背景を同じようにインペイントして、それぞれを入れ替えることをやってみましょう。そうすることで、人物画像と背景画像の組み合わせで新たな画像を拡張することができるようになります。つまり、データセットの画像の枚数の二乗でデータセットを増やすことができるのです。
<それぞれの背景と人物を交換した画像>
#4. Action Scriptでのプログラム
Photosohpにもアクションといって、複数の作業を登録してバッチ(複数の画像に対して一括で処理)を行う機能があるのですが、アクションでは、作業の中にランダムに数値を変更したり、自由に画像ファイル名を指定して開いたり、保存したりすることができません。そういう場合は、Adobeのサブスクリプションに付属するAction Scriptを使うことで行うことができます。
Action Scriptの難点は、プログラムとはいえ裏でPhotosohpを動かしているだけなので、実行速度が遅い点と、処理数が多くなると途中でエラーが出て止まってしまうことが頻繁にあるので、なかなか気が抜けません。
では最初に、Photoshopで行ったインペイント(塗りつぶし)をAction Scriptで行ってみましょう。AdobeのCreative CloudからExtendScript Toolkitをインストールします。
ExtendScriptを起動すると、Adobeのどのアプリケーションにリンクするかを選択します。Photoshopを選択すると、以降ExtendScriptのプログラムでPhotoshopを操作することができるようになります。ExtendScriptのプログラムはJavascriptが基本になります。JavascriptはWeb系では広く使われている言語なので、何か困ったらJavascriptを検索してみるといいかもしれません。
それではインペイント(塗りつぶし)のプログラムを書いてみましょう。
displayDialogs = DialogModes.NO;
var mov_val = 0.2
var bg_num = 5
var count = 0
folderObj = Folder.selectDialog("画像ファイルがあるフォルダを選択してください");
fileList = folderObj.getFiles("*.jpg");
//fsname = folderObj.fsName;
folderObj = Folder.selectDialog("マスクファイルがあるフォルダを選択してください");
var maskObj =new Folder(mkname);
maskList = maskObj.getFiles("*.jpg");
for(fCnt=0; fCnt<fileList.length; fCnt++)
{
str_r = fileList[fCnt].fsName;
fileObj = new File(str_r);
flag = fileObj.exists;
if (flag == true)
{
try{
fileDoc = app.open(fileObj);
}
catch(e)
{
alert(fCnt);
}
}
mask_r = maskList[fCnt].fsName;
mask_img = new File(mask_r);
flag_mask = mask_img.exists;
if (flag_mask == true)
{
try{
maskDoc= app.open(mask_img);
}
catch(e){
alert(fCnt);
}
}
//// Mask copy to Qick mask /////////////////////////////////////////////////////
//activeDocument = maskDoc;
var Width = activeDocument.width;
var Height = activeDocument.height;
activeDocument. selection. selectAll();
activeDocument. selection. copy();
activeDocument = fileDoc;
activeDocument. quickMaskMode = true;
activeDocument. paste();
activeDocument. quickMaskMode = false;
// ====Extend Selection ===================================================
var idExpn = charIDToTypeID( "Expn" );
var desc11 = new ActionDescriptor();
var idBy = charIDToTypeID( "By " );
var idPxl = charIDToTypeID( "#Pxl" );
desc11.putUnitDouble( idBy, idPxl, 10.000000 );
executeAction( idExpn, desc11, DialogModes.NO );
// ===== Fill Content==================================================
var idFl = charIDToTypeID( "Fl " );
var desc12 = new ActionDescriptor();
var idUsng = charIDToTypeID( "Usng" );
var idFlCn = charIDToTypeID( "FlCn" );
var idcontentAware = stringIDToTypeID( "contentAware" );
desc12.putEnumerated( idUsng, idFlCn, idcontentAware );
var idOpct = charIDToTypeID( "Opct" );
var idPrc = charIDToTypeID( "#Prc" );
desc12.putUnitDouble( idOpct, idPrc, 100.000000 );
var idMd = charIDToTypeID( "Md " );
var idBlnM = charIDToTypeID( "BlnM" );
var idNrml = charIDToTypeID( "Nrml" );
desc12.putEnumerated( idMd, idBlnM, idNrml );
executeAction( idFl, desc12, DialogModes.NO );
////// Save /////////////////////////////////////////////////////
thumbDir = 'cleanBG';
str2 = ".jpg";
str = fileDoc.name.split(str2);
var str_dist = str [0] + "_clenBG_" + ".jpg";
var newDir = new Folder(fileDoc.path +'/'+ thumbDir);
if(! newDir.exists){ newDir.create();}
var newFile = new File(fileDoc.path +'/'+ thumbDir +'/'+ str_dist);
var jpegopt = new JPEGSaveOptions();
jpegopt.quality = 10; //0(low)〜12(high);
activeDocument.saveAs(newFile, jpegopt, true);
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
////// BG close /////////////////////////////////////////////////////
activeDocument = maskDoc;
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}
最初に画像ファイルとマスクを一枚づつ選択すると、あとはPhotoshopの手順に習って作業が進み、最後には'cleanBG'というフォルダに画像を保存するというスクリプトです。このように別々のフォルダのファイルを組み合わせて作業し、別名で保存ができるのがAction Scriptの大きな利点です。
では、人物と背景をランダムに組み合わせて新しい画像データを作成するスクリプトを書いてみましょう。
displayDialogs = DialogModes.NO;
var mov_val = 0.2
var bg_num = 5
var count = 0
folderObj = Folder.selectDialog("Frontファイルがあるフォルダを選択してください");
fileList = folderObj.getFiles("*.jpg");
var maskObj = Folder.selectDialog("マスクファイルがあるフォルダを選択してください");
maskList = maskObj.getFiles("*.png");
var bgObj = Folder.selectDialog("背景ファイルがあるフォルダを選択してください");
bgList = bgObj.getFiles("*.jpg");
for(bgCnt=0; bgCnt<bg_num; bgCnt++)
{
for(fCnt=0; fCnt<fileList.length; fCnt++)
{
str_r = fileList[fCnt].fsName;
fileObj = new File(str_r);
flag = fileObj.exists;
if (flag == true)
{
try{
fileDoc = app.open(fileObj);
}
catch(e)
{
alert(fCnt);
}
}
mask_r = maskList[fCnt].fsName;
mask_img = new File(mask_r);
flag_mask = mask_img.exists;
if (flag_mask == true)
{
try{
maskDoc= app.open(mask_img);
}
catch(e){
alert(fCnt);
}
}
//// Mask copy to Qick mask /////////////////////////////////////////////////////
var Width = activeDocument.width;
var Height = activeDocument.height;
activeDocument. selection. selectAll();
activeDocument. selection. copy();
activeDocument = fileDoc;
activeDocument. quickMaskMode = true;
activeDocument. paste();
activeDocument. quickMaskMode = false;
activeDocument. selection. copy();
activeDocument. paste();
rand = Math.random();
var dx = UnitValue( (Math.random() *2 - 1)*Width*mov_val , "px");
var dy = UnitValue( (Math.random() *2 - 1)*Height*mov_val , "px");
var size_high = 100 + 100*Math.random()*mov_val;
var size_width = 100 + 100*Math.random()*mov_val*2;
activeDocument. layers[0]. translate( dx *0.4, dy*0.1 );
activeDocument. layers[0]. resize( size_width, size_high, AnchorPosition. MIDDLECENTER );
/////// Flip val ///////
var flip = false;
if (rand > 0.5)
{
flip = true;
}
if (flip == true)
{
activeDocument. flipCanvas( Direction. HORIZONTAL);
}
//// Mask Doc save & Close /////////////////////////////////////////////////////
activeDocument = maskDoc;
activeDocument. selection. selectAll();
activeDocument. selection. copy();
activeDocument. paste();
activeDocument. layers[0]. translate( dx *0.4, dy*0.1 );
activeDocument. layers[0]. resize( size_width, size_high, AnchorPosition. MIDDLECENTER );
if (flip == true)
{
activeDocument. flipCanvas( Direction. HORIZONTAL);
}
//Fill Black
activeDocument. artLayers. add();
RGBColor = new SolidColor();
RGBColor.red = 0;
RGBColor.green = 0;
RGBColor.blue = 0;
activeDocument. selection. selectAll();
activeDocument.selection.fill(RGBColor,ColorBlendMode.NORMAL, 100, false);
//Move Layer
var bg_srcLayerSetObj = activeDocument. layers[1];
var bg_dstLayerSetObj = activeDocument. layers[0];
bg_srcLayerSetObj. move( bg_dstLayerSetObj, ElementPlacement. PLACEBEFORE);
activeDocument. flatten();
////////// Save New Mask ///////////////////////////////////////////////////////////
thumbDir = 'result';
str2 = ".jpg";
str = maskDoc.name.split(str2);
var str_dist = "mask_" + count + ".jpg";
var newDir = new Folder(maskDoc.path +'/'+ thumbDir);
if(! newDir.exists){ newDir.create();}
var newFile = new File(maskDoc.path +'/'+ thumbDir +'/'+ str_dist);
var jpegopt = new JPEGSaveOptions();
jpegopt.quality = 10; //0(low)〜12(high);
activeDocument.saveAs(newFile, jpegopt, true);
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
//// Bg Doc Copy /////////////////////////////////////////////////////
min = 0;
max= bgList.length;
randomNumber = Math.floor(Math.random() * (max - 1));
bg_r = bgList[randomNumber].fsName;
bg_img = new File(bg_r);
flag = bg_img.exists;
if (flag == true)
{
try{
open(bg_img);
bgDoc= activeDocument;
}
catch(e){
alert(randomNumber);
}
}
if (Math.random() > 0.5)
{
app.activeDocument. flipCanvas( Direction. HORIZONTAL);
}
activeDocument. selection. selectAll();
activeDocument. selection. copy();
activeDocument = fileDoc;
activeDocument. paste();
///////// Resize /////////////////////////////////////////////////////
var canvasWidth = activeDocument.width;
var canvasHeight = activeDocument.height;
var layer = activeDocument. layers[0]
var layerX = layer.bounds[0];
var layerY = layer.bounds[1];
var layerWidth = layer.bounds[2] - layerX;
var layerHeight = layer.bounds[3] - layerY;
var rate;
if(layerHeight * (canvasWidth / layerWidth)>= canvasHeight){
rate = Math.ceil((canvasWidth / layerWidth) * 10000) / 100;
} else {
rate = Math.ceil((canvasHeight / layerHeight) * 10000) / 100;
}
layer.resize(rate*1.1, rate*1.1);
layerX = layer.bounds[0];
layerY = layer.bounds[1];
layerWidth = layer.bounds[2] - layerX;
layerHeight = layer.bounds[3] - layerY;
layer.translate(((canvasWidth - layerWidth) / 2) - layerX + (Math.random()*2 - 1)* layerX*0.3, ((canvasHeight - layerHeight) / 2) - layerY );
activeDocument = fileDoc;
srcLayerSetObj = activeDocument.layers[1];
dstLayerSetObj = activeDocument.layers[0];
srcLayerSetObj. move( dstLayerSetObj, ElementPlacement. PLACEBEFORE);
////// Base Layer delete /////////////////////////////////////////////////////
activeDocument. layers[2]. remove();
////// Save /////////////////////////////////////////////////////
activeDocument. flatten();
str2 = ".jpg";
str = fileDoc.name.split(str2);
var str_dist = "photo_" + count + ".jpg";
var newDir = new Folder(fileDoc.path +'/'+ thumbDir);
if(! newDir.exists){ newDir.create();}
var newFile = new File(fileDoc.path +'/'+ thumbDir +'/'+ str_dist);
var jpegopt = new JPEGSaveOptions();
jpegopt.quality = 10; //0(low)〜12(high);
activeDocument.saveAs(newFile, jpegopt, true);
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
////// BG close /////////////////////////////////////////////////////
activeDocument = bgDoc;
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
count = count + 1;
}
}
最初に各種素材をフォルダを指定します。各フォルダからランダムに画像を取得し、背景と人物を合成して新しい画像を作成していきます。mov_valの変数は、背景に対して合成される人物の位置やサイズのランダム量を設定しています。また、背景レイヤーには左右反転もランダムに適用されてから合成されます。スクリプトが終了すると、指定した画像とマスクフォルダの中にそれぞれresultフォルダが作成され、中には新たに合成された画像とマスクが保存されています。
この二つを使用して、次回はUnetを使ってセグメンテーションを学習させてみたいと思います。