asken テックブログ

askenエンジニアが日々どんなことに取り組み、どんな「学び」を得ているか、よもやま話も織り交ぜつつ綴っていきます。 皆さまにも一緒に学びを楽しんでいただけたら幸いです!

PICTとPythonを使って大量にテスト用の画像データを生成してみた

こんにちは、コンシューマ事業部プロダクト開発部の入江です。

あすけんではバックエンドの開発を担当しています。

今回はPICTとPythonを使ってテスト用の画像を大量に作成してみた話をします。

今回やりたかったこと

現在askenではPHPからKotlinへのリアーキテクチャを段階的に実施中です。

現在、画像アップロード機能を移行しています。

この機能ではアップロードした縦横サイズやExifの情報に応じて処理が分岐しており、テストするためには画像のサイズやExifを因子とした多種多様なファイルを用意する必要がありました。

Exifとは写真の中に埋め込まれるメタデータであり、撮影日時、GPS情報、写真の向きといったデータが格納されています。

そして、このExif情報を読み書きするには特定のツールなどが必要でOSに搭載されているようなデフォルトのツールでは簡単にできません。そのためサイズを変更した上にExifの書き換えを1枚1枚行い、テストデータを準備することはとても大変なことが予想されます。

そこでテストケース作成ツールのPICTとPythonの画像ライブラリを利用してテストケースの定義からテストデータを作成してみました。

実行環境

必要なツール

  • PICT
  • Python

PICTについて

PICTは、組み合わせテストのためのテストケース生成ツールです。

組み合わせテストでは各テスト因子間の全ての可能な組み合わせを作成するとテスト数がとても多くなります。

このツールでは、ペアワイズ法と呼ばれる手法を用いて、各テスト因子の値が少なくとも一度は現れるように調整してテストケースを効率的に生成できます。

ソフトウェアの多くの不具合は因子が1つ、または2つの組み合わせで発生していると言われてます。

このペアワイズ法と呼ばれる方法でテストケースを作成することで、工数を削減しつつも効果的にテストを実施することが可能になります。

今回は2因子間網羅でテストケースを作成しました。

Pythonコード

画像の作成、Exifの設定を行うために、Pillowおよびpiexifと呼ばれるモジュールを利用しました。

PillowはOpenCVほど多機能ではないですが、サンプルコードを見たところ簡単に指定したサイズの画像作成および回転/反転が容易に実装できそうだったからです。

piexifはExifの情報を変更するために利用しています。Pillow単体だとExif情報の参照はできるものの設定するメソッドがなさそうでした。

下記が実際に作成したコードです。

縦横サイズ、Exif情報の有無を記載したCSVを読み込み、パラメータごとに画像を生成、回転、およびExifの設定をしています。

  • generator.py
from PIL import Image, ImageDraw
import piexif
import csv

def createImage(id,record):
    filename = f"./testcase_image/case{id}.jpg"
    #画像を生成する
    im = Image.new("RGB",(int(record["height"]),int(record["width"])),"green")# Imageインスタンスを作る

    orientation = int(record["Orientation"])
    draw = ImageDraw.Draw(im)# im上のImageDrawインスタンスを作る
    draw.text((0,0),f"testcase{id}:{record["height"]} x {record["width"]}",font_size=50)

    #テストケースのExifの向き情報にしたがって画像を回転させる
    if orientation == 2:
        draw.text((0,100),f"FLIP_LEFT_RIGHT",font_size=30)
        im = im.transpose(Image.FLIP_LEFT_RIGHT)
    elif orientation == 3:
        draw.text((0,100),f"ROTATE_180",font_size=30)
        im = im.transpose(Image.ROTATE_180)
    elif orientation == 4:
        draw.text((0,100),f"FLIP_TOP_BOTTOM",font_size=30)
        im = im.transpose(Image.FLIP_TOP_BOTTOM)
    elif orientation == 5:
        draw.text((0,100),f"FLIP_TOP_BOTTOM && ROTATE_90",font_size=30)
        im = im.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM)
    elif orientation == 6:
        draw.text((0,100),f"ROTATE_90",font_size=30)
        im = im.transpose(Image.ROTATE_90)
    elif orientation == 7:
        draw.text((0,100),f"FLIP_TOP_BOTTOM && ROTATE_270",font_size=30)
        im = im.transpose(Image.ROTATE_270).transpose(Image.FLIP_TOP_BOTTOM)
    elif orientation == 8:
        draw.text((0,100),f"ROTATE_270",font_size=30)
        im = im.transpose(Image.ROTATE_270)    
    

    im.save(filename)

    ##Exif情報を埋め込む
    exif_dict = {}
    oth = {piexif.ImageIFD.Make: b"Asken",
        piexif.ImageIFD.Model: b"test"        
        }
    if int(record["Orientation"]) != 0:
        oth |= {piexif.ImageIFD.Orientation: int(record["Orientation"])}
    exif_dict["0th"] = oth

    if int(record["times"]) == 1:
        exif_ifd = {piexif.ExifIFD.DateTimeOriginal: u"2020:01:01 12:34:56",}
        exif_dict["Exif"] = exif_ifd
    if int(record["gps"]) == 1:
        gps = {piexif.GPSIFD.GPSLatitudeRef: b"N",
            piexif.GPSIFD.GPSLatitude: ((10, 1), (20, 1), (30, 1)),
            piexif.GPSIFD.GPSLongitudeRef: b"W",
            piexif.GPSIFD.GPSLongitude:((15, 1), (25, 1), (35, 1))}
        exif_dict["GPS"] = gps

    exif_bytes = piexif.dump(exif_dict)
    piexif.insert(exif_bytes, filename) 

# Main処理
filename = 'input.csv'
idx = 0
with open(filename, encoding='utf8', newline='') as f:
    csvreader = csv.DictReader(f)
    for row in csvreader:
        idx = idx + 1
        createImage(idx,row)

このコード中でExifのOrientation情報に合わせて画像の向きと反転を行っています。

Orientationでは1から8までの数字によって、画像の向きと反転情報を持っており、以下のような組み合わせになっています。

Orientation情報 1 2 3 4 5 6 7 8
意味 なし 左右反転 180度回転 上下反転 上下反転&右90度回転 右90度回転 上下反転&左90度回転 左90度回転

数字から直感的に角度と反転しているかいないかの対応が分かりにくいので、何度かTry & Errorで調整しました。

ちなみに画像生成時にどのテストパターンのテストであるか画像にテキストをdraw.text()を利用して追記してます。

テストケース作成の流れを簡単にまとめます。

  1. テスト設計/水準を作成する

    今回は縦横サイズ、Exifの向き/撮影日時を因子として出しています。因子はspreadsheetに記載してチーム内でRVを行いました。

  2. テスト設計/水準からPICTの入力ファイルを作成する

    この作業は手間ではなかったので手作業で入力ファイルを作成しましたが、複雑になってきた場合は自動生成するようにしても良いなと思っています。

  3. PICTを実行して2因子間網羅のテストケースをcsv形式で作成する
    sh pict ./testcase.txt | gsed -e "s/\s\+/,/g"

    PICTの出力ファイルはスペース区切りになってしまうため、上記のようにsedを使ってCSV形式に直します。後続のPytonでCSV読み込みしたいからです。

  4. generator.pyを実行して画像を作成する

このようにして画像を50枚ほど作成しました。 アップロード処理のテスト実施は画像が意図したものか目視で確認しました。

テストデータ作成時にテストケース番号を画像に入れ込むなどを行ったので実際に試験実施した方からは確認しやすかったとの評価がありました。

※生成した画像の例

生成した画像データ

最後に

今回はテスト用の画像をPICTとpythonで作成してテストの準備を省力化してみました。

応用しだいでは、PICTで生成したファイルを別なプログラムの入力として読み込ませることで、さまざまなバリエーションテストを自動で実行できるのではと考えてます。

誰かの参考になれば幸いです。

askenでは創意工夫して業務を遂行できるエンジニアを幅広く募集しています。 興味を持っていただいた方はカジュアル面談からまずはお話ししませんでしょうか?!

お気軽にご連絡ください。 www.asken.inc

参考

PICT関連

https://qiita.com/odekekepeanuts/items/6eceddc534d87fc797cc

https://developer.mamezou-tech.com/blogs/2022/07/15/pairwise-test-case-creation-tool-pict/

piexif関連

https://pypi.org/project/piexif/

https://qiita.com/mo256man/items/e3d07fbeab58862213ef

pillow関連

https://pypi.org/project/pillow/

https://note.nkmk.me/python-pillow-basic/