
Python と外部ライブラリ OpenCV を使うことで、簡単に画像の拡大・縮小を行うことができる。今回は、ブログやSNSにデジカメで撮った写真を投稿することを想定し、縦横比を保ったまま拡大・縮小するプログラムを作っていくことにする。
目次
サンプル・プログラム
サンプル・プログラムの使い方

インストールできたら、プログラム "resizeImage.py" を実行してみてほしい。ファイルダイアログを表示するので、変換したい画像ファイルを1つ選択する。

すると、選択した画像の幅・高さをピクセルで表示するので、拡大または縮小したいサイズを入力する。後述するように、このプログラムでは縦横比を維持するようにしており、幅を入力すると高さが、高さを入力すると幅のサイズが変化するようになっている。
サイズが指定できたら、[変換]ボタンをクリックする。
サイズが指定できたら、[変換]ボタンをクリックする。

最後に、拡大または縮小した画像をファイル保存するかどうかを確認するダイアログを表示する。[はい]をクリックすると、元のファイルと同じフォルダに、ファイル名に "_resize" を追加した画像ファイルを保存する。もし同名ファイルがあれば上書きしない(保存しない)。
解説:画像ファイルを読み込む
resizeImage.py
def readImage():
"""ファイルダイアログを表示し,画像ファイルを読み込む
Returns:
obj, str: 画像データ, 読み込んだ画像ファイル名(フルパス)
Raises:
FileNotFondError: 画像ファイルが見つからない
ValueError: 画像ファイルではない
"""
dialogTitle = "画像ファイルを選択してください"
filetypes = [("画像ファイル", "*.jpg;*.png;*.webp;*.bmp"), ("すべてのファイル", "*.*")]
fullname = filedialog.askopenfilename(title=dialogTitle, filetypes=filetypes)
# 選択ファイルが存在するかどうか(バリデーション)
if os.path.exists(fullname):
# jpgファイルを読み込む
image = cv2.imread(fullname)
if image is None:
raise ValueError(f"{fullname} は画像ファイルではありません")
else:
raise FileNotFoundError(f"{fullname} が見つかりません")
return image, fullname
ユーザー定義関数 readImage は、ファイルダイアログを表示し、画像ファイルを読み込み、読み込んだ画像データと画像ファイル(フルパス)を返す。
ファイルダイアログは、前回学んだ filedialog.askopenfilenames関数を利用する。
また、前回学んだ imageio.v2.imread を使って画像ファイルを読み込む。ファイルが読み込めなかったり、画像ファイルでない場合には例外を発生する。
ファイルダイアログは、前回学んだ filedialog.askopenfilenames関数を利用する。
また、前回学んだ imageio.v2.imread を使って画像ファイルを読み込む。ファイルが読み込めなかったり、画像ファイルでない場合には例外を発生する。
解説:画像ファイルを保存する
resizeImage.py
def saveImage(fullname, image):
"""画像ファイルを保存する
Args:
fullname(str): 保存する画像ファイル名(フルパス)
image(obj): 保存する画像オブジェクト
"""
# 同名ファイルがあれば例外発生
if os.path.exists(fullname):
raise FileExistsError(f"{fullname} が存在します")
# 画像を保存
else:
cv2.imwrite(fullname, image)
ユーザー定義関数 saveImage は、画像ファイル(フルパス)と画像データを引数とし、画像をファイルに保存する。
同名ファイルが存在する場合には例外を発生する。
ファイル保存は、前回学んだ imwrite を利用する。
同名ファイルが存在する場合には例外を発生する。
ファイル保存は、前回学んだ imwrite を利用する。
解説:画像をリサイズする
resizeImage.py
class resizeImage:
"""画像をリサイズするクラス
"""
def __init__(self, root, image, fullname):
"""コンストラクタ:リサイズする幅、高さをダイアログから入力
Args:
root(obj): tkinterダイアログ
image(obj): 画像オブジェクト
fullname(str): 読み込んだ画像ファイル名(フルパス)
"""
self.sourFname = fullname
self.sourImage = image
self.heightOrigin, self.widthOrigin = self.sourImage.shape[:2]
root.title("リサイズ")
# widthのラベルと入力フィールド
labelWidth = tk.Label(root, text="幅")
labelWidth.grid(row=0, column=0, padx=10, pady=10)
self.entryWidthVar = tk.StringVar()
self.entryWidthVar.set(str(self.widthOrigin))
self.entryWidth = tk.Entry(root, textvariable=self.entryWidthVar)
self.entryWidth.grid(row=0, column=1, padx=10, pady=10)
# widthの入力が変更されたときにupdateWidth()を呼び出す
self.updateWidthID = self.entryWidthVar.trace_add("write", self.updateWidth)
# heightのラベルと入力フィールド
labelHeight = tk.Label(root, text="高さ")
labelHeight.grid(row=1, column=0, padx=10, pady=10)
self.entryHeightVar = tk.StringVar()
self.entryHeightVar.set(str(self.heightOrigin))
self.entryHeight = tk.Entry(root, textvariable=self.entryHeightVar)
self.entryHeight.grid(row=1, column=1, padx=10, pady=10)
# heightの入力が変更されたときにupdateHeight()を呼び出す
self.updateHeightID = self.entryHeightVar.trace_add("write", self.updateHeight)
# ボタン
submitButton = tk.Button(root, text="変換", command=self.resizeImage)
submitButton.grid(row=2, columnspan=2, padx=10, pady=10)
他のプログラムでも利用できるよう、画像の拡大・縮小(リサイズ)するユーザー定義クラス resizeImage を用意した。

コンストラクタでは、左図のウィンドウを Tkinterダイアログを使って実装している。
Tkinterダイアログ はユーザーに値を入力させる単純なモーダルダイアログを作成するためのクラスで、OSに依存しない。GUIオブジェクトの配置は、GUIツールキットの Tcl/Tk に準ずる。
Tkinterダイアログ はユーザーに値を入力させる単純なモーダルダイアログを作成するためのクラスで、OSに依存しない。GUIオブジェクトの配置は、GUIツールキットの Tcl/Tk に準ずる。
ダイアログのタイトルは、titleメソッドで指定する。
GUIオブジェクトの配置は、gridメソッドを使う。Excelのセルにオブジェクトを配置するイメージである。ただし、行番号(row)、列番号(column)は0からはじまる。row=0, column=0 は左上隅を指す。padxには横方向の隙間を、padyには縦方向の隙間を設定する。
GUIオブジェクトの配置は、gridメソッドを使う。Excelのセルにオブジェクトを配置するイメージである。ただし、行番号(row)、列番号(column)は0からはじまる。row=0, column=0 は左上隅を指す。padxには横方向の隙間を、padyには縦方向の隙間を設定する。
resizeImage.py
def updateWidth(self, *args):
"""画像の幅を変更したとき,縦横比が変わらないよう高さを変更する
Args:
*args: イベント処理用
"""
try:
# 幅を取り出す
width = int(self.entryWidth.get())
# 監視を一時停止する
self.entryHeightVar.trace_remove("write", self.updateHeightID)
# 縦横比を保つ
height = int(width * (self.heightOrigin / self.widthOrigin))
self.entryHeightVar.set(str(height))
# 監視を再開する
self.updateHeightID = self.entryHeightVar.trace_add("write", self.updateHeight)
except ValueError:
self.entryWidthVar.set("")
前述したように、このプログラムでは縦横比一定を保つために、画像の幅を変更したときに呼び出されるメソッドが updateWidth である。
あらかじめ保存しておいたオリジナルの幅、高さを参照し、同じ比率で高さの値を変更する。
あらかじめ保存しておいたオリジナルの幅、高さを参照し、同じ比率で高さの値を変更する。
resizeImage.py
def updateHeight(self, *args):
"""画像の高さを変更したとき,縦横比が変わらないよう幅を変更する
Args:
*args: イベント処理用
"""
try:
# 高さを取り出す
height = int(self.entryHeight.get())
# 監視を一時停止する
self.entryWidthVar.trace_remove("write", self.updateWidthID)
# 縦横比を保つ
width = int(height * (self.widthOrigin / self.heightOrigin))
self.entryWidthVar.set(str(width))
同様に、画像の高さを変更したときに呼び出されるメソッドが updateHeight である。
あらかじめ保存しておいたオリジナルの幅、高さを参照し、同じ比率で幅の値を変更する。
あらかじめ保存しておいたオリジナルの幅、高さを参照し、同じ比率で幅の値を変更する。
resizeImage.py
def resizeImage(self):
"""画像をリサイズしてファイルに保存する
"""
try:
width = int(self.entryWidth.get())
height = int(self.entryHeight.get())
res = messagebox.askyesno("保存", "リサイズした画像ファイルを保存しますか?")
if res:
fname, fextension = os.path.splitext(os.path.basename(self.sourFname))
# 新しいファイル名を作成
newFname = fname + '_resize' + fextension
destFname = os.path.join(os.path.dirname(self.sourFname), newFname)
print(destFname);
# 画像をリサイズする
destImage = cv2.resize(self.sourImage, (width, height))
# 画像をファイルに保存する
saveImage(destFname, destImage)
except Exception as e:
messagebox.showerror("error", str(e))
exit()
画像をリサイズし、ファイルに保存するメソッドが resizeImage である。OpenCVの resize を用いる。
解説:メイン・プログラム
resizeImage.py
# メイン・プログラム =======================================================
# 画像ファイルを読み込む
try:
image, fullname = readImage()
except Exception as e:
messagebox.showerror("error", str(e))
exit()
# ウィンドウの作成
root = tk.Tk()
app = resizeImage(root, image, fullname)
# ウィンドウの表示
root.mainloop()
リサイズ処理をクラスに分離したので、メイン・プログラムはとてもシンプルになった。
前述の Tkinterダイアログ を用意したら、表示・入力させるのに mainloopメソッドを使う。
mainloopメソッドは一種の無限ループで、ダイアログを表示し続けることができる。もしこのメソッドがないと、ダイアログが一瞬だけ表示され、すぐにプログラムが終了してしまう。
逆に、mainloopメソッドを脱出する条件がなければ、ループから抜け出すことができない。本プログラムでは、前述のユーザー定義メソッド resizeImage の最後の exit関数を使ってループから脱出しプログラムを終了させる。
前述の Tkinterダイアログ を用意したら、表示・入力させるのに mainloopメソッドを使う。
mainloopメソッドは一種の無限ループで、ダイアログを表示し続けることができる。もしこのメソッドがないと、ダイアログが一瞬だけ表示され、すぐにプログラムが終了してしまう。
逆に、mainloopメソッドを脱出する条件がなければ、ループから抜け出すことができない。本プログラムでは、前述のユーザー定義メソッド resizeImage の最後の exit関数を使ってループから脱出しプログラムを終了させる。
練習問題
次回予告
スマホやデジカメで撮影された画像データには、撮影日時に加え、位置情報(緯度・経度)、絞り、シャッター速度、カメラの機種名、メーカー名など、さまざまな情報が Exif(Exchangeable image file format) と呼ばれるタグ情報となって埋め込まれている。対応している画像フォーマットは JPEG、TIFF、PNGだ。
次回は、Python と外部ライブラリ Pillow と piexif を使い、JPEG画像ファイルから Exif情報を取り出したり削除するプログラムを作ってみることにする。
次回は、Python と外部ライブラリ Pillow と piexif を使い、JPEG画像ファイルから Exif情報を取り出したり削除するプログラムを作ってみることにする。
コラム:画像リサイズのアルゴリズム
前回のコラムでラスターデータについて説明した。今回の本編では OpenCV の resize を使うことでラスターデータのリサイズを行ったが、もしゼロからリサイズ・プログラムを書くとなると、かなり手数がかかる処理である。

話を簡単にするため、1次元のモノクロ画像(256階調)で考えてみることにする。
まず2倍に拡大することを考えてみよう。左図のように4ピクセルから成る1次元画像(直線)がある(①)。これを2倍に拡大して8ピクセルにするわけだが、途中に挿入されたピクセルの色をどうするか(②)――1つの方法として、拡大前の左右のピクセルの平均値をとって挿入してやる(③)。
まず2倍に拡大することを考えてみよう。左図のように4ピクセルから成る1次元画像(直線)がある(①)。これを2倍に拡大して8ピクセルにするわけだが、途中に挿入されたピクセルの色をどうするか(②)――1つの方法として、拡大前の左右のピクセルの平均値をとって挿入してやる(③)。
一番左(255)と左から2番目(160)の平均の207を間に挿入、左から2番目(160)と3番目(80)の平均の120を間に挿入‥‥という具合に計算していく。これをバイリニア補間法と呼ぶ。

次に2分の1に縮小する方法だが、拡大と同様に隣同士の平均値を取って、4つのピクセルを2つに減らす。
実際のラスターデータは2次元のカラー画像なので、左右に加えて上下のピクセルも計算対象にし、さらにRGB値で計算してやる必要があるし、拡大・縮小率も2の倍数とは限らないので、かなり計算量が増える。

実際にバイリニア補間法で画像のリサイズを行ってみると、コントラストが急に変化する境界付近でノイズが発生する。そこで、隣り合うピクセルだけではなく、周辺のピクセルを計算対象にするバイキュービック補間法が考え出された。当然、計算量は増える。

さらに、補間領域を可変的にしたミップマップや、1つ1つのピクセルを扱うのではなく色の変化を周波数成分とみなすフーリエ変換などのリサイズ・アルゴリズムがある。
興味がある方はプログラミングしてみて欲しい。Python はそれほど処理速度が速くないので、40年前の8ビットPCがそうだったように、画面上でリサイズしている様子を目で追うことができるかもしれない。

実際にバイリニア補間法で画像のリサイズを行ってみると、コントラストが急に変化する境界付近でノイズが発生する。そこで、隣り合うピクセルだけではなく、周辺のピクセルを計算対象にするバイキュービック補間法が考え出された。当然、計算量は増える。

さらに、補間領域を可変的にしたミップマップや、1つ1つのピクセルを扱うのではなく色の変化を周波数成分とみなすフーリエ変換などのリサイズ・アルゴリズムがある。
興味がある方はプログラミングしてみて欲しい。Python はそれほど処理速度が速くないので、40年前の8ビットPCがそうだったように、画面上でリサイズしている様子を目で追うことができるかもしれない。
参考サイト
- ニアレストネイバー法、バイリニア法、バイキュービック法のリサイズ比較:ファンタジーリンク
(この項おわり)