2017/10/31

Python Tips:画像を指定のサイズに切り取りたい

Python を使って画像の一部を切り出して保存する方法をご紹介します。

Python 3 の場合は Python 2 で有名な画像処理ライブラリ PIL のフォークである Pillow を使う形がシンプルでかんたんです。

- Pillow: the friendly PIL fork

PIL の Image.open() で元画像を取得して、 crop() メソッドに切り出し場所の座標を渡すと画像を切り出せます。あとは save() で別ファイルとして保存すれば OK です。

from PIL import Image

infile = '01.jpg'
outfile = 'out-01.jpg'
area = (left, top, right, bottom)

img = Image.open(infile)
cropped_img = img.crop(area)
cropped_img.save(outfile)

例えば、画像の中心を切り出す場合のサンプルコードは次のようになります。

# coding: utf-8

"""画像の中心部分を指定されたサイズ切り取った画像を生成する
"""

from pathlib import Path
from PIL import Image


crop_info = (
  # 画像 01.jpg は横 200px - 縦 100px で切り出す
  ('01.jpg', 200, 100),
  # 画像 02.jpg は横 300px - 縦 500px で切り出す
  ('02.jpg', 300, 500),
)


def main():
    print('started.')
    crop_images(crop_info, 'out-')
    print('finished.')


def crop_images(crop_info, prefix):
    for infile, width, height in crop_info:
        path = Path(infile)
        outfile = prefix + path.name
        crop_image(infile, outfile, width, height)


def crop_image(infile, outfile, width=None, height=None):
    img = Image.open(infile)

    width_orig, height_orig = img.size
    if not width:
        width = width_orig
    if not height:
        height = height_orig

    center_h = width_orig / 2
    center_v = height_orig / 2
    width_half = width / 2
    height_half = height / 2

    # 中心の切り出し場所の座標を計算する
    left = center_h - width_half
    top = center_v - height_half
    right = center_h + width_half
    bottom = center_v + height_half
    area = (left, top, right, bottom)

    cropped_img = img.crop(area)
    cropped_img.save(outfile)


if __name__ == "__main__":
    main()

このスクリプトを実行すると、 01.jpg が次のような画像だった場合・・・



Photo by Terry zhou CC BY-NC-SA

次のような横 200px 、縦 100px の画像 out-01.jpg が生成されます。



メソッド名なども直感的でわかりやすく便利ですね。

2017/10/24

Python Tips:Python でファイルに権限を追加したい

Python でファイルに権限を追加する方法について見てみます。

イメージとしては、次のコマンドと同等の処理を Python で行うイメージです。

$ chmod u+w target_file


まず、ファイルの権限を指定する方法についてですが、 Python では os.chmod()pathlib.Path.chmod() を使ってファイルの権限を設定することができます。

os.chmod — Python documentation
pathlib.Path.chmod — Python documentation

一方、ファイルの権限の取得は os.stat()pathlib.Path.stat() で行うことができます。

os.stat — Python documentation
pathlib.Path.stat — Python documentation

また、各権限を表すフラグとして次の定数が用意されています。

# read (読み込み):
# stat.S_IRUSR  256
# stat.S_IRGRP   32
# stat.S_IROTH    4

# write (書き込み):
# stat.S_IWUSR  128
# stat.S_IWGRP   16
# stat.S_IWOTH    2

# execute (実行):
# stat.S_IXUSR   64
# stat.S_IXGRP    8
# stat.S_IXOTH    1

stat — Python documentation

ファイルの権限の追加(や削除)はこれらを組み合わせて行うことになります。
たとえば、書き込み権限を付与する関数 add_write_permission() は次のように書くことができます。

# coding:  utf-8

'''Provides functions to add permissions to files.
'''

import stat
from pathlib import Path
from functools import reduce


def add_write_permission(path: Path, target='u'):
    '''Add "write" permission to specified targets.
    '''
    mode_map = {
        'u': stat.S_IWUSR,
        'g': stat.S_IWGRP,
        'o': stat.S_IWOTH,
        'a': stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH,
    }

    mode_additional = combine_permissions(target, mode_map)

    path.chmod(path.stat().st_mode | mode_additional)


def combine_permissions(target, mode_map):
    modes = map(lambda x: mode_map[x], target)
    return reduce(lambda x, y: x | y, modes)

次のように利用します。

add_write_permission(Path('sample-1.txt'), 'ug')
add_write_permission(Path('sample-2.txt'), 'a')

書き込み権限、実行権限についても追加することができます。
GitHub Gist に書き込み権限、実行権限を付与するための関数も含めたサンプルをあげているので、興味のある方は参考にしてみてください。



Functions to add file permissions with Python. · GitHub

2017/10/17

Python Tips:アニメーション GIF から静止画をまとめて抽出したい

Python でアニメーション GIF ( animated GIF )からフレーム画像を抽出する方法をご紹介します。

早速結論ですが、 Python の画像処理用ライブラリ Pillow を使うのが比較的かんたんです。他にも方法は無数にあるかと思いますが、私は Pillow でやるのがスムーズでした。

Pillow は画像処理ライブラリ PIL のフォークで、 PIL が対応していない Python 3.x に対応しているのがポイントです。 Pillow は pip でインストールするときには名称 Pillow でインストールしますが、スクリプトの中で import するときは PIL と同じ PIL という名前を使用します。

$ pip install Pillow

from PIL import Image

Pillow のドキュメントサイトはこちらです。

Pillow — Pillow (PIL Fork) documentation

実際に、 Pillow を使ってアニメーション GIF を分解する方法を見てみましょう。

# coding: utf-8

'''アニメーション GIF のフレーム画像(静止画)を抽出する
'''

from pathlib import Path
from PIL import Image, ImageSequence

# 分割したいアニメーション GIF 画像
IMAGE_PATH = 'target.gif'
# 分割した画像の出力先ディレクトリ
DESTINATION = 'splitted'
# 現在の状況を標準出力に表示するかどうか
DEBUG_MODE = True


def main():
    frames = get_frames(IMAGE_PATH)
    write_frames(frames, IMAGE_PATH, DESTINATION)


def get_frames(path):
    '''パスで指定されたファイルのフレーム一覧を取得する
    '''
    im = Image.open(path)
    return (frame.copy() for frame in ImageSequence.Iterator(im))


def write_frames(frames, name_original, destination):
    '''フレームを別個の画像ファイルとして保存する
    '''
    path = Path(name_original)

    stem = path.stem
    extension = path.suffix

    # 出力先のディレクトリが存在しなければ作成しておく
    dir_dest = Path(destination)
    if not dir_dest.is_dir():
        dir_dest.mkdir(0o700)
        if DEBUG_MODE:
            print('Destionation directory is created: "{}".'.format(destination))

    for i, f in enumerate(frames):
        name = '{}/{}-{}{}'.format(destination, stem, i + 1, extension)
        f.save(name)
        if DEBUG_MODE:
            print('A frame is saved as "{}".'.format(name))


if __name__ == '__main__':
    main()

このスクリプトを実行すると、アニメーション GIF ファイル target.gif の静止画をすべて抽出して splitted というフォルダに書き出してくれます。元の GIF 画像には変更を加えることなく、抽出した画像を、末尾に連番をつけた形でファイルとして切り出します。

ポイントは PIL.ImageSequence.Iterator クラスです。 PIL.Image で開いた画像をこのコンストラクタに渡すと、アニメーション GIF 内の各フレーム(静止画)を返すイテレータオブジェクトを生成してくれます。

便利ですねー。


参考


逆に、静止画を組み合わせてアニメーション GIF を作る方法については、次の Stack Overflow ページでやりとりされています。興味のある方はこちらもよろしければ。

Programmatically generate video or animated GIF in Python? - Stack Overflow

ライブラリ Pillow の使い方についての日本語の解説はこちらのページが丁寧でわかりやすいです。

Python 3.5 対応画像処理ライブラリ Pillow (PIL) の使い方 - Librabuch

2017/10/09

Python Tips:ターミナルのサイズを取得したい

Python でコマンドラインで利用するちょっとしたツールを作る場合には、現在のターミナルのウィンドウサイズを知りたくなることがあります。

たとえば、よくあるのは「出力を画面幅いっぱいになるようにきれいに出したいので、 1 行の文字数を知りたい」といったケースなどでしょうか。

Python でターミナルのサイズを取得したい場合は組みこみの shutil ライブラリの get_terminal_size() を使えば OK です。

使ってみます。

import shutil

terminal_size = shutil.get_terminal_size()

print(type(terminal_size))
# => <class 'os.terminal_size'>

print(terminal_size.columns)
# => 120 など( 1 行の長さ(文字数))

print(terminal_size.lines)
# => 40 など(行数)

print(terminal_size[0])
# => columns と同じ結果

print(terminal_size[1])
# => lines と同じ結果

get_terminal_size() の戻り値は os.terminal_size クラスの named tuple で、アトリビュートでも、インデックスでも値を抽出することができます。

便利です。

どうも、 os の方にも同名の os.get_terminal_size() という関数があって、これもまったく同じ結果を返しますが、公式のドキュメントによると、「 os.get_terminal_size はローレベルの実装であり、通常はハイレベルな shutil.get_terminal_size() を使いましょう」とのことです。このあたりの詳しい理由はわかりませんでした。。

shutil.get_terminal_size() is the high-level function which should normally be used, os.get_terminal_size is the low-level implementation.


参考

How to get Linux console window width in Python - Stack Overflow

2017/10/04

Python Tips: Python で UTF-8 の BOM ありなしを見分けたい

Python で UTF-8 の BOM のありなしを見分ける方法について見てみたいと思います。

UTF-8 には、「バイト・オーダー・マーク」、通称「 BOM 」と呼ばれるものがあります。これはテキストの始まりをプログラムに伝えるためのデータ内の特定のマークのことであり、具体的にはユニコード文字 U+FEFF がそのマークとして使用されています。

UTF-8 にはこの BOM があるものと無いものとが存在していて、前者を「 BOM あり UTF-8 」( UTF-8 with BOM )、後者を「 BOM なし UTF-8 」あるいはただの「 UTF-8 」と呼んだりします。

詳しくは Wikipedia がわかりやすいので興味のある方はご覧になってみてください。

Byte order mark - Wikipedia


この UTF-8 の BOM を Python で扱う方法について見てみましょう。

ファイルを読む


ファイルを読む場合は、 open() 関数の引数 encoding で指定する文字コードを、 BOM なしの UTF-8 では 'utf-8' 、 BOM あり UTF-8 では 'utf-8-sig' と指定します。

BOM あり UTF-8 をあえて 'utf-8' で読み込むと最初の 1 文字が BOM を表す '\ufeff' (不可視文字)になるので、例えば、 BOM のありなしを自動判定してファイルの中身を読み込む関数を作るとしたら次のようになります。

def open_file_with_utf8(filename):
    '''utf-8 のファイルを BOM ありかどうかを自動判定して読み込む
    '''
    is_with_bom = is_utf8_file_with_bom(filename)

    encoding = 'utf-8-sig' if is_with_bom else 'utf-8'

    return open(filename, encoding=encoding).read()


def is_utf8_file_with_bom(filename):
    '''utf-8 ファイルが BOM ありかどうかを判定する
    '''
    line_first = open(filename, encoding='utf-8').readline()
    return (line_first[0] == '\ufeff')


content = open_file_with_utf8('file_with_utf8.txt')


ファイルを書く


逆にファイルを書くときも encoding を指定すれば OK です。 BOM なし UTF-8 として書きたければ 'utf-8' を、 BOM あり UTF-8 にしたければ 'utf-8-sig' を指定します。

with open('file_out_with_utf8_with_bom', 'w') as f:
    f.write('This is a file written with utf-8 with BOM.')

ちなみに、上述のとおり、 BOM あり UTF-8 のファイルを encoding 'utf-8' で読むと、最初の文字が '\ufeff' になります。逆に、 BOM なし UTF-8 のファイルを encoding 'utf-8-sig' で読むと、普通に読めて特に問題はありません。

BOM のありなしが特に問題にならない場合はよいのですが、ファイルの長さなどを厳密に比較したりしたいような場合には 'utf-8-sig' を適宜使うとよいでしょう。


参考


utf 8 - python utf-8-sig BOM in the middle of the file when appending to the end - Stack Overflow
7.2. codecs — Codec registry and base classes — Python 3.6.3 documentation