Python

OpenCV による画像加工の基本【 画像合成までの基礎 】

Open CV とは

Open CV は画像や動画を処理するための機能がまとめて実装されているライブラリです。
色々できます。目次を見て頂けるとよいかと思います。

インストール

main( core )モジュールのみ

pip install opencv-python

contrib モジュール

contrib( extra )モジュールも必要な場合

pip install opencv-contrib-python

contrib モジュールは開発用のモジュールです。

opencv_contribでは比較的最新のアルゴリズム、試験的な機能が実装されることが多いため、それらのアルゴリズム、機能を試すのに適しています。ただし、テストが十分に行われていなかったり、APIが変更される可能性があったりするため、使用する際にはその旨に注意する必要があります。

---------------------------------------------------------------

opencv_contribで成熟してポピュラーになったモジュールはOpenCVのメインリポジトリに移行されることがあります。

第9回 初めてのOpenCV開発 ― opencv_contrib紹介【OpenCV 3.1.0】

インポート

import cv2

画像の読み込み・表示・保存

画像の読み込み

読み込みにはcv2.imread( )を使用します。

第一引数に読み込みたいファイルのパス、第二引数に読み込み方法を指定します。

引数 フラグ 読み込み方法
1cv2.IMREAD_COLORカラー画像として読み込み。アルファチャンネルは無視される。
0cv2.IMREAD_GRAYSCALEグレースケール画像として読み込む
-1cv2.IMREAD_UNCHANGEDアルファチャンネルを含めて読み込む

読み込み方法をimread()の引数として渡す場合は下記のような表記になります。

import cv2
img = cv2.imread ('D:\\hoge\\test.png', -1 )

相対パスも指定できます。

import cv2
img = cv2.imread ('test.png', -1 )

この段階では読み込んだだけなので特になにも起こりません。

画像の表示

画像をウィンドウに表示するにはcv2.imshow()を使用します。
引数にウィンドウの名前と表示したい画像を指定します。

import cv2

# 画像の読み込み
img = cv2.imread ('test.png', -1 )

# ウィンドウで画像を表示
cv2.imshow('hogeWindow', img)

ただ、このままだと一瞬でウィンドウが消えます。ちゃんと表示し続けるにはもう 2つコマンドを実行します。

import cv2

# 画像の読み込み
img = cv2.imread ('test.png', -1 )

# ウィンドウで画像を表示
cv2.imshow('hogeWindow', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

追加したのは一番下の 2行です。これでちゃんとウィンドウが表示されます。

waitKey はキーボード入力を受け付ける関数、
deestroyAllWInsows は作成したすべてのウィンドウを閉じる関数です。

今回はこちらの画像を使って色々な機能を確認していこうと思います。

表示するだけならdestroyAllWindows()は必要ありません。下記のコードでも問題なくウィンドウが表示できます。

import cv2

# 画像の読み込み
img = cv2.imread ('test.png', -1 )

# ウィンドウで画像を表示
cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

画像の保存

保存は下記のコマンドです。

cv2.imwrite('saved_test.png', img)

絶対パスも指定できます。
ただしフォルダがないと保存されません。
保存の前にフォルダの確認と、なければ作成するコードを書きましょう。

cv2.imwrite('D:\\hoge\\test.jpg', img)

情報取得

ビット数の確認 | img.dtype

import cv2
img = cv2.imread ('pixel_test.png', -1)
print(img.dtype)

dtypeで画像のビット数を確認できます。

uint8

16 bit の場合はunit16と出力されます。

ビットに関しては下記の記事で触れています。

たぶん上記のビットの知識があったほうが OpenCV はわかりやすいと思います。
コンピュータで色を扱う際の基底部分の知識です。

OpenCV の型 は ndarray

import cv2
img = cv2.imread ('test.png', 1)
print(type(img))
<class 'numpy.ndarray'>

openCV は nuumpy の ndarray 型です。

OpenCV は BGRA

わかりやすいように 3px × 2px 四方の画像で中身を見てみます。

print 文で中身を確認できます。

import cv2
img = cv2.imread ('pixel_test.png', -1)
print(img)
[[[  0   0 255 255]
  [  0 255   0 204]
  [255   0   0 163]]

 [[  0   0   0 122]
  [128 128 128  82]
  [255 255 255  41]]]

画像と行列を照らし合わせると下記のことがわかるかと思います。

  • 1ピクセルずつ、RGBA の情報が 0-255 の値で行列で入っている
  • 行列の一番右にくるのがアルファチャンネル
  • 図の左上から順に行列の形で入っている

問題になるのはカラー情報です。行列と図をよく見比べて頂くとわかりますが、
OpenCV は RGBA ではなく BGRA の順で値をとります。

これなんででしょうね。アルファベット順に入れたかったのかな。

ちなみに、16bit 画像の行列の中身をみると数値の桁が違います。

[[[    0     0 65535 65535]
  [    0 65535     0 52427]
  [65535     0     0 41891]]

 [[    0     0     0 31354]
  [53621 53621 53621 21074]
  [64985 64985 64985 10538]]]

画像サイズ・チャンネル数の取得

画像が ndarray の行列として取得できているため、
shape を使用することで( 高さ, 横幅, チャンネル数 )の順で情報が取得できます。

このとき、読み込み方法や画像の内容によって値が変わりますので注意してください。

透過無し

import cv2

# 透過なしで読み込み
img = cv2.imread ('pixel_test.png', 1)
print(img.shape)

アルファチャンネルがない場合は3チャンネルで取得できます。

(2, 3, 3)

グレースケール

import cv2

# グレースケールで読み込み
img = cv2.imread ('pixel_test.png', 0)
print(img.shape)

1チャンネルのみのため、次元がひとつ下がります。

(2, 3)

透過あり

import cv2

# 透過ありで読み込み
img = cv2.imread ('pixel_test.png', -1)
print(img.shape)

アルファチャンネルをもつため 4チャンネル存在します。

(2, 3, 4)

チャンネル操作

画像が ndarray 型で取得できているため、スライス機能を利用して各チャンネル情報のみを取り出せます。

# アルファチャンネルの取得
alpha = img[:,:,3]

アルファチャンネルの取得

import cv2

img = cv2.imread ('color_palettte.png', -1)

# アルファチャンネルの取得
img = img[:,:,3]

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

アルファチャンネルを持たない画像や、透過なしやグレースケールで読み込んでいる場合は エラーで止まりますので注意してください。

B 値の取得

前述したとおり、0番目に来ているのは B値です。
BGRA の順で入っているため、0番目の値は B値になっていることに注意してください。

import cv2

img = cv2.imread ('color_palettte.png', -1)

# B値の取得
img = img[:,:,0]

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

BGR値を分割する

ch_b, ch_g, ch_r = cv2.split(img[:,:,:3])

上記のコードで BGR 値の順に分割できます。アルファチャンネルは取り出せないので注意してください。

import cv2

img = cv2.imread ('pixel_test.png', -1)

print(img)

ch_b, ch_g, ch_r = cv2.split(img[:,:,:3])


print('\n--- B ---\n')
print(ch_b)
print('\n--- G ---\n')
print(ch_g)
print('\n--- R ---\n')
print(ch_r)
[[[  0   0 255 255]
  [  0 255   0 204]
  [255   0   0 163]]

 [[  0   0   0 122]
  [128 128 128  82]
  [255 255 255  41]]]

--- B ---

[[  0   0 255]
 [  0 128 255]]

--- G ---

[[  0 255   0]
 [  0 128 255]]

--- R ---

[[255   0   0]
 [  0 128 255]]

チャンネルの結合

チャンネルを分割するコマンドがあれば、チャンネルを結合させるコマンドもあります。

img = cv2.merge((ch_B, ch_G, ch_R, ch_A))
import cv2

img = cv2.imread ('pixel_test.png', -1)

print(img)

# アルファチャンネルの取得
mask = img[:,:,3]

# BGR の分割
ch_b, ch_g, ch_r = cv2.split(img[:,:,:3])

print('\n--- B ---\n')
print(ch_b)
print('\n--- G ---\n')
print(ch_g)
print('\n--- R ---\n')
print(ch_r)

print('\n--- merge ---\n')

# チャンネルの結合
img = cv2.merge((mask, mask, ch_r, ch_b))
print(img)

cv2.imwrite('saved_test.png', img)

上記は B と G にアルファチャンネル、R に G値、アルファに B値の組み合わせで画像を再構築しています。

[[[  0   0 255 255]
  [  0 255   0 204]
  [255   0   0 163]]

 [[  0   0   0 122]
  [128 128 128  82]
  [255 255 255  41]]]

--- B ---

[[  0   0 255]
 [  0 128 255]]

--- G ---

[[  0 255   0]
 [  0 128 255]]

--- R ---

[[255   0   0]
 [  0 128 255]]

--- merge ---

[[[255 255 255   0]
  [204 204   0   0]
  [163 163   0 255]]

 [[122 122   0   0]
  [ 82  82 128 128]
  [ 41  41 255 255]]]

Pillow ⇔ OpenCV の変換

OpenCV → Pillow

import cv2
from PIL import Image

# 画像の読み込み
img = cv2.imread ('test.png', 0)

# 1チャンネル
if img.ndim == 2:
    pass

# 3チャンネル
elif img.shape[2] == 3:
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 4チャンネル
elif img.shape[2] == 4:
    img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)

img = Image.fromarray(img)

print(type(img))

Pillow → OpenCV

import cv2
import numpy as np
from PIL import Image

img = Image.open('test.png')

img = np.array(img, dtype=np.uint8)

# 1チャンネル
if img.ndim == 2:
    pass

# 3チャンネル
elif img.shape[2] == 3:
    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

# 4チャンネル
elif img.shape[2] == 4:
    img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)

print(type(img))

画像サイズの変更

解像度の変更

resize(対象の画像, (横幅, 高さ))で対象の画像データをリサイズします。

import cv2

img = cv2.imread ('test.png', 1)

# リサイズ
img = cv2.resize(img , (600, 500))

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

アスペクト比を固定したい場合は計算式を書きます。

トリミング

下記のように書くとそれだけでトリミングできます。

img[top : bottom , left : right]

数値を指定するトリミング

最も楽な切り取り方法です。

import cv2

img = cv2.imread ('test.png', -1)

# トリミング
img = img[36:500, 300:1000]

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

ただ、実用性はほとんどないです。
切り取りたい部分がピクセル単位でわかっている場合のみこれで済みます。

相対的なトリミングを行う

CUI で全体の割合を元にトリミングできるようにしてみます。

import cv2

img = cv2.imread ('test.png', 1)

# 切り取る割合の指定
top    = 20
bottom = 10
left   = 50
right  = 0

# 画像の 縦幅 と 横幅 を取得
height = img.shape[0]
width  = img.shape[1]

# 各幅への割合からピクセル数を出す
top_px    = int(height * top / 100)
bottom_px = int(height * (100 - bottom) / 100 )
left_px   = int(width  * left / 100)
right_px  = int(width  * (100 - right) / 100 )

# トリミング
img = img[top_px:bottom_px, left_px:right_px]

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

割合を変更することでトリミングできるようにしました。
top/bottom、left/right の合計値が 100 を超えると元のサイズを超えるため、エラーが起きます。

カラースペースの変換

下記のコマンドでグレースケールに変換できます。

img= cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

他の変換は下記の公式サイトに記載されています。

変換画像がついてなかったり、使用に条件があったりしますので、
とりあえず使ってみるのがいいかもしれません。

使い方 | グレースケール変換

グレースケールの変換を行いながら特徴を見ていきます。

使い方はcvtColorコマンドの第一引数に変換対象のファイル、第二引数に公式ページに記載されているフラグを渡します。

img= cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

ちなみに下記はフォトショの白黒フィルターの自動補正で変換したもの。

色々と違いがあります。
openCV では調整ができません。各色の調整をするならフォトショを使ってください。
あくまで一括で自動処理する場合の利用になるかと思います。
(ここデザイナー向けの解説です)

データとアルファチャンネル

アルファチャンネルの入った画像で詳しく見ていきます。

import cv2
img = cv2.imread ('pixel_test.png', -1)

print(img)
print('\n配列:', img.shape)

# グレースケースに変換
img= cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)

print('\n----- 変換後 -----\n')

print(img)
print('\n配列:', img.shape)

cv2.imwrite('pixel_test_saved.png', img)

変換前と後でデータ内容を比べるとわかりますが、グレースケールにすることでデータ量が格段に減ります。

[[[  0   0 255 255]
  [  0 255   0 204]
  [255   0   0 163]]

 [[  0   0   0 122]
  [128 128 128  82]
  [255 255 255  41]]]

配列: (2, 3, 4)

----- 変換後 -----

[[ 76 150  29]
 [  0 128 255]]

配列: (2, 3)

変換前の2段目をみるとわかりますが、BGR で同じ値が並びます。
グレースケールは BGR が同じ値になるため、配列の次元をひとつ下げることができます。
白黒で問題ない場合は変換しておくとデータが劇的に軽くなります。

また、カラースペースの変換ではアルファチャンネルが消えるという特徴があります。

アルファチャンネルが必要な場合は別途取得します。

他の変換例

ごく一部ですが変換例を載せておきます。

YCrCb |  COLOR_BGR2YCrCb

img = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)

HSV |  COLOR_BGR2HSV

img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

XYZ |  COLOR_BGR2XYZ

img = cv2.cvtColor(img, cv2.COLOR_BGR2XYZ)

チャンネルを増やす

COLOR_BGR2GRAYでチャンネルを減らすことができますが、逆にCOLOR_GRAY2BGRを使用することでチャンネルを増やすこともできます。

import cv2

img = cv2.imread ('pixel_test.png', -1)

print(img)

# アルファチャンネルを取得
img = img[:,:,3]
print('\n-- アルファチャンネル --\n')
print(img)

# チャンネルを増やす
img = cv2.cvtColor( img , cv2.COLOR_GRAY2BGR)

print('\n-- チャンネルを増やす --\n')
print(img)
[[[  0   0 255 255]
  [  0 255   0 204]
  [255   0   0 163]]

 [[  0   0   0 122]
  [128 128 128  82]
  [255 255 255  41]]]

-- アルファチャンネル --

[[255 204 163]
 [122  82  41]]

-- チャンネルを増やす --

[[[255 255 255]
  [204 204 204]
  [163 163 163]]

 [[122 122 122]
  [ 82  82  82]
  [ 41  41  41]]]

画像を合成する際にチャンネルを増やす機会がでてきます。

matplotlib などで描画する際の注意

OpenCV は BGRA の順で値をとる、と解説しました。

これは Open CV 特有のものです。
他のライブラリはほぼ RGBA の順で値を扱います。

そのため、Open CV で読み込んだ画像を matpotlib で描画するとR値 と B値が入れ替わってしまいます。

import cv2
import matplotlib.pyplot as plt
img = cv2.imread ('pixel_test.png', -1)
plt.imshow(img)
plt.show()

行列は同じ値ですが、読み取る際に RGBA の順で読み取るためです。
(背景が白色なのでちょっと色味が変わってます。)

そのため、他のライブラリなどで使用する際は値を変換してから渡します。

チャンネルの置換

cvtColor コマンドには チャンネルを置換するものが用意されています。

# BGRA -> RGBA へ変換
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
import cv2
import matplotlib.pyplot as plt
img = cv2.imread ('pixel_test.png', -1)
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
plt.imshow(img)
plt.show()

画像の合成

下記の二つの画像を合成していきます。
フォトショやイラレのパスファインダーと同じような演算を行います。

import cv2

circle   = cv2.imread ('circle.jpg', 0)
triangle = cv2.imread ('triangle.jpg', 0)

画像演算の前処理

画像合成は画像の合成ではなく、行列と行列の演算に過ぎません。

そのため同じサイズの行列でないと合成ができません。

  • 解像度が同じであること
  • チャンネル数が同じであること
  • ビット数が同じであること

上記が画像を合成できる条件になります。
これに合わない場合は合成前の処理が必要になります。

AND 演算 | A 且つ B

import cv2

circle   = cv2.imread ('circle.jpg', 0)
triangle = cv2.imread ('triangle.jpg', 0)

# AND 演算
img = cv2.bitwise_and(circle, triangle)

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

OR 演算 | A 又は B

import cv2

circle   = cv2.imread ('circle.jpg', 0)
triangle = cv2.imread ('triangle.jpg', 0)

# OR 演算
img = cv2.bitwise_or(circle, triangle)

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

XOR 演算 | Aのみ 且つ Bのみ

import cv2

circle   = cv2.imread ('circle.jpg', 0)
triangle = cv2.imread ('triangle.jpg', 0)

# XOR 演算
img = cv2.bitwise_xor(circle, triangle)

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

NOT 演算 | Aでない

この演算は引数をひとつのみ渡します。
ふたつめを渡してもエラーは起きませんが、意味がありません。

import cv2

circle   = cv2.imread ('circle.jpg', 0)
triangle = cv2.imread ('triangle.jpg', 0)

# NOT 演算
img = cv2.bitwise_not(circle)

cv2.imshow('hogeWindow', img)
cv2.waitKey(0)

-Python
-