2020年6月21日日曜日

PSK31 on Linux



ここにいろいろあり

https://pkgs.org/download/linpsk

winpsk オリジナルはここだろうか
http://www.moetronix.com/ae4jy/


  • 画像処理にはPillow(PIL)使います。

インストール

Python

MacははじめからPythonが入っています。
python --version
としてみてください。入っていなければ、
brew install python
とかで入ります。
brewが入っていることが前提ですが。。。

pipのインストール

ライブラリのインストールを楽にするためにPythonのパッケージ管理ツールであるpipをインストールします。
easy_install pip

モジュール(ライブラリ)のインストール

今回は、numpyとPillow(PIL)を使います。numpyは計算に便利なツール類が、Pillowは画像処理関係のツール類が含まれます。
pip install numpy
pip install pillow

画像を操作してみる

とりあえず、画像を読み込んで表示してみます。
from PIL import Image

im = Image.open("./achan.jpg")
im.show()
ただ表示しているだけも何なので回転させて表示してみます。
py01
from PIL import Image

im = Image.open("./achan.jpg")
im.rotate(30).show()
画像の中心を軸に、反時計周りに30度回転されたかと思います。
py01
実務では、これでいいのですが、内部的にどのような処理をしているのかはわかりません。なので、ここでは、1個1個のピクセルを処理して回転させるプログラムを書いてみたいと思います。

ピクセルを操作してみる

では、個々のピクセルを操作してみます。

ネガポジ反転してみる

では、代表的かつ簡単なピクセル操作処理であるネガポジ反転処理を行ってみます。
#coding:utf-8
from PIL import Image

#画像の読み込み
im = Image.open("./achan.jpg")

#RGBに変換
rgb_im = im.convert('RGB')

#画像サイズを取得
size = rgb_im.size

#取得したサイズと同じ空のイメージを新規に作成
im2 = Image.new('RGBA',size)

#loop
#x
for x in range(size[0]):
    #y
    for y in range(size[1]):
        #ピクセルを取得
        r,g,b = rgb_im.getpixel((x,y))

        #反転処理
        r = 255 - r
        g = 255 - g
        b = 255 - b

        #set pixel
        im2.putpixel((x,y),(r,g,b,0))

#show
im2.show()
ネガに反転されました。
py01

グレースケール

グレースケールは、r,g,bが同じ値を持つことでグレーに見えます。ただ、そのようなルールで同じ値にするかはケースバイケースです。ここではr,g,bの平均値を取得し、その値にしてみます(以下、ピクセル操作のみ抜粋)。
#loop
#x
for x in range(size[0]):
    #y
    for y in range(size[1]):
        #ピクセルを取得
        r,g,b = rgb_im.getpixel((x,y))

        #平均化
        g = (r + g + b)/3

        #set pixel
        im2.putpixel((x,y),(g,g,g,0))

#show
im2.show()
とりあえずグレーになりました。
py01

回転させてみる

画像を回転させる方法はいくつかありますが、最も原始的な方法は、回転行列を利用することです。

回転行列

回転行列は、下記の式で表されます。θに回転させたい角度をラジアンで与えると、その角度だけ回転した座表(x2,y2)を得ることができます。

(x2y2)=(cosθsinθsinθcosθ)(x1y2)
Pythonではnumpyモジュールを使うことで、上記の回転行列を、
m_matrix = np.matrix([
            [np.cos(rad),-1*np.sin(rad)],
            [np.sin(rad),np.cos(rad)]
        ])
と、直感的に表現することができ、かつ、通常の四則演算のように記述することで和や積を求めることができ、非常に便利です。
ここではmatrixを使っていますが、特に理由がない場合、arrayを使ったほうがいいらしい。

コードを書く

では、回転行列を利用して回転するコードを書いてみます。なお、下記のコードでは画像中心ではなく、左上が回転軸として処理されます。さらにintしか扱えないputpixelを利用してるので、画像にムラが発生します(が、ここではわかり易さ重視でこの方法にしています)。
#coding:UTF-8
from PIL import Image
import numpy as np

#画像の読み込み
im = Image.open("./achan.jpg")

#RGBに変換
rgb_im = im.convert('RGB')

#画像サイズを取得
size = rgb_im.size

#取得したサイズと同じ空のイメージを新規に作成
im2 = Image.new('RGBA',size)

#loop
#x
for x in range(size[0]):
    #y
    for y in range(size[1]):

        #ピクセルを取得
        r,g,b = rgb_im.getpixel((x,y))

        #処理

        #30度
        rad = np.pi/6

        #回転行列
        m_matrix = np.matrix([
            [np.cos(rad),-1*np.sin(rad)],
            [np.sin(rad),np.cos(rad)]
        ])

        #適用座標(元の座標)
        p_matrix = np.matrix([
                [x],
                [y]
            ])

        #行列演算
        p2_matrix = m_matrix * p_matrix

        #移動後の作業を取得(intしかputできないのでintに変換)
        x2 = int(p2_matrix[0,0])
        y2 = int(p2_matrix[1,0])

        #画像サイズの内であれば
        if 0 < x2 < size[0] and 0 < y2 < size[1]:
            #移動後の座標に元RGBを指定
            im2.putpixel((x2,y2),(r,g,b,0))

#show
im2.show()
py01

反転させてみる

x軸、y軸、任意軸に対する反転等も行列で行えます。高校数学の1次変換です。
例えば、y軸反転は、下記で与えられます。

反転行列(y軸)


(x2y2)=(1001)(x1y2)
この行列は、Pythonでは、
#y軸対象
y_matrix = np.matrix([
    [-1,0],
    [0,1]
])
として表現することができます。

コードを書いてみる

では、コードを書いてみます。なお、普通にy軸反転すると、全ての点が、マイナス方向にずれるため描画されません。そこで、画像の横幅(x軸)分だけ、平行移動させます。
#coding:UTF-8
from PIL import Image
import numpy as np

#画像の読み込み
im = Image.open("./achan.jpg")

#RGBに変換
rgb_im = im.convert('RGB')

#画像サイズを取得
size = rgb_im.size

#取得したサイズと同じ空のイメージを新規に作成
im2 = Image.new('RGBA',size)

#loop
#x
for x in range(size[0]):
    #y
    for y in range(size[1]):

        #ピクセルを取得
        r,g,b = rgb_im.getpixel((x,y))

        #処理

        #y軸対象
        y_matrix = np.matrix([
            [-1,0],
            [0,1]
        ])

        #適用座標(元の座標)
        p_matrix = np.matrix([
                [x],
                [y]
            ])

        #行列演算
        p2_matrix = y_matrix * p_matrix

        #移動後の作業を取得(intしかputできないのでintに変換)
        x2 = int(p2_matrix[0,0]) + size[0] #加増の横サイズだけx座標を平行移動
        y2 = int(p2_matrix[1,0])

        #画像サイズの内であれば
        if 0 < x2 < size[0] and 0 < y2 < size[1]:
            #移動後の座標に元RGBを指定
            im2.putpixel((x2,y2),(r,g,b,0))

#show
im2.show()
py01
反転しました。いわゆる左右反転。
なんか1ピクセル余計にずれてる感じもしますが。。。ここでは気にしません。

近傍処理

行列演算と同様、画像処理では必須処理となる近傍処理をやってみます。近傍処理は「ぼかし処理」や「輪郭抽出」等で活躍します。

ぼかし

ここでは、比較的簡単な「ぼかし処理」をやってみます。いろいろなアルゴリズムがありますが、ここでは最も単純な8近傍の平均値を取得し、セットするという方法をためしてみます。
8近傍とは、基準座標を取り囲む8つのエリアのことです。
py01
それそれの座標のr,g,b値を取得して、平均化します。
1つ注意点があるのは、画像の辺縁部の座標に置いては、x-1が存在しなかったり、x+1が座標からはみ出たりしますので、その処理が必要になります。では、コードを書いてみます。
#coding:utf-8
from PIL import Image

#画像の読み込み
im = Image.open("./achan.jpg")

#RGBに変換
rgb_im = im.convert('RGB')

#画像サイズを取得
size = rgb_im.size

#取得したサイズと同じ空のイメージを新規に作成
im2 = Image.new('RGBA',size)

#loop
#x
for x in range(size[0]):
    #y
    for y in range(size[1]):

        #対象座標のピクセルを取得
        r0,g0,b0 = rgb_im.getpixel((x,y))

        #初期化(とりあえず、全ての近傍値に現座標値をセット)
        r1 = r2 = r3 = r4 = r5 = r6 = r7 = r8 = r0;
        g1 = g2 = g3 = g4 = g5 = g6 = g7 = g8 = g0;
        b1 = b2 = b3 = b4 = b5 = b6 = b7 = b8 = b0;

        #近傍座標の値を取得

        #1
        if x-1 > 0 and y+1 < size[1]:
            r1,g1,b1 = rgb_im.getpixel((x-1,y+1))

        #2
        if y+1 < size[1]:
            r2,g2,b2 = rgb_im.getpixel((x,y+1))

        #3
        if x+1 < size[0] and y+1 < size[1]:
            r3,g3,b3 = rgb_im.getpixel((x+1,y+1))

        #4
        if x-1 > 0:
            r4,g4,b4 = rgb_im.getpixel((x-1,y))

        #5
        if x+1 < size[0]:
            r5,g5,b5 = rgb_im.getpixel((x+1,y))

        #6
        if x-1 > 0 and y-1 > 0:
            r6,g6,b6 = rgb_im.getpixel((x-1,y-1))

        #7
        if y-1 > 0:
            r7,g7,b7 = rgb_im.getpixel((x,y-1))

        #8
        if x+1 < size[0] and y-1 > 0:
            r8,g8,b8 = rgb_im.getpixel((x+1,y-1))


        #近傍のRGBを平均化
        r = (r0 + r1 + r2 + r3 + r4 + r5 + r6 + r7 + r8)/9
        g = (g0 + g1 + g2 + g3 + g4 + g5 + g6 + g7 + g8)/9
        b = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + b7 + b8)/9

        #描画
        im2.putpixel((x,y),(r,g,b,0))

#show
im2.show()
ちょっとわかりにくいですが、ぼけました。さらに近傍を拡大したり、アルゴリズムを工夫することでいろいろなぼかしが可能となります。
py01
ピクセルの補正など、実務に耐えうる状態にするにはまだまだですが、とりあえず、基本的な内容はここまで。


しや保存、各種画像処理(クリッピング、リサイズそして回転など)の方法についてまとめます。

環境

    • OS: Ubuntu16.04LTS
    • Python 3.7.0
    • Pillow 5.3.0
公式リファレンス: https://pillow.readthedocs.io/en/5.3.x/index.html

インストール

Pillowは3rdパーティ製のモジュールです。自分の環境に無いときはインストールしましょう。公式リファレンスに記載の通りpipコマンドでできます。
$ pip install Pillow
Anaconda環境の場合は以下でインストール可能です。
$ conda install pillow

使い方

Pillowモジュールのインポート

PillowはPIL(Python Imaging Library)から派生して作られた経緯があり、PILとの互換性を保つため、慣例的に以下のように呼び出すそうです。
from PIL import Image

画像データの読み出し

open()メソッドを使います。画像ファイルまたはファイルオブジェクトを引数に設定します。戻りはImageオブジェクトです。
>>> img = Image.open('sample.jpg')
また、Imageオブジェクトは様々なプロパティを持っており、サイズやフォーマットを確認することができます。
# 画像サイズを横幅、縦幅のタプルで返す
>>> img.size
(640, 426)
# ファイル名を返す
>>> img.filename
'sample.jpg'
# 画像フォーマットを返す
>>> img.format
'JPEG'

読みだした画像データを表示

show()メソッドを使います。
>>> img.show()
ただ、公式リファレンスによると、このメソッドは非効率なのでデバック目的にしか使わないほうが良いよ、とのこと。

画像を保存する

保存ファイル名をsave()メソッドの引数に設定します。拡張子を変更することで、自動的にフォーマットを変換することもできます。これは便利そうですね。
# jpg画像を読み込み
>>> img = Image.open('sample.jpg')
# 読み込んだ画像がjpg形式であることを確認
>>> img.format
'JPEG'

# 拡張子をpngにして保存
>>> img.save('sample.png')
>>> img2 = Image.open('sample.png')
# 読み込んだ画像がpng形式に変換されています
>>> img2.format
'PNG'
また、ファイル名の他にオプション引数を記述すると、画質の調整など細かな設定ができますが、細かくなるのでここでは割愛します。詳細は公式リファレンス:Image file formatsをご参照ください。

新しいImageオブジェクトを生成

new()メソッドを使います。
まずは、Lime色の幅200px, 高さ150pxの矩形を表示してみます。
>>> img = Image.new('RGBA', (200, 150), 'lime')

第一引数はカラーモードを表しており、上記例で設定した”RGBA”の他に”RGB”や”CMYK”などいろいろ設定できます。
<カラーモードの例>
    • RGBA:赤(Red),緑(Green), 青(Blue)、α値(不透明度)
RGB: 0 – 255の値(8bitで表せる数値: 2^8)
α : 0 – 255の値(0:透明 ⇔ 255:不透明)
    • RGB: 赤(Red),緑(Green), 青(Blue)
上記のRGBのみで表したもの。
    • CMYK:シアン(Cyan)、マゼンタ(Magenta)、イエロー(Yellow)、キー・プレート(Key plate:黒)
それぞれ0 – 255の値
※この他のカラーコードは公式リファレンスをご参照ください。
第二引数は、描画する矩形の幅、高さを表します。
第三引数は、カラーを表します。デフォルトは黒です。RBGなら3つ、RGBAなら4つのの整数値を要素としたタプルを設定します。また、上記例のようにImageColorモジュールによって定義された文字列で表すこともできます。他にも例を示すと、、、
カラー:赤 RGBA値=(255, 0, 0, 255)の場合
>>> img = Image.new('RGBA', (200, 150), 'red')

カラー:紫 RGBA値=(128, 0, 128, 255)の場合
>>> img = Image.new('RGBA', (200, 150), 'purple')

画像の切り抜き

crop()メソッドを用います。切り取るエリアを座標で表して引数として渡します。
ここで、Pillowにで使われる座標系について簡単におさらいします。

  • 座標系: 画面左上が原点(0, 0)、x軸は画面横方向、y軸は画面縦方向
  • 座標の表し方:
    • 点 : (x, y)の2つタプル
    • 四角形: (左上のx, 左上のy, 右下のx, 右下のy)の4つのタプル
これらを踏まえ、上図のエリアを切り取るには、以下のように記述します。
>>> cropped_img = img.crop((250, 100, 400, 300))
塗りつぶした矩形だと面白くないので、具体的な例を以下に示します。
# 元画像をオープン
>>> img = Image.open('coffee.jpg')

# (40, 50)、(150, 130)で囲われたエリアを切り抜きます
>>> cropped_img = img.crop((40, 50, 150, 130))
 → 

画像のペースト

paste()メソッドを用います。第一引数にペーストしたい画像、第二引数にペーストする座標を設定します。
このメソッドは、元の画像(Imageオブジェクト)を直接変更します。元の画像を変更したくない場合はcopy()メソッドで画像を複製しておきます。
# 元画像をコピー
>>> copy_img = img.copy()
# コピーした画像に、上記で切り抜いた画像を(0, 0)の位置にペースト
>>> copy_img.paste(cropped_img, (0, 0))
>>> copy_img.show()

この他に、maskパラメータを使ってペーストする画像の境界線をぼやかすなどエフェクトを付けることも可能ですが、ここでは割愛します。

画像のリサイズ

resize()メソッドを用います。
以下は元画像の1/4のサイズにする場合の例です。
# 元画像
>>> img.size
(300, 200)
# 元画像のサイズ確認
>>> w, h = img.size

# リサイズ処理
>>> resized_img = img.resize((int(w/2), int(h/2)))
# リサイズ後のサイズ確認
>>> resized_img.size
(150, 100)

画像の回転

rotate()メソッドを使います。引数は、半時計回りの回転角度(degree)です。
90°回転の場合の例
>>> r90_img = img.rotate(90)

この設定だと、画像のサイズが変わらないまま回転してしまうので、画像がはみ出てしまっています。
元の画像全部が表示されるように回転させるには、オプション引数expand=Trueを設定します。
>>> r90_img = img.rotate(90, expand=True)

まとめ

PillowモジュールのImageオブジェクトを使った画像処理についてまとめました。
  • 画像データの呼び出し・保存
  • 画像のクリッピング、ペースト、回転といった基本的な操作