アプリ開発

【matplotlib,Plotly,Pillow】pythonでランキング表の画像を作成する方法

ランキング表(Pillowで作成)

作ったwebサービス(Synchro Song)で、ランキング表の画像を生成しています。

ランキング自体は、該当ページのHTMLのテーブルで表示すれば良いのですが、TwitterカードなどでOGP画像(SNSでシェアされた時に表示する画像)として表示したいため、画像として出力する必要がありました。

Pythonで「表の画像」を作成するためにベストマッチするようなライブラリがないか、かなり探しましたが、該当するようなものはなく、matplotlib、Plotlyというグラフを作成するライブラリを試した末、最後はPillowという画像処理ライブラリに落ち着きました。

本記事では、matplotlib、Plotly、Pillowで試した試行錯誤の過程をソースコードと共に記載します。

matplotlibで表を作成する。

matplotlibは、pythonのためのグラフ描画ライブラリで分析した結果を表示する用途などに広く使われています。私自身も会社員時代に、分析した結果を表示するためによく使用していました。

matplotlibではグラフだけではなく、表(テーブル)を出力できる機能もあります。

実業務では表の作成はPower Pointを使用していたので、使う機会もありませんでしたが・・

ただし、基本的に表はグラフとセットで表示する仕組みになっているので、表だけを表示するためには以下の対応が必要です。

  1. 軸を非表示にする。( plt.axis(‘off’) )
  2. 表を中央に配置する。(loc=’center’)

以下、表を表示するためのサンプルです。

事前にmatplotlibがインストールされている必要があります。インストールしていない場合は、pipでインストールしてください。

pip install matplotlib

 

サンプルコードは以下の通りです。

import matplotlib.pyplot as plt

#サンプルデータ
data = {"A":3.2,"B":2.1,"C":1.2,"D":0.5,"E":0.2,"F":0.1}

#グラフは表示しないように軸を非表示にする。
plt.axis('off')
#表を描写
table = plt.table(
        cellText=tuple(data.items()),
        colLabels=['key','valye'],
        loc='center')     #  デフォルトはグラフの下に表示なので、centerを指定して中央に設定

table.set_fontsize(25)
table.scale(3, 3)
plt.show()

 

結果は以下のようになります。

matplotlibのテーブル出力サンプルmatplotlibのテーブル出力サンプル

この方法を用いて、ランキング表を作成しました。

私が作成しているアプリでは、前段階としてランキング表に表示したい結果は以下のようなタプルのリスト形式で返されます。

synchro_list = [
 ('あの太陽が、この世界を照らし続けるように。', 0.31807124614715576),
 ('君といたいのに', 0.2645844519138336),
 ('memory', 0.25997722148895264),
 ('Bell', 0.2561669945716858),
 ('同じ窓から見てた空', 0.25017043948173523),
 ('miss you', 0.2484913468360901),
 ('I LOVE YOU', 0.24450553953647614),
 ('NOTE', 0.24158775806427002),
 ('願いの詩', 0.24035443365573883),
 ('晴々', 0.23917677998542786)]

 

この結果を用いてランキングを作成するコードは以下のようになります。

#結果をランキング表のために変換(番号付与、類似度をフォーマット)
table_list = [(i, s, '{:.1f}'.format(p*100)+"%" ) 
              for i,(s,p) in enumerate(synchro_list,1)]

#タイトル設定
plt.title("あなたとシンクロするコブクロの歌 ベスト10",
          fontsize=25,
          pad=100)

#表を作成
table = plt.table(cellText=table_list,
                      colLabels=['No.','曲名', 'シンクロ率'],
                      colWidths=[0.05,0.4,0.1] ,
                      cellLoc='center',
                      loc='center')
table.set_fontsize(25)
table.scale(3, 3)


# グラフとセットで表示されるので、表は削除
plt.axis('off')

#保存
plt.savefig("synchrosong-ranking-plt1.png", 
            bbox_inches="tight")
#表示(保存の前に実行すると保存できないので注意)
plt.show()

結果は以下のようになります。

ランキング表(matplotlibで作成)ランキング表(matplotlibで作成)

超シンプルですが、これでも良いかと思ってました。

ただ仮に長い曲のタイトルがランキング内にあった場合はその曲名に合わせて、全体の文字が小さくなってしまいます。

ランキング表(matplotlibで作成)ランキング表(matplotlibで作成) 曲名が長い場合

該当する箇所の文字だけが小さくなるならばまだ良いのですが、全体が小さくなってしまうのは、格好悪いです。

以下の行を追加すれば、文字が勝手に小さくなる事は避けられますが・・

table.auto_set_font_size(False)

今度は、はみ出します・・

ランキング表(matplotlibで作成) 曲名が長い場合2ランキング表(matplotlibで作成) 曲名が長い場合(はみ出す)

はみ出さずに、枠内で切ってくれれば良かったのですが、流石にはみ出すのはまずいです。

曲名をある一定の文字数で、切り取る方法もありますが、一旦ペンディングで他の方法を探すことにしました。

Plotlyで表を作成する。

Plotlyはmatplotlibよりも一歩進んだ視覚化ライブラリで、綺麗なグラフを表示できる他、ブラウザ上でインタラクティブに編集したり動かしたできるグラフを作成する事ができます。

インタラクティブな機能は今回の要件には不要なのですが、こちらにも表を作成する機能があるため、試してみることにしました。

plotlyを使用するためには、plotly,psutil,orcaをインストールする必要があります。

plotly,psutilはpipでインストールできます。

pip install plotly psutil

 

orcaは、私はローカルのmacでは、anacondaでインストールしました。

conda install -c plotly plotly-orca

 

真面目に(?)インストールするには、イメージファイルをダウンロードして展開します。本番環境(Cloud Run)ではDockerfileに以下を記載しインストールしました。

mkdir -p /opt/orca && \
        cd /opt/orca && \
        wget https://github.com/plotly/orca/releases/download/v1.2.1/orca-1.2.1-x86_64.AppImage && \
        chmod +x orca-1.2.1-x86_64.AppImage && \
        ./orca-1.2.1-x86_64.AppImage --appimage-extract && \
        rm orca-1.2.1-x86_64.AppImage && \
        printf '#!/bin/bash \nxvfb-run --auto-servernum --server-args "-screen 0 640x480x24" /opt/orca/squashfs-root/app/orca "$@"' > /usr/bin/orca && \
        chmod +x /usr/bin/orca && \

 

コードは以下のようになります。(なおフリーフォントである「Noto Sans CJK JP」を使用しています。)

#番号
no =[ '①','②','③','④','⑤', '⑥','⑦','⑧','⑨','⑩']

#plotly用に変換
songs = [s for (s, p) in synchro_list]
points = ['{:.1f}'.format(p*100)+"%" for(s, p) in synchro_list]


headerColor = 'white'
rowOddColor = 'rgb(235, 235, 235)'
rowEvenColor = 'white'
lineColor= 'rgb(225, 225, 225)'
lineColor2= 'white'

fig = go.Figure(data=[go.Table(
    columnwidth=[1, 10, 2],
    header=dict(values=['No.', '曲名', 'シンクロ率'],
                line_color='white',
                fill_color=headerColor,
                align='center',
                font_size=26,
                height=45),
    cells=dict(values=[no, songs, points],
               line_color=lineColor,
               fill_color=[[rowOddColor, rowEvenColor]*5],
               align=['center', 'left', 'right'],
               font_size=26,
               height=45)
    )],
    layout={'margin': {'l': 10, 'r': 10, 't': 90, 'b': 0}},
)
fig.update_layout(
    title={
        'text': "あなたとシンクロするコブクロの歌 ベスト10",
        'y': 0.95,
        'x': 0.5},
    font={
        'family': 'Noto Sans CJK JP',
        'size': 18},
    height=630,
    width=1200
)
fig.show()
fig.write_image("synchrosong-ranking-plotty1.png")

結果は以下の通りです。

ランキング表(plotlyで作成) ランキング表(plotlyで作成)

まあまあ、いい感じかと・・

では、曲名が極端に長いケースではどうなるでしょう?

ランキング表(plotlyで作成) ランキング表(plotlyで作成) 曲名が長い場合

枠内に収まらない場合は切れますが、matplotlibがはみ出してしまうのに比べるとはるかに良いでしょう。デザインも色指定など簡単に出来るので、matplolibより、かなりマシです。

一度、これで行こう(Plotlyを採用)と思ったのですが、実際にOGP画像としてシェアした場合に文字が小さすぎて見えにくい事が判明しました。

考えた結果、表を左右2段にする事にしました。コードは以下の通りです。(どんどん汚く長くなって行きますが・・)

import plotly.graph_objects as go

#データを変換
songs = [s  for (s,p) in synchro_list]
points = ['{:.1f}'.format(p*100)+"%"  for (s,p) in synchro_list]

#1位から5位までと6位から10位までで列を分ける。
no1 =[ '①','②','③','④','⑤']
no2 =[ '⑥','⑦','⑧','⑨','⑩']
songs1 = songs[0:5] 
songs2 = songs[5:10]
points1 = points[0:5]
points2 = points[5:10]

headerColor = 'white'
rowOddColor = 'rgb(235, 235, 235)'
rowEvenColor = 'white'
lineColor= 'rgb(225, 225, 225)'
lineColor2= 'white'

#
space = ["","","","",""]

fill_color_normal = [rowOddColor,rowEvenColor,rowOddColor,rowEvenColor,rowOddColor]
fill_color_separete =  [rowEvenColor,rowEvenColor,rowEvenColor,rowEvenColor,rowEvenColor]


fig = go.Figure(data=[go.Table(
    columnwidth = [1,12,3,0.2,1,12,3],
    header=dict(values=['No.', '曲名', 'シンクロ率','','No.', '曲名', 'シンクロ率'],
           line_color='white',
           fill_color=headerColor,
           align='center',
           font_size=[26,26,20,1,26,26,20],
           height=45),    
    cells=dict(values=[no1,songs1,points1,space,no2,songs2,points2],
            fill_color = [fill_color_normal,fill_color_normal,fill_color_normal,
            fill_color_separete,fill_color_normal,fill_color_normal,fill_color_normal],
            align=['center','left','center','center','center','left','center'],
            font_size=[26,26,26,1,26,26,26],
            height=75)
            )],
        layout={'margin': {'l': 0, 'r': 0, 't': 100, 'b': 0}},
)

fig.update_layout(
    title={
        'text': "あなたとシンクロするコブクロの歌 ベスト10",
        'y':0.95,
        'x':0.5},
        font={
            'family':'Noto Sans CJK JP',
            'size': 26},
     height=630,
     width=1200
    )
fig.show()
fig.write_image("synchrosong-ranking-plotty-2-1.png")

結果は以下の通りです。

ランキング表(plotlyで作成)左右2段ランキング表(plotlyで作成)左右2段

枠に収まる曲名の文字数が少なくなりましたが、まあ、許容範囲でしょう・・

・・と思って今度こそ、これで行こうかと思いましたが、新たなる問題が判明しました。

インターラクティブな機能を内包しているからか、Cloud Runのインスタンスのメモリがデフォルトの256MBでは足りなくなりました。

もちろん増やせば使えるのですが、たいした処理もしていないのに、そんなにメモリを使うのは気持ち悪いし、料金やパフォーマンスに影響するので、悩ましいです。

Pillow

Pillowは画像処理ライブラリで、matplotlibやPlotlyとは根本的に用途が異なりますが、画像に文字を埋め込む事が出来ます。

実は一番最初に候補に上がっていたのですが、座標をチマチマ計算して、文字を埋め込んでいかなければいけないので、流石に面倒臭いと思ってやめました。

ただ、面倒臭い分、文字の位置調整や大きさの調整は自由に出来ます。

以下、サンプルを示します。

Pillowはpipでインストールしてください。

pip install pillow

 

ベースの画像は、PowerPointで作成しました。(王冠みたいなのも、付属のアイコンの色を変えたものです。)

ランキング表テンプレート画像ランキング表テンプレート画像

この画像を読み込んで、ランキングの文字を埋め込んで行きます。

ソースコードは以下になります。文字の位置調整、大きさ調整はかなり試行錯誤しました。(フォントはPlotlyと同様、フリーフォントである「Noto Sans CJK JP」を使用しています。)

from PIL import Image, ImageDraw, ImageFont

#ベース画像を読み込む
im = Image.open('image/synchrosong-ranking-template.png')
draw = ImageDraw.Draw(im)

#画像サイズ
W, H = (1200,630)

#枠幅
W_song = 400

#タイトルのフォントサイズとフォント種類
font_size_title = 40
font_title = ImageFont.truetype('NotoSansCJKjp-Black.otf', 
                                font_size_title)

#曲名のフォントサイズとフォント種類
font_size_ranking = 35
font_ranking = ImageFont.truetype('NotoSansCJKjp-Black.otf', 
                                  font_size_ranking)

#シンクロ率のフォントサイズとフォント種類
font_size_point = 25
font_point = ImageFont.truetype('NotoSansCJKjp-Black.otf',
                                font_size_point)

#但し書きのフォントサイズとフォント種類
font_anno = ImageFont.truetype('NotoSansCJKjp-Black.otf', 
                               20)

#標準のフォントカラー
font_color = (0,0,200)
#シンクロ率用のフォントからー
font_color_point = (0,0,150)

#タイトル
title = "あなたとシンクロするコブクロの歌 ベスト10"

#タイトルフォントの設定
draw.font =font_title
#テキストサイズ取得
w_title, h_title = draw.textsize(title)
#テキストサイズが画像の幅より大きい場合は縮小する。
if w_title > W:
    font_size = int(font_size_title * W / w_title )
    draw.font = ImageFont.truetype('NotoSansCJKjp-Black.otf', 
                                   font_size)
    w_title, h_title = draw.textsize(title)

#タイトル描画
draw.text(( (W-w_title) /2 , 35), title,font_color)

#ランキング
for i, (song,point) in enumerate(synchro_list):
    point_f = "(" + '{:.1f}'.format(point*100)+"%)"
 
    #1位から5位、6位から10位でポジションを変更
    w_pos_s = 0 if i < 5 else 600
    w_pos_p = 0 if i < 5 else 605    
    h_pos  = i  if i < 5 else i -5  
    
    #シンクロ率を描画
    draw.font = font_point
    draw.text((490 + w_pos_p, 170 + 99 * h_pos),
              point_f,font_color_point)

    #曲名を描画
    draw.font =font_ranking
    w_song, h_song = draw.textsize(song)
    #枠をはみ出す場合は縮小する。
    if w_song > W_song:
        font_size = int(font_size_ranking * W_song / w_song )
        draw.font = ImageFont.truetype('NotoSansCJKjp-Black.otf',
                                       font_size)
        w_song_n, h_song_n = draw.textsize(song)
        draw.text(
                   (85 + w_pos_s, 165 + 97 * h_pos + 
                   ( h_song  - h_song_n) * h_song_n/h_song * 1.1), 
                  song,font_color)        
    else:
        draw.text((85 + w_pos_s, 165 + 97 * h_pos), song,font_color)

#但し書きを出力
draw.font = font_anno
draw.text((1020, 110), "※()内はシンクロ度",font_color_point)
    
im.show()
im.save("synchrosong-ranking-pillow-1.png")

 

結果は以下のようになります。

ランキング表(Pillowで作成)ランキング表(Pillowで作成)

曲名が長くない場合は、かなり大きい文字で表示でき、曲名が長くて縮小した場合も割といい感じに表示されています。

すごい長い場合は以下のようになってしまいますが、まあ、こんな長い曲名はかなりレアケースでしょう・・

ランキング表(Pillowで作成)曲名が長い場合ランキング表(Pillowで作成)曲名がすごい長い場合

何よりもビジュアルは、matplotlib,Plotlyより飛び抜けて良いので、結局、Pillowを採用することにしました。

まとめ

以上、かなり試行錯誤・・というより紆余曲折の上、Pillowを採用しました。

文字の位置調整、大きさ調整は全て手動コーディングですが、勝手がわかってしまえば、それほど大変ではないです。

学術レポート等ならば、matplotlibやplotlyでも十分かと思いますが、一般向けアプリで、ビジュアル性を重視するためには、ベース画像に文字を埋め込んでいくPillowが良いと思います。