昨年末、著作権フリーの画像を駆使して年賀状を作成しました。しかし、画像の背景が透明でなく白色であったため、少し画像が重なると下の画像を隠してしまうので、画像を配置するのに苦労しました。Photoshopなどの画像処理ソフトを使えば、背景色を透明にできるのでしょうが、もっと簡単に透明にすることができたらいいのにと思ったので、Pythonで特定の色を透明にするプログラムを作ってみました。
準備
今回のプログラムではOpenCVを使用しますので、以下のコマンドでインストールします。
|
1 |
pip install opencv-python |
使い方
プログラムを実行するとすぐに画像を選択するダイアログが開きます。

画像を開くと、以下のような複数のトラックバーが配置されたダイアログと、画像が2行3列に連結されたウィンドウ(Resultウィンドウ)が開きます。


トラックバーではHSV色空間において、Hue(色相)、Saturation(彩度)、Value(明度)の最小値と最大値を変更できるようになっています。また最終的なエッジの閾値を決める値(Threshold1, Threshold2)もあります。
これらのトラックバーをいろいろ調節すると、Resultウィンドウの真ん中下にあるFilled Contourというラベルのついた場所で、元の画像を残す領域が緑色に塗りつぶされます。逆に言うとここで緑色に塗りつぶされていない領域が透明になります。最終的な画像は一番右の下側にある、Finalというラベルがついた場所に表示されます。
トラックバーを調整して、透過させる領域を決定したら、トラックバーダイアログ上でキーボードの「s」ボタンを押してください。するとファイルを保存するダイアログが開くので、透過処理をした画像ファイルを保存します。

以下が元の画像(左)と透過した画像(右)です。右側の画像の背景が透明(チェッカボード)になっているのが分かると思います。


ソースコード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
import os import cv2 import numpy as np import sys import tkinter as tk import tkinter.filedialog as fd #====================================================================== # 複数の画像を1つにまとめて表示する関数 #====================================================================== def stackImages(imgArray,scale,lables=[]): rows = len(imgArray) cols = len(imgArray[0]) rowsAvailable = isinstance(imgArray[0], list) width = imgArray[0][0].shape[1] height = imgArray[0][0].shape[0] if rowsAvailable: for x in range(0, rows): for y in range(0, cols): imgArray[x][y] = cv2.resize(imgArray[x][y], (0, 0), None, scale, scale) if len(imgArray[x][y].shape) == 2: imgArray[x][y]= cv2.cvtColor( imgArray[x][y], cv2.COLOR_GRAY2BGR) imgBlank = np.zeros((height, width, 3), np.uint8) horImage = [imgBlank]*rows for x in range(0, rows): horImage[x] = np.hstack(imgArray[x]) stkImg = np.vstack(horImage) else: for x in range(0, rows): imgArray[x] = cv2.resize(imgArray[x], (0, 0), None, scale, scale) if len(imgArray[x].shape) == 2: imgArray[x] = cv2.cvtColor(imgArray[x], cv2.COLOR_GRAY2BGR) stkImg= np.hstack(imgArray) if len(lables) != 0: eachImgWidth= int(stkImg.shape[1] / cols) eachImgHeight = int(stkImg.shape[0] / rows) for r in range(0, rows): for c in range(0, cols): cv2.rectangle(stkImg, (eachImgWidth*c, eachImgHeight*r), (eachImgWidth*c+len(lables[r][c])*13+27, 30+eachImgHeight*r), (255, 255, 255), cv2.FILLED) cv2.putText(stkImg, lables[r][c], (eachImgWidth*c+10, eachImgHeight*r+20), cv2.FONT_HERSHEY_COMPLEX, 0.7, (255, 0, 255), 2) return stkImg #====================================================================== # トラックバー関連の関数 #====================================================================== def nothing(x): pass def valTrackbars(): h1 = cv2.getTrackbarPos("Hue Min", "Trackbars") h2 = cv2.getTrackbarPos("Hue Max", "Trackbars") s1 = cv2.getTrackbarPos("Sat Min", "Trackbars") s2 = cv2.getTrackbarPos("Sat Max", "Trackbars") v1 = cv2.getTrackbarPos("Val Min", "Trackbars") v2 = cv2.getTrackbarPos("Val Max", "Trackbars") th1 = cv2.getTrackbarPos("Threshold1", "Trackbars") th2 = cv2.getTrackbarPos("Threshold2", "Trackbars") src = h1, h2, s1, s2, v1, v2, th1, th2 return src #====================================================================== # 日本語を含むパスで画像ファイルを読み書きする関数 #====================================================================== def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None def imwrite(filename, img, params=None): try: ext = os.path.splitext(filename)[1] result, n = cv2.imencode(ext, img, params) if result: with open(filename, mode='w+b') as f: n.tofile(f) return True else: return False except Exception as e: print(e) return False #====================================================================== # ファイル入力 #====================================================================== rt = tk.Tk() rt.withdraw() pathImage = fd.askopenfilename(title = "ファイル選択", filetypes = [('', '*.*')]) rt.destroy() if pathImage == "": sys.exit() #====================================================================== # ファイル読み込み #====================================================================== #img = cv2.imread(pathImage) # 日本語を含むパスは読み込めない img = imread(pathImage) # 画像をリサイズする heightImg = 640 widthImg = 480 img = cv2.resize(img, (widthImg, heightImg)) imgA = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA) # アルファチャンネルを追加する imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) #====================================================================== # トラックバーの作成 #====================================================================== cv2.namedWindow("Trackbars") cv2.resizeWindow("Trackbars", 600, 350) cv2.createTrackbar("Hue Min", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Hue Max", "Trackbars", 179, 255, nothing); cv2.createTrackbar("Sat Min", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Sat Max", "Trackbars", 255, 255, nothing); cv2.createTrackbar("Val Min", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Val Max", "Trackbars", 250, 255, nothing); cv2.createTrackbar("Threshold1", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Threshold2", "Trackbars", 200, 255, nothing); b_save = False while True: val = valTrackbars() #================================================================== # プリプロセッシング #================================================================== imgMask = cv2.inRange(imgHSV, (val[0], val[2], val[4]), (val[1], val[3], val[5])) # ガウシアンブラーをかけてぼやかす imgBlur = cv2.GaussianBlur(imgMask, (5, 5), 1) # エッジを検出する imgThreshold = cv2.Canny(imgBlur, val[6], val[7]) # 以下の2処理は必須ではないが、画像にノイズがある場合は役立つ kernel = np.ones((5, 5)) imgDial = cv2.dilate(imgThreshold, kernel, iterations=2) imgThreshold = cv2.erode(imgDial, kernel, iterations=2) #================================================================== # すべての輪郭を見つけて描画する #================================================================== imgContours = img.copy() imgFillContour = img.copy() contours, hierarchy = cv2.findContours(imgThreshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(imgContours, contours, -1, (0, 255, 0), 10) # 輪郭内を緑に塗りつぶす cv2.fillPoly(imgFillContour, contours, (0, 255, 0)) # 緑の部分を白(255)にし、それ以外を黒(0)にする imgFillContourMask = np.where(imgFillContour==(0, 255, 0), 255, 0) #二値化する imgFillContourMask = cv2.inRange(imgFillContourMask, (10, 10, 10), (255, 255, 255)) imgFinal = cv2.bitwise_and(img, img, mask=imgFillContourMask) #================================================================== # #================================================================== imageArray = ([img, imgMask, imgThreshold], [imgContours, imgFillContour, imgFinal]) lables = [["Original","Mask", "Threshold"], ["Contours", "Filled Contour", "Final"]] stackedImage = stackImages(imageArray, 0.75, lables) # lablesは省略できる cv2.imshow("Result", stackedImage) key = cv2.waitKey(1) & 0xFF if key == ord('q'): break if key == ord('s'): b_save = True break #====================================================================== # ファイル保存 #====================================================================== if b_save == True: #================================================================== # ファイルの再読み込み(元のサイズで読み込み直す) #================================================================== #img = cv2.imread(pathImage) # 日本語を含むパスは読み込めない img = imread(pathImage) imgA = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA) imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) #================================================================== # ファイル保存ダイアログ #================================================================== rt = tk.Tk() rt.withdraw() pathImage = fd.asksaveasfilename(title = "ファイル保存", initialfile = "output.png", filetypes = [('', '*.png')]) rt.destroy() if pathImage == "": sys.exit() #================================================================== # プリプロセッシング #================================================================== imgMask = cv2.inRange(imgHSV, (val[0], val[2], val[4]), (val[1], val[3], val[5])) imgBlur = cv2.GaussianBlur(imgMask, (5, 5), 1) imgThreshold = cv2.Canny(imgBlur, val[6], val[7]) kernel = np.ones((5, 5)) imgDial = cv2.dilate(imgThreshold, kernel, iterations=2) imgThreshold = cv2.erode(imgDial, kernel, iterations=2) #================================================================== # すべての輪郭を見つけて描画する #================================================================== imgFillContour = img.copy() contours, hierarchy = cv2.findContours(imgThreshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.fillPoly(imgFillContour, contours, (0, 255, 0)) imgFillContourMask = np.where(imgFillContour==(0, 255, 0), 255, 0) imgFillContourMask = cv2.inRange(imgFillContourMask, (10, 10, 10), (255, 255, 255)) imgFinal = cv2.bitwise_and(imgA, imgA, mask=imgFillContourMask) #cv2.imwrite(pathImage, imgFinal) # 日本語を含むパスには書き込めない imwrite(pathImage, imgFinal) |
ソースコードの説明
|
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#====================================================================== # 複数の画像を1つにまとめて表示する関数 #====================================================================== def stackImages(imgArray,scale,lables=[]): rows = len(imgArray) cols = len(imgArray[0]) rowsAvailable = isinstance(imgArray[0], list) width = imgArray[0][0].shape[1] height = imgArray[0][0].shape[0] if rowsAvailable: for x in range(0, rows): for y in range(0, cols): imgArray[x][y] = cv2.resize(imgArray[x][y], (0, 0), None, scale, scale) if len(imgArray[x][y].shape) == 2: imgArray[x][y]= cv2.cvtColor( imgArray[x][y], cv2.COLOR_GRAY2BGR) imgBlank = np.zeros((height, width, 3), np.uint8) horImage = [imgBlank]*rows for x in range(0, rows): horImage[x] = np.hstack(imgArray[x]) stkImg = np.vstack(horImage) else: for x in range(0, rows): imgArray[x] = cv2.resize(imgArray[x], (0, 0), None, scale, scale) if len(imgArray[x].shape) == 2: imgArray[x] = cv2.cvtColor(imgArray[x], cv2.COLOR_GRAY2BGR) stkImg= np.hstack(imgArray) if len(lables) != 0: eachImgWidth= int(stkImg.shape[1] / cols) eachImgHeight = int(stkImg.shape[0] / rows) for r in range(0, rows): for c in range(0, cols): cv2.rectangle(stkImg, (eachImgWidth*c, eachImgHeight*r), (eachImgWidth*c+len(lables[r][c])*13+27, 30+eachImgHeight*r), (255, 255, 255), cv2.FILLED) cv2.putText(stkImg, lables[r][c], (eachImgWidth*c+10, eachImgHeight*r+20), cv2.FONT_HERSHEY_COMPLEX, 0.7, (255, 0, 255), 2) return stkImg |
複数の画像を1つのウィンドウにまとめて表示する関数です。この関数はCVZoneで公開されているソースコードを参考にさせていただいております。
|
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
#====================================================================== # トラックバー関連の関数 #====================================================================== def nothing(x): pass def valTrackbars(): h1 = cv2.getTrackbarPos("Hue Min", "Trackbars") h2 = cv2.getTrackbarPos("Hue Max", "Trackbars") s1 = cv2.getTrackbarPos("Sat Min", "Trackbars") s2 = cv2.getTrackbarPos("Sat Max", "Trackbars") v1 = cv2.getTrackbarPos("Val Min", "Trackbars") v2 = cv2.getTrackbarPos("Val Max", "Trackbars") th1 = cv2.getTrackbarPos("Threshold1", "Trackbars") th2 = cv2.getTrackbarPos("Threshold2", "Trackbars") src = h1, h2, s1, s2, v1, v2, th1, th2 return src |
トラックバーから値を取り出す関数です。
|
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
#====================================================================== # 日本語を含むパスで画像ファイルを読み書きする関数 #====================================================================== def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None def imwrite(filename, img, params=None): try: ext = os.path.splitext(filename)[1] result, n = cv2.imencode(ext, img, params) if result: with open(filename, mode='w+b') as f: n.tofile(f) return True else: return False except Exception as e: print(e) return False |
画像を読み込む関数と書き出す関数です。OpenCVは画像ファイルのパスに日本語が含まれていると、読み込みエラーとなるのでそれを避けるためにnumpy経由でファイルを開いています。ここまでが以下のプログラムで使用する関数の定義です。
|
96 97 98 99 100 101 102 103 104 |
#====================================================================== # ファイル入力 #====================================================================== rt = tk.Tk() rt.withdraw() pathImage = fd.askopenfilename(title = "ファイル選択", filetypes = [('', '*.*')]) rt.destroy() if pathImage == "": sys.exit() |
ここからプログラムの実行内容です。まずファイル入力ダイアログを開き、ユーザーにファイルを選択してもらいます。
|
106 107 108 109 110 111 112 113 114 115 116 |
#====================================================================== # ファイル読み込み #====================================================================== #img = cv2.imread(pathImage) # 日本語を含むパスは読み込めない img = imread(pathImage) # 画像をリサイズする heightImg = 640 widthImg = 480 img = cv2.resize(img, (widthImg, heightImg)) imgA = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA) # アルファチャンネルを追加する imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) |
ファイルを読み込みます。上で述べたように日本語を含むファイルパスに対応するために独自に定義したimread関数を使用しています。また、透過させる場所を調節するにあたり、画像をスタック(連結)させやすいように、画像サイズを一律で640×480に変更しています。
|
118 119 120 121 122 123 124 125 126 127 128 129 130 |
#====================================================================== # トラックバーの作成 #====================================================================== cv2.namedWindow("Trackbars") cv2.resizeWindow("Trackbars", 600, 350) cv2.createTrackbar("Hue Min", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Hue Max", "Trackbars", 179, 255, nothing); cv2.createTrackbar("Sat Min", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Sat Max", "Trackbars", 255, 255, nothing); cv2.createTrackbar("Val Min", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Val Max", "Trackbars", 250, 255, nothing); cv2.createTrackbar("Threshold1", "Trackbars", 0, 255, nothing); cv2.createTrackbar("Threshold2", "Trackbars", 200, 255, nothing); |
トラックバーとそのダイアログを作成しています。
|
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
b_save = False while True: val = valTrackbars() #================================================================== # プリプロセッシング #================================================================== imgMask = cv2.inRange(imgHSV, (val[0], val[2], val[4]), (val[1], val[3], val[5])) # ガウシアンブラーをかけてぼやかす imgBlur = cv2.GaussianBlur(imgMask, (5, 5), 1) # エッジを検出する imgThreshold = cv2.Canny(imgBlur, val[6], val[7]) # 以下の2処理は必須ではないが、画像にノイズがある場合は役立つ kernel = np.ones((5, 5)) imgDial = cv2.dilate(imgThreshold, kernel, iterations=2) imgThreshold = cv2.erode(imgDial, kernel, iterations=2) #================================================================== # すべての輪郭を見つけて描画する #================================================================== imgContours = img.copy() imgFillContour = img.copy() contours, hierarchy = cv2.findContours(imgThreshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(imgContours, contours, -1, (0, 255, 0), 10) # 輪郭内を緑に塗りつぶす cv2.fillPoly(imgFillContour, contours, (0, 255, 0)) # 緑の部分を白(255)にし、それ以外を黒(0)にする imgFillContourMask = np.where(imgFillContour==(0, 255, 0), 255, 0) #二値化する imgFillContourMask = cv2.inRange(imgFillContourMask, (10, 10, 10), (255, 255, 255)) imgFinal = cv2.bitwise_and(img, img, mask=imgFillContourMask) #================================================================== # #================================================================== imageArray = ([img, imgMask, imgThreshold], [imgContours, imgFillContour, imgFinal]) lables = [["Original","Mask", "Threshold"], ["Contours", "Filled Contour", "Final"]] stackedImage = stackImages(imageArray, 0.75, lables) # lablesは省略できる cv2.imshow("Result", stackedImage) key = cv2.waitKey(1) & 0xFF if key == ord('q'): break if key == ord('s'): b_save = True break |
無限ループで現在のトラックバーの値に合わせて、画像処理を行い、stackImages関数で処理内容を表示しています。キーボードで「q」ボタンを押すとプログラム終了、「s」ボタンを押すとファイルを保存する処理を行います。
|
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
#====================================================================== # ファイル保存 #====================================================================== if b_save == True: #================================================================== # ファイルの再読み込み(元のサイズで読み込み直す) #================================================================== #img = cv2.imread(pathImage) # 日本語を含むパスは読み込めない img = imread(pathImage) imgA = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA) imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) #================================================================== # ファイル保存ダイアログ #================================================================== rt = tk.Tk() rt.withdraw() pathImage = fd.asksaveasfilename(title = "ファイル保存", initialfile = "output.png", filetypes = [('', '*.png')]) rt.destroy() if pathImage == "": sys.exit() #================================================================== # プリプロセッシング #================================================================== imgMask = cv2.inRange(imgHSV, (val[0], val[2], val[4]), (val[1], val[3], val[5])) imgBlur = cv2.GaussianBlur(imgMask, (5, 5), 1) imgThreshold = cv2.Canny(imgBlur, val[6], val[7]) kernel = np.ones((5, 5)) imgDial = cv2.dilate(imgThreshold, kernel, iterations=2) imgThreshold = cv2.erode(imgDial, kernel, iterations=2) #================================================================== # すべての輪郭を見つけて描画する #================================================================== imgFillContour = img.copy() contours, hierarchy = cv2.findContours(imgThreshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.fillPoly(imgFillContour, contours, (0, 255, 0)) imgFillContourMask = np.where(imgFillContour==(0, 255, 0), 255, 0) imgFillContourMask = cv2.inRange(imgFillContourMask, (10, 10, 10), (255, 255, 255)) imgFinal = cv2.bitwise_and(imgA, imgA, mask=imgFillContourMask) #cv2.imwrite(pathImage, imgFinal) # 日本語を含むパスには書き込めない imwrite(pathImage, imgFinal) |
「s」ボタンが押された場合、ダイアログを開き保存するファイル名を取得します。またトラックバーによる画像の調節においては、元の画像を640×480にリサイズしていましたが、保存するのは元のサイズの画像なので、再度、画像ファイルを開いて、パラメータ調節時と同様の画像処理をしています。
- 投稿タグ
- プログラミング