mirror of
https://github.com/SillyTavern/SillyTavern-Extras.git
synced 2026-05-01 11:51:22 +00:00
Live2d Init
This commit is contained in:
0
live2d/tha3/app/__init__.py
Normal file
0
live2d/tha3/app/__init__.py
Normal file
628
live2d/tha3/app/app.py
Normal file
628
live2d/tha3/app/app.py
Normal file
@@ -0,0 +1,628 @@
|
||||
import argparse
|
||||
import cv2
|
||||
import os
|
||||
import random
|
||||
import requests
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import wx
|
||||
from PIL import Image
|
||||
from torchvision import transforms
|
||||
from flask import Flask, render_template, Response, send_file, request
|
||||
from flask_cors import CORS
|
||||
from io import BytesIO
|
||||
|
||||
sys.path.append(os.getcwd())
|
||||
from tha3.mocap.ifacialmocap_constants import *
|
||||
from tha3.mocap.ifacialmocap_pose import create_default_ifacialmocap_pose
|
||||
from tha3.mocap.ifacialmocap_pose_converter import IFacialMocapPoseConverter
|
||||
from tha3.mocap.ifacialmocap_poser_converter_25 import create_ifacialmocap_pose_converter
|
||||
from tha3.poser.modes.load_poser import load_poser
|
||||
from tha3.poser.poser import Poser
|
||||
from tha3.util import (
|
||||
torch_linear_to_srgb, resize_PIL_image, extract_PIL_image_from_filelike,
|
||||
extract_pytorch_image_from_PIL_image
|
||||
)
|
||||
from typing import Optional
|
||||
|
||||
# Add the current working directory to the system path
|
||||
sys.path.append(os.getcwd())
|
||||
|
||||
# Global Variables
|
||||
global_source_image = None
|
||||
global_source_image_path = None
|
||||
global_result_image = None
|
||||
global_reload = None
|
||||
is_talking_override = False
|
||||
is_talking = False
|
||||
|
||||
# Flask setup
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
def start_talking():
|
||||
global is_talking_override
|
||||
is_talking_override = True
|
||||
#return send_file(global_source_image_path, mimetype='image/png')
|
||||
return "started"
|
||||
|
||||
def stop_talking():
|
||||
global is_talking_override
|
||||
is_talking_override = False
|
||||
return "stopped"
|
||||
|
||||
def result_feed():
|
||||
def generate():
|
||||
while True:
|
||||
if global_result_image is not None:
|
||||
|
||||
try:
|
||||
# Encode the numpy array to PNG
|
||||
_, buffer = cv2.imencode('.png', global_result_image)
|
||||
except Exception as e:
|
||||
print(f"Error when trying to write image: {e}")
|
||||
|
||||
# Send the PNG image
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/png\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
def live2d_load_url(url):
|
||||
img = None
|
||||
global global_source_image
|
||||
global global_reload
|
||||
response = requests.get(url)
|
||||
try:
|
||||
img = Image.open(BytesIO(response.content))
|
||||
except Image.UnidentifiedImageError:
|
||||
print(f"Could not identify image from URL: {url}")
|
||||
global_reload = img
|
||||
return 'OK'
|
||||
|
||||
def convert_linear_to_srgb(image: torch.Tensor) -> torch.Tensor:
|
||||
rgb_image = torch_linear_to_srgb(image[0:3, :, :])
|
||||
return torch.cat([rgb_image, image[3:4, :, :]], dim=0)
|
||||
|
||||
def launch_gui(device, model):
|
||||
parser = argparse.ArgumentParser(description='uWu Waifu')
|
||||
|
||||
# Add other parser arguments here
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
try:
|
||||
poser = load_poser(model, device)
|
||||
pose_converter = create_ifacialmocap_pose_converter()
|
||||
|
||||
app = wx.App()
|
||||
main_frame = MainFrame(poser, pose_converter, device)
|
||||
main_frame.SetSize((750, 600))
|
||||
|
||||
#Lload default image (you can pass args.char if required)
|
||||
full_path = os.path.join(os.getcwd(), "live2d\\tha3\\images\\lambda_00.png")
|
||||
main_frame.load_image(None, full_path)
|
||||
|
||||
#main_frame.Show(True)
|
||||
main_frame.capture_timer.Start(100)
|
||||
main_frame.animation_timer.Start(100)
|
||||
app.MainLoop()
|
||||
|
||||
except RuntimeError as e:
|
||||
print(e)
|
||||
sys.exit()
|
||||
|
||||
class FpsStatistics:
|
||||
def __init__(self):
|
||||
self.count = 100
|
||||
self.fps = []
|
||||
|
||||
def add_fps(self, fps):
|
||||
self.fps.append(fps)
|
||||
while len(self.fps) > self.count:
|
||||
del self.fps[0]
|
||||
|
||||
def get_average_fps(self):
|
||||
if len(self.fps) == 0:
|
||||
return 0.0
|
||||
else:
|
||||
return sum(self.fps) / len(self.fps)
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
def __init__(self, poser: Poser, pose_converter: IFacialMocapPoseConverter, device: torch.device):
|
||||
super().__init__(None, wx.ID_ANY, "uWu Waifu")
|
||||
self.pose_converter = pose_converter
|
||||
self.poser = poser
|
||||
self.device = device
|
||||
|
||||
self.image_load_counter = 0
|
||||
self.custom_background_image = None # Add this line
|
||||
|
||||
self.sliders = {}
|
||||
self.ifacialmocap_pose = create_default_ifacialmocap_pose()
|
||||
self.source_image_bitmap = wx.Bitmap(self.poser.get_image_size(), self.poser.get_image_size())
|
||||
self.result_image_bitmap = wx.Bitmap(self.poser.get_image_size(), self.poser.get_image_size())
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
self.last_pose = None
|
||||
self.fps_statistics = FpsStatistics()
|
||||
self.last_update_time = None
|
||||
|
||||
self.create_ui()
|
||||
|
||||
self.create_timers()
|
||||
self.Bind(wx.EVT_CLOSE, self.on_close)
|
||||
|
||||
self.update_source_image_bitmap()
|
||||
self.update_result_image_bitmap()
|
||||
|
||||
def create_timers(self):
|
||||
self.capture_timer = wx.Timer(self, wx.ID_ANY)
|
||||
self.Bind(wx.EVT_TIMER, self.update_capture_panel, id=self.capture_timer.GetId())
|
||||
self.animation_timer = wx.Timer(self, wx.ID_ANY)
|
||||
self.Bind(wx.EVT_TIMER, self.update_result_image_bitmap, id=self.animation_timer.GetId())
|
||||
|
||||
def on_close(self, event: wx.Event):
|
||||
# Stop the timers
|
||||
self.animation_timer.Stop()
|
||||
self.capture_timer.Stop()
|
||||
|
||||
# Destroy the windows
|
||||
self.Destroy()
|
||||
event.Skip()
|
||||
|
||||
def on_start_capture(self, event: wx.Event):
|
||||
message_dialog = wx.MessageDialog(self, "", "Error!", wx.OK)
|
||||
message_dialog.ShowModal()
|
||||
message_dialog.Destroy()
|
||||
return
|
||||
|
||||
def random_generate_value(self, min, max, origin_value):
|
||||
random_value = random.choice(list(range(min, max, 1))) / 2500.0
|
||||
randomized = origin_value + random_value
|
||||
if randomized > 1.0:
|
||||
randomized = 1.0
|
||||
if randomized < 0:
|
||||
randomized = 0
|
||||
return randomized
|
||||
|
||||
def random_generate_pose(self):
|
||||
global is_talking
|
||||
current_pose = self.ifacialmocap_pose
|
||||
|
||||
# NOTE: randomize mouth
|
||||
for blendshape_name in BLENDSHAPE_NAMES:
|
||||
if "jawOpen" in blendshape_name:
|
||||
if is_talking or is_talking_override:
|
||||
current_pose[blendshape_name] = self.random_generate_value(-5000, 5000, abs(1 - current_pose[blendshape_name]))
|
||||
else:
|
||||
current_pose[blendshape_name] = 0
|
||||
|
||||
# NOTE: randomize head and eye bones
|
||||
#for key in [HEAD_BONE_Y, LEFT_EYE_BONE_X, LEFT_EYE_BONE_Y, LEFT_EYE_BONE_Z, RIGHT_EYE_BONE_X, RIGHT_EYE_BONE_Y]:
|
||||
#current_pose[key] = self.random_generate_value(-20, 20, current_pose[key])
|
||||
|
||||
#Make her blink
|
||||
if random.random() <= 0.03:
|
||||
current_pose["eyeBlinkRight"] = 1
|
||||
current_pose["eyeBlinkLeft"] = 1
|
||||
else:
|
||||
current_pose["eyeBlinkRight"] = 0
|
||||
current_pose["eyeBlinkLeft"] = 0
|
||||
|
||||
|
||||
return current_pose #print(current_pose)
|
||||
|
||||
def read_ifacialmocap_pose(self):
|
||||
if not self.animation_timer.IsRunning():
|
||||
return self.ifacialmocap_pose
|
||||
self.ifacialmocap_pose = self.random_generate_pose()
|
||||
return self.ifacialmocap_pose
|
||||
|
||||
def on_erase_background(self, event: wx.Event):
|
||||
pass
|
||||
|
||||
def create_animation_panel(self, parent):
|
||||
self.animation_panel = wx.Panel(parent, style=wx.RAISED_BORDER)
|
||||
self.animation_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.animation_panel.SetSizer(self.animation_panel_sizer)
|
||||
self.animation_panel.SetAutoLayout(1)
|
||||
|
||||
image_size = self.poser.get_image_size()
|
||||
|
||||
# Left Column (Image)
|
||||
self.animation_left_panel = wx.Panel(self.animation_panel, style=wx.SIMPLE_BORDER)
|
||||
self.animation_left_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.animation_left_panel.SetSizer(self.animation_left_panel_sizer)
|
||||
self.animation_left_panel.SetAutoLayout(1)
|
||||
self.animation_panel_sizer.Add(self.animation_left_panel, 1, wx.EXPAND)
|
||||
|
||||
self.result_image_panel = wx.Panel(self.animation_left_panel, size=(image_size, image_size),
|
||||
style=wx.SIMPLE_BORDER)
|
||||
self.result_image_panel.Bind(wx.EVT_PAINT, self.paint_result_image_panel)
|
||||
self.result_image_panel.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
|
||||
self.result_image_panel.Bind(wx.EVT_LEFT_DOWN, self.load_image)
|
||||
self.animation_left_panel_sizer.Add(self.result_image_panel, 1, wx.EXPAND)
|
||||
|
||||
separator = wx.StaticLine(self.animation_left_panel, -1, size=(256, 1))
|
||||
self.animation_left_panel_sizer.Add(separator, 0, wx.EXPAND)
|
||||
|
||||
self.fps_text = wx.StaticText(self.animation_left_panel, label="")
|
||||
self.animation_left_panel_sizer.Add(self.fps_text, wx.SizerFlags().Border())
|
||||
|
||||
|
||||
self.animation_left_panel_sizer.Fit(self.animation_left_panel)
|
||||
|
||||
# Right Column (Sliders)
|
||||
|
||||
self.animation_right_panel = wx.Panel(self.animation_panel, style=wx.SIMPLE_BORDER)
|
||||
self.animation_right_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.animation_right_panel.SetSizer(self.animation_right_panel_sizer)
|
||||
self.animation_right_panel.SetAutoLayout(1)
|
||||
self.animation_panel_sizer.Add(self.animation_right_panel, 1, wx.EXPAND)
|
||||
|
||||
separator = wx.StaticLine(self.animation_right_panel, -1, size=(256, 5))
|
||||
self.animation_right_panel_sizer.Add(separator, 0, wx.EXPAND)
|
||||
|
||||
background_text = wx.StaticText(self.animation_right_panel, label="--- Background ---", style=wx.ALIGN_CENTER)
|
||||
self.animation_right_panel_sizer.Add(background_text, 0, wx.EXPAND)
|
||||
|
||||
self.output_background_choice = wx.Choice(
|
||||
self.animation_right_panel,
|
||||
choices=[
|
||||
"TRANSPARENT",
|
||||
"GREEN",
|
||||
"BLUE",
|
||||
"BLACK",
|
||||
"WHITE",
|
||||
"LOADED",
|
||||
"CUSTOM"
|
||||
]
|
||||
)
|
||||
self.output_background_choice.SetSelection(0)
|
||||
self.animation_right_panel_sizer.Add(self.output_background_choice, 0, wx.EXPAND)
|
||||
|
||||
|
||||
#self.pose_converter.init_pose_converter_panel(self.animation_panel) # this changes sliders to breathing on
|
||||
|
||||
#sliders go here
|
||||
|
||||
|
||||
blendshape_groups = {
|
||||
'Eyes': ['eyeLookOutLeft', 'eyeLookOutRight', 'eyeLookDownLeft', 'eyeLookUpLeft', 'eyeWideLeft', 'eyeWideRight'],
|
||||
'Mouth': ['mouthFrownLeft'],
|
||||
'Cheek': ['cheekSquintLeft', 'cheekSquintRight', 'cheekPuff'],
|
||||
'Brow': ['browDownLeft', 'browOuterUpLeft', 'browDownRight', 'browOuterUpRight', 'browInnerUp'],
|
||||
'Eyelash': ['mouthSmileLeft'],
|
||||
'Nose': ['noseSneerLeft', 'noseSneerRight'],
|
||||
'Misc': ['tongueOut']
|
||||
}
|
||||
|
||||
for group_name, variables in blendshape_groups.items():
|
||||
collapsible_pane = wx.CollapsiblePane(self.animation_right_panel, label=group_name, style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
|
||||
collapsible_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_pane_changed)
|
||||
self.animation_right_panel_sizer.Add(collapsible_pane, 0, wx.EXPAND)
|
||||
pane_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
collapsible_pane.GetPane().SetSizer(pane_sizer)
|
||||
|
||||
for variable in variables:
|
||||
variable_label = wx.StaticText(collapsible_pane.GetPane(), label=variable)
|
||||
|
||||
# Multiply min and max values by 100 for the slider
|
||||
slider = wx.Slider(
|
||||
collapsible_pane.GetPane(),
|
||||
value=0,
|
||||
minValue=0,
|
||||
maxValue=100,
|
||||
size=(150, -1), # Set the width to 150 and height to default
|
||||
style=wx.SL_HORIZONTAL | wx.SL_LABELS
|
||||
)
|
||||
|
||||
slider.SetName(variable)
|
||||
slider.Bind(wx.EVT_SLIDER, self.on_slider_change)
|
||||
self.sliders[slider.GetId()] = slider
|
||||
|
||||
pane_sizer.Add(variable_label, 0, wx.ALIGN_CENTER | wx.ALL, 5)
|
||||
pane_sizer.Add(slider, 0, wx.EXPAND)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
self.animation_right_panel_sizer.Fit(self.animation_right_panel)
|
||||
self.animation_panel_sizer.Fit(self.animation_panel)
|
||||
|
||||
def on_pane_changed(self, event):
|
||||
# Update the layout when a collapsible pane is expanded or collapsed
|
||||
self.animation_right_panel.Layout()
|
||||
|
||||
def on_slider_change(self, event):
|
||||
slider = event.GetEventObject()
|
||||
value = slider.GetValue() / 100.0 # Divide by 100 to get the actual float value
|
||||
#print(value)
|
||||
slider_name = slider.GetName()
|
||||
self.ifacialmocap_pose[slider_name] = value
|
||||
|
||||
def create_ui(self):
|
||||
#MAke the UI Elements
|
||||
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.SetSizer(self.main_sizer)
|
||||
self.SetAutoLayout(1)
|
||||
|
||||
self.capture_pose_lock = threading.Lock()
|
||||
|
||||
#Main panel with JPS
|
||||
self.create_animation_panel(self)
|
||||
self.main_sizer.Add(self.animation_panel, wx.SizerFlags(0).Expand().Border(wx.ALL, 5))
|
||||
|
||||
def update_capture_panel(self, event: wx.Event):
|
||||
data = self.ifacialmocap_pose
|
||||
for rotation_name in ROTATION_NAMES:
|
||||
value = data[rotation_name]
|
||||
|
||||
@staticmethod
|
||||
def convert_to_100(x):
|
||||
return int(max(0.0, min(1.0, x)) * 100)
|
||||
|
||||
def paint_source_image_panel(self, event: wx.Event):
|
||||
wx.BufferedPaintDC(self.source_image_panel, self.source_image_bitmap)
|
||||
|
||||
def update_source_image_bitmap(self):
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.source_image_bitmap)
|
||||
if self.wx_source_image is None:
|
||||
self.draw_nothing_yet_string(dc)
|
||||
else:
|
||||
dc.Clear()
|
||||
dc.DrawBitmap(self.wx_source_image, 0, 0, True)
|
||||
del dc
|
||||
|
||||
def draw_nothing_yet_string(self, dc):
|
||||
dc.Clear()
|
||||
font = wx.Font(wx.FontInfo(14).Family(wx.FONTFAMILY_SWISS))
|
||||
dc.SetFont(font)
|
||||
w, h = dc.GetTextExtent("Nothing yet!")
|
||||
dc.DrawText("Nothing yet!", (self.poser.get_image_size() - w) // 2, (self.poser.get_image_size() - h) // 2)
|
||||
|
||||
def paint_result_image_panel(self, event: wx.Event):
|
||||
wx.BufferedPaintDC(self.result_image_panel, self.result_image_bitmap)
|
||||
|
||||
def update_result_image_bitmap(self, event: Optional[wx.Event] = None):
|
||||
|
||||
global global_result_image # Declare global_source_image as a global variable
|
||||
global global_reload
|
||||
|
||||
if global_reload is not None:
|
||||
#print("Global Reload the Image")
|
||||
MainFrame.load_image(self, event=None, file_path=None) # call load_image function here
|
||||
return
|
||||
|
||||
|
||||
|
||||
ifacialmocap_pose = self.read_ifacialmocap_pose()
|
||||
current_pose = self.pose_converter.convert(ifacialmocap_pose)
|
||||
if self.last_pose is not None and self.last_pose == current_pose:
|
||||
return
|
||||
self.last_pose = current_pose
|
||||
|
||||
if self.torch_source_image is None:
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.result_image_bitmap)
|
||||
self.draw_nothing_yet_string(dc)
|
||||
del dc
|
||||
return
|
||||
|
||||
pose = torch.tensor(current_pose, device=self.device, dtype=self.poser.get_dtype())
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
output_image = self.poser.pose(self.torch_source_image, pose)[0].float()
|
||||
output_image = convert_linear_to_srgb((output_image + 1.0) / 2.0)
|
||||
|
||||
background_choice = self.output_background_choice.GetSelection()
|
||||
if background_choice == 6: # Custom background
|
||||
self.image_load_counter += 1 # Increment the counter
|
||||
if self.image_load_counter <= 1: # Only open the file dialog if the counter is 5 or less
|
||||
file_dialog = wx.FileDialog(self, "Choose a background image", "", "", "*.png", wx.FD_OPEN)
|
||||
if file_dialog.ShowModal() == wx.ID_OK:
|
||||
background_image_path = file_dialog.GetPath()
|
||||
# Load the image and convert it to a torch tensor
|
||||
pil_image = Image.open(background_image_path).convert("RGBA")
|
||||
tensor_image = transforms.ToTensor()(pil_image).to(self.device)
|
||||
# Resize the image to match the output image size
|
||||
tensor_image = F.interpolate(tensor_image.unsqueeze(0), size=output_image.shape[1:], mode="bilinear").squeeze(0)
|
||||
self.custom_background_image = tensor_image # Store the custom background image
|
||||
self.output_background_choice.SetSelection(5)
|
||||
else:
|
||||
# If the user cancelled the dialog or didn't choose a file, reset the choice to "TRANSPARENT"
|
||||
self.output_background_choice.SetSelection(5)
|
||||
else:
|
||||
# Use the stored custom background image
|
||||
output_image = self.blend_with_background(output_image, self.custom_background_image)
|
||||
|
||||
|
||||
else: # Predefined colors
|
||||
self.image_load_counter = 0
|
||||
if background_choice == 0: # Transparent
|
||||
pass
|
||||
elif background_choice == 1: # Green
|
||||
background = torch.zeros(4, output_image.shape[1], output_image.shape[2], device=self.device)
|
||||
background[3, :, :] = 1.0 # set alpha to 1.0
|
||||
background[1, :, :] = 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
elif background_choice == 2: # Blue
|
||||
background = torch.zeros(4, output_image.shape[1], output_image.shape[2], device=self.device)
|
||||
background[3, :, :] = 1.0 # set alpha to 1.0
|
||||
background[2, :, :] = 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
elif background_choice == 3: # Black
|
||||
background = torch.zeros(4, output_image.shape[1], output_image.shape[2], device=self.device)
|
||||
background[3, :, :] = 1.0 # set alpha to 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
elif background_choice == 4: # White
|
||||
background = torch.zeros(4, output_image.shape[1], output_image.shape[2], device=self.device)
|
||||
background[3, :, :] = 1.0 # set alpha to 1.0
|
||||
background[0:3, :, :] = 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
elif background_choice == 5: # Saved Image
|
||||
output_image = self.blend_with_background(output_image, self.custom_background_image)
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
c, h, w = output_image.shape
|
||||
output_image = (255.0 * torch.transpose(output_image.reshape(c, h * w), 0, 1)).reshape(h, w, c).byte()
|
||||
|
||||
|
||||
numpy_image = output_image.detach().cpu().numpy()
|
||||
wx_image = wx.ImageFromBuffer(numpy_image.shape[0],
|
||||
numpy_image.shape[1],
|
||||
numpy_image[:, :, 0:3].tobytes(),
|
||||
numpy_image[:, :, 3].tobytes())
|
||||
wx_bitmap = wx_image.ConvertToBitmap()
|
||||
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.result_image_bitmap)
|
||||
dc.Clear()
|
||||
dc.DrawBitmap(wx_bitmap,
|
||||
(self.poser.get_image_size() - numpy_image.shape[0]) // 2,
|
||||
(self.poser.get_image_size() - numpy_image.shape[1]) // 2, True)
|
||||
|
||||
|
||||
# Assuming numpy_image has shape (height, width, 4) and the channels are in RGB order
|
||||
# Convert color channels from RGB to BGR and keep alpha channel
|
||||
numpy_image_bgra = numpy_image[:, :, [2, 1, 0, 3]]
|
||||
#cv2.imwrite('test2.png', numpy_image_bgra)
|
||||
|
||||
global_result_image = numpy_image_bgra
|
||||
|
||||
|
||||
del dc
|
||||
|
||||
time_now = time.time_ns()
|
||||
if self.last_update_time is not None:
|
||||
elapsed_time = time_now - self.last_update_time
|
||||
fps = 1.0 / (elapsed_time / 10**9)
|
||||
if self.torch_source_image is not None:
|
||||
self.fps_statistics.add_fps(fps)
|
||||
self.fps_text.SetLabelText("FPS = %0.2f" % self.fps_statistics.get_average_fps())
|
||||
self.last_update_time = time_now
|
||||
|
||||
self.Refresh()
|
||||
|
||||
def blend_with_background(self, numpy_image, background):
|
||||
if background is not None:
|
||||
alpha = numpy_image[3:4, :, :]
|
||||
color = numpy_image[0:3, :, :]
|
||||
new_color = color * alpha + (1.0 - alpha) * background[0:3, :, :]
|
||||
return torch.cat([new_color, background[3:4, :, :]], dim=0)
|
||||
else:
|
||||
return numpy_image
|
||||
|
||||
def resize_image(image, size=(512, 512)):
|
||||
image.thumbnail(size, Image.LANCZOS) # Step 1: Resize the image to maintain the aspect ratio with the larger dimension being 512 pixels
|
||||
new_image = Image.new("RGBA", size) # Step 2: Create a new image of size 512x512 with transparency
|
||||
new_image.paste(image, ((size[0] - image.size[0]) // 2,
|
||||
(size[1] - image.size[1]) // 2)) # Step 3: Paste the resized image into the new image, centered
|
||||
return new_image
|
||||
|
||||
def load_image(self, event: wx.Event, file_path=None):
|
||||
|
||||
global global_source_image # Declare global_source_image as a global variable
|
||||
global global_source_image_path # Declare global_source_image as a global variable
|
||||
global global_reload
|
||||
|
||||
if global_reload is not None:
|
||||
file_path = "global_reload"
|
||||
|
||||
#if file_path is None and global_reload is not None:
|
||||
|
||||
if file_path is None:
|
||||
dir_name = "data/images"
|
||||
file_dialog = wx.FileDialog(self, "Choose an image", dir_name, "", "*.png", wx.FD_OPEN)
|
||||
if file_dialog.ShowModal() == wx.ID_OK:
|
||||
file_path = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename())
|
||||
file_dialog.Destroy()
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
|
||||
|
||||
if file_path == "global_reload":
|
||||
pil_image = global_reload # use global_reload directly
|
||||
#print("Loading from Var")
|
||||
else:
|
||||
pil_image = resize_PIL_image(
|
||||
extract_PIL_image_from_filelike(file_path),
|
||||
(self.poser.get_image_size(), self.poser.get_image_size()))
|
||||
|
||||
|
||||
w, h = pil_image.size
|
||||
|
||||
if pil_image.size != (512, 512):
|
||||
print("Resizing Char Card to work")
|
||||
pil_image = MainFrame.resize_image(pil_image)
|
||||
|
||||
w, h = pil_image.size
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if pil_image.mode != 'RGBA':
|
||||
self.source_image_string = "Image must have alpha channel!"
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
else:
|
||||
self.wx_source_image = wx.Bitmap.FromBufferRGBA(w, h, pil_image.convert("RGBA").tobytes())
|
||||
self.torch_source_image = extract_pytorch_image_from_PIL_image(pil_image) \
|
||||
.to(self.device).to(self.poser.get_dtype())
|
||||
|
||||
global_source_image = self.torch_source_image # Set global_source_image as a global variable
|
||||
|
||||
global_source_image_path = image_path = os.path.join(file_path) #set file path
|
||||
|
||||
self.update_source_image_bitmap()
|
||||
|
||||
|
||||
|
||||
except Exception as error:
|
||||
print("Error:")
|
||||
print(error)
|
||||
#message_dialog = wx.MessageDialog(self, "Could not load image " + file_path, "Poser", wx.OK)
|
||||
#message_dialog.ShowModal()
|
||||
#message_dialog.Destroy()
|
||||
global_reload = None #reset the globe load
|
||||
#print("Reseting Load Variable")
|
||||
self.Refresh()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='uWu Waifu')
|
||||
parser.add_argument(
|
||||
'--model',
|
||||
type=str,
|
||||
required=False,
|
||||
default='separable_float',
|
||||
choices=['standard_float', 'separable_float', 'standard_half', 'separable_half'],
|
||||
help='The model to use.'
|
||||
)
|
||||
parser.add_argument('--char', type=str, required=False, help='The path to the character image.')
|
||||
parser.add_argument(
|
||||
'--device',
|
||||
type=str,
|
||||
required=False,
|
||||
default='cuda',
|
||||
choices=['cpu', 'cuda'],
|
||||
help='The device to use for PyTorch ("cuda" for GPU, "cpu" for CPU).'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
# Add the line below to pass the 'args' object to the launch_gui() function
|
||||
launch_gui(device=args.device, model=args.model)
|
||||
62
live2d/tha3/app/endpoints.save
Normal file
62
live2d/tha3/app/endpoints.save
Normal file
@@ -0,0 +1,62 @@
|
||||
@app.route("/load", methods=["POST"])
|
||||
def live2d_load():
|
||||
img = None
|
||||
global global_source_image
|
||||
global global_reload
|
||||
# get variable from POST data IE http://localhost:8000/characters/Aqua.png
|
||||
#curl -X POST -d "live2d_loadchar=http://localhost:8000/characters/Aqua.png" http://localhost:5555/load
|
||||
|
||||
live2d_loadchar = request.form.get('live2d_loadchar')
|
||||
print(live2d_loadchar)
|
||||
# get the image from the url at live2d_loadchar and load it into global_source_variable
|
||||
|
||||
#loads the /Name/live.png vs char Card
|
||||
url = live2d_loadchar.replace('.png', '/live2d.png')
|
||||
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
try:
|
||||
img = Image.open(BytesIO(response.content))
|
||||
except Image.UnidentifiedImageError:
|
||||
print(f"Could not identify image from URL: {url}")
|
||||
|
||||
global_reload = img
|
||||
return 'OK'
|
||||
|
||||
@app.route('/source_feed')
|
||||
def source_feed():
|
||||
return send_file(global_source_image_path, mimetype='image/png')
|
||||
|
||||
@app.route('/start_talking')
|
||||
def start_talking():
|
||||
global is_talking_override
|
||||
is_talking_override = True
|
||||
#return send_file(global_source_image_path, mimetype='image/png')
|
||||
return "started"
|
||||
|
||||
@app.route('/stop_talking')
|
||||
def stop_talking():
|
||||
global is_talking_override
|
||||
is_talking_override = False
|
||||
return "stopped"
|
||||
|
||||
@app.route('/result_feed')
|
||||
def result_feed():
|
||||
def generate():
|
||||
while True:
|
||||
if global_result_image is not None:
|
||||
|
||||
try:
|
||||
# Encode the numpy array to PNG
|
||||
_, buffer = cv2.imencode('.png', global_result_image)
|
||||
except Exception as e:
|
||||
print(f"Error when trying to write image: {e}")
|
||||
|
||||
# Send the PNG image
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/png\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
439
live2d/tha3/app/ifacialmocap_puppeteer.py
Normal file
439
live2d/tha3/app/ifacialmocap_puppeteer.py
Normal file
@@ -0,0 +1,439 @@
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
sys.path.append(os.getcwd())
|
||||
|
||||
from tha3.mocap.ifacialmocap_pose import create_default_ifacialmocap_pose
|
||||
from tha3.mocap.ifacialmocap_v2 import IFACIALMOCAP_PORT, IFACIALMOCAP_START_STRING, parse_ifacialmocap_v2_pose, \
|
||||
parse_ifacialmocap_v1_pose
|
||||
from tha3.poser.modes.load_poser import load_poser
|
||||
|
||||
import torch
|
||||
import wx
|
||||
|
||||
from tha3.poser.poser import Poser
|
||||
from tha3.mocap.ifacialmocap_constants import *
|
||||
from tha3.mocap.ifacialmocap_pose_converter import IFacialMocapPoseConverter
|
||||
from tha3.util import torch_linear_to_srgb, resize_PIL_image, extract_PIL_image_from_filelike, \
|
||||
extract_pytorch_image_from_PIL_image
|
||||
|
||||
|
||||
def convert_linear_to_srgb(image: torch.Tensor) -> torch.Tensor:
|
||||
rgb_image = torch_linear_to_srgb(image[0:3, :, :])
|
||||
return torch.cat([rgb_image, image[3:4, :, :]], dim=0)
|
||||
|
||||
|
||||
class FpsStatistics:
|
||||
def __init__(self):
|
||||
self.count = 100
|
||||
self.fps = []
|
||||
|
||||
def add_fps(self, fps):
|
||||
self.fps.append(fps)
|
||||
while len(self.fps) > self.count:
|
||||
del self.fps[0]
|
||||
|
||||
def get_average_fps(self):
|
||||
if len(self.fps) == 0:
|
||||
return 0.0
|
||||
else:
|
||||
return sum(self.fps) / len(self.fps)
|
||||
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
def __init__(self, poser: Poser, pose_converter: IFacialMocapPoseConverter, device: torch.device):
|
||||
super().__init__(None, wx.ID_ANY, "iFacialMocap Puppeteer (Marigold)")
|
||||
self.pose_converter = pose_converter
|
||||
self.poser = poser
|
||||
self.device = device
|
||||
|
||||
|
||||
self.ifacialmocap_pose = create_default_ifacialmocap_pose()
|
||||
self.source_image_bitmap = wx.Bitmap(self.poser.get_image_size(), self.poser.get_image_size())
|
||||
self.result_image_bitmap = wx.Bitmap(self.poser.get_image_size(), self.poser.get_image_size())
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
self.last_pose = None
|
||||
self.fps_statistics = FpsStatistics()
|
||||
self.last_update_time = None
|
||||
|
||||
self.create_receiving_socket()
|
||||
self.create_ui()
|
||||
self.create_timers()
|
||||
self.Bind(wx.EVT_CLOSE, self.on_close)
|
||||
|
||||
self.update_source_image_bitmap()
|
||||
self.update_result_image_bitmap()
|
||||
|
||||
def create_receiving_socket(self):
|
||||
self.receiving_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.receiving_socket.bind(("", IFACIALMOCAP_PORT))
|
||||
self.receiving_socket.setblocking(False)
|
||||
|
||||
def create_timers(self):
|
||||
self.capture_timer = wx.Timer(self, wx.ID_ANY)
|
||||
self.Bind(wx.EVT_TIMER, self.update_capture_panel, id=self.capture_timer.GetId())
|
||||
self.animation_timer = wx.Timer(self, wx.ID_ANY)
|
||||
self.Bind(wx.EVT_TIMER, self.update_result_image_bitmap, id=self.animation_timer.GetId())
|
||||
|
||||
def on_close(self, event: wx.Event):
|
||||
# Stop the timers
|
||||
self.animation_timer.Stop()
|
||||
self.capture_timer.Stop()
|
||||
|
||||
# Close receiving socket
|
||||
self.receiving_socket.close()
|
||||
|
||||
# Destroy the windows
|
||||
self.Destroy()
|
||||
event.Skip()
|
||||
|
||||
def on_start_capture(self, event: wx.Event):
|
||||
capture_device_ip_address = self.capture_device_ip_text_ctrl.GetValue()
|
||||
out_socket = None
|
||||
try:
|
||||
address = (capture_device_ip_address, IFACIALMOCAP_PORT)
|
||||
out_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
out_socket.sendto(IFACIALMOCAP_START_STRING, address)
|
||||
except Exception as e:
|
||||
message_dialog = wx.MessageDialog(self, str(e), "Error!", wx.OK)
|
||||
message_dialog.ShowModal()
|
||||
message_dialog.Destroy()
|
||||
finally:
|
||||
if out_socket is not None:
|
||||
out_socket.close()
|
||||
|
||||
def read_ifacialmocap_pose(self):
|
||||
if not self.animation_timer.IsRunning():
|
||||
return self.ifacialmocap_pose
|
||||
socket_bytes = None
|
||||
while True:
|
||||
try:
|
||||
socket_bytes = self.receiving_socket.recv(8192)
|
||||
except socket.error as e:
|
||||
break
|
||||
if socket_bytes is not None:
|
||||
socket_string = socket_bytes.decode("utf-8")
|
||||
self.ifacialmocap_pose = parse_ifacialmocap_v2_pose(socket_string)
|
||||
return self.ifacialmocap_pose
|
||||
|
||||
def on_erase_background(self, event: wx.Event):
|
||||
pass
|
||||
|
||||
def create_animation_panel(self, parent):
|
||||
self.animation_panel = wx.Panel(parent, style=wx.RAISED_BORDER)
|
||||
self.animation_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.animation_panel.SetSizer(self.animation_panel_sizer)
|
||||
self.animation_panel.SetAutoLayout(1)
|
||||
|
||||
image_size = self.poser.get_image_size()
|
||||
|
||||
if True:
|
||||
self.input_panel = wx.Panel(self.animation_panel, size=(image_size, image_size + 128),
|
||||
style=wx.SIMPLE_BORDER)
|
||||
self.input_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.input_panel.SetSizer(self.input_panel_sizer)
|
||||
self.input_panel.SetAutoLayout(1)
|
||||
self.animation_panel_sizer.Add(self.input_panel, 0, wx.FIXED_MINSIZE)
|
||||
|
||||
self.source_image_panel = wx.Panel(self.input_panel, size=(image_size, image_size), style=wx.SIMPLE_BORDER)
|
||||
self.source_image_panel.Bind(wx.EVT_PAINT, self.paint_source_image_panel)
|
||||
self.source_image_panel.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
|
||||
self.input_panel_sizer.Add(self.source_image_panel, 0, wx.FIXED_MINSIZE)
|
||||
|
||||
self.load_image_button = wx.Button(self.input_panel, wx.ID_ANY, "Load Image")
|
||||
self.input_panel_sizer.Add(self.load_image_button, 1, wx.EXPAND)
|
||||
self.load_image_button.Bind(wx.EVT_BUTTON, self.load_image)
|
||||
|
||||
self.input_panel_sizer.Fit(self.input_panel)
|
||||
|
||||
if True:
|
||||
self.pose_converter.init_pose_converter_panel(self.animation_panel)
|
||||
|
||||
if True:
|
||||
self.animation_left_panel = wx.Panel(self.animation_panel, style=wx.SIMPLE_BORDER)
|
||||
self.animation_left_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.animation_left_panel.SetSizer(self.animation_left_panel_sizer)
|
||||
self.animation_left_panel.SetAutoLayout(1)
|
||||
self.animation_panel_sizer.Add(self.animation_left_panel, 0, wx.EXPAND)
|
||||
|
||||
self.result_image_panel = wx.Panel(self.animation_left_panel, size=(image_size, image_size),
|
||||
style=wx.SIMPLE_BORDER)
|
||||
self.result_image_panel.Bind(wx.EVT_PAINT, self.paint_result_image_panel)
|
||||
self.result_image_panel.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
|
||||
self.animation_left_panel_sizer.Add(self.result_image_panel, 0, wx.FIXED_MINSIZE)
|
||||
|
||||
separator = wx.StaticLine(self.animation_left_panel, -1, size=(256, 5))
|
||||
self.animation_left_panel_sizer.Add(separator, 0, wx.EXPAND)
|
||||
|
||||
background_text = wx.StaticText(self.animation_left_panel, label="--- Background ---",
|
||||
style=wx.ALIGN_CENTER)
|
||||
self.animation_left_panel_sizer.Add(background_text, 0, wx.EXPAND)
|
||||
|
||||
self.output_background_choice = wx.Choice(
|
||||
self.animation_left_panel,
|
||||
choices=[
|
||||
"TRANSPARENT",
|
||||
"GREEN",
|
||||
"BLUE",
|
||||
"BLACK",
|
||||
"WHITE"
|
||||
])
|
||||
self.output_background_choice.SetSelection(0)
|
||||
self.animation_left_panel_sizer.Add(self.output_background_choice, 0, wx.EXPAND)
|
||||
|
||||
separator = wx.StaticLine(self.animation_left_panel, -1, size=(256, 5))
|
||||
self.animation_left_panel_sizer.Add(separator, 0, wx.EXPAND)
|
||||
|
||||
self.fps_text = wx.StaticText(self.animation_left_panel, label="")
|
||||
self.animation_left_panel_sizer.Add(self.fps_text, wx.SizerFlags().Border())
|
||||
|
||||
self.animation_left_panel_sizer.Fit(self.animation_left_panel)
|
||||
|
||||
self.animation_panel_sizer.Fit(self.animation_panel)
|
||||
|
||||
def create_ui(self):
|
||||
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.SetSizer(self.main_sizer)
|
||||
self.SetAutoLayout(1)
|
||||
|
||||
self.capture_pose_lock = threading.Lock()
|
||||
|
||||
self.create_connection_panel(self)
|
||||
self.main_sizer.Add(self.connection_panel, wx.SizerFlags(0).Expand().Border(wx.ALL, 5))
|
||||
|
||||
self.create_animation_panel(self)
|
||||
self.main_sizer.Add(self.animation_panel, wx.SizerFlags(0).Expand().Border(wx.ALL, 5))
|
||||
|
||||
self.create_capture_panel(self)
|
||||
self.main_sizer.Add(self.capture_panel, wx.SizerFlags(0).Expand().Border(wx.ALL, 5))
|
||||
|
||||
self.main_sizer.Fit(self)
|
||||
|
||||
def create_connection_panel(self, parent):
|
||||
self.connection_panel = wx.Panel(parent, style=wx.RAISED_BORDER)
|
||||
self.connection_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.connection_panel.SetSizer(self.connection_panel_sizer)
|
||||
self.connection_panel.SetAutoLayout(1)
|
||||
|
||||
capture_device_ip_text = wx.StaticText(self.connection_panel, label="Capture Device IP:", style=wx.ALIGN_RIGHT)
|
||||
self.connection_panel_sizer.Add(capture_device_ip_text, wx.SizerFlags(0).FixedMinSize().Border(wx.ALL, 3))
|
||||
|
||||
self.capture_device_ip_text_ctrl = wx.TextCtrl(self.connection_panel, value="192.168.0.1")
|
||||
self.connection_panel_sizer.Add(self.capture_device_ip_text_ctrl, wx.SizerFlags(1).Expand().Border(wx.ALL, 3))
|
||||
|
||||
self.start_capture_button = wx.Button(self.connection_panel, label="START CAPTURE!")
|
||||
self.connection_panel_sizer.Add(self.start_capture_button, wx.SizerFlags(0).FixedMinSize().Border(wx.ALL, 3))
|
||||
self.start_capture_button.Bind(wx.EVT_BUTTON, self.on_start_capture)
|
||||
|
||||
def create_capture_panel(self, parent):
|
||||
self.capture_panel = wx.Panel(parent, style=wx.RAISED_BORDER)
|
||||
self.capture_panel_sizer = wx.FlexGridSizer(cols=5)
|
||||
for i in range(5):
|
||||
self.capture_panel_sizer.AddGrowableCol(i)
|
||||
self.capture_panel.SetSizer(self.capture_panel_sizer)
|
||||
self.capture_panel.SetAutoLayout(1)
|
||||
|
||||
self.rotation_labels = {}
|
||||
self.rotation_value_labels = {}
|
||||
rotation_column_0 = self.create_rotation_column(self.capture_panel, RIGHT_EYE_BONE_ROTATIONS)
|
||||
self.capture_panel_sizer.Add(rotation_column_0, wx.SizerFlags(0).Expand().Border(wx.ALL, 3))
|
||||
rotation_column_1 = self.create_rotation_column(self.capture_panel, LEFT_EYE_BONE_ROTATIONS)
|
||||
self.capture_panel_sizer.Add(rotation_column_1, wx.SizerFlags(0).Expand().Border(wx.ALL, 3))
|
||||
rotation_column_2 = self.create_rotation_column(self.capture_panel, HEAD_BONE_ROTATIONS)
|
||||
self.capture_panel_sizer.Add(rotation_column_2, wx.SizerFlags(0).Expand().Border(wx.ALL, 3))
|
||||
|
||||
def create_rotation_column(self, parent, rotation_names):
|
||||
column_panel = wx.Panel(parent, style=wx.SIMPLE_BORDER)
|
||||
column_panel_sizer = wx.FlexGridSizer(cols=2)
|
||||
column_panel_sizer.AddGrowableCol(1)
|
||||
column_panel.SetSizer(column_panel_sizer)
|
||||
column_panel.SetAutoLayout(1)
|
||||
|
||||
for rotation_name in rotation_names:
|
||||
self.rotation_labels[rotation_name] = wx.StaticText(
|
||||
column_panel, label=rotation_name, style=wx.ALIGN_RIGHT)
|
||||
column_panel_sizer.Add(self.rotation_labels[rotation_name],
|
||||
wx.SizerFlags(1).Expand().Border(wx.ALL, 3))
|
||||
|
||||
self.rotation_value_labels[rotation_name] = wx.TextCtrl(
|
||||
column_panel, style=wx.TE_RIGHT)
|
||||
self.rotation_value_labels[rotation_name].SetValue("0.00")
|
||||
self.rotation_value_labels[rotation_name].Disable()
|
||||
column_panel_sizer.Add(self.rotation_value_labels[rotation_name],
|
||||
wx.SizerFlags(1).Expand().Border(wx.ALL, 3))
|
||||
|
||||
column_panel.GetSizer().Fit(column_panel)
|
||||
return column_panel
|
||||
|
||||
def paint_capture_panel(self, event: wx.Event):
|
||||
self.update_capture_panel(event)
|
||||
|
||||
def update_capture_panel(self, event: wx.Event):
|
||||
data = self.ifacialmocap_pose
|
||||
for rotation_name in ROTATION_NAMES:
|
||||
value = data[rotation_name]
|
||||
self.rotation_value_labels[rotation_name].SetValue("%0.2f" % value)
|
||||
|
||||
@staticmethod
|
||||
def convert_to_100(x):
|
||||
return int(max(0.0, min(1.0, x)) * 100)
|
||||
|
||||
def paint_source_image_panel(self, event: wx.Event):
|
||||
wx.BufferedPaintDC(self.source_image_panel, self.source_image_bitmap)
|
||||
|
||||
def update_source_image_bitmap(self):
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.source_image_bitmap)
|
||||
if self.wx_source_image is None:
|
||||
self.draw_nothing_yet_string(dc)
|
||||
else:
|
||||
dc.Clear()
|
||||
dc.DrawBitmap(self.wx_source_image, 0, 0, True)
|
||||
del dc
|
||||
|
||||
def draw_nothing_yet_string(self, dc):
|
||||
dc.Clear()
|
||||
font = wx.Font(wx.FontInfo(14).Family(wx.FONTFAMILY_SWISS))
|
||||
dc.SetFont(font)
|
||||
w, h = dc.GetTextExtent("Nothing yet!")
|
||||
dc.DrawText("Nothing yet!", (self.poser.get_image_size() - w) // 2, (self.poser.get_image_size() - h) // 2)
|
||||
|
||||
def paint_result_image_panel(self, event: wx.Event):
|
||||
wx.BufferedPaintDC(self.result_image_panel, self.result_image_bitmap)
|
||||
|
||||
def update_result_image_bitmap(self, event: Optional[wx.Event] = None):
|
||||
ifacialmocap_pose = self.read_ifacialmocap_pose()
|
||||
current_pose = self.pose_converter.convert(ifacialmocap_pose)
|
||||
if self.last_pose is not None and self.last_pose == current_pose:
|
||||
return
|
||||
self.last_pose = current_pose
|
||||
|
||||
if self.torch_source_image is None:
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.result_image_bitmap)
|
||||
self.draw_nothing_yet_string(dc)
|
||||
del dc
|
||||
return
|
||||
|
||||
pose = torch.tensor(current_pose, device=self.device, dtype=self.poser.get_dtype())
|
||||
|
||||
with torch.no_grad():
|
||||
output_image = self.poser.pose(self.torch_source_image, pose)[0].float()
|
||||
output_image = convert_linear_to_srgb((output_image + 1.0) / 2.0)
|
||||
|
||||
background_choice = self.output_background_choice.GetSelection()
|
||||
if background_choice == 0:
|
||||
pass
|
||||
else:
|
||||
background = torch.zeros(4, output_image.shape[1], output_image.shape[2], device=self.device)
|
||||
background[3, :, :] = 1.0
|
||||
if background_choice == 1:
|
||||
background[1, :, :] = 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
elif background_choice == 2:
|
||||
background[2, :, :] = 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
elif background_choice == 3:
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
else:
|
||||
background[0:3, :, :] = 1.0
|
||||
output_image = self.blend_with_background(output_image, background)
|
||||
|
||||
c, h, w = output_image.shape
|
||||
output_image = 255.0 * torch.transpose(output_image.reshape(c, h * w), 0, 1).reshape(h, w, c)
|
||||
output_image = output_image.byte()
|
||||
|
||||
numpy_image = output_image.detach().cpu().numpy()
|
||||
wx_image = wx.ImageFromBuffer(numpy_image.shape[0],
|
||||
numpy_image.shape[1],
|
||||
numpy_image[:, :, 0:3].tobytes(),
|
||||
numpy_image[:, :, 3].tobytes())
|
||||
wx_bitmap = wx_image.ConvertToBitmap()
|
||||
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.result_image_bitmap)
|
||||
dc.Clear()
|
||||
dc.DrawBitmap(wx_bitmap,
|
||||
(self.poser.get_image_size() - numpy_image.shape[0]) // 2,
|
||||
(self.poser.get_image_size() - numpy_image.shape[1]) // 2, True)
|
||||
del dc
|
||||
|
||||
time_now = time.time_ns()
|
||||
if self.last_update_time is not None:
|
||||
elapsed_time = time_now - self.last_update_time
|
||||
fps = 1.0 / (elapsed_time / 10**9)
|
||||
if self.torch_source_image is not None:
|
||||
self.fps_statistics.add_fps(fps)
|
||||
self.fps_text.SetLabelText("FPS = %0.2f" % self.fps_statistics.get_average_fps())
|
||||
self.last_update_time = time_now
|
||||
|
||||
self.Refresh()
|
||||
|
||||
def blend_with_background(self, numpy_image, background):
|
||||
alpha = numpy_image[3:4, :, :]
|
||||
color = numpy_image[0:3, :, :]
|
||||
new_color = color * alpha + (1.0 - alpha) * background[0:3, :, :]
|
||||
return torch.cat([new_color, background[3:4, :, :]], dim=0)
|
||||
|
||||
def load_image(self, event: wx.Event):
|
||||
dir_name = "data/images"
|
||||
file_dialog = wx.FileDialog(self, "Choose an image", dir_name, "", "*.png", wx.FD_OPEN)
|
||||
if file_dialog.ShowModal() == wx.ID_OK:
|
||||
image_file_name = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename())
|
||||
try:
|
||||
pil_image = resize_PIL_image(
|
||||
extract_PIL_image_from_filelike(image_file_name),
|
||||
(self.poser.get_image_size(), self.poser.get_image_size()))
|
||||
w, h = pil_image.size
|
||||
if pil_image.mode != 'RGBA':
|
||||
self.source_image_string = "Image must have alpha channel!"
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
else:
|
||||
self.wx_source_image = wx.Bitmap.FromBufferRGBA(w, h, pil_image.convert("RGBA").tobytes())
|
||||
self.torch_source_image = extract_pytorch_image_from_PIL_image(pil_image) \
|
||||
.to(self.device).to(self.poser.get_dtype())
|
||||
self.update_source_image_bitmap()
|
||||
except:
|
||||
message_dialog = wx.MessageDialog(self, "Could not load image " + image_file_name, "Poser", wx.OK)
|
||||
message_dialog.ShowModal()
|
||||
message_dialog.Destroy()
|
||||
file_dialog.Destroy()
|
||||
self.Refresh()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Control characters with movement captured by iFacialMocap.')
|
||||
parser.add_argument(
|
||||
'--model',
|
||||
type=str,
|
||||
required=False,
|
||||
default='standard_float',
|
||||
choices=['standard_float', 'separable_float', 'standard_half', 'separable_half'],
|
||||
help='The model to use.')
|
||||
args = parser.parse_args()
|
||||
|
||||
device = torch.device('cuda')
|
||||
try:
|
||||
poser = load_poser(args.model, device)
|
||||
except RuntimeError as e:
|
||||
print(e)
|
||||
sys.exit()
|
||||
|
||||
from tha3.mocap.ifacialmocap_poser_converter_25 import create_ifacialmocap_pose_converter
|
||||
|
||||
pose_converter = create_ifacialmocap_pose_converter()
|
||||
|
||||
app = wx.App()
|
||||
main_frame = MainFrame(poser, pose_converter, device)
|
||||
main_frame.Show(True)
|
||||
main_frame.capture_timer.Start(10)
|
||||
main_frame.animation_timer.Start(10)
|
||||
app.MainLoop()
|
||||
464
live2d/tha3/app/manual_poser.py
Normal file
464
live2d/tha3/app/manual_poser.py
Normal file
@@ -0,0 +1,464 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
sys.path.append(os.getcwd())
|
||||
|
||||
import PIL.Image
|
||||
import numpy
|
||||
import torch
|
||||
import wx
|
||||
|
||||
from tha3.poser.modes.load_poser import load_poser
|
||||
from tha3.poser.poser import Poser, PoseParameterCategory, PoseParameterGroup
|
||||
from tha3.util import extract_pytorch_image_from_filelike, rgba_to_numpy_image, grid_change_to_numpy_image, \
|
||||
rgb_to_numpy_image, resize_PIL_image, extract_PIL_image_from_filelike, extract_pytorch_image_from_PIL_image
|
||||
|
||||
|
||||
class MorphCategoryControlPanel(wx.Panel):
|
||||
def __init__(self,
|
||||
parent,
|
||||
title: str,
|
||||
pose_param_category: PoseParameterCategory,
|
||||
param_groups: List[PoseParameterGroup]):
|
||||
super().__init__(parent, style=wx.SIMPLE_BORDER)
|
||||
self.pose_param_category = pose_param_category
|
||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.SetSizer(self.sizer)
|
||||
self.SetAutoLayout(1)
|
||||
|
||||
title_text = wx.StaticText(self, label=title, style=wx.ALIGN_CENTER)
|
||||
self.sizer.Add(title_text, 0, wx.EXPAND)
|
||||
|
||||
self.param_groups = [group for group in param_groups if group.get_category() == pose_param_category]
|
||||
self.choice = wx.Choice(self, choices=[group.get_group_name() for group in self.param_groups])
|
||||
if len(self.param_groups) > 0:
|
||||
self.choice.SetSelection(0)
|
||||
self.choice.Bind(wx.EVT_CHOICE, self.on_choice_updated)
|
||||
self.sizer.Add(self.choice, 0, wx.EXPAND)
|
||||
|
||||
self.left_slider = wx.Slider(self, minValue=-1000, maxValue=1000, value=-1000, style=wx.HORIZONTAL)
|
||||
self.sizer.Add(self.left_slider, 0, wx.EXPAND)
|
||||
|
||||
self.right_slider = wx.Slider(self, minValue=-1000, maxValue=1000, value=-1000, style=wx.HORIZONTAL)
|
||||
self.sizer.Add(self.right_slider, 0, wx.EXPAND)
|
||||
|
||||
self.checkbox = wx.CheckBox(self, label="Show")
|
||||
self.checkbox.SetValue(True)
|
||||
self.sizer.Add(self.checkbox, 0, wx.SHAPED | wx.ALIGN_CENTER)
|
||||
|
||||
self.update_ui()
|
||||
|
||||
self.sizer.Fit(self)
|
||||
|
||||
def update_ui(self):
|
||||
param_group = self.param_groups[self.choice.GetSelection()]
|
||||
if param_group.is_discrete():
|
||||
self.left_slider.Enable(False)
|
||||
self.right_slider.Enable(False)
|
||||
self.checkbox.Enable(True)
|
||||
elif param_group.get_arity() == 1:
|
||||
self.left_slider.Enable(True)
|
||||
self.right_slider.Enable(False)
|
||||
self.checkbox.Enable(False)
|
||||
else:
|
||||
self.left_slider.Enable(True)
|
||||
self.right_slider.Enable(True)
|
||||
self.checkbox.Enable(False)
|
||||
|
||||
def on_choice_updated(self, event: wx.Event):
|
||||
param_group = self.param_groups[self.choice.GetSelection()]
|
||||
if param_group.is_discrete():
|
||||
self.checkbox.SetValue(True)
|
||||
self.update_ui()
|
||||
|
||||
def set_param_value(self, pose: List[float]):
|
||||
if len(self.param_groups) == 0:
|
||||
return
|
||||
selected_morph_index = self.choice.GetSelection()
|
||||
param_group = self.param_groups[selected_morph_index]
|
||||
param_index = param_group.get_parameter_index()
|
||||
if param_group.is_discrete():
|
||||
if self.checkbox.GetValue():
|
||||
for i in range(param_group.get_arity()):
|
||||
pose[param_index + i] = 1.0
|
||||
else:
|
||||
param_range = param_group.get_range()
|
||||
alpha = (self.left_slider.GetValue() + 1000) / 2000.0
|
||||
pose[param_index] = param_range[0] + (param_range[1] - param_range[0]) * alpha
|
||||
if param_group.get_arity() == 2:
|
||||
alpha = (self.right_slider.GetValue() + 1000) / 2000.0
|
||||
pose[param_index + 1] = param_range[0] + (param_range[1] - param_range[0]) * alpha
|
||||
|
||||
|
||||
class SimpleParamGroupsControlPanel(wx.Panel):
|
||||
def __init__(self, parent,
|
||||
pose_param_category: PoseParameterCategory,
|
||||
param_groups: List[PoseParameterGroup]):
|
||||
super().__init__(parent, style=wx.SIMPLE_BORDER)
|
||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.SetSizer(self.sizer)
|
||||
self.SetAutoLayout(1)
|
||||
|
||||
self.param_groups = [group for group in param_groups if group.get_category() == pose_param_category]
|
||||
for param_group in self.param_groups:
|
||||
assert not param_group.is_discrete()
|
||||
assert param_group.get_arity() == 1
|
||||
|
||||
self.sliders = []
|
||||
for param_group in self.param_groups:
|
||||
static_text = wx.StaticText(
|
||||
self,
|
||||
label=" ------------ %s ------------ " % param_group.get_group_name(), style=wx.ALIGN_CENTER)
|
||||
self.sizer.Add(static_text, 0, wx.EXPAND)
|
||||
range = param_group.get_range()
|
||||
min_value = int(range[0] * 1000)
|
||||
max_value = int(range[1] * 1000)
|
||||
slider = wx.Slider(self, minValue=min_value, maxValue=max_value, value=0, style=wx.HORIZONTAL)
|
||||
self.sizer.Add(slider, 0, wx.EXPAND)
|
||||
self.sliders.append(slider)
|
||||
|
||||
self.sizer.Fit(self)
|
||||
|
||||
def set_param_value(self, pose: List[float]):
|
||||
if len(self.param_groups) == 0:
|
||||
return
|
||||
for param_group_index in range(len(self.param_groups)):
|
||||
param_group = self.param_groups[param_group_index]
|
||||
slider = self.sliders[param_group_index]
|
||||
param_range = param_group.get_range()
|
||||
param_index = param_group.get_parameter_index()
|
||||
alpha = (slider.GetValue() - slider.GetMin()) * 1.0 / (slider.GetMax() - slider.GetMin())
|
||||
pose[param_index] = param_range[0] + (param_range[1] - param_range[0]) * alpha
|
||||
|
||||
|
||||
def convert_output_image_from_torch_to_numpy(output_image):
|
||||
if output_image.shape[2] == 2:
|
||||
h, w, c = output_image.shape
|
||||
numpy_image = torch.transpose(output_image.reshape(h * w, c), 0, 1).reshape(c, h, w)
|
||||
elif output_image.shape[0] == 4:
|
||||
numpy_image = rgba_to_numpy_image(output_image)
|
||||
elif output_image.shape[0] == 3:
|
||||
numpy_image = rgb_to_numpy_image(output_image)
|
||||
elif output_image.shape[0] == 1:
|
||||
c, h, w = output_image.shape
|
||||
alpha_image = torch.cat([output_image.repeat(3, 1, 1) * 2.0 - 1.0, torch.ones(1, h, w)], dim=0)
|
||||
numpy_image = rgba_to_numpy_image(alpha_image)
|
||||
elif output_image.shape[0] == 2:
|
||||
numpy_image = grid_change_to_numpy_image(output_image, num_channels=4)
|
||||
else:
|
||||
raise RuntimeError("Unsupported # image channels: %d" % output_image.shape[0])
|
||||
numpy_image = numpy.uint8(numpy.rint(numpy_image * 255.0))
|
||||
return numpy_image
|
||||
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
def __init__(self, poser: Poser, device: torch.device):
|
||||
super().__init__(None, wx.ID_ANY, "Poser")
|
||||
self.poser = poser
|
||||
self.dtype = self.poser.get_dtype()
|
||||
self.device = device
|
||||
self.image_size = self.poser.get_image_size()
|
||||
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
|
||||
self.main_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.SetSizer(self.main_sizer)
|
||||
self.SetAutoLayout(1)
|
||||
self.init_left_panel()
|
||||
self.init_control_panel()
|
||||
self.init_right_panel()
|
||||
self.main_sizer.Fit(self)
|
||||
|
||||
self.timer = wx.Timer(self, wx.ID_ANY)
|
||||
self.Bind(wx.EVT_TIMER, self.update_images, self.timer)
|
||||
|
||||
save_image_id = wx.NewIdRef()
|
||||
self.Bind(wx.EVT_MENU, self.on_save_image, id=save_image_id)
|
||||
accelerator_table = wx.AcceleratorTable([
|
||||
(wx.ACCEL_CTRL, ord('S'), save_image_id)
|
||||
])
|
||||
self.SetAcceleratorTable(accelerator_table)
|
||||
|
||||
self.last_pose = None
|
||||
self.last_output_index = self.output_index_choice.GetSelection()
|
||||
self.last_output_numpy_image = None
|
||||
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
self.source_image_bitmap = wx.Bitmap(self.image_size, self.image_size)
|
||||
self.result_image_bitmap = wx.Bitmap(self.image_size, self.image_size)
|
||||
self.source_image_dirty = True
|
||||
|
||||
def init_left_panel(self):
|
||||
self.control_panel = wx.Panel(self, style=wx.SIMPLE_BORDER, size=(self.image_size, -1))
|
||||
self.left_panel = wx.Panel(self, style=wx.SIMPLE_BORDER)
|
||||
left_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.left_panel.SetSizer(left_panel_sizer)
|
||||
self.left_panel.SetAutoLayout(1)
|
||||
|
||||
self.source_image_panel = wx.Panel(self.left_panel, size=(self.image_size, self.image_size),
|
||||
style=wx.SIMPLE_BORDER)
|
||||
self.source_image_panel.Bind(wx.EVT_PAINT, self.paint_source_image_panel)
|
||||
self.source_image_panel.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
|
||||
left_panel_sizer.Add(self.source_image_panel, 0, wx.FIXED_MINSIZE)
|
||||
|
||||
self.load_image_button = wx.Button(self.left_panel, wx.ID_ANY, "\nLoad Image\n\n")
|
||||
left_panel_sizer.Add(self.load_image_button, 1, wx.EXPAND)
|
||||
self.load_image_button.Bind(wx.EVT_BUTTON, self.load_image)
|
||||
|
||||
left_panel_sizer.Fit(self.left_panel)
|
||||
self.main_sizer.Add(self.left_panel, 0, wx.FIXED_MINSIZE)
|
||||
|
||||
def on_erase_background(self, event: wx.Event):
|
||||
pass
|
||||
|
||||
def init_control_panel(self):
|
||||
self.control_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.control_panel.SetSizer(self.control_panel_sizer)
|
||||
self.control_panel.SetMinSize(wx.Size(256, 1))
|
||||
|
||||
morph_categories = [
|
||||
PoseParameterCategory.EYEBROW,
|
||||
PoseParameterCategory.EYE,
|
||||
PoseParameterCategory.MOUTH,
|
||||
PoseParameterCategory.IRIS_MORPH
|
||||
]
|
||||
morph_category_titles = {
|
||||
PoseParameterCategory.EYEBROW: " ------------ Eyebrow ------------ ",
|
||||
PoseParameterCategory.EYE: " ------------ Eye ------------ ",
|
||||
PoseParameterCategory.MOUTH: " ------------ Mouth ------------ ",
|
||||
PoseParameterCategory.IRIS_MORPH: " ------------ Iris morphs ------------ ",
|
||||
}
|
||||
self.morph_control_panels = {}
|
||||
for category in morph_categories:
|
||||
param_groups = self.poser.get_pose_parameter_groups()
|
||||
filtered_param_groups = [group for group in param_groups if group.get_category() == category]
|
||||
if len(filtered_param_groups) == 0:
|
||||
continue
|
||||
control_panel = MorphCategoryControlPanel(
|
||||
self.control_panel,
|
||||
morph_category_titles[category],
|
||||
category,
|
||||
self.poser.get_pose_parameter_groups())
|
||||
self.morph_control_panels[category] = control_panel
|
||||
self.control_panel_sizer.Add(control_panel, 0, wx.EXPAND)
|
||||
|
||||
self.non_morph_control_panels = {}
|
||||
non_morph_categories = [
|
||||
PoseParameterCategory.IRIS_ROTATION,
|
||||
PoseParameterCategory.FACE_ROTATION,
|
||||
PoseParameterCategory.BODY_ROTATION,
|
||||
PoseParameterCategory.BREATHING
|
||||
]
|
||||
for category in non_morph_categories:
|
||||
param_groups = self.poser.get_pose_parameter_groups()
|
||||
filtered_param_groups = [group for group in param_groups if group.get_category() == category]
|
||||
if len(filtered_param_groups) == 0:
|
||||
continue
|
||||
control_panel = SimpleParamGroupsControlPanel(
|
||||
self.control_panel,
|
||||
category,
|
||||
self.poser.get_pose_parameter_groups())
|
||||
self.non_morph_control_panels[category] = control_panel
|
||||
self.control_panel_sizer.Add(control_panel, 0, wx.EXPAND)
|
||||
|
||||
self.control_panel_sizer.Fit(self.control_panel)
|
||||
self.main_sizer.Add(self.control_panel, 1, wx.FIXED_MINSIZE)
|
||||
|
||||
def init_right_panel(self):
|
||||
self.right_panel = wx.Panel(self, style=wx.SIMPLE_BORDER)
|
||||
right_panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.right_panel.SetSizer(right_panel_sizer)
|
||||
self.right_panel.SetAutoLayout(1)
|
||||
|
||||
self.result_image_panel = wx.Panel(self.right_panel,
|
||||
size=(self.image_size, self.image_size),
|
||||
style=wx.SIMPLE_BORDER)
|
||||
self.result_image_panel.Bind(wx.EVT_PAINT, self.paint_result_image_panel)
|
||||
self.result_image_panel.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
|
||||
self.output_index_choice = wx.Choice(
|
||||
self.right_panel,
|
||||
choices=[str(i) for i in range(self.poser.get_output_length())])
|
||||
self.output_index_choice.SetSelection(0)
|
||||
right_panel_sizer.Add(self.result_image_panel, 0, wx.FIXED_MINSIZE)
|
||||
right_panel_sizer.Add(self.output_index_choice, 0, wx.EXPAND)
|
||||
|
||||
self.save_image_button = wx.Button(self.right_panel, wx.ID_ANY, "\nSave Image\n\n")
|
||||
right_panel_sizer.Add(self.save_image_button, 1, wx.EXPAND)
|
||||
self.save_image_button.Bind(wx.EVT_BUTTON, self.on_save_image)
|
||||
|
||||
right_panel_sizer.Fit(self.right_panel)
|
||||
self.main_sizer.Add(self.right_panel, 0, wx.FIXED_MINSIZE)
|
||||
|
||||
def create_param_category_choice(self, param_category: PoseParameterCategory):
|
||||
params = []
|
||||
for param_group in self.poser.get_pose_parameter_groups():
|
||||
if param_group.get_category() == param_category:
|
||||
params.append(param_group.get_group_name())
|
||||
choice = wx.Choice(self.control_panel, choices=params)
|
||||
if len(params) > 0:
|
||||
choice.SetSelection(0)
|
||||
return choice
|
||||
|
||||
def load_image(self, event: wx.Event):
|
||||
dir_name = "data/images"
|
||||
file_dialog = wx.FileDialog(self, "Choose an image", dir_name, "", "*.png", wx.FD_OPEN)
|
||||
if file_dialog.ShowModal() == wx.ID_OK:
|
||||
image_file_name = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename())
|
||||
try:
|
||||
pil_image = resize_PIL_image(extract_PIL_image_from_filelike(image_file_name),
|
||||
(self.poser.get_image_size(), self.poser.get_image_size()))
|
||||
w, h = pil_image.size
|
||||
if pil_image.mode != 'RGBA':
|
||||
self.source_image_string = "Image must have alpha channel!"
|
||||
self.wx_source_image = None
|
||||
self.torch_source_image = None
|
||||
else:
|
||||
self.wx_source_image = wx.Bitmap.FromBufferRGBA(w, h, pil_image.convert("RGBA").tobytes())
|
||||
self.torch_source_image = extract_pytorch_image_from_PIL_image(pil_image)\
|
||||
.to(self.device).to(self.dtype)
|
||||
self.source_image_dirty = True
|
||||
self.Refresh()
|
||||
self.Update()
|
||||
except:
|
||||
message_dialog = wx.MessageDialog(self, "Could not load image " + image_file_name, "Poser", wx.OK)
|
||||
message_dialog.ShowModal()
|
||||
message_dialog.Destroy()
|
||||
file_dialog.Destroy()
|
||||
|
||||
def paint_source_image_panel(self, event: wx.Event):
|
||||
wx.BufferedPaintDC(self.source_image_panel, self.source_image_bitmap)
|
||||
|
||||
def paint_result_image_panel(self, event: wx.Event):
|
||||
wx.BufferedPaintDC(self.result_image_panel, self.result_image_bitmap)
|
||||
|
||||
def draw_nothing_yet_string_to_bitmap(self, bitmap):
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(bitmap)
|
||||
|
||||
dc.Clear()
|
||||
font = wx.Font(wx.FontInfo(14).Family(wx.FONTFAMILY_SWISS))
|
||||
dc.SetFont(font)
|
||||
w, h = dc.GetTextExtent("Nothing yet!")
|
||||
dc.DrawText("Nothing yet!", (self.image_size - w) // 2, (self.image_size - - h) // 2)
|
||||
|
||||
del dc
|
||||
|
||||
def get_current_pose(self):
|
||||
current_pose = [0.0 for i in range(self.poser.get_num_parameters())]
|
||||
for morph_control_panel in self.morph_control_panels.values():
|
||||
morph_control_panel.set_param_value(current_pose)
|
||||
for rotation_control_panel in self.non_morph_control_panels.values():
|
||||
rotation_control_panel.set_param_value(current_pose)
|
||||
return current_pose
|
||||
|
||||
def update_images(self, event: wx.Event):
|
||||
current_pose = self.get_current_pose()
|
||||
if not self.source_image_dirty \
|
||||
and self.last_pose is not None \
|
||||
and self.last_pose == current_pose \
|
||||
and self.last_output_index == self.output_index_choice.GetSelection():
|
||||
return
|
||||
self.last_pose = current_pose
|
||||
self.last_output_index = self.output_index_choice.GetSelection()
|
||||
|
||||
if self.torch_source_image is None:
|
||||
self.draw_nothing_yet_string_to_bitmap(self.source_image_bitmap)
|
||||
self.draw_nothing_yet_string_to_bitmap(self.result_image_bitmap)
|
||||
self.source_image_dirty = False
|
||||
self.Refresh()
|
||||
self.Update()
|
||||
return
|
||||
|
||||
if self.source_image_dirty:
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.source_image_bitmap)
|
||||
dc.Clear()
|
||||
dc.DrawBitmap(self.wx_source_image, 0, 0)
|
||||
self.source_image_dirty = False
|
||||
|
||||
pose = torch.tensor(current_pose, device=self.device, dtype=self.dtype)
|
||||
output_index = self.output_index_choice.GetSelection()
|
||||
with torch.no_grad():
|
||||
output_image = self.poser.pose(self.torch_source_image, pose, output_index)[0].detach().cpu()
|
||||
|
||||
numpy_image = convert_output_image_from_torch_to_numpy(output_image)
|
||||
self.last_output_numpy_image = numpy_image
|
||||
wx_image = wx.ImageFromBuffer(
|
||||
numpy_image.shape[0],
|
||||
numpy_image.shape[1],
|
||||
numpy_image[:, :, 0:3].tobytes(),
|
||||
numpy_image[:, :, 3].tobytes())
|
||||
wx_bitmap = wx_image.ConvertToBitmap()
|
||||
|
||||
dc = wx.MemoryDC()
|
||||
dc.SelectObject(self.result_image_bitmap)
|
||||
dc.Clear()
|
||||
dc.DrawBitmap(wx_bitmap,
|
||||
(self.image_size - numpy_image.shape[0]) // 2,
|
||||
(self.image_size - numpy_image.shape[1]) // 2,
|
||||
True)
|
||||
del dc
|
||||
|
||||
self.Refresh()
|
||||
self.Update()
|
||||
|
||||
def on_save_image(self, event: wx.Event):
|
||||
if self.last_output_numpy_image is None:
|
||||
logging.info("There is no output image to save!!!")
|
||||
return
|
||||
|
||||
dir_name = "data/images"
|
||||
file_dialog = wx.FileDialog(self, "Choose an image", dir_name, "", "*.png", wx.FD_SAVE)
|
||||
if file_dialog.ShowModal() == wx.ID_OK:
|
||||
image_file_name = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename())
|
||||
try:
|
||||
if os.path.exists(image_file_name):
|
||||
message_dialog = wx.MessageDialog(self, f"Override {image_file_name}", "Manual Poser",
|
||||
wx.YES_NO | wx.ICON_QUESTION)
|
||||
result = message_dialog.ShowModal()
|
||||
if result == wx.ID_YES:
|
||||
self.save_last_numpy_image(image_file_name)
|
||||
message_dialog.Destroy()
|
||||
else:
|
||||
self.save_last_numpy_image(image_file_name)
|
||||
except:
|
||||
message_dialog = wx.MessageDialog(self, f"Could not save {image_file_name}", "Manual Poser", wx.OK)
|
||||
message_dialog.ShowModal()
|
||||
message_dialog.Destroy()
|
||||
file_dialog.Destroy()
|
||||
|
||||
def save_last_numpy_image(self, image_file_name):
|
||||
numpy_image = self.last_output_numpy_image
|
||||
pil_image = PIL.Image.fromarray(numpy_image, mode='RGBA')
|
||||
os.makedirs(os.path.dirname(image_file_name), exist_ok=True)
|
||||
pil_image.save(image_file_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Manually pose a character image.')
|
||||
parser.add_argument(
|
||||
'--model',
|
||||
type=str,
|
||||
required=False,
|
||||
default='standard_float',
|
||||
choices=['standard_float', 'separable_float', 'standard_half', 'separable_half'],
|
||||
help='The model to use.')
|
||||
args = parser.parse_args()
|
||||
|
||||
device = torch.device('cuda')
|
||||
try:
|
||||
poser = load_poser(args.model, device)
|
||||
except RuntimeError as e:
|
||||
print(e)
|
||||
sys.exit()
|
||||
|
||||
app = wx.App()
|
||||
main_frame = MainFrame(poser, device)
|
||||
main_frame.Show(True)
|
||||
main_frame.timer.Start(30)
|
||||
app.MainLoop()
|
||||
Reference in New Issue
Block a user