よちよちpython

独習 python/Qpython/Pydroid3/termux

スマホで手書き数字画像を作成しNumpyで加工するメモ

何の役に立つか分かりませんが、スマホで手書きの数字画像を作成し、Numpyで28×28ピクセルにサイズ縮小して保存する方法をメモする。
機械学習の勉強でなんか使えそうだろう。



実行環境


ソニーAndroidスマホ
termux
Python3.8
JupyterNotebook

お絵描きスマホアプリ「sketch」

目次


使ったもの


  • JupyterNotebook : Pythonスクリプトのコーディングと実行、画像の表示

  • お絵描きアプリ : スマホに元々入っていたsketchというアプリ

  • Pythonライブラリ : NumpyとPillow
    pipコマンドなどでinstallしておく

  • 画像表示用モジュール : IPython.display.Image
    JupyterNotebookが使えるなら直ぐつかえます



手順


  1. 手書き数字の画像を作る

  2. 画像処理用PythonライブラリPillowで画像取り込み、グレイスケール化、画像縮小、保存

  3. 数値計算PythonライブラリNumpyで加工等



手書き数字画像の作成


今回はスマホ(ソニー製)に元々入っていた絵描きアプリ「sketch」を使って手書き数字の画像を作ります。
機械学習の勉強でよく使われている画像サイズが28×28ピクセルの正方形なので、画像を作成する時も後で加工しやすいように「正方形1080×1080ピクセル画像」で作りました。

新規作成で何か適当な数字を黒ペンで手書き。ペンの種類や字の太さはデフォルト設定のもので行いました。
できた画像はJupyterNotebookの作業フォルダなど適当な場所に保存。
その他、細かい作成方法は省略します。
sketch以外のお絵描きアプリでも構わないと思います。



書いた画像をJupyterNotebookで表示


JupyterNotebookに表示させます。

# 画像表示モジュールのインポート
from IPython.display import Image

# 画像ファイルパス
image_file= "sketch-0.png"

# 表示
Image(image_file)

f:id:chayarokurokuro:20200119074128p:plain



画像ファイルの取り込みとモード等の確認


CSVExcelファイルなどを開く時に専用モジュールを使うのと同じように、画像ファイルを取り込む為にPillowのImage.open()を使います。

取り込んだら念のため画像情報の「フォーマット」「サイズ」「モード」を確認しておきます。

参考

「モード」を変えると画像の色が変わります。画像を使って説明されています。

PIL/Pillow チートシート - Qiita

# 画像ファイルの取り込み用
from PIL import Image

# 画像ファイルパス
image_file = "sketch-0.png"

# 画像取り込み
im = Image.open(image_file)

# 画像フォーマット、サイズ、モード表示
print(im.format, im.size, im.mode)
PNG (1080, 1080) RGBA

と分かりました。



リサイズ 画像を縮小する


Pillowを使ってサイズを28×28ピクセルに縮小します。

縮小する理由は、機械学習で手書き数字画像の判別をする時によく使ってあるサイズがこれだからというだけで、特に意味はありません。
デカ過ぎると計算が大変、小さすぎると判別し難いなどの理由からこのサイズを使ってあるのかしらん。

# 画像ファイルの取り込み用
from PIL import Image

# 画像ファイルパス
image_file = "sketch-0.png"

# 画像取り込み
im = Image.open(image_file)

# 28×28ピクセルにする
im_resize = im.resize((28, 28))

# 縮小後の画像フォーマット、サイズ、モードの表示
print(im_resize.format, im_resize.size, im_resize.mode)
None (28, 28) RGBA

フォーマットが消えましたね。

参考

[https://water2litter.net/rum/post/python_pil_image_attributes/:title]

「戻り値の型はstrです。Imageオブジェクトにフォーマットが設定されていない場合はNoneが返ります。」

Pillowオフィシャル

[https://pillow.readthedocs.io/en/latest/handbook/image-file-formats.html:title]



pillowのImage.open(画像ファイル)で開いた際はその拡張子でformatを自動判断するようだ。save()する際につける拡張子でも変わる。
上でresizeした時にformat情報が消されたのかな、知らんけど。大して重要でないので飛ばして次にいく。



縮小画像の保存と表示


Pillowで縮小画像の保存、IPython.displayでJupyterNotebookに画像表示をさせます。

Pillowで保存の時は
イメージオブジェクト.save(保存ファイル名)

# 画像ファイルの取り込み用
from PIL import Image

# 画像ファイルパス
image_file = "sketch-0.png"

# 画像取り込み
im = Image.open(image_file)

# 28×28ピクセルにする
im_resize = im.resize((28, 28))

# Pillowで画像の保存
im_resize.save("sketch-0_resize28.png")

# IPythonで画像表示
from IPython.display import Image
Image("sketch-0_resize28.png")

f:id:chayarokurokuro:20200119074230p:plain

灰色の小さな米つぶ画像が表示された。保存されたようです。

画像を表示させるために書いているfrom IPython.display import Imageのことについて。

JupyterNotebookでは、一度セルを実行したらそのあと何度も同じ文を書く必要はありませんが、この場合書かないとTypeError モジュールが呼び出せないナンチャラカンチャラとエラーが出ました。(コメントアウトすれば確認できる)
理由がよく分からないんで推測ですが、ImageというモジュールをPillowも使っており、名前が被ってるんでエラーが出るんじゃないかと思います。
という事で面倒だが画像表示の度にそのimportを書きます。



Numpyで画像の配列化と表示


ここからは、縮小された画像をNumpyでndarrayというNumpyの配列形式に変換し、配列がどんな風になっているかなどを確かめます。

縮小画像をPillowで取り込む所から書きます。

# 画像ファイルの取り込み用
from PIL import Image

# 縮小画像ファイルパス
image_file = "sketch-0_resize28.png"

# 縮小画像取り込み
im = Image.open(image_file)

# 縮小画像フォーマット、サイズ、モードの表示
print("[画像オブジェクト]")
print(im.format, im.size, im.mode)
[画像オブジェクト]
PNG (28, 28) RGBA
# ------ ここからNumpy処理 ------

# Numpyのインポート
import numpy as np
# Numpy配列化
im_array = np.array(im)
im_array
array([[[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ...,
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]],

       [[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ...,
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]],

       [[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ...,
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]],

       ...,

       [[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ...,
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]],

       [[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ...,
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]],

       [[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ...,
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]]], dtype=uint8)

255ばかり並んでいる。

# オブジェクトの型表示
print("オブジェクトの型", type(im_array))
オブジェクトの型 <class 'numpy.ndarray'>
# 配列の形状
im_array.shape
(28, 28, 4)
# 配列の次元数
im_array.ndim
3
# 配列の全要素数
im_array.size
3136
# 配列要素の型
im_array.dtype
dtype('uint8')



一応まとめておきます。

# 画像ファイルの取り込み用
from PIL import Image

# 縮小画像ファイルパス
image_file = "sketch-0_resize28.png"

# 縮小画像取り込み
im = Image.open(image_file)

# 縮小画像フォーマット、サイズ、モードの表示
print("[画像オブジェクト]")
print(im.format, im.size, im.mode)

# ------ ここからNumpy処理 ------

# Numpyのインポート
import numpy as np

# Numpy配列化
im_array = np.array(im)

# オブジェクトの型表示
print("\n[Numpy配列オブジェクト]")
print("オブジェクトの型", type(im_array))

# 配列の形状
print("配列形状", im_array.shape)

# 配列の次元数
print("配列の次元数", im_array.ndim)

# 配列の全要素数
print("配列の要素数", im_array.size)

# 配列要素の型
print("配列要素の型", im_array.dtype)

# 配列表示(出力省略します)
#print("\nNumpy配列\n", im_array)
[画像オブジェクト]
PNG (28, 28) RGBA

[Numpy配列オブジェクト]
オブジェクトの型 <class 'numpy.ndarray'>
配列形状 (28, 28, 4)
配列の次元数 3
配列の要素数 3136
配列要素の型 uint8
  • uint8型要素で(28, 28, 4)の三次元Numpy配列

になっていることが分かった。

上で配列を表示させているが[255,255,255,255]ばかり並んでいた。

この4つの数値は、各ピクセルにおけるRGBAの値を配列で表していて、全部255のときは

  • 赤色=255
  • 緑色=255
  • 青色=255
  • 透明度=255

数値の取りうる値は0~255と決まっており、数値が大きいほどそれぞれの色が濃く、透明度は数値が大きいほど不透明になる。
全部255のときは不透明な白
縮小させた米つぶっぽいゼロの画像は周りが白だったので255がたくさん並んでいるという訳です。

255以外の数値が入っているかどうか確認しておきます。

# 255ではない値の合計数
im_array[im_array != 255].size
267
# 255ではない値を配列全体から抽出
# im_array[im_array != 255] 

267個の数値が表示されるので省略しました。



画像をグレイスケールで二次元に変換


配列が大きいほど計算が大変です。上で三次元配列になっているのを確認しましたが、これをPillowのconvert("L")という関数を使って二次元配列に変換します。
RGBAの4つの値で色を表現していたのを、関数が計算式を使って1つの数値に変換することで次元を二次元に減らします。これによりカラー写真は灰色の濃淡の画像になります。

縮小画像を取り込む所から。

# 画像ファイルの取り込み用
from PIL import Image

# 縮小画像ファイルパス
image_file = "sketch-0_resize28.png"

# 縮小画像取り込み
im = Image.open(image_file)

# 縮小画像フォーマット、サイズ、モードの表示
print("[グレイスケール変換前]")
print(im.format, im.size, im.mode)

# --- ここからグレイスケール化処理 ---

# グレイスケール化
im_gray = im.convert("L")

# 変換後画像フォーマット、サイズ、モードの表示
print("[グレイスケール変換後]")
print(im_gray.format, im_gray.size, im_gray.mode)
[グレイスケール変換前]
PNG (28, 28) RGBA
[グレイスケール変換後]
None (28, 28) L

RGBAからLに変わりました。
これを上でやった方法で画像に保存してもいいのですが、元々白地に黒文字の画像なので見た目で変わり無いので続けます。



グレイスケールをNumpy配列に変換し確認


グレイスケール化した画像オブジェクトをNumpy配列に変換し、情報を確認します。

# ------ ここからNumpy処理 ------

# Numpyのインポート
import numpy as np

# グレイスケールのNumpy配列化
im_array_gray = np.array(im_gray)

# オブジェクトの型表示
print("\n[グレイスケール変換後のNumpy配列オブジェクト]")
print("オブジェクトの型", type(im_array_gray))

# 配列の形状
print("配列形状", im_array_gray.shape)

# 配列の次元数
print("配列の次元数", im_array_gray.ndim)

# 配列の全要素数
print("配列の要素数", im_array_gray.size)

# 配列要素の型
print("配列要素の型", im_array_gray.dtype)
[グレイスケール変換後のNumpy配列オブジェクト]
オブジェクトの型 <class 'numpy.ndarray'>
配列形状 (28, 28)
配列の次元数 2
配列の要素数 784
配列要素の型 uint8

形状が(28, 28, 4)から(28, 28)に、次元数も三次元から二次元になり、要素数も28×28×4=3136個あったものが、4分の1の784個になりました。

グレイスケール変換後のNumpy配列を表示(省略)

# 配列表示(出力省略します)
#print("\nNumpy配列\n", im_array_gray)

省略しておりますが、数値は相変わらず255ばかりです。ほとんど白。
(28, 28)に変換されたので、[255,255,255,255]みたいな配列がカンマ区切りで並んでいたものから、28個の配列が28行並んだものに変わっている。

255以外があるか念のため確認しておきます。

# 255以外の数値があるか確認
im_array_gray[im_array_gray != 255].size
89

89個が255以外。

ついでに0の個数も数えておきましょうかね。0は黒です。

im_array_gray[im_array_gray == 0 ].size
0

あら、無い。
みんな灰色か。グレイゾーンにおるんか!



グレイゾーンは黒に


東京地検特捜部にも負けず劣らず、白以外のグレイな奴は全員黒認定します。推定有罪です。疑わしきは罰するです。
罰する罰する。

Numpyのwhere(条件, trueのときの値, falseのときの値 )を使って255か0以外(グレイ)を0(黒)に置換します。0は今回は無かったので255(白)以外を0にする処理。
これで、0(黒)か255(白)かだけの容赦ない画像になります。

# 2値の配列にする
im_array_bw = np.where(im_array_gray != 255, 0, im_array_gray)

# 配列表示(省略)
# im_array_bw



画像を保存し表示


保存し表示させて、今回は終わりにします。

# Numpy配列をPillowで画像保存
im_bw = Image.fromarray(im_array_bw)
im_bw.save("sketch-0_resize28_bw.png")

# 画像表示
from IPython.display import Image
Image("sketch-0_resize28_bw.png")

f:id:chayarokurokuro:20200119074423p:plain



ありがとうございました。