1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QRコードでライフゲームをやってみた

Posted at

はじめに(概要)

この記事では、LTで使用するQRコードを少しでも印象的に見せるために行った実験について紹介します。
Conwayのライフゲームを用いて、QRコードを初期状態として発展させ、その過程をGIFとして可視化しました。

  • 対象読者
    LT・勉強会・技術イベントなどで登壇されている方
    発表資料のちょっとした演出を工夫したい方
  • 記事の目的
    実装したコードの共有
    もっと面白い見せ方や改良案があれば、ぜひ教えてほしいという思いから

背景・動機

今年から発信力を鍛える目的で、LTに登壇する機会を増やしています。
その際、宣伝としてQiitaのQRコードをスライドに貼っているのですが、ただ表示するだけでは少し味気ないと感じていました。

何か工夫できないかと考えていたところ、以下の記事で「ライフゲーム」を知りました。

単純なルールから複雑なパターンが生まれるライフゲームの性質を使えば、
QRコードが“生成されていくように見える演出”ができるのではないかと思い、実際に試してみた、というのが本記事の内容です。


ライフゲームについて

まずは簡単にライフゲームについて説明します。以下は Wikipedia からの抜粋です。

ライフゲーム(Conway's Game of Life)は1970年にイギリスの数学者ジョン・ホートン・コンウェイによって考案された数理モデルである。
単純なルールから複雑な結果が生成され、生命の誕生、進化、淘汰などを連想させる振る舞いを示す。

ライフゲームでは、各セルが「生」か「死」の状態を持ち、周囲8マスの状態に応じて次の状態が決まるという非常にシンプルなルールで世界が進行します。

今回はこの「白黒が世代ごとに変化していく」という性質を利用し、
QRコードを初期状態として発展させてみよう、という試みになります。


実際にやってみたこと

以下の手順で実験を行いました。

  1. QRコード画像を読み込む
  2. 画像を一定サイズのブロックに分割し、ライフゲーム用のセルに変換
  3. ライフゲームを100エポック実行
  4. 各エポックごとに画像を書き出す
  5. 生成した画像を逆順に並べ、GIFとして可視化

「無の状態からQRコードが浮かび上がってくる」ような表現を狙っています。

今回使用したQRコードはこちらです。
黒を「生」、白を「死」として扱います。
MyQR.png


実装例

実際のコード例を示します。

import numpy as np
import cv2

# ---------------------------
# 6x6 block → cell value
# ---------------------------
def block_to_cell(img, block_size=6):
    H, W = img.shape
    new_H = H // block_size
    new_W = W // block_size
    cells = np.zeros((new_H, new_W), dtype=np.uint8)

    for i in range(new_H):
        for j in range(new_W):
            block = img[i*block_size:(i+1)*block_size,
                        j*block_size:(j+1)*block_size]
            mean_val = block.mean()
            cells[i, j] = 0 if mean_val < 128 else 255  # black=alive, white=dead
    return cells


# ---------------------------
# Cell → 6x6 block image
# ---------------------------
def cell_to_block(cells, block_size=6):
    H, W = cells.shape
    img = np.zeros((H*block_size, W*block_size), dtype=np.uint8)
    for i in range(H):
        for j in range(W):
            img[i*block_size:(i+1)*block_size,
                j*block_size:(j+1)*block_size] = cells[i, j]
    return img


# ---------------------------
# Game of Life (vectorized)
# ---------------------------
def next_gen(img):
    # alive = 0, dead = 255 → 生きているセルを1、死を0に変換
    alive = (img == 0).astype(np.uint8)

    # 8近傍の合計
    neighbors = (
        np.roll(np.roll(alive, -1, 0), -1, 1) +
        np.roll(np.roll(alive, -1, 0),  0, 1) +
        np.roll(np.roll(alive, -1, 0),  1, 1) +
        np.roll(np.roll(alive,  0, 0), -1, 1) +
        np.roll(np.roll(alive,  0, 0),  1, 1) +
        np.roll(np.roll(alive,  1, 0), -1, 1) +
        np.roll(np.roll(alive,  1, 0),  0, 1) +
        np.roll(np.roll(alive,  1, 0),  1, 1)
    )

    # ルール適用(alive=1)
    next_alive = ((alive == 1) & ((neighbors == 2) | (neighbors == 3))) | \
                 ((alive == 0) & (neighbors == 3))
    # 1 → 0(黒)、0 → 255(白)に戻す
    return np.where(next_alive, 0, 255).astype(np.uint8)


# ---------------------------
# Main
# ---------------------------
img = cv2.imread("MyQR.png", cv2.IMREAD_GRAYSCALE)
if img is None:
    raise ValueError("画像が読み込めません")

block_size = 6
epoch_num = 100

# 6x6 ブロック → セル化
cells = block_to_cell(img, block_size)

# 実行
for epoch in range(1, epoch_num+1):
    cells = next_gen(cells)

    # 保存
    out_img = cell_to_block(cells, block_size)
    cv2.imwrite(f"epoch_{epoch}.png", out_img)
    print(f"Saved epoch_{epoch}.png")

print("完了")

GIF画像は以下のコードを書いて、生成しています。なお、生成するときに画像を昇順に並び替える必要があるので、epoch_100.png → epoch_000.png、epoch_99.png → epoch_001.png、、、epoch_1.png → epoch_100.png、元のQRコード → epoch_101.pngという風にラベリングしています。

import os
import shutil
import re

# ===========================
# 設定
# ===========================
src_dir = "image"          # 元画像フォルダ
dst_dir = "renamed"        # 保存先フォルダ
base_number = 100      # 100 - xx
zero_pad = 3               # ゼロ埋め桁数

# ===========================
# 出力フォルダ作成
# ===========================
os.makedirs(dst_dir, exist_ok=True)

# ===========================
# ファイル処理
# ===========================
pattern = re.compile(r"epoch_(\d+)\.png")

for filename in os.listdir(src_dir):
    match = pattern.match(filename)
    if not match:
        continue

    xx = int(match.group(1))
    new_num = base_number - xx
    new_name = f"{str(new_num).zfill(zero_pad)}.png"

    src_path = os.path.join(src_dir, filename)
    dst_path = os.path.join(dst_dir, new_name)

    shutil.copy(src_path, dst_path)
    print(f"{filename} → {new_name}")

print("完了")

実行結果・動作確認

実際に生成されたGIFが以下になります。
output.gif


実際にやってみて

概ね狙い通りの実装はできましたが、いくつか不満点も残りました。

不満点

  • QRコードが唐突に現れる印象がある

今回のライフゲームのルールでは、QRコードの角のQRコードらしさを構成するセルが初期段階で崩れやすく、
結果として急に形が壊れ、急に現れるような見え方になってしまいました。以下が初期(epoch0)の状態と、1世代進めたとき(epoch1)の状態の比較です。

MyQR.png
epoch0
epoch_1.png
epoch1

三隅の黒い四角部分が急に壊れていることがわかるかと思います

まとめ

本記事では、ライフゲームを用いてQRコードを動的に表現する試みを紹介しました。

  • ライフゲームは視覚的な演出として非常に面白い
  • ルール改変や別のセルオートマトンを使えば、より自然な表現ができそう

LTなどで少し変わったQRコードを見せたい場合の、
一つのアイデアとして参考になれば嬉しいです。


参考リンク


おわりに

  • いいね・コメント・フォローいただけると励みになります
  • 誤りの指摘や改善案、大歓迎です
  • 「こういうルールの方が面白いのでは?」といったアイデアもぜひ教えてください
1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?