np.isin()について

概要

np.where()を使って特定の条件を満たす項目を抜き出す処理は良く行う。 今回はその発展として、複数の値に一致する項目を抜き出してきたいという状況を考える。 そのような状況では、np.isin()を使えばよいという話。

詳細

具体的には以下のようなフルーツ( fruits )と食べたいものリスト( i_want_to_eat )があるとして、自分の食べたいものに該当するインデックスを知りたいとする。

>>> fruits = ['apple', 'ringo', 'banana', 'mikan']
>>> i_want_to_eat = ['ringo', 'mikan']

np.isin()を使うと以下のようにfruitに入っている複数のフルーツの内i_want_to_eatに入っているフルーツに該当する部分がTrue、それ以外にFalseが入った配列が返ってくる。

>>> fruits = np.array(fruits)
>>> i_want_to_eat = ['ringo', 'mikan']
>>> np.isin(fruits, i_want_to_eat)
array([False,  True, False,  True])

もし配列からそのフルーツのみ抜き出したければ、普通にnumpyのブールインデックス参照を行えばよい

>>> fruits[np.isin(fruits, i_want_to_eat)]
array(['ringo', 'mikan'], dtype='<U6')

以上。 普通にマニュアルに書いてあるけど、ちょっと調べてなかなかたどり着けなかったので、メモ代わりに書いた。

画像処理にエンジニア検定のエキスパートに合格した

概要

画像処理にエンジニア検定はCG-ARTS協会が行っている検定の一つ。

自分が受けた2018年後期の検定の合格率は40.4%だった。 https://www.cgarts.or.jp/kentei/result/passing.html

真面目に勉強していればまぁまぁ受かるレベルである。

得られたもの

当然ながら資格が得られたわけだが、それは目的にはしていなかった。

自分は最近、仕事で深層学習による画像処理を行っている。 深層学習がいかに便利で有用であっても画像処理を知らずに仕事にはならないわけだが、 画像処理を始めたころは、Webを調べて出てくる断片的な情報だけでその場をしのいでいた。

そこで、流石にまずいということで体系的な勉強の必要性に迫られて、この検定にたどりついた。

結果的にはこの検定の勉強に必要な教本は非常に役に立った。 「ディジタル画像処理」という本なのだが、画像に触るなら検定を受けなくてもいいから、この本は読むべきである。

内容が網羅的、かつ体系的にまとまっている。

ちなみに検定自体は、それほど難しい問題は出ないが、上記の本には数式がちらほら出てくる。 とりあえず、サラッと意味するところを捉える程度で良いと思う。 実務で使うときに真面目に読みこめば良いのではないか。

MNISTとFashion-MNISTのPyTorchのデータローダ

概要

MNISTとFashion-MINISTを組み合わせて学習してみたかったが、既存のデータセットだと多分無理っぽかったので、自作してみた。(車輪のなんとやらではないという言い訳)

ここでは組み合わせるところまでは書かず、一般的なMNISTのローダをどのように書けばよいかが分かるようなものを記載したいと思う。

もし同じような動機に駆られたら参考にしてほしい。

ちなみに、単純にMNISTをPytorchで読み込むだけだったら、以下でいける。とても簡単。

train = torch.utils.data.DataLoader(MNIST('data', train=False))

準備

MNISTおよびFashion-MNISTのバイナリデータ(gzipで圧縮済み)をダウンロードしてきて、 適当なディレクトリに入れておく。 自分の場合は/share/datasets/mnist/data/の下に入れておいた。

MNISTは以下のページにダウンロード元が記載されている。

http://yann.lecun.com/exdb/mnist/

Fashion-MNISTは以下のページにダウロード元が記載されている。

https://github.com/zalandoresearch/fashion-mnist

データの中身

データの中身はバイナリになっている。

ラベルは1バイトにつき、1つのラベル(0-10)が入っている。(たしか)

画像の場合は1バイトが1ピクセルに対応して、0-255までの値が入っている。(多分)

ソースコードと解説

import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
import gzip
import os
import numpy as np
from PIL import Image


class MNIST(Dataset):
    def __init__(self, kind="MNIST", train=True):
        assert(kind in ["MNIST", "Fashion"])
        self.train = train
        if kind == "MNIST":
            self.data_dir="/share/datasets/mnist/data/mnist/"
        else:
            self.data_dir="/share/datasets/mnist/data/fashion/"

        if self.train:
            self.files = {
                "images": "train-images-idx3-ubyte.gz",
                "labels": "train-labels-idx1-ubyte.gz"
            }
        else:
            self.files = {
                "images": "t10k-images-idx3-ubyte.gz",
                "labels": "t10k-labels-idx1-ubyte.gz"
            }

        self.init_images_labels()

        self.augmentor = transforms.Compose([transforms.RandomHorizontalFlip(),
                                             transforms.RandomRotation(degrees=30),
                                             transforms.RandomVerticalFlip()])
        self.loader = transforms.Compose([transforms.ToTensor()])


    def __len__(self):
        return len(self.X)

    def __getitem__(self, idx):
        image = self.X[idx]
        label = self.y[idx]

        image = self._load_image(image)

        label = np.expand_dims(label, axis=0)
        label = torch.from_numpy(np.array(label))

        return image, label

    def _load_image(self, image):
        image = Image.fromarray(image)
        # Gray scale to RGB
        # image = image.convert('RGB')
        if self.train:
            image = self.augmentor(image)
        image = self.loader(image).float()
        return image

    def init_images_labels(self):
        self.y = self._read_labels_from_binary(os.path.join(self.data_dir, self.files["labels"]))
        self.X = self._read_images_from_binary(os.path.join(self.data_dir, self.files["images"]))

    def _read_labels_from_binary(self, filepath):
        with gzip.open(filepath, mode="rb") as f:
            data = f.read()
            array = np.fromstring(data, dtype='<u1')
            # first 8 bytes are metadata
            array = array[8:]

        return array

    def _read_images_from_binary(self, filepath):
        with gzip.open(filepath, mode="rb") as f:
            data = f.read()
            array = np.fromstring(data, dtype='<u1')
            # first 16 bytes are metadata
            array = array[16:]
            len_array = len(array) // 28 // 28
            array = array.reshape((len_array, 28, 28))

        return array

def main():
    mnist = MNIST(kind="Fashion")

if __name__ == "__main__":
    main()

多くはPyTorchに共通する処理なので、説明を割愛し、MNISTに特化した部分について説明する。

init

MNISTとFashion-MNISTはひとつのデータは同じ大きさになっているし、データの数も全く一緒である。 したがってファイルパスの設定を変えれば、MNISTとFashion-MNISTをスイッチできる。 それが冒頭の__init__()の中で定義されたkindの役割である。

_read_labels_from_binary

ラベルをバイナリから読み取るために、ファイルをgzipライブラリで解凍し、numpyのfromstringで配列にする。 読み込み元はバイナリなのだが、まぁそれは置いておいて、<u1で1バイトごとに符号なし整数として読み込んでいる。 冒頭の8バイトはこのデータ全体の説明になっている。詳しくは MNISTの公式ページにある「TRAINING SET LABEL FILE (train-labels-idx1-ubyte)」を読むこと。 データ数等が書いてあるが、正直要らないので、読み捨てにする。

_read_images_from_binary

ラベルとほぼ同じ処理を行ったうえで、冒頭の16バイトをラベルと同じ理由で捨てる。 そしてひとつのデータは28x28の長さの1次元のベクトルなのだが、後で画像として扱うためにreshapeして2次元のベクトルに直している。 reshapeの処理が不安だったので、書いている途中でPIL.Image.fromarray(image).show()として表示される画像をみて確認はしておいた。

以上、何か問題など見つけたら教えてください。

OpenCVでconvexHullで穴埋めされた領域を取り出す

概要

Opencvで輪郭を取得して、更に凸包領域を取り出すのはとても簡単にできる。 以下の公式のとおりである。 領域(輪郭)の特徴 — OpenCV-Python Tutorials 1 documentation

ところで、この凸包領域と元の輪郭の間にある凹み領域を入手したくなったので、入手する方法を考えて実装してみた。

結果はこうなった。ちゃんと取得できた。

f:id:KYudy:20181002123159p:plain

実装

実装は以下のようになった。 重要なのはimage_cave = image_baggy - image_tightである。 OpenCVの画像を二値化して得られたNumpyの配列を引き算している。凹み領域は凸包と輪郭の差分として得られた。

import cv2
import numpy as np
import matplotlib.pyplot as plt

def main():
    image = cv2.imread("./figure.png")
    image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, image_gray = cv2.threshold(image_gray, 127, 255, 0)

    _, contours, _ = cv2.findContours(image_gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    hulls = [cv2.convexHull(contour) for contour in contours]

    image_contour = cv2.drawContours(image_gray.copy(), contours, -1, (0,255,0), 1)
    image_hull = cv2.drawContours(image_gray.copy(), hulls, -1, (0,255,0), 1)

    image_tight = np.zeros((image_gray.shape), np.uint8)
    image_baggy = np.zeros((image_gray.shape), np.uint8)

    for contour, hull in zip(contours, hulls):
        cv2.fillPoly(image_tight, pts =[contour], color=(255,255,255))
        cv2.fillPoly(image_baggy, pts =[hull], color=(255,255,255))

    # diff generates a caved image.
    image_cave = image_baggy - image_tight

    image = np.hstack([image_gray, image_cave])

    cv2.imshow("image", image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

オープンソースカンファレンスでサイコロの認識を展示してきた

概要

以下の記事の続きである。

機械学習でサイコロの目をカウントするモデルを作成する - いわゆる備忘録

OpenCVでサイコロのでた目の数をカウントする - いわゆる備忘録

オープンソースカンファレンスカタンを電子化したものを作って展示したので、それに関して書きたいと思う。

詳細

そもそもサイコロの目の認識は私の所属する札幌電子クラフト部の活動で、カタンを自動化するために必要だった。

オープンソースカンファレンスに出展! – サッポロ電子クラフト部

カタンボードゲームの中でも最も有名なものだと思う。資源を集めつつ、道、家などを立てていき、勝利ポイントを満たすまでひたすらサイコロを振るゲームだ。

このゲームを早く回すコツはサイコロを振った後に、そのサイコロに該当するを見つけて、資源を該当する人に配るプロセスを早くすること。 自分はこのプロセスを半自動化するためにサイコロの目を画像認識するシステムを作った。

ちなみに上記記事からもだいぶ修正を入れた。 変更点↓ * 画像の枚数を増やした * オリジナルのCNNからResNet18へ変更 * 各種オーグメンテーションを導入

実は正確にサイコロの目を認識するためにはtestやvalidationが95%程度などでは全然足りない、ということが実際にやってみてわかったのだ。 照明や背景などの様々な違いがある実環境でも堅牢に動作するためには99%以上の精度が必要だった。

オーグメンテーションを工夫したり、ResNetを使ったりする工夫したのだが、それはそれほど大きな改善を産まなかった。 結局は画像の枚数を増やすことが一番有効だった。 だいたい2,000枚程度の教師画像ができたあたりで実用的になったと感じた。 深層学習は力技が一番効く。

また、もう一つ苦労したのはRaspberry Piだ。 Pytorchを入れて使ったのだが、ライブラリのコンパイルに時間がかかるわ、動作してもモデルにforward()したあたりでたまにセグメンテーションフォルトがおきるわで、なかなか苦労させられた。Raspberry Piに載せるならTensorFlowがベストだと思われる。最近公式にサポートしたらしいので。

TensorFlow 1.9 Officially Supports the Raspberry Pi

これからモバイルで動く深層学習ベースの画像認識の需要は上がっていくだろう、MobileNet, SSD, Yoloなどの物体認識はすでにできている。 今回は問題を分類としたため、認識できるサイコロの数が固定になってしまったが、物体認識に切り替えれば任意の個数のサイコロの認識が可能になるだろう。 今度チャレンジしたいと思う。ただ、物体認識の教師データ作るのがめんどくさいんだよね・・・。誰かに手伝ってもらわなければ。

以上。

np.uniqueの結果をカウントで並びかえる

np.uniqueを使うと、配列の中の重複する要素を一つにしてくれる(関数名からもわかるぐらい単純な動作) これだけでも十分なんだけど、要素の数をキーにして並び変えたいと思うことがあるはず。

shellならば以下でできる。

$ cat $FILE | uniq -c | sort -n

numpyの場合はひと工夫が必要

まずはこんな感じの配列をユニークにするとしよう。

>>> X = np.random.randint(10, size=20)
>>> X
array([6, 5, 9, 8, 3, 3, 9, 8, 7, 0, 0, 7, 9, 8, 9, 6, 6, 1, 9, 7])

特に工夫なくnp.uniqueを実行するとこうなる。

>>> np.unique(X)
array([0, 1, 3, 5, 6, 7, 8, 9])

ユニークな配列はその要素の辞書順にソートされる。

ここでnp.uniquereturn_counts=Trueという引数を渡すと、それぞれの要素が元の配列にいくつあったのかを返してくれる。

>>> np.unique(X, return_counts=True)
(array([0, 1, 3, 5, 6, 7, 8, 9]), array([2, 1, 2, 1, 3, 3, 3, 5], dtype=int64))

これを利用してユニークな配列を要素のカウント数順に並び変える。 argsortでindexを指定するのがややトリッキーな感じはあるが、シンプルに目的を達していることがわかってもらえるかと思う。

>>> uniq, count = np.unique(X, return_counts=True)
>>> uniq[np.argsort(count)]
array([1, 5, 0, 3, 6, 7, 8, 9])

機械学習でサイコロの目をカウントするモデルを作成する

最近機械学習が楽しい。時代の波に乗っているものというのは遊んでも仕事に使っても楽しいものだ。

先日サイコロをOpenCVでカウントするプログラムを作ってみたが、これを機械学習で実現してみたいと思ったのでやってみる。

kyudy.hatenablog.com

機械学習で実装すると、認識が堅牢になることが期待される。

サイコロがひとつ写った画像を認識するモデルを作成

Chainer(Preferred Networks社謹製)でさくっとかいた。

(勉強には時間がかかった。さくっとかけるのはChainerが素晴らしいだけだ。肝に銘じよう)

モデルはたった5層のCNN(Convolutional Neural Network)。

最近流行りの深層学習からすると、だいぶシンプルなモデルで十分な性能を得ることができる。

https://github.com/yudai09/count_saikoro/blob/master/cnn/single/models/CNN.pygithub.com

入力画像の例

f:id:KYudy:20180417223004j:plain

この入力画像に対するラベル(教師)は1となる。

処理概要

  • サイズが(32, 32, 3)の小さな画像を入力(撮影したあとで圧縮した)
  • 畳み込み層を3層、全結合層を2層に設定
  • 最後の層(出力に最も近い層)の活性化関数をsoftmaxに設定しクラス分類

単一のサイコロの写真を2つ写し合計を計算するモデルを作成

仕事で多入力、1出力のモデルを開発する必要があったので、 実験として作ってみた。

github.com

画像は上と同じ(サイコロがひとつ写った)画像を流用した。

異なるのは

  • 写真を2つを同時にネットワークに入力していること
  • その2つの写真のサイコロの目を足し合わせたものを正解のラベルとして扱っていること

フレームワークはいろいろ試してみたかったのでPytorchを使ってみた。 Kerasも使ってみたが、簡単に使える反面で柔軟性にかけるし、生のTensorflowは触る気が起きなかった。 そしてChainerと同じぐらい柔軟で、海外でも流行っているというPytorchに魅力を感じた。 Caffeがよい、と言う話も見かけたが、どうなんだろう。後日試してみたいと思う。

ちなみに、このレベルの実装だと、Chainerとの違いは全結合層の手前でview()を呼ぶことぐらいかなぁという感じだ。

もっと実装を重ねていけば見えてくるものもあるだろう。

モデルはこんな感じ。

class MyModel(torch.nn.Module):
    def __init__(self, nb_class):
        super(MyModel, self).__init__()

        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, 5),
            nn.Conv2d(64, 64, 5),
            nn.Conv2d(64, 128, 5))

        self.conv2 = nn.Sequential(
            nn.Conv2d(3, 64, 5),
            nn.Conv2d(64, 64, 5),
            nn.Conv2d(64, 128, 5))

        self.fc = nn.Sequential(
            nn.Linear(128 * 20 * 20 * 2, 120),
            nn.Linear(120, 84),
            nn.Linear(84, nb_class),
        )

    def forward(self, x, y):
        x1 = self.conv1(x)
        x2 = self.conv2(y)

        x1 = x1.view(-1, 128 * 20 * 20)
        x2 = x2.view(-1, 128 * 20 * 20)

        x1x2 = torch.cat((x1, x2), 1)

        return self.fc(x1x2)

2つの同じ形状の畳み込み層にそれぞれサイコロの写真を入力し、その出力をtorch.catで結合し全結合層へ入力している。 早々にValidationデータに対する正答率が100%に近づく。気持ちいいぐらいに。

OpenCVとおなじことを実現するのはサボった

2つのサイコロの写真の合計の目を出力するモデルの作成は割愛した。 同じことをすれば多少なりとも比較ができてよいのだが、興味がなくなってしまったのだ。

また、必要がでてきたら実装したいと思う。