画像中の特定の領域を切り取る方法

Open Skirtでは、ユーザがアップロードした画像データを分析し、どの部分がスカートかを特定して切り抜く処理を行っています。

f:id:openskirt-master:20180304203810p:plain

本記事では、スカート領域の特定を行う処理についてソースコードを交えて具体的に書いていきます。

 

前提

Ubuntu 16.0.4

Python 2.7

OpenCV 3.0

 

まず、問題を定義する

やりたいこと(要件)は下記です。

  1. WEBサービスの利用者は、PC / スマホからアクセスする一般ユーザである。
  2. 入力画像はユーザによってアップロードされる写真データである。
  3. 入力画像ごとに位置・形状が異なるスカート領域を簡単に抽出できる。

 

解決方法を検討する

最初に検討したのは、WEB画面上でユーザ自身にスカート領域を指定してもらう方法でした。

しかし、ブラウザ上でスカート領域をきれいに切り取らせるのはユーザビリティの観点で問題があると判断し、断念しました。

 

次に検討したのは、アップロードされた画像をAIで自動的に領域抽出する方法です。こちらの手法についての詳細は、「セグメンテーション」というキーワードで検索するといろいろ出てきます。
将来的にはこのような先端的な技術を取り入れたいところですが、開発の初期段階では工数が大きすぎると判断し、いったん見送ることにしました。

 

そんなわけで、Open Skirtで採用したのは、OpenCVに実装されているヒストグラムの逆投影法(cv2.calcBackProject)を使う方法です。

ユーザがブラウザ上でスカートの位置をクリックし、スカート領域を半自動的に特定するようにしました。

 

処理内容

アルゴリズムの概要は下記です。

  1. ユーザからスカートの位置(座標)を受け取る。
  2. 領域抽出の前処理を行う。
  3. 受け取った座標を中心として、所定の範囲の色情報を取得する。
  4. cv2.calcBackProjectでスカート領域の抽出を行う。
  5. 抽出結果を返す

 

ヒストグラムの逆投影法とは

そもそも、ヒストグラムの逆投影法とは何かというところから紹介します。

OpenCVチュートリアルにこんな説明がありました。

簡単に言うと,各画素が対象物体に属している確率を表す入力画像と同じサイズで単相の画像を作成します.さらに言うと,出力画像中で対象物体のように見える部分は白く,それ以外の部分は黒くなります.これは直観的な説明です(これ以上単純に説明できません).

OpenCVのチュートリアルより引用

 

全然簡単に思えませんので捕捉しますと、要するに下記の図のような処理をしますよ、ということです。

f:id:openskirt-master:20180305132829p:plain

 

少しはわかりやすくなったでしょうか。

上記ヒストグラムの逆投影法を行うには、入力画像と対象物体を指定する必要があることがわかりますね。

 

対象物体の扱い方について

入力画像はユーザがアップロードしたものをそのまま使えばいいとして、対象物体については要件に応じて調整する必要がありますので、そこらへんをもう少し詳しく書きます。

 

後述するソースコードを見ていただければわかりますが、 ヒストグラムの逆投影法を行うためには、入力画像・対象物体のヒストグラムを利用します。

ヒストグラムとは何ぞ?という方はこちらを参考になさってください。

ヒストグラムとは画像中の画素値の全体的な分布を知るためのグラフやプロットとみなせます.

f:id:openskirt-master:20180305134425j:plain

OpenCVのチュートリアルより引用

 

ヒストグラムの逆投影法では、入力画像中で対象物体のヒストグラムと似た領域を抽出します。

 

もし対象物体の大きさが小さすぎると(極端な話、対象物体の大きさが1ピクセルだけだとすると)、その色しか抽出されなくなってしまいます。実際の写真データを扱う場合、光の当たり方によって同じ色に見えても正確には同じではないですし、スカートの場合はチェック柄などの柄ものを抽出できなくなってしまうのは困ります。

 

したがって、対象物体の大きさは、抽出したい領域に含まれる色をある程度含む程度の大きさにする必要があります。

 

さて、能書きはこれくらいにして、いよいよソースコードを見ながら具体的な処理を見ていきたいと思います。

 

ソースコード

Open Skirtで実装しているソースコードより抜粋。

import cv2

# ヒストグラムの逆投映法
# img : ユーザアップロードの入力画像
# pos : 対象物体の中心座標
# size: 対象物体の大きさ
def doBackProject(img, pos, size = 30):
    # posを中心に、上下左右size分の正方形領域を対象物体とする。
    maskRect = {
        "top": int(pos[1] - size),
        "bottom": int(pos[1] + size),
        "left": int(pos[0] - size),
        "right": int(pos[0] + size)
    }

    # 領域抽出の前処理を行う
    target = cv2.GaussianBlur(img, ksize=(51,51), sigmaX=2)
    target = cv2.cvtColor(target, cv2.COLOR_BGR2HSV)

    # 対象物体の範囲の色情報(HSV)を取得する
    mask = target[
                int(maskRect["top"]):int(maskRect["bottom"]),
                int(maskRect["left"]):int(maskRect["right"]),
                :
            ]

# 対象物体のヒストグラムを計算する。 maskHist = cv2.calcHist([mask], [0,1], None, [180,256], [0, 180, 0, 256]) cv2.normalize(maskHist, maskHist, 0, 255, cv2.NORM_MINMAX) # ヒストグラムの逆投影法で領域抽出を行う dst = cv2.calcBackProject([target], [0,1], maskHist, [0, 180, 0, 256], 1) disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (17, 17)) cv2.filter2D(dst, -1, disc, dst) # 抽出結果を返す ret, thresh = cv2.threshold(dst, 120, 255, 0) thresh = cv2.merge((thresh, thresh, thresh)) res = cv2.bitwise_and(img, thresh)   return res

 

ソースコード内のコメントで表現しきれない部分を補足します。

 

領域抽出の前処理について

ガウシアンブラー(cv2.GaussianBlur)は、ぼかし処理です。

ぼかす理由は、領域抽出の精度が上がるからです。

なぜぼかすと精度が上がるかというと、抽出領域中のノイズや細かい模様が平準化されるため、対象物体のヒストグラムと類似する確率が上がるためです。

 

対象物体のヒストグラムを計算するについて

このサンプルはOpenCVのチュートリアルに記載の実装と同じです。

[0, 1]は、ヒストグラムを計算する系列の定義です。今回はHSVに変換しているので、0番目と1番目の系列はH(色相)とS(彩度)です。

続く[180, 255] や [0, 180, 0, 255]は各系列に対する集計単位と集計対象の定義になります。OpenCVではHの定義域が0 ~ 180なので、0番目の系列に対する定義に180という数字が出てきています。

 

ヒストグラムの逆投影法で領域抽出を行うについて

cv2.calcBackProjectの引数の[0, 1]や[180, 255]部分は対象物体のヒストグラム計算で用いた値と同じ値を使います。

cv2.getStructuringElementは、単に半径17ピクセルの白い円を取得しているだけです。

cv2.filter2Dについては、OpenCVのドキュメントにあまり詳しく書いてないのでぶっちゃけよく分かっていないです。きちんと理解したい方はOpenCVのソース読んでください。

 

抽出結果を返すについて

ヒストグラムの逆投影法で取得した領域を白黒の2値化して、入力画像と重ね合わせることで対象物体の領域だけ切り抜いています。

 

 

まとめ

 

画像から手軽に不定形領域を切り抜く処理を探している方の参考になれば幸いです。

また、記事中の間違いや、もっといい実装方法があるよ、という方がいらっしゃったら気軽にコメントしてください。