Last active
April 13, 2025 15:15
-
-
Save Tiryoh/8267f745a2f7cdef6cc7283bfe2547e1 to your computer and use it in GitHub Desktop.
mini-pupper-chan avatar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
stack-chanのアバターを生成するスクリプト(Apache-2.0ライセンス, Copyright 2021 Shinya Ishikawa)を参考に作った | |
Mini Pupper用のアバターを生成するスクリプトです | |
瞬きの機能はありません | |
https://github.com/stack-chan/stack-chan/blob/dev/v1.0/firmware/stackchan/renderers/simple-face.ts | |
# (C) 2025 Daisuke Sato | |
# Released under the MIT License | |
# https://tiryoh.mit-license.org/2025- | |
""" | |
from MangDang.mini_pupper.display import Display | |
import time | |
import random | |
from PIL import Image, ImageDraw | |
def createEyelidPart(draw, cx, cy, width, height, side, eye_open, emotion): | |
""" | |
指定された感情(emotion)に基づいて眼瞼部分(アイリッド)の描画を行う関数 | |
引数: | |
draw : PillowのImageDraw.Drawオブジェクト | |
cx, cy : 描画エリアの中心座標 | |
width, height: 眼瞼部品の幅と高さ | |
side : 'left' または 'right' | |
eye_open : 目の開き具合 (0=完全に開, 1=完全に閉) | |
emotion : 'ANGRY', 'SAD', 'SLEEPY', 'HAPPY' などの文字列 | |
""" | |
# 描画領域の基準計算 | |
w = width | |
h = height * (1 - eye_open) | |
x = cx - width / 2 | |
y = cy - height / 2 | |
if emotion in ("ANGRY", "SAD"): | |
# 感情が ANGRY または SAD の場合、上下の頂点位置を計算 | |
h1 = y + (height + h) / 2 | |
h2 = y + h | |
# 左側の場合は h1 と h2 を入れ替える | |
if side == "left": | |
h1, h2 = h2, h1 | |
# SAD の場合、再度交換 | |
if emotion == "SAD": | |
h1, h2 = h2, h1 | |
# ポリゴンとして塗りつぶしで描画 | |
draw.polygon([(x, y), (x, h1), (x + w, h2), (x + w, y)], fill="black") | |
elif emotion == "SLEEPY": | |
# SLEEPY の場合は、矩形で上半分を塗りつぶしで描画 | |
draw.rectangle([x, y, x + w, y + height * 0.5 + h * 0.5], fill="black") | |
elif emotion == "HAPPY": | |
# HAPPYの場合は目を丸く表示するため、何も描画しない | |
return | |
else: | |
# その他の場合は、単純に矩形を塗りつぶしで描画 | |
draw.rectangle([x, y, x + w, y + h], fill="black") | |
def createEyePart(draw, cx, cy, radius, gaze=(0,0), fill="white"): | |
""" | |
指定された中心(cx,cy)・半径 radius で目(楕円)を描画する関数 | |
gaze: (gazeX, gazeY) のタプル(任意、オフセットとして適用) | |
""" | |
offsetX = gaze[0] * 2 if gaze else 0 | |
offsetY = gaze[1] * 2 if gaze else 0 | |
x0 = cx + offsetX - radius | |
y0 = cy + offsetY - radius | |
x1 = cx + offsetX + radius | |
y1 = cy + offsetY + radius | |
draw.ellipse((x0, y0, x1, y1), fill=fill) | |
def createMouthPart(draw, cx, cy, minWidth=32, maxWidth=32, minHeight=8, maxHeight=8, open_ratio=0, fill="white"): | |
""" | |
TS の仕様に近い口を描画 | |
open_ratio: 0(完全に閉)〜1(完全に開) | |
""" | |
h = minHeight + (maxHeight - minHeight) * open_ratio | |
w = minWidth + (maxWidth - minWidth) * (1 - open_ratio) | |
x = cx - w/2 | |
y = cy - h/2 | |
draw.rectangle((x, y, x+w, y+h), fill=fill) | |
def generate_avatar(eyelid_emotion): | |
# 画像設定・黒背景 | |
width, height = 320, 240 | |
img = Image.new('RGB', (width, height), color=(0, 0, 0)) | |
draw = ImageDraw.Draw(img) | |
# TS座標変換 | |
base_scale = 0.8 | |
enlarge_factor = 1.2 | |
effective_scale = base_scale * enlarge_factor # 0.96 | |
offset_x = 160 - int(round(160 * effective_scale)) | |
offset_y = 120 - int(round(112 * effective_scale)) | |
# 左目: TS座標 (90,93) -> effective_scaleとオフセットを適用 | |
left_eye_cx = int(90 * effective_scale + offset_x) | |
left_eye_cy = int(93 * effective_scale + offset_y) | |
left_eye_radius = int(8 * effective_scale) | |
# 右目: TS座標 (230,96) -> effective_scaleとオフセットを適用 | |
right_eye_cx = int(230 * effective_scale + offset_x) | |
right_eye_cy = int(96 * effective_scale + offset_y) | |
right_eye_radius = int(8 * effective_scale) | |
# 目は createEyePart を使用して描画 | |
createEyePart(draw, left_eye_cx, left_eye_cy, left_eye_radius, fill="white") | |
createEyePart(draw, right_eye_cx, right_eye_cy, right_eye_radius, fill="white") | |
# 口: TS座標 (160,148) -> effective_scaleとオフセットを適用 | |
# 口のサイズ: TS版のパラメータを effective_scale で拡大 | |
mouth_cx = int(160 * effective_scale + offset_x) | |
mouth_cy = int(148 * effective_scale + offset_y) | |
createMouthPart(draw, mouth_cx, mouth_cy, | |
minWidth=int(50 * effective_scale), maxWidth=int(90 * effective_scale), | |
minHeight=int(8 * effective_scale), maxHeight=int(58 * effective_scale), | |
open_ratio=0, fill="white") | |
# eyelid 描画のためのパラメータ設定 | |
eye_open = 0.5 # 目開閉 | |
eyelid_width = int(24 * effective_scale) | |
eyelid_height = int(24 * effective_scale) | |
# 左側の eyelid は createEyelidPart で描画 | |
createEyelidPart(draw, left_eye_cx, left_eye_cy, eyelid_width, eyelid_height, | |
"left", eye_open, eyelid_emotion) | |
# 右側の eyelid は createEyelidPart で描画 | |
createEyelidPart(draw, right_eye_cx, right_eye_cy, eyelid_width, eyelid_height, | |
"right", eye_open, eyelid_emotion) | |
return img | |
def main(): | |
disp = Display() | |
interval = 60 | |
while True: | |
eyelid_emotion = random.choice(["ANGRY", "SAD", "SLEEPY", "HAPPY"]) # 感情をランダムに選択 | |
face = generate_avatar(eyelid_emotion) | |
disp.disp.display(face.resize((320,240))) | |
time.sleep(interval) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example https://x.com/tiryoh/status/1886462039528497461?s=46&t=XvgW8pT1WSUAd5wkXPdClQ