ローカルの画像ファイルをpandasで可視化する

# 概要
以下の方法でローカルの画像ファイルを表示する
- ローカルの画像ファイルをBase64形式に変換し、DataFrameをHTMLで表示する
- ローカルの画像ファイルをエクセルに埋め込む

## 環境
jupyter-notebookで行います。

```yml:docker-compose.yml
version: '3.4'
services:
  notebook:
    image: jupyter/scipy-notebook:latest
    command:
    - start-notebook.sh
    - --NotebookApp.token='' # 認証を無効化
    - --NotebookApp.disable_check_xsrf=True # CSRFチェックを無効化
    ports:
    - 8888:8888
    volumes:
    - ./notebook:/home/jovyan/work
```


# やり方

## 画像ファイルをBase64形式に変換する
PILを利用して画像ファイルを読み込み、base64に変換します。

```python
import base64
from PIL import Image
from io import BytesIO

def get_thumbnail(path):
    img = Image.open(path)
    aspect = float(img.height) / img.width
    width = 300
    height = round(width * aspect)
    return img.resize((width, height))

def image_base64(img):
    if isinstance(img, str):
        img = get_thumbnail(img)
    with BytesIO() as buffer:
        img.save(buffer, 'png')
        return base64.b64encode(buffer.getvalue()).decode()

# image_base64("file_path")
```

## HTMLで画像を表示する

```python
import pandas as pd
from IPython.display import HTML
from os import path

def image_formatter(img):
    if img is None:
        return
    # base64形式で表示する
    return f'<img src="data:image/png;base64,{img}">'

def generate_image(file_path):
    return image_base64(file_path) if path.exists(file_path) else None

# 以下からは実際の環境に合わせて変更してください
def add_img(df):
    df['img'] = df.animal.map(lambda name: generate_image(f'/home/jovyan/work/{name}.png'))

df = pd.read_csv('/home/jovyan/work/animal.csv')
add_img(df)
# HTMLに変換した上で表示する。formatterを指定する。
HTML(df.to_html(formatters={'img': image_formatter}, escape=False))
```

![HTML_dataframe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123946/4ab11fed-f165-0eab-32e5-dea1d501c520.png)

base64形式に変換しながら表示することになるので、量が多いと読み込みに時間がかかります。
このままcsvとして保存してもいいですが、ファイルサイズに気をつけてください。

## エクセルで画像を埋め込む

```python
import pandas as pd
from os import path

import openpyxl
from openpyxl.drawing.image import Image


def adjust_column_width(ws):
    for c in ws.columns:
        column_letter = c[0].column_letter
        # 画像に合わせて列幅を調整
        if column_letter == 'C':
            ws.column_dimensions[column_letter].width = 40

def to_image(cell, cell_name, idx):
    try:
        img = Image(str(cell.value))
        aspect = float(img.height) / img.width
        img.width = 300
        img.height = aspect * 300
        ws.add_image(img, cell_name)
        ws.row_dimensions[idx].height = img.height - 30
    except FileNotFoundError:
        pass
    finally:
        # 画像列にパスを埋め込んで変換するので、セルの文字列を削除
        cell.value = ""

def convert_image_column(worksheet):
    ws = worksheet

    row_number = len(list(ws.rows))
    # ヘッダー行は除外
    for idx in range(2, row_number + 1):
        # セルの行は1始まり
        img_cell_name = 'C' + str(idx)
        cell = ws[img_cell_name]
        to_image(cell, img_cell_name, idx)

# 以下からは実際の環境に合わせて変更してください
def add_img(df):
    df['img'] = df.animal.map(lambda name: f'/home/jovyan/work/{name}.png')

# Dataframeにパスを埋め込んでエクセルとして保存
df = pd.read_csv('/home/jovyan/work/animal.csv')
add_img(df)
excel_file_path = '/home/jovyan/work/animal.xlsx'
df.to_excel(excel_file_path)

# openpyxlで読み込んで整形
wb = openpyxl.load_workbook(excel_file_path)
ws = wb.active
adjust_column_width(ws)
convert_image_column(ws)
wb.save(excel_file_path)
wb.close()
```

![excel_dataframe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123946/15769fda-7cf3-ff70-75c4-153105b44feb.png)

# 簡単にまとめ
実際は他にもパラメータが存在していて、条件で絞り込んだ際に画像を確認したりする目的で使いました。
データ量が多くなると重くなったり、ファイルサイズが大きくなるので、先に画像を小さくしておくといいかもしれないです。

# 参考
https://qiita.com/nshinya/items/a46ef0002284d2f77789