🎨

Tableauを用いて楽に絵をかきたい!

に公開

Tableauでピクセルアート? Pythonと連携した自動生成ダッシュボードの作り方

Tableauは、組織内の様々なデータを集約し、データに基づいた迅速な意思決定を支援する「ビジネスインテリジェンス(BI)」ツールです。普段、皆さんは売上データやWebアクセスログなど、ビジネスのためのダッシュボードを作成していることと思います。整然と並んだ棒グラフ、鋭く変化する折れ線グラフ...。それらももちろんTableauの素晴らしい使い方です。ですが、たまには「Tableauでこんなことまで出来るんだ!」という、ちょっと変わった使い方をしてみませんか?

今回は、Tableauをキャンバスに見立てて、Pythonのライブラリを利用した「ピクセルアート」を作成したいと思います。

具体的には画像から座標ごとの色を取得し、散布図に再現することで1枚の絵のように表現します。

さらに1枚の絵を描くにあたり可能な限り楽をする方法を合わせて考えていきたいと思います。

1. 目的:いつものダッシュボード作成に「遊び心」を加えたい

今回の目的は、業務効率化やデータ分析ではありません。ズバリ、Tableauの表現力と外部連携(Python)の面白さを体感することです。

毎日業務データと向き合っていると、Tableauの使い方も固定化しがちです。
しかし、Tableauは本来、非常に自由度の高い表現が可能なツールです。今回はその一端として、デフォルトの機能を用いて、ピクセルアートを作ります。

2. 構成イメージ:フォルダに入れるだけで絵が描ける!

作成のイメージとしては以下Fig.1のような画像からFig.2のようなピクセルアートを作成します。

モナ・リザ
Fig.1:『モナ・リザ』『モナ・リザ風ピクセルアート』
Fig.2:作成イメージ

さらに画像ファイルのダウンロードから限りなく少ない操作でダッシュボード側の絵を切り替える方法を考えます。全体の操作・データ処理のイメージは以下のようになります。

  1. 特定のフォルダ(例:Input_Images)に、ピクセルアート化したい画像(例:picture1.png)を入れます。
  2. Pythonスクリプト①(フォルダ監視)が、画像が追加されたことを検知します。
  3. Pythonスクリプト②(画像処理)が起動し、その画像を読み込んでピクセルデータ(座標と色情報)をCSVファイル(例:pixel_data.csv)として生成します。
  4. Tableauダッシュボードは、このpixel_data.csvをデータソースとしています。
  5. ユーザーがTableauダッシュボードの**「データを更新」**ボタンを押すと、新しいCSVが読み込まれ、picture1.pngのピクセルアートが表示されます。

Fig.3

3. 必要なもの

この仕組みを実現するために、以下の3つの要素が必要です。
ピクセルアート化する画像ファイル

  • お好きな画像(.jpg, .pngなど)をご用意ください。あまり複雑すぎない、シンプルな画像の方がピクセルアート映えします。

Pythonスクリプト(と実行環境)

①フォルダ監視スクリプト
watch_folder.py
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from pathlib import Path
import sys

# --- インポート設定 ---
SCRIPT_DIR = Path(__file__).parent
sys.path.append(str(SCRIPT_DIR))

try:
    from pixel_converter import create_pixel_art_data, INPUT_DIR
except ImportError:
    print("エラー: pixel_converter.py が見つからないか、インポートに失敗しました。")
    sys.exit(1)
# ---

# --- 設定 ---
WATCH_FOLDER = INPUT_DIR  # 監視するフォルダ
PIXEL_WIDTH = 200        # ピクセルアートの幅
# ---

class ImageHandler(FileSystemEventHandler):
    """
    ファイルシステムイベントを処理するハンドラ。
    主に on_created (ファイル作成時) の動作を定義します。
    """
    
    def on_created(self, event):
        """ファイルが作成されたときに呼び出されます。"""
        
        # フォルダが作成された場合は無視
        if event.is_directory:
            return

        filepath = Path(event.src_path)
        
        # サポートする画像形式かチェック
        if filepath.suffix.lower() in ['.png', '.jpg', '.jpeg', '.bmp', '.gif']:
            
            print(f"\n[自動処理] 新しい画像を発見: {filepath.name}")
            
            # ファイルが完全にコピーされるまで少し待つ (書き込みエラー防止)
            time.sleep(2) 
            
            # ピクセルデータ作成関数を実行
            try:
                print(f"変換処理を開始します (幅: {PIXEL_WIDTH}px)...")
                create_pixel_art_data(filepath, 
                                      resize_width=PIXEL_WIDTH)
            except Exception as e:
                print(f"変換処理中に予期せぬエラーが発生しました: {e}")
            
            print(f"次の画像を待機中...")

# --- 監視の起動 ---
if __name__ == "__main__":
    
    if not WATCH_FOLDER.exists():
        WATCH_FOLDER.mkdir(parents=True)
        print(f"{WATCH_FOLDER} を作成しました。")

    event_handler = ImageHandler()
    observer = Observer()
    observer.schedule(event_handler, str(WATCH_FOLDER), recursive=False)
    
    print("-----------------------------------")
    print(" ピクセルアート自動変換を開始します")
    print("-----------------------------------")
    print(f"監視フォルダ: {WATCH_FOLDER}")
    print(f"設定: 幅={PIXEL_WIDTH}px, 固定256色パレット")
    print("(このフォルダに画像ファイルを追加してください)")
    print("(終了する場合は Ctrl + C を押してください)")
    
    observer.start()
    
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n監視を停止しました。")
        observer.stop()
    
    observer.join()
  • Pythonライブラリの watchdog ライブラリを使うと便利です。
  • 指定したフォルダを常に監視し、ファイルが作成・変更されたイベントをトリガーにして、次の処理スクリプト②を呼び出します。
②画像処理・データ生成スクリプト
pixel_convert.py
import pandas as pd
from PIL import Image
import os
from pathlib import Path 
import sys
import colorsys 

# --- パスの設定 ---
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent 
INPUT_DIR = PROJECT_ROOT / "input_images"
OUTPUT_DIR = PROJECT_ROOT / "output_data"
PROCESSED_DIR = PROJECT_ROOT / "processed_images"

# フォルダが存在しない場合は作成
INPUT_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
PROCESSED_DIR.mkdir(exist_ok=True)
# ---

def create_fixed_256_palette():
    """
    R:3bit (8段階), G:3bit (8段階), B:2bit (4段階) の
    固定256色パレットを持つPillowイメージを生成します。
    このパレットはTableauのPreferences.tpsに設定するものと一致します。
    """
    palette_img = Image.new("P", (1, 1))
    
    palette_data = []
    for r in range(8):
        for g in range(8):
            for b in range(4):
                palette_data.extend([
                    (r * 255) // 7,
                    (g * 255) // 7,
                    (b * 255) // 3
                ])
                
    palette_img.putpalette(palette_data)
    return palette_img

# スクリプト読み込み時に固定パレットを生成
FIXED_PALETTE = create_fixed_256_palette()


def get_detailed_color_name_jp(r, g, b):
    """
    RGB値を受け取り、HSV (色相・彩度・明度) に基づいて
    「濃い赤」「明るい青」のような詳細な日本語の色名を返します。
    """
    try:
        r_norm, g_norm, b_norm = r / 255.0, g / 255.0, b / 255.0
        h, s, v = colorsys.rgb_to_hsv(r_norm, g_norm, b_norm)
        
        # 1. 無彩色の判定
        if v < 0.15: return "黒"
        if s < 0.1:
            if v > 0.9: return "白"
            if v < 0.4: return "濃いグレー"
            if v > 0.7: return "明るいグレー"
            return "グレー"

        # 2. 有彩色の判定
        h_deg = h * 360 
        base_color = ""
        if h_deg < 20 or h_deg >= 340:
            if v < 0.5 and s > 0.3: base_color = "茶色"
            elif v > 0.7 and s < 0.6: base_color = "ピンク"
            else: base_color = "赤"
        elif h_deg < 45:
            if v < 0.5: base_color = "茶色"
            else: base_color = "オレンジ"
        elif h_deg < 70:
            if v < 0.6 and s > 0.5: base_color = "黄土色"
            else: base_color = "黄"
        elif h_deg < 100: base_color = "黄緑"
        elif h_deg < 160:
            if v < 0.4: base_color = "深緑"
            else: base_color = "緑"
        elif h_deg < 190: base_color = "青緑 (シアン)"
        elif h_deg < 250:
            if v < 0.4: base_color = "紺"
            elif v > 0.8 and s < 0.5: base_color = "水色"
            else: base_color = "青"
        elif h_deg < 290: base_color = "紫"
        else:
            if v > 0.6 and s < 0.7: base_color = "ピンク"
            else: base_color = "赤紫 (マゼンタ)"
        
        # 3. 修飾語の決定
        special_colors = ["茶色", "黄土色", "深緑", "紺", "水色", "ピンク"]
        if base_color in special_colors: return base_color
        
        prefix = ""
        if v < 0.4: prefix = "暗い"
        elif v > 0.9 and s > 0.7: prefix = "鮮やかな"
        elif v > 0.8 and s < 0.5: prefix = "明るい" 
        elif s < 0.4: prefix = "くすんだ"
        elif s > 0.8 and v > 0.6: prefix = "濃い"

        return f"{prefix}{base_color}"
    except Exception: 
        return "不明"


def create_pixel_art_data(image_path, resize_width=100):
    """
    画像ファイルを読み込み、指定された幅にリサイズし、
    固定256色パレットに減色した後、ピクセルデータをCSVに出力します。
    """
    try:
        img_path = Path(image_path)
        if not img_path.exists():
            print(f"エラー: {img_path} が見つかりません。")
            return

        # 出力ファイルは 'pixel_data.csv' に固定
        output_csv_name = "pixel_data.csv"
        output_csv_path = OUTPUT_DIR / output_csv_name
        
        with Image.open(img_path) as img:
            
            # 1. アスペクト比を維持してリサイズ
            aspect_ratio = img.height / img.width
            resize_height = int(resize_width * aspect_ratio)
            img_resized = img.resize((resize_width, resize_height), Image.Resampling.LANCZOS)
            
            img_rgb = img_resized.convert('RGB')

            # 2. 固定256色パレットに減色
            print(f"処理中: 固定 8-bit (256色) パレットに減色します...")
            # FIXED_PALETTE を使って減色
            # dither を使い、擬似的に中間色を表現
            img_quantized = img_rgb.quantize(palette=FIXED_PALETTE, dither=Image.Dither.FLOYDSTEINBERG)
            
            # ピクセル取得のためRGBモードに戻す
            img_to_process = img_quantized.convert('RGB')

            pixels = img_to_process.load() 
            width, height = img_to_process.size
            
            data = []
            
            # 3. 全ピクセルをCSVデータに変換
            for y in range(height):
                for x in range(width):
                    r, g, b = pixels[x, y]
                    hex_color = f'#{r:02x}{g:02x}{b:02x}'
                    color_name = get_detailed_color_name_jp(r, g, b)
                    
                    data.append({
                        'x': x,
                        'y': height - 1 - y, # Y軸を反転
                        'Color_code': hex_color,
                        'Color_Name': color_name
                    })
            
            df = pd.DataFrame(data)
            
            # 4. CSVファイルに出力 (BOM付きUTF-8で文字化け防止)
            df.to_csv(output_csv_path, index=True, index_label='Row_ID', encoding='utf-8-sig')

            print(f"処理完了: {output_csv_path.name}{width}x{height} のピクセルデータを出力しました。")
            
    except Exception as e:
        print(f"画像処理中にエラーが発生しました: {e}")
        return

    # 5. 処理済み画像を移動
    try:
        processed_image_path = PROCESSED_DIR / img_path.name
        img_path.rename(processed_image_path)
        print(f"画像を {PROCESSED_DIR.name} フォルダに移動しました。")
    except Exception as e:
        print(f"画像の移動中にエラーが発生しました: {e}")


# --- スクリプト単体でのテスト実行 ---
if __name__ == "__main__":
    
    TEST_IMAGE_NAME = "test_image.png" 
    PIXEL_WIDTH = 120                  
    
    test_image_path = INPUT_DIR / TEST_IMAGE_NAME
    
    print(f"--- 手動テスト実行 (固定 256色) ---")
    if test_image_path.exists():
        create_pixel_art_data(test_image_path, 
                              resize_width=PIXEL_WIDTH)
    else:
        print(f"テストエラー: {test_image_path} が見つかりません。")
    print("--- 手動テスト終了 ---")
  • Pillow (PIL) ライブラリを用いて画像を読み込みます。
  • 画像を指定のサイズにリサイズ(縮小)します。これがピクセルアートの「解像度」になります。
    - リサイズした画像の全ピクセルをスキャンし、各ピクセルの (X座標, Y座標, 色情報(R, G, B または Hexコード)) を取得します。
    - 取得したデータを pandasライブラリ使って、Tableauが読み込めるCSVファイルとして出力します。

Tableauダッシュボード
- 上記で生成したデータファイル(pixel_data.csv)に接続します。
- ワークシートで、X座標を「列」に、Y座標を「行」に配置します。
- 本ブログでは、よりモザイク感を出すために乱数を用いて複数の形状を配置してみました。
- 「色」マークに、データから取得した「色情報(Hexコード)」のディメンションを配置します。

作成したシートはこんな感じです!
Fig.4

4. 完成したシステムの動作フロー

これですべての準備が整いました。実際に動かしてみましょう。
フォルダの構成は以下のようになりました。
Fig.5

  1. まず、Pythonのフォルダ監視スクリプト(①)を実行状態にしておきます。
    (コンソールが起動し、監視が開始されます)
  2. お気に入りの「絵画(picture2.png)」を Input_Image フォルダにドラッグ&ドロップします。
    今回は非常に有名なこちらの絵画の画像を入れてみます。

    Fig.6:『真珠の首飾りの少女』
  3. 監視スクリプト①が「picture2.png が追加された!」と検知します。
  4. すかさず、画像処理スクリプト②が実行されます。
  5. スクリプト②は picture2.png をリサイズし、ピクセルデータ(X, Y, 色)を pixel_data.csv に上書き保存します。
    3~5を以下のように自動で処理します
    Fig.7
  6. Tableauダッシュボードを開き、ツールバーの「データソースの更新」アイコンをクリックします。
  7. Tableauが新しい pixel_data.csv を読み込みます。

データ更新前のダッシュボード


Fig.8:「モナ・リザ」風ピクセルアート

データ更新後のダッシュボード


Fig.9:「真珠の首飾りの少女」風ピクセルアート

  1. ダッシュボードの表示が、以前のアートから、今追加した絵画のピクセルアートに瞬時に切り替わりました!

5. 結び

いかがでしたでしょうか?
TableauをPythonと組み合わせることで、単なるBIツールを超えた「動的なアートキャンバス」として使うことができました。

ここから、Tableau PublicやGoogle Drive、バッチ処理などの技術を組み合わせれば描画までの処理をさらに自動化できるかもしれません。

例えば今回は制作期限と用意した画像枚数の都合で諦めましたが、Tableau Publicの1日1回の自動更新を利用した日めくりカレンダーVizのようなものも作れるかも?と思っています。

もちろん、このBlogで用いた技術をそのまま業務に使うことはないかもしれません。
しかし、「Tableauはここまで表現できる」「Pythonと連携すれば、こんな仕組みも作れる」という発見は、皆さんの普段のダッシュボード作成にも新しいアイデアを与えてくれるはずです。

データの「可視化」だけでなく、データを「描画」する。
そんなTableauの奥深さを、ぜひこのピクセルアート作成で体感してみてください。

株式会社キーウォーカー テックブログ

Discussion