九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、前回は久しぶりに2Dの顔画像を3D化するというPythonコードのお話をお届けしました。
個人的にPython開発もメインで請け負っているのですが、最近はLaravel(PHP)やVue(JavaScript)関連での開発が多かったため、久しぶりにPythonをさわることになりました。(といっても前回はgit cloneしただけですが ^^;)
すると、またPythonもコーディングしたくなってきたので、以前このブログで投稿した完全網羅!Intervention Image(PHP)で画像を編集する全実例のように、Pythonの画像処理・定番パッケージOpenCVの基本的な画像変形(リサイズ、回転、水平移動、反転、切り抜き、挿入、モザイク、ぼかし、シャープネス、明るさ、コントラスト、色反転、透過)をまとめてみるることにしました。
Pythonは機械学習の影響もあって世界的に人気の言語になっていますので、ぜひ学習者さんは参考にしてみてくださいね。

開発環境: Python 2.7, OpenCV 3.4
目次
必要なパッケージをインストールする
今回の例ではOpenCV本体と、計算を楽にしてくれるnumpyが必要になります。次のpipコマンドでインストールしておきましょう。
pip install opencv-python pip install numpy
インストールしたらpip listを実行して、opencv-python、numpyが含まれていればインストールは成功です。

ちなみにPythonでOpenCV(と必要になるパッケージ)を利用するには次のようにしてインポートしておく必要があります。
import cv2 import numpy as np
(すでにOpenCVはバージョン3なのに、なぜかいまだにcv2です)
では、次のパンダの画像を使って画像を変形していきましょう。

(画像サイズは、250 x 250 ピクセル)
画像はimread()で読み込んでおいてください。
img = cv2.imread('./images/panda.png')
画像の変形
リサイズする
例えば、横幅を200pxで高さを150pxに変更したい場合です。
width = 200 height = 150 resized_img = cv2.resize(img, (width, height))
※ widthとheightはタプルです。
実際にコードを実行すると画像はこうなります。

なお、interpolationを指定すればリサイズする場合の補間方法を指定することもできます。(つまり、画質が変わります)
cv2.resize(img, (width, height), interpolation=cv2.INTER_LINEAR)
指定できる補間方法は、以下の
- INTER_NEAREST ・・・ 一番画質が悪い
- INTER_LINEAR ・・・ デフォルト
- INTER_AREA ・・・ 画像の大幅な縮小に向いている
- INTER_CUBIC ・・・ そこそこキレイ。
- INTER_LANCZOS4 ・・・ 一番キレイ。
では、一番画質が悪いものといいものでリサイズした画像を見てみましょう。
(INTER_NEAREST)

(INTER_LANCZOS4)

また、どれだけINTER_AREAが縮小に向いているかも比較してみましょう。
左が通常のリサイズで、右がINTER_AREAです。
![]()
回転する
OpenCVには直接画像を回転する関数はありませんが、回転行列をつくる関数getRotationMatrix2D()は用意されていますので、これとwarpAffine()を使って画像を回転させることができます。
height,width = img.shape[:2] center = (int(width/2), int(height/2)) # 中心点 angle = 45 # 左回転 M = cv2.getRotationMatrix2D(center, angle, 1) rotated_img = cv2.warpAffine(img, M, (width, height))
まず、getRotationMatrix2D()で
- 中心点
- 回転する角度
- 画像の拡大/縮小率
を引数にして回転行列Mをつくります。
そして、この行列を使ってwarpAffine()すれば回転した画像をつくることができます。
では、実際の画像を見てみましょう。
(元画像)

回転した画像

なお、拡大率を0.7にした場合です。
M = cv2.getRotationMatrix2D(center, angle, 0.7)

ただし、画像サイズは元のままなのできっちりフィットしたサイズにはなりません。もし回転後にぴったりフィットした画像を取得したい場合は次のような関数を使えばいいでしょう。
def fitting_rotated_image(img, angle, scale=1.0):
height,width = img.shape[:2]
center = (int(width/2), int(height/2))
rdians = np.deg2rad(angle)
M = cv2.getRotationMatrix2D(center, angle, scale)
# 回転後のサイズを計算
new_width = int(abs(np.sin(rdians)*height*scale) + abs(np.cos(rdians)*width*scale))
new_height = int(abs(np.sin(rdians)*width*scale) + abs(np.cos(rdians)*height*scale))
# 回転後の中心を修正
M[0,2] += int((new_width-width)/2)
M[1,2] += int((new_height-height)/2)
return cv2.warpAffine(img, M, (new_width, new_height))
angle = 33
scale = 0.5
rotated_img = fitting_rotated_image(img, angle, scale)
これを実行したものが以下になります。

平行移動させる
たとえば、次のように画像全体を斜め右下に移動させたい場合です。

ではX方向に25px、Y方向に50px移動させた画像をつくってみましょう。
この場合、平行移動行列を作ってwarpAffine()を使います。
moving_x = 25 moving_y = 50 M = np.float32([[1, 0, moving_x], [0, 1, moving_y]]) shifted_img = cv2.warpAffine(img, M, (width, height))
実際に作成した画像はこうなります。

切り抜く(crop)
OpenCVで画像を切り抜くには、直接画像データを変更します。
では、次の白枠のようにX座標50、Y座標が50の地点から幅250、高さ250で画像を切り出してみましょう。

x = 50 y = 50 width = 250 height = 250 cropped_img = img[y:y+height, x:x+width]
実際に切り出した画像はこうなります。

挿入する(ロゴ)
例えば、次のようなロゴを画像のX座標10、Y座標10のところに挿入したい場合です。

img = cv2.imread('./images/panda.png')
original_h, original_w = img.shape[:2]
watermark_img = cv2.imread('./images/watermark_1.png')
watermark_h, watermark_w = watermark_img.shape[:2]
img[10:watermark_h + 10, 10:watermark_h + 10] = watermark_img
実際に画像はこうなります。

また、右下にロゴを挿入したい場合はこうなります。
img[original_h - watermark_h - 10:original_h - 10, original_w - watermark_w - 10:original_w - 10] = watermark_img

また、「ぱ」の文字が透過になっている場合をみてみましょう。
分かりにくいですが、「ぱ」の部分だけ透過です。
↓↓↓

次のように透過部分をチェックして画像の粒をひとつずつ変更していきます。
img = cv2.imread('./images/panda.png', cv2.IMREAD_UNCHANGED)
original_h, original_w = img.shape[:2]
logo_img = cv2.imread('./images/logo_2.png', cv2.IMREAD_UNCHANGED)
logo_h, logo_w = logo_img.shape[:2]
x = 10
y = 10
for i in range(0, logo_h):
for j in range(0, logo_w):
if logo_img[i,j][3] != 0:
img[y+i, x+j] = logo_img[i,j]
これを実行するとこうなります。

※ もし透過と非透過の境界線をぼかしたい場合は、以下のようにifの条件を変更してください。
if logo_img[i,j][3] >= 50:
モザイク処理をする
画像にモザイク処理をかけたい場合の手順はこうなります。
- 元画像をコピーする
- コピーした画像を縮小する
- 縮小した画像を元の大きさに拡大する
こうすると縮小したときに画像データの一部が失われることになるので結果モザイク処理ができるようになるわけですね。
では、まずは画像全体にモザイクをかけてみましょう。
original_h, original_w = img.shape[:2] copy_img = img.copy() mosaic_length = 50 small_img = cv2.resize(copy_img, (mosaic_length, mosaic_length), interpolation=cv2.INTER_NEAREST) mosaic_img = cv2.resize(small_img, (original_h, original_w), interpolation=cv2.INTER_NEAREST)
ここで重要なのが、interpolationにcv2.INTER_NEARESTを使っている部分です。このモードは最も補間が雑なモードですが、他のモードだとなめらかにしてしまうため、結果モザイク処理っぽくならない場合があります。
実際に保存した画像がこちらです。

ちなみにmosaic_lengthを変更するとモザイクの度合いが変わります。以下は10で実行した画像です。

では、次に一部分だけをモザイク処理してみましょう。
X座標150、Y座標10、長さ150の正方形のモザイクです。
x = 150 y = 10 length = 150 block_img = img[y:y+length, x:x+length] small_img = cv2.resize(block_img, (10, 10), interpolation=cv2.INTER_NEAREST) mosaic_img = cv2.resize(small_img, (length, length), interpolation=cv2.INTER_NEAREST) img[y:y+length, x:x+length] = mosaic_img
やっていることは、一部を切り出して縮小&拡大でモザイクを作り、そのモザイクを画像の元の位置に戻すだけです。
実際の画像はこうなります。

ぼかす
OpenCVには画像をぼかす方法がいくつかあるのでひとつずつみていきましょう。
平均
指定した範囲の平均を利用するぼかしです。
blurred_img = cv2.blur(img, (7, 7))
※ 数字が大きくなるに連れてぼかしがきつくなります。
実行結果は次のようになります。
(元画像)

(ぼかした画像)

ガウシアンぼかし
blurred_img = cv2.GaussianBlur(img, (5, 5), 0, 0)
(5, 5)の部分がぼかしサイズ、残りの0はXとYの標準偏差です。どちらも数字が大きくなるとぼかしがきつくなります。
(元画像)

(ガウシアンぼかしを実行した画像)

中央値(メディアン)ぼかし
中央値を利用したボカシで、油絵のようなペタッとしたぼかしになります。
blurred_img = cv2.medianBlur(img, 7)
数値が大きくなるほどぼかしがきつくなります。
また、数値は奇数である必要があります。
(元画像)

(中央値ぼかしをした画像)

バイラテラル
より輪郭を保持したままのぼかしです。
blurred_img = cv2.bilateralFilter(img, 50, 50, 50)
(元画像)

(バイラテラルでぼかした画像)

(あまり変化がないように見えますが、文字の輪郭が際立っています)
画像をくっきりさせる(シャープ)
カーネル(配列)を使ってシャープにする方法です。
sharpened_img = cv2.filter2D(img, -1, kernel)
kernel(シャープの度合いが決まる配列)をいくつか用意しました。
下に行くほどシャープがきつくなります。
(シャープ1)
kernel_1 = np.array([
[0, -1, 0],
[0, 3, 0],
[0, -1, 0]
])

(シャープ2)
kernel_2 = np.array([
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]
])

(シャープ3)
kernel_3 = np.array([
[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]
])

(シャープ4)
kernel_4 = np.array([
[-1, -2, -1],
[-2, 12, -2],
[-1, -2, -1]
])

(シャープ5)
kernel_5 = np.array([
[1, 4, 6, 4, 1],
[4, 16, 24, 16, 4],
[6, 24, -476, 24, 6],
[4, 16, 24, 16, 4],
[1, 4, 6, 4, 1]
])

アンシャープマスク
また、addWeighted()を使った方法もあります。
手順はこうなります。
- 元画像から新しいぼかし画像をつくります
- 元画像からぼかし部分を減らします
- 完成
blurred_img = cv2.GaussianBlur(img, (9,9), 10.0)
unsharp_image = cv2.addWeighted(img, 2.0, blurred_img, -1.0, 0, img)
cv2.imwrite('unsharp_image.png', unsharp_image)
(元画像)

(アンシャープマスクした画像)

明るさを変更する
HSV画像に変換して明るさ調整します。
brightness = 50
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv_img)
if brightness > 0:
v[v > 255-brightness] = 255
v[v <= 255-brightness] += brightness
elif brightness < 0:
abs_brightness = np.abs(brightness)
v[v < 0+abs_brightness] = 0 v[v >= 0+abs_brightness] -= abs_brightness
hsv_img = cv2.merge((h, s, v))
brightness_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR)
※ brightnessの値を変更すると明るさが変わります。マイナスの数値もOKです。
やっていることは、画像をHSVに変換して
- 明るくする ・・・ 「V」の部分に brightness を足す(最大値は
255) - 暗くする ・・・ 「V」の部分から brightness を引く(最小値は
0)
を計算しています。
また、brightnessがゼロの場合は何もしません。
(brightness50の場合)

(brightness-50の場合)

コントラストを変更する
画像をLAB形式へ変換してコントラストをつけてみます。
lab_img = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l,a,b = cv2.split(lab_img) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(15,15)) new_l = clahe.apply(l) lab_img = cv2.merge((new_l, a, b)) contrast_img = cv2.cvtColor(lab_img, cv2.COLOR_LAB2BGR)
(元画像)

(コントラストをかけた画像)

色を反転する
invert_img = 255 - img
(元画像)

(色反転した画像)

半透明にする
alpha_img = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA)
alpha_img[:, :, 3] = 150
cv2.imwrite('alpha.png', alpha_img)
やっていることは、画像を透過ありの画像へ変換し、透過部分を数値(150)で設定しているだけです。ちなみに、0が最大透過で255だと透過なしです。
(元画像)

(透過した画像)

おわりに
PHPなどで画像の編集をしていると分かりにくいかもしれませんが、OpenCVを使うと結局は画像も数値の集まり(配列)ということがよくわかりますね。
ただ、やっぱりOpenCVは技術的な用途に使われるからか、きちんと数学を勉強していないと使いにくい部分があるのは確かです。そういった場合は、もしかするとPythonの中からImageMagickのコマンドを実行するほうが手っ取り早いこともあるかもしれません。(開発内容によって選択するといいんじゃないでしょうか)
ではでは〜!






