4
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?

はじめに

m5stack-avatarをpygameで再現しました!

開発環境

  • Windows 11 PC
  • Python 3.11

導入

Face.py
import pygame
import sys
import math
import random
import numpy as np
import soundcard as sc

import warnings

warnings.filterwarnings("ignore", category=sc.SoundcardRuntimeWarning)

# Initialize Pygame
pygame.init()

# Screen dimensions
screen_width = 1120  # 320
screen_height = 840  # 240

# Colors
COLOR_PRIMARY = (255, 255, 255)  # White
COLOR_BACKGROUND = (0, 0, 0)  # Black

# Create the screen
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Face Display")

# Audio setup
CHUNK = 1024
CHANNELS = 1
RATE = 44100

# Mode to control mouth movement
use_speaker_audio = True  # Set to False to use microphone audio

# Drawable classes
class Drawable:
    def draw(self, surface, rect, ctx):
        pass


class Mouth(Drawable):
    def __init__(self, minWidth, maxWidth, minHeight, maxHeight):
        self.minWidth = minWidth
        self.maxWidth = maxWidth
        self.minHeight = minHeight
        self.maxHeight = maxHeight

    def draw(self, surface, rect, ctx):
        primaryColor = COLOR_PRIMARY
        breath = min(1.0, ctx["breath"])
        openRatio = ctx["mouthOpenRatio"]
        h = self.minHeight + (self.maxHeight - self.minHeight) * openRatio
        w = self.minWidth + (self.maxWidth - self.minWidth) * (1 - openRatio)
        x = rect.centerx - w // 2
        y = rect.centery - h // 2 + int(breath * 2)
        pygame.draw.rect(surface, primaryColor, (x, y, w, h))


class Eye(Drawable):
    def __init__(self, size, is_left):
        self.size = size
        self.is_left = is_left

    def draw(self, surface, rect, ctx):
        exp = ctx["expression"]
        x, y = rect.center
        offsetX = ctx["gaze"]["horizontal"] * 3
        offsetY = ctx["gaze"]["vertical"] * 3
        openRatio = ctx["eyeOpenRatio"]
        primaryColor = COLOR_PRIMARY
        backgroundColor = COLOR_BACKGROUND

        if openRatio > 0:
            pygame.draw.circle(
                surface, primaryColor, (x + offsetX, y + offsetY), self.size
            )
            if exp in ["Angry", "Sad"]:
                x0, y0 = x + offsetX - self.size, y + offsetY - self.size
                x1, y1 = x0 + self.size * 2, y0
                x2, y2 = (
                    x0 if (self.is_left != (exp == "Sad")) else x1
                ), y0 + self.size
                pygame.draw.polygon(
                    surface, backgroundColor, [(x0, y0), (x1, y1), (x2, y2)]
                )
            if exp in ["Happy", "Sleepy"]:
                x0, y0 = x + offsetX - self.size, y + offsetY - self.size
                w, h = self.size * 2 + 4, self.size + 2
                if exp == "Happy":
                    y0 += self.size
                    pygame.draw.circle(
                        surface,
                        backgroundColor,
                        (x + offsetX, y + offsetY),
                        int(self.size / 1.5),
                    )
                pygame.draw.rect(surface, backgroundColor, (x0, y0, w, h))
        else:
            x1, y1 = x - self.size + offsetX, y - 2 + offsetY
            w, h = self.size * 2, 4
            pygame.draw.rect(surface, primaryColor, (x1, y1, w, h))


class Eyebrow(Drawable):
    def __init__(self, width, height, is_left):
        self.width = width
        self.height = height
        self.is_left = is_left

    def draw(self, surface, rect, ctx):
        exp = ctx["expression"]
        x, y = rect.centerx, rect.centery
        primaryColor = COLOR_PRIMARY

        if self.width == 0 or self.height == 0:
            return

        if exp in ["Angry", "Sad"]:
            a = -1 if self.is_left ^ (exp == "Sad") else 1
            dx = a * 3
            dy = a * 5
            x1, y1 = x - self.width // 2, y - self.height // 2 - dy
            x2, y2 = x1 - dx, y + self.height // 2 - dy
            x3, y3 = x + self.width // 2 + dx, y - self.height // 2 + dy
            x4, y4 = x + self.width // 2, y + self.height // 2 + dy
            pygame.draw.polygon(surface, primaryColor, [(x1, y1), (x2, y2), (x3, y3)])
            pygame.draw.polygon(surface, primaryColor, [(x2, y2), (x3, y3), (x4, y4)])
        else:
            x1, y1 = x - self.width // 2, y - self.height // 2
            if exp == "Happy":
                y1 -= 5
            pygame.draw.rect(surface, primaryColor, (x1, y1, self.width, self.height))


class BoundingRect(pygame.Rect):
    def setPosition(self, top, left):
        self.top = top
        self.left = left


class Face:
    def __init__(self):
        scale_x = screen_width / 320
        scale_y = screen_height / 240

        self.mouth = Mouth(50 * scale_x, 90 * scale_x, 4 * scale_y, 60 * scale_y)
        self.mouthPos = BoundingRect(
            screen_width * 0.5 - 25 * scale_x,
            screen_height * 0.6,
            50 * scale_x,
            20 * scale_y,
        )  # Adjusted mouth position to center

        self.eyeR = Eye(8 * scale_x, False)
        self.eyeRPos = BoundingRect(
            screen_width * 0.5 - 60 * scale_x - 8 * scale_x,
            screen_height * 0.3875,
            16 * scale_x,
            16 * scale_y,
        )  # Adjusted eye position

        self.eyeL = Eye(8 * scale_x, True)
        self.eyeLPos = BoundingRect(
            screen_width * 0.5 + 60 * scale_x - 8 * scale_x,
            screen_height * 0.3875,
            16 * scale_x,
            16 * scale_y,
        )  # Adjusted eye position to be symmetrical

        self.eyebrowR = Eyebrow(32 * scale_x, 8 * scale_y, False)
        self.eyebrowRPos = BoundingRect(
            screen_width * 0.5 - 60 * scale_x - 16 * scale_x,
            screen_height * 0.2792,
            32 * scale_x,
            8 * scale_y,
        )  # Adjusted eyebrow position

        self.eyebrowL = Eyebrow(32 * scale_x, 8 * scale_y, True)
        self.eyebrowLPos = BoundingRect(
            screen_width * 0.5 + 60 * scale_x - 16 * scale_x,
            screen_height * 0.2792,
            32 * scale_x,
            8 * scale_y,
        )  # Adjusted eyebrow position to be symmetrical

        self.boundingRect = BoundingRect(0, 0, screen_width, screen_height)

    def draw(self, ctx):
        screen.fill(COLOR_BACKGROUND)
        breath = min(1.0, ctx["breath"])

        rect = self.mouthPos.copy()
        rect.setPosition(rect.top + breath * 3, rect.left)
        self.mouth.draw(screen, rect, ctx)

        rect = self.eyeRPos.copy()
        rect.setPosition(rect.top + breath * 3, rect.left)
        self.eyeR.draw(screen, rect, ctx)

        rect = self.eyeLPos.copy()
        rect.setPosition(rect.top + breath * 3, rect.left)
        self.eyeL.draw(screen, rect, ctx)

        # rect = self.eyebrowRPos.copy()
        # rect.setPosition(rect.top + breath * 3, rect.left)
        # self.eyebrowR.draw(screen, rect, ctx)

        # rect = self.eyebrowLPos.copy()
        # rect.setPosition(rect.top + breath * 3, rect.left)
        # self.eyebrowL.draw(screen, rect, ctx)

        pygame.display.flip()


# Context for drawing
ctx = {
    "breath": 0.5,
    "colorDepth": 32,
    "colorPalette": {
        "COLOR_PRIMARY": COLOR_PRIMARY,
        "COLOR_BACKGROUND": COLOR_BACKGROUND,
    },
    "expression": "Neutral",
    "gaze": {"horizontal": 0, "vertical": 0},
    "eyeOpenRatio": 1.0,
    "mouthOpenRatio": 0.5,
}

# Main loop
face = Face()
running = True
fullscreen = False

# Initialize random seed
random.seed()

# Timers for saccade and blink
saccade_interval = 1000
blink_interval = 1000
last_saccade_time = pygame.time.get_ticks()
last_blink_time = pygame.time.get_ticks()
eye_open = True

# List of expressions
expressions = ["Neutral", "Happy", "Sad", "Angry", "Sleepy"]
current_expression_index = 0

def audio_callback(indata, frames, time, status):
    volume_norm = np.linalg.norm(indata) * 10
    ctx["mouthOpenRatio"] = min(1.0, volume_norm / 100)

samplerate = 48000  # サンプリング周波数 [Hz]

mic_id = str(sc.default_speaker().name) if use_speaker_audio else str(sc.default_microphone().name)

with sc.get_microphone(id=mic_id, include_loopback=use_speaker_audio).recorder(samplerate=samplerate) as mic:
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_f:
                    fullscreen = not fullscreen
                    if fullscreen:
                        screen = pygame.display.set_mode(
                            (screen_width, screen_height), pygame.FULLSCREEN
                        )
                    else:
                        screen = pygame.display.set_mode((screen_width, screen_height))
                elif event.key == pygame.K_SPACE:
                    current_expression_index = (current_expression_index + 1) % len(expressions)
                    ctx["expression"] = expressions[current_expression_index]

        current_time = pygame.time.get_ticks()

        # Saccade logic
        if current_time - last_saccade_time > saccade_interval:
            ctx["gaze"]["horizontal"] = random.uniform(-1, 1)
            ctx["gaze"]["vertical"] = random.uniform(-1, 1)
            saccade_interval = 500 + 100 * random.randint(0, 20)
            last_saccade_time = current_time

        # Blink logic
        if current_time - last_blink_time > blink_interval:
            if eye_open:
                ctx["eyeOpenRatio"] = 1.0
                blink_interval = 2500 + 100 * random.randint(0, 20)
            else:
                ctx["eyeOpenRatio"] = 0.0
                blink_interval = 300 + 10 * random.randint(0, 20)
            eye_open = not eye_open
            last_blink_time = current_time

        # Breath logic
        ctx["breath"] = math.sin(pygame.time.get_ticks() * 2 * math.pi / 2000.0)

        audio_data = mic.record(numframes=CHUNK)
        audio_callback(audio_data, CHUNK, None, None)

        face.draw(ctx)
        pygame.time.delay(33)  # approx. 30fps

pygame.quit()
sys.exit()
キー及び設定 機能
スペースキー 表情を変更("Neutral", "Happy", "Sad", "Angry", "Sleepy")
Fキー フルスクリーンモード
use_speaker_audio Trueでスピーカー音、Falseでマイク音を拾ってリップシンク

実行結果

お疲れさまでした

4
1
0

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
4
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?