【Python/PIL】縦横比を保ったまま矩形に合わせて画像をリサイズ(resize・thumnail)

画像の縦横比を保ったまま矩形に合わせてリサイズする方法の解説ページアイキャッチ

このページでは、PIL を用いて画像の縦横比(アスペクト比)を保ったまま、矩形に合わせて画像をリサイズ・拡大縮小する方法について解説していきます。

例えば下の図のように画像と矩形が与えられた場合、単純に矩形に合わせて画像をリサイズすると画像の縦横比が変化してしまい、縦方向もしくは横方向に不自然に画像が伸びてしまう可能性があります。見た目が悪くなりますね…。

リサイズで画像の縦横比が変わってしまう例

今回の解説内容に基づいてリサイズを行うようにすれば、下の図のように矩形に合わせて、かつ、画像の縦横比を保ったまま画像をリサイズでき、先程の例のように不自然に画像が伸びてしまうことを防ぐことができます。

縦横比を保ったまま画像をリサイズする例

MEMO

このページの猫の画像はリズム727さんによる写真ACからの写真を使用させていただいています

resize メソッドを用いる方法

まずは PIL の resizeメソッドを用いた方法について解説していきます。

縦横比を保ったまま矩形に合わせてリサイズする関数

まず結論を言うと、引数 path で与えられるファイルパスの画像を、引数 size で与えられるサイズの矩形に合わせて縦横比を保ったままリサイズする関数は下記のように記述することができます(以降では、この引数 path で与えられるファイルパスの画像、すなわちリサイズを行う画像を元画像と呼ばせていただきます)。

resizeを用いる方法
from PIL import Image

def keepAspectResize(path, size):

    # 画像の読み込み
    image = Image.open(path)

    # サイズを幅と高さにアンパック
    width, height = size

    # 矩形の幅と画像の幅の比率を計算
    x_ratio = width / image.width

    # 矩形の高さと画像の高さの比率を計算
    y_ratio = height / image.height

    # 画像の幅と高さ両方に小さい方の比率を掛けてリサイズ後のサイズを計算
    if x_ratio < y_ratio:
        resize_size = (width, round(image.height * x_ratio))
    else:
        resize_size = (round(image.width * y_ratio), height)

    # リサイズ後の画像サイズにリサイズ
    resized_image = image.resize(resize_size)

    return resized_image

以降では、まずは画像の縦横比を保ったまま矩形に合わせてリサイズするやり方の考え方について説明し、その後、上記関数で実行している処理の解説を行なっていきたいと思います。

スポンサーリンク

resize メソッドの動作

まず、resize メソッドは、引数に リサイズ後の画像のサイズ(幅と高さのタプル)を指定して実行することで、そのサイズに合わせて元画像のリサイズを行うメソッドになります。

resizeメソッドの動作を示す図

例えば下記のように元画像のオブジェクトである imageresize メソッドを実行させれば、image の画像を幅 400 ピクセル&高さ 300 ピクセルにリサイズした画像を返却値として得ることができます。

resize
size = (400, 300)
resized_image = image.resize(size)

resize メソッドで縦横比を保ったままリサイズする方法

ただし、resize メソッドでは、単純に引数で指定されたサイズに合わせて画像のリサイズが行われるため、「元画像のサイズ の縦横比」と引数に指定する「リサイズ後の画像のサイズ の縦横比」とが異なると、リサイズ後の画像の縦横比が元々の画像の縦横比から変化してしまうことになります。

逆に言えば、「元画像のサイズ の縦横比」と「リサイズ後の画像のサイズ の縦横比」が同じであれば、リサイズ後の画像の縦横比は元々の画像から変化しません。

したがって、画像の縦横比を保ったままリサイズを行うためには、「元画像のサイズ の縦横比」と引数に指定する「リサイズ後の画像のサイズ の縦横比」が同じであれば良いことになります。

元画像のサイズ の縦横比を持つ リサイズ後の画像のサイズ は、(元画像の幅 * 拡大率, 元画像の高さ * 拡大率) といった感じで、元画像の幅と高さに「同じ値」を掛け合わせることで求めることができます。

縦横比を保つためのリサイズ後画像のサイズの計算

resize メソッドで縦横比を保ったまま矩形に合わせてリサイズする方法

前述の通り、リサイズ後の画像のサイズ(元画像の幅 * 拡大率, 元画像の高さ * 拡大率) のように、元画像の幅と高さに「同じ値」を掛け合わせて求めることで縦横比を保ったままリサイズすることが可能です。

では、矩形に合わせてリサイズを行うためには、この 拡大率 はどうやって求めれば良いでしょうか?

矩形に合わせてリサイズを行う場合、この 拡大率 の候補は下記の2つのいずれかとなります。

  • 元画像の幅 に対する 矩形の幅 の比率(つまり 矩形の幅 / 元画像の幅) 
  • 元画像の高さ に対する 矩形の高さ の比率(つまり 矩形の高さ / 元画像の高さ) 

前者を 拡大率 として リサイズ後の画像のサイズ を求めた場合、リサイズ後の画像のサイズ における幅と高さは下記のようになります。

  • リサイズ後の画像の幅矩形の幅
  • リサイズ後の画像の高さ元画像の高さ * 矩形の幅 / 元画像の幅

つまり、リサイズ後の画像の幅 に関しては、必ず 矩形の幅 と一致することになります。つまり矩形に合わせてリサイズを行うことが可能です。

矩形の幅に合わせてリサイズした場合のリサイズ後画像

では、前述の 拡大率 の候補の後者を 拡大率 として リサイズ後の画像のサイズ を求めた場合はどうでしょう?この場合の リサイズ後の画像のサイズ における幅と高さは下記のようになります。

  • リサイズ後の画像の幅元画像の幅 * 矩形の高さ / 元画像の高さ
  • リサイズ後の画像の高さ矩形の高さ

この場合は、リサイズ後の画像の高さ矩形の高さ と一致することになります。ですので、この場合も矩形に合わせたリサイズを行うことができることになります。

矩形の高さに合わせてリサイズした場合のリサイズ後画像

いずれにしても、矩形に合わせたリサイズを行うことはできるのですが、候補のうちのどちらを 拡大率 を採用するかでリサイズ後の画像のサイズは大きく変わります。

まず、この 拡大率 として、前述の候補の2つのうち小さい方を選択した場合、矩形内に収まるようにリサイズ後の画像のサイズが算出されることになります。

小さい方の候補を拡大率とした場合のリサイズ後画像

逆に大きい方を選択した場合、矩形外にまではみ出すようにリサイズ後の画像のサイズが算出されることになります。

大きい方の候補を拡大率とした場合のリサイズ後画像

なので、矩形に合わせてリサイズする際に、矩形内に収まるようにリサイズしたい場合は候補のうちの小さい方を拡大率として選択し、さらに矩形外にはみ出るようにリサイズしたい場合は候補のうちの大きい方を拡大率として選択すれば良いことになります。

スポンサーリンク

keepAspectResize 関数の説明

上記の考えに基づいてリサイズを行うのが、前述で紹介した keepAspectResize 関数になります。

この keepAspectResize 関数は、矩形内に収まるようにリサイズを行う関数であり、下記の2つのうちの小さい方を拡大率として選択しています。

  • 元画像の幅 に対する 矩形の幅 の比率(つまり 矩形の幅 / 元画像の幅) 
  • 元画像の高さ に対する 矩形の高さ の比率(つまり 矩形の高さ / 元画像の高さ) 

上記の2つの比率を求めているのが下記の箇所になります(image.widthimage.height が元画像の幅と高さであり、widthheight が矩形の幅の高さとなります)。

拡大率の候補の算出
# 矩形の幅と画像の幅の比率を計算
x_ratio = width / image.width

# 矩形の高さと画像の高さの比率を計算
y_ratio = height / image.height

さらに、前者の方が小さい場合は、元画像の幅image.width)に 拡大率x_ratio )を掛けた値が 矩形の幅width)となることを前提に、下記で 元画像の高さ * 拡大率image.height * x_ratio )により リサイズ後の画像の高さ のみの計算を行い、これを リサイズ後の画像のサイズresized_size)としています。

リサイズ後の画像のサイズの算出1
if x_ratio < y_ratio:
    resize_size = (width, round(image.height * x_ratio))

後者の方が小さい場合は、元画像の高さimage.height)に 拡大率y_ratio)を掛けた値が 矩形の高さheight)となることを前提に、下記で 元画像の幅 * 拡大率image.width * y_ratio)により リサイズ後の画像の幅 のみの計算を行い、これを リサイズ後の画像のサイズresized_size)としています。

リサイズ後の画像のサイズの算出2
else:
    resize_size = (round(image.width * y_ratio), height)

最後に、この リサイズ後の画像のサイズresized_size)を引数として resize メソッドを実行することで、元画像 image のリサイズを行なっています。

リサイズの実行
resized_image = image.resize(resize_size)

多少誤差が出る可能性はありますが、おそらく下記の関数でも縦横比を保ったまま矩形に合わせて画像のリサイズを行うことはできると思います。こっちの方がシンプルなので私はこっちをよく使用します。

resizeを用いる方法(シンプル版)
from PIL import Image

def keepAspectResizeSimple(path, size):

    # 画像の読み込み
    image = Image.open(path)

    # サイズを幅と高さにアンパック
    width, height = size

    # 矩形と画像の幅・高さの比率の小さい方を拡大率とする
    ratio = min(width / image.width, height / image.height)

    # 画像の幅と高さに拡大率を掛けてリサイズ後の画像サイズを算出
    resize_size = (round(ratio * image.width), round(ratio * image.height))

    # リサイズ後の画像サイズにリサイズ
    resized_image = image.resize(resize_size)

    return resized_image

thumbnail メソッドを用いる方法

続いて thumbnail メソッドを用いて、画像の縦横比を保ったまま矩形に合わせて画像をリサイズする方法について解説していきます。

このリサイズを実現する上では、おそらく resize メソッドを用いるよりも thumbnail メソッドを用いた方が簡単だと思います。が、注意点もいくつかありますので、これらの点には気をつけてください。

thumbnail メソッドを用いたリサイズ

前述の通り、resize メソッドは引数にリサイズ後の画像のサイズを指定する必要があり、このサイズの縦横比と元画像の縦横比が異なる場合、リサイズ時に画像の縦横比が変化してしまいました。

その一方で、thumbnail メソッドはリサイズ時に画像の縦横比が変化しません。さらに、引数で指定したサイズ(幅と高さのタプル)に合わせてリサイズが行われます。また、この時、この引数で指定したサイズからはみ出ないようにリサイズが行われます。

thumbnailメソッドの動作を示す図

したがって「画像の縦横比を保ったまま矩形に合わせて画像をリサイズする」は、thumbnail メソッドの引数に矩形の幅と高さのタプルを指定するだけで実現することができます。

例えば下記を実行すれば、引数 path で与えられるファイルパスの画像を引数 size で与えられるサイズの矩形に合わせて縦横比を保ったままリサイズすることができ、その結果を thumbnail_image として得ることができます。

thumbnailによるリサイズ
thumbnail_image = Image.open(path)
thumbnail_image.thumbnail(size)

resize メソッドの時のように拡大率の計算などを求める必要がないため、簡単に「画像の縦横比を保ったまま矩形に合わせて画像をリサイズする」を実現することができます。

スポンサーリンク

thumbnail メソッドの注意点

ただし、簡単に実現できる分、注意点もあります。この注意点は下記の3つです。

  • 実行したオブジェクトの画像が変化する
  • 縮小にしか対応していない
  • 矩形外にはみ出すようなリサイズに対応していない

実行したオブジェクトの画像が変化する

thumbnail メソッドでは、このメソッドを実行した画像オブジェクトそのものに対してリサイズが行われます。ですので、thumbnail メソッド実行後もリサイズ前の画像を使用したいのであれば、copy メソッドを利用して画像のコピーを行なった後に、thumbnail メソッドを実行する必要があります。

コピーした後のthumbnailメソッド実行
image = Image.open(path)
thumbnail_image = image.copy()
thumbnail_image.thumbnail(size)

resize メソッドの場合は、実行した画像オブジェクトではなく返却されるオブジェクトに対してリサイズが行われることになるため、resize メソッド実行後にリサイズ前の画像を扱いたい場合であっても、上記のようにわざわざ copy メソッドを実行する必要はありません。

縮小にしか対応していない

また、thumbnail メソッドで対応しているのは縮小のみです。リサイズ時に拡大が行われるようなサイズが引数で指定された場合、thumbnail メソッドでは何も行われません。

より具体的には、画像の幅と高さ両方が、引数で指定するサイズの幅と高さ以上である場合、thumbnail メソッドでは何も行われないので注意してください。

矩形外にはみ出すようなリサイズに対応していない

また、thumbnail メソッドでは、引数で指定されたサイズの矩形の中に必ず収まるようにリサイズが行われます。矩形に合わせてリサイズを行う場合に、矩形外にはみ出るようにリサイズしたい場合でも、引数等のパラメータではこれらを指定することができないので注意してください。

まとめ

このページでは、PIL を用いて画像の縦横比(アスペクト比)を保ったまま、矩形に合わせて画像をリサイズする方法について解説しました!

主な方法として、resize メソッドを利用する方法と thumbnail メソッドを利用する方法を紹介しました。

resize メソッドを用いる場合は、矩形に合わせる&縦横比を保つための計算が必要なので若干複雑ですが、自由度は高いかと思います。

また thumbnail メソッドでは簡単に上記のようなリサイズを実現できる分、拡大には対応してないなどの注意点もあるので気をつけてください。あくまでも thumbnail メソッドはサムネイルを作成する目的で利用するのが正しいのかなぁと思います。

画面のサイズやキャンバスのサイズ、用紙のサイズ等に合わせてリサイズを行いたいような場合、今回紹介した「画像の縦横比を保ったまま矩形に合わせて画像をリサイズする」方法は結構使えると思いますので、ぜひこの機会に考え方だけでも理解しておいてください!

また、今回紹介した resize メソッドと thumbnail メソッドの違いについても頭に入れておくと、今後リサイズをする際にどっちを使った方が良いかをすぐに判断できるようになると思います!