最近SNSなどでとある手書き風のアプリがリリースされちょっと話題になっています。
iPhoneを持っていないのでどのようなアプリかはわかっていないのですが、結果の動画や画像を見る限りではOpenCVで再現できそうでした。そこでタイトルにあるように似たようなものを作れないかと思い、自分で色々調べて作ってみたのでその備忘録です。
ちなみに自作したものは以下のような感じになります。
・加工前
・加工後
完全に同じにはなりませんが、割と手書きな感じにはなっているかと思います。
今回の画像加工の手順としては大体「グレースケール化→輪郭線抽出→影部分の抽出→輪郭線と影部分の合成」という順番でやっています。
ではそれぞれについて説明していきます。読むのが面倒な場合は最後のソースコードのところまで飛ばしてください。
1:グレースケール化
輪郭線を抽出する前段階として画像をグレースケール化します。輪郭線を抽出する前段階として一般的なものです。
グレースケール化するには以下の「cv2.cvtColor」関数でできます。
import cv2
image = cv2.imread('hogehoge.jpg')
grayImage = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
2:輪郭線抽出
グレースケール化したものから輪郭線を抽出するのは「cv2.Canny」関数でできます。
outlineImage = 255 - cv2.Canny(grayImage, 110, 70)
cv2.Cannyの第1引数がグレースケール化した画像です。第2引数と第3引数を変化させることで取得する線の調節ができます。
「255」という値から結果を引いているのは、cv2.Cannyの結果として線の部分が白、他が黒になっているためです。255から結果を引くことで色を反転させています。
3:影部分の抽出
影部分の抽出は以下の手順でやっています。
grayImage = cv2.GaussianBlur(grayImage, (31, 31), 30)
ret, shadowImage = cv2.threshold(grayImage, 40, 220, cv2.THRESH_BINARY)
GaussianBlur関数でぼかし、そのあと2値化しています。これで影の部分が取得できます。
4:輪郭線と影部分の合成
2つの画像ができたのであとは合成します。合成には「cv2.bitwise_and」関数を使います。
resultImg = cv2.bitwise_and(outlineImage, shadowImage)
5:ソースコード
基本的には1〜4の手順で行い、適宜コントラストを減らしたりぼかしを加えたりなどして調節しています。
今回作成したソースコードは以下の通りです。
・2018/11/19追記:画像サイズが奇数ピクセルだとエラーになっていたのでソースコードを修正しました。
・cv2Outline.py
import numpy as np
import cv2
import glob
from datetime import datetime
from time import sleep
RESIZE_SIZE = 4
CAMERA_SIZE = 2
CAMERA_FLG = False
IMG_FOLDER_PATH = "./img/*"
SAVE_FOLDER_PATH = "./result/"
def main():
print("--- start ---")
if (CAMERA_FLG):
cap = cv2.VideoCapture(0)
changeCameraImage(cap)
closeWindows(cap)
else:
changeLoadImages(IMG_FOLDER_PATH, SAVE_FOLDER_PATH)
print("--- end ---")
def changeCameraImage(cap):
while True:
ret, frame = cap.read()
resultImg = changeImage(frame)
fx = int(resultImg.shape[1]/CAMERA_SIZE)
fy = int(resultImg.shape[0]/CAMERA_SIZE)
resultImg = cv2.resize(resultImg, (fx, fy))
text = 'Exit is [Q] key'
cv2.putText(resultImg, text, (0,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255,0), 3, cv2.LINE_AA)
cv2.imshow('resultImg', resultImg)
key = cv2.waitKey(1)&0xff
if key == ord('q'):
break
def changeLoadImages(imgFolderPath, saveFolderPath):
images = glob.glob(imgFolderPath)
for fname in images:
frame = cv2.imread(fname)
resultImg = changeImage(frame)
saveImgByTime(saveFolderPath, resultImg)
sleep(1)
def changeImage(colorImg):
gray = cv2.cvtColor(colorImg, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (15, 15), 15)
img_diff = outine(gray)
img_diff = cv2.GaussianBlur(img_diff, (11, 11), 8)
ret, img_diff = cv2.threshold(img_diff, 170, 240, cv2.THRESH_BINARY)
gray = cv2.GaussianBlur(gray, (31, 31), 30)
ret, gray = cv2.threshold(gray, 40, 220, cv2.THRESH_BINARY)
gray = lowContrast(gray)
resultImg = cv2.bitwise_and(img_diff, gray)
return resultImg
def outine(grayImg):
fx = int(grayImg.shape[1]/RESIZE_SIZE)
fy = int(grayImg.shape[0]/RESIZE_SIZE)
grayChangeImg = cv2.resize(grayImg, (fx, fy))
result = 255 - cv2.Canny(grayChangeImg, 110, 70)
result = cv2.resize(result, (grayImg.shape[1], grayImg.shape[0]))
return result
def lowContrast(img):
min_table = 50
max_table = 230
diff_table = max_table - min_table
look_up_table = np.arange(256, dtype = 'uint8' )
for i in range(0, 255):
look_up_table[i] = min_table + i * (diff_table) / 255
result = cv2.LUT(img, look_up_table)
return result
def saveImgByTime(dirPath, img):
date = datetime.now().strftime("%Y%m%d_%H%M%S")
path = dirPath + date + ".png"
cv2.imwrite(path, img)
def closeWindows(cap):
cap.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
少し説明すると「cv2Outline.py」として保存したファイルと同じ場所に「img」、「result」のフォルダをそれぞれ作成します。そして「img」フォルダに加工前の画像を入れて「python3 cv2Outline.py」を実行すると「result」フォルダに結果が保存されます。
また以下の部分の「False」を「True」に書き換えて実行するとWebカメラの表示に切り替わります。
CAMERA_FLG = False
2018/11/19追記:また画像が小さいといい感じに輪郭線の抽出ができていないようだったため、画像サイズに合わせてある程度計算で適切に処理するようなバージョンも作りました。
・alt_cv2Outline.py
import numpy as np
import cv2
import glob
from datetime import datetime
from time import sleep
CAMERA_SIZE = 2
BLUR_VALUE = 25000
PIXEL_VALUE = 300000
CAMERA_FLG = False
IMG_FOLDER_PATH = "./img/*"
SAVE_FOLDER_PATH = "./result/"
def main():
print("--- start ---")
if (CAMERA_FLG):
cap = cv2.VideoCapture(0)
changeCameraImage(cap)
closeWindows(cap)
else:
changeLoadImages(IMG_FOLDER_PATH, SAVE_FOLDER_PATH)
print("--- end ---")
def changeCameraImage(cap):
while True:
ret, frame = cap.read()
resultImg = changeImage(frame)
fx = int(resultImg.shape[1]/CAMERA_SIZE)
fy = int(resultImg.shape[0]/CAMERA_SIZE)
resultImg = cv2.resize(resultImg, (fx, fy))
text = 'Exit is [Q] key'
cv2.putText(resultImg, text, (0,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255,0), 3, cv2.LINE_AA)
cv2.imshow('resultImg', resultImg)
key = cv2.waitKey(1)&0xff
if key == ord('q'):
break
def changeLoadImages(imgFolderPath, saveFolderPath):
images = glob.glob(imgFolderPath)
for fname in images:
frame = cv2.imread(fname)
resultImg = changeImage(frame)
saveImgByTime(saveFolderPath, resultImg)
sleep(1)
def changeImage(colorImg):
gray = cv2.cvtColor(colorImg, cv2.COLOR_BGR2GRAY)
allPixel = colorImg.shape[1] * colorImg.shape[0]
bokashi = calcBlurValue(allPixel)
gray = cv2.GaussianBlur(gray, (bokashi, bokashi), bokashi)
img_diff = outine(gray, allPixel)
bokashiOutline = bokashi
if bokashi > 4:
bokashiOutline = bokashi - 4
img_diff = cv2.GaussianBlur(img_diff, (bokashiOutline, bokashiOutline), bokashiOutline)
ret, img_diff = cv2.threshold(img_diff, 170, 240, cv2.THRESH_BINARY)
gray = cv2.GaussianBlur(gray, (bokashi, bokashi), bokashi)
ret, gray = cv2.threshold(gray, 40, 220, cv2.THRESH_BINARY)
gray = lowContrast(gray)
resultImg = cv2.bitwise_and(img_diff, gray)
return resultImg
def calcBlurValue(allPixel):
result = int(np.sqrt(allPixel/BLUR_VALUE))
if (result%2 == 0):
result = result + 1
return result
def outine(grayImg, allPixel):
z = np.sqrt(PIXEL_VALUE / (grayImg.shape[1] * grayImg.shape[0]))
if (z > 1):
z = 1
fx = int(grayImg.shape[1] * z)
fy = int(grayImg.shape[0] * z)
grayChangeImg = cv2.resize(grayImg, (fx, fy))
result = 255 - cv2.Canny(grayChangeImg, 100, 50)
result = cv2.resize(result, (grayImg.shape[1], grayImg.shape[0]))
return result
def lowContrast(img):
min_table = 50
max_table = 230
diff_table = max_table - min_table
look_up_table = np.arange(256, dtype = 'uint8' )
for i in range(0, 255):
look_up_table[i] = min_table + i * (diff_table) / 255
result = cv2.LUT(img, look_up_table)
return result
def saveImgByTime(dirPath, img):
date = datetime.now().strftime("%Y%m%d_%H%M%S")
path = dirPath + date + ".png"
cv2.imwrite(path, img)
print("saved: " + date + ".png")
def closeWindows(cap):
cap.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
画像の大きさに合わせてそれぞれのソースコードを使い分ければいいかなと思っています。(どうしても画像のサイズや単純なものが写っているかどうかに影響してくるので…)
以上が今回作ってみたものです。OpenCVとpythonがあれば手軽に色々な加工ができて面白いです。
また今回作ったソースコードも一応公開しておきます。
・参考資料