werry-chanの日記.料理とエンジニアリング

料理!コーディング!研究!日常!飯!うんち!睡眠!人間の全て!

写真の油彩化(oilPainting Effect)をpythonでする

本日は"写真の油彩化"をします.

というのは,今やってる研究の先行研究でoilPainting Effectが使われていたので,実装して勉強しようと思ったからです.

具体的な処理は下で書いてますが,とりあえず処理前後の画像で印象を掴みましょう.

f:id:werry-chan:20200531090442j:plain
これが元画像です.

これをoilPainting Effectで処理すると以下のように変化します.

f:id:werry-chan:20200531090532j:plain
OpenCVのxphotoにあるoilPaintingで処理したものになります.


以下でいくつかコードを見せますが,最後に各処理で出力された画像を置いておきます.



さて,まずは"oilPainting effect, python"で実装例を調べた結果,はじめに見つかったのがこちらです.
https://github.com/tartarskunk/OilPainting

gitに落ちていたコードでしたが,処理速度が遅かったです.

500x300程度の画像の場合,処理に10分近くかかりました.

"""
oilPaintingEffect
半径Rの円形フィルターを用いた処理
フィルター適応範囲内における輝度最頻値pixelのcolor積算値を最頻値頻度で割る
このブログ上ではコメントを追加しました.
"""
import cv2
import sys
import numpy as np
import time


start_time = time.time()
image_filename = "imgs/sea_fish.jpg"
color_image = cv2.imread(image_filename, cv2.IMREAD_COLOR)

height = color_image.shape[0]
width = color_image.shape[1]
channel = color_image.shape[2]

gray_image = cv2.cvtColor(color_image, cv2.COLOR_BGR2GRAY)
oil_image = np.zeros((height, width, channel))

R = 8
stroke_mask = []
for y in range(-R, R):
    for x in range(-R, R):
        if y*y + x*x < R*R:
            stroke_mask.append((y, x))

for y in range(height):
    for x in range(width):
        progress = np.round(100*(y*width+x)/(width*height), 2)
        #print("Progress: ", str(progress)+"%", end='\r')
        local_histogram = np.zeros(256)
        local_channel_count = np.zeros((channel, 256))

        #フィルター適応範囲におけるhistogramの測定
        for dy, dx in stroke_mask:
            yy = y+dy
            xx = x+dx
            if yy < 0 or yy >= height or xx <= 0 or xx >= width:
                continue
            intensity = gray_image[yy, xx]
            local_histogram[intensity] += 1
            for c in range(channel):
                local_channel_count[c, intensity] += color_image[yy, xx, c] #color積算

        max_intensity = np.argmax(local_histogram) #輝度最頻値
        max_intensity_count = local_histogram[max_intensity] #最頻値の頻度
        for c in range(channel):
            oil_image[y, x, c] = local_channel_count[c,max_intensity] / max_intensity_count

oil_image = oil_image.astype('int')
cv2.imwrite("imgs/sea_fish_git.jpg", oil_image)
end_time = time.time()
print(end_time - start_time)

少し冗長で読みにくいですね.

numpyを使ってもう少し読みやすくすることができそうです.

通常のopencv-python(opencv-contrib-pythonをなし)を用いて書き換えました.

処理時間は約20秒であり,処理速度は10~100倍になりました.

"""
oilPaintingEffect
矩形フィルターを用いた処理
フィルター適応範囲内における輝度最頻値pixelのcolor積算値を最頻値頻度で割る
"""
import numpy as np
import cv2
import time


start_time = time.time()
src = cv2.imread("imgs/sea_fish.jpg")
height, width, channel = src.shape
dst = np.zeros((height,width,channel), dtype=np.uint8)

gray_img = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

ksize = 8

for i in range(height):
    for j in range(width):
        if i-ksize < 0 or j-ksize < 0 or i+ksize >= height or j+ksize >= width:
            continue
        else:
            applying_area_gray = gray_img[i-ksize:i+ksize, j-ksize:j+ksize]
            applying_area_gray_1d = applying_area_gray.flatten() #np.bincountは1dで利用可能
            gray_histogram = np.bincount(applying_area_gray_1d) #輝度histogram
            gray_modal = np.argmax(gray_histogram) #輝度最頻値
            gray_modal_frequency = gray_histogram[gray_modal] #最頻値の頻度

            applying_area_color = src[i-ksize:i+ksize, j-ksize:j+ksize]
            gray_modal_color = np.sum(applying_area_color[np.where(applying_area_gray == gray_modal)], axis = 0) #輝度最頻値のcolor積算値
            applying_color = np.array(gray_modal_color/gray_modal_frequency, dtype=np.uint8)
            dst[i,j] = applying_color

cv2.imwrite("imgs/sea_fish_temp.jpg",dst)
end_time = time.time()
print(end_time - start_time)

全く同じ処理でなくとも,PILを用いると簡潔に似た処理が可能になります.

処理内容が異なるため,出力画像が若干異なりますが,印象的には同じようなものに思えます.

PILを用いると処理時間は約1秒で,処理速度は100~1000倍程度に高速化しました.

"""
oilPaintingEffect
矩形フィルターを用いた処理
フィルター適応範囲内において最頻値を適用
"""
import numpy as np
from PIL import Image
from PIL import ImageFilter
import time


start_time = time.time()
src = Image.open('imgs/sea_fish.jpg')
width, height = src.size
ksize = 16

dst = src.filter(ImageFilter.ModeFilter(ksize)) #最頻値適用filter
dst.save('imgs/sea_fish_PIL.jpg',quality=100)
end_time = time.time()
print(end_time-start_time)

opencv-contrib-pythonを用いた場合,xphotoが利用できるので大変高速に処理ができます.

pip install opencv-contrib-python

pipでopencv-contrib-pythonをインストールすることで,opencvのxphotoを利用できます.

処理時間は0.5秒で,処理速度は約1000倍になりました.

"""
oilPaintingEffect
矩形フィルターを用いた処理
フィルター適応範囲内における輝度最頻値pixelのcolor積算値を最頻値頻度で割る
"""
import numpy as np
import cv2
from cv2 import xphoto
import time


start_time = time.time()
src = cv2.imread("imgs/sea_fish.jpg")
dst = xphoto.oilPainting(src, 10, 1, cv2.COLOR_BGR2Lab) #oilPainting処理

cv2.imwrite("imgs/sea_fish_cv2oilPainting.jpg",dst)
end_time = time.time()
print(end_time - start_time)


かなり短くなりましたね.

それでは各処理結果を置いておきます.

f:id:werry-chan:20200531090442j:plain
元画像


f:id:werry-chan:20200531090532j:plain
gitに落ちていた処理によるoilPainting Effect


f:id:werry-chan:20200531121506j:plain
僕が少し改善した処理によるoilPainting Effect


f:id:werry-chan:20200531121547j:plain
PILによる擬似的oilPainting Effect


f:id:werry-chan:20200531121618j:plain
cv2.xphotoを利用したoilPainting Effect