【Python】OpenCv(またはPillow)を使ってExcelへの画像の貼り付けを自動化

# はじめに
今回はChromeの拡張サービスである「Colaboratory」と「Google Drive」を使用してPythonのコードを記述していきます。
Google Driveから「新規 → その他 → アプリを追加」からインストールしてください。(Chromeで検索してもインストールできます)

今回業務の中でレポート作成時のExcelへの画像の貼付けを自動化したので、簡単なサンプルを作り実装メモとして残そうと思います。
業務の自動化に対応して作成したものになりますので、改善のポイントは多々あると思いますが、必要な処理だけ切り取っていただき少しでも参考になればと思います。

※ 2022/03/30修正
「Pillow」を使ったほうがリサイズ後の画質が安定しましたので、Pilowで書き直し、OpenCvの処理はコメントアウトしました。


# 何をするか
![スクリーンショット 2022-03-29 10.45.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2526592/048ceb22-e5e6-5423-1f7a-a2fd7c0a9596.png)

このような形でカテゴリーごとに横３枚を上限に画像を貼り付けるプログラムを書いていきます。
以下のようなディレクトリ構成になっています。

- img
  - dog
　   　- dog1.jpeg
　   　- dog2.jpeg
　   　- dog3.jpeg
  - cat
　   　- cat1.jpeg
　   　- cat2.jpeg
　   　- cat3.jpeg
　   　- cat4.jpeg
  - panda
　　   - panda1.jpeg
　　   - panda2.jpeg
- resize

# 全体コード
```python
import openpyxl
from openpyxl.styles import Font
import datetime
import pytz
from pathlib import Path
# import cv2
from PIL import Image
from google.colab import drive

# TimeZone
tz = pytz.timezone('Japan')
start = tz.localize(datetime.datetime.now())
print('処理開始')
print(start.strftime('%Y/%m/%d %H:%M:%S'))

drive.mount('/content/drive')

# ファイル名
OUTPUT_FILE_NAME = f'/content/drive/My Drive/sample/image_output/file/animal_image.xlsx'
# 画像ディレクトリ
IMG_DIR = '/content/drive/My Drive/sample/image_output/file/img/'
RESIZE_IMG_DIR = '/content/drive/My Drive/sample/image_output/file/resize/'
# シート名
SHEET_TITLE = 'animal'
# リサイズ後の横幅
RESIZE_WIDTH = 300


# ディレクトリ内の画像ファイル名の一覧を取得
def get_images(dir_name):
  dir_path = Path(f'{IMG_DIR}{dir_name}/')
  # 画像ファイル名の一覧を取得
  img_file_names = []
  for f in dir_path.iterdir():
    img_file_names.append(f.name)
  # 昇順に並び替え
  img_file_names.sort
  return img_file_names

# 画像サイズをリサイズ
def resize_img(dir_name, img_name):
  original_img = Image.open( f'{IMG_DIR}{dir_name}/{img_name}')
  resize_img = original_img.resize((RESIZE_WIDTH, int(RESIZE_WIDTH * original_img.size[1] / original_img.size[0])))
  resize_height = resize_img.size[1]
  # ドライブ上にディレクトリを作成
  dir_path = Path(f'{RESIZE_IMG_DIR}{dir_name}')
  dir_path.mkdir(exist_ok = True)
  resize_img.save(f'{RESIZE_IMG_DIR}{img_name}')

  #↓OpenCv
  # # 画像ファイルパス
  # img_path = f'{IMG_DIR}{dir_name}/{img_name}'
  # # 画像のサイズを取得してリサイズ
  # original_img = cv2.imread(img_path)
  # height, width = original_img.shape[:2]
  # resize_height = round(height * (RESIZE_WIDTH / width))
  # resize_img = cv2.resize(original_img, dsize=(RESIZE_WIDTH, resize_height))
  # # ドライブ上にディレクトリを作成
  # dir_path = Path(f'{RESIZE_IMG_DIR}{dir_name}')
  # dir_path.mkdir(exist_ok = True)
  # # リサイズした画像を保存
  # cv2.imwrite(f'{RESIZE_IMG_DIR}{dir_name}/{img_name}', resize_img)

  return resize_height


# ブック作成
workbook = openpyxl.Workbook()
# 先頭シート取得
worksheet = workbook.worksheets[0]
# シート名を設定
worksheet.title = SHEET_TITLE
# 動物名のフォント
bold = Font(bold=True)

# Pathオブジェクトを作成
path = Path(IMG_DIR)
# ディレクトリの一覧を取得
dir_list = []
for f in path.iterdir():
  dir_list.append(f.name)

# 行を定義
row = 1
# 行の高さの最大値
max_height = 0

# 書き出し処理
for dir_name in dir_list:
  # 動物名を出力
  worksheet.cell(row, 1, dir_name).font = bold
  row += 1
  # ディレクトリ内の画像ファイル名の一覧を取得
  img_file_names = get_images(dir_name)
  # 列を定義
  column = 1
  # ファイルの横並び数をカウント
  count = 0
  # 画像ファイルを出力
  file_name_row = row
  row += 1
  for i, img_name in enumerate(img_file_names):
    count += 1
    if i != 0 and count <= 3:
      # ３枚めまでは横に配置
      column += 2
    elif count > 3:
      # ３枚以上ある場合改行
      column = 1
      row += 3
      file_name_row += 3
      max_height = 0
    # ファイル名を出力
    worksheet.cell(file_name_row, column, img_name)
    # 画像をリサイズして、リサイズ後の高さを取得
    height = resize_img(dir_name, img_name)
    if max_height < height:
      max_height = height  
    # Imageオブジェクトの生成
    img = openpyxl.drawing.image.Image(f'{RESIZE_IMG_DIR}{dir_name}/{img_name}')
    # セルの行列番号から、そのセルの番地を取得 
    cell_address = worksheet.cell(row, column).coordinate
    img.anchor = cell_address
    
    # ワークシートの行の高さを修正
    worksheet.row_dimensions[row].height = max_height * 0.75
    # セルの行列番号から、そのセルの列番号の文字列を取得
    column_char = openpyxl.utils.cell.coordinate_from_string(cell_address)[0]
    # ワークシートの列の幅を修正
    worksheet.column_dimensions[column_char].width = RESIZE_WIDTH * 0.13
    # シートに画像貼り付け
    worksheet.add_image(img)

  # 次の動物の行を指定
  row += 3


# 保存
workbook.save(OUTPUT_FILE_NAME)

end = tz.localize(datetime.datetime.now())
print('処理完了')
print(end.strftime('%Y/%m/%d %H:%M:%S'))
print(end - start)
```

PythonでのExcel抽出の記事は他にも書いてますので、今回は以下で今回のポイントについて簡単に説明していきます。

# ライブラリのインポート
```python
import openpyxl
from pathlib import Path
import cv2
from google.colab import drive
```
まずは必要なライブラリをインストールします。
今回画像処理で必要になってくるのは
`import cv2`
`from pathlib import Path`この２つになります。
`import openpyxl`こちらでexcelの処理を行います。

Google Drive, Colaboratoryを使用する場合、ドライブのマウントが必要なので`google.clab`から`drive`もインポートしておきます。

### ドライブのマウント
```python
drive.mount('/content/drive')
```
Google Drive, Colaboratoryを使用する場合、ドライブのマウントを行います。


# ディレクトリ一覧の取得
```Python
# Pathオブジェクトを作成
path = Path(IMG_DIR)
# ディレクトリの一覧を取得
dir_list = []
for f in path.iterdir():
  dir_list.append(f.name)
```
定数で定義した画像ディレクトリのパスからPathオブジェクトを生成し、
`.iterdir()`
で画像ディレクトリ配下の各ディレクトリ名を取得します。


# 書き出し処理
```python
for dir_name in dir_list:
  ~
  ~
  ~
```
取得したディレクトリ名をループし各ディレクトリごとに画像を処理していきます。

# ディレクトリ内の画像一覧のファイル名を取得
```python
img_file_names = get_images(dir_name)
```
上で定義した関数を呼び出してディレクトリ内の画像ファイル名を取得します。

```python
def get_images(dir_name):
  dir_path = Path(f'{IMG_DIR}{dir_name}/')
  # 画像ファイル名の一覧を取得
  img_file_names = []
  for f in dir_path.iterdir():
    img_file_names.append(f.name)
  # 昇順に並び替え
  img_file_names.sort
  return img_file_names
```
引数で渡されたディレクトリのPathオブジェクトを作成し、
同様に、`.iterdir()`で配下の画像ファイル一覧を取得し、
`.name`でファイル名を取得します。

# 画像のリサイズ処理
```python
for i, img_name in enumerate(img_file_names):
  ~
  ~
  height = resize_img(dir_name, img_name)
```
取得した画像ファイル名リストをループし、関数を呼び出しリサイズします。

```python
def resize_img(dir_name, img_name):
  # 画像ファイルパス
  img_path = f'{IMG_DIR}{dir_name}/{img_name}'
  # 画像のサイズを取得してリサイズ
  original_img = cv2.imread(img_path)
  height, width = original_img.shape[:2]
  resize_height = round(height * (RESIZE_WIDTH / width))
  resize_img = cv2.resize(original_img, dsize=(RESIZE_WIDTH, resize_height))
  # ドライブ上にディレクトリを作成
  dir_path = Path(f'{RESIZE_IMG_DIR}{dir_name}')
  dir_path.mkdir(exist_ok = True)
  # リサイズした画像を保存
  cv2.imwrite(f'{RESIZE_IMG_DIR}{dir_name}/{img_name}', resize_img)
  return resize_height
```
引数のディレクトリ名、ファイル名から`cv2.imread(img_path)`で元画像を読み込みます。
`original_img.shape[:2]`で画像の高さと幅を取得し、
今回は横幅300で統一してリサイズするので
`round(height * (RESIZE_WIDTH / width))`
でアスペクト比が崩れないようにリサイズ後の高さを取得します。

`dir_path = Path(f'{RESIZE_IMG_DIR}{dir_name}')`
`dir_path.mkdir(exist_ok = True)`
リサイズ後の画像を保存するresizeディレクトリに、元画像のディレクトリと同じようにディレクトリを作成します。
`cv2.resize(original_img, dsize=(RESIZE_WIDTH, resize_height))`でリサイズ後の画像を作成し、
`cv2.imwrite(f'{RESIZE_IMG_DIR}{dir_name}/{img_name}', resize_img)`で画像を保存します。

書き出し処理内で必要になるのでリサイズ後の高さをreturnします。

```python
height = resize_img(dir_name, img_name)
    if max_height < height:
      max_height = height 
```
リサイズ後、同じ行の画像の高さの最大値を設定しておきます。

## Pillowを使う場合(追記)
```python
from PIL import Image
~
~
def resize_img(dir_name, img_name):
  original_img = Image.open( f'{IMG_DIR}{dir_name}/{img_name}')
  resize_img = original_img.resize((RESIZE_WIDTH, int(RESIZE_WIDTH * original_img.size[1] / original_img.size[0])))
  resize_height = resize_img.size[1]
  # ドライブ上にディレクトリを作成
  dir_path = Path(f'{RESIZE_IMG_DIR}{dir_name}')
  dir_path.mkdir(exist_ok = True)
  resize_img.save(f'{RESIZE_IMG_DIR}{img_name}')

  return resize_height
```
「Pillow」を使う場合、ライブラリをインストールし、リサイズの関数を上記のように書いてださい。
処理内容はOpenCvとほぼ変わりません。
私はPillowのほうがリサイズ後の画質が良かったのでこちらを推奨します。


# 画像の貼り付け処理
```python
    # Imageオブジェクトの生成
    img = openpyxl.drawing.image.Image(f'{RESIZE_IMG_DIR}{dir_name}/{img_name}')
    # セルの行列番号から、そのセルの番地を取得 
    cell_address = worksheet.cell(row, column).coordinate
    img.anchor = cell_address
    
    # ワークシートの行の高さを修正
    worksheet.row_dimensions[row].height = max_height * 0.75
    # セルの行列番号から、そのセルの列番号の文字列を取得
    column_char = openpyxl.utils.cell.coordinate_from_string(cell_address)[0]
    # ワークシートの列の幅を修正
    worksheet.column_dimensions[column_char].width = RESIZE_WIDTH * 0.13
    # シートに画像貼り付け
    worksheet.add_image(img)
```
`openpyxl.drawing.image.Image(f'{RESIZE_IMG_DIR}{dir_name}/{img_name}')`
リサイズ後の画像でImageオブジェクトを生成します。
`worksheet.cell(row, column).coordinate`で貼り付けるセルの番地を取得し、
`img.anchor =`Imageオブジェクトに設定します。

`worksheet.row_dimensions[row].height = max_height * 0.75`
`worksheet.column_dimensions[column_char].width = RESIZE_WIDTH * 0.13
高さの最大値で行の高さを修正し、列幅を300で修正します。

列の文字列に関して、
`worksheet.cell(row, column).column`で取得できるという記事も多かったのですが、
私は上記コードで列番号(int)しか取得できなかったため、
`openpyxl.utils.cell.coordinate_from_string(cell_address)[0]`
こちらでセルの番地から取得しました。

`worksheet.add_image(img)`
こちらでワークシートに画像を貼り付けます。

あとはworksheet.save(OUTPUT_FILE_NAME)でファイルを保存して完了です。


# 参考
- https://www.cresco.co.jp/blog/entry/11916/
- https://qiita.com/kenfukaya/items/dfa548309c301c7087c4
- https://qiita.com/kibo_kibo/items/5cc78d35163f949b0a77

Pillow
- https://intellectual-curiosity.tokyo/2019/06/28/python%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%92%E3%83%AA%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/