OpenCVとPIL(Pillow)の画像変換

概要

Pythonで画像を扱う場合はPillowやOpenCVを用いると思います。ですが、OpenCVで開いた画像はNumpyのndarray型であるため、Pillowで処理することはできません。そこで相互のライブラリでの画像変換についてまとめました。

やりたいこと

OpenCVの関数imread()で画像を開いた後に, PIL Imageの形式に変更して使う。

OpenCV→PIL Image

Open CVimread()で開いた画像はnumpyのndarray型となっている。これをPIL Image型にするためには

  • OpenCVで開いた場合はBGRフォーマットとなっているためRGBフォーマットに変更する
  • ndarray型をPIL Image型にする

必要がある。前者は cv2.cvtColor()関数 後者は Image.fromarray()関数 を用いることでできる。以下にコードを示す。

import cv2
import numpy as np
from PIL import Image

img = cv2.imread("hoge/hoge/img.png")

# BGR画像→RGB画像に変換する
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
im_pil = Image.fromarray(img)

pythonのイテレータとジェネレータについて

pythonでのイテレータとジェネレータについてのまとめ。

はじめに

イテレータとジェネレータの特徴について短くまとめると以下のようになります。

  • イテレータ
    配列のような集合的データ構造に含まれている要素を順に1個ずつ取り出せるオブジェクト。要素を取り出す際に、どこまで取り出されているかという状態を記憶しています。
  • ジェネレータ
    イテレータを作る関数, yieldで要素を生成できる

イテラブルとイテレータ

まずイテレータについて説明するためにイテラブルについて紹介します。 Pythonにおけるリスト、文字列、タプル、辞書等はイテラブルです。イテラブルとは要素を順に取り出すことができるオブジェクトのことです。 例えばリストであれば以下のようにfor文で一個ずつ要素を順に取り出せます。

numbers=[1,2,3]
for num in numbers:
    print(num)
#実行結果: 1 2 3

このイテラブルはイテレータと似ていますが、要素をどこまで取り出したかを記憶しているかという点で異なります。例えば、for文でリストの1つ目の要素を取り出した際、リスト自体はどの要素まで取り出されたか記憶していません。一方、イテレータでは要素を取り出した際に何個目までの要素を取り出したかを状態として保存しています。

イテラブルからイテレータを作る

リストからイテレータを作ってみます。 イテラブルからイテレータを作る際はiter()を使うとできます。

numbers=[1,2,3]
numbers_iter=iter(numbers)
type(numbers_iter)
#実行結果: <class 'list_iterator'>

続いて、作ったイテレータから要素を取り出していきます。要素を取り出す際にはnext()を使います。

>>> next(numbers_iter)
1
>>> next(numbers_iter)
2
>>> next(numbers_iter)
3
>>> next(numbers_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

クラスでイテレータを自作する

クラスを定義して自分でイテレータを作ってみます。

class SampleIterator(object):
     def __init__(self, *args): # 引数名に*を付けると可変長引数として扱われます
         self._numbers = args
         self._i = 0
     def __iter__(self):
         return self
     def __next__(self):
         if self._i == len(self._numbers):
             raise StopIteration()
         value = self._numbers[self._i]
         self._i += 1
         return value
sample_iterator(1,2,3)

sample_iterator = SampleIterator(1,2,3)
for num in sample_iterator:
     print(num)

# 1
# 2
# 3

ジェネレータ関数でイテレータを作る

ジェネレータを使うとクラスを書かなくても簡単にイテレータを作ることができます。以下に例を示します。

def my_generator(*args):
    for i in args:
        yield i

g = my_generator(1,2,3)
next(g)
# 1
next(g)
# 2
next(g)
# 3
next(g)
#Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# StopIteration

next()を使わなくても, またfor文で以下のように要素を取り出すことができます。

g = my_generator(1,2,3)
for num in g:
     print(num)

# 1
# 2
# 3

ジェネレータを使うメリットとしては以下の2つが挙げられます。

  • 必要なときに要素を生成するため, 計算コストやメモリの節約につながる
  • あらかじめ繰り返す要素をすべて生成できない場合にも使える

あと, yieldって関数のreturnと似ているので勘違いしやすいのですが,

  • returnは要素を返した後, 関数呼び出しが終了
  • yieldは値を返した後も返す要素がまだあれば終了しない

という違いがあります。

ジェネレータのまとめ

  • ジェネレータはyieldを使って実装する
  • yieldした回数だけ要素が生成される
  • ジェネレータ関数を関数呼び出しするとイテレータのオブジェクトになる

参考文献

大重 美幸, 詳細! Python 3 入門ノート, ソーテック社, 2017/5/24

【便利】pythonでのdefaultdictの使い方

概要

Pythonのdict型は{key: value}のペアを保持するが、存在しないkeyに対しての操作はできない。 このような場合はdefaultdictを利用すると便利な場合がある。

通常のdict

pythonで辞書型のdictを使うと新しいkeyに対して毎回初期化を行う必要がある。
例えば、以下のようなリストの各要素の出現数を数えてみる際に各keyに対して初期化をしないと、、、

d = {}
x_list = [1, 2, 3, 4, 1]
for key in x_list:
     d[key]+=1

# Traceback (most recent call last):
# File "<stdin>", line 2, in <module>
# KeyError: 1

KeyErrorが発生する。 このようにdictは存在しないキーに対して操作を行うとエラーが発生する。

したがって、この場合は、キーが存在するかを確認してキーを生成する必要がある。

d = {}
x_list = [1, 2, 3, 4, 1]
for key in x_list:
     if key not in d: #キーが存在しない場合の処理
         d[key] = 0
     d[key]+=1

defaultdictを使ってみる

defaultdictではdictの初期化を勝手にやってくれるので、毎回キーの存在確認をする必要がない。

from collections import defaultdict
d = defaultdict(int)
x_list = [1, 2, 3, 4, 1]
for key in x_list:
     d[key] +=1
print(d)
# defaultdict(<class 'int'>, {1: 2, 2: 1, 3: 1, 4: 1})

defaultdictのインスタンスを生成するときの引数には"初期化時に実行する関数"を記述する。 上記の例のように「int」と記述した場合は「lambda: int()」, 「lambda: 0」と同じ意味になる。

from collections import defaultdict
d = defaultdict(lambda: 0)
x_list = [1, 2, 3, 4, 1]
for key in x_list:
      d[key] +=1
print(d)
# defaultdict(<function <lambda> at 0x00...>, {1: 2, 2: 1, 3: 1, 4: 1})

またdefaultdictにはlist()やfloat(), dict()といった関数を渡すこともできる。 list()の場合で試してみると,

d = defaultdict(list)
for key in x_list:
     d[key].append(key)
print(d)
# defaultdict(<class 'list'>, {1: [1, 1], 2: [2], 3: [3], 4: [4]})

のようにlistの要素に追加することができる。

pythonで集合setに要素を追加・削除する方法

概要

pythonのsetにおいて要素を追加・削除する方法を説明する。

集合に要素を追加

add()で行う

s = {1, 2, 3}
s.add(4)
print(s)
# {1, 2, 3, 4}

集合の要素を削除

集合の要素を削除するメソッドは、 discard(), remove(), pop(), clear()の4つがある。

  • discard()について
    引数に指定した要素を削除。集合に存在しない要素を指定した場合は何もしない。
s = {1, 2, 3}
s.discard(2)
print(s)
# {1, 3}
s.discard(100)
print(s)
# {1, 3}
  • remove()について
    引数に指定した要素を削除。集合に存在しない要素を指定した場合はKey Errorとなる。
s = {1, 2, 3}
s.remove(2)
print(s)
# {1, 3}
s.remove(100)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# KeyError: 100
  • pop()について
    集合から要素を削除する。どの要素が削除されるかは選べない。空集合に対して実施した場合はKey Errorとなる。
s = {1, 2, 3}
v = s.pop()
print(v)
# 1
print(s)
# {2, 3}

s = {1}
s.pop()
# 1
s.pop()
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# KeyError: 'pop from an empty set'
  • clear()について
    clear()メソッドは集合のすべての要素を削除し、空集合にする。
s = {1, 2, 3}
s.clear()
print(s)
# set()

pythonでのsortとsortedの違い

概要

pythonでリストの要素を降順または昇順にソートする際に使う

  • sort()
  • sorted()
    の違いについてまとめる。
    ひとことでまとめると
  • sort() : リスト型のメソッドで、元のリストをソートして上書き
  • sorted() : 元のリストをソートした新しいリストを生成する
    となっている。

sort()について:元のリストをソートして上書き

sort()はリスト型のメソッドで、ソート結果が上書きされる。破壊的処理。

  • 昇順でソートする場合
a = [3, 2, 1]
a.sort()
print(a)
# [1, 2, 3]
  • 降順でソートする場合
a = [1, 2, 3]
a.sort(reverse=True)
print(a)
# [3, 2, 1]

sorted()について:ソートした新しいリストを生成

sorted()は引数にソートしたいリストを指定すると、ソート済みのリストを新しく生成して返す。

  • 昇順でソートする場合
a = [3, 2, 1]
new_a = sorted(a)
print(new_a)
# [1, 2, 3]
  • 降順でソートする場合
a = [1, 2, 3]
new_a = sorted(a, reverse=True)
print(new_a)
# [3, 2, 1]

shellスクリプトで絶対パスを取得する方法[bash]

概要

shellを実行した際に、スクリプトファイルが存在する絶対パスを取得したいときがある。
スクリプトが存在するディレクトリの外部から実行してもカレントディレクトリを取得できる方法を紹介する。

コード

SCRIPT_DIR=$(cd $(dirname $0); pwd)

説明

実行例

ソースコード test.sh

#!/bin/bash
SCRIPT_DIR=$(cd $(dirname $0); pwd)

echo $SCRIPT_DIR

実行結果

$ bash test.sh
/mnt/c
$ mkdir hoge
$ cd hoge/
$ bash ../test.sh
/mnt/c

tensorflowjs+GANで誰でもギャルになれるWebアプリを作ってみた

f:id:NdBlog:20210304175041p:plain

1. はじめに

こんにちは、大学生のNdです。私は最近、機械学習に興味があり、いろいろな人に遊んでもらえるツールが作れないものかと考えていました。そこで今回、誰でもギャルになれるBegyaruというサイトを作ってみました。本記事ではこのサイトの遊び方と使った技術の簡単な紹介をしていきます。

2.なにができるの?・どんな問題が解決できるの?

みなさんはガングロギャルという単語をご存知でしょうか。ガングロギャルとは90年代から2000年代に流行った、真っ黒に日焼けして髪の色を金髪やピンクにするギャルたちのことです。

f:id:NdBlog:20210304172641p:plain

このガングロメイクですが、メイクに非常に手間がかかったり、紫外線で焼く場合は肌に大きな負担がかかる問題があります。そこで、今回作ったサイトでは、肌を焼いたりせずとも顔の画像をアップロードするだけでブラウザ上で顔検知および変換を行い、ガングロギャルになった姿を見ることができます。

また、今回作ったサイトでは個人情報の流出を防ぐことができます。

f:id:NdBlog:20210304173248p:plain

これはTensorflowjsというライブラリを使用してユーザのPCおよびスマホ上で画像の変換を行っているからです。言い換えると、アップロードした画像は外部に送信されないので、安心して自分の画像を使い遊ぶことができます。さらに、開発者にとっても、個人情報の流出という問題と推論を行うのに必要なサーバがいらなくなるという大きなメリットがあります。

3.使い方

Begyaruにアクセスすると以下のページが開かれると思うので、"ファイルを選択"のボタンをおして、正面を向いた一人だけ映っている画像をアップロードしてください。変換が始まり5~20秒程度で変換できます。 f:id:NdBlog:20210305043724p:plain

Chrome, Firefox, Edgeでは動作確認しています。
Windows, Android, Macでは動作確認できていますが、iPhoneだと動かない場合があります。(iPhoneを持っていないのでデバッグができません...)

4.サービスの仕組み

サービスの仕組みについて以下の図を使いながら紹介します。

f:id:NdBlog:20210304212622p:plain

まず図の左上の水色の範囲をご覧ください。これはサイトのWebサーバを表しています。サイトのWebサーバには主に

機械学習モデル

・推論用のjavascriptコード

が配置されています。まず、1つ目の機械学習モデルはこのサイトの核となるギャル変換を実現するためのモデルです。こちらは自分でデータを集めて学習させたモデルで、顔写真を入力すると出力がギャルとなって出てきます。2つ目の推論用のjavascriptコードは、ユーザのブラウザ上で機械学習モデルをロードし、推論するために必要となるソースコードです。主に画像の加工を行ったり、tensorflowjsの関数を呼び出したりします。

続いて左下はCDNサービスを表しており、このネットワークからTensorflowjsのライブラリやCSSフレームワークのBootstrapが提供されます。

そして最後に、右側のユーザ側では端末上のブラウザが先ほどのWebサーバとCDNから必要なファイルをロードし、サービスを利用できる状態となります。

5.どういった仕組みで変換しているのか


変換には敵対的生成ネットワーク(GAN)という技術を使いました。以下の図を使って簡単な説明をします。 f:id:NdBlog:20210305191208j:plain まずGANでは二人の主要キャラクターがいます。一人は左側のイラストレーターである生成器、もう一人は右側の鑑定士である識別器です。まずイラストレーターは普通の顔写真をもらってガングロギャルの絵に書き換えます。そして書き換えられた偽物の写真は鑑定士に渡されます。鑑定士は偽物と本物のガングロギャル写真がランダムに与えられ見抜けるように学習します。最初、イラストレーターは超初心者であるため、おおよそガングロギャルとは思えない下手な画像を描いてきます。したがって鑑定士側は容易に本物か見抜けるわけですが、しばらくしているとイラストレーターは経験を積み鑑定士を騙すような本物そっくりの画像を送ってきます。そこで鑑定士側も負けじと以前より細かい部分まで特徴をチェックするようになります。このようにイラストレーターと鑑定士がお互いに敵対しあって学習を進めることで、最終的にイラストレーターは人間ですら本物と見間違うような本物そっくりのガングロギャルを描いてくれるまでに成長します。これが敵対的生成ネットワーク(GAN)の仕組みです。(さらに詳しい技術については「CycleGAN」で調べてみてください。)

6.tensorflowjsを使う際のポイント

続いて、tensorflowjsを利用して気付いた点を紹介します。

  • 学習させたモデルをtensorflowjsで扱えるモデルに変換する。
    学習済みのモデルはそのままではtensorflowjsで利用できないためtensorflowjs_converterを使って変換を行う必要があります。 kerasを利用した場合、モデルは以下のようにh5形式で保存します。
model.save('my_model.h5')

この際、モデル全体を保存するように気を付けてください。つまり

model.save_weights('my_model_weights.h5')

は使わないようにしてください。これはsave_weights()を使うとモデルの構造が失われてしまい、tensorflowjsモデルに変換できなくなるためです。 続いて、実際にtensorflowjsモデルに変換していきます。まずtensorflowjsのインストールをします。

pip install tensorflowjs
#成功すると"Successfully installed tensorflowjs-x.x.x"と表示される。

こちらが成功したらいよいよ変換です。以下のコマンドを実行してみましょう。

tensorflowjs_converter --input_format=keras --output_format=tfjs_layers_model
 h5_model_file_path tfjs_model_output_path

コマンドオプションについて、--input_formatはkerasを指定、--output_formatはtfjs_layers_modelを指定します。続いて、h5ファイルのパスと、出力先のパスを指定してあげましょう。なお、この設定のままコマンドを実行すると32bitFloat型でモデルが保存されるのですが、サイズを小さくしたい場合は--quantize_uint16--quantize_uint8オプションを利用してみましょう。多少推論精度が落ちてしまいますが、半分や1/4の大きさまで圧縮することができます。ほかにもいろいろなオプションがあるので、

tensorflowjs_converter --help

で適宜自分の使いたい機能を利用していきましょう。

  • バックエンドの指定
    tensorflowjsでは使用できるバックエンドがcpu,wasm,webglの中から選択できますが、極力webglを利用するようにしましょう。
tf.setBackend('webgl');

WebGLを利用することでGPUを利用することができ、cpuバックエンドと比較して10倍近く速い推論が実現できます。もしWebGL Backendが無理な場合はWebAssembly(wasm)を利用しましょう。

  • Tensorflowjsでは自分でメモリ開放をする必要がある
    pythonでtensorflowやnumpyを利用したとき、あまりメモリ開放について考えることはないでしょう。ですがtensorflowjsでは既に使った配列(テンソル)はメモリ開放してあげないといけません。 例えば以下のような場合
//まずモデルをロードする
const model = await tf.loadLayersModel(model_path);
//tensorを入力し推論結果をpredictionに代入
const prediction = await model.predict(tensor);

~なんらかの処理~
//predictionは利用済み
tf.dispose(prediction); //predictionを破棄し、メモリ開放する。

最後の処理のようにtf.dispose()関数を利用してメモリ開放してあげてください。

7.おわりに

ブログを書くのは初めてだったのですが、作ったツールについて紹介することができたので良かったです。 ブログの内容に関する指摘や実際にツールを使ってみた感想等はTwitter等にお寄せください。