Python - 画像一括縮小コマンドの作成

公開日:2019-12-04 更新日:2020-01-26
[Python]

1. 概要

画像を一括で拡大縮小するコマンド(resize_img)を作成します。
指定したフォルダ配下の画像を、指定したサイズまたは縮小率で一括縮小します。

2. 動画



3. 仕様

resize_img [オプション]

オプション
-i, --input   入力フォルダのパス(デフォルトはカレントパス)
-o, --output  出力フォルダのパス(デフォルトは output)
-r, --rate    拡大縮小率 0 ~ 5。0 ~ 1 の範囲にすると縮小します。
-w, --width   幅   0 ~ 3840
-he, --height 高さ 0 ~ 2160
-f, --format  出力する画像フォーマット(png か jpg)。
              デフォルトは元のフォーマットのまま出力。

出力フォルダが存在しない場合は作成します。
--rate、--width、--height は、どれか1つだけ指定します。

4. 開発環境

以下のリンク先の手順に従い、Anaconda と VS Code のインストール、仮想環境の作成を行います。
第1回 開発環境構築
第16回 開発環境構築

5. VS Code のプロジェクトの作成


5.1 プロジェクトのフォルダ作成

C:\python\resize_img と言うフォルダを作成します。
コマンドプロンプトで仮想環境に切り替えて、VS Code を起動します。
VS Code で作成した resize_img フォルダを開きます。

5.2 settings.json の作成

メニューの、「ファイル」 → 「基本設定」 → 「設定」をクリック、
設定画面の「ワークスペース」をクリック、
下にスクロールして「settings.json で編集」をクリックします。
仮想環境の python.exe のパスを指定します。
{
    "python.pythonPath" : "C:/python/env1/Scripts/python.exe",
}

5.3 launch.json の作成

resize_img フォルダに test.py を新規作成します。
VS Code の左端にある虫(デバッグ)をクリックします。
画面上部の歯車をクリックします。環境の選択のリストから「python file」を選択します。
もし python が表示されない場合は、作成された launch.json と test.py を削除してもう一度試してください。
成功すると launch.json に以下のように自動作成されます。
上手く行かない場合は、以下をコピーして作成しても問題ありません。
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        }
    ]
}

5.4 動作確認

先ほど作成した test.py を以下のようにして、F5 を押して実行します。
ターミナルに test と表示されれば OK です。
print("test")

また、仮想環境に切り替えてあるコマンドプロンプトで以下を実行して動作することも確認してください。
cd C:\python\resize_img
python test.py

これでプロジェクトの作成は完了です。
次に異なるプロジェクトを作成する場合は、settings.json と launch.json をファイルコピーするだけで OK です。

6. コマンド(resize_img)の作成

ユニットテストをしなければ、resize_img.py と resize_img_sub.py だけで動きます。

ファイル構成
├─ src
│   ├─ resize_img.py
│   └─ resize_img_sub.py
└─ tests
     ├─ test_data
     │   ├─ test.jpg (名前がファイル名っぽいフォルダ)
     │   ├─ abc      (フォルダ)
     │   ├─ a0131337.JPG(画像)
     │   ├─ a0171391.JPG(画像)
     │   ├─ a0211454.jpg(画像)
     │   ├─ a0241498.png(画像)
     │   ├─ abc.csv (空ファイル)
     │   └─ test.txt(空ファイル)
     └─ test_resize_img_sub.py (テストコード)

src/resize_img.py
from resize_img_sub import *
import cv2  # pip install opencv-python
import sys

def main():
    # コマンドラインの引数を取得します
    try:
        args = get_args()
    except Exception as e:
        print(e) # エラーメッセージの表示
        exit(1)  # 終了します

    # 出力フォルダがない場合は、フォルダを作成します
    if os.path.exists(args.output) == False:
        os.makedirs(args.output)

    # ファイル一覧を取得します
    path_list = get_path_list(args.input)
    if len(path_list) == 0:
        print('ファイルがありません')
        exit(1) # 終了します

    # ファイル一覧でループして、画像を1つずつリサイズします
    for path in path_list:

        # 画像ファイルを読み込みます
        org_img = cv2.imread(path)
        org_width  = org_img.shape[1]
        org_height = org_img.shape[0]

        # リサイズ後のサイズを取得します
        resize_size = get_resize_size(org_width, org_height, args.width, args.height, args.rate)

        # 画像をリサイズします
        resized_img = cv2.resize(org_img, resize_size)

        # 出力パスを取得します
        output_path = get_output_path(path, args.output, args.format)

        # 画像ファイルを出力します
        cv2.imwrite(output_path, resized_img)

    # 完了
    print('OK')


# メイン
# モジュールとしてインポートされた場合は実行されないようにする
if __name__ == '__main__':
    main()

src/resize_img_sub.py
import argparse
import glob
import os

# コマンドラインの引数を取得します
def get_args():
    parser = argparse.ArgumentParser(description = '画像の拡大縮小')
    parser.add_argument('-i', '--input',  default = '.',            help = '入力フォルダ')
    parser.add_argument('-o', '--output', default = 'output',       help = '出力フォルダ')
    parser.add_argument('-f', '--format', choices = ['jpg', 'png'], help = '画像フォーマット')
    parser.add_argument('-r', '--rate',    type = float,            help = '拡大縮小率')
    parser.add_argument('-w', '--width',   type = int,              help = '幅')
    parser.add_argument('-he', '--height', type = int,              help = '高さ')
    # -h はヘルプとして予約済みなので、-h を使うとエラーになる
    # argparse.ArgumentError: argument -h/--height: conflicting option string: -h
    # parser.add_argument('-h', '--height', type = int, help = '高さ')

    args = parser.parse_args()

    if args.rate != None and args.width != None:
        raise Exception('--rate と --width は、一緒に指定しないでください。')

    if args.rate != None and args.height != None:
        raise Exception('--rate と --height は、一緒に指定しないでください。')

    if args.width != None and args.height != None:
        raise Exception('--width と --height は、一緒に指定しないでください。')

    if args.rate == None and args.width == None and args.height == None:
        raise Exception('--rate、--width、--height のどれかを指定してください。')
 
    if args.rate != None and (args.rate > 5 or 0 >= args.rate) :
        raise Exception('--rate は 0.001 ~ 5 の範囲で指定してください。')

    if args.width != None and (args.width > 1920 * 2 or 0 >= args.width):
        raise Exception('--width は 1 ~ {0} の範囲で指定してください。'.format(1920 * 2))

    if args.height != None and (args.height > 1080 * 2 or 0 >= args.height):
        raise Exception('--height は 1 ~ {0} の範囲で指定してください。'.format(1080 * 2))
    
    if os.path.exists(args.input) == False:
        raise Exception('--input で指定されたフォルダが存在しません。')

    return args


# ファイル一覧を取得します
def get_path_list(dir_path):
    path_list = [path for path in glob.glob(dir_path + '/*.*')
        if path.lower().endswith('.png') or path.lower().endswith('.jpg')
        if os.path.isfile(path)
    ]
    return path_list

# リサイズ後のサイズを取得します
def get_resize_size(from_width, from_height, to_width = None, to_height = None, rate = None):
    # 幅 が指定されている場合は、幅 から拡大縮小率を決定します
    if to_width != None:
        rate = to_width / from_width

    # 高さ が指定されている場合は、高さ から拡大縮小率を決定します
    if to_height != None:
        rate = to_height / from_height
    
    # リサイズ後のサイズを算出します
    width  = int(from_width  * rate)
    height = int(from_height * rate)

    # サイズが 1 未満になった場合は 1 にします
    if width < 1:
        width = 1
    
    if height < 1:
        height = 1
    
    return (width, height)


# 出力パスを取得します
def get_output_path(org_img_path, output_dir_path, img_format = None):
    # パスからファイル名だけを取得します
    filename = os.path.basename(org_img_path)

    # 画像ファイルのフォーマットが指定されている場合は、拡張子を変更します
    if img_format != None:
         # 拡張子を除いたファイル名を取得します
        filename_without_ext = os.path.splitext(filename)[0] # [0]ファイル名  [1]拡張子(先頭のドット付き)
        filename = filename_without_ext + '.' + img_format

    # 出力パスを返します
    return output_dir_path + '/' + filename

tests/test_resize_img_sub.py
import unittest
import sys
import os

# src 配下のモジュールをインポートするため、src フォルダにパスを通します
py_path = os.path.abspath(__file__)
sys.path.append(os.path.dirname(os.path.dirname(py_path)) + "/src")
# __file__ は「python -m モジュール名」で動かした時はフルパスを返しますが、
# 「python ファイル名」で動かした時は相対パスを返すため、絶対パスにしてから追加します
from resize_img_sub import *

class Test_resize_img_sub(unittest.TestCase):

    # get_args のテスト
    # コマンドラインの引数が異常なパターン
    def test_get_args_1(self):
        py = 'resize_img.py'

        # -r と -w と -he を指定しない
        with self.assertRaises(Exception) as e:
            sys.argv = [py]
            args = get_args()
        # print(e.exception.args[0])  # エラーメッセージの表示
        # print(type(e.exception))    # エラーの型の確認

        # -r と -w を同時に指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-r', '0.5', '-w', '300']
            args = get_args()

        # -r と -he を同時に指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-r', '0.5', '-he', '300']
            args = get_args()

        # -w と -he を同時に指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-w', '300', '-he', '300']
            args = get_args()
        
        # -r に 0 ~ 5 の範囲外を指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-r', '0']
            args = get_args()
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-r', '5.1']
            args = get_args()

        # -w に 0 ~ 3840 の範囲外を指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-w', '0']
            args = get_args()
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-w', '3841']
            args = get_args()

        # -he に 0 ~ 2161 の範囲外を指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-he', '0']
            args = get_args()
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-he', '2161']
            args = get_args()

        # -i に存在しないパスを指定
        with self.assertRaises(Exception) as e:
            sys.argv = [py, '-i', 'xyz', '-r', '0.5']
            args = get_args()

    # get_args のテスト
    # コマンドラインの引数が正常パターン
    def test_get_args_2(self):
        py = 'resize_img.py'

        sys.argv = [py, '-r', '0.1']
        args = get_args()
        self.assertEqual(args.rate, 0.1)

        sys.argv = [py, '-r', '5']
        args = get_args()
        self.assertEqual(args.rate, 5)

        sys.argv = [py, '-w', '1']
        args = get_args()
        self.assertEqual(args.width, 1)

        sys.argv = [py, '-w', '3840']
        args = get_args()
        self.assertEqual(args.width, 3840)

        sys.argv = [py, '-he', '1']
        args = get_args()
        self.assertEqual(args.height, 1)

        sys.argv = [py, '-he', '2160']
        args = get_args()
        self.assertEqual(args.height, 2160)

    # get_path_list のテスト
    def test_get_path_list_1(self):
        test_dir = os.path.dirname(os.path.abspath(__file__)) + "/test_data"
        path_list = get_path_list(test_dir)
        self.assertEqual(len(path_list), 4)

    # get_resize_size のテスト
    def test_get_resize_size_1(self):
        size = get_resize_size(from_width = 100, from_height = 1000, to_width = 50)
        self.assertEqual(size[0], 50)
        self.assertEqual(size[1], 500)

        size = get_resize_size(from_width = 1000, from_height = 100, to_height = 50)
        self.assertEqual(size[0], 500)
        self.assertEqual(size[1], 50)

        size = get_resize_size(from_width = 100, from_height = 200, rate = 0.5)
        self.assertEqual(size[0], 50)
        self.assertEqual(size[1], 100)

        size = get_resize_size(from_width = 100, from_height = 100, rate = 0.00001)
        self.assertEqual(size[0], 1)
        self.assertEqual(size[1], 1)

    # get_output_path のテスト
    def test_get_output_path_1(self):
        self.assertEqual(get_output_path('c:/test/test.png', 'c:/output'), 'c:/output/test.png')
        self.assertEqual(get_output_path('c:/test/test.jpg', 'c:/output'), 'c:/output/test.jpg')
        self.assertEqual(get_output_path('c:/test/test.png', 'c:/output', img_format = 'png'), 'c:/output/test.png')
        self.assertEqual(get_output_path('c:/test/test.png', 'c:/output', img_format = 'jpg'), 'c:/output/test.jpg')
        self.assertEqual(get_output_path('c:/test/test.jpg', 'c:/output', img_format = 'png'), 'c:/output/test.png')
        self.assertEqual(get_output_path('c:/test/test.jpg', 'c:/output', img_format = 'jpg'), 'c:/output/test.jpg')

if __name__ == '__main__':
    unittest.main()

7. VS Code上で実行

今回作ったコマンドはコマンドラインの引数が必要なため、
VS Codeで動かす場合は、launch.json でコマンドラインの引数を指定してください。
指定すると、実行時にコマンドラインの引数として渡されるようになります。

launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",

            //追加
            "args": ["-i", "c:/python/resize_img/tests/test_data", "-f", "jpg", "-r", "0.5"]
        }
    ]
}

8. コマンドプロンプトで実行

resize_img.py がある src をカレントディレクトリにして、以下のようにして実行できます。
python resize_img.py -r 0.5
または
python -m resize_img -r 0.5

また、以下の resize_img.bat をパスが通っている場所に作成しておくと、 resize_img と引数だけでどこからでも実行できるようになりますます。

resize_img.bat
python c:/python/resize_img/src/resize_img.py %*

9. ユニットテスト

VS Codeで動かす場合は、
launch.json で指定したコマンドラインの引数をコメントアウトして、
テストコードの test_resize_img_sub.py を開いて実行してください。

コマンドプロンプトで実行する場合は、tests フォルダをカレントディレクトリにして、以下のコマンドを実行してください。
python test_resize_img_sub.py
または
python -m test_resize_img_sub
または
python -m unittest