マイブログ リスト

2024年7月23日火曜日

透過pngの余白を自動調整するプログラムを作ってみた

おはようございます。
最近、キメラを召喚して対戦するゲームを作っています。

キメラの画像はコチラの透過pngの素材を使わせていただいているのですが、
どうにも余白が不揃い(全体的に右寄り)で、少し扱いづらい。
(でもメッチャ可愛いし使いたい)

なので、余白の自動調整をするプログラムを作ります。

ロジックとしては、

1.画像を読み込む
2.余白を全カットする
3.いい感じに余白をつけなおす
4.出力する

という想定。


というわけで、調査開始。

調べてみると、ちょうどいい記事を発見。
【画像処理】OpenCVを使って画像周りの余白削除(トリミング)を自動化してみた!(1)

pythonは3日くらいしか触っていませんが、何となくopenCVをインストールして、ところどころ修正を入れながら、適当な画像で変換開始!



うん、真っ黒だね!!!


コードを触って確かめていくと、どうやら最初の画像読み込み(cv2.imread)の時点で、真っ黒になっているらしく、透過pngの場合には「flags = cv2.IMREAD_UNCHANGED」が必要だったらしい。


    # 画像の読み込み
    img = cv2.imread(image, cv2.IMREAD_UNCHANGED)


オプションをつけてから、読み込んだ画像をそのまま吐き出すと、無事に透過pngが扱えることを確認!

良かった~、と安心して、

修正したプログラムを再度実行!!




うん、真っ黒だね!!!

今度はなんだ(# ゚Д゚)!?

どうやら、グレースケール・2値化したときに、やっぱり真っ黒になっている。
現状のコードではダメらしい(知らんけど)

ロジックとしては、地と図で2値化できればいいはずなので、ChatGPTに聞いてみる。


(こういう限定的な用途での実装コードは結構頼れるなぁ)


    # アルファチャンネルを抽出
    alpha_channel = img[:, :, 3]

    # 透明部分を黒(0)、不透明部分を白(255)に二値化
    _, binary_mask = cv2.threshold(alpha_channel, 0, 255, cv2.THRESH_BINARY)


上記のコードで、いい感じに2値化できるらしい。

というわけで修正したコードを実行。


余白ゼロ、超コンパクト!
いい感じだ( *´艸`)


最後に、余白をいい感じに追加

余白の追加は、cv2.copyMakeBorder()でできるらしいので、
横は中央揃え、縦は下揃えとして適当に計算するコードを追加。
(めんどくさいので全部直書き)


    #完成画像のサイズ定数
    TARGET_HEIGHT = 1300
    TARGET_WIDTH = 1300
    BOTTOM_MARGIN = 100

    # 余白の計算(bottom固定、左右中央揃え)
    crop_h, crop_w = crop_img.shape[:2]
    bm = BOTTOM_MARGIN
    tm = TARGET_HEIGHT - crop_h - bm
    lm = (TARGET_WIDTH - crop_w)//2
    rm = TARGET_WIDTH - lm - crop_w

    # 余白が足りない場合のエラー表示(要:定数の調整)
    if(tm < 0 | lm < 0):
        print("error:" + image_name)
        continue

    # 余白の追加
    margined_image = cv2.copyMakeBorder(crop_img, tm, bm, lm, rm,
                                         cv2.BORDER_CONSTANT, (0,0,0,0))


実行結果

Before

After




プログラムの完成版はこちら


import cv2
import os
import glob

# 余白を削除する関数
def crop(image): #引数は画像の相対パス
    # 画像の読み込み
    img = cv2.imread(image, cv2.IMREAD_UNCHANGED)

    # アルファチャンネルを抽出
    alpha_channel = img[:, :, 3]

    # 透明部分を黒(0)、不透明部分を白(255)に二値化
    _, binary_mask = cv2.threshold(alpha_channel, 0, 255, cv2.THRESH_BINARY)

    # 輪郭を抽出
    contours = cv2.findContours(binary_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]

    # 輪郭の座標をリストに代入していく
    x1 = [] #x座標の最小値
    y1 = [] #y座標の最小値
    x2 = [] #x座標の最大値
    y2 = [] #y座標の最大値
    for i in range(0, len(contours)):
        ret = cv2.boundingRect(contours[i])
        x1.append(ret[0])
        y1.append(ret[1])
        x2.append(ret[0] + ret[2])
        y2.append(ret[1] + ret[3])

    # 輪郭の一番外枠を切り抜き(※5pxの余裕を持たせた)
    x1_min = min(x1)-5
    y1_min = min(y1)-5
    x2_max = max(x2)+5
    y2_max = max(y2)+5
    cv2.rectangle(img, (x1_min, y1_min), (x2_max, y2_max), (0, 255, 0), 3)

    crop_img = img[y1_min:y2_max, x1_min:x2_max]

    return img, crop_img

#フォルダ名、拡張子
INPUTDIR = 'images_fot_trim'
OUTPUTDIR = 'trimmed_images'
EXT = 'png'

#完成画像のサイズ定数
TARGET_HEIGHT = 1300
TARGET_WIDTH = 1300
BOTTOM_MARGIN = 100

# 編集後の画像の保存ディレクトリの作成
if not os.path.isdir(OUTPUTDIR):
    os.mkdir(OUTPUTDIR)

# INPUTDIR内の全ての画像に対してループ
for image in glob.glob(INPUTDIR + '/*.' + EXT):

    # 相対パスの部分を削除
    image_name = os.path.basename(image)

    # クロップ
    img, crop_img = crop(image)

    # 余白の計算(bottom固定、左右中央揃え)
    crop_h, crop_w = crop_img.shape[:2]
    bm = BOTTOM_MARGIN
    tm = TARGET_HEIGHT - crop_h - bm
    lm = (TARGET_WIDTH - crop_w)//2
    rm = TARGET_WIDTH - lm - crop_w

    # 余白が足りない場合のエラー表示(要:定数の調整)
    if(tm < 0 | lm < 0):
        print("error:" + image_name)
        continue

    # 余白の追加
    margined_image = cv2.copyMakeBorder(crop_img, tm, bm, lm, rm,
                                         cv2.BORDER_CONSTANT, (0,0,0,0))

    # 切り取った画像を保存
    cv2.imwrite(OUTPUTDIR + '/' + image_name, margined_image)

    print(image_name + " finished")

print("margin adjustment completed!!")



余談

現状の処理ではモンスターの左右端から同じだけの余白を入れているが、
人の目で見て「中央に揃っている」という印象になるためには、画像の重心が中央にくる必要がありそうだ。
画像の2値化はできているので、x軸に沿って画素を数えることで重心を算出すれば、さらに良くなりそう。

参考リンク


0 件のコメント:

コメントを投稿