よちよちpython

独習 python/Qpython/Pydroid3/termux/Linux

Numpyだけで画像処理を行い画像処理の仕組みを学ぶ

今回は、数値演算に使うPythonライブラリNumpyを使って画像処理にチャレンジ。
PillowやOpenCVなど画像処理ライブラリの処理用メソッドは使いません!
そんなこと出来るのでしょうか!?



はじめに


Pythonで画像処理を行う定番のライブラリにはPillowやOpenCVがある。
それらは画像処理用の便利なメソッドを用意しているので、メソッドを呼び出すだけで割りと簡単に色を変えたり回転させたり切り抜いたりの画像加工ができてしまいます。

一方で、簡単な反面、内部で何が行われているのか見えにくい。元の画像にどういう処理をほどこせばその結果が出てくるのかの仕組みがあまり分からない。

「でも、べつに仕組みを知る必要って無くね?」

ところで、機械学習フレームワークscikit-learnには勉強用に7種類のデータセットが付属しています。その中に手書き数字の画像データがある。
機械学習を使うと、数字の手書き画像が何の数字であるかをコンピュータが判別できるようになるという。
なんて賢いんだ!

で、あらゆる画像はしょせん数値の羅列であるらしくてですね、その数値の羅列が何の画像を指しているかというラベル(答え)とペアの情報を大量にコンピュータに読み込ませてパターンを学習させると、画像の分類や判別ができるようになってしまうというのです。

「数値しか見てない奴に現実の何がわかるってんだッ!」

機械学習への興味が画像処理の仕組みへの興味へと進化の過程をさかのぼる、まるでロックやジャズの興味から古いブルースやソウルに興味が移るみたいな、現在の中東情勢のニュースからローマ帝国オスマントルコ大航海時代の歴史への興味を涌かせるみたいな心境で、

「数値の羅列に過ぎない画像データをNumpyでイジクリ回すと画像が編集できる、その仕組みをさわりだけでも学ぼう」

というのが今回の趣旨です。

長くなった。
冒頭が長いわりには内容はショボそうだが、よし、先に進もう。



目次




実行環境


Androidスマホ
termux
Python3.8
JupyterNotebook



寝ながらでも出来るのでスマホを使っています。特に意味はありません。



必要なライブラリの準備


  • JupyterNotebook : Pythonコードの実行と画像表示用
  • Numpy : 画像データの配列をイジクリ回す用
  • matplotlib : 画像の取り込みと保存、表示用
  • Pillow : 参考用

これらをpipやcondaでinstallしておきます。



画像ファイルのndarray化


まずは画像ファイルを読み込み、Numpy配列に変換します。

方法としては

  1. pillowを使う
    • PIL.Image.open(ファイル名)をnp.array()に渡す。
      np.array(PIL.Image.open(ファイル名))

  2. OpenCVを使う
    • cv2.imread(ファイル名)をnp.array()に渡す。
      np.array(cv2.imread(ファイル名))

  3. matplotlib.pyplotを使う
    • imread(ファイル名)をnp.array()に渡す。
      matplotlib.pyplot.imread(ファイル名)

  4. scikit-imageを使う

等々。



pillowを使って画像をndarray化


pillowで、

  1. 画像をNumpy配列に変換する
  2. Numpy配列を画像に変換し直し保存する

の2つを行うのに利用します。画像処理には使いません。



【pillow 使用上の注意】

  • pip installする時は「Pillow」や「pillow」で行うが、
  • importする時は「PIL」と全て大文字で。



Numpyでイジクリ回す予定の画像ファイルをネットから頂いてきました。

# 作業フォルダ内のpng画像ファイル表示
!ls | grep  *.png
gyouretsu.png
scatter1.png
scatter2.png
scatter3.png

一番上のgyouretsu.png (行列.png !)というピッタリな名前の画像ファイル。これを使います。

画像ファイルをndarray化

# pillowのインポート
from PIL import Image

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

# 画像ファイル
image_file = "gyouretsu.png"

# numpy配列に変換
im = np.array(Image.open(image_file))



ndarrayデータの確認


Numpy配列に変換したので型などを確認していきます。

# タイプ確認
type(im)
numpy.ndarray
# 要素のタイプ確認
im.dtype
dtype('uint8')
# 形状の確認
im.shape
(180, 180, 4)
# 次元の確認
im.ndim
3
# 全要素数
im.size
129600
  • ndarray型の配列
  • 8ビット符号なし整数型の要素
  • 形状 (180, 180, 4)
  • 3次元
  • 129600個の要素数

になっていることがわかった。
配列の中身を表示させてみる。

im
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],
        ...,
        [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]],

       ...,

       [[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],
        ...,
        [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)

ゼロが並んでます。途中は省略されています。

ちなみに元の画像はこれ↓

20200114122240



行列には違いないッ
いらすとやさんから拝借しました。



形状 (180, 180, 4)が意味するもの


im.shapeで取得した形状

  • (180, 180, 4)

  • (行数, 列数, 色(チャンネル、色空間))

を表しているそうです。

行と列は、

  • 画像を縦に行数分、
  • 横に列数分

分割してマス目にした感じでしょうか。マス目ごとに色を置く訳です。体育祭のときにやったり北朝鮮で有名なマスゲームを想像すると分かりやすいですかね。

色は

  • pillowの場合はRGB(赤・緑・青)
  • OpenCVの場合はBGR(青・緑・赤)

の三色になるらしいのですが、三次元目は4色表現のデータになってますね。何でしょう?
確認するにはpillowを使って次のようにする。

Image.fromarray(im).mode
'RGBA'

RGBAのAはalpha 透明度を表すんですと。
RGBは赤・緑・青を示していることは前に書きました。



画像は

  • 左上を原点とし、
  • 横軸を右側にプラス方向
  • 縦軸は下側にプラス方向

と決められている。
行が横軸で、列が縦軸を指すので、
配列の(行, 列) = 画像の(y, x)。



画像を4等分し一部分をベタ塗りする実験


本当にそうなっているかをこれから確認していきます。

# 0行0列の値(色のデータ)を抽出
im[0][0]
array([0, 0, 0, 0], dtype=uint8)

0行0列目の値は[0,0,0,0]になっています。この4つの値で色を表現している。

色を表す4つの数値をイジくれば、その場所の色が変わります。
本当にそうなるか確かめるために簡単な実験をします。

  1. 画像を縦横四等分して、
  2. 左上部分の色の数値を別の数値に変え

てみます。
数値は全て同じにします。 加工後の画像4分割の左上部分はベタ塗りした感じになるはずです。



180行180列の画像データなので、縦横4分割するために行と列を真ん中で切り分けます。
スライスで指定して、画像左上を抽出する。

# 画像左上4分の1カットを抽出して見る
im_1_4 = im[0:90, 0:90]
im_1_4
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],
        ...,
        [  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]],

       ...,

       [[  0,   0,   0,   0],
        [ 96, 102, 109, 209],
        [104, 110, 116, 255],
        ...,
        [ 98, 172, 200, 255],
        [ 96, 170, 199, 255],
        [ 97, 171, 200, 255]],

       [[  0,   0,   0,   0],
        [103, 110, 115, 223],
        [108, 114, 120, 255],
        ...,
        [ 97, 171, 200, 255],
        [ 96, 170, 199, 255],
        [ 95, 170, 198, 255]],

       [[  0,   0,   0,   0],
        [105, 111, 117, 213],
        [105, 111, 117, 255],
        ...,
        [ 89, 164, 194, 255],
        [ 84, 159, 190, 255],
        [ 94, 169, 197, 255]]], dtype=uint8)
# 抽出部分のデータ形状
print(im_1_4.shape)

# データの個数
print(im_1_4.size)
(90, 90, 4)
32400



左上を抽出しました。
90行90列に32400個のデータがある。

抽出部分をそのまま画像に変換し直してもいいんですが、色の書き換えも見たいので続けます。

今取り出した部分の色の数値データを全てゼロにします。何色かのベタ塗りになるはず。
np.zeros(個数)を使うと要素が全部ゼロの配列が生成できる。

# 全要素ゼロの配列を形状変形して
chgd_data = np.zeros(32400).reshape(90,90,4)

# 置換する
im[0:90,0:90]=chgd_data

これで、0以上90行目未満の0以上90列未満の部分の色データが全てゼロに置換されました。

pillowでndarrayを画像に変換・保存


pil_img = Image.fromarray(im)
print(pil_img.mode)
# RGB

pil_img.save("changed_gyouretsu.png")
RGBA

出来た画像

20200114123027



画像4等分の左上が真っ黒になりました。RGBAが全てゼロだと黒になるんですね。

追記
PCで見ると、ページの背景色が黒なので加工した左上の四角い部分も黒に見えるが、
スマホで見ると、ページの背景色が白なので加工部分は白に表示されていますね。
透明度も0の値にしたので背景色に応じて変わっているみたいです。
失礼しました。



Numpyを使って簡単な画像の加工ができることがわかりました。
行列をスライスで抽出して画像に変換しなおしたり、狙った場所の色データを書き換えれば好きなように切り貼りや色加工も出来そうです。



matplotlibとnumpyで画像編集


matplotlibでも出来るらしい。やってみましょう。
画像の左半分の5列おきに色を変換して縦縞模様の画像を作ります。

import numpy as np
import matplotlib.pyplot as plt

# 画像ファイル
image_file = "gyouretsu.png"

# matplotlibでndarray化
im_mpl = plt.imread(image_file)



# 配列表示
print(im_mpl)
# データのタイプ
print(type(im_mpl))
# 形状の確認
print(im_mpl.shape)
# 要素のタイプ
print(im_mpl.dtype)
[[[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.]
  ...
  [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.]]

 ...

 [[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.]
  ...
  [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.]]]
<class 'numpy.ndarray'>
(180, 180, 4)
float32

matplotlibの色のデータはfloat32型になっていますね。

# 全行、5列おきの色の値
im_mpl[:, 0:90:5] 
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.],
        ...,
        [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.]],

       ...,

       [[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.],
        ...,
        [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=float32)
# 全行、5列おきに値を適当に置換
im_mpl[:, 0:90:5] = np.array([255,250,225,255], dtype="float")/255

色の値は0から1までの範囲のfloat型で指定しないといけないらしく、ERRORを出しまくった。

この辺に参考になりそうなことが書いてある。よくわからんので今回は無視する。 python - How to convert a 3d RGB NumPy array to float without changing the image? - Stack Overflow



とりあえず、上のコードでエラーは出ない。

画像の保存


# 画像表示の準備
fig = plt.figure()
ax = fig.add_subplot(111)

# 画像表示
plt.imshow(im_mpl)

# 画像の保存
plt.imsave("v_stripes_gyouretsu.png", im_mpl)

出来た画像

f:id:chayarokurokuro:20200114122440p:plain



おわりに


Numpyを使って画像処理の方法を簡単にふれてきましたが、勉強になりますね。結構良かった。
もう少し踏み込んで、色の付け方や処理の仕方をやるのもいいかも。

matplotlibを使う方法は色の設定値(float型の)の所が躓きポイントですね。検索すると困ってる人が多い。
pillowをnumpy配列化と保存で使う方法が簡単にできるかと思いました。

今日はここまで。

参考
Python, NumPyで画像処理(読み込み、演算、保存) | note.nkmk.me

行列による画像処理 基礎編&目次 ~Python画像処理の再発明家~ - Qiita

rgba()-CSS3リファレンス