Feedforce Developer Blog

フィードフォース開発者ブログ

画像上の文字を自動削除する仕組みを作ってみた

こんにちは  データサイエンティストの八百俊哉です。

今回は画像上に存在する文字を自動的に削除し、背景を補完する仕組みを作成しました。ただ、弊社のプロダクトに実装される可能性が極めて低いので、自由研究の結果としてここに残そうと思います。

弊社のサービスはインターネット広告と深く関わりがあり、インターネット広告に関する分析を実施することが多いです。今回はインターネット広告の画像に関する調査を実施したので、その結果を共有します。

背景

インターネット広告では、商品画像上にプロモーションのロゴや行動を促すフレーズが入っている場合、不承認とされ広告表示されなくなる可能性があります。

プロモーションが入った商品画像の例

support.ecbooster.jp

商品数が少ない場合や再度商品の写真を用意できる場合は、商品画像の差し替えが可能ですが、そうではない場合はプロモーションが入っていない画像をいちから用意するのは非常に困難です。

一部画像においては、Googleが公開している画像の自動改善を用いることで、プロモーションを削除することができますが、例には背景が白のもののみになっています。一方で商品画像は何らかの背景(机の上・壁紙)が写真に写り込んでいることが多く、白い背景以外にも対応したプロモーション削除方法が必要です。

そこで今回は背景ありのプロモーションが掲載されている商品画像から自動的にプロモーションを削除する機能が必要ではないかと考え検証を行いました。

全体の流れ

サンプルとして以下の画像を用意しました。(自作なのでクオリティは低いです)

サンプル画像

今回はこのサンプル画像にある送料無料という文字を消すことを目的にしたいと思います。

以下に示すフローで文字を消し、背景を補完します。

全体のフロー

最終的には以下のような文字除去ができました。

実行することで文字除去に成功した例

では実際にどのように文字を除去したのかを紹介します。

画像上のどこに文字があるか判定する・文字の部分だけ黒い画像を用意

まずはじめに画像の上のどこに文字があるかを判定します。今回はOCRを用いて文字を見つけていきます。

OCR(Optical Character Recognition/Reader、オーシーアール、光学的文字認識)とは、手書きや印刷された文字を、イメージスキャナやデジタルカメラによって読みとり、コンピュータが利用できるデジタルの文字コードに変換する技術です。 https://mediadrive.jp/technology/ocr

まずpythonでOCRを使用するために必要なライブリラリをインストールします。(今回はColabを用いた実行を想定しています。)

!pip install --upgrade opencv-contrib-python
!apt install tesseract-ocr libtesseract-dev tesseract-ocr-jpn
!pip install pyocr

以下がOCRを用いて、文字を検知する関数になります。

tools = pyocr.get_available_tools()
tool = tools[0]

def get_box(img, tool, show=True):
    """
    img:PIL.Image.Image
    """

    results = tool.image_to_string(
        img,
        lang='jpn',
        builder=pyocr.builders.LineBoxBuilder(tesseract_layout=6)
    )

    img_np = np.array(img)

    w, h = img_np.shape[0], img_np.shape[1]

    for box in results:
        if box.content == ' ' or box.content == '':
            continue
        if (box.position[0][0] == 0) and (box.position[0][1] == 0) and (box.position[1][0] == w) and (box.position[1][1] == h):
            continue
        cv2.rectangle(img_np, box.position[0], box.position[1], (0, 255, 0), 1)
    if show:
        display(Image.fromarray(img_np))
    return img_np, results

しかし、ただ画像を上記の関数に渡すだけでは文字を認識できる精度が低いという課題がありました。そこで以下のようにすることで文字認識の精度向上を行いました。

  1. 画像を拡大する
  2. 拡大した画像を5×5に分割する
  3. 分割後の画像をそれぞれOCRにかける
  4. 分割前の拡大した画像をOCRにかける
  5. 3,4で文字と判定され部分(OR)を文字がある場所とする

図で示すと以下のようになっています。

文字検知の精度向上のための取り組み

実際には以下のようにして求めます。

def split_images(img, h_split, w_split):
    """
    画像を分割し、分割後の画像と元画像における座標を返す
    img:numpy
    """

    h,w = img.shape[0],img.shape[1]
    images = []

    new_h = int(h / h_split)
    new_w = int(w / w_split)

    start_coordinates = []

    for _h in range(h_split):
        h_start = _h * new_h
        h_end = h_start + new_h

        for _w in range(w_split):
            w_start = _w * new_w
            w_end = w_start + new_w
            images.append(img[h_start:h_end, w_start:w_end, :])

            coordinate = {}
            coordinate['h_start'] = h_start
            coordinate['h_end'] = h_end
            coordinate['w_start'] = w_start
            coordinate['w_end'] = w_end

            start_coordinates.append(coordinate)
    
    return images, start_coordinates

def get_splitImages2originalPositins(images, img_resize, start_coordinates, show=True):
    """
    分割後の画像それぞれのどこに文字があるかどうかを識別する
    また、それらの結果を元の座標系に戻してpositionsとして返す
    images:list
    img_resize:numpy
    start_coordinates:list
    """
    positions = []
    for i, (img, coordinate)  in enumerate(zip(images, start_coordinates)):

        bb,results = get_box(Image.fromarray(img), tool, show=False)
        w,h = img.shape[0],img.shape[1]

        for box in results:
            if box.content == ' ' or box.content == '':
                continue
            if (box.position[0][0] == 0) and (box.position[0][1] == 0) and (box.position[1][0] == w) and (box.position[1][1] == h):
                continue
            
            position = [[p[0],p[1]] for p in box.position]
            position[0][0] += coordinate['w_start']
            position[0][1] += coordinate['h_start']
            position[1][0] += coordinate['w_start']
            position[1][1] += coordinate['h_start']
            cv2.rectangle(img_resize, position[0], position[1], (0, 255, 0), 1)
            positions.append(position)
    if show:
        display(Image.fromarray(img_resize))
    return positions

def get_mask(img, positions):
    mask = np.zeros_like(img)
    for p in positions:
        cv2.rectangle(mask, p[0], p[1], (255, 255, 255), thickness=-1)
    return mask

def results2positions(results):
    positions = []
    for box in results:
        if box.content == ' ' or box.content == '':
            continue
        if (box.position[0][0] == 0) and (box.position[0][1] == 0) and (box.position[1][0] == w) and (box.position[1][1] == h):
            continue
        positions.append([box.position[0], box.position[1]])
    return positions

# 画像の拡大
img_resize = cv2.resize(img_np, (int(img_np.shape[0] * 4), int(img_np.shape[1] * 4)), interpolation=cv2.INTER_CUBIC)

# 画像を分割する
images, start_coordinates = split_images(np.array(img_resize), 5, 5)
# 分割画像のそれぞれをOCRに入れ、どこに文字が有るか判定する
# また、このときに返ってくるpositionsには、分割前の座標系でどこに文字があったのかが示されている
positions = get_splitImages2originalPositins(images, img_resize, start_coordinates, show=False)
# 分割画像から特定された文字の位置がマスクになるようにする
mask_resize = get_mask(img_resize, positions)
# 元の画像サイズに戻す
mask_resize = cv2.resize(mask_resize, (img_np.shape[0],img_np.shape[1]))

# リサイズ画像を分割せずに、どこに文字が有るかを求める
img_np, results = get_box(img1, tool, show=False)
positions = results2positions(results)
# リサイズ画像から特定された文字の位置がマスクになるようにする
original_mask = get_mask(img_np, positions)

# mask_resize(分割画像)とoriginal_mask(リサイズ画像)のORを求める
bitwise_or = cv2.bitwise_or(original_mask[:,:,0], mask_resize[:,:,0])

上記を実行することで、画像上に文字があるとされた部分の画素だけが255のマスク画像がbitwise_orに入っていることになります。

画像上の黒い部分を周りの情報から補完

次に先程作成したマスク画像と元画像を用いることで、文字の部分を周りの状況から補完していきます。

今回はOpneCVのInpaintingを用いて、文字の除去を行っていきます。 labs.eecs.tottori-u.ac.jp

実際に以下のようにして複数のパラメータでInpaintingを実行し、結果を確認できるようにしました。(参考:OpenCVで画像のInpaintingの解説)

def get_inpainting(img, mask):
    img = np.array(img)
    mask = np.array(mask)

    if mask.ndim == 3:
        mask = mask[:,:,0]
    
    dst11 = cv2.inpaint(img, mask , 0, cv2.INPAINT_TELEA)
    dst12 = cv2.inpaint(img, mask , 3, cv2.INPAINT_TELEA)
    dst13 = cv2.inpaint(img, mask , 10, cv2.INPAINT_TELEA)
    dst21 = cv2.inpaint(img, mask , 0, cv2.INPAINT_NS)
    dst22 = cv2.inpaint(img, mask , 3, cv2.INPAINT_NS)
    dst23 = cv2.inpaint(img, mask , 10, cv2.INPAINT_NS)

    print("\nOutput (INPAINT_TELEA, radius=0) : dst11")
    display(Image.fromarray(dst11))
    print("\nOutput (INPAINT_TELEA, radius=3) : dst12")
    display(Image.fromarray(dst11))
    print("\nOutput (INPAINT_TELEA, radius=10) : dst13")
    display(Image.fromarray(dst11))

    print("\nOutput (INPAINT_NS, radius=0) : dst21")
    display(Image.fromarray(dst21))
    print("\nOutput (INPAINT_NS, radius=3) : dst22")
    display(Image.fromarray(dst22))
    print("\nOutput (INPAINT_NS, radius=10) : dst23")
    display(Image.fromarray(dst23))

    return dst11,dst12,dst13,dst21,dst22,dst23

dst11,dst12,dst13,dst21,dst22,dst23 = get_inpainting(img_np, bitwise_or)

こちらを実行すると以下の出力が得られます。

結果の出力例

出力画像を確認するとしっかりと文字が消えており、文字があった部分は背景色と同じ色が補完されていることがわかります。

まとめ

今回は画像上に存在する文字を自動的に削除し、背景を補完する仕組みの紹介を行いました。

はじめに画像上の文字を検知し、その検知結果をもとにマスク画像を作成します。そのマスク画像と元画像をInpaintingに入力することで、画像の補完を行いました。

特に文字を検知する部分で、文字検知の取りこぼしが多く見られたので画像を拡大・分割するなどの工夫を実施したところがポイントです。ただし、現状の文字検知の方法でもかなり取りこぼしが多い状態なので、実践で使用する場合にはもう少し工夫が必要になります。

大学の頃は画像系の卒論を書いたこともあり、とても楽しめながら作成することができました。 最後までお付き合いいただき、ありがとうございます。