画像を読み込んでExcel上でセルの背景色で描画するプログラム作ってみた

# はじめに

画像を読み込んで Excel 上でセルの背景色で描画するプログラムを作ってみました。

きっかけは、研究室の先輩の Tweet でした。

> VBAで画像読み込んで、エクセルのセルの色でその画像を再現するっていう~~クソ~~プログラムを思いついたから誰か作って

画像そのものをいじるプログラムを組んだ経験あんまりないけど、興味があったので挑戦してみることにしました。

Excel の背景で描画なんてできたらおもしろいですよね:blush:

# 技術選定

VBA はバイト先で触ったことあるけど時代は Python かなと思ったので、Python とそのライブラリ Openpyxl で開発を進めることにしました。

## 使ったもの
- Python
    - Openpyxl
    - NumPy
    - PIL
- VSCode
- Ubuntu-20.04
- Excel

# おおまかに開発

実装していきます。

## ライブラリのインポート

今回使うライブラリはこれ！

```python
import numpy as np
import openpyxl
import math
import sys
from PIL import Image
from openpyxl.styles import PatternFill
```


## 画像ファイルの読み込み

まずは画像ファイルを読み込みましょう。

サンプルとして以下の画像を使います。（友達が描いてくれた私のアイコンです:heart:）

![](https://raw.githubusercontent.com/acannie/ExcelDrawing/main/figure/icon.jpg "icon.jpg")


```python
# ファイルのインポート
input_file = 'icon.jpg'
im = np.array(Image.open(input_file))
```

`im` に画像の情報を読み込みました。

`im.shape` から画像サイズを取得できます。

```python
print(im.shape)

# (482, 482, 3)
```

今回の場合は $482$x$482$ であることがわかりました。
$3$ は RGB の $3$ 色のことです。

```python
# 画像のサイズを取得
IMG_HEIGHT = im.shape[0]
IMG_WIDTH = im.shape[1]
COLOR_VARIETY = im.shape[2]
```

わかりやすいよう変数に保存しておきましょう。

## Excel ファイルの作成

出力用 Excel ファイルを作ります。
Openpyxl の出番です:blush:

```python
# ワークブックを作成
wb = openpyxl.Workbook()
ws = wb.worksheets[0]
```

以降、シート `ws` に書き込みしていきます。


## 書き込み

画像データの色の情報を $1$ ピクセルずつ `ws` のセルの背景色に移植していきます。

```python
for i in range(IMG_HEIGHT):
    for j in range(IMG_WIDTH):
        RGB = [0,0,0]
        for color in range(COLOR_VARIETY):
            RGB[color] = im[i][j][color]

        fill = PatternFill(patternType='solid', fgColor=RGBtoColorCode(RGB))
        ws.cell(row=i+1, column=j+1).fill = fill
```

上記プログラムで使っている関数 `RGBtoColorCode()` は、RGB の値を $16$ 進数カラーコードに変換して返す関数です。
関数 `PatternFill()` の引数 `fgColor` の値は $16$ 進数カラーコードである必要があるためです。
カラーコード詳細はこちら: [16進数カラーコードの意味と変換計算式](https://www.peko-step.com/tool/tfcolor.html)

関数は以下の通り定義しました。

```python
# RGBの数値が1桁の場合冒頭に0をつけて2桁にして返す
def addZero(strHexNum='0xAA'):
    if len(strHexNum) == 3:
        return '0x0' + strHexNum[2]
    elif len(strHexNum) == 4:
        return strHexNum
    else:
        print('error!')
        sys.exit()

# RGBをカラーコードに変換
def RGBtoColorCode(RGB=[0, 0, 0]):
    color_code = ''
    for color in range(COLOR_VARIETY):
        color_code += str(addZero(hex(RGB[color])))
    color_code = color_code.replace('0x', '') # 0xrr0xgg0xbb を rrggbb の形にする
    return color_code
```

## 出力

最後に、Excel ファイルとして出力します。
Opnepyxl で開いた workbook は忘れず close しましょう:star:
(先日 close せずに何度もプログラムを実行したところ、次回 PC 起動時にエクセルが $100$ 枚ぐらい開いて地獄を見ました)

```python
# ファイルのエクスポート
output_file = input_file.replace('.jpg', '.xlsx') # 元画像名と同名の .xlsx ファイルとして出力
wb.save('result/' + output_file)
wb.close()
```

# 改良

以上で一応ちゃんと出力できるようになります。

しかしお察しの通り、出力された Excel を開くと・・・

はいっ
![stretched_output_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433437/9b3da4d2-4f37-d25d-a5bf-c229e64cc570.png)

めっちゃ横に伸びています。

これは、Excel のセルがデフォルトで横長であるためです。

思いつく対処方法は $2$ 通りあります。

- セルの横幅を縮めて正方形にする。
- 元画像を横方向に圧縮して横長のセルで辻褄を合わせる。

どちらもできそうですが、今回のプロジェクトの面白さは「普段使っている Excel 上に絵が描かれていること」だと思ったので、後者を採用します。
書き込み処理が減るので実行時間も少し短くなりますね。

セルの縦横比はデフォルトでおよそ $1:3$ なので、元画像で横方向に並んだ $3$ ピクセルずつの色の平均をとって、セルの背景色に設定していきます。

```python
# 出力改良版
N = 3
for i in range(IMG_HEIGHT):
    for j in range(math.floor(IMG_WIDTH / N)):
        # 横に並んだ N ピクセルの平均 RGB をセルの背景色とする
        ave_RGB = [0, 0, 0]

        for n in range(N):
            for color in range(COLOR_VARIETY):
                ave_RGB[color] += im[i][N * j + n][color]

        for color in range(COLOR_VARIETY):
            ave_RGB[color] = round(ave_RGB[color] / N)

        fill = PatternFill(patternType='solid', fgColor=RGBtoColorCode(ave_RGB))
        ws.cell(row=i+1, column=j+1).fill = fill
```

これで、出力された Excel は:arrow_down:のようになります。

![good_output_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433437/fcd17347-1b4e-80cb-0a36-ed3bb9408e96.png)

# おまけ (プログレスバーの実装)

今回の主旨とズレますが、おまけでプログレスバーを実装しました。
画像処理や Excel の扱いって実行に時間がかかるので、「本当に進んでる？？」って心配になりがち。
そんな悩みを解決してくれます。

最後の for ループを以下のように書き換えます。

```python
# ゲージ
GAUGE_WIDTH = 30
now_pacentage = 0

print('0', end='')
for g in range(GAUGE_WIDTH):
    print(' ', end='')
print('100%')

print(' ', end='')

# 出力
N = 3
for i in range(IMG_HEIGHT):
    for j in range(math.floor(IMG_WIDTH / N)):
        # 横に並んだ N ピクセルの平均 RGB をセルの背景色とする
        ave_RGB = [0, 0, 0]

        for n in range(N):
            for color in range(COLOR_VARIETY):
                ave_RGB[color] += im[i][N * j + n][color]

        for color in range(COLOR_VARIETY):
            ave_RGB[color] = round(ave_RGB[color] / N)

        fill = PatternFill(patternType='solid', fgColor=RGBtoColorCode(ave_RGB))
        ws.cell(row=i+1, column=j+1).fill = fill

    # ゲージを増やす
    if math.ceil(i / IMG_HEIGHT * GAUGE_WIDTH) > now_pacentage:
        print("■", end="", flush=True)
        now_pacentage = math.ceil(i / IMG_HEIGHT * GAUGE_WIDTH)

print('')
```

結果、:arrow_down:のようにゲージが進んでいくようになります。

![progress_bar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433437/08e0737e-60a5-bcfd-fc79-7b4a39f69ec3.png)


# おわりに

プログラムの完成形を GitHub にアップしました。
記事よりも綺麗にまとまっていると思うのでよかったら見てみてください！:arrow_down:

[ExcelDrawing](https://github.com/acannie/ExcelDrawing)

今回みたいな基本的な機能を作ると、こんなことできたらいいなあが広がりますね。

ちなみに今、今回学んだノウハウを応用して機械学習とか組み合わせて[あつまれ どうぶつの森](https://www.nintendo.co.jp/switch/acbaa/index.html) のマイデザイン作成支援ツールを開発しています。

こんな感じにできそうっ

![mickey_midesign.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433437/8f80a812-7652-5d88-1768-ae61c24b2145.png)
![mickey_midesign_acnh.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433437/8ccbe51c-a02d-ab98-6714-eba5698d2a6e.jpeg)

お楽しみに:sparkles: