パワポで作成した図をPythonで自動で塗りつぶす方法

### TL;DR
パワポなどで作成した図を自動で塗りつぶしたいときがあります（手書きのヒートマップを複数個自動で生成したいときとか）。今回の方法は輪郭が分かれている（互いに外枠を共有していない）図形であればどんな図形でも指定したRGB値で塗りつぶすことができます。

### 環境
Python 3.8.10
opencv-python 4.9.0.80
Pillow 9.5.0

### 入力画像
例として四角形4つからなる画像を使います。

![image_orig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953049/f9502349-a321-c840-3006-80aec8d1be85.png)


### 実装
```cv2```で画像を読み込み、```threshold```でバイナリ画像に変換したのち、```findContours```で輪郭（contours）を抽出します。

```python
import cv2
from PIL import Image

image = cv2.imread('./image_orig.png')

# convert input to binary image to find contours
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, binary_image = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
```

```imread```で読み込んだ画像はデフォルトでBGR形式なので、RGB形式に変換し、```drawContours```で図形を塗りつぶしていきます。```thickness=-1```にすることによって検出した輪郭の内部を塗りつぶしています。詳しくは公式ドキュメントを見てください。


https://docs.opencv.org/3.4/d6/d6e/group__imgproc__draw.html#ga746c0625f1781f1ffc9056259103edbc

重要な点として、検出した輪郭はy軸の値が小さい順（画像の下側）、y軸の値が同じ場合はx軸の値が大きい順（画像の右側）から```contours```に格納されます。今回の4つの四角形の例では、右下 → 左下 → 右上 → 左上の順に色が塗られていきます。

```python
rgba_values = [(100,100,200),
               (100,200,100),
               (200,100,100),
               (0,100,200)]

# convert BGR style to RGB style
image_fill = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# fill inside contours
for i in range(len(contours)):
    image_fill = cv2.drawContours(image=image_fill, 
                                  contours=[contours[i]], 
                                  contourIdx=0, 
                                  color=rgba_values[i], 
                                  thickness=-1)
# save image                                  
fname = f'./image_filled.png'
Image.fromarray(image_fill).save(fname, format='PNG')
```

```PIL.Image```で図形を保存して完了です。

### 保存した図形
![image_background_black.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953049/5a917326-3dcd-4b7a-5ce1-51588c64d21c.png)

あれまあ！完成してないですね。
この方法だと背景がどうしても黒く塗りつぶされてしまうようなので、背景を透過させておきます。

```python
# load image
img = Image.open(fname).convert("RGBA")
data = img.getdata()
newData = []

# detect black (RGB = (0,0,0) ) and set alpha = 0
for item in data:
    if item[0] == 0 and item[1] == 0 and item[2] == 0:
        newData.append((255, 255, 255, 0))
    else:
        newData.append(item)

# save image
img.putdata(newData)
img.save(fname, format='PNG')
```
この方法では黒 (0,0,0) の領域を検出しalphaチャネル (透明度) を0にしています。背景以外に黒い箇所があるとそこも透明になってしまうので注意してください。

### 完成
![image_filled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953049/e23121ae-3a40-6469-fdef-bdbd84d56c86.png)


### 実装まとめ
```python fill_image.py
import cv2
from PIL import Image

image = cv2.imread('./image_orig.png')

# convert input to binary image to find contours
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, binary_image = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

rgba_values = [(100,100,200),
               (100,200,100),
               (200,100,100),
               (0,100,200)]

# convert BGR style to RGB style
image_fill = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# fill inside contours
for i in range(len(contours)):
    image_fill = cv2.drawContours(image=image_fill, 
                                  contours=[contours[i]], 
                                  contourIdx=0, 
                                  color=rgba_values[i], 
                                  thickness=-1)
# save image (black background)                                 
fname = f'./image_filled.png'
Image.fromarray(image_fill).save(fname, format='PNG')

# load the above image again
img = Image.open(fname).convert("RGBA")
data = img.getdata()
newData = []

# detect black (RGB = (0,0,0) ) and set alpha = 0
for item in data:
    if item[0] == 0 and item[1] == 0 and item[2] == 0:
        newData.append((255, 255, 255, 0))
    else:
        newData.append(item)

# save image
img.putdata(newData)
img.save(fname, format='PNG')
```

### おわりに
imreadのデフォルトがBGRなのには、PCの歴史が関係しているみたいですね。面白いですね

https://qiita.com/yoya/items/bfef7404ded22649a2af