画像の一部を消すInpainting という技術とその実装 (python)

Python + OpenCVにより,画像の修復の技術,「Inpainting 」の実装を行います.

目次

Inpainting とは?

Inpaint は日本語では修復するという意味です.画像処理の分野では,「いらないものを消す」ということでしょうか.では,どのようなことができるのでしょうか.まずはこの論文を見てみてください.

– Image Inpainting –, Marcelo Bertalmio and Guillermo Sapiro and Vicent Caselles and Coloma Ballester

Inpainting, the technique of modifying an image in an undetectable
form, is as ancient as art itself. The goals and applications of inpainting
are numerous, from the restoration of damaged paintings
and photographs to the removal/replacement of selected objects

Inpainting の例.上の論文のFigure 6 より引用しました.

なんと,引っかき傷や,画像の上に書かれた文字などがナチュラルに復元されているではありませんか!!これは驚きですね. この記事では,どのようにしてこれが実現されているのか,数学的に説明したあと,python のコードにより,実際にInpainting を行ってみたいと思います.

Inpainting の理論

画像$I^0 (i,j)$を初期画像,つまりinpainting する前の状態とし,これをinpainting time $n$によって更新していくことによってinpainting を実現します.具体的には \begin{align} I^{n+1} (i,j) = I^n (i,j) + \Delta t I_{t}^n (i,j),\ \ \forall (i,j) \in \Omega \end{align}

という式を考えます.この更新の仕方$I_t$をどのように与えてやるかが大事です

更新の様子.

上の論文のFigure.7 を引用しています.

上の論文では,

\begin{align} I_t^{n} (i,j) = \delta L^n (i,j) \cdot N^{n} (i,j) \end{align}

としています.ただし,$L^n$は画像のラプラシアンであり,以下の式で与えられます. \begin{align} L^n (i,j) = I^n_{xx} (i,j) + I_{yy}^n (i,j) \end{align}

そして,$N$はinpainting を行っている領域に対する法線方向成分です.

まずここで,画像のラプラシアンとはどのような値なのか,簡単な画像を用いて確認してみましょう. 今回は以下の画像を用いてみます.

この画像から一部分を削ってみます.削る際には,画像処理のアプリケーションであるImageJ を用いると便利です.

import cv2
import numpy as np
##from matplotlib import pyplot as plt
im = cv2.imread('01.png',0)
cv2.imshow('Orignial',im)
cv2.waitKey(0) ## plot image

## Calculate Laplacian
lap = cv2.Laplacian(im,cv2.CV_32F)
cv2.imshow('Laplacian',lap)
cv2.waitKey(0)


im_b = cv2.imread('01_b.png',0)
cv2.imshow('Inpaint_b',im_b)
cv2.waitKey(0) ## plot image

## Calculate Laplacian
lap_b = cv2.Laplacian(im_b,cv2.CV_32F)
cv2.imshow('Laplacian_b',lap_b)
cv2.waitKey(0)
code01.py

Inpainting の実装

① まずInpainting したい領域をはじめに設定しておきます.今回は白く塗りつぶしている領域のInpainting を行いたいため,白の部分のみを取り出します.

import cv2
import numpy as np

im_b = cv2.imread('01_b.png',0)
mask = im_b == 255
mask = np.array(mask)
mask = mask.astype(np.uint8)
cv2.imshow('hoge',mask*255)
cv2.waitKey(0)
code02.py

Mask 領域を取得

② それぞれの重要な値の計算を行います.

\begin{align} \delta L^n (i,j) = (L^n(i+1,j) - L^n (i-1,j), L^n(i,j+1) - L^n (i,j-1)) \end{align}

im_lap = cv2.Laplacian(im_b,cv2.CV_32F)
lap_tx = np.zeros((row,col))
lap_ty = np.zeros((row,col))
for i in range(0,row):
  for j in range(0,col):
    if mask[i,j] == 1:
       lap_tx[i,j] = im_lap[i+1,j] - im_lap[i-1,j]
       lap_ty[i,j] = im_lap[i,j+1] - im_lap[i,j-1]
code03.py

\begin{align} \frac{N}{|N|} = \frac{(-I_y (i,j),I_x(i,j))}{\sqrt{I_x(i,j)^2 + I_y(i,j)^2}} \end{align}

## calculate normal

kernelx = np.array([[0, 0, 0],
                   [-1, 0, 1],
                   [0, 0, 0]])
dstx = cv2.filter2D(im_b, cv2.CV_64F, kernelx)

kernely = np.array([[0, -1, 0],
                    [0, 0, 0],
                    [0, 1, 0]])
dsty = cv2.filter2D(im_b, cv2.CV_64F, kernely)
N_mod = np.power(np.power(dstx,2) + np.power(dstx,2),1/2)
code04.py

③ 以下の更新式に従って更新を行います.

\begin{align} I_t (i,j) = \left(\delta L(i,j) \cdot \frac{N}{|N|} \right) |\nabla I (i,j)| \end{align}

Open CV を使った実装

上の方法で出来るはずなのですが,現状うまく動きませんでした.なので,今回はOpenCVのライブラリを使って代用したいと思います.

OpenCV にはInpainting を実行してくれる関数があります. 元画像とマスク画像を与えるだけです. 以下,サンプルコードになります.

import numpy as np
import cv2

img = cv2.imread('01_bb.png')
mask = cv2.imread('mask2.png',0)
mask = 1- mask/255
dst = cv2.inpaint(img,mask,3,cv2.INPAINT_NS)

cv2.imshow('dst',dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
sample02.py

お.なんかプログラムが動いている感じですね.しかし,やっぱりこの欠損を埋めるのは画像一枚からは難しいのでしょうね.

上手くいった例

以下のような雲画像と,mask 画像を考えてみると,上手くいきました.

空の画像

これを元画像にします.

マスク画像

これをマスク画像にします.

上のコードを実行すると

空の画像

マスク領域の部分の空がなくなっているのがわかりますか・・・?

なるほど!背景は均一にしなければならないのですね.

まとめ

今回,Inpainting という技術を紹介しました.元論文を読んで実装しようとしてみましたが,少し上手くいかず,OpenCVのライブラリを使うことにしました.

DeepLearningで自動的にマスク領域に何かを生成する,という分野も最近流行っていますが,今回紹介したのはもっと古典期なものです.このようなものを実装したあと,DeepLearning に触れると,新しい考え方が生まれるかもしれませんね!