ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

Pythonでセグメンテーションの領域を一回り大きく切り抜く

社内勉強会のネタにしようと思ったけど、せっかくなのでブログに書く。 既についているアノーテーションを一回り大きく取って、別の画像に張りつけるやりかたをまとめました。

Instance Segmatationのアノテーションを、一回り大きくくりぬきたいって時よくありますよね。 領域をくりぬいて資料に貼り付けるとか別の使い方をしたいときにギリで囲ってあると領域が見えなかったり、アノテーション雑すぎて対象物の境界情報がなくなってるとか。 ここで一回り大きくの定義を厳密に決めようすると難しいので、なんとなく一回り大きく取れていればOKくらいの感覚で先に進む。

入力データ

雑なアノーテーションで困ってます。

真面目な戦略

囲っている領域の点を全体的に外側に移動させれば良い。

ここで一つ問題がでてくる。

左側のような図形であれば真ん中から外側がなんとでも計算できるけど、右側のように凹凸がでてくると外側はどっちかをアルゴリズムで決める必要がある。

教科書的にやろうとすると多分こうなる。

  • 注目している点の2つの隣の点から線分を2つ作る。
  • 線分のなす角から真ん中の角度を求める。ここから点を動かす方向が2点に絞られる。
  • 両方の方向について、Segmentationで囲んだ領域の内側か外側かを判定する。これは専用のアルゴリズムがある。
  • 領域の外側に向かっている方向に、点を一定量移動させる。

だいぶ面倒。

画像処理のライブラリ使おう

手っ取り早く領域を広くする方法として、太い線で領域を描く方法と、領域に対してぼかす系のフィルターをかける方法がある。 太い線で十分だけど、そのあと別の画像に貼り付ける時はガウシアンフィルターが有効だったので両方紹介する。

左上が元々の領域。この領域を太い線で描画して、元の領域と重ね合わせると左中の領域になる。 線の太さ分だけ領域が大きくなっている。角が気になる場合は、角に丸を描画すると良い。

さらにぼかす系のフィルターとしてガウシアンフィルターをかけたものが左下の画像。ガウシアンフィルターの半径分だけ領域が増えている。これを合成用のアルファとして使う事で、合成を少しましにしている。ただ領域を増やしたいだけなら2値化すればよい。

どちらにしても広くする量をピクセルの単位で指定できるのがメリット。

何もしない場合

画像を読み込んで、背景画像を用意した後、マスク用の画像を用意する。 draw.polygon()で切り抜く領域をマスク用の画像に描画し、そのマスク用画像を使って合成する。

    bg = Image.new('RGB', (width, height), (0, 0, 255))

    # mask 画像を用意
    mask = Image.new("L", (width, height), 0)
    draw = ImageDraw.Draw(mask)
    draw.polygon(segment, fill=255)

    # maskを使って、背景に合成
    composite = Image.composite(im, bg, mask)

結果。アノーテーションの領域がそのまま貼り付けられる。

太い線で描く方法

draw.polygon()に引数widthがあるが、期待通りの動きをしないのでdraw.line()に太さを指定してマスク用画像を描画する。 draw.line()は塗りつぶしをしないので、線を引いた後、draw.polygon()で元々の領域を塗りつぶす。

    # 画像を太い線で広くする
    line_width = 30
    for i in range(len(segment)-1):
        draw.line((segment[i], segment[i+1]), fill=255, width=line_width)
    else:
        draw.line((segment[0], segment[-1]), fill=255, width=line_width)
    # 本来の場所の塗りつぶし
    draw.polygon(segment, fill=255)

結果。ちょっと領域が広くなっている。

ガウシアンフィルター

太い線を描いた後ガウシアンフィルターをかければOK。

    # 画像を太い線で広くする
    line_width = 30
    for i in range(len(segment)-1):
        draw.line((segment[i], segment[i+1]), fill=255, width=line_width)
    else:
        draw.line((segment[0], segment[-1]), fill=255, width=line_width)

    # ガウシアンフィルターをかける。
    mask = mask.filter(ImageFilter.GaussianBlur(radius=10))

結果。合成の境目にぼかしが入った。ぼかしは領域を広くする方向にのみ入る。(領域の内側はぼかしが聞いていない)。

効果が分かりやすいように単色背景にしているが、自然画像を背景にするとちょっと良さが分かると思う。やってみて。

ソースコード

import os
import PIL
from PIL import Image, ImageDraw, ImageFilter

input_file = 'img/sta.jpg'

# segmentationの座標リスト
segment = [(8, 1665), (98, 1515), (248, 1334), (430, 1187), (705, 1112), (1202, 1250), (1345, 1390), (1467, 1559), (1505, 1687), (736, 1965), (14, 2000)]


# 1. そのまま背景に合成する
def proc1():
    im = PIL.Image.open(input_file)

    # 画像のサイズを取得
    width, height = im.size
    # 青の背景を用意
    bg = Image.new('RGB', (width, height), (0, 0, 255))

    # mask 画像を用意
    mask = Image.new("L", (width, height), 0)
    draw = ImageDraw.Draw(mask)
    draw.polygon(segment, fill=255)

    # maskを使って、背景に合成
    composite = Image.composite(im, bg, mask)

    # 保存
    composite.save('img/proc1.jpg')

def proc2():
    im = PIL.Image.open(input_file)

    # 画像のサイズを取得
    width, height = im.size
    # 青の背景を用意
    bg = Image.new('RGB', (width, height), (0, 0, 255))

    # mask 画像を用意
    mask = Image.new("L", (width, height), 0)
    draw = ImageDraw.Draw(mask)

    # 画像を太い線で広くする
    line_width = 30
    for i in range(len(segment)-1):
        draw.line((segment[i], segment[i+1]), fill=255, width=line_width)
    else:
        draw.line((segment[0], segment[-1]), fill=255, width=line_width)
    # 本来の場所の塗りつぶし
    draw.polygon(segment, fill=255)

    # maskを使って、背景に合成
    composite = Image.composite(im, bg, mask)

    # 保存
    composite.save('img/proc2.jpg')

def proc3():
    im = PIL.Image.open(input_file)

    # 画像のサイズを取得
    width, height = im.size
    # 青の背景を用意
    bg = Image.new('RGB', (width, height), (0, 0, 255))

    # mask 画像を用意
    mask = Image.new("L", (width, height), 0)
    draw = ImageDraw.Draw(mask)

    # 画像を太い線で広くする
    line_width = 30
    for i in range(len(segment)-1):
        draw.line((segment[i], segment[i+1]), fill=255, width=line_width)
    else:
        draw.line((segment[0], segment[-1]), fill=255, width=line_width)

    # ガウシアンフィルターをかける。
    mask = mask.filter(ImageFilter.GaussianBlur(radius=10))
    draw = ImageDraw.Draw(mask)

    # 本来の場所の塗りつぶし
    draw.polygon(segment, fill=255)

    # maskを使って、背景に合成
    composite = Image.composite(im, bg, mask)

    # 保存
    composite.save('img/proc3.jpg')


if __name__ == '__main__':
    # 3つのアルゴリズムを比較する
    # 1. そのまま背景に合成する
    # 2. 画像を太い線で広くしてから合成する
    # 3. 2.に加え、ガウシアンフィルターをかけて合成する。

    # 1. そのまま背景に合成する
    proc1()
    # 2. 画像を太い線で広くしてから合成する
    proc2()

    # 3. 2.に加え、ガウシアンフィルターをかけて合成する。
    proc3()