第0回 Minecraft × PLY点群: PythonでMinecraftに静止画と動画を描画してみた

に公開

Minecraft API(mcpi)を使って、画像や動画をピクセルアートとしてゲーム内に描画してみました。
この記事では、

  1. 静止画をブロックに変換
  2. 動画をフレームごとに描画
  3. 動画の処理を最適化した高速版

MinecraftでPythonの環境設定は以下を参考↓
https://www.youtube.com/watch?v=iK3V8q2EiI8


1. 静止画をMinecraftに描画

まずは指定した画像をMinecraftのブロックで再現するスクリプトを作成しました。
画像を読み込み、各ピクセルの色に最も近い羊毛ブロックの色を選び、プレイヤーの位置を基準に設置してます。

Pillowで画像を読み込み、各ピクセルのRGB値を羊毛ブロックの色に最も近いものに変換して設置します。

# 必要なライブラリをインポート
from PIL import Image
from mcpi.minecraft import Minecraft
from mcpi import block
import math

# プレイヤーの位置を取得するためにMinecraftに接続
mc = Minecraft.create("127.0.0.1")

# 画像ファイルのパスと描画サイズを指定
image_path = "Python.jpg"
image_size = (40, 40)

# 羊毛ブロックのIDと各色のRGB値
WOOL = block.WOOL.id
wool_colors = {
    (249, 255, 255): 0,  # White
    (249, 131, 23): 1,   # Orange
    (199, 78, 201): 2,   # Magenta
    (103, 151, 203): 3,  # Light Blue
    (229, 227, 23): 4,   # Yellow
    (114, 201, 23): 5,   # Lime
    (249, 151, 175): 6,  # Pink
    (67, 67, 67): 7,     # Gray
    (159, 159, 159): 8,  # Light Gray
    (23, 151, 175): 9,   # Cyan
    (127, 67, 183): 10,  # Purple
    (51, 78, 175): 11,   # Blue
    (103, 51, 23): 12,   # Brown
    (78, 103, 23): 13,   # Green
    (151, 23, 23): 14,   # Red
    (23, 23, 23): 15,    # Black
}

def find_nearest_color(rgb):
    min_distance = float('inf')
    best_match = 0
    for color_rgb, data_id in wool_colors.items():
        distance = math.sqrt(
            (rgb[0] - color_rgb[0])**2 +
            (rgb[1] - color_rgb[1])**2 +
            (rgb[2] - color_rgb[2])**2
        )
        if distance < min_distance:
            min_distance = distance
            best_match = data_id
    return best_match

def create_pixel_art(image_path, size):
    image = Image.open(image_path).convert("RGB").resize(size)
    width, height = image.size
    pixels = image.load()

    start_pos = mc.player.getTilePos()
    for x in range(width):
        for z in range(height):
            r, g, b = pixels[x, z]
            data_id = find_nearest_color((r, g, b))
            mc.setBlock(start_pos.x + x, start_pos.y, start_pos.z + z, WOOL, data_id)

create_pixel_art(image_path, image_size)

出力結果

今回は 40×40ピクセル に縮小したPythonロゴを使って描画してみました。
このサイズなら、1〜2秒程度で描画が完了しました。


2. 動画をMinecraftに描画

OpenCVで動画ファイルを読み込み、1フレームごとに静止画と同じ処理を適用してMinecraft内に描画します。
これにより、フレームが切り替わることでアニメーションのように動画が再現されます。

処理の流れは次のとおりです。

  1. OpenCVで動画をフレーム単位で取得
  2. 各フレームをPillowで画像オブジェクトに変換
  3. ピクセルごとに羊毛ブロックの近似色を計算
  4. プレイヤー位置からの相対座標にブロックを設置
  5. 指定したFPSに合わせて次のフレームを描画

今回はテストとして、短いGIF動画を変換して描画してみました。

from PIL import Image
from mcpi.minecraft import Minecraft
from mcpi import block
import math
import cv2
import time

mc = Minecraft.create("127.0.0.1")

WOOL = block.WOOL.id
wool_colors = {
    (249, 255, 255): 0, (249, 131, 23): 1, (199, 78, 201): 2, (103, 151, 203): 3,
    (229, 227, 23): 4, (114, 201, 23): 5, (249, 151, 175): 6, (67, 67, 67): 7,
    (159, 159, 159): 8, (23, 151, 175): 9, (127, 67, 183): 10, (51, 78, 175): 11,
    (103, 51, 23): 12, (78, 103, 23): 13, (151, 23, 23): 14, (23, 23, 23): 15
}

def find_nearest_color(rgb):
    min_distance = float('inf')
    best_match = 0
    for color_rgb, data_id in wool_colors.items():
        distance = math.sqrt(sum((a-b)**2 for a, b in zip(rgb, color_rgb)))
        if distance < min_distance:
            min_distance = distance
            best_match = data_id
    return best_match

def create_pixel_art(image, size, start_pos):
    image = image.resize(size)
    width, height = image.size
    pixels = image.load()
    for x in range(width):
        for z in range(height):
            r, g, b = pixels[x, z]
            mc.setBlock(start_pos.x + x, start_pos.y, start_pos.z + z, WOOL, find_nearest_color((r, g, b)))

def create_video_pixel_art(video_path, pixel_size, frames_to_process=10, fps=1, loop=False):
    while True:
        cap = cv2.VideoCapture(video_path)
        if not cap.isOpened():
            print(f"Error: Could not open video file at {video_path}")
            return
        start_pos = mc.player.getTilePos()
        frame_count = 0
        while cap.isOpened() and (frame_count < frames_to_process or loop):
            ret, frame = cap.read()
            if not ret:
                if loop:
                    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
                    ret, frame = cap.read()
                else:
                    break
            img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            pil_img = Image.fromarray(img_rgb)
            create_pixel_art(pil_img, pixel_size, start_pos)
            frame_count += 1
            time.sleep(1.0 / fps)
        cap.release()
        if not loop:
            break

video_path = "Gif.mp4"
pixel_art_size = (16, 16)
create_video_pixel_art(video_path, pixel_art_size, frames_to_process=100, fps=1, loop=True)

出力結果

Gifで調べて出てきたいい感じに色と動きがあるやつを使ってます。

16×16ピクセル
https://youtu.be/Z3VgUSMIOPo


3. 高速化版(キャッシュ利用)

動画をフレームごとに描画する場合、ピクセルごとの色 → 羊毛ブロック色の変換処理を毎回計算すると非常に時間がかかります。

そこで、色判定の結果をキャッシュすることで処理を高速化しました。

変更点

  • 量子化(quantize)
    近似色計算の精度を少し落とし、色を5ビット単位に丸めることで、同じ色が再利用されやすくなるようにしました。

  • キャッシュ辞書(color_cache
    一度計算したRGB → 羊毛ブロックIDの対応を辞書に保存し、同じ色が出たら再計算せずキャッシュから取得するようにしました。

効果

  • 同じ色の計算を何度も繰り返す無駄がなくなる
  • フレーム全体で色変換が大幅に高速化
from PIL import Image
from mcpi.minecraft import Minecraft
from mcpi import block
import cv2
import time

mc = Minecraft.create("127.0.0.1")

WOOL = block.WOOL.id
wool_colors = {
    (249, 255, 255): 0, (249, 131, 23): 1, (199, 78, 201): 2, (103, 151, 203): 3,
    (229, 227, 23): 4, (114, 201, 23): 5, (249, 151, 175): 6, (67, 67, 67): 7,
    (159, 159, 159): 8, (23, 151, 175): 9, (127, 67, 183): 10, (51, 78, 175): 11,
    (103, 51, 23): 12, (78, 103, 23): 13, (151, 23, 23): 14, (23, 23, 23): 15
}

color_cache = {}

def quantize_rgb(rgb, bits=5):
    shift = 8 - bits
    return ((rgb[0] >> shift) << shift,
            (rgb[1] >> shift) << shift,
            (rgb[2] >> shift) << shift)

def find_nearest_color(rgb):
    key = quantize_rgb(rgb)
    if key in color_cache:
        return color_cache[key]
    min_distance = float('inf')
    best_match = 0
    for color_rgb, data_id in wool_colors.items():
        distance = sum((a-b)**2 for a, b in zip(key, color_rgb)) ** 0.5
        if distance < min_distance:
            min_distance = distance
            best_match = data_id
    color_cache[key] = best_match
    return best_match

def create_pixel_art(image, size, start_pos):
    image = image.resize(size)
    width, height = image.size
    pixels = image.load()
    for x in range(width):
        for z in range(height):
            r, g, b = pixels[x, z]
            mc.setBlock(start_pos.x + x, start_pos.y, start_pos.z + z, WOOL, find_nearest_color((r, g, b)))

def create_video_pixel_art(video_path, pixel_size, frames_to_process=10, fps=1, loop=False):
    while True:
        cap = cv2.VideoCapture(video_path)
        if not cap.isOpened():
            print(f"Error: Could not open video file at {video_path}")
            return
        start_pos = mc.player.getTilePos()
        frame_count = 0
        while cap.isOpened() and (frame_count < frames_to_process or loop):
            ret, frame = cap.read()
            if not ret:
                if loop:
                    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
                    ret, frame = cap.read()
                else:
                    break
            img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            pil_img = Image.fromarray(img_rgb)
            create_pixel_art(pil_img, pixel_size, start_pos)
            frame_count += 1
            time.sleep(1.0 / fps)
        cap.release()
        if not loop:
            break

video_path = "Gif.mp4"
pixel_art_size = (32, 32)
create_video_pixel_art(video_path, pixel_art_size, frames_to_process=100, fps=1, loop=True)

出力結果

32×32ピクセル
https://youtu.be/Vey7ZGf5D2g


まとめ

  • 静止画 → Pillowで画像を読み込み、羊毛ブロックの近似色を選んで設置
  • 動画 → OpenCVでフレームを取得し、連続的に設置してアニメーション再現
  • 高速化版 → 色判定をキャッシュ化して処理時間を短縮

感想

今回の実装で、Minecraft内に静止画・動画を描画する仕組みが一通りできました。

今回は低ビット&解像度を落とした状態で処理したため、高速に動作しましたが、
ブロック数が増える大規模描画では、さらなる効率化の仕組みを考える余地がありますね。

もともとコーディングとの相性は良いと思っていましたが、
実際にやってみるとMinecraftの世界を自由に書き換えられる感覚はなかなか楽しかったです。

次は、立体のブロックアートにも挑戦してみようと思います。
動画を壁ではなく立体で再現できれば、さらに迫力のある表現が可能になりそうです。

※このスクリプトは教育・学習目的で作成したものであり、商用利用や外部サーバーでの利用は行っておりません。

Discussion