From 9975b56c3595837ecabbf32b442f27644872f67b Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Sat, 16 Aug 2025 20:04:43 -0400 Subject: [PATCH 01/46] release updates --- 01A.构建运行环境(默认源).bat | 41 ------------ 01B.构建运行环境(国内源).bat | 41 ------------ ...ž„建TensorRT模型(N卡开启TensorRT使用).bat | 9 +-- 02A.启动器.bat | 8 +-- 02B.启动器(调试输出).bat | 6 +- ezvtb_rt_interface.py | 8 --- pack_release.bat | 66 +++++++++++++++++++ 7 files changed, 72 insertions(+), 107 deletions(-) delete mode 100644 01A.构建运行环境(默认源).bat delete mode 100644 01B.构建运行环境(国内源).bat create mode 100644 pack_release.bat diff --git a/01A.构建运行环境(默认源).bat b/01A.构建运行环境(默认源).bat deleted file mode 100644 index dfba141..0000000 --- a/01A.构建运行环境(默认源).bat +++ /dev/null @@ -1,41 +0,0 @@ -@echo on -cd /D "%~dp0" - -WHERE conda -IF %ERRORLEVEL% NEQ 0 ( - if not exist "envs" mkdir envs - if not exist "envs\miniconda3" mkdir envs\miniconda3 - - IF not EXIST %~dp0envs\miniconda3\Scripts ( - @RD /S /Q %~dp0envs\miniconda3 - mkdir %~dp0envs\miniconda3 - echo "Downloading miniconda..." - powershell -Command "(New-Object Net.WebClient).DownloadFile('https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe', '.\envs\miniconda3.exe')" - - echo "Installling minconda..." - start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 - echo "Successfully install minconda" - del envs\miniconda3.exe - ) -) - -IF EXIST %~dp0envs\miniconda3\Scripts SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% - -call activate -call conda env list -call conda update -y --all -call conda create -y -n ezvtb_rt_venv python=3.10 -call conda activate ezvtb_rt_venv -call conda env list - -call conda install -y nvidia/label/cuda-12.9.0::cuda-nvcc-dev_win-64 -call conda install -y conda-forge::pycuda - -call python -m pip install --upgrade pip wheel -echo yes|python -m pip install nvidia-cudnn-cu12 - -echo yes|pip install tensorrt_cu12_libs==10.11.0.33 tensorrt_cu12_bindings==10.11.0.33 tensorrt==10.11.0.33 --extra-index-url https://pypi.nvidia.com - -call python -m pip install -r requirements.txt --no-warn-script-location - -pause \ No newline at end of file diff --git a/01B.构建运行环境(国内源).bat b/01B.构建运行环境(国内源).bat deleted file mode 100644 index 989ca17..0000000 --- a/01B.构建运行环境(国内源).bat +++ /dev/null @@ -1,41 +0,0 @@ -@echo on -cd /D "%~dp0" - -WHERE conda -IF %ERRORLEVEL% NEQ 0 ( - if not exist "envs" mkdir envs - if not exist "envs\miniconda3" mkdir envs\miniconda3 - - IF not EXIST %~dp0envs\miniconda3\Scripts ( - @RD /S /Q %~dp0envs\miniconda3 - mkdir %~dp0envs\miniconda3 - echo "Downloading miniconda..." - powershell -Command "(New-Object Net.WebClient).DownloadFile('https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe', '.\envs\miniconda3.exe')" - - echo "Installling minconda..." - start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 - echo "Successfully install minconda" - del envs\miniconda3.exe - ) -) - -IF EXIST %~dp0envs\miniconda3\Scripts SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% - -call activate -call conda env list -call conda update -y --all -call conda create -y -n ezvtb_rt_venv python=3.10 -call conda activate ezvtb_rt_venv -call conda env list - -call conda install -y nvidia/label/cuda-12.9.0::cuda-nvcc-dev_win-64 -call conda install -y pycuda -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ - -call python -m pip install --upgrade pip wheel -i https://mirrors.aliyun.com/pypi/simple/ -echo yes|python -m pip install nvidia-cudnn-cu12 -i https://mirrors.aliyun.com/pypi/simple/ - -echo yes|pip install tensorrt_cu12_libs==10.11.0.33 tensorrt_cu12_bindings==10.11.0.33 tensorrt==10.11.0.33 --extra-index-url https://pypi.nvidia.com - -call python -m pip install -r requirements.txt --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ - -pause \ No newline at end of file diff --git a/02.构建TensorRT模型(N卡开启TensorRT使用).bat b/02.构建TensorRT模型(N卡开启TensorRT使用).bat index 0315d6f..09a61de 100644 --- a/02.构建TensorRT模型(N卡开启TensorRT使用).bat +++ b/02.构建TensorRT模型(N卡开启TensorRT使用).bat @@ -1,10 +1,7 @@ -@echo off +@echo on cd /D "%~dp0" -IF EXIST %~dp0envs\miniconda3\Scripts SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% -call activate -call conda env list -call conda activate ezvtb_rt_venv -call conda env list + +SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% python ezvtb_rt_interface.py diff --git a/02A.启动器.bat b/02A.启动器.bat index 2a714e4..09c6fc0 100644 --- a/02A.启动器.bat +++ b/02A.启动器.bat @@ -1,10 +1,6 @@ -@echo off +@echo on cd /D "%~dp0" -IF EXIST %~dp0envs\miniconda3\Scripts SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% -call activate -call conda env list -call conda activate ezvtb_rt_venv -call conda env list +SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% pythonw launcher2.py \ No newline at end of file diff --git a/02B.启动器(调试输出).bat b/02B.启动器(调试输出).bat index eee185a..d35ae38 100644 --- a/02B.启动器(调试输出).bat +++ b/02B.启动器(调试输出).bat @@ -1,11 +1,7 @@ @echo on cd /D "%~dp0" -IF EXIST %~dp0envs\miniconda3\Scripts SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% -call activate -call conda env list -call conda activate ezvtb_rt_venv -call conda env list +SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% python launcher2.py diff --git a/ezvtb_rt_interface.py b/ezvtb_rt_interface.py index 0801eb4..5272a69 100644 --- a/ezvtb_rt_interface.py +++ b/ezvtb_rt_interface.py @@ -4,13 +4,6 @@ import subprocess import urllib.request import shutil -def init_ezvtb_rt(search_dir = '.'): - if not os.path.exists(ezvtb_path) and not os.path.exists(ezvtb_main_path): #Check if rt not exist - zip_path = 'https://github.com/zpeng11/ezvtuber-rt/archive/refs/heads/main.zip' - filehandle, _ = urllib.request.urlretrieve(zip_path) - os.rename(filehandle, filehandle + '.zip') - shutil.unpack_archive(filehandle + '.zip' , '.') - dir_path = os.path.dirname(os.path.realpath(__file__)) ezvtb_path = os.path.join(dir_path, 'ezvtuber-rt') ezvtb_main_path = os.path.join(dir_path, 'ezvtuber-rt-main') @@ -114,7 +107,6 @@ def get_core( return core if __name__ == '__main__': - init_ezvtb_rt() from ezvtb_rt.trt_utils import check_build_all_models, cudaSetDevice device_id = 0 import sys diff --git a/pack_release.bat b/pack_release.bat new file mode 100644 index 0000000..2704cef --- /dev/null +++ b/pack_release.bat @@ -0,0 +1,66 @@ +@echo on +REM ======================================================================== +REM IMPORTANT: Before running this script, please make sure to: +REM 1. Checkout to the release_package branch: git checkout release_package +REM 2. Rebase with the latest changes: git rebase main +REM ======================================================================== + +cd /D "%~dp0" +@REM git clean -fdx +git submodule update --init --recursive + +if not exist "envs" mkdir envs +if not exist "envs\miniconda3" mkdir envs\miniconda3 + +IF not EXIST %~dp0envs\miniconda3\Scripts ( + @RD /S /Q %~dp0envs\miniconda3 + mkdir %~dp0envs\miniconda3 + echo "Downloading miniconda..." + powershell -Command "Invoke-WebRequest -Uri 'https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe' -OutFile '.\envs\miniconda3.exe' -UseBasicParsing -Headers @{'User-Agent'='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'}" + + echo "Installling minconda..." + start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 + echo "Successfully install minconda" + del envs\miniconda3.exe +) + +SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% + +call activate +call conda env list +call conda update -y --all +call conda create -n ezvtb_rt_venv_release -c conda-forge conda-pack python=3.10 -y +call conda activate ezvtb_rt_venv_release +call conda env list + +call conda install -y -c conda-forge cudatoolkit=12.9.1 cudnn=12.9.1 +call conda install -y nvidia/label/cuda-12.9.1::cuda-nvcc-dev_win-64 +call conda install -y pycuda -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ + +call conda-pack -n ezvtb_rt_venv_release -o %~dp0envs\python_embedded --format no-archive + + +SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% + +call python -m pip install --upgrade pip wheel -i https://mirrors.aliyun.com/pypi/simple/ +echo yes|python -m pip install nvidia-cudnn-cu12 -i https://mirrors.aliyun.com/pypi/simple/ + +echo yes|pip install tensorrt_cu12_libs==10.11.0.33 tensorrt_cu12_bindings==10.11.0.33 tensorrt==10.11.0.33 --extra-index-url https://pypi.nvidia.com + +call python -m pip install -r requirements.txt --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ + +if not exist "data\models" mkdir data\models +echo "Downloading model zip file..." +powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/zpeng11/ezvtuber-rt/releases/download/0.0.1/20241220.zip', '.\data\models\model.zip')" +echo "Unzipping model zip file..." +powershell -Command "Expand-Archive -Path '.\data\models\model.zip' -DestinationPath '.\data\models\temp' -Force; Move-Item -Path '.\data\models\temp\20241220\*' -Destination '.\data\models\' -Force; Remove-Item -Path '.\data\models\temp' -Recurse -Force" +del .\data\models\model.zip + +@REM To verify if environment works +call python ezvtb_rt_interface.py + +@REM Clean up step +echo "Cleaning up .trt files in data\models directory..." +for /R %~dp0data\models %%f in (*.trt) do del "%%f" +@RD /S /Q %~dp0envs\miniconda3 +pause \ No newline at end of file From 8e9fbd0ad6f86c3edde3794c6cd07af8aeaa989e Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:48:02 +0800 Subject: [PATCH 02/46] fisrt commit --- .gitignore | 4 +- THA4_SETUP.md | 65 +++++++++++++++++++ THA4_USAGE.md | 127 ++++++++++++++++++++++++++++++++++++ launcher2.py | 44 +++++++++++-- main.py | 107 ++++++++++++++++++++----------- models.py | 64 ++++++++++++++++++ pack_release.bat | 74 ++++++++++----------- tha4 | 1 + tha4_adapter.py | 128 ++++++++++++++++++++++++++++++++++++ tha4_core.py | 164 +++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 696 insertions(+), 82 deletions(-) create mode 100644 THA4_SETUP.md create mode 100644 THA4_USAGE.md create mode 160000 tha4 create mode 100644 tha4_adapter.py create mode 100644 tha4_core.py diff --git a/.gitignore b/.gitignore index 0241e25..25a244d 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,6 @@ data/ !data/models/placeholder.txt *.iml .idea/ -*.pt \ No newline at end of file +*.pt + +.serena \ No newline at end of file diff --git a/THA4_SETUP.md b/THA4_SETUP.md new file mode 100644 index 0000000..a569f99 --- /dev/null +++ b/THA4_SETUP.md @@ -0,0 +1,65 @@ +# THA4 Model Setup Guide + +## Required Files Structure + +Place your THA4 model files in `data/models/tha4/` with the following structure: + +``` +data/models/tha4/ +├── character_model.yaml # Configuration file (paths to other files) +├── character.png # Character source image (RGBA, 512x512) +├── face_morpher.pt # Face morphing model weights +└── body_morpher.pt # Body morphing model weights +``` + +## Example character_model.yaml + +Create a file named `character_model.yaml` with the following content: + +```yaml +character_image_file_name: character.png +face_morpher_file_name: face_morpher.pt +body_morpher_file_name: body_morpher.pt +``` + +**Note:** All paths in the YAML are relative to the directory containing the YAML file. + +## Alternative Structure + +You can also organize files in subdirectories: + +``` +data/models/tha4/ +├── my_character/ +│ ├── character_model.yaml +│ ├── image.png +│ ├── face_morpher.pt +│ └── body_morpher.pt +``` + +Then update your YAML accordingly: + +```yaml +character_image_file_name: image.png +face_morpher_file_name: face_morpher.pt +body_morpher_file_name: body_morpher.pt +``` + +## Usage in EasyVtuber + +1. Place your files as shown above +2. Launch the EasyVtuber launcher +3. Select "THA4" from the "Model Select" dropdown +4. The system will automatically: + - Load `character_model.yaml` + - Read the PNG character image + - Load both PT model files + - Use the PNG image as the source for animation + +## Differences from THA3 + +- **THA4**: Uses YAML to define character (image + models as a package) +- **THA3**: Uses separate image file selected by user + shared model files + +The THA4 approach bundles character and model together, making it easier to +switch between different characters with their trained models. diff --git a/THA4_USAGE.md b/THA4_USAGE.md new file mode 100644 index 0000000..f3f42a2 --- /dev/null +++ b/THA4_USAGE.md @@ -0,0 +1,127 @@ +# THA4 模型使用指南 (新版本) + +## 文件结构 + +THA4 模型现在支持直接放在 `data/images/` 目录中,与 THA3 的 PNG 图片一起管理。 + +### 所需文件 + +对于名为 `mycharacter` 的角色,需要在 `data/images/` 中放置以下文件: + +``` +data/images/ +├── mycharacter.yaml # 配置文件 +├── mycharacter.png # 角色源图片 (512x512 RGBA) +├── mycharacter_face_morpher.pt # 面部变形模型权重 +└── mycharacter_body_morpher.pt # 身体变形模型权重 +``` + +### YAML 配置格式 + +YAML 文件 (`mycharacter.yaml`) 应包含相对路径指向其他文件: + +```yaml +character_image_file_name: mycharacter.png +face_morpher_file_name: mycharacter_face_morpher.pt +body_morpher_file_name: mycharacter_body_morpher.pt +``` + +**重要说明:** +- YAML 中的所有路径都是相对于 YAML 文件所在位置的相对路径 +- 由于所有文件都在 `data/images/` 中,只需使用文件名即可 +- 角色图片必须是 512x512 像素的 RGBA 格式 +- 模型文件 (`.pt`) 是从 THA4 训练得到的 PyTorch 权重 + +## 在 EasyVtuber 中使用 + +### 自动检测 + +1. 将所有 4 个文件放入 `data/images/` 目录 +2. 打开启动器 (`02A.启动器.bat`) +3. 在 Character 下拉菜单中选择 `.yaml` 文件(例如 `mycharacter`) +4. 程序将自动: + - 检测这是 THA4 模型 + - 切换到 THA4 推理模式 + - 从 YAML 配置加载角色图片和模型 + +### 模型选择锁定 + +当你选择 YAML 文件时: +- 模型**自动锁定到 THA4** +- 使用 YAML 中的角色图片(启动器中的 Character 选择仅用于选择 YAML) +- 手动的模型选择将被覆盖 + +## 示例设置 + +假设你为名为 "alice" 的角色训练了 THA4 模型: + +1. 将这些文件复制到 `data/images/`: + ``` + alice.yaml + alice.png + alice_face_morpher.pt + alice_body_morpher.pt + ``` + +2. 创建 `alice.yaml`,内容为: + ```yaml + character_image_file_name: alice.png + face_morpher_file_name: alice_face_morpher.pt + body_morpher_file_name: alice_body_morpher.pt + ``` + +3. 在启动器中,从 Character 下拉菜单选择 "alice" +4. 启动 - 程序将检测到 YAML 并自动使用 THA4! + +## 与 THA3 的区别 + +| 特性 | THA3 | THA4 | +|------|------|------| +| 角色选择 | data/images/ 中的任意 PNG | 选择 YAML 文件 | +| 模型文件 | data/models/tha3/ 中的共享模型 | 每个角色独立的 .pt 文件 | +| 图片加载 | 运行时选择 | 与训练模型绑定 | +| 模型类型 | Standard/Separate, Half/Full | 单一类型 (float32) | +| 可互换性 | 可使用任意图片 | 图片与训练模型绑定 | + +## 文件命名建议 + +为了保持清晰,建议使用一致的命名规则: + +``` +<角色名>.yaml # 配置文件 +<角色名>.png # 源图片 +<角色名>_face_morpher.pt # 面部模型 +<角色名>_body_morpher.pt # 身体模型 +``` + +## 故障排除 + +**错误:"THA4 character model YAML not found"** +- 检查 `.yaml` 文件是否存在于 `data/images/` 中 +- 验证文件名是否与你选择的一致 + +**错误:"No such file or directory" 找不到 PNG 或 PT 文件** +- 检查 YAML 中的路径是否正确 +- 确保所有 4 个文件都在同一个 `data/images/` 目录中 +- 验证文件名完全匹配(区分大小写) + +**黑屏 / 无输出** +- 验证角色 PNG 是 512x512 RGBA 格式 +- 检查 .pt 模型文件是否来自正确训练的 THA4 模型 +- 确保模型文件与用于训练的角色图片匹配 + +## 旧文件迁移 + +如果你之前将文件放在 `data/models/tha4/` 中: + +1. 将所有文件移动到 `data/images/`: + ``` + mv data/models/tha4/character_model.yaml data/images/mycharacter.yaml + mv data/models/tha4/character.png data/images/mycharacter.png + mv data/models/tha4/face_morpher.pt data/images/mycharacter_face_morpher.pt + mv data/models/tha4/body_morpher.pt data/images/mycharacter_body_morpher.pt + ``` + +2. 更新 YAML 文件中的路径为新的文件名 + +3. 在启动器中选择新的 YAML 文件名 diff --git a/launcher2.py b/launcher2.py index fdd90aa..6abcfdc 100644 --- a/launcher2.py +++ b/launcher2.py @@ -80,6 +80,9 @@ def refreshList(): for item in sorted(os.listdir(dirPath), key=lambda x: -os.path.getmtime(os.path.join(dirPath, x))): if '.png' == item[-4:]: characterList.append(item[:-4]) + elif '.yaml' == item[-5:] or '.yml' == item[-4:]: + # Support THA4 YAML files + characterList.append(item[:-5] if item.endswith('.yaml') else item[:-4]) refreshList() @@ -227,9 +230,32 @@ class LauncherPanel(wx.Panel): choices=['10', '15', '20', '30', '60']) addOption('preset', title='Performance Preset', desc='性能预设,注意修改后会覆盖后续配置', choices=['Low', 'Medium', 'High', 'Ultra', 'Custom']) - addOption('model_select', title='Model Select', desc='选择使用的模型\nStandard Full精度较高性能较低', - choices=['Seperable Half', 'Seperable Full', 'Standard Half', 'Standard Full'], - mapping=['seperable_half', 'seperable_full', 'standard_half', 'standard_full']) + model_select_option = addOption('model_select', title='Model Select', desc='选择使用的模型\nStandard Full精度较高性能较低', + choices=['Seperable Half', 'Seperable Full', 'Standard Half', 'Standard Full', 'THA4'], + mapping=['seperable_half', 'seperable_full', 'standard_half', 'standard_full', 'tha4']) + + # Add event handler for character selection to auto-lock THA4 when YAML is selected + def on_character_change(event): + character_name = self.optionDict['character'].GetValue() + # Check if selected character has a YAML file + yaml_exists = False + for ext in ['.yaml', '.yml']: + if os.path.exists(os.path.join(dirPath, character_name + ext)): + yaml_exists = True + break + + if yaml_exists: + # Auto-select THA4 model and disable the control + model_select_option.control.SetSelection(4) # THA4 is index 4 + model_select_option.control.Enable(False) + else: + # Re-enable model selection for non-YAML files + model_select_option.control.Enable(True) + + self.optionDict['character'].control.Bind(wx.EVT_CHOICE, on_character_change) + + # Trigger initial check + on_character_change(None) addOption('ram_cache_size', title='RAM Cache Size', desc='分配内存缓存大小\n用于存储最终运算结果', choices=['Off', '1GB', '2GB', '4GB', '8GB', '16GB'], mapping=['0b', '1gb', '2gb', '4gb', '8gb', '16gb']) @@ -411,10 +437,14 @@ class LauncherPanel(wx.Panel): run_args.append('4') if args['model_select'] is not None: - if 'seperable' in args['model_select']: - run_args.append('--model_seperable') - if 'half' in args['model_select']: - run_args.append('--model_half') + if 'tha4' in args['model_select']: + run_args.append('--model') + run_args.append('tha4') + else: + if 'seperable' in args['model_select']: + run_args.append('--model_seperable') + if 'half' in args['model_select']: + run_args.append('--model_half') if args['frame_rate_limit'] is not None: run_args.append('--frame_rate_limit') diff --git a/main.py b/main.py index ca072b0..f250514 100644 --- a/main.py +++ b/main.py @@ -330,12 +330,13 @@ class MouseClientProcess(Process): class ModelClientProcess(Process): - def __init__(self, input_image): + def __init__(self, input_image, yaml_path=None): super().__init__() self.should_terminate = Value('b', False) self.updated = Value('b', False) self.data = None self.input_image = input_image + self.yaml_path = yaml_path self.output_queue = Queue() self.input_queue = Queue() self.shms = [shared_memory.SharedMemory(create=True, size=args.model_output_size * args.model_output_size * 4) @@ -346,25 +347,36 @@ class ModelClientProcess(Process): self.gpu_cache_hit_ratio = Value('f', 0.0) def run(self): - self.model = get_core(device_id=args.device_id, - use_tensorrt=args.use_tensorrt, + # Check if using THA4 model + if args.model == 'tha4': + from tha4_core import get_tha4_core + self.model = get_tha4_core( + device_id=args.device_id, + use_eyebrow=args.eyebrow, + interpolation_scale=args.interpolation_scale if args.use_interpolation else 1, + yaml_path=self.yaml_path + ) + else: + # Use existing ezvtb_rt interface for THA3 models + self.model = get_core(device_id=args.device_id, + use_tensorrt=args.use_tensorrt, - model_seperable = args.model_seperable, - model_half=args.model_half, - model_cache_size=args.max_gpu_cache_len, - model_use_eyebrow=args.eyebrow, + model_seperable = args.model_seperable, + model_half=args.model_half, + model_cache_size=args.max_gpu_cache_len, + model_use_eyebrow=args.eyebrow, - use_interpolation=args.use_interpolation, - interpolation_scale=args.interpolation_scale, - interpolation_half=args.interpolation_half, + use_interpolation=args.use_interpolation, + interpolation_scale=args.interpolation_scale, + interpolation_half=args.interpolation_half, - cacher_quality=args.cacher_quality, - cacher_ram_size=args.max_cache_len, + cacher_quality=args.cacher_quality, + cacher_ram_size=args.max_cache_len, - use_sr=args.use_sr, - sr_half=args.sr_half, - sr_x4=args.sr_x4, - sr_noise=args.sr_noise) + use_sr=args.use_sr, + sr_half=args.sr_half, + sr_x4=args.sr_x4, + sr_noise=args.sr_noise) self.model.setImage(self.input_image) input_pose = np.zeros((1, 45), dtype=np.float32) @@ -529,26 +541,47 @@ class ModelClientProcess(Process): @torch.no_grad() def main(): - img = Image.open(f"data/images/{args.character}.png") - img = img.convert('RGBA') - ow, oh = img.size - for i, px in enumerate(img.getdata()): - if px[3] <= 0: - y = i // ow - x = i % ow - img.putpixel((x, y), (0, 0, 0, 0)) - if ow != 512 or oh != 512: - img = resize_to_512_center(img) - if args.alpha_clean: - curves = { - 'a': [ - (60, 0), - (200, 255) - ] - } - img = apply_color_curves(img, curves) - input_image = np.array(img) - input_image = cv2.cvtColor(input_image, cv2.COLOR_RGBA2BGRA) + # Check if character is a YAML file (THA4 model) + yaml_path = None + is_tha4 = False + + # Try to find YAML file + for ext in ['.yaml', '.yml']: + yaml_candidate = f"data/images/{args.character}{ext}" + if os.path.exists(yaml_candidate): + yaml_path = yaml_candidate + is_tha4 = True + print(f"Detected THA4 YAML config: {yaml_path}") + # Override model selection to THA4 + args.model = 'tha4' + break + + # Load character image (only for non-THA4 models) + if not is_tha4: + img = Image.open(f"data/images/{args.character}.png") + img = img.convert('RGBA') + ow, oh = img.size + for i, px in enumerate(img.getdata()): + if px[3] <= 0: + y = i // ow + x = i % ow + img.putpixel((x, y), (0, 0, 0, 0)) + if ow != 512 or oh != 512: + img = resize_to_512_center(img) + if args.alpha_clean: + curves = { + 'a': [ + (60, 0), + (200, 255) + ] + } + img = apply_color_curves(img, curves) + input_image = np.array(img) + input_image = cv2.cvtColor(input_image, cv2.COLOR_RGBA2BGRA) + else: + # For THA4, create a dummy image (will be ignored) + input_image = np.zeros((512, 512, 4), dtype=np.uint8) + print("THA4 mode: Character image will be loaded from YAML") print("Character Image Loaded:", args.character) cap = None @@ -652,7 +685,7 @@ def main(): } model_output = None - model_process = ModelClientProcess(input_image) + model_process = ModelClientProcess(input_image, yaml_path=yaml_path) model_process.daemon = True model_process.start() model_output_nps = [np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, diff --git a/models.py b/models.py index e91620a..34abb08 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,6 @@ import time +import torch import torch.nn as nn import tha2.poser.modes.mode_20 @@ -13,6 +14,14 @@ from args import args from collections import OrderedDict +# THA4 适配器导入 +try: + from tha4_adapter import THA4Wrapper + THA4_AVAILABLE = True +except ImportError: + THA4_AVAILABLE = False + print("Warning: THA4 adapter not available") + class TalkingAnimeLight(nn.Module): def __init__(self): @@ -171,3 +180,58 @@ class TalkingAnime(nn.Module): rotate_image = self.two_algo_face_rotator(x, pose_vector)[:2] output_image = self.combiner(rotate_image[0], rotate_image[1], pose_vector) return output_image + + +class TalkingAnime4(nn.Module): + """ + THA4 model class, compatible with THA3 TalkingAnime3 interface + Uses THA4's Siren architecture for inference + PyTorch only, float32 precision + """ + def __init__(self, device): + super(TalkingAnime4, self).__init__() + if not THA4_AVAILABLE: + raise RuntimeError( + "THA4 adapter not available. Check tha4_adapter.py" + ) + + # Create THA4 wrapper + self.wrapper = THA4Wrapper(device=device) + + print("THA4 model initialized (float32)") + + def forward(self, image, mouth_eye_vector, pose_vector, eyebrow_vector, + mouth_eye_vector_c, eyebrow_vector_c, ratio=None): + """ + Forward inference, compatible with THA3 interface + + Args: + image: [batch, 4, 512, 512] + mouth_eye_vector: [batch, 27] + pose_vector: [batch, 6] + eyebrow_vector: [batch, 12] + mouth_eye_vector_c: compressed (for caching) + eyebrow_vector_c: compressed (for caching) + ratio: GPU cache hit ratio + + Returns: + output_image: [batch, 4, 512, 512] + """ + if args.perf == 'model': + tic = time.perf_counter() + + # Call THA4 wrapper + output_image = self.wrapper.forward( + image, mouth_eye_vector, pose_vector, eyebrow_vector, + mouth_eye_vector_c, eyebrow_vector_c, ratio + ) + + if args.perf == 'model': + print(" - tha4_inference", (time.perf_counter() - tic) * 1000) + + return output_image + + def to(self, device): + """Move model to specified device""" + self.wrapper.to(device) + return super().to(device) diff --git a/pack_release.bat b/pack_release.bat index 2704cef..747e7df 100644 --- a/pack_release.bat +++ b/pack_release.bat @@ -7,57 +7,57 @@ REM ======================================================================== cd /D "%~dp0" @REM git clean -fdx -git submodule update --init --recursive +@REM git submodule update --init --recursive -if not exist "envs" mkdir envs -if not exist "envs\miniconda3" mkdir envs\miniconda3 +@REM if not exist "envs" mkdir envs +@REM if not exist "envs\miniconda3" mkdir envs\miniconda3 -IF not EXIST %~dp0envs\miniconda3\Scripts ( - @RD /S /Q %~dp0envs\miniconda3 - mkdir %~dp0envs\miniconda3 - echo "Downloading miniconda..." - powershell -Command "Invoke-WebRequest -Uri 'https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe' -OutFile '.\envs\miniconda3.exe' -UseBasicParsing -Headers @{'User-Agent'='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'}" +@REM IF not EXIST %~dp0envs\miniconda3\Scripts ( +@REM @RD /S /Q %~dp0envs\miniconda3 +@REM mkdir %~dp0envs\miniconda3 +@REM echo "Downloading miniconda..." +@REM powershell -Command "Invoke-WebRequest -Uri 'https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe' -OutFile '.\envs\miniconda3.exe' -UseBasicParsing -Headers @{'User-Agent'='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'}" - echo "Installling minconda..." - start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 - echo "Successfully install minconda" - del envs\miniconda3.exe -) +@REM echo "Installling minconda..." +@REM start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 +@REM echo "Successfully install minconda" +@REM del envs\miniconda3.exe +@REM ) -SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% +@REM SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% -call activate -call conda env list -call conda update -y --all -call conda create -n ezvtb_rt_venv_release -c conda-forge conda-pack python=3.10 -y -call conda activate ezvtb_rt_venv_release -call conda env list +@REM call activate +@REM call conda env list +@REM call conda update -y --all +@REM call conda create -n ezvtb_rt_venv_release -c conda-forge conda-pack python=3.10 -y +@REM call conda activate ezvtb_rt_venv_release +@REM call conda env list -call conda install -y -c conda-forge cudatoolkit=12.9.1 cudnn=12.9.1 -call conda install -y nvidia/label/cuda-12.9.1::cuda-nvcc-dev_win-64 -call conda install -y pycuda -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ +@REM call conda install -y -c conda-forge cudatoolkit=12.9.1 cudnn=12.9.1 +@REM call conda install -y nvidia/label/cuda-12.9.1::cuda-nvcc-dev_win-64 +@REM call conda install -y pycuda -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ -call conda-pack -n ezvtb_rt_venv_release -o %~dp0envs\python_embedded --format no-archive +@REM call conda-pack -n ezvtb_rt_venv_release -o %~dp0envs\python_embedded --format no-archive -SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% +@REM SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% -call python -m pip install --upgrade pip wheel -i https://mirrors.aliyun.com/pypi/simple/ -echo yes|python -m pip install nvidia-cudnn-cu12 -i https://mirrors.aliyun.com/pypi/simple/ +@REM call python -m pip install --upgrade pip wheel -i https://mirrors.aliyun.com/pypi/simple/ +@REM echo yes|python -m pip install nvidia-cudnn-cu12 -i https://mirrors.aliyun.com/pypi/simple/ -echo yes|pip install tensorrt_cu12_libs==10.11.0.33 tensorrt_cu12_bindings==10.11.0.33 tensorrt==10.11.0.33 --extra-index-url https://pypi.nvidia.com +@REM echo yes|pip install tensorrt_cu12_libs==10.11.0.33 tensorrt_cu12_bindings==10.11.0.33 tensorrt==10.11.0.33 --extra-index-url https://pypi.nvidia.com -call python -m pip install -r requirements.txt --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ +@REM call python -m pip install -r requirements.txt --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ -if not exist "data\models" mkdir data\models -echo "Downloading model zip file..." -powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/zpeng11/ezvtuber-rt/releases/download/0.0.1/20241220.zip', '.\data\models\model.zip')" -echo "Unzipping model zip file..." -powershell -Command "Expand-Archive -Path '.\data\models\model.zip' -DestinationPath '.\data\models\temp' -Force; Move-Item -Path '.\data\models\temp\20241220\*' -Destination '.\data\models\' -Force; Remove-Item -Path '.\data\models\temp' -Recurse -Force" -del .\data\models\model.zip +@REM if not exist "data\models" mkdir data\models +@REM echo "Downloading model zip file..." +@REM powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/zpeng11/ezvtuber-rt/releases/download/0.0.1/20241220.zip', '.\data\models\model.zip')" +@REM echo "Unzipping model zip file..." +@REM powershell -Command "Expand-Archive -Path '.\data\models\model.zip' -DestinationPath '.\data\models\temp' -Force; Move-Item -Path '.\data\models\temp\20241220\*' -Destination '.\data\models\' -Force; Remove-Item -Path '.\data\models\temp' -Recurse -Force" +@REM del .\data\models\model.zip -@REM To verify if environment works -call python ezvtb_rt_interface.py +@REM @REM To verify if environment works +@REM call python ezvtb_rt_interface.py @REM Clean up step echo "Cleaning up .trt files in data\models directory..." diff --git a/tha4 b/tha4 new file mode 160000 index 0000000..3206401 --- /dev/null +++ b/tha4 @@ -0,0 +1 @@ +Subproject commit 320640116abd604de22d7b56faec85b4d95770e0 diff --git a/tha4_adapter.py b/tha4_adapter.py new file mode 100644 index 0000000..73c6432 --- /dev/null +++ b/tha4_adapter.py @@ -0,0 +1,128 @@ +""" +THA4 Adapter Module - Simplified Version +PyTorch only, no TensorRT/ONNX support +Adapts THA4 models to existing EasyVtuber architecture with THA3-compatible interface +Uses CharacterModel to properly load YAML + PNG + PT files +""" +import sys +import os +import torch + +# Add tha4 source code path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'tha4', 'src')) + +from tha4.charmodel.character_model import CharacterModel + + +def convert_tha3_pose_to_tha4(eyebrow_vector, mouth_eye_vector, pose_vector): + """ + Convert THA3 pose format to THA4 format + + THA3 inputs: + - eyebrow_vector: [12] eyebrow parameters + - mouth_eye_vector: [27] facial expression parameters + - pose_vector: [6] head pose parameters + + THA4 input: + - pose: [45] unified pose vector + - [0:39] face pose (12 eyebrow + 27 face) + - [39:45] body pose (6 head rotation) + + Returns: + torch.Tensor [batch, 45] + """ + # Concatenate: [eyebrow(12), mouth_eye(27), pose(6)] = [45] + face_pose = torch.cat([eyebrow_vector, mouth_eye_vector], dim=1) + tha4_pose = torch.cat([face_pose, pose_vector], dim=1) + return tha4_pose + + +class THA4Wrapper: + """ + THA4 Poser wrapper providing THA3 TalkingAnime3-compatible interface + Uses CharacterModel to load YAML config + PNG image + PT models + """ + + def __init__(self, device: torch.device, yaml_path: str = None): + """ + Initialize THA4 wrapper + + Args: + device: torch device (cuda/cpu) + yaml_path: path to character model YAML (required) + """ + self.device = device + + # YAML path is required + if yaml_path is None: + raise ValueError( + "THA4 requires a YAML config path. " + "Please select a .yaml file in the Character selection." + ) + + # Check if YAML exists + if not os.path.exists(yaml_path): + raise FileNotFoundError( + f"THA4 character model YAML not found: {yaml_path}\n" + f"Expected files in data/images/:\n" + f" - .yaml (config with relative paths)\n" + f" - .png (source image)\n" + f" - _face_morpher.pt (face model)\n" + f" - _body_morpher.pt (body model)" + ) + + # Load CharacterModel from YAML + print(f"Loading THA4 CharacterModel from: {yaml_path}") + self.character_model = CharacterModel.load(yaml_path) + + # Get poser (lazy loads .pt files) + self.poser = self.character_model.get_poser(device) + + # Get character image tensor (loads .png file) + char_img = self.character_model.get_character_image(device) + + # Add batch dimension if needed: [4, 512, 512] -> [1, 4, 512, 512] + if len(char_img.shape) == 3: + self.character_image = char_img.unsqueeze(0) + else: + self.character_image = char_img + + print(f"THA4 model loaded successfully:") + print(f" Device: {device}") + print(f" Image: {self.character_model.character_image_file_name}") + print(f" Face: {self.character_model.face_morpher_file_name}") + print(f" Body: {self.character_model.body_morpher_file_name}") + + def forward(self, image, mouth_eye_vector, pose_vector, eyebrow_vector, + mouth_eye_vector_c, eyebrow_vector_c, ratio=None): + """ + Forward inference, compatible with THA3 TalkingAnime3.forward() + + Args: + image: [batch, 4, 512, 512] + mouth_eye_vector: [batch, 27] + pose_vector: [batch, 6] + eyebrow_vector: [batch, 12] + mouth_eye_vector_c: compressed version (not used by THA4) + eyebrow_vector_c: compressed version (not used by THA4) + ratio: GPU cache hit ratio (not used by THA4) + + Returns: + output image [batch, 4, 512, 512] + """ + # Convert pose format + tha4_pose = convert_tha3_pose_to_tha4( + eyebrow_vector, mouth_eye_vector, pose_vector + ) + + # Call THA4 poser + with torch.no_grad(): + output = self.poser.pose(image, tha4_pose, output_index=0) + + return output + + def to(self, device: torch.device): + """Move model to specified device""" + self.device = device + self.poser.to(device) + return self diff --git a/tha4_core.py b/tha4_core.py new file mode 100644 index 0000000..d57ad70 --- /dev/null +++ b/tha4_core.py @@ -0,0 +1,164 @@ +""" +THA4 Core - Pure PyTorch implementation +Provides interface compatible with CoreTRT/CoreORT for THA4 models +""" +import torch +import numpy as np +import cv2 +from tha4_adapter import THA4Wrapper, convert_tha3_pose_to_tha4 + +# Import color space conversion from THA4 +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'tha4', 'src')) +from tha4.image_util import convert_linear_to_srgb + + +class THA4Core: + """ + THA4 Core class with interface compatible with CoreTRT/CoreORT + + Uses CharacterModel to load YAML + PNG + PT files properly. + The PNG image from YAML is used as the source image. + + Expected interface: + - setImage(img: np.ndarray) - NOT USED, image comes from YAML + - inference(pose: np.ndarray) -> List[np.ndarray] - run inference + """ + + def __init__(self, device_id=0, use_eyebrow=True, yaml_path=None, + interpolation_scale=1): + """ + Initialize THA4 Core + + Args: + device_id: GPU device ID + use_eyebrow: whether to use eyebrow parameters + yaml_path: path to character_model.yaml + Default: 'data/models/tha4/character_model.yaml' + interpolation_scale: number of output frames to generate + (THA4 doesn't support real interpolation, will duplicate frames) + """ + device_str = f'cuda:{device_id}' if torch.cuda.is_available() else 'cpu' + self.device = torch.device(device_str) + self.use_eyebrow = use_eyebrow + self.interpolation_scale = interpolation_scale + + # Create THA4 wrapper (loads YAML + PNG + PT) + self.wrapper = THA4Wrapper(device=self.device, yaml_path=yaml_path) + + # Use character image from YAML (already loaded by wrapper) + self.input_image_tensor = self.wrapper.character_image + + # For compatibility (no caching in THA4 core) + self.cacher = None + + print(f"THA4 Core initialized on {self.device}") + print(f"Using character image from model (ignoring setImage calls)") + if interpolation_scale > 1: + print(f"Note: THA4 doesn't support interpolation, " + f"will duplicate frames {interpolation_scale}x") + + def setImage(self, img: np.ndarray): + """ + Set input character image - IGNORED for THA4 + + THA4 uses the character image from the YAML/PNG files loaded + during initialization. This method exists only for interface + compatibility with CoreTRT/CoreORT. + + Args: + img: numpy array in BGRA format (ignored) + """ + # THA4 uses character image from YAML, ignore external image + print("THA4: setImage() called but ignored (using image from YAML)") + + def inference(self, pose: np.ndarray) -> list: + """ + Run inference + + Args: + pose: numpy array of shape (1, 45) + [0:12] - eyebrow parameters + [12:39] - face parameters + [39:45] - head pose parameters + + Returns: + List of output images as numpy arrays [H, W, 4] in BGRA format, uint8 + """ + if self.input_image_tensor is None: + raise RuntimeError("Image not set. Call setImage() first.") + + # Extract pose components + if self.use_eyebrow: + eyebrow_pose = pose[:, :12] # [1, 12] + else: + eyebrow_pose = np.zeros((1, 12), dtype=np.float32) + + face_pose = pose[:, 12:39] # [1, 27] + head_pose = pose[:, 39:45] # [1, 6] + + # Convert to torch tensors + eyebrow_tensor = torch.from_numpy(eyebrow_pose).to(self.device) + face_tensor = torch.from_numpy(face_pose).to(self.device) + head_tensor = torch.from_numpy(head_pose).to(self.device) + + # Call wrapper (it handles the pose conversion internally) + # We need to create dummy compressed versions for interface compatibility + eyebrow_c = eyebrow_pose[0].tolist() + face_c = face_pose[0].tolist() + + with torch.no_grad(): + output_tensor = self.wrapper.forward( + self.input_image_tensor, + face_tensor, + head_tensor, + eyebrow_tensor, + face_c, + eyebrow_c, + ratio=None + ) + + # THA4 outputs in linear color space, convert to sRGB + # Output is [batch, 4, H, W] in range [-1, 1] + output_image = output_tensor[0].float() # [4, 512, 512] + + # Clip to [-1, 1] and convert to [0, 1] + output_image = torch.clamp((output_image + 1.0) / 2.0, 0.0, 1.0) + + # Convert linear RGB to sRGB (keep alpha as-is) + output_image = convert_linear_to_srgb(output_image) + + # Convert to [H, W, C] numpy + output_np = output_image.permute(1, 2, 0).cpu().numpy() + + # Convert to uint8 + output_np = (output_np * 255.0).astype(np.uint8) + + # Convert RGBA to BGRA for output + output_bgra = cv2.cvtColor(output_np, cv2.COLOR_RGBA2BGRA) + + # Return as list with proper number of frames + # THA4 doesn't support real interpolation, so duplicate frames if needed + return [output_bgra] * self.interpolation_scale + + +def get_tha4_core(device_id=0, use_eyebrow=True, yaml_path=None, + interpolation_scale=1): + """ + Factory function to create THA4 core + + Args: + device_id: GPU device ID + use_eyebrow: whether to use eyebrow control + yaml_path: path to character_model.yaml + Default: 'data/models/tha4/character_model.yaml' + interpolation_scale: number of frames (for compatibility with interpolation) + + Returns: + THA4Core instance + """ + return THA4Core(device_id=device_id, + use_eyebrow=use_eyebrow, + yaml_path=yaml_path, + interpolation_scale=interpolation_scale) From 629623cbac8e459fb4fd1ac5c654bc88e0121a8e Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:45:43 +0800 Subject: [PATCH 03/46] tha4 teachermodel support --- .gitignore | 5 +- args.py | 1 + convert_onnx_to_trt_fp16.py | 51 ++++ ezvtb_rt_interface.py | 126 ++++++--- ezvtuber-rt | 2 +- launcher2.py | 3 +- main.py | 43 ++-- onnx_export_tha4.py | 232 +++++++++++++++++ tha4_core.py | 382 +++++++++++++++++++++++++-- tha4_core_old.py | 498 ++++++++++++++++++++++++++++++++++++ 10 files changed, 1253 insertions(+), 90 deletions(-) create mode 100644 convert_onnx_to_trt_fp16.py create mode 100644 onnx_export_tha4.py create mode 100644 tha4_core_old.py diff --git a/.gitignore b/.gitignore index 25a244d..64d75dc 100644 --- a/.gitignore +++ b/.gitignore @@ -144,4 +144,7 @@ data/ .idea/ *.pt -.serena \ No newline at end of file +.serena +onnx_model_tha4/fp16 +onnx_model_tha4/fp32 +onnx_model_tha4/tmp \ No newline at end of file diff --git a/args.py b/args.py index 2c2d6eb..28032b1 100644 --- a/args.py +++ b/args.py @@ -37,6 +37,7 @@ parser.add_argument('--simplify', type=int, default=1) parser.add_argument('--device_id', type=int, default=0) parser.add_argument('--use_tensorrt', action='store_true') +parser.add_argument('--use_tha4', action='store_true') parser.add_argument('--model_seperable', action='store_true') parser.add_argument('--model_half', action='store_true') diff --git a/convert_onnx_to_trt_fp16.py b/convert_onnx_to_trt_fp16.py new file mode 100644 index 0000000..3825543 --- /dev/null +++ b/convert_onnx_to_trt_fp16.py @@ -0,0 +1,51 @@ +""" +将 onnx_model_tha4/fp32 目录中的 ONNX 模型转换为 fp16 精度的 TensorRT 模型 +并保存到 onnx_model_tha4/fp16 目录 +""" +import os +import sys +from pathlib import Path + +# 添加 ezvtuber-rt 到路径 +sys.path.insert(0, str(Path(__file__).parent / 'ezvtuber-rt')) + +from ezvtb_rt.trt_utils import build_engine, save_engine + +def convert_onnx_to_trt(): + # 输入和输出目录 + input_dir = Path('onnx_model_tha4/fp32') + output_dir = Path('onnx_model_tha4/fp16') + + # 创建输出目录 + output_dir.mkdir(parents=True, exist_ok=True) + + # 获取所有 ONNX 文件 + onnx_files = list(input_dir.glob('*.onnx')) + + if not onnx_files: + print(f"在 {input_dir} 中未找到 ONNX 模型文件") + return + + print(f"找到 {len(onnx_files)} 个 ONNX 模型文件") + print("开始转换为 fp16 精度的 TensorRT 模型...\n") + + for onnx_file in onnx_files: + print(f"正在转换: {onnx_file.name}") + + # 构建输出文件路径 + trt_filename = onnx_file.stem + '.trt' + trt_path = output_dir / trt_filename + + try: + # 构建并保存 TensorRT 引擎 + engine = build_engine(str(onnx_file), 'fp16') + save_engine(engine, str(trt_path)) + print(f"✓ 成功转换: {onnx_file.name} -> {trt_path}\n") + except Exception as e: + print(f"✗ 转换失败: {onnx_file.name}") + print(f" 错误信息: {str(e)}\n") + + print("转换完成!") + +if __name__ == '__main__': + convert_onnx_to_trt() diff --git a/ezvtb_rt_interface.py b/ezvtb_rt_interface.py index 5272a69..4c0b537 100644 --- a/ezvtb_rt_interface.py +++ b/ezvtb_rt_interface.py @@ -37,72 +37,120 @@ def get_core( use_sr:bool = False, sr_x4:bool = True, sr_half:bool = True, - sr_noise:int = 1 + sr_noise:int = 1, + #THA4 model setting (NEW) + use_tha4:bool = False # Use THA4 instead of THA3 ): support_trt = False - if use_tensorrt: #Verify tensorrt + if use_tensorrt: print('Verifying TensorRT') try: - from ezvtb_rt.trt_utils import cudaSetDevice, check_build_all_models + from ezvtb_rt.trt_utils import cudaSetDevice cudaSetDevice(device_id) - # check_build_all_models() support_trt = True - except: + except Exception: support_trt = False - if support_trt == False and use_tensorrt == True: + if not support_trt and use_tensorrt: print('TensorRT option selected but not supported') use_tensorrt = False if use_tensorrt: os.environ['CUDA_DEVICE'] = str(device_id) import pycuda.autoinit - print(f'Using devcie {pycuda.autoinit.device.name()}') + print(f'Using device {pycuda.autoinit.device.name()}') + + # Determine model directory based on THA version + if use_tha4: + # THA4: Use onnx_model_tha4 directory + if use_tensorrt: + tha_model_dir = os.path.join('.', 'onnx_model_tha4', 'fp16') + else: + tha_model_dir = os.path.join('.', 'onnx_model_tha4', 'fp32') + print(f'THA4 Mode - Model Path: {tha_model_dir}') + else: + # THA3: Use data/models/tha3 directory + tha_model_dir = os.path.join( + '.', 'data', 'models', 'tha3', + 'seperable' if model_seperable else 'standard', + 'fp16' if model_half else 'fp32') + print(f'THA3 Mode - Model Path: {tha_model_dir}') - tha_model_dir = os.path.join('.','data', 'models', 'tha3', - 'seperable' if model_seperable else 'standard', - 'fp16' if model_half else 'fp32') rife_model_path = '' if use_interpolation: - rife_model_path = os.path.join('.','data', 'models','rife_512', - f'x{interpolation_scale}', 'fp16' if interpolation_half else 'fp32') - + rife_model_path = os.path.join( + '.', 'data', 'models', 'rife_512', + f'x{interpolation_scale}', + 'fp16' if interpolation_half else 'fp32') + sr_model_path = '' if use_sr: if sr_x4: if sr_half: - sr_model_path = os.path.join('.','data', 'models', 'Real-ESRGAN', 'exported_256_fp16') + sr_model_path = os.path.join( + '.', 'data', 'models', 'Real-ESRGAN', + 'exported_256_fp16') else: - sr_model_path = os.path.join('.','data', 'models', 'Real-ESRGAN', 'exported_256') - else: #x2 + sr_model_path = os.path.join( + '.', 'data', 'models', 'Real-ESRGAN', + 'exported_256') + else: if sr_half: - sr_model_path = os.path.join('.','data', 'models', 'waifu2x_upconv', 'fp16', 'upconv_7', 'art', f'noise{sr_noise}_scale2x') + sr_model_path = os.path.join( + '.', 'data', 'models', 'waifu2x_upconv', + 'fp16', 'upconv_7', 'art', + f'noise{sr_noise}_scale2x') else: - sr_model_path = os.path.join('.','data', 'models', 'waifu2x_upconv', 'fp32', 'upconv_7', 'art', f'noise{sr_noise}_scale2x') + sr_model_path = os.path.join( + '.', 'data', 'models', 'waifu2x_upconv', + 'fp32', 'upconv_7', 'art', + f'noise{sr_noise}_scale2x') - print(f'THA3 Path:{tha_model_dir}') - print(f'RIFE Path:{rife_model_path}') - print(f'SR Path:{sr_model_path}') + print(f'RIFE Path: {rife_model_path}') + print(f'SR Path: {sr_model_path}') core = None if use_tensorrt: - from ezvtb_rt.core import CoreTRT - core = CoreTRT(tha_model_dir, - vram_cache_size = model_cache_size, - use_eyebrow = model_use_eyebrow, - rife_dir = rife_model_path if len(rife_model_path) > 0 else None, - sr_dir = sr_model_path if len(sr_model_path) > 0 else None, - cache_max_volume = cacher_ram_size, - cache_quality = cacher_quality, - ) - else: #Use directml - from ezvtb_rt.core_ort import CoreORT - core = CoreORT(tha_model_dir, - rife_path = rife_model_path if len(rife_model_path) > 0 else None, - sr_path = sr_model_path if len(sr_model_path) > 0 else None, - device_id=device_id, - cache_max_volume = cacher_ram_size, - cache_quality = cacher_quality, - use_eyebrow = model_use_eyebrow) + if use_tha4: + from ezvtb_rt.core_tha4 import CoreTHA4TRT + core = CoreTHA4TRT( + tha_model_dir, + vram_cache_size=model_cache_size, + use_eyebrow=model_use_eyebrow, + rife_dir=rife_model_path if rife_model_path else None, + sr_dir=sr_model_path if sr_model_path else None, + cache_max_volume=cacher_ram_size, + cache_quality=cacher_quality) + else: + from ezvtb_rt.core import CoreTRT + core = CoreTRT( + tha_model_dir, + vram_cache_size=model_cache_size, + use_eyebrow=model_use_eyebrow, + rife_dir=rife_model_path if rife_model_path else None, + sr_dir=sr_model_path if sr_model_path else None, + cache_max_volume=cacher_ram_size, + cache_quality=cacher_quality) + else: + if use_tha4: + from ezvtb_rt.core_tha4_ort import CoreTHA4ORT + core = CoreTHA4ORT( + tha_model_dir, + rife_path=rife_model_path if rife_model_path else None, + sr_path=sr_model_path if sr_model_path else None, + device_id=device_id, + cache_max_volume=cacher_ram_size, + cache_quality=cacher_quality, + use_eyebrow=model_use_eyebrow) + else: + from ezvtb_rt.core_ort import CoreORT + core = CoreORT( + tha_model_dir, + rife_path=rife_model_path if rife_model_path else None, + sr_path=sr_model_path if sr_model_path else None, + device_id=device_id, + cache_max_volume=cacher_ram_size, + cache_quality=cacher_quality, + use_eyebrow=model_use_eyebrow) return core diff --git a/ezvtuber-rt b/ezvtuber-rt index d602899..7ce4ce8 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit d602899f2dde6561d6d5dfbbfa908117c10900b3 +Subproject commit 7ce4ce8d50ca17409b01644cbd714b3f8a5c1412 diff --git a/launcher2.py b/launcher2.py index 6abcfdc..8e1fcab 100644 --- a/launcher2.py +++ b/launcher2.py @@ -438,8 +438,7 @@ class LauncherPanel(wx.Panel): if args['model_select'] is not None: if 'tha4' in args['model_select']: - run_args.append('--model') - run_args.append('tha4') + run_args.append('--use_tha4') else: if 'seperable' in args['model_select']: run_args.append('--model_seperable') diff --git a/main.py b/main.py index f250514..4bcfd0b 100644 --- a/main.py +++ b/main.py @@ -347,36 +347,27 @@ class ModelClientProcess(Process): self.gpu_cache_hit_ratio = Value('f', 0.0) def run(self): - # Check if using THA4 model - if args.model == 'tha4': - from tha4_core import get_tha4_core - self.model = get_tha4_core( - device_id=args.device_id, - use_eyebrow=args.eyebrow, - interpolation_scale=args.interpolation_scale if args.use_interpolation else 1, - yaml_path=self.yaml_path - ) - else: - # Use existing ezvtb_rt interface for THA3 models - self.model = get_core(device_id=args.device_id, - use_tensorrt=args.use_tensorrt, + # Use unified ezvtb_rt interface for both THA3 and THA4 + self.model = get_core(device_id=args.device_id, + use_tensorrt=args.use_tensorrt, + use_tha4=args.use_tha4, - model_seperable = args.model_seperable, - model_half=args.model_half, - model_cache_size=args.max_gpu_cache_len, - model_use_eyebrow=args.eyebrow, + model_seperable = args.model_seperable, + model_half=args.model_half, + model_cache_size=args.max_gpu_cache_len, + model_use_eyebrow=args.eyebrow, - use_interpolation=args.use_interpolation, - interpolation_scale=args.interpolation_scale, - interpolation_half=args.interpolation_half, + use_interpolation=args.use_interpolation, + interpolation_scale=args.interpolation_scale, + interpolation_half=args.interpolation_half, - cacher_quality=args.cacher_quality, - cacher_ram_size=args.max_cache_len, + cacher_quality=args.cacher_quality, + cacher_ram_size=args.max_cache_len, - use_sr=args.use_sr, - sr_half=args.sr_half, - sr_x4=args.sr_x4, - sr_noise=args.sr_noise) + use_sr=args.use_sr, + sr_half=args.sr_half, + sr_x4=args.sr_x4, + sr_noise=args.sr_noise) self.model.setImage(self.input_image) input_pose = np.zeros((1, 45), dtype=np.float32) diff --git a/onnx_export_tha4.py b/onnx_export_tha4.py new file mode 100644 index 0000000..b346189 --- /dev/null +++ b/onnx_export_tha4.py @@ -0,0 +1,232 @@ +import torch +from os.path import join +from pathlib import Path +import sys +import os +sys.path.append(os.getcwd()) +sys.path.append(join(os.getcwd(), 'tha4', 'src')) +import numpy as np +import PIL.Image +import cv2 +import onnx +from onnxsim import simplify +import onnxruntime as ort +from torch import Tensor +from torch.nn import Module +from typing import List, Optional +from torch.nn.functional import interpolate +import matplotlib.pyplot as plt + +# ================================================================================= +# START: Monkey Patch for torch.nn.functional.affine_grid (CORRECTED, DEVICE-AWARE) +# ================================================================================= +import torch.nn.functional + +def linspace_from_neg_one(num_steps, dtype=torch.float32, device='cpu'): + r = torch.linspace(-1, 1, num_steps, dtype=torch.float32, device=device) + if num_steps > 1: r = r * (num_steps - 1) / num_steps + return r +def create_grid(N, C, H, W, device='cpu'): + grid = torch.empty((N, H, W, 3), dtype=torch.float32, device=device) + grid.select(-1, 0).copy_(linspace_from_neg_one(W, device=device)) + grid.select(-1, 1).copy_(linspace_from_neg_one(H, device=device).unsqueeze_(-1)) + grid.select(-1, 2).fill_(1); return grid +def custom_affine_grid(theta, size, align_corners=False): + N, C, H, W = size; device = theta.device + grid = create_grid(N, C, H, W, device=device) + grid = grid.view(N, H * W, 3).bmm(theta.transpose(1, 2)) + grid = grid.view(N, H, W, 2); return grid +def apply_affine_grid_patch(): + torch.nn.functional.affine_grid = custom_affine_grid + print("✅ Applied monkey patch to torch.nn.functional.affine_grid (Device-aware version).") +# ================================================================================= +# END: Monkey Patch +# ================================================================================= + +apply_affine_grid_patch() + +if len(sys.argv) != 2: + print("Usage: python onnx_export_tha4.py "); print("Available models: float, half") + raise ValueError('Do not get model name') + +MODEL_NAME = sys.argv[1] +HALF = (MODEL_NAME == 'half') +DEVICE_NAME = 'cuda:0' +IMAGE_INPUT = os.path.join('.', 'data', 'images', 'character.png') + +TMP_DIR = join('.', 'onnx_model_tha4', 'tmp'); Path(TMP_DIR).mkdir(parents=True, exist_ok=True) +MODEL_DIR = join('.', 'onnx_model_tha4', 'fp32' if not HALF else 'fp16'); Path(MODEL_DIR).mkdir(parents=True, exist_ok=True) +TMP_FILE_WRITE = join(TMP_DIR, 'tmp.onnx') + +gpu_device = torch.device(DEVICE_NAME); cpu_device = torch.device('cpu') +gpu_dtype = torch.float16 if HALF else torch.float32 +cpu_dtype = torch.float32 + +print(f"Using GPU device for validation: {gpu_device}, dtype: {gpu_dtype}") +print(f"Using CPU device for ONNX export, dtype: {cpu_dtype}") + +def torch_srgb_to_linear(x: torch.Tensor): + x = torch.clip(x, 0.0, 1.0); return torch.where(torch.le(x, 0.04045), x / 12.92, ((x + 0.055) / 1.055) ** 2.4) +def torch_linear_to_srgb(x): + x = torch.clip(x, 0.0, 1.0); return torch.where(torch.le(x, 0.003130804953560372), x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) +def numpy_linear_to_srgb(x): + x = np.clip(x, 0.0, 1.0); return np.where(x <= 0.003130804953560372, x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) +def load_tha4_poser(device: torch.device, use_half: bool): + from tha4.poser.modes.mode_07 import create_poser + m_files = { 'eyebrow_decomposer': 'data/models/tha4/eyebrow_decomposer.pt', 'eyebrow_morphing_combiner': 'data/models/tha4/eyebrow_morphing_combiner.pt', 'face_morpher': 'data/models/tha4/face_morpher.pt', 'body_morpher': 'data/models/tha4/body_morpher.pt', 'upscaler': 'data/models/tha4/upscaler.pt' } + print(f"Loading THA4 poser onto {device}..."); poser = create_poser(device=device, module_file_names=m_files) + if use_half and device.type == 'cuda': print("Converting poser modules to half precision (FP16)..."); poser.half() + return poser + +gpu_poser = load_tha4_poser(gpu_device, HALF) +cpu_poser = load_tha4_poser(cpu_device, False) +pose_size = gpu_poser.get_num_parameters(); print(f"Pose parameters: {pose_size}") + +def resize_PIL_image(img, size=(512, 512)): return img.resize(size, PIL.Image.Resampling.LANCZOS) +def extract_PIL_image_from_filelike(path): return PIL.Image.open(path) +def extract_pytorch_image_from_PIL_image(img): + arr = np.array(img) + if arr.shape[2] != 4: raise ValueError("Input image must be RGBA") + t = torch.from_numpy(arr).float() / 255.0 + t[:, :, :3] = torch_srgb_to_linear(t[:, :, :3]) + t = t.permute(2, 0, 1) * 2.0 - 1.0; return t + +pil_image = resize_PIL_image(extract_PIL_image_from_filelike(IMAGE_INPUT)) +pt_img_gpu = extract_pytorch_image_from_PIL_image(pil_image).to(gpu_dtype).unsqueeze(0).to(gpu_device) +zero_pose_gpu = torch.zeros(1, pose_size, dtype=gpu_dtype, device=gpu_device) +with torch.no_grad(): poser_torch_res = gpu_poser.pose(pt_img_gpu, zero_pose_gpu) +print(f"Reference GPU output shape: {poser_torch_res.shape}") + +print("\n" + "="*80); print("Starting ONNX export process on CPU..."); print("="*80) +cpu_modules = cpu_poser.get_modules() +cv_img_numpy = cv2.imread(IMAGE_INPUT, cv2.IMREAD_UNCHANGED) +cv_img_tensor_cpu = torch.from_numpy(cv_img_numpy) + +class EyebrowDecomposerWrapper(Module): + def __init__(self, obj): super().__init__(); self.m = obj + def forward(self, img: Tensor) -> List[Tensor]: + img_f = img.to(cpu_dtype) / 255.0; img_f[:, :, :3] = torch_srgb_to_linear(img_f[:, :, :3]) + # --- THE FIX: Swap BGR to RGB --- + img_f = img_f[:, :, [2, 1, 0, 3]] + img_c = img_f.permute(2, 0, 1).unsqueeze(0) * 2.0 - 1.0 + crop = img_c[:, :, 64:192, 192:320]; res = self.m(crop) + return [res[3], res[0], img_c] +eyebrow_decomposer_wrapper = EyebrowDecomposerWrapper(cpu_modules['eyebrow_decomposer']).eval() +with torch.no_grad(): decomposer_res_cpu = eyebrow_decomposer_wrapper(cv_img_tensor_cpu) +torch.onnx.export(eyebrow_decomposer_wrapper, cv_img_tensor_cpu, TMP_FILE_WRITE, opset_version=16, input_names=['input_image'], output_names=["background_layer", "eyebrow_layer", "image_prepared"]) +onnx_model = onnx.load(TMP_FILE_WRITE); onnx.checker.check_model(onnx_model); onnx_model_sim, check = simplify(onnx_model) +onnx.save(onnx_model_sim if check else onnx_model, join(MODEL_DIR, "decomposer.onnx")); print("✓ Decomposer exported") + +eyebrow_pose_cpu = torch.zeros(1, 12, dtype=cpu_dtype) +class EyebrowCombinerWrapper(Module): + def __init__(self, obj): super().__init__(); self.m = obj + def forward(self, img: Tensor, bg: Tensor, layer: Tensor, pose: Tensor) -> Tensor: + crop = img[:, :, 32:224, 160:352].clone() + morphed = self.m(bg, layer, pose)[2] + crop[:, :, 32:160, 32:160] = morphed; return crop +eyebrow_combiner_wrapper = EyebrowCombinerWrapper(cpu_modules['eyebrow_morphing_combiner']).eval() +combiner_inputs_cpu = (decomposer_res_cpu[2], decomposer_res_cpu[0], decomposer_res_cpu[1], eyebrow_pose_cpu) +with torch.no_grad(): combiner_res_cpu = eyebrow_combiner_wrapper(*combiner_inputs_cpu) +torch.onnx.export(eyebrow_combiner_wrapper, combiner_inputs_cpu, TMP_FILE_WRITE, opset_version=16, input_names=['image_prepared', 'eyebrow_background_layer', "eyebrow_layer", 'eyebrow_pose'], output_names=['eyebrow_image']) +onnx_model = onnx.load(TMP_FILE_WRITE); onnx.checker.check_model(onnx_model); onnx_model_sim, check = simplify(onnx_model) +onnx.save(onnx_model_sim if check else onnx_model, join(MODEL_DIR, "combiner.onnx")); print("✓ Combiner exported") + +face_pose_cpu = torch.zeros(1, 27, dtype=cpu_dtype) +class FaceMorpherWrapper(Module): + def __init__(self, obj): super().__init__(); self.m = obj + def forward(self, img: Tensor, crop: Tensor, pose: Tensor) -> List[Tensor]: + morphed = self.m(crop, pose)[0]; full = img.clone() + full[:, :, 32:224, 160:352] = morphed + half = interpolate(full, size=(256, 256), mode='bilinear', align_corners=False); return [full, half] +face_morpher_wrapper = FaceMorpherWrapper(cpu_modules['face_morpher']).eval() +morpher_inputs_cpu = (decomposer_res_cpu[2], combiner_res_cpu, face_pose_cpu) +with torch.no_grad(): morpher_res_cpu = face_morpher_wrapper(*morpher_inputs_cpu) +torch.onnx.export(face_morpher_wrapper, morpher_inputs_cpu, TMP_FILE_WRITE, opset_version=16, input_names=['image_prepared', 'im_morpher_crop', 'face_pose'], output_names=['face_morphed_full', 'face_morphed_half']) +onnx_model = onnx.load(TMP_FILE_WRITE); onnx.checker.check_model(onnx_model); onnx_model_sim, check = simplify(onnx_model) +onnx.save(onnx_model_sim if check else onnx_model, join(MODEL_DIR, "morpher.onnx")); print("✓ Face Morpher exported") + +rotation_pose_cpu = torch.zeros(1, 6, dtype=cpu_dtype) +class BodyMorpherWrapper(Module): + def __init__(self, obj): super().__init__(); self.m = obj + def forward(self, img: Tensor, pose: Tensor) -> List[Tensor]: + res = self.m(img, pose); return [res[0], res[3]] +body_morpher_wrapper = BodyMorpherWrapper(cpu_modules['body_morpher']).eval() +body_morpher_inputs_cpu = (morpher_res_cpu[1], rotation_pose_cpu) +with torch.no_grad(): body_morpher_res_cpu = body_morpher_wrapper(*body_morpher_inputs_cpu) +torch.onnx.export(body_morpher_wrapper, body_morpher_inputs_cpu, TMP_FILE_WRITE, opset_version=16, input_names=['face_morphed_half', 'rotation_pose'], output_names=['half_res_posed_image', 'half_res_grid_change']) +onnx_model = onnx.load(TMP_FILE_WRITE); onnx.checker.check_model(onnx_model); onnx_model_sim, check = simplify(onnx_model) +onnx.save(onnx_model_sim if check else onnx_model, join(MODEL_DIR, "body_morpher.onnx")); print("✓ Body Morpher exported") + +class UpscalerWrapper(Module): + def __init__(self, obj): super().__init__(); self.m = obj + def forward(self, rest_img: Tensor, posed_half: Tensor, grid_half: Tensor, pose: Tensor) -> List[Tensor]: + posed_full = interpolate(posed_half, size=(512, 512), mode='bilinear', align_corners=False) + grid_full = interpolate(grid_half, size=(512, 512), mode='bilinear', align_corners=False) + res = self.m(rest_img, posed_full, grid_full, pose)[0] + output = res / 2.0 + 0.5; output[:, :3, :, :] = torch_linear_to_srgb(output[:, :3, :, :]) + cv_res = (output.squeeze(0).permute(1, 2, 0)[:, :, [2, 1, 0, 3]] * 255).clamp(0, 255).to(torch.uint8) + return [output, cv_res] +upscaler_wrapper = UpscalerWrapper(cpu_modules['upscaler']).eval() +upscaler_inputs_cpu = (morpher_res_cpu[0], body_morpher_res_cpu[0], body_morpher_res_cpu[1], rotation_pose_cpu) +with torch.no_grad(): upscaler_res_cpu = upscaler_wrapper(*upscaler_inputs_cpu) +torch.onnx.export(upscaler_wrapper, upscaler_inputs_cpu, TMP_FILE_WRITE, opset_version=16, input_names=['rest_image', 'half_res_posed_image', 'half_res_grid_change', 'rotation_pose'], output_names=['result', 'cv_result']) +onnx_model = onnx.load(TMP_FILE_WRITE); onnx.checker.check_model(onnx_model); onnx_model_sim, check = simplify(onnx_model) +onnx.save(onnx_model_sim if check else onnx_model, join(MODEL_DIR, "upscaler.onnx")); print("✓ Upscaler exported") + +print("\n" + "="*80); print("Validation: Running ONNX inference..."); print("="*80) +class THA4ONNXRunner: + def __init__(self, model_dir, use_gpu=True): + providers = [("CUDAExecutionProvider", {"device_id": 0})] if use_gpu else ['CPUExecutionProvider'] + self.decomposer_sess = self._create_session(join(model_dir, 'decomposer.onnx'), providers) + self.combiner_sess = self._create_session(join(model_dir, "combiner.onnx"), providers) + self.morpher_sess = self._create_session(join(model_dir, "morpher.onnx"), providers) + self.body_morpher_sess = self._create_session(join(model_dir, "body_morpher.onnx"), providers) + self.upscaler_sess = self._create_session(join(model_dir, "upscaler.onnx"), providers) + print("✓ All ONNX models loaded") + def _create_session(self, path, providers): + try: return ort.InferenceSession(path, providers=providers) + except Exception: print(f"Failed with {providers}, falling back to CPU for {os.path.basename(path)}"); return ort.InferenceSession(path, providers=['CPUExecutionProvider']) + def run(self, img_path, p_eye, p_face, p_rot): + img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED) + d_res = self.decomposer_sess.run(None, {'input_image': img}) + c_res = self.combiner_sess.run(None, {'image_prepared': d_res[2], 'eyebrow_background_layer': d_res[0], "eyebrow_layer": d_res[1], 'eyebrow_pose': p_eye}) + m_res = self.morpher_sess.run(None, {'image_prepared': d_res[2], 'im_morpher_crop': c_res[0], 'face_pose': p_face}) + b_res = self.body_morpher_sess.run(None, {'face_morphed_half': m_res[1], 'rotation_pose': p_rot}) + u_res = self.upscaler_sess.run(None, {'rest_image': m_res[0], 'half_res_posed_image': b_res[0], 'half_res_grid_change': b_res[1], 'rotation_pose': p_rot}) + return u_res[0] + +def save_comparison_images(onnx_img_np, torch_img_np, prefix=""): + onnx_hwc = np.transpose(onnx_img_np.squeeze(0), (1, 2, 0))[:, :, :3] + torch_hwc = np.transpose(torch_img_np.squeeze(0), (1, 2, 0))[:, :, :3] + onnx_hwc = np.clip(onnx_hwc, 0, 1) + torch_hwc = np.clip(torch_hwc, 0, 1) + onnx_path = f"{prefix}onnx_output.png"; torch_path = f"{prefix}pytorch_output.png" + plt.imsave(onnx_path, onnx_hwc); plt.imsave(torch_path, torch_hwc) + print(f"✅ Saved comparison images: {onnx_path} and {torch_path}") + +runner = THA4ONNXRunner(MODEL_DIR) +p_eye_z, p_face_z, p_rot_z = np.zeros((1, 12), dtype=np.float32), np.zeros((1, 27), dtype=np.float32), np.zeros((1, 6), dtype=np.float32) + +print("\nRunning ONNX inference with zero pose...") +onnx_output = runner.run(IMAGE_INPUT, p_eye_z, p_face_z, p_rot_z) +ref_np = poser_torch_res.cpu().numpy() / 2.0 + 0.5; ref_np[0, :3, :, :] = numpy_linear_to_srgb(ref_np[0, :3, :, :]) +save_comparison_images(onnx_output, ref_np, "zero_pose_") +mse = ((onnx_output - ref_np) ** 2).mean(); print(f"\n{'='*80}\nMSE (zero pose): {mse:.10f}\n{'='*80}") +ACCEPTABLE_MSE_THRESHOLD = 1e-2 +if mse < ACCEPTABLE_MSE_THRESHOLD: print(f"✅ Validation PASSED! MSE is within the acceptable range of {ACCEPTABLE_MSE_THRESHOLD}.") +else: print(f"⚠️ Warning: MSE is higher than expected! But this can be normal due to precision differences.") + +print("\n" + "="*80); print("Testing with random poses..."); print("="*80) +p_eye_r = np.random.randn(1, 12).astype(np.float32) * 0.5 +p_face_r = np.random.randn(1, 27).astype(np.float32) * 0.5 +p_rot_r = np.random.randn(1, 6).astype(np.float32) * 0.3 +onnx_output_r = runner.run(IMAGE_INPUT, p_eye_r, p_face_r, p_rot_r) +pose_r_gpu = torch.from_numpy(np.concatenate([p_eye_r, p_face_r, p_rot_r], axis=1)).to(gpu_dtype).to(gpu_device) +with torch.no_grad(): ref_output_r = gpu_poser.pose(pt_img_gpu, pose_r_gpu) +ref_np_r = ref_output_r.cpu().numpy() / 2.0 + 0.5; ref_np_r[0, :3, :, :] = numpy_linear_to_srgb(ref_np_r[0, :3, :, :]) +save_comparison_images(onnx_output_r, ref_np_r, "random_pose_") +mse_r = ((onnx_output_r - ref_np_r) ** 2).mean(); print(f"MSE (random pose): {mse_r:.10f}") +if mse_r < ACCEPTABLE_MSE_THRESHOLD: print(f"✅ Random pose test PASSED! (Threshold: {ACCEPTABLE_MSE_THRESHOLD})") +else: print(f"⚠️ Warning: High MSE with random pose! But this can be normal.") + +print(f"\n{'='*80}\nExport and validation completed!\nModels saved to: {MODEL_DIR}\n{'='*80}") \ No newline at end of file diff --git a/tha4_core.py b/tha4_core.py index d57ad70..6d2d33d 100644 --- a/tha4_core.py +++ b/tha4_core.py @@ -1,5 +1,5 @@ """ -THA4 Core - Pure PyTorch implementation +THA4 Core - PyTorch THA4 model with optional TensorRT RIFE/SR acceleration Provides interface compatible with CoreTRT/CoreORT for THA4 models """ import torch @@ -13,10 +13,22 @@ import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'tha4', 'src')) from tha4.image_util import convert_linear_to_srgb +# Add ezvtuber-rt path for TensorRT modules +dir_path = os.path.dirname(os.path.realpath(__file__)) +ezvtb_path = os.path.join(dir_path, 'ezvtuber-rt') +if os.path.exists(ezvtb_path) and ezvtb_path not in sys.path: + sys.path.append(ezvtb_path) + class THA4Core: """ - THA4 Core class with interface compatible with CoreTRT/CoreORT + THA4 Core class - Pure PyTorch implementation (No TensorRT) + + This class only uses PyTorch for THA4 model inference. + It does NOT support real frame interpolation or super resolution. + If interpolation_scale > 1, frames will be simply duplicated. + + For real RIFE interpolation and SR with TensorRT, use THA4CoreWithAccel. Uses CharacterModel to load YAML + PNG + PT files properly. The PNG image from YAML is used as the source image. @@ -26,7 +38,7 @@ class THA4Core: - inference(pose: np.ndarray) -> List[np.ndarray] - run inference """ - def __init__(self, device_id=0, use_eyebrow=True, yaml_path=None, + def __init__(self, device_id=0, use_eyebrow=True, yaml_path=None, interpolation_scale=1): """ Initialize THA4 Core @@ -35,11 +47,12 @@ class THA4Core: device_id: GPU device ID use_eyebrow: whether to use eyebrow parameters yaml_path: path to character_model.yaml - Default: 'data/models/tha4/character_model.yaml' interpolation_scale: number of output frames to generate - (THA4 doesn't support real interpolation, will duplicate frames) + (THA4 doesn't support real interpolation, + will duplicate frames) """ - device_str = f'cuda:{device_id}' if torch.cuda.is_available() else 'cpu' + device_str = f'cuda:{device_id}' if torch.cuda.is_available() \ + else 'cpu' self.device = torch.device(device_str) self.use_eyebrow = use_eyebrow self.interpolation_scale = interpolation_scale @@ -54,10 +67,12 @@ class THA4Core: self.cacher = None print(f"THA4 Core initialized on {self.device}") - print(f"Using character image from model (ignoring setImage calls)") + print("Using character image from model (ignoring setImage calls)") if interpolation_scale > 1: - print(f"Note: THA4 doesn't support interpolation, " - f"will duplicate frames {interpolation_scale}x") + print(f"Note: Pure PyTorch mode doesn't support real " + f"interpolation, will duplicate frames {interpolation_scale}x") + print(" Use THA4CoreWithAccel with TensorRT for " + "real RIFE interpolation") def setImage(self, img: np.ndarray): """ @@ -80,11 +95,12 @@ class THA4Core: Args: pose: numpy array of shape (1, 45) [0:12] - eyebrow parameters - [12:39] - face parameters + [12:39] - face parameters [39:45] - head pose parameters Returns: - List of output images as numpy arrays [H, W, 4] in BGRA format, uint8 + List of output images as numpy arrays [H, W, 4] + in BGRA format, uint8 """ if self.input_image_tensor is None: raise RuntimeError("Image not set. Call setImage() first.") @@ -104,7 +120,8 @@ class THA4Core: head_tensor = torch.from_numpy(head_pose).to(self.device) # Call wrapper (it handles the pose conversion internally) - # We need to create dummy compressed versions for interface compatibility + # We need to create dummy compressed versions for + # interface compatibility eyebrow_c = eyebrow_pose[0].tolist() face_c = face_pose[0].tolist() @@ -139,12 +156,305 @@ class THA4Core: output_bgra = cv2.cvtColor(output_np, cv2.COLOR_RGBA2BGRA) # Return as list with proper number of frames - # THA4 doesn't support real interpolation, so duplicate frames if needed + # Pure PyTorch mode: duplicate frames (no real interpolation) + # Use THA4CoreWithAccel with TensorRT for real RIFE interpolation return [output_bgra] * self.interpolation_scale +class THA4CoreWithAccel: + """ + THA4 Core with TensorRT acceleration for RIFE and SR + + Architecture: + - THA4 main model: PyTorch (PT files) - always PyTorch + - RIFE interpolation: TensorRT (optional) - REAL frame interpolation + - SR super-resolution: TensorRT (optional) - REAL upscaling + - Cacher: RAM cache (optional) + + This class provides REAL interpolation and super resolution using + TensorRT, unlike THA4Core which only duplicates frames. + + Follows the same design as CoreTRT for THA3 models. + """ + + def __init__(self, device_id=0, use_eyebrow=True, yaml_path=None, + use_tensorrt=False, use_interpolation=False, + interpolation_scale=2, interpolation_half=True, + use_sr=False, sr_x4=True, sr_half=True, sr_noise=1, + cacher_quality=85, cacher_ram_size=2.0): + """ + Initialize THA4 Core with optional TensorRT acceleration + + Args: + device_id: GPU device ID + use_eyebrow: whether to use eyebrow parameters + yaml_path: path to character_model.yaml + use_tensorrt: enable TensorRT for RIFE/SR + use_interpolation: enable RIFE frame interpolation + interpolation_scale: interpolation scale (2/3/4) + interpolation_half: use FP16 for RIFE + use_sr: enable super resolution + sr_x4: use 4x SR (else 2x) + sr_half: use FP16 for SR + sr_noise: noise level for waifu2x (1/2/3) + cacher_quality: cache compression quality + cacher_ram_size: RAM cache size in GB + """ + self.device_id = device_id + self.use_eyebrow = use_eyebrow + self.use_tensorrt = use_tensorrt + self.use_interpolation = use_interpolation + self.use_sr = use_sr + + # Initialize PyTorch device + device_str = f'cuda:{device_id}' if torch.cuda.is_available() \ + else 'cpu' + self.device = torch.device(device_str) + + # Initialize THA4 wrapper (PyTorch) + self.wrapper = THA4Wrapper(device=self.device, yaml_path=yaml_path) + self.input_image_tensor = self.wrapper.character_image + + # Initialize TensorRT components if requested + self.rife = None + self.sr = None + self.cacher = None + self.scale = 1 + self.instream = None + self.output_memory = None + + if use_tensorrt and (use_interpolation or use_sr): + # Initialize CUDA context for TensorRT + self._init_tensorrt(device_id) + + # Build model paths + rife_model_path = '' + if use_interpolation: + rife_model_path = os.path.join( + '.', 'data', 'models', 'rife_512', + f'x{interpolation_scale}', + 'fp16' if interpolation_half else 'fp32') + + sr_model_path = '' + if use_sr: + if sr_x4: + base_path = os.path.join('.', 'data', 'models', + 'Real-ESRGAN') + sr_model_path = os.path.join( + base_path, + 'exported_256_fp16' if sr_half else 'exported_256') + else: # x2 + base_path = os.path.join('.', 'data', 'models', + 'waifu2x_upconv', + 'fp16' if sr_half else 'fp32', + 'upconv_7', 'art') + sr_model_path = os.path.join( + base_path, f'noise{sr_noise}_scale2x') + + # Initialize TensorRT modules + self._init_trt_modules(rife_model_path, sr_model_path) + + # Initialize cacher + if cacher_ram_size > 0.0: + from ezvtb_rt.cache import Cacher + self.cacher = Cacher(cacher_ram_size, cacher_quality) + + print("THA4 Core initialized:") + print(f" Device: {self.device}") + print(f" TensorRT: {use_tensorrt}") + interp_info = f"scale: {interpolation_scale}" \ + if use_interpolation else "N/A" + print(f" Interpolation: {use_interpolation} ({interp_info})") + if use_interpolation: + print(" (Real RIFE interpolation via TensorRT)") + print(f" Super Resolution: {use_sr}") + + def _init_tensorrt(self, device_id): + """Initialize TensorRT and CUDA context""" + try: + from ezvtb_rt.trt_utils import cudaSetDevice + cudaSetDevice(device_id) + os.environ['CUDA_DEVICE'] = str(device_id) + import pycuda.autoinit + device_name = pycuda.autoinit.device.name() + print(f'TensorRT initialized on device: {device_name}') + except Exception as e: + print(f'Warning: Failed to initialize TensorRT: {e}') + self.use_tensorrt = False + + def _init_trt_modules(self, rife_model_path, sr_model_path): + """Initialize RIFE and SR TensorRT modules""" + try: + import pycuda.driver as cuda + + # Create CUDA stream + self.instream = cuda.Stream() + + # Create output memory compatible with RIFE input + # [1, 512, 512, 4] + output_shape = (1, 512, 512, 4) + self.output_memory = self._create_host_device_mem( + output_shape, np.uint8) + + # Initialize RIFE + if self.use_interpolation and rife_model_path: + from ezvtb_rt.rife import RIFE + + self.rife = RIFE(rife_model_path, self.instream, + self.output_memory) + self.scale = self.rife.scale + print(f"RIFE TensorRT module loaded: {rife_model_path}") + + # Initialize SR + if self.use_sr and sr_model_path: + from ezvtb_rt.sr import SR + + if self.rife is not None: + instream = self.rife.instream + mems = [self.rife.memories['framegen_'+str(i)] + for i in range(self.rife.scale)] + else: + instream = self.instream + mems = [self.output_memory] + + self.sr = SR(sr_model_path, instream, mems) + print(f"SR TensorRT module loaded: {sr_model_path}") + + except Exception as e: + print(f'Warning: Failed to initialize TensorRT modules: {e}') + self.rife = None + self.sr = None + + def _create_host_device_mem(self, shape, dtype): + """Create HostDeviceMem for TensorRT modules""" + import pycuda.driver as cuda + from ezvtb_rt.trt_utils import HostDeviceMem + + host_mem = cuda.pagelocked_empty(shape, dtype) + device_mem = cuda.mem_alloc(host_mem.nbytes) + return HostDeviceMem(host_mem, device_mem) + + def setImage(self, img: np.ndarray): + """Set input character image - IGNORED for THA4""" + # THA4 uses character image from YAML, ignore external image + pass + + def inference(self, pose: np.ndarray) -> list: + """ + Run full inference pipeline: THA4 -> RIFE (TRT) -> SR (TRT) + + Args: + pose: numpy array of shape (1, 45) + + Returns: + List of output images [H, W, 4] in BGRA format, uint8 + """ + if self.input_image_tensor is None: + raise RuntimeError("Image not set.") + + pose = pose.astype(np.float32) + + # Cache management + need_cache_write = 0 + res_carrier = None + + # Step 1: THA4 inference (PyTorch) with optional caching + if self.cacher is None: + output_bgra = self._run_tha4_inference(pose) + res_carrier = [output_bgra] + else: + hs = hash(str(pose)) + cached = self.cacher.read(hs) + + if cached is not None: # Cache hit + output_bgra = cached + res_carrier = [cached] + + # If using TensorRT, upload to GPU + if self.use_tensorrt and \ + (self.rife is not None or self.sr is not None): + np.copyto(self.output_memory.host, + output_bgra.reshape(1, 512, 512, 4)) + self.output_memory.htod(self.instream) + else: # Cache miss + output_bgra = self._run_tha4_inference(pose) + need_cache_write = hs + res_carrier = [output_bgra] + + # Step 2: RIFE interpolation (TensorRT) + if self.rife is not None: + self.rife.inference() + res_carrier = self.rife + + # Step 3: SR super resolution (TensorRT) + if self.sr is not None: + self.sr.inference() + res_carrier = self.sr + + # Step 4: Fetch results + if need_cache_write != 0: + self.cacher.write(need_cache_write, output_bgra) + + # Get final results + if type(res_carrier) is not list: + res_carrier = res_carrier.fetchRes() + + return res_carrier + + def _run_tha4_inference(self, pose: np.ndarray) -> np.ndarray: + """Run THA4 PyTorch inference and return BGRA numpy array""" + # Extract pose components + if self.use_eyebrow: + eyebrow_pose = pose[:, :12] + else: + eyebrow_pose = np.zeros((1, 12), dtype=np.float32) + + face_pose = pose[:, 12:39] + head_pose = pose[:, 39:45] + + # Convert to torch tensors + eyebrow_tensor = torch.from_numpy(eyebrow_pose).to(self.device) + face_tensor = torch.from_numpy(face_pose).to(self.device) + head_tensor = torch.from_numpy(head_pose).to(self.device) + + eyebrow_c = eyebrow_pose[0].tolist() + face_c = face_pose[0].tolist() + + # Run THA4 inference + with torch.no_grad(): + output_tensor = self.wrapper.forward( + self.input_image_tensor, + face_tensor, + head_tensor, + eyebrow_tensor, + face_c, + eyebrow_c, + ratio=None + ) + + # Post-process output + output_image = output_tensor[0].float() + output_image = torch.clamp((output_image + 1.0) / 2.0, 0.0, 1.0) + output_image = convert_linear_to_srgb(output_image) + output_np = output_image.permute(1, 2, 0).cpu().numpy() + output_np = (output_np * 255.0).astype(np.uint8) + output_bgra = cv2.cvtColor(output_np, cv2.COLOR_RGBA2BGRA) + + # Upload to GPU memory if TensorRT is active + if self.use_tensorrt and \ + (self.rife is not None or self.sr is not None): + np.copyto(self.output_memory.host, + output_bgra.reshape(1, 512, 512, 4)) + self.output_memory.htod(self.instream) + + return output_bgra + + def get_tha4_core(device_id=0, use_eyebrow=True, yaml_path=None, - interpolation_scale=1): + interpolation_scale=1, use_tensorrt=False, + use_interpolation=False, interpolation_half=True, + use_sr=False, sr_x4=True, sr_half=True, sr_noise=1, + cacher_quality=85, cacher_ram_size=2.0): """ Factory function to create THA4 core @@ -152,13 +462,43 @@ def get_tha4_core(device_id=0, use_eyebrow=True, yaml_path=None, device_id: GPU device ID use_eyebrow: whether to use eyebrow control yaml_path: path to character_model.yaml - Default: 'data/models/tha4/character_model.yaml' - interpolation_scale: number of frames (for compatibility with interpolation) + interpolation_scale: number of frames for interpolation + use_tensorrt: enable TensorRT acceleration for RIFE/SR + use_interpolation: enable RIFE frame interpolation + interpolation_half: use FP16 for RIFE + use_sr: enable super resolution + sr_x4: use 4x SR (else 2x) + sr_half: use FP16 for SR + sr_noise: noise level for waifu2x + cacher_quality: cache compression quality + cacher_ram_size: RAM cache size in GB Returns: - THA4Core instance + THA4Core or THA4CoreWithAccel instance """ - return THA4Core(device_id=device_id, - use_eyebrow=use_eyebrow, - yaml_path=yaml_path, - interpolation_scale=interpolation_scale) + # Use accelerated version if TensorRT is requested + if use_tensorrt and (use_interpolation or use_sr): + return THA4CoreWithAccel( + device_id=device_id, + use_eyebrow=use_eyebrow, + yaml_path=yaml_path, + use_tensorrt=use_tensorrt, + use_interpolation=use_interpolation, + interpolation_scale=interpolation_scale, + interpolation_half=interpolation_half, + use_sr=use_sr, + sr_x4=sr_x4, + sr_half=sr_half, + sr_noise=sr_noise, + cacher_quality=cacher_quality, + cacher_ram_size=cacher_ram_size + ) + else: + # Use simple PyTorch-only version + return THA4Core( + device_id=device_id, + use_eyebrow=use_eyebrow, + yaml_path=yaml_path, + interpolation_scale=interpolation_scale if use_interpolation + else 1 + ) diff --git a/tha4_core_old.py b/tha4_core_old.py new file mode 100644 index 0000000..5add4d5 --- /dev/null +++ b/tha4_core_old.py @@ -0,0 +1,498 @@ +""" +THA4 Core - ONNX-based THA4 model with optional TensorRT/RIFE/SR acceleration +Provides interface compatible with CoreTRT/CoreORT for THA4 models +Uses teacher models (5 models) from onnx_model_tha4 folder +""" +import numpy as np +import sys +import os + +# Add ezvtuber-rt path for TensorRT/RIFE/SR modules +dir_path = os.path.dirname(os.path.realpath(__file__)) +ezvtb_path = os.path.join(dir_path, 'ezvtuber-rt') +if os.path.exists(ezvtb_path) and ezvtb_path not in sys.path: + sys.path.append(ezvtb_path) + + +class THA4CoreTRT: + """ + THA4 Core class - Pure PyTorch implementation (No TensorRT) + + This class only uses PyTorch for THA4 model inference. + It does NOT support real frame interpolation or super resolution. + If interpolation_scale > 1, frames will be simply duplicated. + + For real RIFE interpolation and SR with TensorRT, use THA4CoreWithAccel. + + Uses CharacterModel to load YAML + PNG + PT files properly. + The PNG image from YAML is used as the source image. + + Expected interface: + - setImage(img: np.ndarray) - NOT USED, image comes from YAML + - inference(pose: np.ndarray) -> List[np.ndarray] - run inference + """ + + def __init__(self, device_id=0, use_eyebrow=True, yaml_path=None, + interpolation_scale=1): + """ + Initialize THA4 Core + + Args: + device_id: GPU device ID + use_eyebrow: whether to use eyebrow parameters + yaml_path: path to character_model.yaml + interpolation_scale: number of output frames to generate + (THA4 doesn't support real interpolation, + will duplicate frames) + """ + device_str = f'cuda:{device_id}' if torch.cuda.is_available() \ + else 'cpu' + self.device = torch.device(device_str) + self.use_eyebrow = use_eyebrow + self.interpolation_scale = interpolation_scale + + # Create THA4 wrapper (loads YAML + PNG + PT) + self.wrapper = THA4Wrapper(device=self.device, yaml_path=yaml_path) + + # Use character image from YAML (already loaded by wrapper) + self.input_image_tensor = self.wrapper.character_image + + # For compatibility (no caching in THA4 core) + self.cacher = None + + print(f"THA4 Core initialized on {self.device}") + print("Using character image from model (ignoring setImage calls)") + if interpolation_scale > 1: + print(f"Note: Pure PyTorch mode doesn't support real " + f"interpolation, will duplicate frames {interpolation_scale}x") + print(" Use THA4CoreWithAccel with TensorRT for " + "real RIFE interpolation") + + def setImage(self, img: np.ndarray): + """ + Set input character image - IGNORED for THA4 + + THA4 uses the character image from the YAML/PNG files loaded + during initialization. This method exists only for interface + compatibility with CoreTRT/CoreORT. + + Args: + img: numpy array in BGRA format (ignored) + """ + # THA4 uses character image from YAML, ignore external image + print("THA4: setImage() called but ignored (using image from YAML)") + + def inference(self, pose: np.ndarray) -> list: + """ + Run inference + + Args: + pose: numpy array of shape (1, 45) + [0:12] - eyebrow parameters + [12:39] - face parameters + [39:45] - head pose parameters + + Returns: + List of output images as numpy arrays [H, W, 4] + in BGRA format, uint8 + """ + if self.input_image_tensor is None: + raise RuntimeError("Image not set. Call setImage() first.") + + # Extract pose components + if self.use_eyebrow: + eyebrow_pose = pose[:, :12] # [1, 12] + else: + eyebrow_pose = np.zeros((1, 12), dtype=np.float32) + + face_pose = pose[:, 12:39] # [1, 27] + head_pose = pose[:, 39:45] # [1, 6] + + # Convert to torch tensors + eyebrow_tensor = torch.from_numpy(eyebrow_pose).to(self.device) + face_tensor = torch.from_numpy(face_pose).to(self.device) + head_tensor = torch.from_numpy(head_pose).to(self.device) + + # Call wrapper (it handles the pose conversion internally) + # We need to create dummy compressed versions for + # interface compatibility + eyebrow_c = eyebrow_pose[0].tolist() + face_c = face_pose[0].tolist() + + with torch.no_grad(): + output_tensor = self.wrapper.forward( + self.input_image_tensor, + face_tensor, + head_tensor, + eyebrow_tensor, + face_c, + eyebrow_c, + ratio=None + ) + + # THA4 outputs in linear color space, convert to sRGB + # Output is [batch, 4, H, W] in range [-1, 1] + output_image = output_tensor[0].float() # [4, 512, 512] + + # Clip to [-1, 1] and convert to [0, 1] + output_image = torch.clamp((output_image + 1.0) / 2.0, 0.0, 1.0) + + # Convert linear RGB to sRGB (keep alpha as-is) + output_image = convert_linear_to_srgb(output_image) + + # Convert to [H, W, C] numpy + output_np = output_image.permute(1, 2, 0).cpu().numpy() + + # Convert to uint8 + output_np = (output_np * 255.0).astype(np.uint8) + + # Convert RGBA to BGRA for output + output_bgra = cv2.cvtColor(output_np, cv2.COLOR_RGBA2BGRA) + + # Return as list with proper number of frames + # Pure PyTorch mode: duplicate frames (no real interpolation) + # Use THA4CoreWithAccel with TensorRT for real RIFE interpolation + return [output_bgra] * self.interpolation_scale + + +class THA4CoreWithAccel: + """ + THA4 Core with TensorRT acceleration for RIFE and SR + + Architecture: + - THA4 main model: PyTorch (PT files) - always PyTorch + - RIFE interpolation: TensorRT (optional) - REAL frame interpolation + - SR super-resolution: TensorRT (optional) - REAL upscaling + - Cacher: RAM cache (optional) + + This class provides REAL interpolation and super resolution using + TensorRT, unlike THA4Core which only duplicates frames. + + Follows the same design as CoreTRT for THA3 models. + """ + + def __init__(self, device_id=0, use_eyebrow=True, yaml_path=None, + use_tensorrt=False, use_interpolation=False, + interpolation_scale=2, interpolation_half=True, + use_sr=False, sr_x4=True, sr_half=True, sr_noise=1, + cacher_quality=85, cacher_ram_size=2.0): + """ + Initialize THA4 Core with optional TensorRT acceleration + + Args: + device_id: GPU device ID + use_eyebrow: whether to use eyebrow parameters + yaml_path: path to character_model.yaml + use_tensorrt: enable TensorRT for RIFE/SR + use_interpolation: enable RIFE frame interpolation + interpolation_scale: interpolation scale (2/3/4) + interpolation_half: use FP16 for RIFE + use_sr: enable super resolution + sr_x4: use 4x SR (else 2x) + sr_half: use FP16 for SR + sr_noise: noise level for waifu2x (1/2/3) + cacher_quality: cache compression quality + cacher_ram_size: RAM cache size in GB + """ + self.device_id = device_id + self.use_eyebrow = use_eyebrow + self.use_tensorrt = use_tensorrt + self.use_interpolation = use_interpolation + self.use_sr = use_sr + + # Initialize PyTorch device + device_str = f'cuda:{device_id}' if torch.cuda.is_available() \ + else 'cpu' + self.device = torch.device(device_str) + + # Initialize THA4 wrapper (PyTorch) + self.wrapper = THA4Wrapper(device=self.device, yaml_path=yaml_path) + self.input_image_tensor = self.wrapper.character_image + + # Initialize TensorRT components if requested + self.rife = None + self.sr = None + self.cacher = None + self.scale = 1 + self.instream = None + self.output_memory = None + + if use_tensorrt and (use_interpolation or use_sr): + # Initialize CUDA context for TensorRT + self._init_tensorrt(device_id) + + # Build model paths + rife_model_path = '' + if use_interpolation: + rife_model_path = os.path.join( + '.', 'data', 'models', 'rife_512', + f'x{interpolation_scale}', + 'fp16' if interpolation_half else 'fp32') + + sr_model_path = '' + if use_sr: + if sr_x4: + base_path = os.path.join('.', 'data', 'models', + 'Real-ESRGAN') + sr_model_path = os.path.join( + base_path, + 'exported_256_fp16' if sr_half else 'exported_256') + else: # x2 + base_path = os.path.join('.', 'data', 'models', + 'waifu2x_upconv', + 'fp16' if sr_half else 'fp32', + 'upconv_7', 'art') + sr_model_path = os.path.join( + base_path, f'noise{sr_noise}_scale2x') + + # Initialize TensorRT modules + self._init_trt_modules(rife_model_path, sr_model_path) + + # Initialize cacher + if cacher_ram_size > 0.0: + from ezvtb_rt.cache import Cacher + self.cacher = Cacher(cacher_ram_size, cacher_quality) + + print("THA4 Core initialized:") + print(f" Device: {self.device}") + print(f" TensorRT: {use_tensorrt}") + interp_info = f"scale: {interpolation_scale}" \ + if use_interpolation else "N/A" + print(f" Interpolation: {use_interpolation} ({interp_info})") + if use_interpolation: + print(" (Real RIFE interpolation via TensorRT)") + print(f" Super Resolution: {use_sr}") + + def _init_tensorrt(self, device_id): + """Initialize TensorRT and CUDA context""" + try: + from ezvtb_rt.trt_utils import cudaSetDevice + cudaSetDevice(device_id) + os.environ['CUDA_DEVICE'] = str(device_id) + import pycuda.autoinit + device_name = pycuda.autoinit.device.name() + print(f'TensorRT initialized on device: {device_name}') + except Exception as e: + print(f'Warning: Failed to initialize TensorRT: {e}') + self.use_tensorrt = False + + def _init_trt_modules(self, rife_model_path, sr_model_path): + """Initialize RIFE and SR TensorRT modules""" + try: + import pycuda.driver as cuda + + # Create CUDA stream + self.instream = cuda.Stream() + + # Create output memory compatible with RIFE input + # [1, 512, 512, 4] + output_shape = (1, 512, 512, 4) + self.output_memory = self._create_host_device_mem( + output_shape, np.uint8) + + # Initialize RIFE + if self.use_interpolation and rife_model_path: + from ezvtb_rt.rife import RIFE + + self.rife = RIFE(rife_model_path, self.instream, + self.output_memory) + self.scale = self.rife.scale + print(f"RIFE TensorRT module loaded: {rife_model_path}") + + # Initialize SR + if self.use_sr and sr_model_path: + from ezvtb_rt.sr import SR + + if self.rife is not None: + instream = self.rife.instream + mems = [self.rife.memories['framegen_'+str(i)] + for i in range(self.rife.scale)] + else: + instream = self.instream + mems = [self.output_memory] + + self.sr = SR(sr_model_path, instream, mems) + print(f"SR TensorRT module loaded: {sr_model_path}") + + except Exception as e: + print(f'Warning: Failed to initialize TensorRT modules: {e}') + self.rife = None + self.sr = None + + def _create_host_device_mem(self, shape, dtype): + """Create HostDeviceMem for TensorRT modules""" + import pycuda.driver as cuda + from ezvtb_rt.trt_utils import HostDeviceMem + + host_mem = cuda.pagelocked_empty(shape, dtype) + device_mem = cuda.mem_alloc(host_mem.nbytes) + return HostDeviceMem(host_mem, device_mem) + + def setImage(self, img: np.ndarray): + """Set input character image - IGNORED for THA4""" + # THA4 uses character image from YAML, ignore external image + pass + + def inference(self, pose: np.ndarray) -> list: + """ + Run full inference pipeline: THA4 -> RIFE (TRT) -> SR (TRT) + + Args: + pose: numpy array of shape (1, 45) + + Returns: + List of output images [H, W, 4] in BGRA format, uint8 + """ + if self.input_image_tensor is None: + raise RuntimeError("Image not set.") + + pose = pose.astype(np.float32) + + # Cache management + need_cache_write = 0 + res_carrier = None + + # Step 1: THA4 inference (PyTorch) with optional caching + if self.cacher is None: + output_bgra = self._run_tha4_inference(pose) + res_carrier = [output_bgra] + else: + hs = hash(str(pose)) + cached = self.cacher.read(hs) + + if cached is not None: # Cache hit + output_bgra = cached + res_carrier = [cached] + + # If using TensorRT, upload to GPU + if self.use_tensorrt and \ + (self.rife is not None or self.sr is not None): + np.copyto(self.output_memory.host, + output_bgra.reshape(1, 512, 512, 4)) + self.output_memory.htod(self.instream) + else: # Cache miss + output_bgra = self._run_tha4_inference(pose) + need_cache_write = hs + res_carrier = [output_bgra] + + # Step 2: RIFE interpolation (TensorRT) + if self.rife is not None: + self.rife.inference() + res_carrier = self.rife + + # Step 3: SR super resolution (TensorRT) + if self.sr is not None: + self.sr.inference() + res_carrier = self.sr + + # Step 4: Fetch results + if need_cache_write != 0: + self.cacher.write(need_cache_write, output_bgra) + + # Get final results + if type(res_carrier) is not list: + res_carrier = res_carrier.fetchRes() + + return res_carrier + + def _run_tha4_inference(self, pose: np.ndarray) -> np.ndarray: + """Run THA4 PyTorch inference and return BGRA numpy array""" + # Extract pose components + if self.use_eyebrow: + eyebrow_pose = pose[:, :12] + else: + eyebrow_pose = np.zeros((1, 12), dtype=np.float32) + + face_pose = pose[:, 12:39] + head_pose = pose[:, 39:45] + + # Convert to torch tensors + eyebrow_tensor = torch.from_numpy(eyebrow_pose).to(self.device) + face_tensor = torch.from_numpy(face_pose).to(self.device) + head_tensor = torch.from_numpy(head_pose).to(self.device) + + eyebrow_c = eyebrow_pose[0].tolist() + face_c = face_pose[0].tolist() + + # Run THA4 inference + with torch.no_grad(): + output_tensor = self.wrapper.forward( + self.input_image_tensor, + face_tensor, + head_tensor, + eyebrow_tensor, + face_c, + eyebrow_c, + ratio=None + ) + + # Post-process output + output_image = output_tensor[0].float() + output_image = torch.clamp((output_image + 1.0) / 2.0, 0.0, 1.0) + output_image = convert_linear_to_srgb(output_image) + output_np = output_image.permute(1, 2, 0).cpu().numpy() + output_np = (output_np * 255.0).astype(np.uint8) + output_bgra = cv2.cvtColor(output_np, cv2.COLOR_RGBA2BGRA) + + # Upload to GPU memory if TensorRT is active + if self.use_tensorrt and \ + (self.rife is not None or self.sr is not None): + np.copyto(self.output_memory.host, + output_bgra.reshape(1, 512, 512, 4)) + self.output_memory.htod(self.instream) + + return output_bgra + + +def get_tha4_core(device_id=0, use_eyebrow=True, yaml_path=None, + interpolation_scale=1, use_tensorrt=False, + use_interpolation=False, interpolation_half=True, + use_sr=False, sr_x4=True, sr_half=True, sr_noise=1, + cacher_quality=85, cacher_ram_size=2.0): + """ + Factory function to create THA4 core + + Args: + device_id: GPU device ID + use_eyebrow: whether to use eyebrow control + yaml_path: path to character_model.yaml + interpolation_scale: number of frames for interpolation + use_tensorrt: enable TensorRT acceleration for RIFE/SR + use_interpolation: enable RIFE frame interpolation + interpolation_half: use FP16 for RIFE + use_sr: enable super resolution + sr_x4: use 4x SR (else 2x) + sr_half: use FP16 for SR + sr_noise: noise level for waifu2x + cacher_quality: cache compression quality + cacher_ram_size: RAM cache size in GB + + Returns: + THA4Core or THA4CoreWithAccel instance + """ + # Use accelerated version if TensorRT is requested + if use_tensorrt and (use_interpolation or use_sr): + return THA4CoreWithAccel( + device_id=device_id, + use_eyebrow=use_eyebrow, + yaml_path=yaml_path, + use_tensorrt=use_tensorrt, + use_interpolation=use_interpolation, + interpolation_scale=interpolation_scale, + interpolation_half=interpolation_half, + use_sr=use_sr, + sr_x4=sr_x4, + sr_half=sr_half, + sr_noise=sr_noise, + cacher_quality=cacher_quality, + cacher_ram_size=cacher_ram_size + ) + else: + # Use simple PyTorch-only version + return THA4Core( + device_id=device_id, + use_eyebrow=use_eyebrow, + yaml_path=yaml_path, + interpolation_scale=interpolation_scale if use_interpolation + else 1 + ) From ddaa10a40be6bacc883ec523a71e286bd65c321e Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:49:22 +0800 Subject: [PATCH 04/46] submodule link update --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index ebfeccc..e1a865b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ezvtuber-rt"] path = ezvtuber-rt - url = https://github.com/zpeng11/ezvtuber-rt.git - branch = main \ No newline at end of file + url = https://github.com/nick-haoran/ezvtuber-rt.git + branch = tha4 \ No newline at end of file From ef741ca9d248d627c6f3fc4b56ba431804d72e00 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:35:05 +0800 Subject: [PATCH 05/46] submodule redirect --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index e1a865b..e0dd809 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ezvtuber-rt"] path = ezvtuber-rt - url = https://github.com/nick-haoran/ezvtuber-rt.git + url = https://github.com/zpeng11/ezvtuber-rt.git branch = tha4 \ No newline at end of file From f203b9be5f24993b73d46bc366186bcf5eb6affc Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:56:02 +0800 Subject: [PATCH 06/46] submodule update --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index e0dd809..e1a865b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ezvtuber-rt"] path = ezvtuber-rt - url = https://github.com/zpeng11/ezvtuber-rt.git + url = https://github.com/nick-haoran/ezvtuber-rt.git branch = tha4 \ No newline at end of file From 9112a25bf573973810a4b088d2723df604ca9088 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:57:45 +0800 Subject: [PATCH 07/46] submodule update --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 7ce4ce8..b1c511b 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 7ce4ce8d50ca17409b01644cbd714b3f8a5c1412 +Subproject commit b1c511bdc617e4186d9fa6e5677d77d1dff3d571 From 07fa7a6d6957c48a02f2fd51d39e116c5c860cef Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:20:28 +0800 Subject: [PATCH 08/46] remove submodule --- tha4 | 1 - 1 file changed, 1 deletion(-) delete mode 160000 tha4 diff --git a/tha4 b/tha4 deleted file mode 160000 index 3206401..0000000 --- a/tha4 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 320640116abd604de22d7b56faec85b4d95770e0 From 60f130ae890267771013b8fc8492d3090bc9f59c Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:27:40 +0800 Subject: [PATCH 09/46] update gtignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 64d75dc..b983db8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ pretrained/** !pretrained/PUT_MODEL_HERE character src +!tha4/src +tha4/distiller-ui-doc +tha4/docs +tha4/poetry dst .idea bin @@ -147,4 +151,4 @@ data/ .serena onnx_model_tha4/fp16 onnx_model_tha4/fp32 -onnx_model_tha4/tmp \ No newline at end of file +onnx_model_tha4/ \ No newline at end of file From 68505c57d7a50132949e5e3003db03cc2b6fb3c5 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:29:29 +0800 Subject: [PATCH 10/46] tha4 src recite --- tha4/src/tha4/__init__.py | 0 tha4/src/tha4/app/__init__.py | 0 .../character_model_ifacialmocap_puppeteer.py | 417 ++++++++++ .../tha4/app/character_model_manual_poser.py | 438 ++++++++++ .../character_model_mediapipe_puppeteer.py | 431 ++++++++++ tha4/src/tha4/app/distill.py | 25 + tha4/src/tha4/app/distiller_ui.py | 13 + tha4/src/tha4/app/full_manual_poser.py | 468 +++++++++++ tha4/src/tha4/charmodel/__init__.py | 0 tha4/src/tha4/charmodel/character_model.py | 69 ++ tha4/src/tha4/dataset/__init__.py | 0 .../image_poses_and_aother_images_dataset.py | 35 + tha4/src/tha4/distiller/__init__.py | 0 .../distiller/config_based_training_tasks.py | 98 +++ .../tha4/distiller/distill_body_morpher.py | 16 + .../tha4/distiller/distill_face_morpher.py | 16 + tha4/src/tha4/distiller/distiller_config.py | 310 +++++++ tha4/src/tha4/distiller/ui/__init__.py | 0 .../distiller/ui/distiller_config_state.py | 142 ++++ .../distiller/ui/distiller_ui_main_frame.py | 761 ++++++++++++++++++ tha4/src/tha4/image_util.py | 58 ++ tha4/src/tha4/mocap/__init__.py | 0 tha4/src/tha4/mocap/ifacialmocap_constants.py | 239 ++++++ tha4/src/tha4/mocap/ifacialmocap_pose.py | 27 + .../tha4/mocap/ifacialmocap_pose_converter.py | 12 + .../mocap/ifacialmocap_pose_converter_25.py | 614 ++++++++++++++ tha4/src/tha4/mocap/ifacialmocap_v2.py | 89 ++ tha4/src/tha4/mocap/mediapipe_constants.py | 216 +++++ tha4/src/tha4/mocap/mediapipe_face_pose.py | 41 + .../mocap/mediapipe_face_pose_converter.py | 17 + .../mocap/mediapipe_face_pose_converter_00.py | 662 +++++++++++++++ tha4/src/tha4/nn/__init__.py | 0 tha4/src/tha4/nn/common/__init__.py | 0 tha4/src/tha4/nn/common/conv_block_factory.py | 55 ++ tha4/src/tha4/nn/common/poser_args.py | 68 ++ .../nn/common/poser_encoder_decoder_00.py | 121 +++ .../poser_encoder_decoder_00_separable.py | 92 +++ .../nn/common/resize_conv_encoder_decoder.py | 100 +++ tha4/src/tha4/nn/common/resize_conv_unet.py | 107 +++ tha4/src/tha4/nn/common/unet.py | 658 +++++++++++++++ tha4/src/tha4/nn/conv.py | 189 +++++ .../tha4/nn/eyebrow_decomposer/__init__.py | 0 .../eyebrow_decomposer_00.py | 81 ++ .../nn/eyebrow_morphing_combiner/__init__.py | 0 .../eyebrow_morphing_combiner_00.py | 91 +++ tha4/src/tha4/nn/face_morpher/__init__.py | 0 .../tha4/nn/face_morpher/face_morpher_08.py | 211 +++++ tha4/src/tha4/nn/image_processing_util.py | 58 ++ tha4/src/tha4/nn/init_function.py | 76 ++ tha4/src/tha4/nn/morpher/__init__.py | 0 tha4/src/tha4/nn/morpher/morpher_00.py | 80 ++ tha4/src/tha4/nn/nonlinearity_factory.py | 72 ++ tha4/src/tha4/nn/normalization.py | 126 +++ tha4/src/tha4/nn/pass_through.py | 9 + tha4/src/tha4/nn/resnet_block.py | 67 ++ tha4/src/tha4/nn/resnet_block_seperable.py | 71 ++ tha4/src/tha4/nn/separable_conv.py | 119 +++ tha4/src/tha4/nn/siren/__init__.py | 0 .../tha4/nn/siren/face_morpher/__init__.py | 0 .../face_morpher/siren_face_morpher_00.py | 59 ++ .../siren_face_morpher_00_trainer.py | 228 ++++++ .../siren_face_morpher_protocols_00.py | 220 +++++ tha4/src/tha4/nn/siren/morpher/__init__.py | 0 .../tha4/nn/siren/morpher/siren_morpher_03.py | 153 ++++ .../siren/morpher/siren_morpher_03_trainer.py | 276 +++++++ .../morpher/siren_morpher_protocols_03.py | 352 ++++++++ tha4/src/tha4/nn/siren/vanilla/__init__.py | 0 tha4/src/tha4/nn/siren/vanilla/siren.py | 100 +++ tha4/src/tha4/nn/spectral_norm.py | 9 + tha4/src/tha4/nn/upscaler/__init__.py | 0 tha4/src/tha4/nn/upscaler/upscaler_02.py | 110 +++ tha4/src/tha4/nn/util.py | 40 + tha4/src/tha4/poser/__init__.py | 0 tha4/src/tha4/poser/general_poser_02.py | 98 +++ tha4/src/tha4/poser/modes/__init__.py | 0 tha4/src/tha4/poser/modes/mode_07.py | 315 ++++++++ tha4/src/tha4/poser/modes/mode_12.py | 202 +++++ tha4/src/tha4/poser/modes/mode_14.py | 162 ++++ tha4/src/tha4/poser/modes/pose_parameters.py | 36 + tha4/src/tha4/poser/poser.py | 162 ++++ tha4/src/tha4/pytasuku/__init__.py | 0 tha4/src/tha4/pytasuku/indexed/__init__.py | 0 tha4/src/tha4/pytasuku/indexed/all_tasks.py | 26 + .../indexed/bundled_indexed_file_tasks.py | 31 + .../pytasuku/indexed/indexed_file_tasks.py | 19 + .../tha4/pytasuku/indexed/indexed_tasks.py | 34 + .../indexed/no_index_command_tasks.py | 41 + .../pytasuku/indexed/no_index_file_tasks.py | 56 ++ .../pytasuku/indexed/one_index_file_tasks.py | 66 ++ .../indexed/simple_no_index_file_tasks.py | 28 + .../indexed/two_indices_file_tasks.py | 67 ++ tha4/src/tha4/pytasuku/indexed/util.py | 71 ++ tha4/src/tha4/pytasuku/task.py | 100 +++ tha4/src/tha4/pytasuku/task_selector_ui.py | 113 +++ tha4/src/tha4/pytasuku/util.py | 15 + tha4/src/tha4/pytasuku/workspace.py | 168 ++++ tha4/src/tha4/sampleoutput/__init__.py | 0 .../general_sample_output_protocol.py | 142 ++++ .../poser_sampler_output_protocol.py | 73 ++ .../tha4/sampleoutput/sample_image_creator.py | 148 ++++ tha4/src/tha4/shion/__init__.py | 0 tha4/src/tha4/shion/base/__init__.py | 0 tha4/src/tha4/shion/base/dataset/__init__.py | 0 .../tha4/shion/base/dataset/lazy_dataset.py | 20 + .../shion/base/dataset/lazy_tensor_dataset.py | 33 + .../shion/base/dataset/png_in_dir_dataset.py | 53 ++ tha4/src/tha4/shion/base/dataset/util.py | 29 + .../shion/base/dataset/xformed_dataset.py | 15 + tha4/src/tha4/shion/base/image_util.py | 243 ++++++ tha4/src/tha4/shion/base/loss/__init__.py | 0 .../shion/base/loss/computed_scale_loss.py | 22 + .../base/loss/computed_scaled_l2_loss.py | 29 + tha4/src/tha4/shion/base/loss/l1_loss.py | 67 ++ tha4/src/tha4/shion/base/loss/l2_loss.py | 25 + tha4/src/tha4/shion/base/loss/sum_loss.py | 32 + .../loss/time_dependently_weighted_loss.py | 29 + .../tha4/shion/base/module_accumulators.py | 29 + .../tha4/shion/base/optimizer_factories.py | 46 ++ ...k_from_batch_input_computation_protocol.py | 29 + tha4/src/tha4/shion/base/training/__init__.py | 0 .../shion/base/training/single_network.py | 108 +++ .../training/single_network_with_minibatch.py | 87 ++ .../two_networks_training_protocol.py | 106 +++ tha4/src/tha4/shion/core/__init__.py | 0 .../src/tha4/shion/core/cached_computation.py | 112 +++ tha4/src/tha4/shion/core/load_save.py | 14 + tha4/src/tha4/shion/core/loss.py | 15 + .../src/tha4/shion/core/module_accumulator.py | 10 + tha4/src/tha4/shion/core/module_factory.py | 9 + tha4/src/tha4/shion/core/optimizer_factory.py | 10 + tha4/src/tha4/shion/core/training/__init__.py | 0 .../shion/core/training/distrib/__init__.py | 0 .../core/training/distrib/device_mapper.py | 17 + .../training/distrib/distributed_trainer.py | 419 ++++++++++ .../distrib/distributed_training_states.py | 234 ++++++ .../distrib/distributed_training_tasks.py | 139 ++++ .../core/training/sample_output_protocol.py | 43 + .../shion/core/training/single/__init__.py | 0 .../core/training/single/training_states.py | 182 +++++ .../core/training/single/training_tasks.py | 357 ++++++++ .../shion/core/training/swarm/__init__.py | 0 .../training/swarm/swarm_training_tasks.py | 62 ++ .../core/training/swarm/swarm_unit_trainer.py | 345 ++++++++ .../shion/core/training/training_protocol.py | 73 ++ tha4/src/tha4/shion/core/training/util.py | 41 + .../core/training/validation_protocol.py | 43 + tha4/src/tha4/shion/nn00/__init__.py | 0 tha4/src/tha4/shion/nn00/block_args.py | 22 + tha4/src/tha4/shion/nn00/conv.py | 102 +++ .../tha4/shion/nn00/initialization_funcs.py | 58 ++ .../src/tha4/shion/nn00/linear_module_args.py | 30 + .../tha4/shion/nn00/nonlinearity_factories.py | 87 ++ .../nn00/normalization_layer_factories.py | 111 +++ .../shion/nn00/normalization_layer_factory.py | 12 + tha4/src/tha4/shion/nn00/pass_through.py | 9 + tha4/src/tha4/shion/nn00/resnet_block.py | 55 ++ 156 files changed, 14998 insertions(+) create mode 100644 tha4/src/tha4/__init__.py create mode 100644 tha4/src/tha4/app/__init__.py create mode 100644 tha4/src/tha4/app/character_model_ifacialmocap_puppeteer.py create mode 100644 tha4/src/tha4/app/character_model_manual_poser.py create mode 100644 tha4/src/tha4/app/character_model_mediapipe_puppeteer.py create mode 100644 tha4/src/tha4/app/distill.py create mode 100644 tha4/src/tha4/app/distiller_ui.py create mode 100644 tha4/src/tha4/app/full_manual_poser.py create mode 100644 tha4/src/tha4/charmodel/__init__.py create mode 100644 tha4/src/tha4/charmodel/character_model.py create mode 100644 tha4/src/tha4/dataset/__init__.py create mode 100644 tha4/src/tha4/dataset/image_poses_and_aother_images_dataset.py create mode 100644 tha4/src/tha4/distiller/__init__.py create mode 100644 tha4/src/tha4/distiller/config_based_training_tasks.py create mode 100644 tha4/src/tha4/distiller/distill_body_morpher.py create mode 100644 tha4/src/tha4/distiller/distill_face_morpher.py create mode 100644 tha4/src/tha4/distiller/distiller_config.py create mode 100644 tha4/src/tha4/distiller/ui/__init__.py create mode 100644 tha4/src/tha4/distiller/ui/distiller_config_state.py create mode 100644 tha4/src/tha4/distiller/ui/distiller_ui_main_frame.py create mode 100644 tha4/src/tha4/image_util.py create mode 100644 tha4/src/tha4/mocap/__init__.py create mode 100644 tha4/src/tha4/mocap/ifacialmocap_constants.py create mode 100644 tha4/src/tha4/mocap/ifacialmocap_pose.py create mode 100644 tha4/src/tha4/mocap/ifacialmocap_pose_converter.py create mode 100644 tha4/src/tha4/mocap/ifacialmocap_pose_converter_25.py create mode 100644 tha4/src/tha4/mocap/ifacialmocap_v2.py create mode 100644 tha4/src/tha4/mocap/mediapipe_constants.py create mode 100644 tha4/src/tha4/mocap/mediapipe_face_pose.py create mode 100644 tha4/src/tha4/mocap/mediapipe_face_pose_converter.py create mode 100644 tha4/src/tha4/mocap/mediapipe_face_pose_converter_00.py create mode 100644 tha4/src/tha4/nn/__init__.py create mode 100644 tha4/src/tha4/nn/common/__init__.py create mode 100644 tha4/src/tha4/nn/common/conv_block_factory.py create mode 100644 tha4/src/tha4/nn/common/poser_args.py create mode 100644 tha4/src/tha4/nn/common/poser_encoder_decoder_00.py create mode 100644 tha4/src/tha4/nn/common/poser_encoder_decoder_00_separable.py create mode 100644 tha4/src/tha4/nn/common/resize_conv_encoder_decoder.py create mode 100644 tha4/src/tha4/nn/common/resize_conv_unet.py create mode 100644 tha4/src/tha4/nn/common/unet.py create mode 100644 tha4/src/tha4/nn/conv.py create mode 100644 tha4/src/tha4/nn/eyebrow_decomposer/__init__.py create mode 100644 tha4/src/tha4/nn/eyebrow_decomposer/eyebrow_decomposer_00.py create mode 100644 tha4/src/tha4/nn/eyebrow_morphing_combiner/__init__.py create mode 100644 tha4/src/tha4/nn/eyebrow_morphing_combiner/eyebrow_morphing_combiner_00.py create mode 100644 tha4/src/tha4/nn/face_morpher/__init__.py create mode 100644 tha4/src/tha4/nn/face_morpher/face_morpher_08.py create mode 100644 tha4/src/tha4/nn/image_processing_util.py create mode 100644 tha4/src/tha4/nn/init_function.py create mode 100644 tha4/src/tha4/nn/morpher/__init__.py create mode 100644 tha4/src/tha4/nn/morpher/morpher_00.py create mode 100644 tha4/src/tha4/nn/nonlinearity_factory.py create mode 100644 tha4/src/tha4/nn/normalization.py create mode 100644 tha4/src/tha4/nn/pass_through.py create mode 100644 tha4/src/tha4/nn/resnet_block.py create mode 100644 tha4/src/tha4/nn/resnet_block_seperable.py create mode 100644 tha4/src/tha4/nn/separable_conv.py create mode 100644 tha4/src/tha4/nn/siren/__init__.py create mode 100644 tha4/src/tha4/nn/siren/face_morpher/__init__.py create mode 100644 tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00.py create mode 100644 tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00_trainer.py create mode 100644 tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_protocols_00.py create mode 100644 tha4/src/tha4/nn/siren/morpher/__init__.py create mode 100644 tha4/src/tha4/nn/siren/morpher/siren_morpher_03.py create mode 100644 tha4/src/tha4/nn/siren/morpher/siren_morpher_03_trainer.py create mode 100644 tha4/src/tha4/nn/siren/morpher/siren_morpher_protocols_03.py create mode 100644 tha4/src/tha4/nn/siren/vanilla/__init__.py create mode 100644 tha4/src/tha4/nn/siren/vanilla/siren.py create mode 100644 tha4/src/tha4/nn/spectral_norm.py create mode 100644 tha4/src/tha4/nn/upscaler/__init__.py create mode 100644 tha4/src/tha4/nn/upscaler/upscaler_02.py create mode 100644 tha4/src/tha4/nn/util.py create mode 100644 tha4/src/tha4/poser/__init__.py create mode 100644 tha4/src/tha4/poser/general_poser_02.py create mode 100644 tha4/src/tha4/poser/modes/__init__.py create mode 100644 tha4/src/tha4/poser/modes/mode_07.py create mode 100644 tha4/src/tha4/poser/modes/mode_12.py create mode 100644 tha4/src/tha4/poser/modes/mode_14.py create mode 100644 tha4/src/tha4/poser/modes/pose_parameters.py create mode 100644 tha4/src/tha4/poser/poser.py create mode 100644 tha4/src/tha4/pytasuku/__init__.py create mode 100644 tha4/src/tha4/pytasuku/indexed/__init__.py create mode 100644 tha4/src/tha4/pytasuku/indexed/all_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/bundled_indexed_file_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/indexed_file_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/indexed_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/no_index_command_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/no_index_file_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/one_index_file_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/simple_no_index_file_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/two_indices_file_tasks.py create mode 100644 tha4/src/tha4/pytasuku/indexed/util.py create mode 100644 tha4/src/tha4/pytasuku/task.py create mode 100644 tha4/src/tha4/pytasuku/task_selector_ui.py create mode 100644 tha4/src/tha4/pytasuku/util.py create mode 100644 tha4/src/tha4/pytasuku/workspace.py create mode 100644 tha4/src/tha4/sampleoutput/__init__.py create mode 100644 tha4/src/tha4/sampleoutput/general_sample_output_protocol.py create mode 100644 tha4/src/tha4/sampleoutput/poser_sampler_output_protocol.py create mode 100644 tha4/src/tha4/sampleoutput/sample_image_creator.py create mode 100644 tha4/src/tha4/shion/__init__.py create mode 100644 tha4/src/tha4/shion/base/__init__.py create mode 100644 tha4/src/tha4/shion/base/dataset/__init__.py create mode 100644 tha4/src/tha4/shion/base/dataset/lazy_dataset.py create mode 100644 tha4/src/tha4/shion/base/dataset/lazy_tensor_dataset.py create mode 100644 tha4/src/tha4/shion/base/dataset/png_in_dir_dataset.py create mode 100644 tha4/src/tha4/shion/base/dataset/util.py create mode 100644 tha4/src/tha4/shion/base/dataset/xformed_dataset.py create mode 100644 tha4/src/tha4/shion/base/image_util.py create mode 100644 tha4/src/tha4/shion/base/loss/__init__.py create mode 100644 tha4/src/tha4/shion/base/loss/computed_scale_loss.py create mode 100644 tha4/src/tha4/shion/base/loss/computed_scaled_l2_loss.py create mode 100644 tha4/src/tha4/shion/base/loss/l1_loss.py create mode 100644 tha4/src/tha4/shion/base/loss/l2_loss.py create mode 100644 tha4/src/tha4/shion/base/loss/sum_loss.py create mode 100644 tha4/src/tha4/shion/base/loss/time_dependently_weighted_loss.py create mode 100644 tha4/src/tha4/shion/base/module_accumulators.py create mode 100644 tha4/src/tha4/shion/base/optimizer_factories.py create mode 100644 tha4/src/tha4/shion/base/protocol/single_network_from_batch_input_computation_protocol.py create mode 100644 tha4/src/tha4/shion/base/training/__init__.py create mode 100644 tha4/src/tha4/shion/base/training/single_network.py create mode 100644 tha4/src/tha4/shion/base/training/single_network_with_minibatch.py create mode 100644 tha4/src/tha4/shion/base/training/two_networks_training_protocol.py create mode 100644 tha4/src/tha4/shion/core/__init__.py create mode 100644 tha4/src/tha4/shion/core/cached_computation.py create mode 100644 tha4/src/tha4/shion/core/load_save.py create mode 100644 tha4/src/tha4/shion/core/loss.py create mode 100644 tha4/src/tha4/shion/core/module_accumulator.py create mode 100644 tha4/src/tha4/shion/core/module_factory.py create mode 100644 tha4/src/tha4/shion/core/optimizer_factory.py create mode 100644 tha4/src/tha4/shion/core/training/__init__.py create mode 100644 tha4/src/tha4/shion/core/training/distrib/__init__.py create mode 100644 tha4/src/tha4/shion/core/training/distrib/device_mapper.py create mode 100644 tha4/src/tha4/shion/core/training/distrib/distributed_trainer.py create mode 100644 tha4/src/tha4/shion/core/training/distrib/distributed_training_states.py create mode 100644 tha4/src/tha4/shion/core/training/distrib/distributed_training_tasks.py create mode 100644 tha4/src/tha4/shion/core/training/sample_output_protocol.py create mode 100644 tha4/src/tha4/shion/core/training/single/__init__.py create mode 100644 tha4/src/tha4/shion/core/training/single/training_states.py create mode 100644 tha4/src/tha4/shion/core/training/single/training_tasks.py create mode 100644 tha4/src/tha4/shion/core/training/swarm/__init__.py create mode 100644 tha4/src/tha4/shion/core/training/swarm/swarm_training_tasks.py create mode 100644 tha4/src/tha4/shion/core/training/swarm/swarm_unit_trainer.py create mode 100644 tha4/src/tha4/shion/core/training/training_protocol.py create mode 100644 tha4/src/tha4/shion/core/training/util.py create mode 100644 tha4/src/tha4/shion/core/training/validation_protocol.py create mode 100644 tha4/src/tha4/shion/nn00/__init__.py create mode 100644 tha4/src/tha4/shion/nn00/block_args.py create mode 100644 tha4/src/tha4/shion/nn00/conv.py create mode 100644 tha4/src/tha4/shion/nn00/initialization_funcs.py create mode 100644 tha4/src/tha4/shion/nn00/linear_module_args.py create mode 100644 tha4/src/tha4/shion/nn00/nonlinearity_factories.py create mode 100644 tha4/src/tha4/shion/nn00/normalization_layer_factories.py create mode 100644 tha4/src/tha4/shion/nn00/normalization_layer_factory.py create mode 100644 tha4/src/tha4/shion/nn00/pass_through.py create mode 100644 tha4/src/tha4/shion/nn00/resnet_block.py diff --git a/tha4/src/tha4/__init__.py b/tha4/src/tha4/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/app/__init__.py b/tha4/src/tha4/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/app/character_model_ifacialmocap_puppeteer.py b/tha4/src/tha4/app/character_model_ifacialmocap_puppeteer.py new file mode 100644 index 0000000..872d8d9 --- /dev/null +++ b/tha4/src/tha4/app/character_model_ifacialmocap_puppeteer.py @@ -0,0 +1,417 @@ +import os +import socket +import sys +import threading +import time +from typing import Optional + +import PIL.Image + +from tha4.shion.base.image_util import torch_linear_to_srgb +from tha4.image_util import convert_linear_to_srgb +from tha4.mocap.ifacialmocap_pose_converter_25 import create_ifacialmocap_pose_converter +from tha4.app.full_manual_poser import resize_PIL_image +from tha4.charmodel.character_model import CharacterModel + +sys.path.append(os.getcwd()) + +from tha4.mocap.ifacialmocap_pose import create_default_ifacialmocap_pose +from tha4.mocap.ifacialmocap_v2 import IFACIALMOCAP_PORT, IFACIALMOCAP_START_STRING, parse_ifacialmocap_v2_pose + +import torch +import wx + +from tha4.mocap.ifacialmocap_constants import * +from tha4.mocap.ifacialmocap_pose_converter import IFacialMocapPoseConverter + + +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): + IMAGE_SIZE = 512 + + def __init__(self, pose_converter: IFacialMocapPoseConverter, device: torch.device): + super().__init__(None, wx.ID_ANY, "iFacialMocap Puppeteer (Fuji)") + self.poser = None + self.pose_converter = pose_converter + self.device = device + + self.ifacialmocap_pose = create_default_ifacialmocap_pose() + self.source_image_bitmap = wx.Bitmap(MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE) + self.result_image_bitmap = wx.Bitmap(MainFrame.IMAGE_SIZE, MainFrame.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 = MainFrame.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_model_button = wx.Button(self.input_panel, wx.ID_ANY, "Load Model") + self.input_panel_sizer.Add(self.load_model_button, 1, wx.EXPAND) + self.load_model_button.Bind(wx.EVT_BUTTON, self.load_model) + + 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!", (MainFrame.IMAGE_SIZE - w) // 2, (MainFrame.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 or self.poser 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 = torch.clip((output_image + 1.0) / 2.0, 0.0, 1.0) + output_image = convert_linear_to_srgb(output_image) + + 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, + (MainFrame.IMAGE_SIZE - numpy_image.shape[0]) // 2, + (MainFrame.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_model(self, event: wx.Event): + dir_name = "data/character_models" + file_dialog = wx.FileDialog(self, "Choose a model", dir_name, "", "*.yaml", wx.FD_OPEN) + if file_dialog.ShowModal() == wx.ID_OK: + character_model_json_file_name = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename()) + try: + self.character_model = CharacterModel.load(character_model_json_file_name) + self.torch_source_image = self.character_model.get_character_image(self.device) + pil_image = resize_PIL_image( + PIL.Image.open(self.character_model.character_image_file_name), + (MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE)) + w, h = pil_image.size + self.wx_source_image = wx.Bitmap.FromBufferRGBA(w, h, pil_image.convert("RGBA").tobytes()) + self.update_source_image_bitmap() + self.poser = self.character_model.get_poser(self.device) + except Exception: + message_dialog = wx.MessageDialog( + self, "Could not load character model " + character_model_json_file_name, "Poser", wx.OK) + message_dialog.ShowModal() + message_dialog.Destroy() + file_dialog.Destroy() + self.Refresh() + + +if __name__ == "__main__": + device = torch.device('cuda:0') + + pose_converter = create_ifacialmocap_pose_converter() + + app = wx.App() + main_frame = MainFrame(pose_converter, device) + main_frame.Show(True) + main_frame.capture_timer.Start(10) + main_frame.animation_timer.Start(10) + app.MainLoop() diff --git a/tha4/src/tha4/app/character_model_manual_poser.py b/tha4/src/tha4/app/character_model_manual_poser.py new file mode 100644 index 0000000..53b6757 --- /dev/null +++ b/tha4/src/tha4/app/character_model_manual_poser.py @@ -0,0 +1,438 @@ +import logging +import os +import sys +import time +from typing import List + +from tha4.charmodel.character_model import CharacterModel +from tha4.image_util import resize_PIL_image, convert_output_image_from_torch_to_numpy +from tha4.poser.modes.mode_14 import get_pose_parameters + +sys.path.append(os.getcwd()) + +import PIL.Image +import torch +import wx + +from tha4.poser.poser import PoseParameterCategory, PoseParameterGroup + + +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 + + +class MainFrame(wx.Frame): + IMAGE_SIZE = 512 + OUTPUT_LENGTH = 6 + NUM_PARAMETERS = 45 + + def __init__(self, device: torch.device): + super().__init__(None, wx.ID_ANY, "Poser") + self.poser = None + self.device = device + + 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(MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE) + self.result_image_bitmap = wx.Bitmap(MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE) + self.source_image_dirty = True + + def init_left_panel(self): + self.control_panel = wx.Panel(self, style=wx.SIMPLE_BORDER, size=(MainFrame.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=(MainFrame.IMAGE_SIZE, MainFrame.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_model_button = wx.Button(self.left_panel, wx.ID_ANY, "\nLoad Model\n\n") + left_panel_sizer.Add(self.load_model_button, 1, wx.EXPAND) + self.load_model_button.Bind(wx.EVT_BUTTON, self.load_model) + + 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 = {} + param_groups = get_pose_parameters().get_pose_parameter_groups() + for category in morph_categories: + 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, + param_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: + 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, + param_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=(MainFrame.IMAGE_SIZE, MainFrame.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(MainFrame.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_model(self, event: wx.Event): + dir_name = "data/character_models" + file_dialog = wx.FileDialog(self, "Choose a model", dir_name, "", "*.yaml", wx.FD_OPEN) + if file_dialog.ShowModal() == wx.ID_OK: + character_model_file_name = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename()) + try: + self.character_model = CharacterModel.load(character_model_file_name) + self.torch_source_image = self.character_model.get_character_image(self.device) + pil_image = resize_PIL_image( + PIL.Image.open(self.character_model.character_image_file_name), + (MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE)) + w, h = pil_image.size + self.wx_source_image = wx.Bitmap.FromBufferRGBA(w, h, pil_image.convert("RGBA").tobytes()) + self.poser = self.character_model.get_poser(self.device) + self.source_image_dirty = True + self.Refresh() + self.Update() + except RuntimeError as e: + message_dialog = wx.MessageDialog( + self, "Could not load character model " + character_model_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!", (MainFrame.IMAGE_SIZE - w) // 2, (MainFrame.IMAGE_SIZE - - h) // 2) + + del dc + + def get_current_pose(self): + current_pose = [0.0 for i in range(MainFrame.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 or self.poser 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) + output_index = self.output_index_choice.GetSelection() + with torch.no_grad(): + start_cuda_event = torch.cuda.Event(enable_timing=True) + end_cuda_event = torch.cuda.Event(enable_timing=True) + start_cuda_event.record() + start_time = time.time() + + output_image = self.poser.pose(self.torch_source_image, pose, output_index)[0].detach().cpu() + + end_time = time.time() + end_cuda_event.record() + torch.cuda.synchronize() + print("cuda time (ms):", start_cuda_event.elapsed_time(end_cuda_event)) + print("elapsed time (ms):", (end_time - start_time) * 1000.0) + + 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, + (MainFrame.IMAGE_SIZE - numpy_image.shape[0]) // 2, + (MainFrame.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__": + device = torch.device('cuda:0') + app = wx.App() + main_frame = MainFrame(device) + main_frame.Show(True) + main_frame.timer.Start(16) + app.MainLoop() diff --git a/tha4/src/tha4/app/character_model_mediapipe_puppeteer.py b/tha4/src/tha4/app/character_model_mediapipe_puppeteer.py new file mode 100644 index 0000000..e030d3a --- /dev/null +++ b/tha4/src/tha4/app/character_model_mediapipe_puppeteer.py @@ -0,0 +1,431 @@ +import os +import sys +import threading +import time +from typing import Optional +import PIL.Image + +import cv2 +import mediapipe +from scipy.spatial.transform import Rotation + +from tha4.shion.base.image_util import resize_PIL_image +from tha4.charmodel.character_model import CharacterModel +from tha4.image_util import convert_linear_to_srgb +from tha4.mocap.mediapipe_constants import HEAD_ROTATIONS, HEAD_X, HEAD_Y, HEAD_Z +from tha4.mocap.mediapipe_face_pose import MediaPipeFacePose +from tha4.mocap.mediapipe_face_pose_converter_00 import MediaPoseFacePoseConverter00 + +sys.path.append(os.getcwd()) + +import torch +import wx + + +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): + IMAGE_SIZE = 512 + + def __init__(self, + pose_converter: MediaPoseFacePoseConverter00, + video_capture, + face_landmarker, + device: torch.device): + super().__init__(None, wx.ID_ANY, "THA4 Character Model MediaPipe Puppeteer") + self.face_landmarker = face_landmarker + self.video_capture = video_capture + self.pose_converter = pose_converter + self.device = device + + self.source_image_bitmap = wx.Bitmap(MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE) + self.result_image_bitmap = wx.Bitmap(MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE) + self.webcam_capture_bitmap = wx.Bitmap(256, 192) + self.wx_source_image = None + self.torch_source_image = None + self.last_pose = None + self.mediapipe_face_pose = None + self.fps_statistics = FpsStatistics() + self.last_update_time = None + self.character_model = None + self.poser = 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_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 = MainFrame.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_model_button = wx.Button(self.input_panel, wx.ID_ANY, "Load Model") + self.input_panel_sizer.Add(self.load_model_button, 1, wx.EXPAND) + self.load_model_button.Bind(wx.EVT_BUTTON, self.load_model) + + self.input_panel_sizer.Fit(self.input_panel) + + if True: + def current_pose_supplier() -> Optional[MediaPipeFacePose]: + return self.mediapipe_face_pose + + self.pose_converter.init_pose_converter_panel(self.animation_panel, current_pose_supplier) + + 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_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_capture_panel(self, parent): + self.capture_panel = wx.Panel(parent, style=wx.RAISED_BORDER) + self.capture_panel_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.capture_panel.SetSizer(self.capture_panel_sizer) + self.capture_panel.SetAutoLayout(1) + + self.webcam_capture_panel = wx.Panel(self.capture_panel, size=(256, 192), style=wx.SIMPLE_BORDER) + self.webcam_capture_panel.Bind(wx.EVT_PAINT, self.paint_webcam_capture_panel) + self.webcam_capture_panel.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background) + self.capture_panel_sizer.Add(self.webcam_capture_panel, wx.SizerFlags(0).FixedMinSize().Border(wx.ALL, 5)) + + self.rotation_labels = {} + self.rotation_value_labels = {} + rotation_column = self.create_rotation_column(self.capture_panel, HEAD_ROTATIONS) + self.capture_panel_sizer.Add(rotation_column, wx.SizerFlags(0).Expand().Border(wx.ALL, 3)) + + def paint_webcam_capture_panel(self, event: wx.Event): + wx.BufferedPaintDC(self.webcam_capture_panel, self.webcam_capture_bitmap) + + 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 update_capture_panel(self, event: wx.Event): + there_is_frame, frame = self.video_capture.read() + if not there_is_frame: + dc = wx.MemoryDC() + dc.SelectObject(self.webcam_capture_bitmap) + self.draw_nothing_yet_string(dc) + del dc + return + + rgb_frame = cv2.flip(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB), 1) + # Use INTER_AREA for downscaling (better quality than INTER_LINEAR) + resized_frame = cv2.resize( + rgb_frame, (256, 192), + interpolation=cv2.INTER_AREA + ) + wx_image = wx.ImageFromBuffer(256, 192, resized_frame.tobytes()) + wx_bitmap = wx_image.ConvertToBitmap() + + dc = wx.MemoryDC() + dc.SelectObject(self.webcam_capture_bitmap) + dc.Clear() + dc.DrawBitmap(wx_bitmap, 0, 0, True) + del dc + + self.webcam_capture_panel.Refresh() + + time_ms = int(time.time() * 1000) + mediapipe_image = mediapipe.Image(image_format=mediapipe.ImageFormat.SRGB, data=rgb_frame) + detection_result = self.face_landmarker.detect_for_video(mediapipe_image, time_ms) + self.update_mediapipe_face_pose(detection_result) + + def update_mediapipe_face_pose(self, detection_result): + if len(detection_result.facial_transformation_matrixes) == 0: + return + + xform_matrix = detection_result.facial_transformation_matrixes[0] + blendshape_params = {} + for item in detection_result.face_blendshapes[0]: + blendshape_params[item.category_name] = item.score + M = xform_matrix[0:3, 0:3] + rot = Rotation.from_matrix(M) + euler_angles = rot.as_euler('xyz', degrees=True) + + self.rotation_value_labels[HEAD_X].SetValue("%0.2f" % euler_angles[0]) + self.rotation_value_labels[HEAD_X].Refresh() + self.rotation_value_labels[HEAD_Y].SetValue("%0.2f" % euler_angles[1]) + self.rotation_value_labels[HEAD_Y].Refresh() + self.rotation_value_labels[HEAD_Z].SetValue("%0.2f" % euler_angles[2]) + self.rotation_value_labels[HEAD_Z].Refresh() + + self.mediapipe_face_pose = MediaPipeFacePose(blendshape_params, xform_matrix) + + @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!", (MainFrame.IMAGE_SIZE - w) // 2, (MainFrame.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): + if self.mediapipe_face_pose is None or self.poser is None: + dc = wx.MemoryDC() + dc.SelectObject(self.result_image_bitmap) + self.draw_nothing_yet_string(dc) + del dc + return + + current_pose = self.pose_converter.convert(self.mediapipe_face_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 = torch.clip((output_image + 1.0) / 2.0, 0.0, 1.0) + output_image = convert_linear_to_srgb(output_image) + + 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, + (MainFrame.IMAGE_SIZE - numpy_image.shape[0]) // 2, + (MainFrame.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_model(self, event: wx.Event): + dir_name = "data/character_models" + file_dialog = wx.FileDialog(self, "Choose a model", dir_name, "", "*.yaml", wx.FD_OPEN) + if file_dialog.ShowModal() == wx.ID_OK: + character_model_json_file_name = os.path.join(file_dialog.GetDirectory(), file_dialog.GetFilename()) + try: + self.character_model = CharacterModel.load(character_model_json_file_name) + self.torch_source_image = self.character_model.get_character_image(self.device) + pil_image = resize_PIL_image( + PIL.Image.open(self.character_model.character_image_file_name), + (MainFrame.IMAGE_SIZE, MainFrame.IMAGE_SIZE)) + w, h = pil_image.size + self.wx_source_image = wx.Bitmap.FromBufferRGBA(w, h, pil_image.convert("RGBA").tobytes()) + self.update_source_image_bitmap() + self.poser = self.character_model.get_poser(self.device) + except Exception: + message_dialog = wx.MessageDialog( + self, "Could not load character model " + character_model_json_file_name, "Poser", wx.OK) + message_dialog.ShowModal() + message_dialog.Destroy() + file_dialog.Destroy() + self.Refresh() + + +if __name__ == "__main__": + device = torch.device("cuda:0") + + pose_converter = MediaPoseFacePoseConverter00() + + face_landmarker_base_options = mediapipe.tasks.BaseOptions( + model_asset_path='data/thirdparty/mediapipe/face_landmarker_v2_with_blendshapes.task') + options = mediapipe.tasks.vision.FaceLandmarkerOptions( + base_options=face_landmarker_base_options, + running_mode=mediapipe.tasks.vision.RunningMode.VIDEO, + output_face_blendshapes=True, + output_facial_transformation_matrixes=True, + num_faces=1) + face_landmarker = mediapipe.tasks.vision.FaceLandmarker.create_from_options(options) + + video_capture = cv2.VideoCapture(0) + + app = wx.App() + main_frame = MainFrame(pose_converter, video_capture, face_landmarker, device) + main_frame.Show(True) + main_frame.capture_timer.Start(30) + main_frame.animation_timer.Start(30) + app.MainLoop() diff --git a/tha4/src/tha4/app/distill.py b/tha4/src/tha4/app/distill.py new file mode 100644 index 0000000..8a81842 --- /dev/null +++ b/tha4/src/tha4/app/distill.py @@ -0,0 +1,25 @@ +import argparse +import logging + +from tha4.distiller.distiller_config import DistillerConfig +from tha4.pytasuku.workspace import Workspace + + +def run_config(config_file_name: str): + config = DistillerConfig.load(config_file_name) + + logging.basicConfig(level=logging.INFO, force=True) + workspace = Workspace() + config.define_tasks(workspace) + + workspace.start_session() + workspace.run(f"{config.prefix}/all") + workspace.end_session() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Training script.') + parser.add_argument("--config_file", type=str, required=True, + help="The name of the config file for the distillation process.") + args = parser.parse_args() + run_config(args.config_file) diff --git a/tha4/src/tha4/app/distiller_ui.py b/tha4/src/tha4/app/distiller_ui.py new file mode 100644 index 0000000..a9d991d --- /dev/null +++ b/tha4/src/tha4/app/distiller_ui.py @@ -0,0 +1,13 @@ +from tha4.app.distill import run_config +from tha4.distiller.ui.distiller_ui_main_frame import DistillerUiMainFrame + +import wx + +if __name__ == "__main__": + app = wx.App() + main_frame = DistillerUiMainFrame() + main_frame.Show(True) + app.MainLoop() + + if main_frame.config_file_to_run is not None: + run_config(main_frame.config_file_to_run) diff --git a/tha4/src/tha4/app/full_manual_poser.py b/tha4/src/tha4/app/full_manual_poser.py new file mode 100644 index 0000000..79d225c --- /dev/null +++ b/tha4/src/tha4/app/full_manual_poser.py @@ -0,0 +1,468 @@ +import logging +import os +import sys +import time +from typing import List + +from tha4.shion.base.image_util import extract_pytorch_image_from_PIL_image, pytorch_rgba_to_numpy_image, \ + pytorch_rgb_to_numpy_image +from tha4.image_util import grid_change_to_numpy_image, resize_PIL_image + +sys.path.append(os.getcwd()) + +import PIL.Image +import numpy +import torch +import wx + +from tha4.poser.poser import Poser, PoseParameterCategory, PoseParameterGroup + + +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 = pytorch_rgba_to_numpy_image(output_image) + elif output_image.shape[0] == 3: + numpy_image = pytorch_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 = pytorch_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(PIL.Image.open(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(): + start_cuda_event = torch.cuda.Event(enable_timing=True) + end_cuda_event = torch.cuda.Event(enable_timing=True) + start_cuda_event.record() + start_time = time.time() + + output_image = self.poser.pose(self.torch_source_image, pose, output_index)[0].detach().cpu() + + end_time = time.time() + end_cuda_event.record() + torch.cuda.synchronize() + print("cuda time (ms):", start_cuda_event.elapsed_time(end_cuda_event)) + print("elapsed time (ms):", (end_time - start_time) * 1000.0) + + 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__": + device = torch.device('cuda:0') + try: + import tha4.poser.modes.mode_07 + + poser = tha4.poser.modes.mode_07.create_poser(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(16) + app.MainLoop() diff --git a/tha4/src/tha4/charmodel/__init__.py b/tha4/src/tha4/charmodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/charmodel/character_model.py b/tha4/src/tha4/charmodel/character_model.py new file mode 100644 index 0000000..15b3012 --- /dev/null +++ b/tha4/src/tha4/charmodel/character_model.py @@ -0,0 +1,69 @@ +import json +import os.path + +import PIL.Image +import torch +from omegaconf import OmegaConf + +from tha4.shion.base.image_util import extract_pytorch_image_from_PIL_image +from tha4.poser.modes.mode_14 import create_poser, KEY_FACE_MORPHER, KEY_BODY_MORPHER + + +class CharacterModel: + def __init__(self, + character_image_file_name: str, + face_morpher_file_name: str, + body_morpher_file_name: str): + self.body_morpher_file_name = body_morpher_file_name + self.face_morpher_file_name = face_morpher_file_name + self.character_image_file_name = character_image_file_name + self.poser = None + self.character_image = None + + def get_poser(self, device: torch.device): + if self.poser is not None: + self.poser.to(device) + else: + self.poser = create_poser( + device, + module_file_names={ + KEY_FACE_MORPHER: self.face_morpher_file_name, + KEY_BODY_MORPHER: self.body_morpher_file_name + }) + return self.poser + + def get_character_image(self, device: torch.device): + if self.character_image is None: + pil_image = PIL.Image.open(self.character_image_file_name) + if pil_image.mode != 'RGBA': + raise RuntimeError("Character image is not an RGBA image!") + self.character_image = extract_pytorch_image_from_PIL_image(pil_image) + self.character_image = self.character_image.to(device) + return self.character_image + + def save(self, file_name: str): + dir = os.path.dirname(file_name) + rel_char_image_file_name = os.path.relpath(self.character_image_file_name, dir) + rel_face_morpher_file_name = os.path.relpath(self.face_morpher_file_name, dir) + rel_body_morpher_file_name = os.path.relpath(self.body_morpher_file_name, dir) + data = { + "character_image_file_name": rel_char_image_file_name, + "face_morpher_file_name": rel_face_morpher_file_name, + "body_morpher_file_name": rel_body_morpher_file_name, + } + conf = OmegaConf.create(data) + os.makedirs(dir, exist_ok=True) + with open(file_name, "wt") as fout: + fout.write(OmegaConf.to_yaml(conf)) + + @staticmethod + def load(file_name: str): + conf = OmegaConf.to_container(OmegaConf.load(file_name)) + dir = os.path.dirname(file_name) + character_image_file_name = os.path.join(dir, conf["character_image_file_name"]) + face_morpher_file_name = os.path.join(dir, conf["face_morpher_file_name"]) + body_morpher_file_name = os.path.join(dir, conf["body_morpher_file_name"]) + return CharacterModel( + character_image_file_name, + face_morpher_file_name, + body_morpher_file_name) diff --git a/tha4/src/tha4/dataset/__init__.py b/tha4/src/tha4/dataset/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/dataset/image_poses_and_aother_images_dataset.py b/tha4/src/tha4/dataset/image_poses_and_aother_images_dataset.py new file mode 100644 index 0000000..9832ec3 --- /dev/null +++ b/tha4/src/tha4/dataset/image_poses_and_aother_images_dataset.py @@ -0,0 +1,35 @@ +from typing import List, Callable + +from torch import Tensor +from torch.utils.data import Dataset + + +class ImagePosesAndOtherImagesDataset(Dataset): + def __init__(self, + main_image_func: Callable[[], Tensor], + pose_dataset: Dataset, + other_image_funcs: List[Callable[[], Tensor]]): + self.main_image_func = main_image_func + self.other_image_funcs = other_image_funcs + self.pose_dataset = pose_dataset + self.main_image = None + self.other_images = [None for i in range(len(self.other_image_funcs))] + + def get_main_image(self): + if self.main_image is None: + self.main_image = self.main_image_func() + return self.main_image + + def get_other_image(self, image_index: int): + if self.other_images[image_index] is None: + self.other_images[image_index] = self.other_image_funcs[image_index]() + return self.other_images[image_index] + + def __len__(self): + return len(self.pose_dataset) + + def __getitem__(self, index): + main_image = self.get_main_image() + pose = self.pose_dataset[index][0] + other_images = [self.get_other_image(i) for i in range(len(self.other_image_funcs))] + return [main_image, pose] + other_images diff --git a/tha4/src/tha4/distiller/__init__.py b/tha4/src/tha4/distiller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/distiller/config_based_training_tasks.py b/tha4/src/tha4/distiller/config_based_training_tasks.py new file mode 100644 index 0000000..2e13b1e --- /dev/null +++ b/tha4/src/tha4/distiller/config_based_training_tasks.py @@ -0,0 +1,98 @@ +import logging +import os +import sys +from typing import Callable, List, Optional + +from tha4.pytasuku.workspace import Workspace +from tha4.shion.core.training.distrib.distributed_trainer import DistributedTrainer +from tha4.shion.core.training.distrib.distributed_training_states import DistributedTrainingState + + +def get_torchrun_executable(): + return os.path.dirname(sys.executable) + os.path.sep + "torchrun" + + +class RdzvConfig: + def __init__(self, id: int, port: int): + self.port = port + self.id = id + + +def run_standalone_config_based_training_script( + training_script_file_name: str, + config_file_name: str, + num_proc_per_node: int, + target_checkpoint_examples: Optional[int] = None, + rdzv_config: Optional[RdzvConfig] = None): + command = f"{get_torchrun_executable()} " \ + f"--nnodes=1 " \ + f"--nproc_per_node={num_proc_per_node} " + if rdzv_config is not None: + command += f"--rdzv_endpoint=localhost:{rdzv_config.port} " + command += "--rdzv_backend=c10d " + command += f"--rdzv_id={rdzv_config.id} " + else: + command += "--standalone " + command += f"{training_script_file_name} " + if target_checkpoint_examples is not None: + command += f"--target_checkpoint_examples {target_checkpoint_examples} " + command += f"--config_file={config_file_name} " + logging.info(f"Executing -- {command}") + os.system(command) + + +def define_standalone_config_based_training_tasks( + workspace: Workspace, + distributed_trainer_func: Callable[[], DistributedTrainer], + training_script_file_name: str, + config_file_name: str, + num_proc_per_node: int, + dependencies: Optional[List[str]] = None, + rdzv_config: Optional[RdzvConfig] = None): + trainer = distributed_trainer_func() + checkpoint_examples = trainer.training_protocol.get_checkpoint_examples() + assert len(checkpoint_examples) >= 1 + assert checkpoint_examples[0] > 0 + checkpoint_examples = [0] + checkpoint_examples + + if dependencies is None: + dependencies = [] + module_file_dependencies = dependencies[:] + for module_name in trainer.pretrained_module_file_names: + module_file_dependencies.append(trainer.pretrained_module_file_names[module_name]) + + def create_train_func(target_checkpoint_examples: int): + return lambda: run_standalone_config_based_training_script( + training_script_file_name, + config_file_name, + num_proc_per_node, + target_checkpoint_examples, + rdzv_config=rdzv_config) + + train_tasks = [] + for checkpoint_index in range(0, len(checkpoint_examples)): + for module_name in trainer.module_names: + module_file_name = DistributedTrainingState.get_module_file_name( + trainer.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + module_file_name, + module_file_dependencies, + create_train_func(trainer.checkpoint_examples[checkpoint_index])) + for module_name in trainer.accumulators: + accumulated_module_file_name = DistributedTrainingState.get_accumulated_module_file_name( + trainer.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + accumulated_module_file_name, + module_file_dependencies, + create_train_func(checkpoint_examples[checkpoint_index])) + workspace.create_command_task( + trainer.get_checkpoint_prefix(checkpoint_index) + "/train_standalone", + module_file_dependencies, + create_train_func(checkpoint_examples[checkpoint_index])) + train_tasks.append(trainer.get_checkpoint_prefix(checkpoint_index) + "/train_standlone") + workspace.create_file_task( + trainer.prefix + "/train_standalone", + module_file_dependencies, + create_train_func(checkpoint_examples[-1])) diff --git a/tha4/src/tha4/distiller/distill_body_morpher.py b/tha4/src/tha4/distiller/distill_body_morpher.py new file mode 100644 index 0000000..ae6752f --- /dev/null +++ b/tha4/src/tha4/distiller/distill_body_morpher.py @@ -0,0 +1,16 @@ +import logging + +from tha4.shion.core.training.distrib.distributed_trainer import DistributedTrainer +from tha4.distiller.distiller_config import DistillerConfig + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + parser = DistributedTrainer.get_default_arg_parser() + parser.add_argument('--config_file', type=str) + args = parser.parse_args() + + config_file_name = args.config_file + config = DistillerConfig.load(config_file_name) + + DistributedTrainer.run_with_args(config.get_body_morpher_trainer, args) diff --git a/tha4/src/tha4/distiller/distill_face_morpher.py b/tha4/src/tha4/distiller/distill_face_morpher.py new file mode 100644 index 0000000..2979fe6 --- /dev/null +++ b/tha4/src/tha4/distiller/distill_face_morpher.py @@ -0,0 +1,16 @@ +import logging + +from tha4.shion.core.training.distrib.distributed_trainer import DistributedTrainer +from tha4.distiller.distiller_config import DistillerConfig + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + parser = DistributedTrainer.get_default_arg_parser() + parser.add_argument('--config_file', type=str) + args = parser.parse_args() + + config_file_name = args.config_file + config = DistillerConfig.load(config_file_name) + + DistributedTrainer.run_with_args(config.get_face_morpher_trainer, args) diff --git a/tha4/src/tha4/distiller/distiller_config.py b/tha4/src/tha4/distiller/distiller_config.py new file mode 100644 index 0000000..08e6edc --- /dev/null +++ b/tha4/src/tha4/distiller/distiller_config.py @@ -0,0 +1,310 @@ +import os.path +import shutil +import PIL.Image +from dataclasses import dataclass +from typing import Optional + +from omegaconf import OmegaConf +from tha4.charmodel.character_model import CharacterModel +from tha4.pytasuku.workspace import Workspace, file_task +from tha4.distiller.config_based_training_tasks import define_standalone_config_based_training_tasks +from tha4.nn.siren.face_morpher.siren_face_morpher_00_trainer import SirenFaceMorpher00TrainerArgs +from tha4.nn.siren.morpher.siren_morpher_03_trainer import SirenMorpher03TrainerArgs, TrainingPhases, TrainingPhase, \ + LossWeights, LossTerm +from tha4.shion.base.image_util import pil_image_has_transparency + +POSE_DATASET_FILE_NAME = 'data/pose_dataset.pt' + + +def copy_file(source_file_name: str, dest_file_name): + os.makedirs(os.path.dirname(dest_file_name), exist_ok=True) + shutil.copyfile(source_file_name, dest_file_name) + + +@dataclass +class DistillerConfig: + prefix: str + character_image_file_name: str + face_mask_image_file_name: str + + face_morpher_random_seed_0: int = 12771885812175595441 + face_morpher_random_seed_1: int = 14367217090963479175 + face_morpher_num_training_examples_per_sample_output: Optional[int] = 10_000 + face_morpher_batch_size: int = 8 + + body_morpher_random_seed_0: int = 2892221210020292507 + body_morpher_random_seed_1: int = 9998918537095922080 + body_morpher_num_training_examples_per_sample_output: Optional[int] = 10_000 + body_morpher_batch_size: int = 8 + + num_cpu_workers: int = 1 + num_gpus: int = 1 + + def check(self): + DistillerConfig.check_prefix(self.prefix) + DistillerConfig.check_character_image_file_name(self.character_image_file_name) + DistillerConfig.check_face_mask_image_file_name(self.face_mask_image_file_name) + + DistillerConfig.check_num_cpu_workers(self.num_cpu_workers) + DistillerConfig.check_num_gpus(self.num_gpus) + + DistillerConfig.check_random_seed(self.face_morpher_random_seed_0, "face_morpher_random_seed_0") + DistillerConfig.check_random_seed(self.face_morpher_random_seed_1, "face_morpher_random_seed_1") + DistillerConfig.check_batch_size(self.face_morpher_batch_size, "face_morpher_batch_size") + DistillerConfig.check_num_training_examples_per_sample_output( + self.face_morpher_num_training_examples_per_sample_output, + "face_morpher_num_training_examples_per_sample_output") + + DistillerConfig.check_random_seed(self.body_morpher_random_seed_0, "body_morpher_random_seed_0") + DistillerConfig.check_random_seed(self.body_morpher_random_seed_1, "body_morpher_random_seed_1") + DistillerConfig.check_batch_size(self.body_morpher_batch_size, "body_morpher_batch_size") + DistillerConfig.check_num_training_examples_per_sample_output( + self.body_morpher_num_training_examples_per_sample_output, + "body_morpher_num_training_examples_per_sample_output") + + @staticmethod + def check_prefix(prefix): + assert os.path.isdir(prefix), "The 'prefix' must be a directory." + assert os.path.exists(prefix), f"The {prefix} directory does not exist." + + @staticmethod + def check_character_image_file_name(file_name): + _, ext = os.path.splitext(file_name) + assert os.path.isfile(file_name), \ + f"The specified character image file name, {file_name}, does not point to a file." + assert ext.lower() == ".png", "The character image file name must have extension '.png'." + + image = PIL.Image.open(file_name) + assert pil_image_has_transparency(image), "The character image must have an alpha channel." + assert image.width == 512 and image.height == 512, "The character image must be 512x512." + image.close() + + @staticmethod + def check_face_mask_image_file_name(file_name): + _, ext = os.path.splitext(file_name) + assert os.path.isfile(file_name), \ + f"The specified face mask image file name, {file_name}, does not point to a file." + assert ext.lower() == ".png", "The face mask image file name must have extension '.png'." + + image = PIL.Image.open(file_name) + assert image.width == 512 and image.height == 512, "The face mask image must be 512x512." + assert image.mode == "RGB", "The face mask image must be an RGB image." + for x in range(512): + for y in range(512): + r, g, b = image.getpixel((x, y)) + assert (r == 0) or (r == 255), "The R channel of the face mask image must be 0 or 255" + assert (g == 0) or (g == 255), "The G channel of the face mask image must be 0 or 255" + assert (b == 0) or (b == 255), "The B channel of the face mask image must be 0 or 255" + image.close() + + @staticmethod + def check_batch_size(value, field_name: str): + assert isinstance(value, int), f"The {field_name} must be an integer." + assert value >= 1, f"The {field_name} must be at least 1." + assert value <= 8, f"The {field_name} must be at most 8." + + @staticmethod + def check_num_cpu_workers(value): + assert value >= 1, "The value of 'num_cpu_workers must be at least 1." + + @staticmethod + def check_num_gpus(value): + assert value >= 1, "The value of 'num_gpus' must be at least 1." + + @staticmethod + def check_random_seed(value, field_name: str): + assert isinstance(value, int), f"The {field_name} must be an integer." + assert value >= 0 and value <= 0x_ffff_ffff_ffff_ffff, "A random seed must be between 0 and 2**64-1." + + @staticmethod + def check_num_training_examples_per_sample_output(value, field_name): + assert value in [10_000, 100_000, 1_000_000, + None], f"The {field_name} must be 10_000, 100_00, 1_000_000_000, or None." + + def save(self, file_name: str): + conf = OmegaConf.structured(self) + os.makedirs(self.prefix, exist_ok=True) + with open(file_name, "wt") as fout: + fout.write(OmegaConf.to_yaml(conf)) + + def config_yaml_file_name(self): + return f"{self.prefix}/config.yaml" + + def create_config_yaml_file(self): + if os.path.exists(self.config_yaml_file_name()): + return + self.save(self.config_yaml_file_name()) + + @staticmethod + def load(file_name: str) -> 'DistillerConfig': + conf = OmegaConf.to_container(OmegaConf.load(file_name)) + args = DistillerConfig(**conf) + args.check() + return args + + def face_morpher_prefix(self): + return f"{self.prefix}/face_morpher" + + def get_face_morpher_trainer(self, world_size: Optional[int] = None, backend: str = 'gloo'): + if world_size is None: + world_size = self.num_gpus + args = SirenFaceMorpher00TrainerArgs( + character_file_name=self.character_image_file_name, + face_mask_file_name=self.face_mask_image_file_name, + pose_dataset_file_name=POSE_DATASET_FILE_NAME, + total_worker=self.num_cpu_workers, + num_training_examples_per_sample_output=self.face_morpher_num_training_examples_per_sample_output, + total_batch_size=self.face_morpher_batch_size, + training_random_seed=self.face_morpher_random_seed_0, + sample_output_random_seed=self.face_morpher_random_seed_1) + return args.create_trainer(self.face_morpher_prefix(), world_size, backend) + + def body_morpher_prefix(self): + return f"{self.prefix}/body_morpher" + + def get_body_morpher_trainer(self, world_size: Optional[int] = None, backend: str = 'gloo'): + if world_size is None: + world_size = self.num_gpus + args = SirenMorpher03TrainerArgs( + character_file_name=self.character_image_file_name, + pose_dataset_file_name=POSE_DATASET_FILE_NAME, + total_worker=self.num_cpu_workers, + num_training_examples_per_sample_output=self.body_morpher_num_training_examples_per_sample_output, + training_random_seed=self.body_morpher_random_seed_0, + sample_output_random_seed=self.body_morpher_random_seed_1, + total_batch_size=self.body_morpher_batch_size, + sample_output_batch_size=1, + training_phases=TrainingPhases([ + TrainingPhase( + num_examples_upper_bound=200_000, + learning_rate=1e-4, + loss_weights=LossWeights(weights={ + LossTerm.full_blended: 0.25, + LossTerm.full_warped: 0.25, + LossTerm.full_grid_change: 0.5, + LossTerm.full_color_change: 2.0, + })), + TrainingPhase( + num_examples_upper_bound=400_000, + learning_rate=3e-5, + loss_weights=LossWeights(weights={ + LossTerm.full_blended: 0.25, + LossTerm.full_warped: 0.25, + LossTerm.full_grid_change: 0.5, + LossTerm.full_color_change: 2.0, + })), + TrainingPhase( + num_examples_upper_bound=600_000, + learning_rate=3e-5, + loss_weights=LossWeights(weights={ + LossTerm.full_blended: 1.0, + LossTerm.full_warped: 2.5, + LossTerm.full_grid_change: 5.0, + LossTerm.full_color_change: 1.0, + })), + TrainingPhase( + num_examples_upper_bound=800_000, + learning_rate=1e-5, + loss_weights=LossWeights(weights={ + LossTerm.full_blended: 1.0, + LossTerm.full_warped: 2.5, + LossTerm.full_grid_change: 5.0, + LossTerm.full_color_change: 1.0, + })), + TrainingPhase( + num_examples_upper_bound=1_300_000, + learning_rate=1e-5, + loss_weights=LossWeights(weights={ + LossTerm.full_blended: 10.0, + LossTerm.full_warped: 1.0, + LossTerm.full_grid_change: 1.0, + LossTerm.full_color_change: 1.0, + })), + TrainingPhase( + num_examples_upper_bound=1_500_000, + learning_rate=3e-6, + loss_weights=LossWeights(weights={ + LossTerm.full_blended: 10.0, + LossTerm.full_warped: 1.0, + LossTerm.full_grid_change: 1.0, + LossTerm.full_color_change: 1.0, + })), + ])) + return args.create_trainer(self.body_morpher_prefix(), world_size, backend) + + def character_model_prefix(self): + return f"{self.prefix}/character_model" + + def character_model_face_morpher_file_name(self): + return f"{self.character_model_prefix()}/face_morpher.pt" + + def character_model_body_morpher_file_name(self): + return f"{self.character_model_prefix()}/body_morpher.pt" + + def character_model_character_png_file_name(self): + return f"{self.character_model_prefix()}/character.png" + + def character_model_yaml_file_name(self): + return f"{self.character_model_prefix()}/character_model.yaml" + + def define_tasks(self, workspace: Workspace): + workspace.create_file_task(self.config_yaml_file_name(), [], self.create_config_yaml_file) + + define_standalone_config_based_training_tasks( + workspace, + self.get_face_morpher_trainer, + "src/tha4/distiller/distill_face_morpher.py", + self.config_yaml_file_name(), + num_proc_per_node=self.num_gpus, + dependencies=[ + self.config_yaml_file_name(), + ]) + + define_standalone_config_based_training_tasks( + workspace, + self.get_body_morpher_trainer, + "src/tha4/distiller/distill_body_morpher.py", + self.config_yaml_file_name(), + num_proc_per_node=self.num_gpus, + dependencies=[ + self.config_yaml_file_name(), + ]) + + @file_task(workspace, self.character_model_character_png_file_name(), [self.character_image_file_name]) + def copy_character_image_file_name(): + copy_file(self.character_image_file_name, self.character_model_character_png_file_name()) + + @file_task(workspace, self.character_model_face_morpher_file_name(), [ + f"{self.face_morpher_prefix()}/checkpoint/0010/module_module.pt", + ]) + def copy_face_morpher(): + copy_file( + f"{self.face_morpher_prefix()}/checkpoint/0010/module_module.pt", + self.character_model_face_morpher_file_name()) + + @file_task(workspace, self.character_model_body_morpher_file_name(), [ + f"{self.body_morpher_prefix()}/checkpoint/0015/module_module.pt", + ]) + def copy_face_morpher(): + copy_file( + f"{self.body_morpher_prefix()}/checkpoint/0015/module_module.pt", + self.character_model_body_morpher_file_name()) + + @file_task(workspace, self.character_model_yaml_file_name(), []) + def create_character_model_yaml_file(): + character_model = CharacterModel( + self.character_model_character_png_file_name(), + self.character_model_face_morpher_file_name(), + self.character_model_body_morpher_file_name()) + character_model.save(self.character_model_yaml_file_name()) + + workspace.create_command_task( + f"{self.prefix}/all", + [ + f"{self.face_morpher_prefix()}/train_standalone", + f"{self.body_morpher_prefix()}/train_standalone", + self.character_model_character_png_file_name(), + self.character_model_face_morpher_file_name(), + self.character_model_body_morpher_file_name(), + self.character_model_yaml_file_name(), + ]) diff --git a/tha4/src/tha4/distiller/ui/__init__.py b/tha4/src/tha4/distiller/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/distiller/ui/distiller_config_state.py b/tha4/src/tha4/distiller/ui/distiller_config_state.py new file mode 100644 index 0000000..a3cc822 --- /dev/null +++ b/tha4/src/tha4/distiller/ui/distiller_config_state.py @@ -0,0 +1,142 @@ +import os.path +from contextlib import contextmanager +from pathlib import PurePath, Path +from typing import Callable, Any, Optional + +from tha4.distiller.distiller_config import DistillerConfig + + +class DistillerConfigState: + def __init__(self): + self.config = DistillerConfig(prefix="", character_image_file_name="", face_mask_image_file_name="") + self.last_saved_timestamp = None + self.dirty = False + + def load(self, file_name): + self.config = DistillerConfig.load(file_name) + if os.path.exists(self.config.config_yaml_file_name()): + self.last_saved_timestamp = os.path.getmtime(self.config.config_yaml_file_name()) + else: + self.last_saved_timestamp = None + self.dirty = False + + def need_to_check_overwrite(self): + if self.last_saved_timestamp is None: + return True + if not os.path.exists(self.config.config_yaml_file_name()): + return False + if self.last_saved_timestamp < os.path.getmtime(self.config.config_yaml_file_name()): + return True + return False + + def save(self): + self.config.save(self.config.config_yaml_file_name()) + self.dirty = False + self.last_saved_timestamp = os.path.getmtime(self.config.config_yaml_file_name()) + + @contextmanager + def updating_value(self, value_func: Callable[[], Any]): + old_value = value_func() + yield + new_value = value_func() + if new_value != old_value: + self.dirty = True + + def set_prefix(self, new_value): + with self.updating_value(lambda: self.config.prefix): + new_relative_path = self.get_relative_path_to_cwd( + new_value, + "The prefix directory must be a subdirectory of the talking-head-anime-4-demo's source code directory.") + DistillerConfig.check_prefix(new_relative_path) + self.config.prefix = new_relative_path + + def set_character_image_file_name(self, new_value): + with self.updating_value(lambda: self.config.character_image_file_name): + new_relative_path = self.get_relative_path_to_cwd( + new_value, + "The character image file must be under talking-head-anime-4-demo's source code directory.") + DistillerConfig.check_character_image_file_name(new_relative_path) + self.config.character_image_file_name = new_relative_path + + def set_face_mask_image_file_name(self, new_value): + with self.updating_value(lambda: self.config.face_mask_image_file_name): + new_relative_path = self.get_relative_path_to_cwd( + new_value, + "The face mask image file must be under talking-head-anime-4-demo's source code directory.") + DistillerConfig.check_face_mask_image_file_name(new_relative_path) + self.config.face_mask_image_file_name = new_relative_path + + def set_num_cpu_workers(self, new_value: int): + with self.updating_value(lambda: self.config.num_cpu_workers): + DistillerConfig.check_num_cpu_workers(new_value) + self.config.num_cpu_workers = new_value + + def set_num_gpus(self, new_value: int): + with self.updating_value(lambda: self.config.num_gpus): + DistillerConfig.check_num_cpu_workers(new_value) + self.config.num_gpus = new_value + + def set_face_morpher_random_seed_0(self, new_value: int): + with self.updating_value(lambda: self.config.face_morpher_random_seed_0): + DistillerConfig.check_random_seed(new_value, "face_morpher_random_seed_0") + self.config.face_morpher_random_seed_0 = new_value + + def set_face_morpher_random_seed_1(self, new_value: int): + with self.updating_value(lambda: self.config.face_morpher_random_seed_1): + DistillerConfig.check_random_seed(new_value, "face_morpher_random_seed_1") + self.config.face_morpher_random_seed_1 = new_value + + def set_face_morpher_num_training_examples_per_sample_output(self, new_value: Optional[int]): + with self.updating_value(lambda: self.config.face_morpher_num_training_examples_per_sample_output): + DistillerConfig.check_num_training_examples_per_sample_output( + new_value, "face_morpher_num_training_examples_per_sample_output") + self.config.face_morpher_num_training_examples_per_sample_output = new_value + + def set_face_morpher_batch_size(self, new_value: int): + with self.updating_value(lambda: self.config.face_morpher_batch_size): + DistillerConfig.check_batch_size(new_value, "face_morpher_batch_size") + self.config.face_morpher_batch_size = new_value + + def set_body_morpher_random_seed_0(self, new_value: int): + with self.updating_value(lambda: self.config.body_morpher_random_seed_0): + DistillerConfig.check_random_seed(new_value, "body_morpher_random_seed_0") + self.config.body_morpher_random_seed_0 = new_value + + def set_body_morpher_random_seed_1(self, new_value: int): + with self.updating_value(lambda: self.config.body_morpher_random_seed_1): + DistillerConfig.check_random_seed(new_value, "body_morpher_random_seed_1") + self.config.body_morpher_random_seed_1 = new_value + + def set_body_morpher_num_training_examples_per_sample_output(self, new_value: Optional[int]): + with self.updating_value(lambda: self.config.body_morpher_num_training_examples_per_sample_output): + DistillerConfig.check_num_training_examples_per_sample_output( + new_value, "body_morpher_num_training_examples_per_sample_output") + self.config.body_morpher_num_training_examples_per_sample_output = new_value + + def set_body_morpher_batch_size(self, new_value: int): + with self.updating_value(lambda: self.config.body_morpher_batch_size): + DistillerConfig.check_batch_size(new_value, "body_morpher_batch_size") + self.config.body_morpher_batch_size = new_value + + def get_relative_path_to_cwd(self, file_name: str, message: str): + cwd = os.getcwd() + assert os.path.commonprefix([cwd, file_name]) == cwd, message + cwd_path = Path(cwd).as_posix() + new_path = Path(file_name).as_posix() + new_relative_path = os.path.relpath(str(new_path), cwd_path) + new_relative_path = str(Path(new_relative_path).as_posix()) + return new_relative_path + + def can_show_character_image(self): + return os.path.isfile(self.config.character_image_file_name) + + def can_show_face_mask_image(self): + return os.path.isfile(self.config.face_mask_image_file_name) + + def can_show_mask_on_face_image(self): + return self.can_show_character_image() and self.can_show_face_mask_image() + + def can_save(self): + return os.path.isdir(self.config.prefix) \ + and os.path.isfile(self.config.character_image_file_name) \ + and os.path.isfile(self.config.face_mask_image_file_name) diff --git a/tha4/src/tha4/distiller/ui/distiller_ui_main_frame.py b/tha4/src/tha4/distiller/ui/distiller_ui_main_frame.py new file mode 100644 index 0000000..20bb7d3 --- /dev/null +++ b/tha4/src/tha4/distiller/ui/distiller_ui_main_frame.py @@ -0,0 +1,761 @@ +import multiprocessing +import random +from contextlib import contextmanager +from typing import Callable +import PIL.Image + +import torch +import wx +import wx.html +import wx.lib.intctrl +from tha4.distiller.ui.distiller_config_state import DistillerConfigState +from tha4.image_util import convert_output_image_from_torch_to_numpy +from tha4.shion.base.image_util import extract_pytorch_image_from_PIL_image + + +def wx_bind_event(widget, evt): + def f(handler): + widget.Bind(evt, handler) + return handler + + return f + + +class DistillerUiMainFrame(wx.Frame): + PARAM_NAME_STATIC_TEXT_MIN_WIDTH = 400 + NUM_TRAINING_EXAMPLES_PER_SAMPLE_OUTPUT_CHOICES = [ + "10_000", "100_000", "1_000_000", "Do not generate sample outputs"] + + def __init__(self): + super().__init__(None, wx.ID_ANY, "Distiller UI") + + self.init_ui() + self.init_menus() + self.init_bitmaps() + self.Bind(wx.EVT_CLOSE, self.on_close) + + self.state = DistillerConfigState() + self.update_ui() + + self.config_file_to_run = None + + def init_ui(self): + main_sizer = wx.BoxSizer(wx.HORIZONTAL) + + self.SetSizer(main_sizer) + self.SetAutoLayout(1) + + left_panel = self.init_left_panel(self) + main_sizer.Add(left_panel, 0, wx.FIXED_MINSIZE) + + middle_panel = self.init_middle_panel(self) + main_sizer.Add(middle_panel, 0, wx.EXPAND) + + right_panel = self.init_right_panel(self) + main_sizer.Add(right_panel, 1, wx.EXPAND) + + main_sizer.Fit(self) + + def init_menus(self): + self.file_menu = wx.Menu() + + self.new_menu_id = wx.Window.NewControlId() + self.file_menu.Append( + self.new_menu_id, item="&New\tCTRL+N", helpString="Create a new distiller configuration.") + self.Bind(wx.EVT_MENU, self.on_new, id=self.new_menu_id) + + self.open_menu_id = wx.Window.NewControlId() + self.file_menu.Append( + self.open_menu_id, item="&Open\tCTRL+O", helpString="Open a distiller confuguration.") + self.Bind(wx.EVT_MENU, self.on_open, id=self.open_menu_id) + + self.save_menu_id = wx.Window.NewControlId() + self.save_menu_item = wx.MenuItem( + self.file_menu, id=self.save_menu_id, text="&Save\tCTRL+S", + helpString="Save the current distiller configuration. Error message will be shown it it is not well formed.") + self.Bind(wx.EVT_MENU, self.on_save, id=self.save_menu_id) + self.file_menu.Append(self.save_menu_item) + + self.file_menu.AppendSeparator() + + self.exit_menu_id = wx.ID_EXIT + self.file_menu.Append( + self.exit_menu_id, item="E&xit\tCTRL+Q", helpString="Exit the application.") + self.Bind(wx.EVT_MENU, self.on_close, id=self.exit_menu_id) + + self.menu_bar = wx.MenuBar() + self.menu_bar.Append(self.file_menu, "&File") + + self.SetMenuBar(self.menu_bar) + + def init_bitmaps(self): + self.face_image_bitmap = wx.Bitmap(128, 128) + self.face_image_pytorch = None + self.face_mask_image_bitmap = wx.Bitmap(128, 128) + self.face_mask_image_pytorch = None + self.mask_on_face_image_bitmap = wx.Bitmap(128, 128) + self.draw_nothing_yet_string_to_bitmap(self.face_image_bitmap, 128, 128) + self.draw_nothing_yet_string_to_bitmap(self.face_mask_image_bitmap, 128, 128) + self.draw_nothing_yet_string_to_bitmap(self.mask_on_face_image_bitmap, 128, 128) + + @contextmanager + def create_panel(self, parent, sizer, *args, **kwargs): + panel = wx.Panel(parent, *args, **kwargs) + panel.SetSizer(sizer) + panel.SetAutoLayout(1) + + try: + yield panel, sizer + finally: + sizer.Fit(panel) + + def init_left_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, sizer): + self.face_image_panel = wx.Panel(panel, size=(128, 128), style=wx.SIMPLE_BORDER) + self.face_image_panel.Bind(wx.EVT_PAINT, self.on_face_image_panel_paint) + sizer.Add(self.face_image_panel, 0, wx.EXPAND) + + static_text = wx.StaticText(panel, label="Face", style=wx.ALIGN_CENTER) + sizer.Add(static_text, 0, wx.EXPAND) + + self.face_mask_image_panel = wx.Panel(panel, size=(128, 128), style=wx.SIMPLE_BORDER) + self.face_mask_image_panel.Bind(wx.EVT_PAINT, self.on_face_mask_image_panel_paint) + sizer.Add(self.face_mask_image_panel, 0, wx.EXPAND) + + static_text = wx.StaticText(panel, label="Face mask", style=wx.ALIGN_CENTER) + sizer.Add(static_text, 0, wx.EXPAND) + + self.mask_on_face_image_panel = wx.Panel(panel, size=(128, 128), style=wx.SIMPLE_BORDER) + self.mask_on_face_image_panel.Bind(wx.EVT_PAINT, self.on_mask_on_face_image_panel_paint) + sizer.Add(self.mask_on_face_image_panel, 0, wx.EXPAND) + + static_text = wx.StaticText(panel, label="Mask upon face", style=wx.ALIGN_CENTER) + sizer.Add(static_text, 0, wx.EXPAND) + + return panel + + def on_erase_background(self, event): + pass + + def on_face_image_panel_paint(self, event): + wx.BufferedPaintDC(self.face_image_panel, self.face_image_bitmap) + + def on_face_mask_image_panel_paint(self, event): + wx.BufferedPaintDC(self.face_mask_image_panel, self.face_mask_image_bitmap) + + def on_mask_on_face_image_panel_paint(self, event): + wx.BufferedPaintDC(self.mask_on_face_image_panel, self.mask_on_face_image_bitmap) + + def init_middle_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, sizer): + sizer.Add(self.init_prefix_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_character_image_file_name_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_face_mask_image_file_name_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_num_cpu_workers_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_num_gpus_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_face_morpher_random_seed_0_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_face_morpher_random_seed_1_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_face_morpher_batch_size_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_body_morpher_random_seed_0_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_body_morpher_random_seed_1_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_body_morpher_batch_size_panel(panel), 0, wx.EXPAND) + sizer.Add(self.init_num_training_examples_per_sample_output_panel(panel), 0, wx.EXPAND) + + self.run_button = wx.Button(panel, label="RUN") + self.run_button.SetMinSize((-1, 64)) + self.run_button.Bind(wx.EVT_BUTTON, self.on_run) + sizer.Add(self.run_button, 1, wx.EXPAND) + + return panel + + def init_prefix_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "prefix (i.e. project directory)", + self.create_help_button_func("distiller-ui-doc/params/prefix.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) \ + as (prefix_panel, prefix_sizer): + self.prefix_text_ctrl = wx.TextCtrl(prefix_panel, value="") + self.prefix_text_ctrl.SetEditable(False) + prefix_sizer.Add(self.prefix_text_ctrl, 1, wx.EXPAND) + + self.prefix_change_button = wx.Button(prefix_panel, label="Change...") + self.prefix_change_button.Bind(wx.EVT_BUTTON, self.on_prefix_change_button) + prefix_sizer.Add(self.prefix_change_button, 0, wx.EXPAND) + panel_sizer.Add(prefix_panel, 1, wx.EXPAND) + + return panel + + def on_prefix_change_button(self, event): + dir_dialog = wx.DirDialog(self, "Choose a directory.", style=wx.DD_DEFAULT_STYLE | wx.DD_NEW_DIR_BUTTON) + if dir_dialog.ShowModal() != wx.ID_OK: + return + prefix_value = dir_dialog.GetPath() + try: + self.state.set_prefix(prefix_value) + self.update_ui() + except Exception as e: + message_dialog = wx.MessageDialog(self, str(e), "Error", wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + + def init_character_image_file_name_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "character_image_file_name", + self.create_help_button_func("distiller-ui-doc/params/character_image_file_name.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) as (sub_panel, sub_sizer): + self.character_image_file_name_text_ctrl = wx.TextCtrl(sub_panel, value="") + self.character_image_file_name_text_ctrl.SetEditable(False) + sub_sizer.Add(self.character_image_file_name_text_ctrl, 1, wx.EXPAND) + + self.character_image_change_button = wx.Button(sub_panel, label="Change...") + self.character_image_change_button.Bind(wx.EVT_BUTTON, self.on_character_image_change_button) + sub_sizer.Add(self.character_image_change_button, 0, wx.EXPAND) + panel_sizer.Add(sub_panel, 1, wx.EXPAND) + + return panel + + def on_character_image_change_button(self, event): + file_dialog = wx.FileDialog(self, "Choose a PNG file", wildcard="*.png", style=wx.FD_OPEN) + if file_dialog.ShowModal() != wx.ID_OK: + return + file_name = file_dialog.GetPath() + try: + self.state.set_character_image_file_name(file_name) + self.update_face_image_bitmap(file_name) + self.update_ui() + except Exception as e: + message_dialog = wx.MessageDialog(self, str(e), "Error", wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + + def update_face_image_bitmap(self, new_file_name: str): + pil_image = PIL.Image.open(new_file_name) + subimage = pil_image.crop((256 - 64, 80, 256 + 64, 208)) + self.face_image_bitmap = wx.Bitmap.FromBufferRGBA(128, 128, subimage.convert("RGBA").tobytes()) + self.face_image_pytorch = extract_pytorch_image_from_PIL_image(subimage).to(torch.float) + self.update_mask_on_face_image_bitmap() + + def init_face_mask_image_file_name_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "face_mask_image_file_name", + self.create_help_button_func("distiller-ui-doc/params/face_mask_image_file_name.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) as (sub_panel, sub_sizer): + self.face_mask_image_file_name_text_ctrl = wx.TextCtrl(sub_panel, value="") + self.face_mask_image_file_name_text_ctrl.SetEditable(False) + sub_sizer.Add(self.face_mask_image_file_name_text_ctrl, 1, wx.EXPAND) + + self.face_mask_image_file_name_change_button = wx.Button(sub_panel, label="Change...") + self.face_mask_image_file_name_change_button.Bind(wx.EVT_BUTTON, self.on_face_mask_image_change_button) + sub_sizer.Add(self.face_mask_image_file_name_change_button, 0, wx.EXPAND) + panel_sizer.Add(sub_panel, 1, wx.EXPAND) + + return panel + + def on_face_mask_image_change_button(self, event): + file_dialog = wx.FileDialog(self, "Choose a PNG file", wildcard="*.png", style=wx.FD_OPEN) + if file_dialog.ShowModal() != wx.ID_OK: + return + file_name = file_dialog.GetPath() + try: + self.state.set_face_mask_image_file_name(file_name) + self.update_face_mask_image_bitmap(file_name) + self.update_ui() + except Exception as e: + message_dialog = wx.MessageDialog(self, str(e), "Error", wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + + def update_face_mask_image_bitmap(self, new_file_name): + pil_image = PIL.Image.open(new_file_name) + subimage = pil_image.crop((256 - 64, 80, 256 + 64, 208)) + self.face_mask_image_bitmap = wx.Bitmap.FromBufferRGBA(128, 128, subimage.convert("RGBA").tobytes()) + self.face_mask_image_pytorch = extract_pytorch_image_from_PIL_image(subimage).to(torch.float) + self.face_mask_image_pytorch = self.face_mask_image_pytorch[0:1, :, :] + self.update_mask_on_face_image_bitmap() + + def update_mask_on_face_image_bitmap(self): + if self.face_image_pytorch is None: + return + if self.face_mask_image_pytorch is None: + return + + mask_on_face_image = (0.5 * self.face_image_pytorch) + (0.5 * self.face_mask_image_pytorch) + numpy_image = convert_output_image_from_torch_to_numpy(mask_on_face_image) + wx_image = wx.ImageFromBuffer( + numpy_image.shape[0], + numpy_image.shape[1], + numpy_image[:, :, 0:3].tobytes(), + numpy_image[:, :, 3].tobytes()) + self.mask_on_face_image_bitmap = wx_image.ConvertToBitmap() + + def init_num_cpu_workers_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "num_cpu_workers", + self.create_help_button_func("distiller-ui-doc/params/num_cpu_workers.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + num_cpus = multiprocessing.cpu_count() + self.num_cpu_workers_spin_ctrl = wx.SpinCtrl(panel, initial=1, min=1, max=num_cpus) + + @wx_bind_event(self.num_cpu_workers_spin_ctrl, wx.EVT_SPINCTRL) + def on_num_cpu_workers_spin_ctrl(event): + self.state.set_num_cpu_workers(self.num_cpu_workers_spin_ctrl.GetValue()) + self.Refresh() + + panel_sizer.Add(self.num_cpu_workers_spin_ctrl, 1, wx.EXPAND) + + return panel + + def init_num_gpus_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "num_gpus", + self.create_help_button_func("distiller-ui-doc/params/num_gpus.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + num_gpus = torch.cuda.device_count() + self.num_gpus_spin_ctrl = wx.SpinCtrl(panel, initial=1, min=1, max=max(1, num_gpus)) + + @wx_bind_event(self.num_gpus_spin_ctrl, wx.EVT_SPINCTRL) + def on_num_cpu_workers_spin_ctrl(event): + self.state.set_num_gpus(self.num_gpus_spin_ctrl.GetValue()) + self.Refresh() + + panel_sizer.Add(self.num_gpus_spin_ctrl, 1, wx.EXPAND) + + return panel + + def init_face_morpher_random_seed_0_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "face_morpher_random_seed_0", + self.create_help_button_func("distiller-ui-doc/params/face_morpher_random_seed_0.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) as (sub_panel, sub_sizer): + initial_value = random.randint(0, 2 ** 64 - 1) + self.face_morpher_random_seed_0_int_ctrl = wx.lib.intctrl.IntCtrl( + sub_panel, value=initial_value, min=0, max=0x_ffff_ffff_ffff_ffff) + + @wx_bind_event(self.face_morpher_random_seed_0_int_ctrl, wx.EVT_TEXT) + def on_face_morpher_random_seed_0_int_ctrl_text(event): + self.state.set_face_morpher_random_seed_0(self.face_morpher_random_seed_0_int_ctrl.GetValue()) + + sub_sizer.Add(self.face_morpher_random_seed_0_int_ctrl, 1, wx.EXPAND) + + self.face_morpher_random_seed_0_randomize_button = wx.Button(sub_panel, label="Randomize") + + @wx_bind_event(self.face_morpher_random_seed_0_randomize_button, wx.EVT_BUTTON) + def on_face_morpher_random_seed_0_randomize_button(event): + new_value = random.randint(0, 0x_ffff_ffff_ffff_ffff) + self.face_morpher_random_seed_0_int_ctrl.SetValue(new_value) + self.state.set_face_morpher_random_seed_0(new_value) + + sub_sizer.Add(self.face_morpher_random_seed_0_randomize_button, 0, wx.EXPAND) + panel_sizer.Add(sub_panel, 1, wx.EXPAND) + + return panel + + def init_face_morpher_random_seed_1_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "face_morpher_random_seed_1", + self.create_help_button_func("distiller-ui-doc/params/face_morpher_random_seed_1.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) as (sub_panel, sub_sizer): + initial_value = random.randint(0, 2 ** 64 - 1) + self.face_morpher_random_seed_1_int_ctrl = wx.lib.intctrl.IntCtrl( + sub_panel, value=initial_value, min=0, max=0x_ffff_ffff_ffff_ffff) + + @wx_bind_event(self.face_morpher_random_seed_1_int_ctrl, wx.EVT_TEXT) + def on_face_morpher_random_seed_1_int_ctrl_text(event): + self.state.set_face_morpher_random_seed_1(self.face_morpher_random_seed_1_int_ctrl.GetValue()) + + sub_sizer.Add(self.face_morpher_random_seed_1_int_ctrl, 1, wx.EXPAND) + + self.face_morpher_random_seed_1_randomize_button = wx.Button(sub_panel, label="Randomize") + + @wx_bind_event(self.face_morpher_random_seed_1_randomize_button, wx.EVT_BUTTON) + def on_face_morpher_random_seed_1_randomize_button(event): + new_value = random.randint(0, 0x_ffff_ffff_ffff_ffff) + self.face_morpher_random_seed_1_int_ctrl.SetValue(new_value) + self.state.set_face_morpher_random_seed_1(new_value) + + sub_sizer.Add(self.face_morpher_random_seed_1_randomize_button, 0, wx.EXPAND) + panel_sizer.Add(sub_panel, 1, wx.EXPAND) + + return panel + + def init_face_morpher_batch_size_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "face_morpher_batch_size", + self.create_help_button_func("distiller-ui-doc/params/face_morpher_batch_size.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + self.face_morpher_batch_size_spin_ctrl = wx.SpinCtrl(panel, initial=8, min=1, max=8) + + @wx_bind_event(self.face_morpher_batch_size_spin_ctrl, wx.EVT_SPINCTRL) + def on_face_morpher_batch_size_spin_ctrl(event): + self.state.set_face_morpher_batch_size(self.face_morpher_batch_size_spin_ctrl.GetValue()) + + panel_sizer.Add(self.face_morpher_batch_size_spin_ctrl, 1, wx.EXPAND) + + return panel + + def init_body_morpher_random_seed_0_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "body_morpher_random_seed_0", + self.create_help_button_func("distiller-ui-doc/params/body_morpher_random_seed_0.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) as (sub_panel, sub_sizer): + initial_value = random.randint(0, 2 ** 64 - 1) + self.body_morpher_random_seed_0_int_ctrl = wx.lib.intctrl.IntCtrl( + sub_panel, value=initial_value, min=0, max=0x_ffff_ffff_ffff_ffff) + + @wx_bind_event(self.body_morpher_random_seed_0_int_ctrl, wx.EVT_TEXT) + def on_body_morpher_random_seed_0_int_ctrl_text(event): + self.state.set_body_morpher_random_seed_0(self.body_morpher_random_seed_0_int_ctrl.GetValue()) + + sub_sizer.Add(self.body_morpher_random_seed_0_int_ctrl, 1, wx.EXPAND) + + self.body_morpher_random_seed_0_randomize_button = wx.Button(sub_panel, label="Randomize") + + @wx_bind_event(self.body_morpher_random_seed_0_randomize_button, wx.EVT_BUTTON) + def on_body_morpher_random_seed_0_randomize_button(event): + new_value = random.randint(0, 0x_ffff_ffff_ffff_ffff) + self.body_morpher_random_seed_0_int_ctrl.SetValue(new_value) + self.state.set_body_morpher_random_seed_0(new_value) + + sub_sizer.Add(self.body_morpher_random_seed_0_randomize_button, 0, wx.EXPAND) + panel_sizer.Add(sub_panel, 1, wx.EXPAND) + + return panel + + def init_body_morpher_random_seed_1_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "body_morpher_random_seed_1", + self.create_help_button_func("distiller-ui-doc/params/body_morpher_random_seed_1.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + with self.create_panel(panel, wx.BoxSizer(wx.HORIZONTAL), style=wx.BORDER_NONE) as (sub_panel, sub_sizer): + initial_value = random.randint(0, 2 ** 64 - 1) + self.body_morpher_random_seed_1_int_ctrl = wx.lib.intctrl.IntCtrl( + sub_panel, value=initial_value, min=0, max=0x_ffff_ffff_ffff_ffff) + + @wx_bind_event(self.body_morpher_random_seed_1_int_ctrl, wx.EVT_TEXT) + def on_body_morpher_random_seed_1_int_ctrl_text(event): + self.state.set_body_morpher_random_seed_1(self.body_morpher_random_seed_1_int_ctrl.GetValue()) + + sub_sizer.Add(self.body_morpher_random_seed_1_int_ctrl, 1, wx.EXPAND) + + self.body_morpher_random_seed_1_randomize_button = wx.Button(sub_panel, label="Randomize") + + @wx_bind_event(self.body_morpher_random_seed_1_randomize_button, wx.EVT_BUTTON) + def on_body_morpher_random_seed_1_randomize_button(event): + new_value = random.randint(0, 0x_ffff_ffff_ffff_ffff) + self.body_morpher_random_seed_1_int_ctrl.SetValue(new_value) + self.state.set_body_morpher_random_seed_1(new_value) + + sub_sizer.Add(self.body_morpher_random_seed_1_randomize_button, 0, wx.EXPAND) + panel_sizer.Add(sub_panel, 1, wx.EXPAND) + + return panel + + def init_body_morpher_batch_size_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "body_morpher_batch_size", + self.create_help_button_func("distiller-ui-doc/params/body_morpher_batch_size.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + self.body_morpher_batch_size_spin_ctrl = wx.SpinCtrl(panel, initial=8, min=1, max=8) + + @wx_bind_event(self.body_morpher_batch_size_spin_ctrl, wx.EVT_SPINCTRL) + def on_body_morpher_batch_size_spin_ctrl(event): + self.state.set_body_morpher_batch_size(self.body_morpher_batch_size_spin_ctrl.GetValue()) + + panel_sizer.Add(self.body_morpher_batch_size_spin_ctrl, 1, wx.EXPAND) + + return panel + + def init_num_training_examples_per_sample_output_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, panel_sizer): + prefix_param_name_panel = self.create_param_name_panel_with_help_button( + panel, + "num_training_examples_per_sample_output", + self.create_help_button_func("distiller-ui-doc/params/num_training_examples_per_sample_output.html")) + panel_sizer.Add(prefix_param_name_panel, 1, wx.EXPAND) + + self.num_training_examples_per_sample_output_combobox = \ + wx.ComboBox(panel, + value="10_000", + choices=DistillerUiMainFrame.NUM_TRAINING_EXAMPLES_PER_SAMPLE_OUTPUT_CHOICES) + + @wx_bind_event(self.num_training_examples_per_sample_output_combobox, wx.EVT_COMBOBOX) + def on_num_training_examples_per_sample_output_combobox(event): + index = self.num_training_examples_per_sample_output_combobox.GetSelection() + if index == 3: + self.state.set_face_morpher_num_training_examples_per_sample_output(None) + self.state.set_body_morpher_num_training_examples_per_sample_output(None) + else: + selected = DistillerUiMainFrame.NUM_TRAINING_EXAMPLES_PER_SAMPLE_OUTPUT_CHOICES[index] + new_value = int(selected) + self.state.set_face_morpher_num_training_examples_per_sample_output(new_value) + self.state.set_body_morpher_num_training_examples_per_sample_output(new_value) + + panel_sizer.Add(self.num_training_examples_per_sample_output_combobox, 1, wx.EXPAND) + + return panel + + def on_close(self, event): + if self.state.dirty: + confirmation_dialog = wx.MessageDialog( + parent=self, + message=f"You have not saved your work. Do you want to exit anyway?", + caption="Confirmation", + style=wx.YES_NO | wx.ICON_QUESTION) + result = confirmation_dialog.ShowModal() + if result == wx.ID_NO: + return + + self.Destroy() + + def create_help_button_func(self, html_file_name: str): + def init_help_button_func(parent): + button = wx.Button(parent, label="Help") + + @wx_bind_event(button, wx.EVT_BUTTON) + def on_prefix_button(event): + self.html_window.LoadPage(html_file_name) + self.Refresh() + + return button + + return init_help_button_func + + def create_param_name_panel_with_help_button( + self, parent, param_name: str, help_button_func: Callable[[wx.Window], wx.Button]): + with self.create_panel(parent, wx.BoxSizer(wx.HORIZONTAL), style=wx.NO_BORDER) \ + as (panel, sizer): + title_text_panel = self.create_vertically_centered_text_panel( + panel, param_name, DistillerUiMainFrame.PARAM_NAME_STATIC_TEXT_MIN_WIDTH) + sizer.Add(title_text_panel, 1, wx.EXPAND) + + help_button = help_button_func(panel) + sizer.Add(help_button, 0, wx.EXPAND) + return panel + + def create_vertically_centered_text_panel(self, parent, text: str, min_width: int): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.NO_BORDER) as (panel, sizer): + sizer.AddStretchSpacer(1) + text = wx.StaticText( + panel, + label=text, + style=wx.ALIGN_CENTER) + text.SetMinSize((min_width, -1)) + sizer.Add(text, 0, wx.EXPAND) + sizer.AddStretchSpacer(1) + return panel + + def init_right_panel(self, parent): + with self.create_panel(parent, wx.BoxSizer(wx.VERTICAL), style=wx.BORDER_SIMPLE) as (panel, sizer): + self.html_window = wx.html.HtmlWindow(panel) + self.html_window.SetMinSize((600, 600)) + self.html_window.SetFonts("Times New Roman", "Courier New", sizes=[10, 12, 14, 16, 18, 20, 24]) + self.html_window.LoadPage("distiller-ui-doc/index.html") + sizer.Add(self.html_window, 1, wx.EXPAND) + + go_to_main_documentation_button = wx.Button(panel, label="Go to Main Documentation") + sizer.Add(go_to_main_documentation_button, 0, wx.EXPAND) + + @wx_bind_event(go_to_main_documentation_button, wx.EVT_BUTTON) + def on_go_to_main_documentation_button(event): + self.html_window.LoadPage("distiller-ui-doc/index.html") + self.Refresh() + + return panel + + def populate_distiller_config(self): + self.state.config.prefix = self.prefix_text_ctrl.GetValue() + self.state.config.character_image_file_name = self.character_image_file_name_text_ctrl.GetValue() + self.state.config.face_mask_image_file_name = self.face_mask_image_file_name_text_ctrl.GetValue() + + self.state.config.num_cpu_workers = self.num_cpu_workers_spin_ctrl.GetValue() + self.state.config.num_gpus = self.num_gpus_spin_ctrl.GetValue() + + self.state.config.face_morpher_random_seed_0 = self.face_morpher_random_seed_0_int_ctrl.GetValue() + self.state.config.face_morpher_random_seed_1 = self.face_morpher_random_seed_1_int_ctrl.GetValue() + self.state.config.face_morpher_batch_size = self.face_morpher_batch_size_spin_ctrl.GetValue() + + self.state.config.body_morpher_random_seed_0 = self.body_morpher_random_seed_0_int_ctrl.GetValue() + self.state.config.body_morpher_random_seed_1 = self.body_morpher_random_seed_1_int_ctrl.GetValue() + self.state.config.body_morpher_batch_size = self.body_morpher_batch_size_spin_ctrl.GetValue() + + if self.num_training_examples_per_sample_output_combobox.GetValue() == \ + DistillerUiMainFrame.NUM_TRAINING_EXAMPLES_PER_SAMPLE_OUTPUT_CHOICES[-1]: + self.state.config.face_morpher_num_training_examples_per_sample_output = None + self.state.config.body_morpher_num_training_examples_per_sample_output = None + else: + value = int(self.num_training_examples_per_sample_output_combobox.GetValue()) + self.state.config.face_morpher_num_training_examples_per_sample_output = value + self.state.config.body_morpher_num_training_examples_per_sample_output = value + + def update_ui(self): + self.prefix_text_ctrl.SetValue(self.state.config.prefix) + self.character_image_file_name_text_ctrl.SetValue(self.state.config.character_image_file_name) + self.face_mask_image_file_name_text_ctrl.SetValue(self.state.config.face_mask_image_file_name) + + if not self.state.can_show_character_image(): + self.draw_nothing_yet_string_to_bitmap(self.face_image_bitmap, 128, 128) + if not self.state.can_show_face_mask_image(): + self.draw_nothing_yet_string_to_bitmap(self.face_mask_image_bitmap, 128, 128) + if not self.state.can_show_mask_on_face_image(): + self.draw_nothing_yet_string_to_bitmap(self.mask_on_face_image_bitmap, 128, 128) + + self.num_cpu_workers_spin_ctrl.SetValue(self.state.config.num_cpu_workers) + self.num_gpus_spin_ctrl.SetValue(self.state.config.num_gpus) + + self.face_morpher_random_seed_0_int_ctrl.SetValue(self.state.config.face_morpher_random_seed_0) + self.face_morpher_random_seed_1_int_ctrl.SetValue(self.state.config.face_morpher_random_seed_1) + self.face_morpher_batch_size_spin_ctrl.SetValue(self.state.config.face_morpher_batch_size) + + self.body_morpher_random_seed_0_int_ctrl.SetValue(self.state.config.body_morpher_random_seed_0) + self.body_morpher_random_seed_1_int_ctrl.SetValue(self.state.config.body_morpher_random_seed_1) + self.body_morpher_batch_size_spin_ctrl.SetValue(self.state.config.body_morpher_batch_size) + + if self.state.config.body_morpher_num_training_examples_per_sample_output is None: + self.num_training_examples_per_sample_output_combobox.SetSelection(3) + else: + choices = [int(x) for x in DistillerUiMainFrame.NUM_TRAINING_EXAMPLES_PER_SAMPLE_OUTPUT_CHOICES[:-1]] + self.num_training_examples_per_sample_output_combobox.SetSelection( + choices.index(self.state.config.body_morpher_num_training_examples_per_sample_output)) + + self.save_menu_item.Enable(self.state.can_save()) + + self.Refresh() + + def draw_nothing_yet_string_to_bitmap(self, bitmap, width: int, height: int): + 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!", (width - w) // 2, (height - h) // 2) + + del dc + + def try_saving(self): + if not self.state.can_save(): + message_dialog = wx.MessageDialog( + self, + "Cannot save yet! Please make sure you set the prefix, character_image_file_name, " + "and face_mask_image_file_name first.", + "Error", + wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + return False + else: + if self.state.need_to_check_overwrite(): + confirmation_dialog = wx.MessageDialog( + parent=self, + message=f"Overwriting {self.state.config.config_yaml_file_name()}?", + caption="Confirmation", + style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION) + result = confirmation_dialog.ShowModal() + if result == wx.ID_YES: + self.state.save() + return True + elif result == wx.ID_NO: + return False + else: + return False + else: + self.state.save() + return True + + def on_save(self, event): + return self.try_saving() + + def on_new(self, event): + if self.state.dirty: + confirmation_dialog = wx.MessageDialog( + parent=self, + message=f"You have not saved the current config. Do you want to proceed?", + caption="Confirmation", + style=wx.YES_NO | wx.ICON_QUESTION) + result = confirmation_dialog.ShowModal() + if result == wx.ID_NO: + return + self.state = DistillerConfigState() + self.update_ui() + + def on_open(self, event): + if self.state.dirty: + confirmation_dialog = wx.MessageDialog( + parent=self, + message=f"You have not saved the current config. Do you want to proceed?", + caption="Confirmation", + style=wx.YES_NO | wx.ICON_QUESTION) + result = confirmation_dialog.ShowModal() + if result == wx.ID_NO: + return + + file_dialog = wx.FileDialog(self, "Choose a YAML file", wildcard="*.yaml", style=wx.FD_OPEN) + if file_dialog.ShowModal() != wx.ID_OK: + return + file_name = file_dialog.GetPath() + try: + self.state.load(file_name) + self.face_image_pytorch = None + self.face_mask_image_pytorch = None + self.update_face_image_bitmap(self.state.config.character_image_file_name) + self.update_face_mask_image_bitmap(self.state.config.face_mask_image_file_name) + self.update_ui() + except Exception as e: + message_dialog = wx.MessageDialog(self, str(e), "Error", wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + + def on_run(self, event): + try: + self.state.config.check() + except Exception as e: + message_dialog = wx.MessageDialog(self, str(e), "Error", wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + return + + if self.state.dirty: + message_dialog = wx.MessageDialog( + self, + "Please save the configuration first.", + "Error", + wx.OK | wx.ICON_ERROR) + message_dialog.ShowModal() + return + + self.config_file_to_run = self.state.config.config_yaml_file_name() + self.Destroy() diff --git a/tha4/src/tha4/image_util.py b/tha4/src/tha4/image_util.py new file mode 100644 index 0000000..cbe8482 --- /dev/null +++ b/tha4/src/tha4/image_util.py @@ -0,0 +1,58 @@ +import math + +import PIL.Image +import numpy +import torch +from matplotlib import cm +from tha4.shion.base.image_util import numpy_linear_to_srgb, pytorch_rgba_to_numpy_image, pytorch_rgb_to_numpy_image, \ + torch_linear_to_srgb + + +def grid_change_to_numpy_image(torch_image, num_channels=3): + height = torch_image.shape[1] + width = torch_image.shape[2] + size_image = (torch_image[0, :, :] ** 2 + torch_image[1, :, :] ** 2).sqrt().view(height, width, 1).numpy() + hsv = cm.get_cmap('hsv') + angle_image = hsv(((torch.atan2( + torch_image[0, :, :].view(height * width), + torch_image[1, :, :].view(height * width)).view(height, width) + math.pi) / (2 * math.pi)).numpy()) * 3 + numpy_image = size_image * angle_image[:, :, 0:3] + rgb_image = numpy_linear_to_srgb(numpy_image) + if num_channels == 3: + return rgb_image + elif num_channels == 4: + return numpy.concatenate([rgb_image, numpy.ones_like(size_image)], axis=2) + else: + raise RuntimeError("Unsupported num_channels: " + str(num_channels)) + + +def resize_PIL_image(pil_image, size=(256, 256)): + w, h = pil_image.size + d = min(w, h) + r = ((w - d) // 2, (h - d) // 2, (w + d) // 2, (h + d) // 2) + return pil_image.resize(size, resample=PIL.Image.LANCZOS, box=r) + + +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 = pytorch_rgba_to_numpy_image(output_image) + elif output_image.shape[0] == 3: + numpy_image = pytorch_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 = pytorch_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 + + +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) diff --git a/tha4/src/tha4/mocap/__init__.py b/tha4/src/tha4/mocap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/mocap/ifacialmocap_constants.py b/tha4/src/tha4/mocap/ifacialmocap_constants.py new file mode 100644 index 0000000..27031ac --- /dev/null +++ b/tha4/src/tha4/mocap/ifacialmocap_constants.py @@ -0,0 +1,239 @@ +EYE_LOOK_IN_LEFT = "eyeLookInLeft" +EYE_LOOK_OUT_LEFT = "eyeLookOutLeft" +EYE_LOOK_DOWN_LEFT = "eyeLookDownLeft" +EYE_LOOK_UP_LEFT = "eyeLookUpLeft" +EYE_BLINK_LEFT = "eyeBlinkLeft" +EYE_SQUINT_LEFT = "eyeSquintLeft" +EYE_WIDE_LEFT = "eyeWideLeft" +EYE_LOOK_IN_RIGHT = "eyeLookInRight" +EYE_LOOK_OUT_RIGHT = "eyeLookOutRight" +EYE_LOOK_DOWN_RIGHT = "eyeLookDownRight" +EYE_LOOK_UP_RIGHT = "eyeLookUpRight" +EYE_BLINK_RIGHT = "eyeBlinkRight" +EYE_SQUINT_RIGHT = "eyeSquintRight" +EYE_WIDE_RIGHT = "eyeWideRight" +BROW_DOWN_LEFT = "browDownLeft" +BROW_OUTER_UP_LEFT = "browOuterUpLeft" +BROW_DOWN_RIGHT = "browDownRight" +BROW_OUTER_UP_RIGHT = "browOuterUpRight" +BROW_INNER_UP = "browInnerUp" +NOSE_SNEER_LEFT = "noseSneerLeft" +NOSE_SNEER_RIGHT = "noseSneerRight" +CHEEK_SQUINT_LEFT = "cheekSquintLeft" +CHEEK_SQUINT_RIGHT = "cheekSquintRight" +CHEEK_PUFF = "cheekPuff" +MOUTH_LEFT = "mouthLeft" +MOUTH_DIMPLE_LEFT = "mouthDimpleLeft" +MOUTH_FROWN_LEFT = "mouthFrownLeft" +MOUTH_LOWER_DOWN_LEFT = "mouthLowerDownLeft" +MOUTH_PRESS_LEFT = "mouthPressLeft" +MOUTH_SMILE_LEFT = "mouthSmileLeft" +MOUTH_STRETCH_LEFT = "mouthStretchLeft" +MOUTH_UPPER_UP_LEFT = "mouthUpperUpLeft" +MOUTH_RIGHT = "mouthRight" +MOUTH_DIMPLE_RIGHT = "mouthDimpleRight" +MOUTH_FROWN_RIGHT = "mouthFrownRight" +MOUTH_LOWER_DOWN_RIGHT = "mouthLowerDownRight" +MOUTH_PRESS_RIGHT = "mouthPressRight" +MOUTH_SMILE_RIGHT = "mouthSmileRight" +MOUTH_STRETCH_RIGHT = "mouthStretchRight" +MOUTH_UPPER_UP_RIGHT = "mouthUpperUpRight" +MOUTH_CLOSE = "mouthClose" +MOUTH_FUNNEL = "mouthFunnel" +MOUTH_PUCKER = "mouthPucker" +MOUTH_ROLL_LOWER = "mouthRollLower" +MOUTH_ROLL_UPPER = "mouthRollUpper" +MOUTH_SHRUG_LOWER = "mouthShrugLower" +MOUTH_SHRUG_UPPER = "mouthShrugUpper" +JAW_LEFT = "jawLeft" +JAW_RIGHT = "jawRight" +JAW_FORWARD = "jawForward" +JAW_OPEN = "jawOpen" +TONGUE_OUT = "tongueOut" + +BLENDSHAPE_NAMES = [ + EYE_LOOK_IN_LEFT, # 0 + EYE_LOOK_OUT_LEFT, # 1 + EYE_LOOK_DOWN_LEFT, # 2 + EYE_LOOK_UP_LEFT, # 3 + EYE_BLINK_LEFT, # 4 + EYE_SQUINT_LEFT, # 5 + EYE_WIDE_LEFT, # 6 + EYE_LOOK_IN_RIGHT, # 7 + EYE_LOOK_OUT_RIGHT, # 8 + EYE_LOOK_DOWN_RIGHT, # 9 + EYE_LOOK_UP_RIGHT, # 10 + EYE_BLINK_RIGHT, # 11 + EYE_SQUINT_RIGHT, # 12 + EYE_WIDE_RIGHT, # 13 + BROW_DOWN_LEFT, # 14 + BROW_OUTER_UP_LEFT, # 15 + BROW_DOWN_RIGHT, # 16 + BROW_OUTER_UP_RIGHT, # 17 + BROW_INNER_UP, # 18 + NOSE_SNEER_LEFT, # 19 + NOSE_SNEER_RIGHT, # 20 + CHEEK_SQUINT_LEFT, # 21 + CHEEK_SQUINT_RIGHT, # 22 + CHEEK_PUFF, # 23 + MOUTH_LEFT, # 24 + MOUTH_DIMPLE_LEFT, # 25 + MOUTH_FROWN_LEFT, # 26 + MOUTH_LOWER_DOWN_LEFT, # 27 + MOUTH_PRESS_LEFT, # 28 + MOUTH_SMILE_LEFT, # 29 + MOUTH_STRETCH_LEFT, # 30 + MOUTH_UPPER_UP_LEFT, # 31 + MOUTH_RIGHT, # 32 + MOUTH_DIMPLE_RIGHT, # 33 + MOUTH_FROWN_RIGHT, # 34 + MOUTH_LOWER_DOWN_RIGHT, # 35 + MOUTH_PRESS_RIGHT, # 36 + MOUTH_SMILE_RIGHT, # 37 + MOUTH_STRETCH_RIGHT, # 38 + MOUTH_UPPER_UP_RIGHT, # 39 + MOUTH_CLOSE, # 40 + MOUTH_FUNNEL, # 41 + MOUTH_PUCKER, # 42 + MOUTH_ROLL_LOWER, # 43 + MOUTH_ROLL_UPPER, # 44 + MOUTH_SHRUG_LOWER, # 45 + MOUTH_SHRUG_UPPER, # 46 + JAW_LEFT, # 47 + JAW_RIGHT, # 48 + JAW_FORWARD, # 49 + JAW_OPEN, # 50 + TONGUE_OUT, # 51 +] + +EYE_LEFT_BLENDSHAPES = [ + EYE_LOOK_IN_LEFT, # 0 + EYE_LOOK_OUT_LEFT, # 1 + EYE_LOOK_DOWN_LEFT, # 2 + EYE_LOOK_UP_LEFT, # 3 + EYE_BLINK_LEFT, # 4 + EYE_SQUINT_LEFT, # 5 + EYE_WIDE_LEFT, # 6 +] + +EYE_RIGHT_BLENDSHAPES = [ + EYE_LOOK_IN_RIGHT, # 7 + EYE_LOOK_OUT_RIGHT, # 8 + EYE_LOOK_DOWN_RIGHT, # 9 + EYE_LOOK_UP_RIGHT, # 10 + EYE_BLINK_RIGHT, # 11 + EYE_SQUINT_RIGHT, # 12 + EYE_WIDE_RIGHT, # 13 +] + +BROW_LEFT_BLENDSHAPES = [ + BROW_DOWN_LEFT, # 14 + BROW_OUTER_UP_LEFT, # 15 + +] + +BROW_RIGHT_BLENDSHAPES = [ + BROW_DOWN_RIGHT, # 16 + BROW_OUTER_UP_RIGHT, # 17 + +] + +BROW_BOTH_BLENDSHAPES = [ + BROW_INNER_UP, # 18 +] + +NOSE_BLENDSHAPES = [ + NOSE_SNEER_LEFT, # 19 + NOSE_SNEER_RIGHT, # 20 +] + +CHECK_BLENDSHAPES = [ + CHEEK_SQUINT_LEFT, # 21 + CHEEK_SQUINT_RIGHT, # 22 + CHEEK_PUFF, # 23 +] + +MOUTH_LEFT_BLENDSHAPES = [ + MOUTH_LEFT, # 24 + MOUTH_DIMPLE_LEFT, # 25 + MOUTH_FROWN_LEFT, # 26 + MOUTH_LOWER_DOWN_LEFT, # 27 + MOUTH_PRESS_LEFT, # 28 + MOUTH_SMILE_LEFT, # 29 + MOUTH_STRETCH_LEFT, # 30 + MOUTH_UPPER_UP_LEFT, # 31 +] + +MOUTH_RIGHT_BLENDSHAPES = [ + MOUTH_RIGHT, # 32 + MOUTH_DIMPLE_RIGHT, # 33 + MOUTH_FROWN_RIGHT, # 34 + MOUTH_LOWER_DOWN_RIGHT, # 35 + MOUTH_PRESS_RIGHT, # 36 + MOUTH_SMILE_RIGHT, # 37 + MOUTH_STRETCH_RIGHT, # 38 + MOUTH_UPPER_UP_RIGHT, # 39 +] + +MOUTH_BOTH_BLENDSHAPES = [ + MOUTH_CLOSE, # 40 + MOUTH_FUNNEL, # 41 + MOUTH_PUCKER, # 42 + MOUTH_ROLL_LOWER, # 43 + MOUTH_ROLL_UPPER, # 44 + MOUTH_SHRUG_LOWER, # 45 + MOUTH_SHRUG_UPPER, # 46 +] + +JAW_BLENDSHAPES = [ + JAW_LEFT, # 47 + JAW_RIGHT, # 48 + JAW_FORWARD, # 49 + JAW_OPEN, # 50 +] + +TONGUE_BLENDSHAPES = [ + TONGUE_OUT, # 51 +] + +COLUMN_0_BLENDSHAPES = EYE_RIGHT_BLENDSHAPES + BROW_RIGHT_BLENDSHAPES + [NOSE_SNEER_RIGHT, CHEEK_SQUINT_RIGHT] +COLUMN_1_BLENDSHAPES = EYE_LEFT_BLENDSHAPES + BROW_LEFT_BLENDSHAPES + [NOSE_SNEER_LEFT, CHEEK_SQUINT_LEFT] +COLUMN_2_BLENDSHAPES = MOUTH_RIGHT_BLENDSHAPES + [JAW_RIGHT] +COLUMN_3_BLENDSHAPES = MOUTH_LEFT_BLENDSHAPES + [JAW_LEFT] +COLUMN_4_BLENDSHAPES = [BROW_INNER_UP, CHEEK_PUFF] + MOUTH_BOTH_BLENDSHAPES + [JAW_FORWARD, JAW_OPEN, TONGUE_OUT] + +BLENDSHAPE_COLUMNS = [ + COLUMN_0_BLENDSHAPES, + COLUMN_1_BLENDSHAPES, + COLUMN_2_BLENDSHAPES, + COLUMN_3_BLENDSHAPES, + COLUMN_4_BLENDSHAPES, +] + +RIGHT_EYE_BONE_X = "rightEyeBoneX" +RIGHT_EYE_BONE_Y = "rightEyeBoneY" +RIGHT_EYE_BONE_Z = "rightEyeBoneZ" +RIGHT_EYE_BONE_ROTATIONS = [RIGHT_EYE_BONE_X, RIGHT_EYE_BONE_Y, RIGHT_EYE_BONE_Z] + +LEFT_EYE_BONE_X = "leftEyeBoneX" +LEFT_EYE_BONE_Y = "leftEyeBoneY" +LEFT_EYE_BONE_Z = "leftEyeBoneZ" +LEFT_EYE_BONE_ROTATIONS = [LEFT_EYE_BONE_X, LEFT_EYE_BONE_Y, LEFT_EYE_BONE_Z] + +HEAD_BONE_X = "headBoneX" +HEAD_BONE_Y = "headBoneY" +HEAD_BONE_Z = "headBoneZ" +HEAD_BONE_ROTATIONS = [HEAD_BONE_X, HEAD_BONE_Y, HEAD_BONE_Z] + +ROTATION_NAMES = RIGHT_EYE_BONE_ROTATIONS + LEFT_EYE_BONE_ROTATIONS + HEAD_BONE_ROTATIONS + +RIGHT_EYE_BONE_QUAT = "rightEyeBoneQuat" +LEFT_EYE_BONE_QUAT = "leftEyeBoneQuat" +HEAD_BONE_QUAT = "headBoneQuat" +QUATERNION_NAMES = [ + RIGHT_EYE_BONE_QUAT, + LEFT_EYE_BONE_QUAT, + HEAD_BONE_QUAT +] + +IFACIALMOCAP_DATETIME_FORMAT = "%Y/%m/%d-%H:%M:%S.%f" diff --git a/tha4/src/tha4/mocap/ifacialmocap_pose.py b/tha4/src/tha4/mocap/ifacialmocap_pose.py new file mode 100644 index 0000000..595b3c8 --- /dev/null +++ b/tha4/src/tha4/mocap/ifacialmocap_pose.py @@ -0,0 +1,27 @@ +from tha4.mocap.ifacialmocap_constants import BLENDSHAPE_NAMES, HEAD_BONE_X, HEAD_BONE_Y, HEAD_BONE_Z, \ + HEAD_BONE_QUAT, LEFT_EYE_BONE_X, LEFT_EYE_BONE_Y, LEFT_EYE_BONE_Z, LEFT_EYE_BONE_QUAT, RIGHT_EYE_BONE_X, \ + RIGHT_EYE_BONE_Y, RIGHT_EYE_BONE_Z, RIGHT_EYE_BONE_QUAT + + +def create_default_ifacialmocap_pose(): + data = {} + + for blendshape_name in BLENDSHAPE_NAMES: + data[blendshape_name] = 0.0 + + data[HEAD_BONE_X] = 0.0 + data[HEAD_BONE_Y] = 0.0 + data[HEAD_BONE_Z] = 0.0 + data[HEAD_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + + data[LEFT_EYE_BONE_X] = 0.0 + data[LEFT_EYE_BONE_Y] = 0.0 + data[LEFT_EYE_BONE_Z] = 0.0 + data[LEFT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + + data[RIGHT_EYE_BONE_X] = 0.0 + data[RIGHT_EYE_BONE_Y] = 0.0 + data[RIGHT_EYE_BONE_Z] = 0.0 + data[RIGHT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + + return data \ No newline at end of file diff --git a/tha4/src/tha4/mocap/ifacialmocap_pose_converter.py b/tha4/src/tha4/mocap/ifacialmocap_pose_converter.py new file mode 100644 index 0000000..390460b --- /dev/null +++ b/tha4/src/tha4/mocap/ifacialmocap_pose_converter.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod +from typing import Dict, List + + +class IFacialMocapPoseConverter(ABC): + @abstractmethod + def convert(self, ifacialmocap_pose: Dict[str, float]) -> List[float]: + pass + + @abstractmethod + def init_pose_converter_panel(self, parent): + pass \ No newline at end of file diff --git a/tha4/src/tha4/mocap/ifacialmocap_pose_converter_25.py b/tha4/src/tha4/mocap/ifacialmocap_pose_converter_25.py new file mode 100644 index 0000000..25a7bab --- /dev/null +++ b/tha4/src/tha4/mocap/ifacialmocap_pose_converter_25.py @@ -0,0 +1,614 @@ +import math +import time +from enum import Enum +from typing import Optional, Dict, List, Callable + +import numpy +import scipy.optimize +import wx + +from tha4.mocap.ifacialmocap_constants import MOUTH_SMILE_LEFT, MOUTH_SHRUG_UPPER, MOUTH_SMILE_RIGHT, \ + BROW_INNER_UP, BROW_OUTER_UP_RIGHT, BROW_OUTER_UP_LEFT, BROW_DOWN_LEFT, BROW_DOWN_RIGHT, EYE_WIDE_LEFT, \ + EYE_WIDE_RIGHT, EYE_BLINK_LEFT, EYE_BLINK_RIGHT, CHEEK_SQUINT_LEFT, CHEEK_SQUINT_RIGHT, EYE_LOOK_IN_LEFT, \ + EYE_LOOK_OUT_LEFT, EYE_LOOK_IN_RIGHT, EYE_LOOK_OUT_RIGHT, EYE_LOOK_UP_LEFT, EYE_LOOK_UP_RIGHT, EYE_LOOK_DOWN_RIGHT, \ + EYE_LOOK_DOWN_LEFT, HEAD_BONE_X, HEAD_BONE_Y, HEAD_BONE_Z, JAW_OPEN, MOUTH_FROWN_LEFT, MOUTH_FROWN_RIGHT, \ + MOUTH_LOWER_DOWN_LEFT, MOUTH_LOWER_DOWN_RIGHT, MOUTH_FUNNEL, MOUTH_PUCKER +from tha4.mocap.ifacialmocap_pose_converter import IFacialMocapPoseConverter +from tha4.poser.modes.pose_parameters import get_pose_parameters + + +class EyebrowDownMode(Enum): + TROUBLED = 1 + ANGRY = 2 + LOWERED = 3 + SERIOUS = 4 + + +class WinkMode(Enum): + NORMAL = 1 + RELAXED = 2 + + +def rad_to_deg(rad): + return rad * 180.0 / math.pi + + +def deg_to_rad(deg): + return deg * math.pi / 180.0 + + +def clamp(x, min_value, max_value): + return max(min_value, min(max_value, x)) + + +class IFacialMocapPoseConverter25Args: + def __init__(self, + smile_threshold_min: float = 0.4, + smile_threshold_max: float = 0.6, + eyebrow_down_mode: EyebrowDownMode = EyebrowDownMode.ANGRY, + wink_mode: WinkMode = WinkMode.NORMAL, + eye_surprised_max: float = 0.5, + eye_blink_max: float = 0.8, + eyebrow_down_max: float = 0.4, + cheek_squint_min: float = 0.1, + cheek_squint_max: float = 0.7, + eye_rotation_factor: float = 1.0 / 0.75, + jaw_open_min: float = 0.1, + jaw_open_max: float = 0.4, + mouth_frown_max: float = 0.6, + mouth_funnel_min: float = 0.25, + mouth_funnel_max: float = 0.5, + iris_small_left=0.0, + iris_small_right=0.0): + self.iris_small_right = iris_small_left + self.iris_small_left = iris_small_right + + self.wink_mode = wink_mode + + self.mouth_funnel_max = mouth_funnel_max + self.mouth_funnel_min = mouth_funnel_min + self.mouth_frown_max = mouth_frown_max + + self.jaw_open_max = jaw_open_max + self.jaw_open_min = jaw_open_min + + self.eye_rotation_factor = eye_rotation_factor + + self.cheek_squint_max = cheek_squint_max + self.cheek_squint_min = cheek_squint_min + + self.eyebrow_down_max = eyebrow_down_max + + self.eye_blink_max = eye_blink_max + self.eye_surprised_max = eye_surprised_max + self.eyebrow_down_mode = eyebrow_down_mode + + self.smile_threshold_min = smile_threshold_min + self.smile_threshold_max = smile_threshold_max + + def set_smile_threshold_min(self, new_value: float): + self.smile_threshold_min = new_value + + def set_smile_threshold_max(self, new_value: float): + self.smile_threshold_max = new_value + + def set_eye_surprised_max(self, new_value: float): + self.eye_surprised_max = new_value + + def set_eye_blink_max(self, new_value: float): + self.eye_blink_max = new_value + + def set_eyebrow_down_max(self, new_value: float): + self.eyebrow_down_max = new_value + + def set_cheek_squint_min(self, new_value: float): + self.cheek_squint_min = new_value + + def set_cheek_squint_max(self, new_value: float): + self.cheek_squint_max = new_value + + def set_jaw_open_min(self, new_value: float): + self.jaw_open_min = new_value + + def set_jaw_open_max(self, new_value: float): + self.jaw_open_max = new_value + + def set_mouth_frown_max(self, new_value: float): + self.mouth_frown_max = new_value + + def set_mouth_funnel_min(self, new_value: float): + self.mouth_funnel_min = new_value + + def set_mouth_funnel_max(self, new_value: float): + self.mouth_funnel_min = new_value + + +class IFacialMocapPoseConverter25(IFacialMocapPoseConverter): + def __init__(self, args: Optional[IFacialMocapPoseConverter25Args] = None): + super().__init__() + if args is None: + args = IFacialMocapPoseConverter25Args() + self.args = args + pose_parameters = get_pose_parameters() + self.pose_size = 45 + + self.eyebrow_troubled_left_index = pose_parameters.get_parameter_index("eyebrow_troubled_left") + self.eyebrow_troubled_right_index = pose_parameters.get_parameter_index("eyebrow_troubled_right") + self.eyebrow_angry_left_index = pose_parameters.get_parameter_index("eyebrow_angry_left") + self.eyebrow_angry_right_index = pose_parameters.get_parameter_index("eyebrow_angry_right") + self.eyebrow_happy_left_index = pose_parameters.get_parameter_index("eyebrow_happy_left") + self.eyebrow_happy_right_index = pose_parameters.get_parameter_index("eyebrow_happy_right") + self.eyebrow_raised_left_index = pose_parameters.get_parameter_index("eyebrow_raised_left") + self.eyebrow_raised_right_index = pose_parameters.get_parameter_index("eyebrow_raised_right") + self.eyebrow_lowered_left_index = pose_parameters.get_parameter_index("eyebrow_lowered_left") + self.eyebrow_lowered_right_index = pose_parameters.get_parameter_index("eyebrow_lowered_right") + self.eyebrow_serious_left_index = pose_parameters.get_parameter_index("eyebrow_serious_left") + self.eyebrow_serious_right_index = pose_parameters.get_parameter_index("eyebrow_serious_right") + + self.eye_surprised_left_index = pose_parameters.get_parameter_index("eye_surprised_left") + self.eye_surprised_right_index = pose_parameters.get_parameter_index("eye_surprised_right") + self.eye_wink_left_index = pose_parameters.get_parameter_index("eye_wink_left") + self.eye_wink_right_index = pose_parameters.get_parameter_index("eye_wink_right") + self.eye_happy_wink_left_index = pose_parameters.get_parameter_index("eye_happy_wink_left") + self.eye_happy_wink_right_index = pose_parameters.get_parameter_index("eye_happy_wink_right") + self.eye_relaxed_left_index = pose_parameters.get_parameter_index("eye_relaxed_left") + self.eye_relaxed_right_index = pose_parameters.get_parameter_index("eye_relaxed_right") + self.eye_raised_lower_eyelid_left_index = pose_parameters.get_parameter_index("eye_raised_lower_eyelid_left") + self.eye_raised_lower_eyelid_right_index = pose_parameters.get_parameter_index("eye_raised_lower_eyelid_right") + + self.iris_small_left_index = pose_parameters.get_parameter_index("iris_small_left") + self.iris_small_right_index = pose_parameters.get_parameter_index("iris_small_right") + + self.iris_rotation_x_index = pose_parameters.get_parameter_index("iris_rotation_x") + self.iris_rotation_y_index = pose_parameters.get_parameter_index("iris_rotation_y") + + self.head_x_index = pose_parameters.get_parameter_index("head_x") + self.head_y_index = pose_parameters.get_parameter_index("head_y") + self.neck_z_index = pose_parameters.get_parameter_index("neck_z") + + self.mouth_aaa_index = pose_parameters.get_parameter_index("mouth_aaa") + self.mouth_iii_index = pose_parameters.get_parameter_index("mouth_iii") + self.mouth_uuu_index = pose_parameters.get_parameter_index("mouth_uuu") + self.mouth_eee_index = pose_parameters.get_parameter_index("mouth_eee") + self.mouth_ooo_index = pose_parameters.get_parameter_index("mouth_ooo") + + self.mouth_lowered_corner_left_index = pose_parameters.get_parameter_index("mouth_lowered_corner_left") + self.mouth_lowered_corner_right_index = pose_parameters.get_parameter_index("mouth_lowered_corner_right") + self.mouth_raised_corner_left_index = pose_parameters.get_parameter_index("mouth_raised_corner_left") + self.mouth_raised_corner_right_index = pose_parameters.get_parameter_index("mouth_raised_corner_right") + + self.body_y_index = pose_parameters.get_parameter_index("body_y") + self.body_z_index = pose_parameters.get_parameter_index("body_z") + self.breathing_index = pose_parameters.get_parameter_index("breathing") + + self.breathing_start_time = time.time() + + self.panel = None + + def init_pose_converter_panel(self, parent): + self.panel = wx.Panel(parent, style=wx.SIMPLE_BORDER) + self.panel_sizer = wx.BoxSizer(wx.VERTICAL) + self.panel.SetSizer(self.panel_sizer) + self.panel.SetAutoLayout(1) + parent.GetSizer().Add(self.panel, 0, wx.EXPAND) + + if True: + eyebrow_down_mode_text = wx.StaticText(self.panel, label=" --- Eyebrow Down Mode --- ", + style=wx.ALIGN_CENTER) + self.panel_sizer.Add(eyebrow_down_mode_text, 0, wx.EXPAND) + + self.eyebrow_down_mode_choice = wx.Choice( + self.panel, + choices=[ + "ANGRY", + "TROUBLED", + "SERIOUS", + "LOWERED", + ]) + self.eyebrow_down_mode_choice.SetSelection(0) + self.panel_sizer.Add(self.eyebrow_down_mode_choice, 0, wx.EXPAND) + self.eyebrow_down_mode_choice.Bind(wx.EVT_CHOICE, self.change_eyebrow_down_mode) + + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + if True: + wink_mode_text = wx.StaticText(self.panel, label=" --- Wink Mode --- ", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(wink_mode_text, 0, wx.EXPAND) + + self.wink_mode_choice = wx.Choice( + self.panel, + choices=[ + "NORMAL", + "RELAXED", + ]) + self.wink_mode_choice.SetSelection(0) + self.panel_sizer.Add(self.wink_mode_choice, 0, wx.EXPAND) + self.wink_mode_choice.Bind(wx.EVT_CHOICE, self.change_wink_mode) + + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + if True: + iris_size_text = wx.StaticText(self.panel, label=" --- Iris Size --- ", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(iris_size_text, 0, wx.EXPAND) + + self.iris_left_slider = wx.Slider(self.panel, minValue=0, maxValue=1000, value=0, style=wx.HORIZONTAL) + self.panel_sizer.Add(self.iris_left_slider, 0, wx.EXPAND) + self.iris_left_slider.Bind(wx.EVT_SLIDER, self.change_iris_size) + + self.iris_right_slider = wx.Slider(self.panel, minValue=0, maxValue=1000, value=0, style=wx.HORIZONTAL) + self.panel_sizer.Add(self.iris_right_slider, 0, wx.EXPAND) + self.iris_right_slider.Bind(wx.EVT_SLIDER, self.change_iris_size) + self.iris_right_slider.Enable(False) + + self.link_left_right_irises = wx.CheckBox( + self.panel, label="Use same value for both sides") + self.link_left_right_irises.SetValue(True) + self.panel_sizer.Add(self.link_left_right_irises, wx.SizerFlags().CenterHorizontal().Border()) + self.link_left_right_irises.Bind(wx.EVT_CHECKBOX, self.link_left_right_irises_clicked) + + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + if True: + breathing_frequency_text = wx.StaticText( + self.panel, label=" --- Breathing --- ", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(breathing_frequency_text, 0, wx.EXPAND) + + self.restart_breathing_cycle_button = wx.Button(self.panel, label="Restart Breathing Cycle") + self.restart_breathing_cycle_button.Bind(wx.EVT_BUTTON, self.restart_breathing_cycle_clicked) + self.panel_sizer.Add(self.restart_breathing_cycle_button, 0, wx.EXPAND) + + self.breathing_frequency_slider = wx.Slider( + self.panel, minValue=0, maxValue=60, value=20, style=wx.HORIZONTAL) + self.panel_sizer.Add(self.breathing_frequency_slider, 0, wx.EXPAND) + + self.breathing_gauge = wx.Gauge(self.panel, style=wx.GA_HORIZONTAL, range=1000) + self.panel_sizer.Add(self.breathing_gauge, 0, wx.EXPAND) + + if True: + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + convertion_parameters_text = wx.StaticText( + self.panel, label="--- Conversion Parameters ---", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(convertion_parameters_text, 0, wx.EXPAND) + + conversion_param_panel = wx.Panel(self.panel) + self.panel_sizer.Add(conversion_param_panel, 0, wx.EXPAND) + conversion_panel_sizer = wx.FlexGridSizer(cols=2) + conversion_panel_sizer.AddGrowableCol(1) + conversion_param_panel.SetSizer(conversion_panel_sizer) + conversion_param_panel.SetAutoLayout(1) + + self.smile_thresold_min_spin = self.create_spin_control( + conversion_param_panel, + "Smile Threshold Min:", self.args.smile_threshold_min, self.args.set_smile_threshold_min) + self.smile_thresold_max_spin = self.create_spin_control( + conversion_param_panel, + "Smile Threshold Max:", self.args.smile_threshold_max, self.args.set_smile_threshold_max) + self.eye_surprised_max_spin = self.create_spin_control( + conversion_param_panel, + "Eye Surprised Max:", self.args.eye_surprised_max, self.args.set_eye_surprised_max) + self.eye_blink_max_spin = self.create_spin_control( + conversion_param_panel, + "Eye Blink Max:", self.args.eye_blink_max, self.args.set_eye_blink_max) + self.eyebrow_down_max_spin = self.create_spin_control( + conversion_param_panel, + "Eyebrow Down Max:", self.args.eyebrow_down_max, self.args.set_eyebrow_down_max) + self.cheek_squint_min_spin = self.create_spin_control( + conversion_param_panel, + "Cheek Squint Min:", self.args.cheek_squint_min, self.args.set_cheek_squint_min) + self.cheek_squint_max_spin = self.create_spin_control( + conversion_param_panel, + "Cheek Squint Max:", self.args.cheek_squint_max, self.args.set_cheek_squint_max) + self.jaw_open_min_spin = self.create_spin_control( + conversion_param_panel, + "Jaw Open Min:", self.args.jaw_open_min, self.args.set_jaw_open_min) + self.jaw_open_max_spin = self.create_spin_control( + conversion_param_panel, + "Jaw Open Max:", self.args.jaw_open_max, self.args.set_jaw_open_max) + self.mouth_frown_max_spin = self.create_spin_control( + conversion_param_panel, + "Mouth Frown Max:", self.args.mouth_frown_max, self.args.set_mouth_frown_max) + self.mouth_funnel_min_spin = self.create_spin_control( + conversion_param_panel, + "Mouth Funnel Min:", self.args.mouth_funnel_min, self.args.set_mouth_funnel_min) + self.mouth_funnel_max_spin = self.create_spin_control( + conversion_param_panel, + "Mouth Funnel Max:", self.args.mouth_funnel_max, self.args.set_mouth_funnel_max) + + self.panel_sizer.Fit(self.panel) + + def create_spin_control(self, parent, label: str, initial_value: float, set_func: Callable[[float], None]): + sizer = parent.GetSizer() + + text = wx.StaticText(parent, label=label) + sizer.Add(text, wx.SizerFlags().Right().Border(wx.ALL, 2)) + + spin_ctrl = wx.SpinCtrlDouble( + parent, + wx.ID_ANY, + min=0.0, + max=1.0, + initial=initial_value, + inc=0.01) + sizer.Add(spin_ctrl, wx.SizerFlags().Border(wx.ALL, 2).Expand()) + + def handler(event: wx.Event): + new_value = spin_ctrl.GetValue() + set_func(new_value) + + spin_ctrl.Bind(wx.EVT_SPINCTRLDOUBLE, handler) + + return spin_ctrl + + def restart_breathing_cycle_clicked(self, event: wx.Event): + self.breathing_start_time = time.time() + + def change_eyebrow_down_mode(self, event: wx.Event): + selected_index = self.eyebrow_down_mode_choice.GetSelection() + if selected_index == 0: + self.args.eyebrow_down_mode = EyebrowDownMode.ANGRY + elif selected_index == 1: + self.args.eyebrow_down_mode = EyebrowDownMode.TROUBLED + elif selected_index == 2: + self.args.eyebrow_down_mode = EyebrowDownMode.SERIOUS + else: + self.args.eyebrow_down_mode = EyebrowDownMode.LOWERED + + def change_wink_mode(self, event: wx.Event): + selected_index = self.wink_mode_choice.GetSelection() + if selected_index == 0: + self.args.wink_mode = WinkMode.NORMAL + else: + self.args.wink_mode = WinkMode.RELAXED + + def change_iris_size(self, event: wx.Event): + if self.link_left_right_irises.GetValue(): + left_value = self.iris_left_slider.GetValue() + right_value = self.iris_right_slider.GetValue() + if left_value != right_value: + self.iris_right_slider.SetValue(left_value) + self.args.iris_small_left = left_value / 1000.0 + self.args.iris_small_right = left_value / 1000.0 + else: + self.args.iris_small_left = self.iris_left_slider.GetValue() / 1000.0 + self.args.iris_small_right = self.iris_right_slider.GetValue() / 1000.0 + + def link_left_right_irises_clicked(self, event: wx.Event): + if self.link_left_right_irises.GetValue(): + self.iris_right_slider.Enable(False) + else: + self.iris_right_slider.Enable(True) + self.change_iris_size(event) + + def decompose_head_body_param(self, param, threshold=2.0 / 3): + if abs(param) < threshold: + return (param, 0.0) + else: + if param < 0: + sign = -1.0 + else: + sign = 1.0 + return (threshold * sign, (abs(param) - threshold) * sign) + + def convert(self, ifacialmocap_pose: Dict[str, float]) -> List[float]: + pose = [0.0 for i in range(self.pose_size)] + + smile_value = \ + (ifacialmocap_pose[MOUTH_SMILE_LEFT] + ifacialmocap_pose[MOUTH_SMILE_RIGHT]) / 2.0 \ + + ifacialmocap_pose[MOUTH_SHRUG_UPPER] + if self.args.smile_threshold_min >= self.args.smile_threshold_max: + smile_degree = 0.0 + else: + if smile_value < self.args.smile_threshold_min: + smile_degree = 0.0 + elif smile_value > self.args.smile_threshold_max: + smile_degree = 1.0 + else: + smile_degree = (smile_value - self.args.smile_threshold_min) / ( + self.args.smile_threshold_max - self.args.smile_threshold_min) + + # Eyebrow + if True: + brow_inner_up = ifacialmocap_pose[BROW_INNER_UP] + brow_outer_up_right = ifacialmocap_pose[BROW_OUTER_UP_RIGHT] + brow_outer_up_left = ifacialmocap_pose[BROW_OUTER_UP_LEFT] + + brow_up_left = clamp(brow_inner_up + brow_outer_up_left, 0.0, 1.0) + brow_up_right = clamp(brow_inner_up + brow_outer_up_right, 0.0, 1.0) + pose[self.eyebrow_raised_left_index] = brow_up_left + pose[self.eyebrow_raised_right_index] = brow_up_right + + if self.args.eyebrow_down_max <= 0.0: + brow_down_left = 0.0 + brow_down_right = 0.0 + else: + brow_down_left = (1.0 - smile_degree) \ + * clamp(ifacialmocap_pose[BROW_DOWN_LEFT] / self.args.eyebrow_down_max, 0.0, 1.0) + brow_down_right = (1.0 - smile_degree) \ + * clamp(ifacialmocap_pose[BROW_DOWN_RIGHT] / self.args.eyebrow_down_max, 0.0, 1.0) + + if self.args.eyebrow_down_mode == EyebrowDownMode.TROUBLED: + pose[self.eyebrow_troubled_left_index] = brow_down_left + pose[self.eyebrow_troubled_right_index] = brow_down_right + elif self.args.eyebrow_down_mode == EyebrowDownMode.ANGRY: + pose[self.eyebrow_angry_left_index] = brow_down_left + pose[self.eyebrow_angry_right_index] = brow_down_right + elif self.args.eyebrow_down_mode == EyebrowDownMode.LOWERED: + pose[self.eyebrow_lowered_left_index] = brow_down_left + pose[self.eyebrow_lowered_right_index] = brow_down_right + elif self.args.eyebrow_down_mode == EyebrowDownMode.SERIOUS: + pose[self.eyebrow_serious_left_index] = brow_down_left + pose[self.eyebrow_serious_right_index] = brow_down_right + + brow_happy_value = clamp(smile_value, 0.0, 1.0) * smile_degree + pose[self.eyebrow_happy_left_index] = brow_happy_value + pose[self.eyebrow_happy_right_index] = brow_happy_value + + # Eye + if True: + # Surprised + if self.args.eye_surprised_max <= 0.0: + pose[self.eye_surprised_left_index] = 0.0 + pose[self.eye_surprised_right_index] = 0.0 + else: + pose[self.eye_surprised_left_index] = clamp( + ifacialmocap_pose[EYE_WIDE_LEFT] / self.args.eye_surprised_max, 0.0, 1.0) + pose[self.eye_surprised_right_index] = clamp( + ifacialmocap_pose[EYE_WIDE_RIGHT] / self.args.eye_surprised_max, 0.0, 1.0) + + # Wink + if self.args.wink_mode == WinkMode.NORMAL: + wink_left_index = self.eye_wink_left_index + wink_right_index = self.eye_wink_right_index + else: + wink_left_index = self.eye_relaxed_left_index + wink_right_index = self.eye_relaxed_right_index + if self.args.eye_blink_max <= 0: + pose[wink_left_index] = 0.0 + pose[wink_right_index] = 0.0 + pose[self.eye_happy_wink_left_index] = 0.0 + pose[self.eye_happy_wink_right_index] = 0.0 + else: + pose[wink_left_index] = (1.0 - smile_degree) * clamp( + ifacialmocap_pose[EYE_BLINK_LEFT] / self.args.eye_blink_max, 0.0, 1.0) + pose[wink_right_index] = (1.0 - smile_degree) * clamp( + ifacialmocap_pose[EYE_BLINK_RIGHT] / self.args.eye_blink_max, 0.0, 1.0) + pose[self.eye_happy_wink_left_index] = smile_degree * clamp( + ifacialmocap_pose[EYE_BLINK_LEFT] / self.args.eye_blink_max, 0.0, 1.0) + pose[self.eye_happy_wink_right_index] = smile_degree * clamp( + ifacialmocap_pose[EYE_BLINK_RIGHT] / self.args.eye_blink_max, 0.0, 1.0) + + # Lower eyelid + cheek_squint_denom = self.args.cheek_squint_max - self.args.cheek_squint_min + if cheek_squint_denom <= 0.0: + pose[self.eye_raised_lower_eyelid_left_index] = 0.0 + pose[self.eye_raised_lower_eyelid_right_index] = 0.0 + else: + pose[self.eye_raised_lower_eyelid_left_index] = \ + clamp( + (ifacialmocap_pose[CHEEK_SQUINT_LEFT] - self.args.cheek_squint_min) / cheek_squint_denom, + 0.0, 1.0) + pose[self.eye_raised_lower_eyelid_right_index] = \ + clamp( + (ifacialmocap_pose[CHEEK_SQUINT_RIGHT] - self.args.cheek_squint_min) / cheek_squint_denom, + 0.0, 1.0) + + # Iris rotation + if True: + eye_rotation_y = (ifacialmocap_pose[EYE_LOOK_IN_LEFT] + - ifacialmocap_pose[EYE_LOOK_OUT_LEFT] + - ifacialmocap_pose[EYE_LOOK_IN_RIGHT] + + ifacialmocap_pose[EYE_LOOK_OUT_RIGHT]) / 2.0 * self.args.eye_rotation_factor + pose[self.iris_rotation_y_index] = clamp(eye_rotation_y, -1.0, 1.0) + + eye_rotation_x = (ifacialmocap_pose[EYE_LOOK_UP_LEFT] + + ifacialmocap_pose[EYE_LOOK_UP_RIGHT] + - ifacialmocap_pose[EYE_LOOK_DOWN_LEFT] + - ifacialmocap_pose[EYE_LOOK_DOWN_RIGHT]) / 2.0 * self.args.eye_rotation_factor + pose[self.iris_rotation_x_index] = clamp(eye_rotation_x, -1.0, 1.0) + + # Iris size + if True: + pose[self.iris_small_left_index] = self.args.iris_small_left + pose[self.iris_small_right_index] = self.args.iris_small_right + + # Head rotation + if True: + x_param = clamp(-ifacialmocap_pose[HEAD_BONE_X] * 180.0 / math.pi, -15.0, 15.0) / 15.0 + pose[self.head_x_index] = x_param + + y_param = clamp(-ifacialmocap_pose[HEAD_BONE_Y] * 180.0 / math.pi, -10.0, 10.0) / 10.0 + pose[self.head_y_index] = y_param + pose[self.body_y_index] = y_param + + z_param = clamp(ifacialmocap_pose[HEAD_BONE_Z] * 180.0 / math.pi, -15.0, 15.0) / 15.0 + pose[self.neck_z_index] = z_param + pose[self.body_z_index] = z_param + + # Mouth + if True: + jaw_open_denom = self.args.jaw_open_max - self.args.jaw_open_min + if jaw_open_denom <= 0: + mouth_open = 0.0 + else: + mouth_open = clamp((ifacialmocap_pose[JAW_OPEN] - self.args.jaw_open_min) / jaw_open_denom, 0.0, 1.0) + pose[self.mouth_aaa_index] = mouth_open + pose[self.mouth_raised_corner_left_index] = clamp(smile_value, 0.0, 1.0) + pose[self.mouth_raised_corner_right_index] = clamp(smile_value, 0.0, 1.0) + + is_mouth_open = mouth_open > 0.0 + if not is_mouth_open: + if self.args.mouth_frown_max > 0: + mouth_frown_value = 0.0 + else: + mouth_frown_value = clamp( + (ifacialmocap_pose[MOUTH_FROWN_LEFT] + ifacialmocap_pose[ + MOUTH_FROWN_RIGHT]) / self.args.mouth_frown_max, 0.0, 1.0) + pose[self.mouth_lowered_corner_left_index] = mouth_frown_value + pose[self.mouth_lowered_corner_right_index] = mouth_frown_value + else: + mouth_lower_down = clamp( + ifacialmocap_pose[MOUTH_LOWER_DOWN_LEFT] + ifacialmocap_pose[MOUTH_LOWER_DOWN_RIGHT], 0.0, 1.0) + mouth_funnel = ifacialmocap_pose[MOUTH_FUNNEL] + mouth_pucker = ifacialmocap_pose[MOUTH_PUCKER] + + mouth_point = [mouth_open, mouth_lower_down, mouth_funnel, mouth_pucker] + + aaa_point = [1.0, 1.0, 0.0, 0.0] + iii_point = [0.0, 1.0, 0.0, 0.0] + uuu_point = [0.5, 0.3, 0.25, 0.75] + ooo_point = [1.0, 0.5, 0.5, 0.4] + + decomp = numpy.array([0, 0, 0, 0]) + M = numpy.array([ + aaa_point, + iii_point, + uuu_point, + ooo_point + ]) + + def loss(decomp): + return numpy.linalg.norm(numpy.matmul(decomp, M) - mouth_point) \ + + 0.01 * numpy.linalg.norm(decomp, ord=1) + + opt_result = scipy.optimize.minimize( + loss, decomp, bounds=[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)]) + decomp = opt_result["x"] + restricted_decomp = [decomp.item(0), decomp.item(1), decomp.item(2), decomp.item(3)] + pose[self.mouth_aaa_index] = restricted_decomp[0] + pose[self.mouth_iii_index] = restricted_decomp[1] + mouth_funnel_denom = self.args.mouth_funnel_max - self.args.mouth_funnel_min + if mouth_funnel_denom <= 0: + ooo_alpha = 0.0 + uo_value = 0.0 + else: + ooo_alpha = clamp((mouth_funnel - self.args.mouth_funnel_min) / mouth_funnel_denom, 0.0, 1.0) + uo_value = clamp(restricted_decomp[2] + restricted_decomp[3], 0.0, 1.0) + pose[self.mouth_uuu_index] = uo_value * (1.0 - ooo_alpha) + pose[self.mouth_ooo_index] = uo_value * ooo_alpha + + if self.panel is not None: + frequency = self.breathing_frequency_slider.GetValue() + if frequency == 0: + value = 0.0 + pose[self.breathing_index] = value + self.breathing_start_time = time.time() + else: + period = 60.0 / frequency + now = time.time() + diff = now - self.breathing_start_time + frac = (diff % period) / period + value = (-math.cos(2 * math.pi * frac) + 1.0) / 2.0 + pose[self.breathing_index] = value + self.breathing_gauge.SetValue(int(1000 * value)) + + return pose + + +def create_ifacialmocap_pose_converter( + args: Optional[IFacialMocapPoseConverter25Args] = None) -> IFacialMocapPoseConverter: + return IFacialMocapPoseConverter25(args) diff --git a/tha4/src/tha4/mocap/ifacialmocap_v2.py b/tha4/src/tha4/mocap/ifacialmocap_v2.py new file mode 100644 index 0000000..f61a34b --- /dev/null +++ b/tha4/src/tha4/mocap/ifacialmocap_v2.py @@ -0,0 +1,89 @@ +import math + +from tha4.mocap.ifacialmocap_constants import BLENDSHAPE_NAMES, HEAD_BONE_X, HEAD_BONE_Y, HEAD_BONE_Z, \ + RIGHT_EYE_BONE_X, RIGHT_EYE_BONE_Y, RIGHT_EYE_BONE_Z, LEFT_EYE_BONE_X, LEFT_EYE_BONE_Y, LEFT_EYE_BONE_Z, \ + HEAD_BONE_QUAT, LEFT_EYE_BONE_QUAT, RIGHT_EYE_BONE_QUAT + +IFACIALMOCAP_PORT = 49983 +IFACIALMOCAP_START_STRING = "iFacialMocap_sahuasouryya9218sauhuiayeta91555dy3719|sendDataVersion=v2".encode('utf-8') + + +def parse_ifacialmocap_v2_pose(ifacialmocap_output): + output = {} + parts = ifacialmocap_output.split("|") + for part in parts: + part = part.strip() + if len(part) == 0: + continue + if "&" in part: + components = part.split("&") + assert len(components) == 2 + key = components[0] + value = float(components[1]) / 100.0 + if key.endswith("_L"): + key = key[:-2] + "Left" + elif key.endswith("_R"): + key = key[:-2] + "Right" + if key in BLENDSHAPE_NAMES: + output[key] = value + elif part.startswith("=head#"): + components = part[len("=head#"):].split(",") + assert len(components) == 6 + output[HEAD_BONE_X] = float(components[0]) * math.pi / 180 + output[HEAD_BONE_Y] = float(components[1]) * math.pi / 180 + output[HEAD_BONE_Z] = float(components[2]) * math.pi / 180 + elif part.startswith("rightEye#"): + components = part[len("rightEye#"):].split(",") + output[RIGHT_EYE_BONE_X] = float(components[0]) * math.pi / 180 + output[RIGHT_EYE_BONE_Y] = float(components[1]) * math.pi / 180 + output[RIGHT_EYE_BONE_Z] = float(components[2]) * math.pi / 180 + elif part.startswith("leftEye#"): + components = part[len("leftEye#"):].split(",") + output[LEFT_EYE_BONE_X] = float(components[0]) * math.pi / 180 + output[LEFT_EYE_BONE_Y] = float(components[1]) * math.pi / 180 + output[LEFT_EYE_BONE_Z] = float(components[2]) * math.pi / 180 + output[HEAD_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + output[LEFT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + output[RIGHT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + return output + + +def parse_ifacialmocap_v1_pose(ifacialmocap_output): + output = {} + parts = ifacialmocap_output.split("|") + for part in parts: + part = part.strip() + if len(part) == 0: + continue + if part.startswith("=head#"): + components = part[len("=head#"):].split(",") + assert len(components) == 6 + output[HEAD_BONE_X] = float(components[0]) * math.pi / 180 + output[HEAD_BONE_Y] = float(components[1]) * math.pi / 180 + output[HEAD_BONE_Z] = float(components[2]) * math.pi / 180 + elif part.startswith("rightEye#"): + components = part[len("rightEye#"):].split(",") + output[RIGHT_EYE_BONE_X] = float(components[0]) * math.pi / 180 + output[RIGHT_EYE_BONE_Y] = float(components[1]) * math.pi / 180 + output[RIGHT_EYE_BONE_Z] = float(components[2]) * math.pi / 180 + elif part.startswith("leftEye#"): + components = part[len("leftEye#"):].split(",") + output[LEFT_EYE_BONE_X] = float(components[0]) * math.pi / 180 + output[LEFT_EYE_BONE_Y] = float(components[1]) * math.pi / 180 + output[LEFT_EYE_BONE_Z] = float(components[2]) * math.pi / 180 + else: + components = part.split("-") + assert len(components) == 2 + key = components[0] + value = float(components[1]) / 100.0 + if key.endswith("_L"): + key = key[:-2] + "Left" + elif key.endswith("_R"): + key = key[:-2] + "Right" + if key in BLENDSHAPE_NAMES: + output[key] = value + output[HEAD_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + output[LEFT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + output[RIGHT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] + return output + diff --git a/tha4/src/tha4/mocap/mediapipe_constants.py b/tha4/src/tha4/mocap/mediapipe_constants.py new file mode 100644 index 0000000..0fc61ee --- /dev/null +++ b/tha4/src/tha4/mocap/mediapipe_constants.py @@ -0,0 +1,216 @@ +EYE_LOOK_IN_LEFT = "eyeLookInLeft" +EYE_LOOK_OUT_LEFT = "eyeLookOutLeft" +EYE_LOOK_DOWN_LEFT = "eyeLookDownLeft" +EYE_LOOK_UP_LEFT = "eyeLookUpLeft" +EYE_BLINK_LEFT = "eyeBlinkLeft" +EYE_SQUINT_LEFT = "eyeSquintLeft" +EYE_WIDE_LEFT = "eyeWideLeft" +EYE_LOOK_IN_RIGHT = "eyeLookInRight" +EYE_LOOK_OUT_RIGHT = "eyeLookOutRight" +EYE_LOOK_DOWN_RIGHT = "eyeLookDownRight" +EYE_LOOK_UP_RIGHT = "eyeLookUpRight" +EYE_BLINK_RIGHT = "eyeBlinkRight" +EYE_SQUINT_RIGHT = "eyeSquintRight" +EYE_WIDE_RIGHT = "eyeWideRight" +BROW_DOWN_LEFT = "browDownLeft" +BROW_OUTER_UP_LEFT = "browOuterUpLeft" +BROW_DOWN_RIGHT = "browDownRight" +BROW_OUTER_UP_RIGHT = "browOuterUpRight" +BROW_INNER_UP = "browInnerUp" +NOSE_SNEER_LEFT = "noseSneerLeft" +NOSE_SNEER_RIGHT = "noseSneerRight" +CHEEK_SQUINT_LEFT = "cheekSquintLeft" +CHEEK_SQUINT_RIGHT = "cheekSquintRight" +CHEEK_PUFF = "cheekPuff" +MOUTH_LEFT = "mouthLeft" +MOUTH_DIMPLE_LEFT = "mouthDimpleLeft" +MOUTH_FROWN_LEFT = "mouthFrownLeft" +MOUTH_LOWER_DOWN_LEFT = "mouthLowerDownLeft" +MOUTH_PRESS_LEFT = "mouthPressLeft" +MOUTH_SMILE_LEFT = "mouthSmileLeft" +MOUTH_STRETCH_LEFT = "mouthStretchLeft" +MOUTH_UPPER_UP_LEFT = "mouthUpperUpLeft" +MOUTH_RIGHT = "mouthRight" +MOUTH_DIMPLE_RIGHT = "mouthDimpleRight" +MOUTH_FROWN_RIGHT = "mouthFrownRight" +MOUTH_LOWER_DOWN_RIGHT = "mouthLowerDownRight" +MOUTH_PRESS_RIGHT = "mouthPressRight" +MOUTH_SMILE_RIGHT = "mouthSmileRight" +MOUTH_STRETCH_RIGHT = "mouthStretchRight" +MOUTH_UPPER_UP_RIGHT = "mouthUpperUpRight" +MOUTH_CLOSE = "mouthClose" +MOUTH_FUNNEL = "mouthFunnel" +MOUTH_PUCKER = "mouthPucker" +MOUTH_ROLL_LOWER = "mouthRollLower" +MOUTH_ROLL_UPPER = "mouthRollUpper" +MOUTH_SHRUG_LOWER = "mouthShrugLower" +MOUTH_SHRUG_UPPER = "mouthShrugUpper" +JAW_LEFT = "jawLeft" +JAW_RIGHT = "jawRight" +JAW_FORWARD = "jawForward" +JAW_OPEN = "jawOpen" +NEUTRAL = "_neutral" + +BLENDSHAPE_NAMES = [ + EYE_LOOK_IN_LEFT, # 0 + EYE_LOOK_OUT_LEFT, # 1 + EYE_LOOK_DOWN_LEFT, # 2 + EYE_LOOK_UP_LEFT, # 3 + EYE_BLINK_LEFT, # 4 + EYE_SQUINT_LEFT, # 5 + EYE_WIDE_LEFT, # 6 + EYE_LOOK_IN_RIGHT, # 7 + EYE_LOOK_OUT_RIGHT, # 8 + EYE_LOOK_DOWN_RIGHT, # 9 + EYE_LOOK_UP_RIGHT, # 10 + EYE_BLINK_RIGHT, # 11 + EYE_SQUINT_RIGHT, # 12 + EYE_WIDE_RIGHT, # 13 + BROW_DOWN_LEFT, # 14 + BROW_OUTER_UP_LEFT, # 15 + BROW_DOWN_RIGHT, # 16 + BROW_OUTER_UP_RIGHT, # 17 + BROW_INNER_UP, # 18 + NOSE_SNEER_LEFT, # 19 + NOSE_SNEER_RIGHT, # 20 + CHEEK_SQUINT_LEFT, # 21 + CHEEK_SQUINT_RIGHT, # 22 + CHEEK_PUFF, # 23 + MOUTH_LEFT, # 24 + MOUTH_DIMPLE_LEFT, # 25 + MOUTH_FROWN_LEFT, # 26 + MOUTH_LOWER_DOWN_LEFT, # 27 + MOUTH_PRESS_LEFT, # 28 + MOUTH_SMILE_LEFT, # 29 + MOUTH_STRETCH_LEFT, # 30 + MOUTH_UPPER_UP_LEFT, # 31 + MOUTH_RIGHT, # 32 + MOUTH_DIMPLE_RIGHT, # 33 + MOUTH_FROWN_RIGHT, # 34 + MOUTH_LOWER_DOWN_RIGHT, # 35 + MOUTH_PRESS_RIGHT, # 36 + MOUTH_SMILE_RIGHT, # 37 + MOUTH_STRETCH_RIGHT, # 38 + MOUTH_UPPER_UP_RIGHT, # 39 + MOUTH_CLOSE, # 40 + MOUTH_FUNNEL, # 41 + MOUTH_PUCKER, # 42 + MOUTH_ROLL_LOWER, # 43 + MOUTH_ROLL_UPPER, # 44 + MOUTH_SHRUG_LOWER, # 45 + MOUTH_SHRUG_UPPER, # 46 + JAW_LEFT, # 47 + JAW_RIGHT, # 48 + JAW_FORWARD, # 49 + JAW_OPEN, # 50 + NEUTRAL, # 51 +] + +EYE_LEFT_BLENDSHAPES = [ + EYE_LOOK_IN_LEFT, # 0 + EYE_LOOK_OUT_LEFT, # 1 + EYE_LOOK_DOWN_LEFT, # 2 + EYE_LOOK_UP_LEFT, # 3 + EYE_BLINK_LEFT, # 4 + EYE_SQUINT_LEFT, # 5 + EYE_WIDE_LEFT, # 6 +] + +EYE_RIGHT_BLENDSHAPES = [ + EYE_LOOK_IN_RIGHT, # 7 + EYE_LOOK_OUT_RIGHT, # 8 + EYE_LOOK_DOWN_RIGHT, # 9 + EYE_LOOK_UP_RIGHT, # 10 + EYE_BLINK_RIGHT, # 11 + EYE_SQUINT_RIGHT, # 12 + EYE_WIDE_RIGHT, # 13 +] + +BROW_LEFT_BLENDSHAPES = [ + BROW_DOWN_LEFT, # 14 + BROW_OUTER_UP_LEFT, # 15 + +] + +BROW_RIGHT_BLENDSHAPES = [ + BROW_DOWN_RIGHT, # 16 + BROW_OUTER_UP_RIGHT, # 17 + +] + +BROW_BOTH_BLENDSHAPES = [ + BROW_INNER_UP, # 18 +] + +NOSE_BLENDSHAPES = [ + NOSE_SNEER_LEFT, # 19 + NOSE_SNEER_RIGHT, # 20 +] + +CHECK_BLENDSHAPES = [ + CHEEK_SQUINT_LEFT, # 21 + CHEEK_SQUINT_RIGHT, # 22 + CHEEK_PUFF, # 23 +] + +MOUTH_LEFT_BLENDSHAPES = [ + MOUTH_LEFT, # 24 + MOUTH_DIMPLE_LEFT, # 25 + MOUTH_FROWN_LEFT, # 26 + MOUTH_LOWER_DOWN_LEFT, # 27 + MOUTH_PRESS_LEFT, # 28 + MOUTH_SMILE_LEFT, # 29 + MOUTH_STRETCH_LEFT, # 30 + MOUTH_UPPER_UP_LEFT, # 31 +] + +MOUTH_RIGHT_BLENDSHAPES = [ + MOUTH_RIGHT, # 32 + MOUTH_DIMPLE_RIGHT, # 33 + MOUTH_FROWN_RIGHT, # 34 + MOUTH_LOWER_DOWN_RIGHT, # 35 + MOUTH_PRESS_RIGHT, # 36 + MOUTH_SMILE_RIGHT, # 37 + MOUTH_STRETCH_RIGHT, # 38 + MOUTH_UPPER_UP_RIGHT, # 39 +] + +MOUTH_BOTH_BLENDSHAPES = [ + MOUTH_CLOSE, # 40 + MOUTH_FUNNEL, # 41 + MOUTH_PUCKER, # 42 + MOUTH_ROLL_LOWER, # 43 + MOUTH_ROLL_UPPER, # 44 + MOUTH_SHRUG_LOWER, # 45 + MOUTH_SHRUG_UPPER, # 46 +] + +JAW_BLENDSHAPES = [ + JAW_LEFT, # 47 + JAW_RIGHT, # 48 + JAW_FORWARD, # 49 + JAW_OPEN, # 50 +] + +NEUTRAL_BLENDSHAPES = [ + NEUTRAL, # 51 +] + +COLUMN_0_BLENDSHAPES = EYE_RIGHT_BLENDSHAPES + BROW_RIGHT_BLENDSHAPES + [NOSE_SNEER_RIGHT, CHEEK_SQUINT_RIGHT] +COLUMN_1_BLENDSHAPES = EYE_LEFT_BLENDSHAPES + BROW_LEFT_BLENDSHAPES + [NOSE_SNEER_LEFT, CHEEK_SQUINT_LEFT] +COLUMN_2_BLENDSHAPES = MOUTH_RIGHT_BLENDSHAPES + [JAW_RIGHT] +COLUMN_3_BLENDSHAPES = MOUTH_LEFT_BLENDSHAPES + [JAW_LEFT] +COLUMN_4_BLENDSHAPES = [BROW_INNER_UP, CHEEK_PUFF] + MOUTH_BOTH_BLENDSHAPES + [JAW_FORWARD, JAW_OPEN, NEUTRAL] + +BLENDSHAPE_COLUMNS = [ + COLUMN_0_BLENDSHAPES, + COLUMN_1_BLENDSHAPES, + COLUMN_2_BLENDSHAPES, + COLUMN_3_BLENDSHAPES, + COLUMN_4_BLENDSHAPES, +] + +HEAD_X = "headX" +HEAD_Y = "headY" +HEAD_Z = "headZ" +HEAD_ROTATIONS = [HEAD_X, HEAD_Y, HEAD_Z] \ No newline at end of file diff --git a/tha4/src/tha4/mocap/mediapipe_face_pose.py b/tha4/src/tha4/mocap/mediapipe_face_pose.py new file mode 100644 index 0000000..305b4d6 --- /dev/null +++ b/tha4/src/tha4/mocap/mediapipe_face_pose.py @@ -0,0 +1,41 @@ +import json +import os +from typing import Optional, Dict + +import numpy + + +class MediaPipeFacePose: + KEY_BLENDSHAPE_PARAMS = "blendshape_params" + KEY_XFORM_MATRIX = "xform_matrix" + + def __init__(self, blendshape_params: Optional[Dict[str, float]], xform_matrix: Optional[numpy.ndarray]): + if blendshape_params is None: + blendshape_params = {} + if xform_matrix is None: + self.xform_matrix = numpy.zeros(4, 4) + for i in range(4): + self.xform_matrix[i, i] = 1.0 + + self.blendshape_params = blendshape_params + self.xform_matrix = xform_matrix + + def get_json(self): + return { + MediaPipeFacePose.KEY_BLENDSHAPE_PARAMS: self.blendshape_params.copy(), + MediaPipeFacePose.KEY_XFORM_MATRIX: self.xform_matrix.tolist() + } + + def save(self, file_name: str): + os.makedirs(os.path.dirname(file_name), exist_ok=True) + with open(file_name, "wt") as fout: + fout.write(json.dumps(self.get_json())) + + @staticmethod + def load(file_name: str): + with open(file_name, "rt") as fin: + s = fin.read() + json_data = json.loads(s) + return MediaPipeFacePose( + json_data[MediaPipeFacePose.KEY_BLENDSHAPE_PARAMS], + xform_matrix = numpy.array(json_data[MediaPipeFacePose.KEY_XFORM_MATRIX])) diff --git a/tha4/src/tha4/mocap/mediapipe_face_pose_converter.py b/tha4/src/tha4/mocap/mediapipe_face_pose_converter.py new file mode 100644 index 0000000..0cdc389 --- /dev/null +++ b/tha4/src/tha4/mocap/mediapipe_face_pose_converter.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod +from typing import List, Callable, Optional + +from tha4.mocap.mediapipe_face_pose import MediaPipeFacePose + + +class MediaPipeFacePoseConverter(ABC): + @abstractmethod + def convert(self, mediapipe_face_pose: MediaPipeFacePose) -> List[float]: + pass + + @abstractmethod + def init_pose_converter_panel( + self, + parent, + current_pose_supplier: Callable[[], Optional[MediaPipeFacePose]]): + pass \ No newline at end of file diff --git a/tha4/src/tha4/mocap/mediapipe_face_pose_converter_00.py b/tha4/src/tha4/mocap/mediapipe_face_pose_converter_00.py new file mode 100644 index 0000000..c2b6708 --- /dev/null +++ b/tha4/src/tha4/mocap/mediapipe_face_pose_converter_00.py @@ -0,0 +1,662 @@ +import math +import time +from enum import Enum +from typing import Optional, List, Callable + +import numpy +import scipy.optimize +import wx +from scipy.spatial.transform import Rotation + +from tha4.poser.modes.pose_parameters import get_pose_parameters +from tha4.mocap.mediapipe_constants import MOUTH_SMILE_LEFT, MOUTH_SHRUG_UPPER, MOUTH_SMILE_RIGHT, \ + BROW_INNER_UP, BROW_OUTER_UP_RIGHT, BROW_OUTER_UP_LEFT, BROW_DOWN_LEFT, BROW_DOWN_RIGHT, EYE_WIDE_LEFT, \ + EYE_WIDE_RIGHT, EYE_BLINK_LEFT, EYE_BLINK_RIGHT, CHEEK_SQUINT_LEFT, CHEEK_SQUINT_RIGHT, EYE_LOOK_IN_LEFT, \ + EYE_LOOK_OUT_LEFT, EYE_LOOK_IN_RIGHT, EYE_LOOK_OUT_RIGHT, EYE_LOOK_UP_LEFT, EYE_LOOK_UP_RIGHT, EYE_LOOK_DOWN_RIGHT, \ + EYE_LOOK_DOWN_LEFT, JAW_OPEN, MOUTH_FROWN_LEFT, MOUTH_FROWN_RIGHT, \ + MOUTH_LOWER_DOWN_LEFT, MOUTH_LOWER_DOWN_RIGHT, MOUTH_FUNNEL, MOUTH_PUCKER +from tha4.mocap.mediapipe_face_pose import MediaPipeFacePose +from tha4.mocap.mediapipe_face_pose_converter import MediaPipeFacePoseConverter + + +class EyebrowDownMode(Enum): + TROUBLED = 1 + ANGRY = 2 + LOWERED = 3 + SERIOUS = 4 + + +class WinkMode(Enum): + NORMAL = 1 + RELAXED = 2 + + +def rad_to_deg(rad): + return rad * 180.0 / math.pi + + +def deg_to_rad(deg): + return deg * math.pi / 180.0 + + +def clamp(x, min_value, max_value): + return max(min_value, min(max_value, x)) + + +class MediaPipeFacePoseConverter00Args: + def __init__(self, + smile_threshold_min: float = 0.4, + smile_threshold_max: float = 0.6, + eyebrow_down_mode: EyebrowDownMode = EyebrowDownMode.ANGRY, + wink_mode: WinkMode = WinkMode.NORMAL, + eye_surprised_max: float = 0.5, + eye_blink_max: float = 0.8, + eyebrow_down_max: float = 0.4, + cheek_squint_min: float = 0.1, + cheek_squint_max: float = 0.7, + eye_rotation_factor: float = 1.0 / 0.75, + jaw_open_min: float = 0.1, + jaw_open_max: float = 0.4, + mouth_frown_max: float = 0.6, + mouth_funnel_min: float = 0.25, + mouth_funnel_max: float = 0.5, + iris_small_left=0.0, + iris_small_right=0.0, + head_x_offset=0.0, + head_y_offset=0.0, + head_z_offset=0.0): + self.iris_small_right = iris_small_left + self.iris_small_left = iris_small_right + + self.wink_mode = wink_mode + + self.mouth_funnel_max = mouth_funnel_max + self.mouth_funnel_min = mouth_funnel_min + self.mouth_frown_max = mouth_frown_max + + self.jaw_open_max = jaw_open_max + self.jaw_open_min = jaw_open_min + + self.eye_rotation_factor = eye_rotation_factor + + self.cheek_squint_max = cheek_squint_max + self.cheek_squint_min = cheek_squint_min + + self.eyebrow_down_max = eyebrow_down_max + + self.eye_blink_max = eye_blink_max + self.eye_surprised_max = eye_surprised_max + + self.smile_threshold_min = smile_threshold_min + self.smile_threshold_max = smile_threshold_max + + self.head_z_offset = head_z_offset + self.head_y_offset = head_y_offset + self.head_x_offset = head_x_offset + + self.eyebrow_down_mode = eyebrow_down_mode + + def set_smile_threshold_min(self, new_value: float): + self.smile_threshold_min = new_value + + def set_smile_threshold_max(self, new_value: float): + self.smile_threshold_max = new_value + + def set_eye_surprised_max(self, new_value: float): + self.eye_surprised_max = new_value + + def set_eye_blink_max(self, new_value: float): + self.eye_blink_max = new_value + + def set_eyebrow_down_max(self, new_value: float): + self.eyebrow_down_max = new_value + + def set_cheek_squint_min(self, new_value: float): + self.cheek_squint_min = new_value + + def set_cheek_squint_max(self, new_value: float): + self.cheek_squint_max = new_value + + def set_jaw_open_min(self, new_value: float): + self.jaw_open_min = new_value + + def set_jaw_open_max(self, new_value: float): + self.jaw_open_max = new_value + + def set_mouth_frown_max(self, new_value: float): + self.mouth_frown_max = new_value + + def set_mouth_funnel_min(self, new_value: float): + self.mouth_funnel_min = new_value + + def set_mouth_funnel_max(self, new_value: float): + self.mouth_funnel_min = new_value + + +class MediaPoseFacePoseConverter00(MediaPipeFacePoseConverter): + def __init__(self, args: Optional[MediaPipeFacePoseConverter00Args] = None): + super().__init__() + if args is None: + args = MediaPipeFacePoseConverter00Args() + self.args = args + pose_parameters = get_pose_parameters() + self.pose_size = 45 + + self.eyebrow_troubled_left_index = pose_parameters.get_parameter_index("eyebrow_troubled_left") + self.eyebrow_troubled_right_index = pose_parameters.get_parameter_index("eyebrow_troubled_right") + self.eyebrow_angry_left_index = pose_parameters.get_parameter_index("eyebrow_angry_left") + self.eyebrow_angry_right_index = pose_parameters.get_parameter_index("eyebrow_angry_right") + self.eyebrow_happy_left_index = pose_parameters.get_parameter_index("eyebrow_happy_left") + self.eyebrow_happy_right_index = pose_parameters.get_parameter_index("eyebrow_happy_right") + self.eyebrow_raised_left_index = pose_parameters.get_parameter_index("eyebrow_raised_left") + self.eyebrow_raised_right_index = pose_parameters.get_parameter_index("eyebrow_raised_right") + self.eyebrow_lowered_left_index = pose_parameters.get_parameter_index("eyebrow_lowered_left") + self.eyebrow_lowered_right_index = pose_parameters.get_parameter_index("eyebrow_lowered_right") + self.eyebrow_serious_left_index = pose_parameters.get_parameter_index("eyebrow_serious_left") + self.eyebrow_serious_right_index = pose_parameters.get_parameter_index("eyebrow_serious_right") + + self.eye_surprised_left_index = pose_parameters.get_parameter_index("eye_surprised_left") + self.eye_surprised_right_index = pose_parameters.get_parameter_index("eye_surprised_right") + self.eye_wink_left_index = pose_parameters.get_parameter_index("eye_wink_left") + self.eye_wink_right_index = pose_parameters.get_parameter_index("eye_wink_right") + self.eye_happy_wink_left_index = pose_parameters.get_parameter_index("eye_happy_wink_left") + self.eye_happy_wink_right_index = pose_parameters.get_parameter_index("eye_happy_wink_right") + self.eye_relaxed_left_index = pose_parameters.get_parameter_index("eye_relaxed_left") + self.eye_relaxed_right_index = pose_parameters.get_parameter_index("eye_relaxed_right") + self.eye_raised_lower_eyelid_left_index = pose_parameters.get_parameter_index("eye_raised_lower_eyelid_left") + self.eye_raised_lower_eyelid_right_index = pose_parameters.get_parameter_index("eye_raised_lower_eyelid_right") + + self.iris_small_left_index = pose_parameters.get_parameter_index("iris_small_left") + self.iris_small_right_index = pose_parameters.get_parameter_index("iris_small_right") + + self.iris_rotation_x_index = pose_parameters.get_parameter_index("iris_rotation_x") + self.iris_rotation_y_index = pose_parameters.get_parameter_index("iris_rotation_y") + + self.head_x_index = pose_parameters.get_parameter_index("head_x") + self.head_y_index = pose_parameters.get_parameter_index("head_y") + self.neck_z_index = pose_parameters.get_parameter_index("neck_z") + + self.mouth_aaa_index = pose_parameters.get_parameter_index("mouth_aaa") + self.mouth_iii_index = pose_parameters.get_parameter_index("mouth_iii") + self.mouth_uuu_index = pose_parameters.get_parameter_index("mouth_uuu") + self.mouth_eee_index = pose_parameters.get_parameter_index("mouth_eee") + self.mouth_ooo_index = pose_parameters.get_parameter_index("mouth_ooo") + + self.mouth_lowered_corner_left_index = pose_parameters.get_parameter_index("mouth_lowered_corner_left") + self.mouth_lowered_corner_right_index = pose_parameters.get_parameter_index("mouth_lowered_corner_right") + self.mouth_raised_corner_left_index = pose_parameters.get_parameter_index("mouth_raised_corner_left") + self.mouth_raised_corner_right_index = pose_parameters.get_parameter_index("mouth_raised_corner_right") + + self.body_y_index = pose_parameters.get_parameter_index("body_y") + self.body_z_index = pose_parameters.get_parameter_index("body_z") + self.breathing_index = pose_parameters.get_parameter_index("breathing") + + self.breathing_start_time = time.time() + + self.panel = None + self.current_pose_supplier = None + + def init_pose_converter_panel( + self, + parent, + current_pose_supplier: Callable[[], Optional[MediaPipeFacePose]]): + self.panel = wx.Panel(parent, style=wx.SIMPLE_BORDER) + self.panel_sizer = wx.BoxSizer(wx.VERTICAL) + self.panel.SetSizer(self.panel_sizer) + self.panel.SetAutoLayout(1) + parent.GetSizer().Add(self.panel, 0, wx.EXPAND) + + self.current_pose_supplier = current_pose_supplier + + if True: + eyebrow_down_mode_text = wx.StaticText(self.panel, label=" --- Eyebrow Down Mode --- ", + style=wx.ALIGN_CENTER) + self.panel_sizer.Add(eyebrow_down_mode_text, 0, wx.EXPAND) + + self.eyebrow_down_mode_choice = wx.Choice( + self.panel, + choices=[ + "ANGRY", + "TROUBLED", + "SERIOUS", + "LOWERED", + ]) + self.eyebrow_down_mode_choice.SetSelection(0) + self.panel_sizer.Add(self.eyebrow_down_mode_choice, 0, wx.EXPAND) + self.eyebrow_down_mode_choice.Bind(wx.EVT_CHOICE, self.change_eyebrow_down_mode) + + if True: + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + wink_mode_text = wx.StaticText(self.panel, label=" --- Wink Mode --- ", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(wink_mode_text, 0, wx.EXPAND) + + self.wink_mode_choice = wx.Choice( + self.panel, + choices=[ + "NORMAL", + "RELAXED", + ]) + self.wink_mode_choice.SetSelection(0) + self.panel_sizer.Add(self.wink_mode_choice, 0, wx.EXPAND) + self.wink_mode_choice.Bind(wx.EVT_CHOICE, self.change_wink_mode) + + if True: + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + iris_size_text = wx.StaticText(self.panel, label=" --- Iris Size --- ", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(iris_size_text, 0, wx.EXPAND) + + self.iris_left_slider = wx.Slider(self.panel, minValue=0, maxValue=1000, value=0, style=wx.HORIZONTAL) + self.panel_sizer.Add(self.iris_left_slider, 0, wx.EXPAND) + self.iris_left_slider.Bind(wx.EVT_SLIDER, self.change_iris_size) + + self.iris_right_slider = wx.Slider(self.panel, minValue=0, maxValue=1000, value=0, style=wx.HORIZONTAL) + self.panel_sizer.Add(self.iris_right_slider, 0, wx.EXPAND) + self.iris_right_slider.Bind(wx.EVT_SLIDER, self.change_iris_size) + self.iris_right_slider.Enable(False) + + self.link_left_right_irises = wx.CheckBox( + self.panel, label="Use same value for both sides") + self.link_left_right_irises.SetValue(True) + self.panel_sizer.Add(self.link_left_right_irises, wx.SizerFlags().CenterHorizontal().Border()) + self.link_left_right_irises.Bind(wx.EVT_CHECKBOX, self.link_left_right_irises_clicked) + + if True: + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + breathing_frequency_text = wx.StaticText( + self.panel, label=" --- Breathing --- ", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(breathing_frequency_text, 0, wx.EXPAND) + + self.restart_breathing_cycle_button = wx.Button(self.panel, label="Restart Breathing Cycle") + self.restart_breathing_cycle_button.Bind(wx.EVT_BUTTON, self.restart_breathing_cycle_clicked) + self.panel_sizer.Add(self.restart_breathing_cycle_button, 0, wx.EXPAND) + + self.breathing_frequency_slider = wx.Slider( + self.panel, minValue=0, maxValue=60, value=20, style=wx.HORIZONTAL) + self.panel_sizer.Add(self.breathing_frequency_slider, 0, wx.EXPAND) + + self.breathing_gauge = wx.Gauge(self.panel, style=wx.GA_HORIZONTAL, range=1000) + self.panel_sizer.Add(self.breathing_gauge, 0, wx.EXPAND) + + if True: + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + face_orientation_text = wx.StaticText( + self.panel, label="--- Face Orientation ---", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(face_orientation_text, 0, wx.EXPAND) + + self.calibrate_face_orientation_button = wx.Button(self.panel, label="Calibrate (I'm looking forward)") + self.calibrate_face_orientation_button.Bind(wx.EVT_BUTTON, self.calibrate_face_orientation_clicked) + self.panel_sizer.Add(self.calibrate_face_orientation_button, 0, wx.EXPAND) + + if True: + separator = wx.StaticLine(self.panel, -1, size=(256, 5)) + self.panel_sizer.Add(separator, 0, wx.EXPAND) + + convertion_parameters_text = wx.StaticText( + self.panel, label="--- Conversion Parameters ---", style=wx.ALIGN_CENTER) + self.panel_sizer.Add(convertion_parameters_text, 0, wx.EXPAND) + + conversion_param_panel = wx.Panel(self.panel) + self.panel_sizer.Add(conversion_param_panel, 0, wx.EXPAND) + conversion_panel_sizer = wx.FlexGridSizer(cols=2) + conversion_panel_sizer.AddGrowableCol(1) + conversion_param_panel.SetSizer(conversion_panel_sizer) + conversion_param_panel.SetAutoLayout(1) + + self.smile_thresold_min_spin = self.create_spin_control( + conversion_param_panel, + "Smile Threshold Min:", self.args.smile_threshold_min, self.args.set_smile_threshold_min) + self.smile_thresold_max_spin = self.create_spin_control( + conversion_param_panel, + "Smile Threshold Max:", self.args.smile_threshold_max, self.args.set_smile_threshold_max) + self.eye_surprised_max_spin = self.create_spin_control( + conversion_param_panel, + "Eye Surprised Max:", self.args.eye_surprised_max, self.args.set_eye_surprised_max) + self.eye_blink_max_spin = self.create_spin_control( + conversion_param_panel, + "Eye Blink Max:", self.args.eye_blink_max, self.args.set_eye_blink_max) + self.eyebrow_down_max_spin = self.create_spin_control( + conversion_param_panel, + "Eyebrow Down Max:", self.args.eyebrow_down_max, self.args.set_eyebrow_down_max) + self.cheek_squint_min_spin = self.create_spin_control( + conversion_param_panel, + "Cheek Squint Min:", self.args.cheek_squint_min, self.args.set_cheek_squint_min) + self.cheek_squint_max_spin = self.create_spin_control( + conversion_param_panel, + "Cheek Squint Max:", self.args.cheek_squint_max, self.args.set_cheek_squint_max) + self.jaw_open_min_spin = self.create_spin_control( + conversion_param_panel, + "Jaw Open Min:", self.args.jaw_open_min, self.args.set_jaw_open_min) + self.jaw_open_max_spin = self.create_spin_control( + conversion_param_panel, + "Jaw Open Max:", self.args.jaw_open_max, self.args.set_jaw_open_max) + self.mouth_frown_max_spin = self.create_spin_control( + conversion_param_panel, + "Mouth Frown Max:", self.args.mouth_frown_max, self.args.set_mouth_frown_max) + self.mouth_funnel_min_spin = self.create_spin_control( + conversion_param_panel, + "Mouth Funnel Min:", self.args.mouth_funnel_min, self.args.set_mouth_funnel_min) + self.mouth_funnel_max_spin = self.create_spin_control( + conversion_param_panel, + "Mouth Funnel Max:", self.args.mouth_funnel_max, self.args.set_mouth_funnel_max) + + self.panel_sizer.Fit(self.panel) + + def create_spin_control(self, parent, label: str, initial_value: float, set_func: Callable[[float], None]): + sizer = parent.GetSizer() + + text = wx.StaticText(parent, label=label) + sizer.Add(text, wx.SizerFlags().Right().Border(wx.ALL, 2)) + + spin_ctrl = wx.SpinCtrlDouble( + parent, + wx.ID_ANY, + min=0.0, + max=1.0, + initial=initial_value, + inc=0.01) + sizer.Add(spin_ctrl, wx.SizerFlags().Border(wx.ALL, 2).Expand()) + + def handler(event: wx.Event): + new_value = spin_ctrl.GetValue() + set_func(new_value) + + spin_ctrl.Bind(wx.EVT_SPINCTRLDOUBLE, handler) + + return spin_ctrl + + def extract_euler_angles(self, mediapipe_face_pose: MediaPipeFacePose): + M = mediapipe_face_pose.xform_matrix[0:3, 0:3] + rot = Rotation.from_matrix(M) + return rot.as_euler('xyz', degrees=False) + + def calibrate_face_orientation_clicked(self, event: wx.Event): + if self.current_pose_supplier is None: + return + + mediapipe_face_pose = self.current_pose_supplier() + if mediapipe_face_pose is None: + return + + euler_angles = self.extract_euler_angles(mediapipe_face_pose) + self.args.head_x_offset = euler_angles[0] + self.args.head_y_offset = euler_angles[1] + self.args.head_z_offset = euler_angles[2] + + def restart_breathing_cycle_clicked(self, event: wx.Event): + self.breathing_start_time = time.time() + + def change_eyebrow_down_mode(self, event: wx.Event): + selected_index = self.eyebrow_down_mode_choice.GetSelection() + if selected_index == 0: + self.args.eyebrow_down_mode = EyebrowDownMode.ANGRY + elif selected_index == 1: + self.args.eyebrow_down_mode = EyebrowDownMode.TROUBLED + elif selected_index == 2: + self.args.eyebrow_down_mode = EyebrowDownMode.SERIOUS + else: + self.args.eyebrow_down_mode = EyebrowDownMode.LOWERED + + def change_wink_mode(self, event: wx.Event): + selected_index = self.wink_mode_choice.GetSelection() + if selected_index == 0: + self.args.wink_mode = WinkMode.NORMAL + else: + self.args.wink_mode = WinkMode.RELAXED + + def change_iris_size(self, event: wx.Event): + if self.link_left_right_irises.GetValue(): + left_value = self.iris_left_slider.GetValue() + right_value = self.iris_right_slider.GetValue() + if left_value != right_value: + self.iris_right_slider.SetValue(left_value) + self.args.iris_small_left = left_value / 1000.0 + self.args.iris_small_right = left_value / 1000.0 + else: + self.args.iris_small_left = self.iris_left_slider.GetValue() / 1000.0 + self.args.iris_small_right = self.iris_right_slider.GetValue() / 1000.0 + + def link_left_right_irises_clicked(self, event: wx.Event): + if self.link_left_right_irises.GetValue(): + self.iris_right_slider.Enable(False) + else: + self.iris_right_slider.Enable(True) + self.change_iris_size(event) + + def decompose_head_body_param(self, param, threshold=2.0 / 3): + if abs(param) < threshold: + return (param, 0.0) + else: + if param < 0: + sign = -1.0 + else: + sign = 1.0 + return (threshold * sign, (abs(param) - threshold) * sign) + + def convert(self, mediapipe_face_pose: MediaPipeFacePose) -> List[float]: + pose = [0.0 for i in range(self.pose_size)] + + blendshape_params = mediapipe_face_pose.blendshape_params + + smile_value = \ + (blendshape_params[MOUTH_SMILE_LEFT] + blendshape_params[MOUTH_SMILE_RIGHT]) / 2.0 \ + + blendshape_params[MOUTH_SHRUG_UPPER] + if self.args.smile_threshold_min >= self.args.smile_threshold_max: + smile_degree = 0.0 + else: + if smile_value < self.args.smile_threshold_min: + smile_degree = 0.0 + elif smile_value > self.args.smile_threshold_max: + smile_degree = 1.0 + else: + smile_degree = (smile_value - self.args.smile_threshold_min) / ( + self.args.smile_threshold_max - self.args.smile_threshold_min) + + # Eyebrow + if True: + brow_inner_up = blendshape_params[BROW_INNER_UP] + brow_outer_up_right = blendshape_params[BROW_OUTER_UP_RIGHT] + brow_outer_up_left = blendshape_params[BROW_OUTER_UP_LEFT] + + brow_up_left = clamp(brow_inner_up + brow_outer_up_left, 0.0, 1.0) + brow_up_right = clamp(brow_inner_up + brow_outer_up_right, 0.0, 1.0) + pose[self.eyebrow_raised_left_index] = brow_up_left + pose[self.eyebrow_raised_right_index] = brow_up_right + + if self.args.eyebrow_down_max <= 0.0: + brow_down_left = 0.0 + brow_down_right = 0.0 + else: + brow_down_left = (1.0 - smile_degree) \ + * clamp(blendshape_params[BROW_DOWN_LEFT] / self.args.eyebrow_down_max, 0.0, 1.0) + brow_down_right = (1.0 - smile_degree) \ + * clamp(blendshape_params[BROW_DOWN_RIGHT] / self.args.eyebrow_down_max, 0.0, 1.0) + + if self.args.eyebrow_down_mode == EyebrowDownMode.TROUBLED: + pose[self.eyebrow_troubled_left_index] = brow_down_left + pose[self.eyebrow_troubled_right_index] = brow_down_right + elif self.args.eyebrow_down_mode == EyebrowDownMode.ANGRY: + pose[self.eyebrow_angry_left_index] = brow_down_left + pose[self.eyebrow_angry_right_index] = brow_down_right + elif self.args.eyebrow_down_mode == EyebrowDownMode.LOWERED: + pose[self.eyebrow_lowered_left_index] = brow_down_left + pose[self.eyebrow_lowered_right_index] = brow_down_right + elif self.args.eyebrow_down_mode == EyebrowDownMode.SERIOUS: + pose[self.eyebrow_serious_left_index] = brow_down_left + pose[self.eyebrow_serious_right_index] = brow_down_right + + brow_happy_value = clamp(smile_value, 0.0, 1.0) * smile_degree + pose[self.eyebrow_happy_left_index] = brow_happy_value + pose[self.eyebrow_happy_right_index] = brow_happy_value + + # Eye + if True: + # Surprised + if self.args.eye_surprised_max <= 0.0: + pose[self.eye_surprised_left_index] = 0.0 + pose[self.eye_surprised_right_index] = 0.0 + else: + pose[self.eye_surprised_left_index] = clamp( + blendshape_params[EYE_WIDE_LEFT] / self.args.eye_surprised_max, 0.0, 1.0) + pose[self.eye_surprised_right_index] = clamp( + blendshape_params[EYE_WIDE_RIGHT] / self.args.eye_surprised_max, 0.0, 1.0) + + # Wink + if self.args.wink_mode == WinkMode.NORMAL: + wink_left_index = self.eye_wink_left_index + wink_right_index = self.eye_wink_right_index + else: + wink_left_index = self.eye_relaxed_left_index + wink_right_index = self.eye_relaxed_right_index + if self.args.eye_blink_max <= 0: + pose[wink_left_index] = 0.0 + pose[wink_right_index] = 0.0 + pose[self.eye_happy_wink_left_index] = 0.0 + pose[self.eye_happy_wink_right_index] = 0.0 + else: + pose[wink_left_index] = (1.0 - smile_degree) * clamp( + blendshape_params[EYE_BLINK_LEFT] / self.args.eye_blink_max, 0.0, 1.0) + pose[wink_right_index] = (1.0 - smile_degree) * clamp( + blendshape_params[EYE_BLINK_RIGHT] / self.args.eye_blink_max, 0.0, 1.0) + pose[self.eye_happy_wink_left_index] = smile_degree * clamp( + blendshape_params[EYE_BLINK_LEFT] / self.args.eye_blink_max, 0.0, 1.0) + pose[self.eye_happy_wink_right_index] = smile_degree * clamp( + blendshape_params[EYE_BLINK_RIGHT] / self.args.eye_blink_max, 0.0, 1.0) + + # Lower eyelid + cheek_squint_denom = self.args.cheek_squint_max - self.args.cheek_squint_min + if cheek_squint_denom <= 0.0: + pose[self.eye_raised_lower_eyelid_left_index] = 0.0 + pose[self.eye_raised_lower_eyelid_right_index] = 0.0 + else: + pose[self.eye_raised_lower_eyelid_left_index] = \ + clamp( + (blendshape_params[CHEEK_SQUINT_LEFT] - self.args.cheek_squint_min) / cheek_squint_denom, + 0.0, 1.0) + pose[self.eye_raised_lower_eyelid_right_index] = \ + clamp( + (blendshape_params[CHEEK_SQUINT_RIGHT] - self.args.cheek_squint_min) / cheek_squint_denom, + 0.0, 1.0) + + # Iris rotation + if True: + eye_rotation_y = (blendshape_params[EYE_LOOK_IN_LEFT] + - blendshape_params[EYE_LOOK_OUT_LEFT] + - blendshape_params[EYE_LOOK_IN_RIGHT] + + blendshape_params[EYE_LOOK_OUT_RIGHT]) / 2.0 * self.args.eye_rotation_factor + pose[self.iris_rotation_y_index] = clamp(eye_rotation_y, -1.0, 1.0) + + eye_rotation_x = (blendshape_params[EYE_LOOK_UP_LEFT] + + blendshape_params[EYE_LOOK_UP_RIGHT] + - blendshape_params[EYE_LOOK_DOWN_LEFT] + - blendshape_params[EYE_LOOK_DOWN_RIGHT]) / 2.0 * self.args.eye_rotation_factor + pose[self.iris_rotation_x_index] = clamp(eye_rotation_x, -1.0, 1.0) + + # Iris size + if True: + pose[self.iris_small_left_index] = self.args.iris_small_left + pose[self.iris_small_right_index] = self.args.iris_small_right + + # Head rotation + if True: + euler_angles = self.extract_euler_angles(mediapipe_face_pose) + euler_angles[0] -= self.args.head_x_offset + euler_angles[1] -= self.args.head_y_offset + euler_angles[2] -= self.args.head_z_offset + + x_param = clamp(-euler_angles[0] * 180.0 / math.pi, -15.0, 15.0) / 15.0 + pose[self.head_x_index] = x_param + + y_param = clamp(-euler_angles[1] * 180.0 / math.pi, -10.0, 10.0) / 10.0 + pose[self.head_y_index] = y_param + pose[self.body_y_index] = y_param + + z_param = clamp(euler_angles[2] * 180.0 / math.pi, -15.0, 15.0) / 15.0 + pose[self.neck_z_index] = z_param + pose[self.body_z_index] = z_param + + # Mouth + if True: + jaw_open_denom = self.args.jaw_open_max - self.args.jaw_open_min + if jaw_open_denom <= 0: + mouth_open = 0.0 + else: + mouth_open = clamp((blendshape_params[JAW_OPEN] - self.args.jaw_open_min) / jaw_open_denom, 0.0, 1.0) + pose[self.mouth_aaa_index] = mouth_open + pose[self.mouth_raised_corner_left_index] = clamp(smile_value, 0.0, 1.0) + pose[self.mouth_raised_corner_right_index] = clamp(smile_value, 0.0, 1.0) + + is_mouth_open = mouth_open > 0.0 + if not is_mouth_open: + if self.args.mouth_frown_max <= 0: + mouth_frown_value = 0.0 + else: + mouth_frown_value = clamp( + (blendshape_params[MOUTH_FROWN_LEFT] + blendshape_params[ + MOUTH_FROWN_RIGHT]) / self.args.mouth_frown_max, 0.0, 1.0) + pose[self.mouth_lowered_corner_left_index] = mouth_frown_value + pose[self.mouth_lowered_corner_right_index] = mouth_frown_value + else: + mouth_lower_down = clamp( + blendshape_params[MOUTH_LOWER_DOWN_LEFT] + blendshape_params[MOUTH_LOWER_DOWN_RIGHT], 0.0, 1.0) + mouth_funnel = blendshape_params[MOUTH_FUNNEL] + mouth_pucker = blendshape_params[MOUTH_PUCKER] + + mouth_point = [mouth_open, mouth_lower_down, mouth_funnel, mouth_pucker] + + aaa_point = [1.0, 1.0, 0.0, 0.0] + iii_point = [0.0, 1.0, 0.0, 0.0] + uuu_point = [0.5, 0.3, 0.25, 0.75] + ooo_point = [1.0, 0.5, 0.5, 0.4] + + decomp = numpy.array([0, 0, 0, 0]) + M = numpy.array([ + aaa_point, + iii_point, + uuu_point, + ooo_point + ]) + + def loss(decomp): + return numpy.linalg.norm(numpy.matmul(decomp, M) - mouth_point) \ + + 0.01 * numpy.linalg.norm(decomp, ord=1) + + opt_result = scipy.optimize.minimize( + loss, decomp, bounds=[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)]) + decomp = opt_result["x"] + restricted_decomp = [decomp.item(0), decomp.item(1), decomp.item(2), decomp.item(3)] + pose[self.mouth_aaa_index] = restricted_decomp[0] + pose[self.mouth_iii_index] = restricted_decomp[1] + mouth_funnel_denom = self.args.mouth_funnel_max - self.args.mouth_funnel_min + if mouth_funnel_denom <= 0: + ooo_alpha = 0.0 + uo_value = 0.0 + else: + ooo_alpha = clamp((mouth_funnel - self.args.mouth_funnel_min) / mouth_funnel_denom, 0.0, 1.0) + uo_value = clamp(restricted_decomp[2] + restricted_decomp[3], 0.0, 1.0) + pose[self.mouth_uuu_index] = uo_value * (1.0 - ooo_alpha) + pose[self.mouth_ooo_index] = uo_value * ooo_alpha + + if self.panel is not None: + frequency = self.breathing_frequency_slider.GetValue() + if frequency == 0: + value = 0.0 + pose[self.breathing_index] = value + self.breathing_start_time = time.time() + else: + period = 60.0 / frequency + now = time.time() + diff = now - self.breathing_start_time + frac = (diff % period) / period + value = (-math.cos(2 * math.pi * frac) + 1.0) / 2.0 + pose[self.breathing_index] = value + self.breathing_gauge.SetValue(int(1000 * value)) + + return pose diff --git a/tha4/src/tha4/nn/__init__.py b/tha4/src/tha4/nn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/common/__init__.py b/tha4/src/tha4/nn/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/common/conv_block_factory.py b/tha4/src/tha4/nn/common/conv_block_factory.py new file mode 100644 index 0000000..d27d813 --- /dev/null +++ b/tha4/src/tha4/nn/common/conv_block_factory.py @@ -0,0 +1,55 @@ +from typing import Optional + +from tha4.nn.conv import create_conv7_block_from_block_args, create_conv3_block_from_block_args, \ + create_downsample_block_from_block_args, create_conv3 +from tha4.nn.resnet_block import ResnetBlock +from tha4.nn.resnet_block_seperable import ResnetBlockSeparable +from tha4.nn.separable_conv import create_separable_conv7_block, create_separable_conv3_block, \ + create_separable_downsample_block, create_separable_conv3 +from tha4.nn.util import BlockArgs + + +class ConvBlockFactory: + def __init__(self, + block_args: BlockArgs, + use_separable_convolution: bool = False): + self.use_separable_convolution = use_separable_convolution + self.block_args = block_args + + def create_conv3(self, + in_channels: int, + out_channels: int, + bias: bool, + initialization_method: Optional[str] = None): + if initialization_method is None: + initialization_method = self.block_args.initialization_method + if self.use_separable_convolution: + return create_separable_conv3( + in_channels, out_channels, bias, initialization_method, self.block_args.use_spectral_norm) + else: + return create_conv3( + in_channels, out_channels, bias, initialization_method, self.block_args.use_spectral_norm) + + def create_conv7_block(self, in_channels: int, out_channels: int): + if self.use_separable_convolution: + return create_separable_conv7_block(in_channels, out_channels, self.block_args) + else: + return create_conv7_block_from_block_args(in_channels, out_channels, self.block_args) + + def create_conv3_block(self, in_channels: int, out_channels: int): + if self.use_separable_convolution: + return create_separable_conv3_block(in_channels, out_channels, self.block_args) + else: + return create_conv3_block_from_block_args(in_channels, out_channels, self.block_args) + + def create_downsample_block(self, in_channels: int, out_channels: int, is_output_1x1: bool): + if self.use_separable_convolution: + return create_separable_downsample_block(in_channels, out_channels, is_output_1x1, self.block_args) + else: + return create_downsample_block_from_block_args(in_channels, out_channels, is_output_1x1) + + def create_resnet_block(self, num_channels: int, is_1x1: bool): + if self.use_separable_convolution: + return ResnetBlockSeparable.create(num_channels, is_1x1, block_args=self.block_args) + else: + return ResnetBlock.create(num_channels, is_1x1, block_args=self.block_args) \ No newline at end of file diff --git a/tha4/src/tha4/nn/common/poser_args.py b/tha4/src/tha4/nn/common/poser_args.py new file mode 100644 index 0000000..ba1c701 --- /dev/null +++ b/tha4/src/tha4/nn/common/poser_args.py @@ -0,0 +1,68 @@ +from typing import Optional + +from torch.nn import Sigmoid, Sequential, Tanh + +from tha4.nn.conv import create_conv3, create_conv3_from_block_args +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs + + +class PoserArgs00: + def __init__(self, + image_size: int, + input_image_channels: int, + output_image_channels: int, + start_channels: int, + num_pose_params: int, + block_args: Optional[BlockArgs] = None): + self.num_pose_params = num_pose_params + self.start_channels = start_channels + self.output_image_channels = output_image_channels + self.input_image_channels = input_image_channels + self.image_size = image_size + if block_args is None: + self.block_args = BlockArgs( + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=True)) + else: + self.block_args = block_args + + def create_alpha_block(self): + from torch.nn import Sequential + return Sequential( + create_conv3( + in_channels=self.start_channels, + out_channels=1, + bias=True, + initialization_method=self.block_args.initialization_method, + use_spectral_norm=False), + Sigmoid()) + + def create_all_channel_alpha_block(self): + from torch.nn import Sequential + return Sequential( + create_conv3( + in_channels=self.start_channels, + out_channels=self.output_image_channels, + bias=True, + initialization_method=self.block_args.initialization_method, + use_spectral_norm=False), + Sigmoid()) + + def create_color_change_block(self): + return Sequential( + create_conv3_from_block_args( + in_channels=self.start_channels, + out_channels=self.output_image_channels, + bias=True, + block_args=self.block_args), + Tanh()) + + def create_grid_change_block(self): + return create_conv3( + in_channels=self.start_channels, + out_channels=2, + bias=False, + initialization_method='zero', + use_spectral_norm=False) \ No newline at end of file diff --git a/tha4/src/tha4/nn/common/poser_encoder_decoder_00.py b/tha4/src/tha4/nn/common/poser_encoder_decoder_00.py new file mode 100644 index 0000000..06f7e3d --- /dev/null +++ b/tha4/src/tha4/nn/common/poser_encoder_decoder_00.py @@ -0,0 +1,121 @@ +import math +from typing import Optional, List + +import torch +from torch import Tensor +from torch.nn import ModuleList, Module + +from tha4.nn.common.poser_args import PoserArgs00 +from tha4.nn.conv import create_conv3_block_from_block_args, create_downsample_block_from_block_args, \ + create_upsample_block_from_block_args +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.resnet_block import ResnetBlock +from tha4.nn.util import BlockArgs + + +class PoserEncoderDecoder00Args(PoserArgs00): + def __init__(self, + image_size: int, + input_image_channels: int, + output_image_channels: int, + num_pose_params: int , + start_channels: int, + bottleneck_image_size, + num_bottleneck_blocks, + max_channels: int, + block_args: Optional[BlockArgs] = None): + super().__init__( + image_size, input_image_channels, output_image_channels, start_channels, num_pose_params, block_args) + self.max_channels = max_channels + self.num_bottleneck_blocks = num_bottleneck_blocks + self.bottleneck_image_size = bottleneck_image_size + assert bottleneck_image_size > 1 + + if block_args is None: + self.block_args = BlockArgs( + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=True)) + else: + self.block_args = block_args + + +class PoserEncoderDecoder00(Module): + def __init__(self, args: PoserEncoderDecoder00Args): + super().__init__() + self.args = args + + self.num_levels = int(math.log2(args.image_size // args.bottleneck_image_size)) + 1 + + self.downsample_blocks = ModuleList() + self.downsample_blocks.append( + create_conv3_block_from_block_args( + args.input_image_channels, + args.start_channels, + args.block_args)) + current_image_size = args.image_size + current_num_channels = args.start_channels + while current_image_size > args.bottleneck_image_size: + next_image_size = current_image_size // 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.downsample_blocks.append(create_downsample_block_from_block_args( + in_channels=current_num_channels, + out_channels=next_num_channels, + is_output_1x1=False, + block_args=args.block_args)) + current_image_size = next_image_size + current_num_channels = next_num_channels + assert len(self.downsample_blocks) == self.num_levels + + self.bottleneck_blocks = ModuleList() + self.bottleneck_blocks.append(create_conv3_block_from_block_args( + in_channels=current_num_channels + args.num_pose_params, + out_channels=current_num_channels, + block_args=args.block_args)) + for i in range(1, args.num_bottleneck_blocks): + self.bottleneck_blocks.append( + ResnetBlock.create( + num_channels=current_num_channels, + is1x1=False, + block_args=args.block_args)) + + self.upsample_blocks = ModuleList() + while current_image_size < args.image_size: + next_image_size = current_image_size * 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.upsample_blocks.append(create_upsample_block_from_block_args( + in_channels=current_num_channels, + out_channels=next_num_channels, + block_args=args.block_args)) + current_image_size = next_image_size + current_num_channels = next_num_channels + + def get_num_output_channels_from_level(self, level: int): + return self.get_num_output_channels_from_image_size(self.args.image_size // (2 ** level)) + + def get_num_output_channels_from_image_size(self, image_size: int): + return min(self.args.start_channels * (self.args.image_size // image_size), self.args.max_channels) + + def forward(self, image: Tensor, pose: Optional[Tensor] = None) -> List[Tensor]: + if self.args.num_pose_params != 0: + assert pose is not None + else: + assert pose is None + outputs = [] + feature = image + outputs.append(feature) + for block in self.downsample_blocks: + feature = block(feature) + outputs.append(feature) + if pose is not None: + n, c = pose.shape + pose = pose.view(n, c, 1, 1).repeat(1, 1, self.args.bottleneck_image_size, self.args.bottleneck_image_size) + feature = torch.cat([feature, pose], dim=1) + for block in self.bottleneck_blocks: + feature = block(feature) + outputs.append(feature) + for block in self.upsample_blocks: + feature = block(feature) + outputs.append(feature) + outputs.reverse() + return outputs diff --git a/tha4/src/tha4/nn/common/poser_encoder_decoder_00_separable.py b/tha4/src/tha4/nn/common/poser_encoder_decoder_00_separable.py new file mode 100644 index 0000000..52dceb9 --- /dev/null +++ b/tha4/src/tha4/nn/common/poser_encoder_decoder_00_separable.py @@ -0,0 +1,92 @@ +import math +from typing import Optional, List + +import torch +from torch import Tensor +from torch.nn import ModuleList, Module + +from tha4.nn.common.poser_encoder_decoder_00 import PoserEncoderDecoder00Args +from tha4.nn.resnet_block_seperable import ResnetBlockSeparable +from tha4.nn.separable_conv import create_separable_conv3_block, create_separable_downsample_block, \ + create_separable_upsample_block + + +class PoserEncoderDecoder00Separable(Module): + def __init__(self, args: PoserEncoderDecoder00Args): + super().__init__() + self.args = args + + self.num_levels = int(math.log2(args.image_size // args.bottleneck_image_size)) + 1 + + self.downsample_blocks = ModuleList() + self.downsample_blocks.append( + create_separable_conv3_block( + args.input_image_channels, + args.start_channels, + args.block_args)) + current_image_size = args.image_size + current_num_channels = args.start_channels + while current_image_size > args.bottleneck_image_size: + next_image_size = current_image_size // 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.downsample_blocks.append(create_separable_downsample_block( + in_channels=current_num_channels, + out_channels=next_num_channels, + is_output_1x1=False, + block_args=args.block_args)) + current_image_size = next_image_size + current_num_channels = next_num_channels + assert len(self.downsample_blocks) == self.num_levels + + self.bottleneck_blocks = ModuleList() + self.bottleneck_blocks.append(create_separable_conv3_block( + in_channels=current_num_channels + args.num_pose_params, + out_channels=current_num_channels, + block_args=args.block_args)) + for i in range(1, args.num_bottleneck_blocks): + self.bottleneck_blocks.append( + ResnetBlockSeparable.create( + num_channels=current_num_channels, + is1x1=False, + block_args=args.block_args)) + + self.upsample_blocks = ModuleList() + while current_image_size < args.image_size: + next_image_size = current_image_size * 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.upsample_blocks.append(create_separable_upsample_block( + in_channels=current_num_channels, + out_channels=next_num_channels, + block_args=args.block_args)) + current_image_size = next_image_size + current_num_channels = next_num_channels + + def get_num_output_channels_from_level(self, level: int): + return self.get_num_output_channels_from_image_size(self.args.image_size // (2 ** level)) + + def get_num_output_channels_from_image_size(self, image_size: int): + return min(self.args.start_channels * (self.args.image_size // image_size), self.args.max_channels) + + def forward(self, image: Tensor, pose: Optional[Tensor] = None) -> List[Tensor]: + if self.args.num_pose_params != 0: + assert pose is not None + else: + assert pose is None + outputs = [] + feature = image + outputs.append(feature) + for block in self.downsample_blocks: + feature = block(feature) + outputs.append(feature) + if pose is not None: + n, c = pose.shape + pose = pose.view(n, c, 1, 1).repeat(1, 1, self.args.bottleneck_image_size, self.args.bottleneck_image_size) + feature = torch.cat([feature, pose], dim=1) + for block in self.bottleneck_blocks: + feature = block(feature) + outputs.append(feature) + for block in self.upsample_blocks: + feature = block(feature) + outputs.append(feature) + outputs.reverse() + return outputs diff --git a/tha4/src/tha4/nn/common/resize_conv_encoder_decoder.py b/tha4/src/tha4/nn/common/resize_conv_encoder_decoder.py new file mode 100644 index 0000000..aaf0149 --- /dev/null +++ b/tha4/src/tha4/nn/common/resize_conv_encoder_decoder.py @@ -0,0 +1,100 @@ +import math +from typing import Optional, List + +import torch +from torch import Tensor +from torch.nn import Module, ModuleList, Sequential, Upsample + +from tha4.nn.common.conv_block_factory import ConvBlockFactory +from tha4.nn.nonlinearity_factory import LeakyReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs + + +class ResizeConvEncoderDecoderArgs: + def __init__(self, + image_size: int, + input_channels: int, + start_channels: int, + bottleneck_image_size, + num_bottleneck_blocks, + max_channels: int, + block_args: Optional[BlockArgs] = None, + upsample_mode: str = 'bilinear', + use_separable_convolution=False): + self.use_separable_convolution = use_separable_convolution + self.upsample_mode = upsample_mode + self.block_args = block_args + self.max_channels = max_channels + self.num_bottleneck_blocks = num_bottleneck_blocks + self.bottleneck_image_size = bottleneck_image_size + self.start_channels = start_channels + self.image_size = image_size + self.input_channels = input_channels + + +class ResizeConvEncoderDecoder(Module): + def __init__(self, args: ResizeConvEncoderDecoderArgs): + super().__init__() + self.args = args + + self.num_levels = int(math.log2(args.image_size // args.bottleneck_image_size)) + 1 + + conv_block_factory = ConvBlockFactory(args.block_args, args.use_separable_convolution) + + self.downsample_blocks = ModuleList() + self.downsample_blocks.append(conv_block_factory.create_conv7_block(args.input_channels, args.start_channels)) + current_image_size = args.image_size + current_num_channels = args.start_channels + while current_image_size > args.bottleneck_image_size: + next_image_size = current_image_size // 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.downsample_blocks.append(conv_block_factory.create_downsample_block( + in_channels=current_num_channels, + out_channels=next_num_channels, + is_output_1x1=False)) + current_image_size = next_image_size + current_num_channels = next_num_channels + assert len(self.downsample_blocks) == self.num_levels + + self.bottleneck_blocks = ModuleList() + for i in range(args.num_bottleneck_blocks): + self.bottleneck_blocks.append(conv_block_factory.create_resnet_block(current_num_channels, is_1x1=False)) + + self.output_image_sizes = [current_image_size] + self.output_num_channels = [current_num_channels] + self.upsample_blocks = ModuleList() + if args.upsample_mode == 'nearest': + align_corners = None + else: + align_corners = False + while current_image_size < args.image_size: + next_image_size = current_image_size * 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.upsample_blocks.append( + Sequential( + Upsample(scale_factor=2, mode=args.upsample_mode, align_corners=align_corners), + conv_block_factory.create_conv3_block( + in_channels=current_num_channels, out_channels=next_num_channels))) + current_image_size = next_image_size + current_num_channels = next_num_channels + self.output_image_sizes.append(current_image_size) + self.output_num_channels.append(current_num_channels) + + def get_num_output_channels_from_level(self, level: int): + return self.get_num_output_channels_from_image_size(self.args.image_size // (2 ** level)) + + def get_num_output_channels_from_image_size(self, image_size: int): + return min(self.args.start_channels * (self.args.image_size // image_size), self.args.max_channels) + + def forward(self, feature: Tensor) -> List[Tensor]: + outputs = [] + for block in self.downsample_blocks: + feature = block(feature) + for block in self.bottleneck_blocks: + feature = block(feature) + outputs.append(feature) + for block in self.upsample_blocks: + feature = block(feature) + outputs.append(feature) + return outputs \ No newline at end of file diff --git a/tha4/src/tha4/nn/common/resize_conv_unet.py b/tha4/src/tha4/nn/common/resize_conv_unet.py new file mode 100644 index 0000000..ca6ca08 --- /dev/null +++ b/tha4/src/tha4/nn/common/resize_conv_unet.py @@ -0,0 +1,107 @@ +from typing import Optional, List + +import torch +from torch import Tensor +from torch.nn import ModuleList, Module, Upsample + +from tha4.nn.common.conv_block_factory import ConvBlockFactory +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs + + +class ResizeConvUNetArgs: + def __init__(self, + image_size: int, + input_channels: int, + start_channels: int, + bottleneck_image_size: int, + num_bottleneck_blocks: int, + max_channels: int, + upsample_mode: str = 'bilinear', + block_args: Optional[BlockArgs] = None, + use_separable_convolution: bool = False): + if block_args is None: + block_args = BlockArgs( + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=False)) + + self.use_separable_convolution = use_separable_convolution + self.block_args = block_args + self.upsample_mode = upsample_mode + self.max_channels = max_channels + self.num_bottleneck_blocks = num_bottleneck_blocks + self.bottleneck_image_size = bottleneck_image_size + self.input_channels = input_channels + self.start_channels = start_channels + self.image_size = image_size + + +class ResizeConvUNet(Module): + def __init__(self, args: ResizeConvUNetArgs): + super().__init__() + self.args = args + conv_block_factory = ConvBlockFactory(args.block_args, args.use_separable_convolution) + + self.downsample_blocks = ModuleList() + self.downsample_blocks.append(conv_block_factory.create_conv3_block( + self.args.input_channels, + self.args.start_channels)) + current_channels = self.args.start_channels + current_size = self.args.image_size + + size_to_channel = { + current_size: current_channels + } + while current_size > self.args.bottleneck_image_size: + next_size = current_size // 2 + next_channels = min(self.args.max_channels, current_channels * 2) + self.downsample_blocks.append(conv_block_factory.create_downsample_block( + current_channels, + next_channels, + is_output_1x1=False)) + current_size = next_size + current_channels = next_channels + size_to_channel[current_size] = current_channels + + self.bottleneck_blocks = ModuleList() + for i in range(self.args.num_bottleneck_blocks): + self.bottleneck_blocks.append(conv_block_factory.create_resnet_block(current_channels, is_1x1=False)) + + self.output_image_sizes = [current_size] + self.output_num_channels = [current_channels] + self.upsample_blocks = ModuleList() + while current_size < self.args.image_size: + next_size = current_size * 2 + next_channels = size_to_channel[next_size] + self.upsample_blocks.append(conv_block_factory.create_conv3_block( + current_channels + next_channels, + next_channels)) + current_size = next_size + current_channels = next_channels + self.output_image_sizes.append(current_size) + self.output_num_channels.append(current_channels) + + if args.upsample_mode == 'nearest': + align_corners = None + else: + align_corners = False + self.double_resolution = Upsample(scale_factor=2, mode=args.upsample_mode, align_corners=align_corners) + + def forward(self, feature: Tensor) -> List[Tensor]: + downsampled_features = [] + for block in self.downsample_blocks: + feature = block(feature) + downsampled_features.append(feature) + + for block in self.bottleneck_blocks: + feature = block(feature) + + outputs = [feature] + for i in range(0, len(self.upsample_blocks)): + feature = self.double_resolution(feature) + feature = torch.cat([feature, downsampled_features[-i - 2]], dim=1) + feature = self.upsample_blocks[i](feature) + outputs.append(feature) + + return outputs \ No newline at end of file diff --git a/tha4/src/tha4/nn/common/unet.py b/tha4/src/tha4/nn/common/unet.py new file mode 100644 index 0000000..3049c21 --- /dev/null +++ b/tha4/src/tha4/nn/common/unet.py @@ -0,0 +1,658 @@ +import math +from enum import Enum +from typing import Optional, List + +import torch +from torch import zero_, Tensor +from torch.nn import Module, GroupNorm, Sequential, SiLU, Conv2d, AvgPool2d, Linear, Dropout, ModuleList +from torch.nn.functional import interpolate + +from tha4.shion.core.module_factory import ModuleFactory + + +class Identity(Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x + + +class IdentityFactory(ModuleFactory): + def create(self) -> Module: + return Identity() + + +def init_to_zero(module: Module): + with torch.no_grad(): + zero_(module.weight) + zero_(module.bias) + return module + + +class Upsample(Module): + def __init__(self, in_channels: int, out_channels: Optional[int] = None, use_conv: bool = False): + super().__init__() + if out_channels is None: + out_channels = in_channels + self.in_channels = in_channels + if use_conv or in_channels != out_channels: + self.postprocess = Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1) + else: + self.postprocess = Identity() + + def forward(self, x): + assert x.shape[1] == self.in_channels + return self.postprocess(interpolate(x, scale_factor=2, mode="nearest")) + + +class Downsample(Module): + def __init__(self, in_channels: int, out_channels: Optional[int] = None, use_conv: bool = False): + super().__init__() + if out_channels is None: + out_channels = in_channels + self.in_channels = in_channels + if use_conv or in_channels != out_channels: + self.op = Conv2d(in_channels, out_channels, kernel_size=3, stride=2, padding=1) + else: + self.op = AvgPool2d(kernel_size=2, stride=2) + + def forward(self, x): + assert x.shape[1] == self.in_channels + return self.op(x) + + +def GroupNorm32(channels): + return GroupNorm(min(32, channels), channels) + + +class SamplingMode(Enum): + SAME_RESOLUTION = 0 + UPSAMPLING = 1 + DOWNSAMPING = 2 + + +class ResBlockArgs: + def __init__(self, + dropout_prob: float, + use_cond0: bool = True, + use_cond1: bool = False, + init_conditioned_residual_to_zero: bool = False, + use_conv_on_skip_connection: bool = False): + assert not use_cond1 or use_cond0 + self.use_conv_on_skip_connection = use_conv_on_skip_connection + self.use_cond1 = use_cond1 + self.use_cond0 = use_cond0 + self.init_conditioned_residual_to_zero = init_conditioned_residual_to_zero + self.dropout_prob = dropout_prob + + +def apply_scaleshift(x: Tensor, scaleshift: Tensor, condition_bias: float = 1.0) -> Tensor: + assert len(scaleshift.shape) == 2 + assert len(x.shape) == 4 + assert x.shape[0] == scaleshift.shape[0] + assert 2 * x.shape[1] == scaleshift.shape[1] + scaleshift = scaleshift.reshape(scaleshift.shape[0], scaleshift.shape[1], 1, 1) + scale, shift = torch.chunk(scaleshift, 2, dim=1) + return x * (condition_bias + scale) + shift + + +class ResBlock(Module): + def __init__(self, + in_channels: int, + out_channels: int, + cond0_channels: Optional[int] = None, + cond1_channels: Optional[int] = None, + sampling_mode: SamplingMode = SamplingMode.SAME_RESOLUTION, + dropout_prob: float = 0.1, + condition_bias: float = 1.0): + super().__init__() + assert cond0_channels is not None or cond1_channels is None + + self.in_channels = in_channels + self.out_channels = out_channels + self.sampling_mode = sampling_mode + self.cond0_channels = cond0_channels + self.cond1_channels = cond1_channels + self.condition_bias = condition_bias + + if sampling_mode == SamplingMode.UPSAMPLING: + self.x_resample = Upsample(in_channels) + self.h_resample = Upsample(in_channels) + elif sampling_mode == SamplingMode.DOWNSAMPING: + self.x_resample = Downsample(in_channels) + self.h_resample = Downsample(in_channels) + else: + self.x_resample = Identity() + self.h_resample = Identity() + + self.nonlinear = SiLU() + + # Layers before conditioning + self.norm0 = GroupNorm32(in_channels) + self.conv0 = Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1) + + # Conditioning layers + if cond0_channels is not None: + self.cond0_layers = Sequential( + SiLU(), + Linear(cond0_channels, 2 * out_channels)) + self.norm1 = GroupNorm32(out_channels) + self.dropout = Dropout(dropout_prob) + self.conv1 = init_to_zero(Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)) + if cond1_channels is not None: + self.cond1_layers = Sequential( + SiLU(), + Linear(cond0_channels, 2 * out_channels)) + + # Skip layer + if in_channels == out_channels: + self.skip = Identity() + else: + self.skip = Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0) + + def forward(self, x: Tensor, cond0: Optional[Tensor] = None, cond1: Optional[Tensor] = None) -> Tensor: + assert self.cond0_channels is None or cond0 is not None + assert self.cond1_channels is None or cond1 is not None + + h = self.conv0(self.h_resample(self.nonlinear(self.norm0(x)))) + if self.cond0_channels is not None: + h = self.norm1(h) + h = apply_scaleshift(h, self.cond0_layers(cond0), self.condition_bias) + if self.cond1_channels is not None: + h = apply_scaleshift(h, self.cond1_layers(cond1), self.condition_bias) + h = self.conv1(self.dropout(self.nonlinear(h))) + return self.skip(self.x_resample(x)) + h + + +class AttentionBlockArgs: + def __init__(self, + num_heads: Optional[int] = 1, + num_head_channels: Optional[int] = None, + use_new_attention_order: bool = False): + self.use_new_attention_order = use_new_attention_order + self.num_head_channels = num_head_channels + self.num_heads = num_heads + + +def qkv_attention_legacy(qkv: torch.Tensor, num_heads: int): + assert len(qkv.shape) == 3 + B, W, L = qkv.shape + H = num_heads + assert W % (3 * H) == 0 + C = W // (3 * H) + q, k, v = qkv.reshape(B * H, C * 3, L).split(C, dim=1) + scale = 1.0 / math.sqrt(math.sqrt(C)) + weight = torch.einsum('bct,bcs->bts', q * scale, k * scale) + weight = torch.softmax(weight, dim=-1) + output = torch.einsum("bts,bcs->bct", weight, v) + return output.reshape(B, H * C, L) + + +def qkv_attention(qkv: torch.Tensor, num_heads: int): + B, W, L = qkv.shape + H = num_heads + assert W % (3 * H) == 0 + C = W // (3 * H) + q, k, v = qkv.chunk(3, dim=1) + scale = 1.0 / math.sqrt(math.sqrt(C)) + weight = torch.einsum("bct,bcs->bts", (q * scale).view(B * H, C, L), (k * scale).view(B * H, C, L)) + weight = torch.softmax(weight, dim=-1) + output = torch.einsum("bts,bcs->bct", weight, v.reshape(B * H, C, L)) + return output.reshape(B, H * C, L) + + +class AttentionBlock(Module): + def __init__(self, + num_channels: int, + args: AttentionBlockArgs): + super().__init__() + self.use_new_attention_order = args.use_new_attention_order + + if args.num_head_channels is None: + assert args.num_heads is not None + assert num_channels % args.num_heads == 0 + self.num_heads = args.num_heads + self.num_head_channels = num_channels // self.num_heads + elif args.num_heads is None: + assert args.num_head_channels is not None + assert num_channels % args.num_head_channels == 0 + self.num_heads = num_channels // args.num_head_channels + self.num_head_channels = args.num_head_channels + + self.norm = GroupNorm32(num_channels) + self.qkv = Conv2d(num_channels, 3 * num_channels, kernel_size=1, stride=1, padding=0) + self.conv = Conv2d(num_channels, num_channels, kernel_size=1, stride=1, padding=0) + with torch.no_grad(): + zero_(self.conv.weight) + zero_(self.conv.bias) + + def forward(self, x: torch.Tensor): + assert len(x.shape) == 4 + B, C, H, W = x.shape + qkv = self.qkv(self.norm(x)).reshape(B, 3 * C, H * W) + if self.use_new_attention_order: + h = qkv_attention(qkv, self.num_heads) + else: + h = qkv_attention_legacy(qkv, self.num_heads) + h = self.conv(h.reshape(B, C, H, W)) + return x + h + + +class Arity3To1(Module): + def __init__(self, module: Module): + super().__init__() + self.module = module + + def forward(self, x: Tensor, y: Optional[Tensor] = None, z: Optional[Tensor] = None): + return self.module(x) + + +class DownsamplingBlock(Module): + def __init__(self, + in_channels: int, + out_channels: int, + cond0_channels: Optional[int], + cond1_channels: Optional[int], + num_res_blocks: int, + dropout_prob: float, + use_attention: bool, + perform_downsampling: bool, + resample_with_res_block: bool, + use_conv_to_resample: bool, + attention_block_args: AttentionBlockArgs, + condition_bias: float = 1.0): + super().__init__() + self.use_attention = use_attention + self.res_blocks = ModuleList() + self.attention_blocks = ModuleList() + self.perform_downsampling = perform_downsampling + self.output_channels = [] + for j in range(num_res_blocks): + self.res_blocks.append(ResBlock( + in_channels=in_channels if j == 0 else out_channels, + out_channels=out_channels, + cond0_channels=cond0_channels, + cond1_channels=cond1_channels, + dropout_prob=dropout_prob, + condition_bias=condition_bias)) + if use_attention: + self.attention_blocks.append(AttentionBlock(out_channels, attention_block_args)) + self.output_channels.append(out_channels) + if perform_downsampling: + if resample_with_res_block: + self.downsample = ResBlock( + in_channels=out_channels, + out_channels=out_channels, + cond0_channels=cond0_channels, + cond1_channels=cond1_channels, + dropout_prob=dropout_prob, + sampling_mode=SamplingMode.DOWNSAMPING, + condition_bias=condition_bias) + else: + self.downsample = Arity3To1(Downsample(out_channels, use_conv_to_resample)) + self.output_channels.append(out_channels) + + def forward(self, h: Tensor, cond0: Optional[Tensor] = None, cond1: Optional[Tensor] = None) -> List[Tensor]: + hs = [] + for i in range(len(self.res_blocks)): + h = self.res_blocks[i].forward(h, cond0, cond1) + if self.use_attention: + h = self.attention_blocks[i].forward(h) + hs.append(h) + if self.perform_downsampling: + hs.append(self.downsample(h, cond0, cond1)) + return hs + + +class UpsamplingBlock(Module): + def __init__(self, + in_channels: int, + out_channels: int, + cond0_channels: Optional[int], + cond1_channels: Optional[int], + num_resnet_blocks: int, + skip_channels: List[int], + dropout_prob: float, + use_attention: bool, + perform_upsampling: bool, + resample_with_res_block: bool, + use_conv_to_resample: bool, + attention_block_args: AttentionBlockArgs, + condition_bias: float = 1.0): + super().__init__() + self.use_attention = use_attention + self.resnet_blocks = ModuleList() + self.attention_blocks = ModuleList() + self.perform_upsampling = perform_upsampling + for i in range(num_resnet_blocks): + self.resnet_blocks.append(ResBlock( + in_channels=(in_channels if i == 0 else out_channels) + skip_channels[i], + out_channels=out_channels, + cond0_channels=cond0_channels, + cond1_channels=cond1_channels, + dropout_prob=dropout_prob, + condition_bias=condition_bias)) + if use_attention: + self.attention_blocks.append(AttentionBlock(out_channels, attention_block_args)) + if perform_upsampling: + if resample_with_res_block: + self.upsample = ResBlock( + in_channels=out_channels, + out_channels=out_channels, + cond0_channels=cond0_channels, + cond1_channels=cond1_channels, + sampling_mode=SamplingMode.UPSAMPLING, + dropout_prob=dropout_prob, + condition_bias=condition_bias) + else: + self.upsample = Arity3To1(Upsample(out_channels, use_conv_to_resample)) + + def forward(self, + h: Tensor, + skips: List[Tensor], + cond0: Optional[Tensor] = None, + cond1: Optional[Tensor] = None) -> Tensor: + for i in range(len(self.resnet_blocks)): + h = self.resnet_blocks[i].forward(torch.concat([h, skips[i]], dim=1), cond0, cond1) + if self.use_attention: + h = self.attention_blocks[i].forward(h) + if self.perform_upsampling: + h = self.upsample.forward(h, cond0, cond1) + return h + + +def compute_timestep_embedding(t: Tensor, out_channels: int): + assert len(t.shape) == 2 + b, c = t.shape + assert c == 1 + half_channels = out_channels // 2 + scale = -math.log(10000.0) / (half_channels - 1) + log_times = scale * torch.arange(0, half_channels, device=t.device) + times = torch.exp(log_times).reshape(1, half_channels) * t + t_emb = torch.cat([torch.cos(times), torch.sin(times)], dim=1) + if out_channels % 2 == 1: + t_emb = torch.nn.functional.pad(t_emb, (1, 1), mode='constant') + return t_emb + + +class TimeEmbedding(Module): + def __init__(self, out_channels: int): + super().__init__() + self.out_channels = out_channels + + def forward(self, t: Tensor): + return compute_timestep_embedding(t, self.out_channels) + + +class UnetArgs: + def __init__(self, + in_channels: int = 3, + out_channels: int = 3, + model_channels: int = 64, + level_channel_multipliers: Optional[List[int]] = None, + level_use_attention: Optional[List[bool]] = None, + num_res_blocks_per_level: int = 2, + num_middle_res_blocks: int = 2, + time_embedding_channels: Optional[int] = None, + cond_input_channels: int = 4, + cond_internal_channels: int = 512, + attention_block_args: Optional[AttentionBlockArgs] = None, + dropout_prob: float = 0.1, + resample_with_res_block: bool = True, + use_conv_to_resample=False, + condition_bias: float = 1.0): + assert len(level_channel_multipliers) == len(level_use_attention) + assert not use_conv_to_resample or not resample_with_res_block + + if time_embedding_channels is None: + time_embedding_channels = model_channels + if level_channel_multipliers is None: + level_channel_multipliers = [1, 2, 4, 8] + if level_use_attention is None: + level_use_attention = [False for _ in level_channel_multipliers] + if attention_block_args is None: + attention_block_args = AttentionBlockArgs( + num_heads=1, + num_head_channels=None, + use_new_attention_order=False) + + self.condition_bias = condition_bias + self.use_conv_to_resample = use_conv_to_resample + self.resample_with_res_block = resample_with_res_block + self.cond_internal_channels = cond_internal_channels + self.dropout_prob = dropout_prob + self.attention_block_args = attention_block_args + self.time_embedding_channels = time_embedding_channels + self.num_res_blocks_per_level = num_res_blocks_per_level + self.level_use_attention = level_use_attention + self.level_channel_multipliers = level_channel_multipliers + self.model_channels = model_channels + self.out_channels = out_channels + self.in_channels = in_channels + self.num_levels = len(level_channel_multipliers) + self.num_middle_res_blocks = num_middle_res_blocks + self.cond_input_channels = cond_input_channels + + +class Unet(Module): + def __init__(self, args: UnetArgs): + super().__init__() + self.args = args + + self.time_embed = Sequential( + TimeEmbedding(self.args.time_embedding_channels), + Linear(self.args.time_embedding_channels, self.args.cond_internal_channels), + SiLU(), + Linear(self.args.cond_internal_channels, self.args.cond_internal_channels)) + + self.cond_embed = Sequential( + Linear(self.args.cond_input_channels, self.args.cond_internal_channels), + SiLU(), + Linear(self.args.cond_internal_channels, self.args.cond_internal_channels)) + + self.first_conv = Conv2d(args.in_channels, args.model_channels, kernel_size=3, stride=1, padding=1) + current_channels = args.model_channels + channels = [current_channels] + + # Downsampling blocks + self.down_blocks = ModuleList() + for i in range(args.num_levels): + out_channels = args.model_channels * args.level_channel_multipliers[i] + perform_downsampling = i < args.num_levels - 1 + down_block = DownsamplingBlock( + in_channels=current_channels, + out_channels=out_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + num_res_blocks=args.num_res_blocks_per_level, + dropout_prob=args.dropout_prob, + use_attention=args.level_use_attention[i], + perform_downsampling=perform_downsampling, + attention_block_args=args.attention_block_args, + resample_with_res_block=args.resample_with_res_block, + use_conv_to_resample=args.use_conv_to_resample, + condition_bias=args.condition_bias) + self.down_blocks.append(down_block) + current_channels = out_channels + channels += down_block.output_channels + + # Middle blocks + self.middle_blocks = ModuleList() + for i in range(self.args.num_middle_res_blocks - 1): + self.middle_blocks.append(ResBlock( + in_channels=current_channels, + out_channels=current_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + dropout_prob=args.dropout_prob, + condition_bias=args.condition_bias)) + self.middle_blocks.append( + Arity3To1(AttentionBlock(num_channels=current_channels, args=args.attention_block_args))) + self.middle_blocks.append(ResBlock( + in_channels=current_channels, + out_channels=current_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + dropout_prob=args.dropout_prob, + condition_bias=args.condition_bias)) + + # Upsampling blocks + self.up_blocks = ModuleList() + for i in reversed(range(args.num_levels)): + skip_channels = [] + for j in range(args.num_res_blocks_per_level + 1): + skip_channels.append(channels.pop()) + perform_upsampling = i > 0 + out_channels = args.model_channels * args.level_channel_multipliers[i] + up_block = UpsamplingBlock( + in_channels=current_channels, + out_channels=out_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + num_resnet_blocks=args.num_res_blocks_per_level + 1, + skip_channels=skip_channels, + dropout_prob=args.dropout_prob, + use_attention=args.level_use_attention[i], + perform_upsampling=perform_upsampling, + attention_block_args=args.attention_block_args, + resample_with_res_block=args.resample_with_res_block, + use_conv_to_resample=args.use_conv_to_resample, + condition_bias=args.condition_bias) + self.up_blocks.append(up_block) + current_channels = out_channels + assert len(channels) == 0 + + self.last = Sequential( + GroupNorm32(current_channels), + SiLU(), + init_to_zero(Conv2d(current_channels, args.out_channels, kernel_size=3, stride=1, padding=1))) + + def forward(self, x: Tensor, t: Tensor, cond: Tensor): + t_emb = self.time_embed(t) + cond_emb = self.cond_embed(cond) + hs = [self.first_conv(x)] + for block in self.down_blocks: + hs += block.forward(hs[-1], t_emb, cond_emb) + h = hs[-1] + for block in self.middle_blocks: + h = block(h, t_emb, cond_emb) + for block in self.up_blocks: + skips = [] + for i in range(self.args.num_res_blocks_per_level + 1): + skips.append(hs.pop()) + h = block.forward(h, skips, t_emb, cond_emb) + assert len(hs) == 0 + return self.last(h) + + +class UnetWithFirstConvAddition(Module): + def __init__(self, args: UnetArgs): + super().__init__() + self.args = args + + self.time_embed = Sequential( + TimeEmbedding(self.args.time_embedding_channels), + Linear(self.args.time_embedding_channels, self.args.cond_internal_channels), + SiLU(), + Linear(self.args.cond_internal_channels, self.args.cond_internal_channels)) + + self.cond_embed = Sequential( + Linear(self.args.cond_input_channels, self.args.cond_internal_channels), + SiLU(), + Linear(self.args.cond_internal_channels, self.args.cond_internal_channels)) + + self.first_conv = Conv2d(args.in_channels, args.model_channels, kernel_size=3, stride=1, padding=1) + current_channels = args.model_channels + channels = [current_channels] + + # Downsampling blocks + self.down_blocks = ModuleList() + for i in range(args.num_levels): + out_channels = args.model_channels * args.level_channel_multipliers[i] + perform_downsampling = i < args.num_levels - 1 + down_block = DownsamplingBlock( + in_channels=current_channels, + out_channels=out_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + num_res_blocks=args.num_res_blocks_per_level, + dropout_prob=args.dropout_prob, + use_attention=args.level_use_attention[i], + perform_downsampling=perform_downsampling, + attention_block_args=args.attention_block_args, + resample_with_res_block=args.resample_with_res_block, + use_conv_to_resample=args.use_conv_to_resample, + condition_bias=args.condition_bias) + self.down_blocks.append(down_block) + current_channels = out_channels + channels += down_block.output_channels + + # Middle blocks + self.middle_blocks = ModuleList() + for i in range(self.args.num_middle_res_blocks - 1): + self.middle_blocks.append(ResBlock( + in_channels=current_channels, + out_channels=current_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + dropout_prob=args.dropout_prob, + condition_bias=args.condition_bias)) + self.middle_blocks.append( + Arity3To1(AttentionBlock(num_channels=current_channels, args=args.attention_block_args))) + self.middle_blocks.append(ResBlock( + in_channels=current_channels, + out_channels=current_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + dropout_prob=args.dropout_prob, + condition_bias=args.condition_bias)) + + # Upsampling blocks + self.up_blocks = ModuleList() + for i in reversed(range(args.num_levels)): + skip_channels = [] + for j in range(args.num_res_blocks_per_level + 1): + skip_channels.append(channels.pop()) + perform_upsampling = i > 0 + out_channels = args.model_channels * args.level_channel_multipliers[i] + up_block = UpsamplingBlock( + in_channels=current_channels, + out_channels=out_channels, + cond0_channels=args.cond_internal_channels, + cond1_channels=args.cond_internal_channels, + num_resnet_blocks=args.num_res_blocks_per_level + 1, + skip_channels=skip_channels, + dropout_prob=args.dropout_prob, + use_attention=args.level_use_attention[i], + perform_upsampling=perform_upsampling, + attention_block_args=args.attention_block_args, + resample_with_res_block=args.resample_with_res_block, + use_conv_to_resample=args.use_conv_to_resample, + condition_bias=args.condition_bias) + self.up_blocks.append(up_block) + current_channels = out_channels + assert len(channels) == 0 + + self.last = Sequential( + GroupNorm32(current_channels), + SiLU(), + init_to_zero(Conv2d(current_channels, args.out_channels, kernel_size=3, stride=1, padding=1))) + + def forward(self, x: Tensor, t: Tensor, cond: Tensor, first_conv_addition: Tensor): + t_emb = self.time_embed(t) + cond_emb = self.cond_embed(cond) + first_conv = self.first_conv(x) + hs = [first_conv + first_conv_addition] + for block in self.down_blocks: + hs += block.forward(hs[-1], t_emb, cond_emb) + h = hs[-1] + for block in self.middle_blocks: + h = block(h, t_emb, cond_emb) + for block in self.up_blocks: + skips = [] + for i in range(self.args.num_res_blocks_per_level + 1): + skips.append(hs.pop()) + h = block.forward(h, skips, t_emb, cond_emb) + assert len(hs) == 0 + return self.last(h) diff --git a/tha4/src/tha4/nn/conv.py b/tha4/src/tha4/nn/conv.py new file mode 100644 index 0000000..140de5d --- /dev/null +++ b/tha4/src/tha4/nn/conv.py @@ -0,0 +1,189 @@ +from typing import Optional, Union, Callable + +from torch.nn import Conv2d, Module, Sequential, ConvTranspose2d + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.nonlinearity_factory import resolve_nonlinearity_factory +from tha4.nn.normalization import NormalizationLayerFactory +from tha4.nn.util import wrap_conv_or_linear_module, BlockArgs + + +def create_conv7(in_channels: int, out_channels: int, + bias: bool = False, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + use_spectral_norm: bool = False) -> Module: + return wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=7, stride=1, padding=3, bias=bias), + initialization_method, + use_spectral_norm) + + +def create_conv7_from_block_args(in_channels: int, + out_channels: int, + bias: bool = False, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return create_conv7( + in_channels, out_channels, bias, + block_args.initialization_method, + block_args.use_spectral_norm) + + +def create_conv3(in_channels: int, + out_channels: int, + bias: bool = False, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + use_spectral_norm: bool = False) -> Module: + return wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=bias), + initialization_method, + use_spectral_norm) + + +def create_conv3_from_block_args(in_channels: int, out_channels: int, + bias: bool = False, + block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return create_conv3(in_channels, out_channels, bias, + block_args.initialization_method, + block_args.use_spectral_norm) + + +def create_conv1(in_channels: int, out_channels: int, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + bias: bool = False, + use_spectral_norm: bool = False) -> Module: + return wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=bias), + initialization_method, + use_spectral_norm) + + +def create_conv1_from_block_args(in_channels: int, + out_channels: int, + bias: bool = False, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return create_conv1( + in_channels=in_channels, + out_channels=out_channels, + initialization_method=block_args.initialization_method, + bias=bias, + use_spectral_norm=block_args.use_spectral_norm) + + +def create_conv7_block(in_channels: int, out_channels: int, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + nonlinearity_factory: Optional[ModuleFactory] = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + use_spectral_norm: bool = False) -> Module: + nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + return Sequential( + create_conv7(in_channels, out_channels, + bias=False, initialization_method=initialization_method, use_spectral_norm=use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(out_channels, affine=True), + resolve_nonlinearity_factory(nonlinearity_factory).create()) + + +def create_conv7_block_from_block_args( + in_channels: int, out_channels: int, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return create_conv7_block(in_channels, out_channels, + block_args.initialization_method, + block_args.nonlinearity_factory, + block_args.normalization_layer_factory, + block_args.use_spectral_norm) + + +def create_conv3_block(in_channels: int, out_channels: int, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + nonlinearity_factory: Optional[ModuleFactory] = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + use_spectral_norm: bool = False) -> Module: + nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + return Sequential( + create_conv3(in_channels, out_channels, + bias=False, initialization_method=initialization_method, use_spectral_norm=use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(out_channels, affine=True), + resolve_nonlinearity_factory(nonlinearity_factory).create()) + + +def create_conv3_block_from_block_args( + in_channels: int, out_channels: int, block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return create_conv3_block(in_channels, out_channels, + block_args.initialization_method, + block_args.nonlinearity_factory, + block_args.normalization_layer_factory, + block_args.use_spectral_norm) + + +def create_downsample_block(in_channels: int, out_channels: int, + is_output_1x1: bool = False, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + nonlinearity_factory: Optional[ModuleFactory] = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + use_spectral_norm: bool = False) -> Module: + if is_output_1x1: + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False), + initialization_method, + use_spectral_norm), + resolve_nonlinearity_factory(nonlinearity_factory).create()) + else: + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False), + initialization_method, + use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(out_channels, affine=True), + resolve_nonlinearity_factory(nonlinearity_factory).create()) + + +def create_downsample_block_from_block_args(in_channels: int, out_channels: int, + is_output_1x1: bool = False, + block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return create_downsample_block( + in_channels, out_channels, + is_output_1x1, + block_args.initialization_method, + block_args.nonlinearity_factory, + block_args.normalization_layer_factory, + block_args.use_spectral_norm) + + +def create_upsample_block(in_channels: int, + out_channels: int, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + nonlinearity_factory: Optional[ModuleFactory] = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + use_spectral_norm: bool = False) -> Module: + nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + return Sequential( + wrap_conv_or_linear_module( + ConvTranspose2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False), + initialization_method, + use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(out_channels, affine=True), + resolve_nonlinearity_factory(nonlinearity_factory).create()) + + +def create_upsample_block_from_block_args(in_channels: int, + out_channels: int, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return create_upsample_block(in_channels, out_channels, + block_args.initialization_method, + block_args.nonlinearity_factory, + block_args.normalization_layer_factory, + block_args.use_spectral_norm) diff --git a/tha4/src/tha4/nn/eyebrow_decomposer/__init__.py b/tha4/src/tha4/nn/eyebrow_decomposer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/eyebrow_decomposer/eyebrow_decomposer_00.py b/tha4/src/tha4/nn/eyebrow_decomposer/eyebrow_decomposer_00.py new file mode 100644 index 0000000..6064f1e --- /dev/null +++ b/tha4/src/tha4/nn/eyebrow_decomposer/eyebrow_decomposer_00.py @@ -0,0 +1,81 @@ +from typing import List, Optional + +import torch +from torch import Tensor +from torch.nn import Module + +from tha4.nn.common.poser_encoder_decoder_00 import PoserEncoderDecoder00Args, PoserEncoderDecoder00 +from tha4.nn.image_processing_util import apply_color_change +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs + + +class EyebrowDecomposer00Args(PoserEncoderDecoder00Args): + def __init__(self, + image_size: int = 128, + image_channels: int = 4, + start_channels: int = 64, + bottleneck_image_size=16, + num_bottleneck_blocks=6, + max_channels: int = 512, + block_args: Optional[BlockArgs] = None): + super().__init__( + image_size, + image_channels, + image_channels, + 0, + start_channels, + bottleneck_image_size, + num_bottleneck_blocks, + max_channels, + block_args) + + +class EyebrowDecomposer00(Module): + def __init__(self, args: EyebrowDecomposer00Args): + super().__init__() + self.args = args + self.body = PoserEncoderDecoder00(args) + self.background_layer_alpha = self.args.create_alpha_block() + self.background_layer_color_change = self.args.create_color_change_block() + self.eyebrow_layer_alpha = self.args.create_alpha_block() + self.eyebrow_layer_color_change = self.args.create_color_change_block() + + def forward(self, image: Tensor, *args) -> List[Tensor]: + feature = self.body(image)[0] + + background_layer_alpha = self.background_layer_alpha(feature) + background_layer_color_change = self.background_layer_color_change(feature) + background_layer_1 = apply_color_change(background_layer_alpha, background_layer_color_change, image) + + eyebrow_layer_alpha = self.eyebrow_layer_alpha(feature) + eyebrow_layer_color_change = self.eyebrow_layer_color_change(feature) + eyebrow_layer = apply_color_change(eyebrow_layer_alpha, image, eyebrow_layer_color_change) + + return [ + eyebrow_layer, # 0 + eyebrow_layer_alpha, # 1 + eyebrow_layer_color_change, # 2 + background_layer_1, # 3 + background_layer_alpha, # 4 + background_layer_color_change, # 5 + ] + + EYEBROW_LAYER_INDEX = 0 + EYEBROW_LAYER_ALPHA_INDEX = 1 + EYEBROW_LAYER_COLOR_CHANGE_INDEX = 2 + BACKGROUND_LAYER_INDEX = 3 + BACKGROUND_LAYER_ALPHA_INDEX = 4 + BACKGROUND_LAYER_COLOR_CHANGE_INDEX = 5 + OUTPUT_LENGTH = 6 + + +class EyebrowDecomposer00Factory(ModuleFactory): + def __init__(self, args: EyebrowDecomposer00Args): + super().__init__() + self.args = args + + def create(self) -> Module: + return EyebrowDecomposer00(self.args) diff --git a/tha4/src/tha4/nn/eyebrow_morphing_combiner/__init__.py b/tha4/src/tha4/nn/eyebrow_morphing_combiner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/eyebrow_morphing_combiner/eyebrow_morphing_combiner_00.py b/tha4/src/tha4/nn/eyebrow_morphing_combiner/eyebrow_morphing_combiner_00.py new file mode 100644 index 0000000..eb579b5 --- /dev/null +++ b/tha4/src/tha4/nn/eyebrow_morphing_combiner/eyebrow_morphing_combiner_00.py @@ -0,0 +1,91 @@ +from typing import List, Optional + +import torch +from torch import Tensor +from torch.nn import Module + +from tha4.nn.common.poser_encoder_decoder_00 import PoserEncoderDecoder00Args, PoserEncoderDecoder00 +from tha4.nn.image_processing_util import apply_color_change, apply_grid_change, apply_rgb_change +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs + + +class EyebrowMorphingCombiner00Args(PoserEncoderDecoder00Args): + def __init__(self, + image_size: int = 128, + image_channels: int = 4, + num_pose_params: int = 12, + start_channels: int = 64, + bottleneck_image_size=16, + num_bottleneck_blocks=6, + max_channels: int = 512, + block_args: Optional[BlockArgs] = None): + super().__init__( + image_size, + 2 * image_channels, + image_channels, + num_pose_params, + start_channels, + bottleneck_image_size, + num_bottleneck_blocks, + max_channels, + block_args) + + +class EyebrowMorphingCombiner00(Module): + def __init__(self, args: EyebrowMorphingCombiner00Args): + super().__init__() + self.args = args + self.body = PoserEncoderDecoder00(args) + self.morphed_eyebrow_layer_grid_change = self.args.create_grid_change_block() + self.morphed_eyebrow_layer_alpha = self.args.create_alpha_block() + self.morphed_eyebrow_layer_color_change = self.args.create_color_change_block() + self.combine_alpha = self.args.create_alpha_block() + + def forward(self, background_layer: Tensor, eyebrow_layer: Tensor, pose: Tensor, *args) -> List[Tensor]: + combined_image = torch.cat([background_layer, eyebrow_layer], dim=1) + feature = self.body(combined_image, pose)[0] + + morphed_eyebrow_layer_grid_change = self.morphed_eyebrow_layer_grid_change(feature) + morphed_eyebrow_layer_alpha = self.morphed_eyebrow_layer_alpha(feature) + morphed_eyebrow_layer_color_change = self.morphed_eyebrow_layer_color_change(feature) + warped_eyebrow_layer = apply_grid_change(morphed_eyebrow_layer_grid_change, eyebrow_layer) + morphed_eyebrow_layer = apply_color_change( + morphed_eyebrow_layer_alpha, morphed_eyebrow_layer_color_change, warped_eyebrow_layer) + + combine_alpha = self.combine_alpha(feature) + eyebrow_image = apply_rgb_change(combine_alpha, morphed_eyebrow_layer, background_layer) + eyebrow_image_no_combine_alpha = apply_rgb_change( + (morphed_eyebrow_layer[:, 3:4, :, :] + 1.0) / 2.0, morphed_eyebrow_layer, background_layer) + + return [ + eyebrow_image, # 0 + combine_alpha, # 1 + eyebrow_image_no_combine_alpha, # 2 + morphed_eyebrow_layer, # 3 + morphed_eyebrow_layer_alpha, # 4 + morphed_eyebrow_layer_color_change, # 5 + warped_eyebrow_layer, # 6 + morphed_eyebrow_layer_grid_change, # 7 + ] + + EYEBROW_IMAGE_INDEX = 0 + COMBINE_ALPHA_INDEX = 1 + EYEBROW_IMAGE_NO_COMBINE_ALPHA_INDEX = 2 + MORPHED_EYEBROW_LAYER_INDEX = 3 + MORPHED_EYEBROW_LAYER_ALPHA_INDEX = 4 + MORPHED_EYEBROW_LAYER_COLOR_CHANGE_INDEX = 5 + WARPED_EYEBROW_LAYER_INDEX = 6 + MORPHED_EYEBROW_LAYER_GRID_CHANGE_INDEX = 7 + OUTPUT_LENGTH = 8 + + +class EyebrowMorphingCombiner00Factory(ModuleFactory): + def __init__(self, args: EyebrowMorphingCombiner00Args): + super().__init__() + self.args = args + + def create(self) -> Module: + return EyebrowMorphingCombiner00(self.args) diff --git a/tha4/src/tha4/nn/face_morpher/__init__.py b/tha4/src/tha4/nn/face_morpher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/face_morpher/face_morpher_08.py b/tha4/src/tha4/nn/face_morpher/face_morpher_08.py new file mode 100644 index 0000000..d9bf5b1 --- /dev/null +++ b/tha4/src/tha4/nn/face_morpher/face_morpher_08.py @@ -0,0 +1,211 @@ +import math +from typing import List, Optional + +import torch +from torch import Tensor +from torch.nn import ModuleList, Sequential, Sigmoid, Tanh, Module +from torch.nn.functional import affine_grid, grid_sample + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.conv import create_conv3_block_from_block_args, \ + create_downsample_block_from_block_args, create_upsample_block_from_block_args, create_conv3_from_block_args, \ + create_conv3 +from tha4.nn.nonlinearity_factory import LeakyReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.resnet_block import ResnetBlock +from tha4.nn.util import BlockArgs + + +class FaceMorpher08Args: + def __init__(self, + image_size: int = 256, + image_channels: int = 4, + num_expression_params: int = 67, + start_channels: int = 16, + bottleneck_image_size=4, + num_bottleneck_blocks=3, + max_channels: int = 512, + block_args: Optional[BlockArgs] = None, + output_iris_mouth_grid_change: bool = False): + self.max_channels = max_channels + self.num_bottleneck_blocks = num_bottleneck_blocks + assert bottleneck_image_size > 1 + self.bottleneck_image_size = bottleneck_image_size + self.start_channels = start_channels + self.image_channels = image_channels + self.num_expression_params = num_expression_params + self.image_size = image_size + self.output_iris_mouth_grid_change = output_iris_mouth_grid_change + + if block_args is None: + self.block_args = BlockArgs( + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=LeakyReLUFactory(negative_slope=0.2, inplace=True)) + else: + self.block_args = block_args + + +class FaceMorpher08(Module): + def __init__(self, args: FaceMorpher08Args): + super().__init__() + self.args = args + self.num_levels = int(math.log2(args.image_size // args.bottleneck_image_size)) + 1 + + self.downsample_blocks = ModuleList() + self.downsample_blocks.append( + create_conv3_block_from_block_args( + args.image_channels, + args.start_channels, + args.block_args)) + current_image_size = args.image_size + current_num_channels = args.start_channels + while current_image_size > args.bottleneck_image_size: + next_image_size = current_image_size // 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.downsample_blocks.append(create_downsample_block_from_block_args( + in_channels=current_num_channels, + out_channels=next_num_channels, + is_output_1x1=False, + block_args=args.block_args)) + current_image_size = next_image_size + current_num_channels = next_num_channels + assert len(self.downsample_blocks) == self.num_levels + + self.bottleneck_blocks = ModuleList() + self.bottleneck_blocks.append(create_conv3_block_from_block_args( + in_channels=current_num_channels + args.num_expression_params, + out_channels=current_num_channels, + block_args=args.block_args)) + for i in range(1, args.num_bottleneck_blocks): + self.bottleneck_blocks.append( + ResnetBlock.create( + num_channels=current_num_channels, + is1x1=False, + block_args=args.block_args)) + + self.upsample_blocks = ModuleList() + while current_image_size < args.image_size: + next_image_size = current_image_size * 2 + next_num_channels = self.get_num_output_channels_from_image_size(next_image_size) + self.upsample_blocks.append(create_upsample_block_from_block_args( + in_channels=current_num_channels, + out_channels=next_num_channels, + block_args=args.block_args)) + current_image_size = next_image_size + current_num_channels = next_num_channels + + self.iris_mouth_grid_change = self.create_grid_change_block() + self.iris_mouth_color_change = self.create_color_change_block() + self.iris_mouth_alpha = self.create_alpha_block() + + self.eye_color_change = self.create_color_change_block() + self.eye_alpha = self.create_alpha_block() + + def create_alpha_block(self): + return Sequential( + create_conv3( + in_channels=self.args.start_channels, + out_channels=1, + bias=True, + initialization_method=self.args.block_args.initialization_method, + use_spectral_norm=False), + Sigmoid()) + + def create_color_change_block(self): + return Sequential( + create_conv3_from_block_args( + in_channels=self.args.start_channels, + out_channels=self.args.image_channels, + bias=True, + block_args=self.args.block_args), + Tanh()) + + def create_grid_change_block(self): + return create_conv3( + in_channels=self.args.start_channels, + out_channels=2, + bias=False, + initialization_method='zero', + use_spectral_norm=False) + + def get_num_output_channels_from_level(self, level: int): + return self.get_num_output_channels_from_image_size(self.args.image_size // (2 ** level)) + + def get_num_output_channels_from_image_size(self, image_size: int): + return min(self.args.start_channels * (self.args.image_size // image_size), self.args.max_channels) + + def merge_down(self, top_layer: Tensor, bottom_layer: Tensor): + top_layer_rgb = top_layer[:, 0:3, :, :] + top_layer_a = top_layer[:, 3:4, :, :] + return bottom_layer * (1 - top_layer_a) + torch.cat([top_layer_rgb * top_layer_a, top_layer_a], dim=1) + + def apply_grid_change(self, grid_change, image: Tensor) -> Tensor: + n, c, h, w = image.shape + device = grid_change.device + grid_change = torch.transpose(grid_change.view(n, 2, h * w), 1, 2).view(n, h, w, 2) + identity = torch.tensor( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + device=device, + dtype=grid_change.dtype).unsqueeze(0).repeat(n, 1, 1) + base_grid = affine_grid(identity, [n, c, h, w], align_corners=False) + grid = base_grid + grid_change + resampled_image = grid_sample(image, grid, mode='bilinear', padding_mode='border', align_corners=False) + return resampled_image + + def apply_color_change(self, alpha, color_change, image: Tensor) -> Tensor: + return color_change * alpha + image * (1 - alpha) + + def forward(self, image: Tensor, pose: Tensor, *args) -> List[Tensor]: + feature = image + for block in self.downsample_blocks: + feature = block(feature) + n, c = pose.shape + pose = pose.view(n, c, 1, 1).repeat(1, 1, self.args.bottleneck_image_size, self.args.bottleneck_image_size) + feature = torch.cat([feature, pose], dim=1) + for block in self.bottleneck_blocks: + feature = block(feature) + for block in self.upsample_blocks: + feature = block(feature) + + iris_mouth_grid_change = self.iris_mouth_grid_change(feature) + iris_mouth_image_0 = self.apply_grid_change(iris_mouth_grid_change, image) + iris_mouth_color_change = self.iris_mouth_color_change(feature) + iris_mouth_alpha = self.iris_mouth_alpha(feature) + iris_mouth_image_1 = self.apply_color_change(iris_mouth_alpha, iris_mouth_color_change, iris_mouth_image_0) + + eye_color_change = self.eye_color_change(feature) + eye_alpha = self.eye_alpha(feature) + output_image = self.apply_color_change(eye_alpha, eye_color_change, iris_mouth_image_1.detach()) + + outputs = [ + output_image, # 0 + eye_alpha, # 1 + eye_color_change, # 2 + iris_mouth_image_1, # 3 + iris_mouth_alpha, # 4 + iris_mouth_color_change, # 5 + iris_mouth_image_0, # 6 + ] + + if self.args.output_iris_mouth_grid_change: + outputs.append(iris_mouth_grid_change) + + return outputs + + OUTPUT_IMAGE_INDEX = 0 + EYE_ALPHA_INDEX = 1 + EYE_COLOR_CHANGE_INDEX = 2 + IRIS_MOUTH_IMAGE_1_INDEX = 3 + IRIS_MOUTH_ALPHA_INDEX = 4 + IRIS_MOUTH_COLOR_CHANGE_INDEX = 5 + IRIS_MOUTH_IMAGE_0_INDEX = 6 + IRIS_MOUTH_GRID_CHANGE_INDEX = 7 + + +class FaceMorpher08Factory(ModuleFactory): + def __init__(self, args: FaceMorpher08Args): + super().__init__() + self.args = args + + def create(self) -> Module: + return FaceMorpher08(self.args) diff --git a/tha4/src/tha4/nn/image_processing_util.py b/tha4/src/tha4/nn/image_processing_util.py new file mode 100644 index 0000000..e1d1a05 --- /dev/null +++ b/tha4/src/tha4/nn/image_processing_util.py @@ -0,0 +1,58 @@ +import torch +from torch import Tensor +from torch.nn.functional import affine_grid, grid_sample + + +def apply_rgb_change(alpha: Tensor, color_change: Tensor, image: Tensor): + image_rgb = image[:, 0:3, :, :] + color_change_rgb = color_change[:, 0:3, :, :] + output_rgb = color_change_rgb * alpha + image_rgb * (1 - alpha) + return torch.cat([output_rgb, image[:, 3:4, :, :]], dim=1) + + +def apply_grid_change(grid_change, image: Tensor) -> Tensor: + n, c, h, w = image.shape + device = grid_change.device + grid_change = torch.transpose(grid_change.view(n, 2, h * w), 1, 2).view(n, h, w, 2) + identity = torch.tensor( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + dtype=grid_change.dtype, + device=device).unsqueeze(0).repeat(n, 1, 1) + base_grid = affine_grid(identity, [n, c, h, w], align_corners=False) + grid = base_grid + grid_change + resampled_image = grid_sample(image, grid, mode='bilinear', padding_mode='border', align_corners=False) + return resampled_image + + +class GridChangeApplier: + def __init__(self): + self.last_n = None + self.last_device = None + self.last_identity = None + + def apply(self, grid_change: Tensor, image: Tensor, align_corners: bool = False) -> Tensor: + n, c, h, w = image.shape + device = grid_change.device + grid_change = torch.transpose(grid_change.view(n, 2, h * w), 1, 2).view(n, h, w, 2) + + if n == self.last_n and device == self.last_device: + identity = self.last_identity + else: + identity = torch.tensor( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + dtype=grid_change.dtype, + device=device, + requires_grad=False) \ + .unsqueeze(0).repeat(n, 1, 1) + self.last_identity = identity + self.last_n = n + self.last_device = device + base_grid = affine_grid(identity, [n, c, h, w], align_corners=align_corners) + + grid = base_grid + grid_change + resampled_image = grid_sample(image, grid, mode='bilinear', padding_mode='border', align_corners=align_corners) + return resampled_image + + +def apply_color_change(alpha, color_change, image: Tensor) -> Tensor: + return color_change * alpha + image * (1 - alpha) diff --git a/tha4/src/tha4/nn/init_function.py b/tha4/src/tha4/nn/init_function.py new file mode 100644 index 0000000..b5061bd --- /dev/null +++ b/tha4/src/tha4/nn/init_function.py @@ -0,0 +1,76 @@ +from typing import Callable + +import torch +from torch import zero_ +from torch.nn import Module +from torch.nn.init import kaiming_normal_, xavier_normal_, normal_ + + +def create_init_function(method: str = 'none') -> Callable[[Module], Module]: + def init(module: Module): + if method == 'none': + return module + elif method == 'he': + kaiming_normal_(module.weight) + return module + elif method == 'xavier': + xavier_normal_(module.weight) + return module + elif method == 'dcgan': + normal_(module.weight, 0.0, 0.02) + return module + elif method == 'dcgan_001': + normal_(module.weight, 0.0, 0.01) + return module + elif method == "zero": + with torch.no_grad(): + zero_(module.weight) + return module + else: + raise ("Invalid initialization method %s" % method) + + return init + + +class HeInitialization: + def __init__(self, a: int = 0, mode: str = 'fan_in', nonlinearity: str = 'leaky_relu'): + self.nonlinearity = nonlinearity + self.mode = mode + self.a = a + + def __call__(self, module: Module) -> Module: + with torch.no_grad(): + kaiming_normal_(module.weight, a=self.a, mode=self.mode, nonlinearity=self.nonlinearity) + return module + + +class NormalInitialization: + def __init__(self, mean: float = 0.0, std: float = 1.0): + self.std = std + self.mean = mean + + def __call__(self, module: Module) -> Module: + with torch.no_grad(): + normal_(module.weight, self.mean, self.std) + return module + + +class XavierInitialization: + def __init__(self, gain: float = 1.0): + self.gain = gain + + def __call__(self, module: Module) -> Module: + with torch.no_grad(): + xavier_normal_(module.weight, self.gain) + return module + + +class ZeroInitialization: + def __call__(self, module: Module) -> Module: + with torch.no_grad: + zero_(module.weight) + return module + +class NoInitialization: + def __call__(self, module: Module) -> Module: + return module \ No newline at end of file diff --git a/tha4/src/tha4/nn/morpher/__init__.py b/tha4/src/tha4/nn/morpher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/morpher/morpher_00.py b/tha4/src/tha4/nn/morpher/morpher_00.py new file mode 100644 index 0000000..b75ba2d --- /dev/null +++ b/tha4/src/tha4/nn/morpher/morpher_00.py @@ -0,0 +1,80 @@ +from typing import List + +import torch +from torch import Tensor +from torch.nn import Module + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.image_processing_util import GridChangeApplier +from tha4.nn.common.unet import UnetArgs, Unet, AttentionBlockArgs + + +def apply_color_change(alpha, color_change, image: Tensor) -> Tensor: + return color_change * alpha + image * (1 - alpha) + + +class Morpher00Args: + def __init__(self, + image_size: int, + image_channels: int, + num_pose_parameters: int, + unet_args: UnetArgs): + assert unet_args.in_channels == image_channels + assert unet_args.out_channels == ( + image_channels + # direct + 2 + # warp + 1 # alpha + ) + assert unet_args.cond_input_channels == num_pose_parameters + self.image_channels = image_channels + self.image_size = image_size + self.num_pose_parameters = num_pose_parameters + self.unet_args = unet_args + + +class Morpher00(Module): + def __init__(self, args: Morpher00Args): + super().__init__() + self.args = args + self.body = Unet(args.unet_args) + self.grid_change_applier = GridChangeApplier() + + def forward(self, image: torch.Tensor, pose: torch.Tensor) -> List[Tensor]: + assert len(image.shape) == 4 + assert image.shape[1] == self.args.image_channels + assert image.shape[2] == self.args.image_size + assert image.shape[3] == self.args.image_size + assert len(pose.shape) == 2 + assert image.shape[0] == pose.shape[0] + assert pose.shape[1] == self.args.num_pose_parameters + + t = torch.zeros(image.shape[0], 1, device=image.device) + body_output = self.body(image, t, pose) + direct = body_output[:, 0:self.args.image_channels, :, :] + grid_change = body_output[:, self.args.image_channels:self.args.image_channels + 2, :, :] + alpha = torch.sigmoid(body_output[:, self.args.image_channels + 2:self.args.image_channels + 3, :, :]) + + warped = self.grid_change_applier.apply(grid_change, image) + merged = apply_color_change(alpha, direct, warped) + + return [ + merged, + alpha, + warped, + grid_change, + direct + ] + + INDEX_MERGED = 0 + INDEX_ALPHA = 1 + INDEX_WARPED = 2 + INDEX_GRID_CHANGE = 3 + INDEX_DIRECT = 4 + + +class Morpher00Factory(ModuleFactory): + def __init__(self, args: Morpher00Args): + self.args = args + + def create(self) -> Module: + return Morpher00(self.args) diff --git a/tha4/src/tha4/nn/nonlinearity_factory.py b/tha4/src/tha4/nn/nonlinearity_factory.py new file mode 100644 index 0000000..a935119 --- /dev/null +++ b/tha4/src/tha4/nn/nonlinearity_factory.py @@ -0,0 +1,72 @@ +from typing import Optional + +from torch.nn import Module, ReLU, LeakyReLU, ELU, ReLU6, Hardswish, SiLU, Tanh, Sigmoid + +from tha4.shion.core.module_factory import ModuleFactory + + +class ReLUFactory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return ReLU(self.inplace) + + +class LeakyReLUFactory(ModuleFactory): + def __init__(self, inplace: bool = False, negative_slope: float = 1e-2): + self.negative_slope = negative_slope + self.inplace = inplace + + def create(self) -> Module: + return LeakyReLU(inplace=self.inplace, negative_slope=self.negative_slope) + + +class ELUFactory(ModuleFactory): + def __init__(self, inplace: bool = False, alpha: float = 1.0): + self.alpha = alpha + self.inplace = inplace + + def create(self) -> Module: + return ELU(inplace=self.inplace, alpha=self.alpha) + + +class ReLU6Factory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return ReLU6(inplace=self.inplace) + + +class SiLUFactory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return SiLU(inplace=self.inplace) + + +class HardswishFactory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return Hardswish(inplace=self.inplace) + + +class TanhFactory(ModuleFactory): + def create(self) -> Module: + return Tanh() + + +class SigmoidFactory(ModuleFactory): + def create(self) -> Module: + return Sigmoid() + + +def resolve_nonlinearity_factory(nonlinearity_fatory: Optional[ModuleFactory]) -> ModuleFactory: + if nonlinearity_fatory is None: + return ReLUFactory(inplace=False) + else: + return nonlinearity_fatory diff --git a/tha4/src/tha4/nn/normalization.py b/tha4/src/tha4/nn/normalization.py new file mode 100644 index 0000000..4d65584 --- /dev/null +++ b/tha4/src/tha4/nn/normalization.py @@ -0,0 +1,126 @@ +from abc import ABC, abstractmethod +from typing import Optional + +import torch +from torch import layer_norm +from torch.nn import Module, BatchNorm2d, InstanceNorm2d, Parameter +from torch.nn.init import normal_, constant_ + +from tha4.nn.pass_through import PassThrough + + +class PixelNormalization(Module): + def __init__(self, epsilon=1e-8): + super().__init__() + self.epsilon = epsilon + + def forward(self, x): + return x / torch.sqrt((x ** 2).mean(dim=1, keepdim=True) + self.epsilon) + + +class NormalizationLayerFactory(ABC): + def __init__(self): + super().__init__() + + @abstractmethod + def create(self, num_features: int, affine: bool = True) -> Module: + pass + + @staticmethod + def resolve_2d(factory: Optional['NormalizationLayerFactory']) -> 'NormalizationLayerFactory': + if factory is None: + return InstanceNorm2dFactory() + else: + return factory + + +class Bias2d(Module): + def __init__(self, num_features: int): + super().__init__() + self.num_features = num_features + self.bias = Parameter(torch.zeros(1, num_features, 1, 1)) + + def forward(self, x): + return x + self.bias + + +class NoNorm2dFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + if affine: + return Bias2d(num_features) + else: + return PassThrough() + + +class BatchNorm2dFactory(NormalizationLayerFactory): + def __init__(self, + weight_mean: Optional[float] = None, + weight_std: Optional[float] = None, + bias: Optional[float] = None): + super().__init__() + self.bias = bias + self.weight_std = weight_std + self.weight_mean = weight_mean + + def get_weight_mean(self): + if self.weight_mean is None: + return 1.0 + else: + return self.weight_mean + + def get_weight_std(self): + if self.weight_std is None: + return 0.02 + else: + return self.weight_std + + def create(self, num_features: int, affine: bool = True) -> Module: + module = BatchNorm2d(num_features=num_features, affine=affine) + if affine: + if self.weight_mean is not None or self.weight_std is not None: + normal_(module.weight, self.get_weight_mean(), self.get_weight_std()) + if self.bias is not None: + constant_(module.bias, self.bias) + return module + + +class InstanceNorm2dFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + return InstanceNorm2d(num_features=num_features, affine=affine) + + +class PixelNormFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + return PixelNormalization() + + +class LayerNorm2d(Module): + def __init__(self, channels: int, affine: bool = True): + super(LayerNorm2d, self).__init__() + self.channels = channels + self.affine = affine + + if self.affine: + self.weight = Parameter(torch.ones(1, channels, 1, 1)) + self.bias = Parameter(torch.zeros(1, channels, 1, 1)) + + def forward(self, x): + shape = x.size()[1:] + y = layer_norm(x, shape) * self.weight + self.bias + return y + +class LayerNorm2dFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + return LayerNorm2d(channels=num_features, affine=affine) diff --git a/tha4/src/tha4/nn/pass_through.py b/tha4/src/tha4/nn/pass_through.py new file mode 100644 index 0000000..c64d678 --- /dev/null +++ b/tha4/src/tha4/nn/pass_through.py @@ -0,0 +1,9 @@ +from torch.nn import Module + + +class PassThrough(Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x \ No newline at end of file diff --git a/tha4/src/tha4/nn/resnet_block.py b/tha4/src/tha4/nn/resnet_block.py new file mode 100644 index 0000000..43f80c0 --- /dev/null +++ b/tha4/src/tha4/nn/resnet_block.py @@ -0,0 +1,67 @@ +from typing import Optional + +import torch +from torch.nn import Module, Sequential, Parameter + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.conv import create_conv1, create_conv3 +from tha4.nn.nonlinearity_factory import resolve_nonlinearity_factory +from tha4.nn.normalization import NormalizationLayerFactory +from tha4.nn.util import BlockArgs + + +class ResnetBlock(Module): + @staticmethod + def create(num_channels: int, + is1x1: bool = False, + use_scale_parameters: bool = False, + block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return ResnetBlock(num_channels, + is1x1, + block_args.initialization_method, + block_args.nonlinearity_factory, + block_args.normalization_layer_factory, + block_args.use_spectral_norm, + use_scale_parameters) + + def __init__(self, + num_channels: int, + is1x1: bool = False, + initialization_method: str = 'he', + nonlinearity_factory: ModuleFactory = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + use_spectral_norm: bool = False, + use_scale_parameter: bool = False): + super().__init__() + self.use_scale_parameter = use_scale_parameter + if self.use_scale_parameter: + self.scale = Parameter(torch.zeros(1)) + nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + if is1x1: + self.resnet_path = Sequential( + create_conv1(num_channels, num_channels, initialization_method, + bias=True, + use_spectral_norm=use_spectral_norm), + nonlinearity_factory.create(), + create_conv1(num_channels, num_channels, initialization_method, + bias=True, + use_spectral_norm=use_spectral_norm)) + else: + self.resnet_path = Sequential( + create_conv3(num_channels, num_channels, + bias=False, initialization_method=initialization_method, + use_spectral_norm=use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(num_channels, affine=True), + nonlinearity_factory.create(), + create_conv3(num_channels, num_channels, + bias=False, initialization_method=initialization_method, + use_spectral_norm=use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(num_channels, affine=True)) + + def forward(self, x): + if self.use_scale_parameter: + return x + self.scale * self.resnet_path(x) + else: + return x + self.resnet_path(x) diff --git a/tha4/src/tha4/nn/resnet_block_seperable.py b/tha4/src/tha4/nn/resnet_block_seperable.py new file mode 100644 index 0000000..4562a75 --- /dev/null +++ b/tha4/src/tha4/nn/resnet_block_seperable.py @@ -0,0 +1,71 @@ +from typing import Optional + +import torch +from torch.nn import Module, Sequential, Parameter + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.conv import create_conv1 +from tha4.nn.nonlinearity_factory import resolve_nonlinearity_factory +from tha4.nn.normalization import NormalizationLayerFactory +from tha4.nn.separable_conv import create_separable_conv3 +from tha4.nn.util import BlockArgs + + +class ResnetBlockSeparable(Module): + @staticmethod + def create(num_channels: int, + is1x1: bool = False, + use_scale_parameters: bool = False, + block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return ResnetBlockSeparable( + num_channels, + is1x1, + block_args.initialization_method, + block_args.nonlinearity_factory, + block_args.normalization_layer_factory, + block_args.use_spectral_norm, + use_scale_parameters) + + def __init__(self, + num_channels: int, + is1x1: bool = False, + initialization_method: str = 'he', + nonlinearity_factory: ModuleFactory = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + use_spectral_norm: bool = False, + use_scale_parameter: bool = False): + super().__init__() + self.use_scale_parameter = use_scale_parameter + if self.use_scale_parameter: + self.scale = Parameter(torch.zeros(1)) + nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + if is1x1: + self.resnet_path = Sequential( + create_conv1(num_channels, num_channels, initialization_method, + bias=True, + use_spectral_norm=use_spectral_norm), + nonlinearity_factory.create(), + create_conv1(num_channels, num_channels, initialization_method, + bias=True, + use_spectral_norm=use_spectral_norm)) + else: + self.resnet_path = Sequential( + create_separable_conv3( + num_channels, num_channels, + bias=False, initialization_method=initialization_method, + use_spectral_norm=use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(num_channels, affine=True), + nonlinearity_factory.create(), + create_separable_conv3( + num_channels, num_channels, + bias=False, initialization_method=initialization_method, + use_spectral_norm=use_spectral_norm), + NormalizationLayerFactory.resolve_2d(normalization_layer_factory).create(num_channels, affine=True)) + + def forward(self, x): + if self.use_scale_parameter: + return x + self.scale * self.resnet_path(x) + else: + return x + self.resnet_path(x) diff --git a/tha4/src/tha4/nn/separable_conv.py b/tha4/src/tha4/nn/separable_conv.py new file mode 100644 index 0000000..c1ff1f4 --- /dev/null +++ b/tha4/src/tha4/nn/separable_conv.py @@ -0,0 +1,119 @@ +from typing import Optional + +from torch.nn import Sequential, Conv2d, ConvTranspose2d, Module + +from tha4.nn.normalization import NormalizationLayerFactory +from tha4.nn.util import BlockArgs, wrap_conv_or_linear_module + + +def create_separable_conv3(in_channels: int, out_channels: int, + bias: bool = False, + initialization_method='he', + use_spectral_norm: bool = False) -> Module: + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1, bias=False, groups=in_channels), + initialization_method, + use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=bias), + initialization_method, + use_spectral_norm)) + + +def create_separable_conv7(in_channels: int, out_channels: int, + bias: bool = False, + initialization_method='he', + use_spectral_norm: bool = False) -> Module: + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, in_channels, kernel_size=7, stride=1, padding=3, bias=False, groups=in_channels), + initialization_method, + use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=bias), + initialization_method, + use_spectral_norm)) + + +def create_separable_conv3_block( + in_channels: int, out_channels: int, block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1, bias=False, groups=in_channels), + block_args.initialization_method, + block_args.use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False), + block_args.initialization_method, + block_args.use_spectral_norm), + NormalizationLayerFactory.resolve_2d(block_args.normalization_layer_factory).create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) + + +def create_separable_conv7_block( + in_channels: int, out_channels: int, block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, in_channels, kernel_size=7, stride=1, padding=3, bias=False, groups=in_channels), + block_args.initialization_method, + block_args.use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False), + block_args.initialization_method, + block_args.use_spectral_norm), + NormalizationLayerFactory.resolve_2d(block_args.normalization_layer_factory).create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) + + +def create_separable_downsample_block( + in_channels: int, out_channels: int, is_output_1x1: bool, block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + if is_output_1x1: + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, in_channels, kernel_size=4, stride=2, padding=1, bias=False, groups=in_channels), + block_args.initialization_method, + block_args.use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False), + block_args.initialization_method, + block_args.use_spectral_norm), + block_args.nonlinearity_factory.create()) + else: + return Sequential( + wrap_conv_or_linear_module( + Conv2d(in_channels, in_channels, kernel_size=4, stride=2, padding=1, bias=False, groups=in_channels), + block_args.initialization_method, + block_args.use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False), + block_args.initialization_method, + block_args.use_spectral_norm), + NormalizationLayerFactory.resolve_2d(block_args.normalization_layer_factory) + .create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) + + +def create_separable_upsample_block( + in_channels: int, out_channels: int, block_args: Optional[BlockArgs] = None): + if block_args is None: + block_args = BlockArgs() + return Sequential( + wrap_conv_or_linear_module( + ConvTranspose2d( + in_channels, in_channels, kernel_size=4, stride=2, padding=1, bias=False, groups=in_channels), + block_args.initialization_method, + block_args.use_spectral_norm), + wrap_conv_or_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False), + block_args.initialization_method, + block_args.use_spectral_norm), + NormalizationLayerFactory.resolve_2d(block_args.normalization_layer_factory) + .create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) diff --git a/tha4/src/tha4/nn/siren/__init__.py b/tha4/src/tha4/nn/siren/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/siren/face_morpher/__init__.py b/tha4/src/tha4/nn/siren/face_morpher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00.py b/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00.py new file mode 100644 index 0000000..ace842f --- /dev/null +++ b/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00.py @@ -0,0 +1,59 @@ +from typing import Optional, List + +import torch +from torch import Tensor +from torch.nn import Module +from torch.nn.functional import affine_grid + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.siren.vanilla.siren import SirenArgs, Siren + + +class SirenFaceMorpher00Args: + def __init__(self, + image_size: int, + image_channels: int, + pose_size: int, + siren_args: SirenArgs): + assert siren_args.in_channels == pose_size + 2 + assert siren_args.out_channels == image_channels + assert not siren_args.use_tanh + + self.siren_args = siren_args + self.pose_size = pose_size + self.image_size = image_size + self.image_channels = image_channels + + +class SirenFaceMorpher00(Module): + def __init__(self, args: SirenFaceMorpher00Args): + super().__init__() + self.args = args + self.siren = Siren(self.args.siren_args) + + def forward(self, pose: Tensor, position: Optional[Tensor] = None) -> Tensor: + n, p = pose.shape[0], pose.shape[1] + device = pose.device + + if position is None: + h, w = self.args.image_size, self.args.image_size + identity = torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], device=device).unsqueeze(0) + position = affine_grid(identity, [1, 1, h, w], align_corners=False) \ + .view(1, h * w, 2) + position = torch.transpose(position, dim0=1, dim1=2).view(1, 2, h, w) \ + .repeat(n, 1, 1, 1) + + h, w = position.shape[2], position.shape[3] + pose_image = pose.view(n, p, 1, 1).repeat(1, 1, h, w) + + siren_input = torch.cat([position, pose_image], dim=1) + + return self.siren.forward(siren_input) + + +class SirenFaceMorpher00Factory(ModuleFactory): + def __init__(self, args: SirenFaceMorpher00Args): + self.args = args + + def create(self) -> Module: + return SirenFaceMorpher00(self.args) diff --git a/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00_trainer.py b/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00_trainer.py new file mode 100644 index 0000000..f606c70 --- /dev/null +++ b/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_00_trainer.py @@ -0,0 +1,228 @@ +from typing import Dict, List, Optional, Callable + +import torch +from tha4.shion.base.dataset.lazy_tensor_dataset import LazyTensorDataset +from tha4.shion.base.image_util import extract_pytorch_image_from_filelike +from tha4.shion.base.loss.l1_loss import L1Loss, MaskedL1Loss +from tha4.shion.base.loss.sum_loss import SumLoss +from tha4.shion.base.optimizer_factories import AdamOptimizerFactory +from tha4.shion.core.training.distrib.distributed_trainer import DistributedTrainer +from tha4.dataset.image_poses_and_aother_images_dataset import ImagePosesAndOtherImagesDataset +from tha4.nn.siren.face_morpher.siren_face_morpher_00 import SirenFaceMorpher00Factory, SirenFaceMorpher00Args +from tha4.nn.siren.face_morpher.siren_face_morpher_protocols_00 import SirenFaceMorpherComputationProtocol00, \ + SirenFaceMorpherSampleOutputProtocol00 +from tha4.nn.siren.morpher.siren_morpher_protocols_03 import SirenMorpherTrainingProtocol03 +from tha4.nn.siren.vanilla.siren import SirenArgs +from tha4.poser.poser import Poser +from torch import Tensor + +KEY_MODULE = "module" +KEY_POSER = "poser" + + +def get_poser(): + import tha4.poser.modes.mode_12 + poser = tha4.poser.modes.mode_12.create_poser(torch.device('cpu')) + return poser + + +class SirenFaceMorpher00TrainerArgs: + def __init__(self, + character_file_name: str, + face_mask_file_name: str, + pose_dataset_file_name: str, + num_training_total_examples: int = 1_000_000, + num_training_examples_per_checkpoint: int = 100_000, + num_training_examples_lr_boundaries: Optional[List[int]] = None, + num_training_examples_per_sample_output: Optional[int] = 5_000, + num_training_examples_per_snapshot: int = 10_000, + total_batch_size: int = 8, + training_random_seed: int = 2965603729, + sample_output_random_seed: int = 3522651501, + total_worker: int = 16, + poser_func: Optional[Callable[[], Poser]] = None, + base_learning_rate: float = 1e-4): + assert num_training_total_examples % num_training_examples_per_checkpoint == 0 + + if num_training_examples_lr_boundaries is None: + num_training_examples_lr_boundaries = [ + int(num_training_examples_per_checkpoint * 2), + int(num_training_examples_per_checkpoint * 5), + int(num_training_examples_per_checkpoint * 8), + ] + + for x in num_training_examples_lr_boundaries: + assert x % num_training_examples_per_snapshot == 0 + + if poser_func is None: + poser_func = get_poser + + self.face_mask_file_name = face_mask_file_name + self.base_learning_rate = base_learning_rate + self.poser_func = poser_func + self.total_worker = total_worker + self.num_training_examples_per_snapshot = num_training_examples_per_snapshot + self.num_training_examples_per_sample_output = num_training_examples_per_sample_output + self.sample_output_random_seed = sample_output_random_seed + self.training_random_seed = training_random_seed + self.total_batch_size = total_batch_size + self.num_training_total_examples = num_training_total_examples + self.num_training_examples_per_checkpoint = num_training_examples_per_checkpoint + self.num_training_examples_lr_boundaries = num_training_examples_lr_boundaries + self.pose_dataset_file_name = pose_dataset_file_name + self.character_file_name = character_file_name + + def get_character_image(self): + return extract_pytorch_image_from_filelike( + self.character_file_name, + scale=2.0, + offset=-1.0, + premultiply_alpha=True, + perform_srgb_to_linear=True) + + def get_face_mask_image(self): + loaded_image = extract_pytorch_image_from_filelike( + self.face_mask_file_name, + scale=1.0, + offset=0.0, + premultiply_alpha=True, + perform_srgb_to_linear=True) + output_image = torch.zeros(4, 128, 128) + center_x = 256 + center_y = 128 + 16 + for i in range(4): + output_image[i, :, :] = loaded_image[0, center_y - 64:center_y + 64, center_x - 64:center_x + 64] + return output_image + + def get_training_dataset(self): + return ImagePosesAndOtherImagesDataset( + main_image_func=self.get_character_image, + other_image_funcs=[self.get_face_mask_image], + pose_dataset=LazyTensorDataset(self.pose_dataset_file_name)) + + def get_module_factory(self): + return SirenFaceMorpher00Factory( + SirenFaceMorpher00Args( + image_size=128, + image_channels=4, + pose_size=39, + siren_args=SirenArgs( + in_channels=39 + 2, + out_channels=4, + intermediate_channels=128, + num_sine_layers=8))) + + def transform_pose_to_module_input(self, pose: Tensor): + return pose[:, 0:39] + + def transform_original_image_to_module_input(self, image: Tensor): + center_x = 256 + center_y = 128 + 16 + return image[:, :, center_y - 64:center_y + 64, center_x - 64:center_x + 64] + + def transform_poser_posed_image_to_groundtruth(self, image: Tensor): + center_x = 96 + center_y = 96 + 16 + return image[:, :, center_y - 64:center_y + 64, center_x - 64:center_x + 64] + + def get_training_computation_protocol(self): + return SirenFaceMorpherComputationProtocol00( + transform_pose_to_module_input_func=self.transform_pose_to_module_input, + transform_original_image_to_module_input_func=self.transform_original_image_to_module_input, + transform_poser_posed_image_to_groundtruth_func=self.transform_poser_posed_image_to_groundtruth) + + def get_learning_rate(self, examples_seen_so_far) -> Dict[str, float]: + if examples_seen_so_far < self.num_training_examples_lr_boundaries[0]: + return { + KEY_MODULE: self.base_learning_rate, + } + elif examples_seen_so_far < self.num_training_examples_lr_boundaries[1]: + return { + KEY_MODULE: self.base_learning_rate / 3.0, + } + elif examples_seen_so_far < self.num_training_examples_lr_boundaries[2]: + return { + KEY_MODULE: self.base_learning_rate / 10.0, + } + else: + return { + KEY_MODULE: self.base_learning_rate / 30.0, + } + + def get_optimizer_factories(self): + return { + KEY_MODULE: AdamOptimizerFactory(betas=(0.9, 0.999)), + } + + def get_poser(self): + return self.poser_func() + + def get_training_protocol(self, world_size: int): + total_examples = self.num_training_total_examples + per_checkpoint_examples = self.num_training_examples_per_checkpoint + num_checkpoints = total_examples // per_checkpoint_examples + batch_size = self.total_batch_size // world_size + return SirenMorpherTrainingProtocol03( + check_point_examples=[per_checkpoint_examples * (i + 1) for i in range(num_checkpoints)], + batch_size=batch_size, + learning_rate=self.get_learning_rate, + optimizer_factories=self.get_optimizer_factories(), + random_seed=self.training_random_seed, + poser_func=self.get_poser, + key_module=KEY_MODULE, + key_poser=KEY_POSER) + + def get_sample_output_protocol(self): + return SirenFaceMorpherSampleOutputProtocol00( + num_images=8, + image_size=128, + images_per_row=2, + examples_per_sample_output=self.num_training_examples_per_sample_output, + computation_protocol=self.get_training_computation_protocol(), + poser_func=self.get_poser, + random_seed=self.sample_output_random_seed) + + def get_loss(self): + protocol = self.get_training_computation_protocol() + return SumLoss([ + ( + 'full', + L1Loss( + expected_func=protocol.get_output_func(protocol.keys.groundtruth_posed_image), + actual_func=protocol.get_output_func(protocol.keys.predicted_posed_image), + weight=1.0) + ), + ( + 'eye_mouth', + MaskedL1Loss( + expected_func=protocol.get_output_func(protocol.keys.groundtruth_posed_image), + actual_func=protocol.get_output_func(protocol.keys.predicted_posed_image), + mask_func=protocol.get_output_func(protocol.keys.eye_mouth_mask), + weight=20.0) + ), + ]) + + def create_trainer(self, prefix: str, world_size: int, distrib_backend: str = 'gloo'): + if self.num_training_examples_per_sample_output is not None: + sample_output_protocol = self.get_sample_output_protocol() + else: + sample_output_protocol = None + + return DistributedTrainer( + prefix=prefix, + module_factories={ + KEY_MODULE: self.get_module_factory(), + }, + accumulators={}, + losses={ + KEY_MODULE: self.get_loss(), + }, + training_dataset=self.get_training_dataset(), + validation_dataset=self.get_training_dataset(), + training_protocol=self.get_training_protocol(world_size), + validation_protocol=None, + sample_output_protocol=sample_output_protocol, + pretrained_module_file_names={}, + example_per_snapshot=self.num_training_examples_per_snapshot, + num_data_loader_workers=max(1, self.total_worker // world_size), + distrib_backend=distrib_backend) diff --git a/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_protocols_00.py b/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_protocols_00.py new file mode 100644 index 0000000..83094f0 --- /dev/null +++ b/tha4/src/tha4/nn/siren/face_morpher/siren_face_morpher_protocols_00.py @@ -0,0 +1,220 @@ +import os +from dataclasses import dataclass +from typing import Dict, Any, Optional, Callable + +import PIL.Image +import numpy +import torch +from tha4.shion.base.dataset.util import get_indexed_batch +from tha4.shion.base.image_util import pytorch_rgba_to_numpy_image +from tha4.shion.core.cached_computation import CachedComputationProtocol, ComputationState, \ + ComposableCachedComputationProtocol, batch_indexing_func, add_step +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.poser.general_poser_02 import GeneralPoser02 +from torch import Tensor +from torch.nn import Module +from torch.utils.data import Dataset + +KEY_MODULE = "module" +KEY_POSER = "poser" + + +@dataclass +class SirenMorpherProtocol00Keys: + module: str = KEY_MODULE + module_output: str = "module_output" + + poser: str = KEY_POSER + poser_output: str = "poser_output" + + original_image: str = "original_image" + original_pose: str = "original_pose" + + module_input_image: str = "module_input_image" + module_input_pose: str = "module_input_pose" + + groundtruth_posed_image: str = 'groundtruth_posed_image' + predicted_posed_image: str = 'predicted_posed_image' + + eye_mouth_mask: str = 'eye_mouth_mask' + + +@dataclass +class SirenMorpherProtocol00Indices: + batch_original_image: int = 0 + batch_pose: int = 1 + batch_eye_mouth_mask: int = 2 + poser_posed_image: int = 0 + + +class SirenFaceMorpherComputationProtocol00(ComposableCachedComputationProtocol): + def __init__(self, + transform_pose_to_module_input_func: Callable[[Tensor], Tensor], + transform_original_image_to_module_input_func: Callable[[Tensor], Tensor], + transform_poser_posed_image_to_groundtruth_func: Callable[[Tensor], Tensor], + keys: Optional[SirenMorpherProtocol00Keys] = None, + indices: Optional[SirenMorpherProtocol00Indices] = None): + super().__init__() + + if keys is None: + keys = SirenMorpherProtocol00Keys() + if indices is None: + indices = SirenMorpherProtocol00Indices() + + self.keys = keys + self.indices = indices + self.transform_image_to_module_input_func = transform_original_image_to_module_input_func + self.transform_pose_to_module_input_func = transform_pose_to_module_input_func + self.transform_poser_posed_image_to_groundtruth_func = transform_poser_posed_image_to_groundtruth_func + + self.computation_steps[keys.original_image] = batch_indexing_func(indices.batch_original_image) + self.computation_steps[keys.original_pose] = batch_indexing_func(indices.batch_pose) + + @add_step(self.computation_steps, keys.module_input_pose) + def get_module_input_pose(protocol: CachedComputationProtocol, state: ComputationState): + original_pose = protocol.get_output(keys.original_pose, state) + return transform_pose_to_module_input_func(original_pose) + + @add_step(self.computation_steps, keys.module_input_image) + def get_module_input_image(protocol: CachedComputationProtocol, state: ComputationState): + original_image = protocol.get_output(keys.original_image, state) + return transform_original_image_to_module_input_func(original_image) + + @add_step(self.computation_steps, keys.poser_output) + def get_poser_output(protocol: CachedComputationProtocol, state: ComputationState): + with torch.no_grad(): + poser = state.modules[keys.poser] + pose = protocol.get_output(keys.original_pose, state) + image = protocol.get_output(keys.original_image, state) + return poser.get_posing_outputs(image, pose) + + @add_step(self.computation_steps, keys.groundtruth_posed_image) + def get_groundtruth_posed_image(protocol: CachedComputationProtocol, state: ComputationState): + poser_output = protocol.get_output(keys.poser_output, state) + poser_posed_image = poser_output[indices.poser_posed_image] + return transform_poser_posed_image_to_groundtruth_func(poser_posed_image) + + @add_step(self.computation_steps, keys.module_output) + def get_module_output(protocol: CachedComputationProtocol, state: ComputationState): + module_input_pose = protocol.get_output(keys.module_input_pose, state) + module = state.modules[keys.module] + return module.forward(module_input_pose) + + @add_step(self.computation_steps, keys.predicted_posed_image) + def get_predicted_image(protocol: CachedComputationProtocol, state: ComputationState): + return protocol.get_output(keys.module_output, state) + + self.computation_steps[keys.eye_mouth_mask] = batch_indexing_func(indices.batch_eye_mouth_mask) + + +class SirenFaceMorpherSampleOutputProtocol00(SampleOutputProtocol): + def __init__(self, + num_images: int, + image_size: int, + images_per_row: int, + examples_per_sample_output: int, + computation_protocol: SirenFaceMorpherComputationProtocol00, + poser_func: Callable[[], GeneralPoser02], + random_seed: int = 54859395058, + batch_size: Optional[int] = None): + if batch_size is None: + batch_size = num_images + + self.batch_size = batch_size + self.poser_func = poser_func + self.random_seed = random_seed + self.examples_per_sample_output = examples_per_sample_output + self.images_per_row = images_per_row + self.image_size = image_size + self.num_images = num_images + self.computation_protocol = computation_protocol + + def get_examples_per_sample_output(self) -> int: + return self.examples_per_sample_output + + def get_random_seed(self) -> int: + return self.random_seed + + def get_sample_output_data(self, validation_dataset: Dataset, device: torch.device) -> dict: + example_indices = torch.randint(0, len(validation_dataset), (self.num_images,)) + example_indices = [example_indices[i].item() for i in range(self.num_images)] + batch = get_indexed_batch(validation_dataset, example_indices, device) + poser = self.poser_func() + poser.to(device) + with torch.no_grad(): + ground_truth = poser.pose( + batch[self.computation_protocol.indices.batch_original_image], + batch[self.computation_protocol.indices.batch_pose]) + return { + 'batch': batch, + 'ground_truth': ground_truth + } + + def save_sample_output_data(self, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + sample_output_data: Any, + prefix: str, + examples_seen_so_far: int, + device: torch.device): + batch = sample_output_data['batch'] + ground_truth = sample_output_data['ground_truth'] + ground_truth = self.computation_protocol.transform_poser_posed_image_to_groundtruth_func(ground_truth) + + module = modules[self.computation_protocol.keys.module] + module.train(False) + + if self.batch_size == self.num_images: + with torch.no_grad(): + state = ComputationState( + modules=modules, + accumulated_modules=accumulated_modules, + batch=batch, + outputs={}) + poser_output_images = self.computation_protocol.get_output( + self.computation_protocol.keys.predicted_posed_image, state) + else: + poser_output_images_list = [] + start = 0 + while start < self.num_images: + end = start + self.batch_size + end = min(self.num_images, end) + minibatch = [batch[i][start:end] for i in range(len(batch))] + state = ComputationState( + modules=modules, + accumulated_modules=accumulated_modules, + batch=minibatch, + outputs={}) + with torch.no_grad(): + poser_output_images = self.computation_protocol.get_output( + self.computation_protocol.keys.predicted_posed_image, state) + poser_output_images_list.append(poser_output_images) + start = end + poser_output_images = torch.cat(poser_output_images_list, dim=0) + + num_rows = self.num_images // self.images_per_row + if self.num_images % self.images_per_row > 0: + num_rows += 1 + num_cols = 2 * self.images_per_row + + image_channels = 4 + output_image = numpy.zeros([self.image_size * num_rows, self.image_size * num_cols, image_channels]) + + for image_index in range(self.num_images): + row = image_index // self.images_per_row + start_row = row * self.image_size + + col = 2 * (image_index % self.images_per_row) + start_col = col * self.image_size + output_image[start_row:start_row + self.image_size, start_col:start_col + self.image_size, :] \ + = pytorch_rgba_to_numpy_image(ground_truth[image_index].detach().cpu()) + + start_col += self.image_size + output_image[start_row:start_row + self.image_size, start_col:start_col + self.image_size, :] \ + = pytorch_rgba_to_numpy_image(poser_output_images[image_index].detach().cpu()) + + file_name = "%s/sample_output_%010d.png" % (prefix, examples_seen_so_far) + os.makedirs(os.path.dirname(file_name), exist_ok=True) + pil_image = PIL.Image.fromarray(numpy.uint8(numpy.rint(output_image * 255.0)), mode='RGBA') + pil_image.save(file_name) + print("Saved %s" % file_name) diff --git a/tha4/src/tha4/nn/siren/morpher/__init__.py b/tha4/src/tha4/nn/siren/morpher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/siren/morpher/siren_morpher_03.py b/tha4/src/tha4/nn/siren/morpher/siren_morpher_03.py new file mode 100644 index 0000000..a4d18ff --- /dev/null +++ b/tha4/src/tha4/nn/siren/morpher/siren_morpher_03.py @@ -0,0 +1,153 @@ +from typing import List, Optional, Callable + +import torch +from torch import Tensor +from torch.nn import Module, ModuleList, Sequential, Conv2d +from torch.nn.functional import affine_grid, interpolate + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.nn00.initialization_funcs import HeInitialization +from tha4.nn.image_processing_util import GridChangeApplier +from tha4.nn.siren.vanilla.siren import SineLinearLayer + + +class SirenMorpherLevelArgs: + def __init__(self, + image_size: int, + intermediate_channels: int, + num_sine_layers: int): + assert num_sine_layers >= 2 + self.image_size = image_size + self.num_sine_layers = num_sine_layers + self.intermediate_channels = intermediate_channels + + +class SirenMorpher03Args: + def __init__(self, + image_size: int, + image_channels: int, + pose_size: int, + level_args: List[SirenMorpherLevelArgs], + init_func: Optional[Callable[[Module], Module]] = None): + assert len(level_args) >= 2 + if init_func is None: + init_func = HeInitialization() + self.image_size = image_size + self.init_func = init_func + self.level_args = level_args + self.pose_size = pose_size + self.image_channels = image_channels + + +class SirenMorpher03(Module): + def __init__(self, args: SirenMorpher03Args): + super().__init__() + self.args = args + + self.siren_layers = ModuleList() + + for i in range(len(args.level_args)): + level_args = args.level_args[i] + + layers = [] + + if i == 0: + layers.append(SineLinearLayer( + in_channels=args.pose_size + 2, + out_channels=level_args.intermediate_channels, + is_first=True)) + else: + layers.append(SineLinearLayer( + in_channels=level_args.intermediate_channels + args.pose_size + 2, + out_channels=level_args.intermediate_channels, + is_first=False)) + + for j in range(1, level_args.num_sine_layers - 1): + layers.append(SineLinearLayer( + in_channels=level_args.intermediate_channels, + out_channels=level_args.intermediate_channels, + is_first=False)) + + if i == len(args.level_args) - 1: + out_channels = level_args.intermediate_channels + else: + out_channels = args.level_args[i + 1].intermediate_channels + layers.append(SineLinearLayer( + in_channels=level_args.intermediate_channels, + out_channels=out_channels, + is_first=False)) + + self.siren_layers.append(Sequential(*layers)) + + self.last_linear = args.init_func(Conv2d( + args.level_args[-1].intermediate_channels, + args.image_channels + 2 + 1, + kernel_size=1, + stride=1, + padding=0, + bias=True)) + + self.grid_change_applier = GridChangeApplier() + + def get_position_grid(self, n: int, image_size: int, device: torch.device): + h, w = image_size, image_size + identity = torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], device=device).unsqueeze(0) + position = affine_grid(identity, [1, 1, h, w], align_corners=False) \ + .view(1, h * w, 2) + position = torch.transpose(position, dim0=1, dim1=2).view(1, 2, h, w) \ + .repeat(n, 1, 1, 1) + return position + + def get_pose_image(self, pose: Tensor, image_size: int): + n, p = pose.shape[0], pose.shape[1] + h, w = image_size, image_size + pose_image = pose.view(n, p, 1, 1).repeat(1, 1, h, w) + return pose_image + + def forward(self, image: Tensor, pose: Tensor) -> List[Tensor]: + n = pose.shape[0] + device = pose.device + + x = None + for i in range(len(self.args.level_args)): + args = self.args.level_args[i] + position_and_pose = torch.cat([ + self.get_position_grid(n, args.image_size, device), + self.get_pose_image(pose, args.image_size) + ], dim=1) + if i == 0: + x = self.siren_layers[i].forward(position_and_pose) + else: + x = interpolate(x, size=(args.image_size, args.image_size), mode='bilinear') + x = torch.cat([x, position_and_pose], dim=1) + x = self.siren_layers[i].forward(x) + + siren_output = self.last_linear(x) + + grid_change = siren_output[:, 0:2, :, :] + alpha = siren_output[:, 2:3, :, :] + color_change = siren_output[:, 3:, :, :] + warped_image = self.grid_change_applier.apply(grid_change, image, align_corners=False) + blended_image = (1 - alpha) * warped_image + alpha * color_change + + return [ + blended_image, + alpha, + color_change, + warped_image, + grid_change + ] + + INDEX_BLENDED_IMAGE = 0 + INDEX_ALPHA = 1 + INDEX_COLOR_CHANGE = 2 + INDEX_WARPED_IMAGE = 3 + INDEX_GRID_CHANGE = 4 + + +class SirenMorpher03Factory(ModuleFactory): + def __init__(self, args: SirenMorpher03Args): + self.args = args + + def create(self): + return SirenMorpher03(self.args) diff --git a/tha4/src/tha4/nn/siren/morpher/siren_morpher_03_trainer.py b/tha4/src/tha4/nn/siren/morpher/siren_morpher_03_trainer.py new file mode 100644 index 0000000..5100105 --- /dev/null +++ b/tha4/src/tha4/nn/siren/morpher/siren_morpher_03_trainer.py @@ -0,0 +1,276 @@ +from enum import Enum +from typing import Dict, List, Optional, Callable + +import torch +from tha4.shion.base.dataset.lazy_tensor_dataset import LazyTensorDataset +from tha4.shion.base.image_util import extract_pytorch_image_from_filelike +from tha4.shion.base.loss.l1_loss import L1Loss +from tha4.shion.base.loss.sum_loss import SumLoss +from tha4.shion.base.loss.time_dependently_weighted_loss import TimeDependentlyWeightedLoss +from tha4.shion.base.optimizer_factories import AdamOptimizerFactory +from tha4.shion.core.training.distrib.distributed_trainer import DistributedTrainer +from tha4.dataset.image_poses_and_aother_images_dataset import ImagePosesAndOtherImagesDataset +from tha4.nn.siren.morpher.siren_morpher_03 import SirenMorpherLevelArgs, SirenMorpher03Factory, SirenMorpher03Args +from tha4.nn.siren.morpher.siren_morpher_protocols_03 import SirenMorpherComputationProtocol03, \ + SirenMorpherProtocol03Indices, KEY_MODULE, KEY_POSER, KEY_EXAMPLES_SEEN_SO_FAR, SirenMorpherTrainingProtocol03, \ + SirenMorpherSampleOutputProtocol +from tha4.poser.poser import Poser + + +def get_poser(): + import tha4.poser.modes.mode_07 + poser = tha4.poser.modes.mode_07.create_poser(torch.device('cpu')) + return poser + + +class LossTerm(Enum): + full_blended = 1 + full_warped = 2 + full_grid_change = 3 + full_color_change = 4 + + def get_loss(self, protocol: SirenMorpherComputationProtocol03): + if self == LossTerm.full_blended: + return L1Loss( + expected_func=protocol.get_output_func(protocol.keys.groundtruth_posed_image), + actual_func=protocol.get_output_func(protocol.keys.predicted_posed_image)) + elif self == LossTerm.full_warped: + return L1Loss( + expected_func=protocol.get_output_func(protocol.keys.groundtruth_warped_image), + actual_func=protocol.get_output_func(protocol.keys.predicted_warped_image)) + elif self == LossTerm.full_grid_change: + return L1Loss( + expected_func=protocol.get_output_func(protocol.keys.groundtruth_grid_change), + actual_func=protocol.get_output_func(protocol.keys.predicted_grid_change)) + elif self == LossTerm.full_color_change: + return L1Loss( + expected_func=protocol.get_output_func(protocol.keys.groundtruth_posed_image), + actual_func=protocol.get_output_func(protocol.keys.predicted_color_change)) + else: + raise RuntimeError(f"Unsupported loss term {self}") + + +class LossWeights: + def __init__(self, weights: Optional[Dict[LossTerm, float]] = None): + self.weights = {} + for term in LossTerm: + self.weights[term] = 0.0 + if weights is not None: + for term in LossTerm: + if term in weights: + self.weights[term] = weights[term] + + +class TrainingPhase: + def __init__(self, + num_examples_upper_bound: int, + learning_rate: float, + loss_weights: LossWeights): + self.loss_weights = loss_weights + self.learning_rate = learning_rate + self.num_examples_upper_bound = num_examples_upper_bound + + +class LearningRateFunc: + def __init__(self, phases: List[TrainingPhase], keys: List[str]): + self.phases = phases + self.keys = keys + + def make_learning_rate_dict(self, keys: List[str], value: float): + output = {} + for key in keys: + output[key] = value + return output + + def __call__(self, examples_seen_so_far: int) -> Dict[str, float]: + for i in range(len(self.phases) - 1): + if examples_seen_so_far < self.phases[i].num_examples_upper_bound: + return self.make_learning_rate_dict(self.keys, self.phases[i].learning_rate) + return self.make_learning_rate_dict(self.keys, self.phases[-1].learning_rate) + + +class LossWeightFunc: + def __init__(self, phases: List[TrainingPhase], term: LossTerm): + self.term = term + self.phases = phases + + def __call__(self, examples_seen_so_far: int) -> float: + for i in range(len(self.phases) - 1): + if examples_seen_so_far < self.phases[i].num_examples_upper_bound: + return self.phases[i].loss_weights.weights[self.term] + return self.phases[-1].loss_weights.weights[self.term] + + +class TrainingPhases: + def __init__(self, phases: List[TrainingPhase]): + assert len(phases) > 0 + for i in range(1, len(phases)): + assert phases[i - 1].num_examples_upper_bound < phases[i].num_examples_upper_bound + + self.phases = phases + + def make_learning_rate_dict(self, keys: List[str], value: float): + output = {} + for key in keys: + output[key] = value + return output + + def get_learning_rate_func(self, keys: List[str]): + return LearningRateFunc(self.phases, keys) + + def get_loss_weight_func(self, term: LossTerm) -> Callable[[int], float]: + return LossWeightFunc(self.phases, term) + + +class SirenMorpher03TrainerArgs: + def __init__(self, + character_file_name: str, + pose_dataset_file_name: str, + training_phases: TrainingPhases, + num_training_examples_per_checkpoint: int = 100_000, + num_training_examples_per_sample_output: Optional[int] = 10_000, + num_training_examples_per_snapshot: int = 10_000, + total_batch_size: int = 8, + training_random_seed: int = 2965603729, + sample_output_random_seed: int = 3522651501, + total_worker: int = 8, + poser_func: Optional[Callable[[], Poser]] = None, + sample_output_batch_size: Optional[int] = None, + pretrained_module_file_name: Optional[str] = None): + for phase in training_phases.phases: + assert phase.num_examples_upper_bound % num_training_examples_per_checkpoint == 0 + + if poser_func is None: + poser_func = get_poser + + self.training_phases = training_phases + self.pretrained_module_file_name = pretrained_module_file_name + self.sample_output_batch_size = sample_output_batch_size + self.poser_func = poser_func + self.total_worker = total_worker + self.num_training_examples_per_snapshot = num_training_examples_per_snapshot + self.num_training_examples_per_sample_output = num_training_examples_per_sample_output + self.sample_output_random_seed = sample_output_random_seed + self.training_random_seed = training_random_seed + self.total_batch_size = total_batch_size + self.num_training_examples_per_checkpoint = num_training_examples_per_checkpoint + self.pose_dataset_file_name = pose_dataset_file_name + self.character_file_name = character_file_name + + def get_character_image(self): + return extract_pytorch_image_from_filelike( + self.character_file_name, + scale=2.0, + offset=-1.0, + premultiply_alpha=True, + perform_srgb_to_linear=True) + + def get_training_dataset(self): + return ImagePosesAndOtherImagesDataset( + main_image_func=self.get_character_image, + pose_dataset=LazyTensorDataset(self.pose_dataset_file_name), + other_image_funcs=[]) + + def get_module_factory(self): + return SirenMorpher03Factory( + SirenMorpher03Args( + image_size=512, + image_channels=4, + pose_size=45, + level_args=[ + SirenMorpherLevelArgs( + image_size=128, + intermediate_channels=360, + num_sine_layers=3), + SirenMorpherLevelArgs( + image_size=256, + intermediate_channels=180, + num_sine_layers=3), + SirenMorpherLevelArgs( + image_size=512, + intermediate_channels=90, + num_sine_layers=3), + ])) + + def get_training_computation_protocol(self): + return SirenMorpherComputationProtocol03( + indices=SirenMorpherProtocol03Indices( + batch_image=0, + batch_pose=1, + batch_face_mask=2)) + + def get_optimizer_factories(self): + return { + KEY_MODULE: AdamOptimizerFactory(betas=(0.9, 0.999)), + } + + def get_poser(self): + return self.poser_func() + + def get_training_protocol(self, world_size: int): + total_examples = self.training_phases.phases[-1].num_examples_upper_bound + per_checkpoint_examples = self.num_training_examples_per_checkpoint + num_checkpoints = total_examples // per_checkpoint_examples + batch_size = self.total_batch_size // world_size + return SirenMorpherTrainingProtocol03( + check_point_examples=[per_checkpoint_examples * (i + 1) for i in range(num_checkpoints)], + batch_size=batch_size, + learning_rate=self.training_phases.get_learning_rate_func([KEY_MODULE]), + optimizer_factories=self.get_optimizer_factories(), + random_seed=self.training_random_seed, + poser_func=self.get_poser, + key_module=KEY_MODULE, + key_poser=KEY_POSER) + + def get_sample_output_protocol(self): + return SirenMorpherSampleOutputProtocol( + num_images=4, + image_size=512, + examples_per_sample_output=self.num_training_examples_per_sample_output, + computation_protocol=self.get_training_computation_protocol(), + poser_func=self.get_poser, + random_seed=self.sample_output_random_seed, + batch_size=self.sample_output_batch_size, + batch_pose_index=1, + batch_image_index=0) + + def get_loss(self): + protocol = self.get_training_computation_protocol() + losses = [] + for term in LossTerm: + base_loss = term.get_loss(protocol) + loss = TimeDependentlyWeightedLoss( + base_loss, + examples_seen_so_far_func=lambda state: state.outputs[KEY_EXAMPLES_SEEN_SO_FAR], + weight_func=self.training_phases.get_loss_weight_func(term)) + losses.append((term.name, loss)) + return SumLoss(losses) + + def create_trainer(self, prefix: str, world_size: int, distrib_backend: str = 'gloo'): + if self.num_training_examples_per_sample_output is not None: + sample_output_protocol = self.get_sample_output_protocol() + else: + sample_output_protocol = None + + pretrained_module_file_names = {} + if self.pretrained_module_file_name is not None: + pretrained_module_file_names[KEY_MODULE] = self.pretrained_module_file_name + + return DistributedTrainer( + prefix=prefix, + module_factories={ + KEY_MODULE: self.get_module_factory(), + }, + accumulators={}, + losses={ + KEY_MODULE: self.get_loss(), + }, + training_dataset=self.get_training_dataset(), + validation_dataset=self.get_training_dataset(), + training_protocol=self.get_training_protocol(world_size), + validation_protocol=None, + sample_output_protocol=sample_output_protocol, + pretrained_module_file_names=pretrained_module_file_names, + example_per_snapshot=self.num_training_examples_per_snapshot, + num_data_loader_workers=max(1, self.total_worker // world_size), + distrib_backend=distrib_backend) diff --git a/tha4/src/tha4/nn/siren/morpher/siren_morpher_protocols_03.py b/tha4/src/tha4/nn/siren/morpher/siren_morpher_protocols_03.py new file mode 100644 index 0000000..c366e85 --- /dev/null +++ b/tha4/src/tha4/nn/siren/morpher/siren_morpher_protocols_03.py @@ -0,0 +1,352 @@ +from dataclasses import dataclass +from typing import Optional, List, Callable, Dict, Any + +import torch +from tha4.shion.base.dataset.util import get_indexed_batch + +from tha4.shion.core.cached_computation import output_array_indexing_func, add_step, ComputationState, \ + CachedComputationProtocol, ComposableCachedComputationProtocol, batch_indexing_func, proxy_func +from tha4.shion.core.loss import Loss +from tha4.shion.core.optimizer_factory import OptimizerFactory +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.shion.core.training.training_protocol import AbstractTrainingProtocol +from tha4.nn.image_processing_util import GridChangeApplier +from tha4.nn.siren.morpher.siren_morpher_03 import SirenMorpher03 +from tha4.poser.general_poser_02 import GeneralPoser02 +from tha4.sampleoutput.sample_image_creator import SampleImageSpec, ImageSource, ImageType, SampleImageSaver +from torch.nn import Module +from torch.optim import Optimizer +from torch.utils.data import Dataset + +KEY_MODULE = "module" +KEY_POSER = "poser" +KEY_EXAMPLES_SEEN_SO_FAR = "examples_seen_so_far" + + +@dataclass +class SirenMorpherProtocol03Keys: + module: str = KEY_MODULE + module_output: str = "module_output" + + poser: str = KEY_POSER + poser_output: str = "poser_output" + + image: str = "image" + pose: str = "pose" + face_mask: str = 'face_mask' + + groundtruth_posed_image: str = 'groundtruth_posed_image' + groundtruth_grid_change: str = 'groundtruth_grid_change' + groundtruth_posed_face_mask: str = 'groundtruth_posed_face_mask' + predicted_posed_image: str = 'predicted_posed_image' + + module_input_image: str = "module_input_image" + + predicted_grid_change: str = "predicted_grid_change" + predicted_color_change: str = "predicted_color_change" + predicted_warped_image: str = "predicted_warped_image" + predicted_alpha: str = "predicted_alpha" + + groundtruth_alpha: str = "groundtruth_alpha" + groundtruth_warped_image: str = "groundtruth_warped_image" + + zero: str = "zero" + + +@dataclass +class SirenMorpherProtocol03Indices: + batch_image: int = 0 + batch_face_mask: int = 1 + batch_pose: int = 2 + poser_posed_image: int = 0 + poser_grid_change: int = 3 + + poser_output_module_input_image_index: int = 5 + poser_alpha: int = 1 + poser_warped_image: int = 2 + + module_blended_image: int = SirenMorpher03.INDEX_BLENDED_IMAGE + module_grid_change: int = SirenMorpher03.INDEX_GRID_CHANGE + module_color_change: int = SirenMorpher03.INDEX_COLOR_CHANGE + module_warped_image: int = SirenMorpher03.INDEX_WARPED_IMAGE + module_alpha: int = SirenMorpher03.INDEX_ALPHA + + +class SirenMorpherComputationProtocol03(ComposableCachedComputationProtocol): + def __init__(self, + keys: Optional[SirenMorpherProtocol03Keys] = None, + indices: Optional[SirenMorpherProtocol03Indices] = None): + super().__init__() + + if keys is None: + keys = SirenMorpherProtocol03Keys() + if indices is None: + indices = SirenMorpherProtocol03Indices() + + self.keys = keys + self.indices = indices + + self.computation_steps[keys.image] = batch_indexing_func(indices.batch_image) + self.computation_steps[keys.pose] = batch_indexing_func(indices.batch_pose) + self.computation_steps[keys.face_mask] = batch_indexing_func(indices.batch_face_mask) + self.grid_change_applier = GridChangeApplier() + + @add_step(self.computation_steps, keys.module_output) + def get_module_output(protocol: CachedComputationProtocol, state: ComputationState): + pose = protocol.get_output(keys.pose, state) + module = state.modules[self.keys.module] + return module.forward(pose) + + self.computation_steps[keys.predicted_posed_image] = proxy_func(keys.module_output) + + @add_step(self.computation_steps, keys.poser_output) + def get_poser_output(protocol: CachedComputationProtocol, state: ComputationState): + with torch.no_grad(): + poser = state.modules[keys.poser] + pose = protocol.get_output(keys.pose, state) + image = protocol.get_output(keys.image, state) + return poser.get_posing_outputs(image, pose) + + @add_step(self.computation_steps, keys.groundtruth_posed_image) + def get_groundtruth_posed_image(protocol: CachedComputationProtocol, state: ComputationState): + return protocol.get_output(keys.poser_output, state)[indices.poser_posed_image] + + @add_step(self.computation_steps, keys.groundtruth_grid_change) + def get_groundtruth_posed_image(protocol: CachedComputationProtocol, state: ComputationState): + return protocol.get_output(keys.poser_output, state)[indices.poser_grid_change] + + @add_step(self.computation_steps, keys.groundtruth_posed_face_mask) + def get_groundtruth_posed_face_mask(protocol: CachedComputationProtocol, state: ComputationState): + face_mask = protocol.get_output(keys.face_mask, state) + groundtruth_grid_change = protocol.get_output(keys.groundtruth_grid_change, state) + with torch.no_grad(): + return self.grid_change_applier.apply(groundtruth_grid_change, face_mask) + + @add_step(self.computation_steps, keys.module_input_image) + def get_module_input_image(protocol: CachedComputationProtocol, state: ComputationState): + poser_output = protocol.get_output(keys.poser_output, state) + return poser_output[indices.poser_output_module_input_image_index] + + @add_step(self.computation_steps, keys.module_output) + def get_module_output(protocol: CachedComputationProtocol, state: ComputationState): + image = protocol.get_output(keys.module_input_image, state) + pose = protocol.get_output(keys.pose, state) + module = state.modules[self.keys.module] + return module.forward(image, pose) + + self.computation_steps[keys.predicted_posed_image] = output_array_indexing_func( + keys.module_output, indices.module_blended_image) + self.computation_steps[keys.predicted_grid_change] = output_array_indexing_func( + keys.module_output, indices.module_grid_change) + self.computation_steps[keys.predicted_color_change] = output_array_indexing_func( + keys.module_output, indices.module_color_change) + self.computation_steps[keys.predicted_warped_image] = output_array_indexing_func( + keys.module_output, indices.module_warped_image) + self.computation_steps[keys.predicted_alpha] = output_array_indexing_func( + keys.module_output, indices.module_alpha) + + self.computation_steps[keys.groundtruth_alpha] = output_array_indexing_func( + keys.poser_output, indices.poser_alpha) + self.computation_steps[keys.groundtruth_warped_image] = output_array_indexing_func( + keys.poser_output, indices.poser_warped_image) + + @add_step(self.computation_steps, keys.zero) + def get_zero(protocol: CachedComputationProtocol, state: ComputationState): + pose = protocol.get_output(keys.pose, state) + device = pose.device + return torch.zeros(1, device=device) + + +class SirenMorpherTrainingProtocol03(AbstractTrainingProtocol): + def __init__(self, + check_point_examples: List[int], + batch_size: int, + learning_rate: Callable[[int], Dict[str, float]], + optimizer_factories: Dict[str, OptimizerFactory], + random_seed: int, + poser_func: Callable[[], GeneralPoser02], + key_module: str, + key_poser: str = KEY_POSER, + key_examples_seen_so_far: str = KEY_EXAMPLES_SEEN_SO_FAR): + super().__init__(check_point_examples, batch_size, learning_rate, optimizer_factories, random_seed) + self.key_examples_seen_so_far = key_examples_seen_so_far + self.key_poser = key_poser + self.key_module = key_module + self.poser_func = poser_func + self.poser = None + + def run_training_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer], + losses: Dict[str, Loss], + create_log_func: Optional[Callable[[str, int], Callable[[str, float], None]]], + device: torch.device): + if self.poser is None: + self.poser = self.poser_func() + self.poser.to(device) + + module = modules[self.key_module] + module.train(True) + module_optimizer = optimizers[self.key_module] + module_optimizer.zero_grad(set_to_none=True) + + loss = losses[self.key_module] + if create_log_func is not None: + log_func = create_log_func(f"training_{self.key_module}", examples_seen_so_far) + else: + log_func = None + state = ComputationState( + modules={ + **modules, + self.key_poser: self.poser, + }, + accumulated_modules=accumulated_modules, + batch=batch, + outputs={ + self.key_examples_seen_so_far: examples_seen_so_far, + }) + loss_value = loss.compute(state, log_func) + loss_value.backward() + module_optimizer.step() + + +class SirenMorpherSampleOutputProtocol(SampleOutputProtocol): + def __init__(self, + num_images: int, + image_size: int, + examples_per_sample_output: int, + computation_protocol, + poser_func: Callable[[], GeneralPoser02], + random_seed: int = 54859395058, + batch_image_index: int = 0, + batch_pose_index: int = 2, + batch_size: Optional[int] = None, + sample_image_specs: Optional[List[SampleImageSpec]] = None, + cell_size: Optional[int] = None): + if batch_size is None: + batch_size = num_images + + if sample_image_specs is None: + sample_image_specs = [ + SampleImageSpec( + ImageSource.BATCH, + computation_protocol.indices.poser_posed_image, + ImageType.COLOR), + SampleImageSpec( + ImageSource.OUTPUT, + computation_protocol.indices.module_blended_image, + ImageType.COLOR), + SampleImageSpec( + ImageSource.OUTPUT, + computation_protocol.indices.module_alpha, + ImageType.ALPHA), + SampleImageSpec( + ImageSource.OUTPUT, + computation_protocol.indices.module_color_change, + ImageType.COLOR), + SampleImageSpec( + ImageSource.OUTPUT, + computation_protocol.indices.module_warped_image, + ImageType.COLOR), + SampleImageSpec( + ImageSource.BATCH, + computation_protocol.indices.poser_grid_change, + ImageType.GRID_CHANGE), + SampleImageSpec( + ImageSource.OUTPUT, + computation_protocol.indices.module_grid_change, + ImageType.GRID_CHANGE), + ] + + if cell_size is None: + cell_size = image_size + + self.batch_size = batch_size + self.batch_pose_index = batch_pose_index + self.batch_image_index = batch_image_index + self.poser_func = poser_func + self.random_seed = random_seed + self.examples_per_sample_output = examples_per_sample_output + self.image_size = image_size + self.num_images = num_images + self.computation_protocol = computation_protocol + self.cell_size = cell_size + + self.sample_image_saver = SampleImageSaver(image_size, cell_size, 4, sample_image_specs) + + def get_examples_per_sample_output(self) -> int: + return self.examples_per_sample_output + + def get_random_seed(self) -> int: + return self.random_seed + + def get_sample_output_data(self, validation_dataset: Dataset, device: torch.device) -> dict: + example_indices = torch.randint(0, len(validation_dataset), (self.num_images,)) + example_indices = [example_indices[i].item() for i in range(self.num_images)] + batch = get_indexed_batch(validation_dataset, example_indices, device) + poser = self.poser_func() + poser.to(device) + with torch.no_grad(): + ground_truth = poser.get_posing_outputs(batch[self.batch_image_index], batch[self.batch_pose_index]) + return { + 'batch': batch, + 'ground_truth': ground_truth + } + + def save_sample_output_data(self, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + sample_output_data: Any, + prefix: str, + examples_seen_so_far: int, + device: torch.device): + batch = sample_output_data['batch'] + ground_truth = sample_output_data['ground_truth'] + + module = modules[self.computation_protocol.keys.module] + module.train(False) + if self.batch_size == self.num_images: + with torch.no_grad(): + state = ComputationState( + modules=modules, + accumulated_modules=accumulated_modules, + batch=batch, + outputs={ + self.computation_protocol.keys.poser_output: ground_truth, + }) + module_outputs = self.computation_protocol.get_output( + self.computation_protocol.keys.module_output, state) + else: + module_outputs_list = [] + start = 0 + while start < self.num_images: + end = start + self.batch_size + end = min(self.num_images, end) + minibatch = [batch[i][start:end] for i in range(len(batch))] + ground_truth_batch = [ground_truth[i][start:end] for i in range(len(ground_truth))] + state = ComputationState( + modules=modules, + accumulated_modules=accumulated_modules, + batch=minibatch, + outputs={ + self.computation_protocol.keys.poser_output: ground_truth_batch + }) + with torch.no_grad(): + module_outputs = self.computation_protocol.get_output( + self.computation_protocol.keys.module_output, state) + module_outputs_list.append(module_outputs) + start = end + + module_outputs = [] + for i in range(len(module_outputs_list[0])): + tensor_list = [] + for j in range(len(module_outputs_list)): + tensor_list.append(module_outputs_list[j][i]) + module_output = torch.cat(tensor_list, dim=0) + module_outputs.append(module_output) + + self.sample_image_saver.save_sample_output_data(ground_truth, module_outputs, prefix, examples_seen_so_far) diff --git a/tha4/src/tha4/nn/siren/vanilla/__init__.py b/tha4/src/tha4/nn/siren/vanilla/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/siren/vanilla/siren.py b/tha4/src/tha4/nn/siren/vanilla/siren.py new file mode 100644 index 0000000..b951e11 --- /dev/null +++ b/tha4/src/tha4/nn/siren/vanilla/siren.py @@ -0,0 +1,100 @@ +import math +from typing import Callable, Optional, List + +import torch +from torch import Tensor +from torch.nn import Module, Conv2d, ModuleList + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.nn00.initialization_funcs import HeInitialization + + +class SineLinearLayer(Module): + def __init__(self, + in_channels: int, + out_channels: int, + is_first=False, + omega_0=30.0): + super().__init__() + self.out_channels = out_channels + self.in_channels = in_channels + self.omega_0 = omega_0 + self.is_first = is_first + self.linear = Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=True) + with torch.no_grad(): + if self.is_first: + self.linear.weight.uniform_(-1 / in_channels, 1.0 / in_channels) + else: + self.linear.weight.uniform_( + -math.sqrt(6.0 / in_channels) / self.omega_0, + math.sqrt(6.0 / in_channels) / self.omega_0) + + def forward(self, x: Tensor): + return torch.sin(self.omega_0 * self.linear(x)) + + +class SirenArgs: + def __init__( + self, + in_channels: int, + out_channels: int, + intermediate_channels: int, + num_sine_layers: int, + use_tanh: bool = False, + init_func: Optional[Callable[[Module], Module]] = None): + if init_func is None: + init_func = HeInitialization() + self.init_func = init_func + self.use_tanh = use_tanh + assert num_sine_layers >= 1 + self.intermediate_channels = intermediate_channels + self.num_sine_layers = num_sine_layers + self.out_channels = out_channels + self.in_channels = in_channels + + +class Siren(Module): + def __init__(self, args: SirenArgs): + super().__init__() + self.args = args + self.sine_layers = ModuleList() + self.sine_layers.append( + SineLinearLayer( + in_channels=args.in_channels, out_channels=args.intermediate_channels, is_first=True)) + for i in range(args.num_sine_layers - 1): + self.sine_layers.append( + SineLinearLayer( + in_channels=args.intermediate_channels, + out_channels=args.intermediate_channels, + is_first=False)) + self.last_linear = args.init_func(Conv2d( + args.intermediate_channels, + args.out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=True)) + + def forward(self, x: Tensor) -> Tensor: + for i in range(self.args.num_sine_layers): + x = self.sine_layers[i].forward(x) + x = self.last_linear(x) + if self.args.use_tanh: + return torch.tanh(x) + else: + return x + + +class SirenFactory(ModuleFactory): + def __init__(self, args: SirenArgs): + super().__init__() + self.args = args + + def create(self) -> Module: + return Siren(self.args) diff --git a/tha4/src/tha4/nn/spectral_norm.py b/tha4/src/tha4/nn/spectral_norm.py new file mode 100644 index 0000000..4f08dd4 --- /dev/null +++ b/tha4/src/tha4/nn/spectral_norm.py @@ -0,0 +1,9 @@ +from torch.nn import Module +from torch.nn.utils import spectral_norm + + +def apply_spectral_norm(module: Module, use_spectrial_norm: bool = False) -> Module: + if use_spectrial_norm: + return spectral_norm(module) + else: + return module diff --git a/tha4/src/tha4/nn/upscaler/__init__.py b/tha4/src/tha4/nn/upscaler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/nn/upscaler/upscaler_02.py b/tha4/src/tha4/nn/upscaler/upscaler_02.py new file mode 100644 index 0000000..bf2de92 --- /dev/null +++ b/tha4/src/tha4/nn/upscaler/upscaler_02.py @@ -0,0 +1,110 @@ +from typing import List + +import torch +from torch import Tensor, zero_ +from torch.nn import Module, Conv2d + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.image_processing_util import GridChangeApplier +from tha4.nn.common.unet import UnetArgs, AttentionBlockArgs, UnetWithFirstConvAddition + + +class Upscaler02Args: + def __init__(self, + image_size: int, + image_channels: int, + num_pose_parameters: int, + unet_args: UnetArgs): + assert unet_args.in_channels == ( + image_channels + ) + assert unet_args.out_channels == ( + image_channels + # direct + 2 + # warp + 1 # alpha + ) + assert unet_args.cond_input_channels == num_pose_parameters + self.image_channels = image_channels + self.image_size = image_size + self.num_pose_parameters = num_pose_parameters + self.unet_args = unet_args + + +def apply_color_change(alpha, color_change, image: Tensor) -> Tensor: + return color_change * alpha + image * (1 - alpha) + + +class Upscaler02(Module): + def __init__(self, args: Upscaler02Args): + super().__init__() + self.args = args + self.body = UnetWithFirstConvAddition(args.unet_args) + self.grid_change_applier = GridChangeApplier() + self.coarse_image_conv = Conv2d( + args.image_channels + args.image_channels + 2, + args.unet_args.model_channels, + kernel_size=3, + stride=1, + padding=1) + with torch.no_grad(): + zero_(self.coarse_image_conv.weight) + zero_(self.coarse_image_conv.bias) + + def check_image(self, image: torch.Tensor): + assert len(image.shape) == 4 + assert image.shape[1] == self.args.image_channels + assert image.shape[2] == self.args.image_size + assert image.shape[3] == self.args.image_size + + def forward(self, + rest_image: torch.Tensor, + coarse_posed_image: torch.Tensor, + coarse_grid_change: torch.Tensor, + pose: torch.Tensor) -> List[Tensor]: + self.check_image(rest_image) + self.check_image(coarse_posed_image) + + assert len(pose.shape) == 2 + assert rest_image.shape[0] == pose.shape[0] + assert coarse_posed_image.shape[0] == pose.shape[0] + assert coarse_grid_change.shape[0] == pose.shape[0] + assert coarse_grid_change.shape[1] == 2 + assert coarse_grid_change.shape[2] == self.args.image_size + assert coarse_grid_change.shape[3] == self.args.image_size + assert pose.shape[1] == self.args.num_pose_parameters + + warped_image = self.grid_change_applier.apply(coarse_grid_change, rest_image) + + t = torch.zeros(rest_image.shape[0], 1, device=rest_image.device) + feature = torch.cat([coarse_posed_image, warped_image, coarse_grid_change], dim=1) + first_conv_addition = self.coarse_image_conv(feature) + + body_output = self.body(rest_image, t, pose, first_conv_addition) + + direct = body_output[:, 0:self.args.image_channels, :, :] + grid_change = body_output[:, self.args.image_channels:self.args.image_channels + 2, :, :] + alpha = torch.sigmoid(body_output[:, self.args.image_channels + 2:self.args.image_channels + 3, :, :]) + warped = self.grid_change_applier.apply(grid_change, rest_image) + merged = apply_color_change(alpha, direct, warped) + + return [ + merged, + alpha, + warped, + grid_change, + direct + ] + + INDEX_MERGED = 0 + INDEX_ALPHA = 1 + INDEX_WARPED = 2 + INDEX_GRID_CHANGE = 3 + INDEX_DIRECT = 4 + + +class Upscaler02Factory(ModuleFactory): + def __init__(self, args: Upscaler02Args): + self.args = args + + def create(self) -> Module: + return Upscaler02(self.args) diff --git a/tha4/src/tha4/nn/util.py b/tha4/src/tha4/nn/util.py new file mode 100644 index 0000000..9f46989 --- /dev/null +++ b/tha4/src/tha4/nn/util.py @@ -0,0 +1,40 @@ +from typing import Optional, Callable, Union + +from torch.nn import Module + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.nn.init_function import create_init_function +from tha4.nn.nonlinearity_factory import resolve_nonlinearity_factory +from tha4.nn.normalization import NormalizationLayerFactory +from tha4.nn.spectral_norm import apply_spectral_norm + + +def wrap_conv_or_linear_module(module: Module, + initialization_method: Union[str, Callable[[Module], Module]], + use_spectral_norm: bool): + if isinstance(initialization_method, str): + init = create_init_function(initialization_method) + else: + init = initialization_method + return apply_spectral_norm(init(module), use_spectral_norm) + + +class BlockArgs: + def __init__(self, + initialization_method: Union[str, Callable[[Module], Module]] = 'he', + use_spectral_norm: bool = False, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + nonlinearity_factory: Optional[ModuleFactory] = None): + self.nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + self.normalization_layer_factory = normalization_layer_factory + self.use_spectral_norm = use_spectral_norm + self.initialization_method = initialization_method + + def wrap_module(self, module: Module) -> Module: + return wrap_conv_or_linear_module(module, self.get_init_func(), self.use_spectral_norm) + + def get_init_func(self) -> Callable[[Module], Module]: + if isinstance(self.initialization_method, str): + return create_init_function(self.initialization_method) + else: + return self.initialization_method diff --git a/tha4/src/tha4/poser/__init__.py b/tha4/src/tha4/poser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/poser/general_poser_02.py b/tha4/src/tha4/poser/general_poser_02.py new file mode 100644 index 0000000..7535451 --- /dev/null +++ b/tha4/src/tha4/poser/general_poser_02.py @@ -0,0 +1,98 @@ +from typing import List, Optional, Tuple, Dict, Callable + +import torch +from tha4.shion.core.cached_computation import ComputationState +from tha4.poser.poser import PoseParameterGroup, Poser +from torch import Tensor +from torch.nn import Module + + +class GeneralPoser02(Poser): + def __init__(self, + module_loaders: Dict[str, Callable[[], Module]], + device: torch.device, + output_length: int, + pose_parameters: List[PoseParameterGroup], + output_list_func: Callable[[ComputationState], List[Tensor]], + subrect: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None, + default_output_index: int = 0, + image_size: int = 256, + dtype: torch.dtype = torch.float): + self.dtype = dtype + self.image_size = image_size + self.default_output_index = default_output_index + self.output_list_func = output_list_func + self.subrect = subrect + self.pose_parameters = pose_parameters + self.device = device + self.module_loaders = module_loaders + + self.modules = None + + self.num_parameters = 0 + for pose_parameter in self.pose_parameters: + self.num_parameters += pose_parameter.get_arity() + + self.output_length = output_length + + def get_image_size(self) -> int: + return self.image_size + + def get_modules(self): + if self.modules is None: + self.modules = {} + for key in self.module_loaders: + module = self.module_loaders[key]() + self.modules[key] = module + module.to(self.device) + module.train(False) + return self.modules + + def get_pose_parameter_groups(self) -> List[PoseParameterGroup]: + return self.pose_parameters + + def get_num_parameters(self) -> int: + return self.num_parameters + + def pose(self, image: Tensor, pose: Tensor, output_index: Optional[int] = None) -> Tensor: + if output_index is None: + output_index = self.default_output_index + output_list = self.get_posing_outputs(image, pose) + return output_list[output_index] + + def get_posing_outputs(self, image: Tensor, pose: Tensor) -> List[Tensor]: + modules = self.get_modules() + + if len(image.shape) == 3: + image = image.unsqueeze(0) + if len(pose.shape) == 1: + pose = pose.unsqueeze(0) + if self.subrect is not None: + image = image[:, :, self.subrect[0][0]:self.subrect[0][1], self.subrect[1][0]:self.subrect[1][1]] + batch = [image, pose] + + state = ComputationState( + modules=modules, + accumulated_modules={}, + batch=batch, + outputs={}) + return self.output_list_func(state) + + def get_output_length(self) -> int: + return self.output_length + + def free(self): + self.modules = None + + def get_dtype(self) -> torch.dtype: + return self.dtype + + def to(self, device: torch.device) -> 'GeneralPoser02': + if device == self.device: + return self + modules = self.get_modules() + self.device = device + for key in modules: + module = modules[key] + module.to(self.device) + return self diff --git a/tha4/src/tha4/poser/modes/__init__.py b/tha4/src/tha4/poser/modes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/poser/modes/mode_07.py b/tha4/src/tha4/poser/modes/mode_07.py new file mode 100644 index 0000000..e7e7fce --- /dev/null +++ b/tha4/src/tha4/poser/modes/mode_07.py @@ -0,0 +1,315 @@ +from enum import Enum +from typing import List, Dict, Optional + +import torch +from tha4.shion.core.cached_computation import CachedComputationProtocol, ComputationState +from tha4.shion.core.load_save import torch_load +from tha4.nn.eyebrow_decomposer.eyebrow_decomposer_00 import EyebrowDecomposer00, \ + EyebrowDecomposer00Factory, EyebrowDecomposer00Args +from tha4.nn.eyebrow_morphing_combiner.eyebrow_morphing_combiner_00 import \ + EyebrowMorphingCombiner00Factory, EyebrowMorphingCombiner00Args, EyebrowMorphingCombiner00 +from tha4.nn.face_morpher.face_morpher_08 import FaceMorpher08Args, FaceMorpher08Factory +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs +from tha4.nn.common.unet import UnetArgs, AttentionBlockArgs +from tha4.nn.morpher.morpher_00 import Morpher00Args, Morpher00 +from tha4.nn.upscaler.upscaler_02 import Upscaler02Args, Upscaler02 +from tha4.poser.general_poser_02 import GeneralPoser02 +from tha4.poser.modes.pose_parameters import get_pose_parameters +from torch import Tensor +from torch.nn.functional import interpolate + + +class Network(Enum): + eyebrow_decomposer = 1 + eyebrow_morphing_combiner = 2 + face_morpher = 3 + body_morpher = 4 + upscaler = 5 + + @property + def outputs_key(self): + return f"{self.name}_outputs" + + +class Branch(Enum): + face_morphed_half = 1 + face_morphed_full = 2 + all_outputs = 3 + + +NUM_EYEBROW_PARAMS = 12 +NUM_FACE_PARAMS = 27 +NUM_ROTATION_PARAMS = 6 + + +class FiveStepPoserComputationProtocol(CachedComputationProtocol): + def __init__(self, eyebrow_morphed_image_index: int): + super().__init__() + self.eyebrow_morphed_image_index = eyebrow_morphed_image_index + self.cached_batch_0 = None + self.cached_eyebrow_decomposer_output = None + + def compute_func(self): + def func(state: ComputationState) -> List[Tensor]: + if self.cached_batch_0 is None: + new_batch_0 = True + elif state.batch[0].shape[0] != self.cached_batch_0.shape[0]: + new_batch_0 = True + else: + new_batch_0 = torch.max((state.batch[0] - self.cached_batch_0).abs()).item() > 0 + if not new_batch_0: + state.outputs[Network.eyebrow_decomposer.outputs_key] = self.cached_eyebrow_decomposer_output + output = self.get_output(Branch.all_outputs.name, state) + if new_batch_0: + self.cached_batch_0 = state.batch[0] + self.cached_eyebrow_decomposer_output = state.outputs[Network.eyebrow_decomposer.outputs_key] + return output + + return func + + def compute_output(self, key: str, state: ComputationState) -> List[Tensor]: + if key == Network.eyebrow_decomposer.outputs_key: + input_image = state.batch[0][:, :, 64:192, 64 + 128:192 + 128] + return state.modules[Network.eyebrow_decomposer.name].forward(input_image) + elif key == Network.eyebrow_morphing_combiner.outputs_key: + eyebrow_decomposer_output = self.get_output(Network.eyebrow_decomposer.outputs_key, state) + background_layer = eyebrow_decomposer_output[EyebrowDecomposer00.BACKGROUND_LAYER_INDEX] + eyebrow_layer = eyebrow_decomposer_output[EyebrowDecomposer00.EYEBROW_LAYER_INDEX] + eyebrow_pose = state.batch[1][:, :NUM_EYEBROW_PARAMS] + return state.modules[Network.eyebrow_morphing_combiner.name].forward( + background_layer, + eyebrow_layer, + eyebrow_pose) + elif key == Network.face_morpher.outputs_key: + eyebrow_morphing_combiner_output = self.get_output( + Network.eyebrow_morphing_combiner.outputs_key, state) + eyebrow_morphed_image = eyebrow_morphing_combiner_output[self.eyebrow_morphed_image_index] + input_image = state.batch[0][:, :, 32:32 + 192, (32 + 128):(32 + 192 + 128)].clone() + input_image[:, :, 32:32 + 128, 32:32 + 128] = eyebrow_morphed_image + face_pose = state.batch[1][:, NUM_EYEBROW_PARAMS:NUM_EYEBROW_PARAMS + NUM_FACE_PARAMS] + return state.modules[Network.face_morpher.name].forward(input_image, face_pose) + elif key == Branch.face_morphed_full.name: + face_morpher_output = self.get_output(Network.face_morpher.outputs_key, state) + face_morphed_image = face_morpher_output[0] + input_image = state.batch[0].clone() + input_image[:, :, 32:32 + 192, 32 + 128:32 + 192 + 128] = face_morphed_image + return [input_image] + elif key == Branch.face_morphed_half.name: + face_morphed_full = self.get_output(Branch.face_morphed_full.name, state)[0] + return [ + interpolate(face_morphed_full, size=(256, 256), mode='bilinear', align_corners=False) + ] + elif key == Network.body_morpher.outputs_key: + face_morphed_half = self.get_output(Branch.face_morphed_half.name, state)[0] + rotation_pose = state.batch[1][:, NUM_EYEBROW_PARAMS + NUM_FACE_PARAMS:] + return state.modules[Network.body_morpher.name].forward(face_morphed_half, rotation_pose) + elif key == Network.upscaler.outputs_key: + rest_image = self.get_output(Branch.face_morphed_full.name, state)[0] + body_morpher_outputs = self.get_output( + Network.body_morpher.outputs_key, state) + half_res_posed_image = body_morpher_outputs[Morpher00.INDEX_MERGED] + half_res_grid_change = body_morpher_outputs[Morpher00.INDEX_GRID_CHANGE] + coarse_posed_image = interpolate(half_res_posed_image, size=(512, 512), mode='bilinear') + coarse_grid_change = interpolate(half_res_grid_change, size=(512, 512), mode='bilinear') + rotation_pose = state.batch[1][:, NUM_EYEBROW_PARAMS + NUM_FACE_PARAMS:] + return state.modules[Network.upscaler.name].forward( + rest_image, coarse_posed_image, coarse_grid_change, rotation_pose) + elif key == Branch.all_outputs.name: + upscaler_output = self.get_output(Network.upscaler.outputs_key, state) + face_morphed_full = self.get_output(Branch.face_morphed_full.name, state) + body_morpher_output = self.get_output(Network.body_morpher.outputs_key, state) + face_morpher_output = self.get_output(Network.face_morpher.outputs_key, state) + eyebrow_morphing_combiner_output = self.get_output(Network.eyebrow_morphing_combiner.outputs_key, state) + eyebrow_decomposer_output = self.get_output(Network.eyebrow_decomposer.outputs_key, state) + output = upscaler_output \ + + face_morphed_full \ + + body_morpher_output \ + + face_morpher_output \ + + eyebrow_morphing_combiner_output \ + + eyebrow_decomposer_output + return output + else: + raise RuntimeError("Unsupported key: " + key) + + +def load_eyebrow_decomposer(file_name: str): + factory = EyebrowDecomposer00Factory( + EyebrowDecomposer00Args( + image_size=128, + image_channels=4, + start_channels=64, + bottleneck_image_size=16, + num_bottleneck_blocks=6, + max_channels=512, + block_args=BlockArgs( + initialization_method='he', + use_spectral_norm=False, + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=True)))) + print("Loading the eyebrow decomposer ... ", end="") + module = factory.create() + module.load_state_dict(torch_load(file_name)) + print("DONE!!!") + return module + + +def load_eyebrow_morphing_combiner(file_name: str): + factory = EyebrowMorphingCombiner00Factory( + EyebrowMorphingCombiner00Args( + image_size=128, + image_channels=4, + start_channels=64, + num_pose_params=12, + bottleneck_image_size=16, + num_bottleneck_blocks=6, + max_channels=512, + block_args=BlockArgs( + initialization_method='he', + use_spectral_norm=False, + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=True)))) + print("Loading the eyebrow morphing conbiner ... ", end="") + module = factory.create() + module.load_state_dict(torch_load(file_name)) + print("DONE!!!") + return module + + +def load_face_morpher(file_name: str): + factory = FaceMorpher08Factory( + FaceMorpher08Args( + image_size=192, + image_channels=4, + num_expression_params=27, + start_channels=64, + bottleneck_image_size=24, + num_bottleneck_blocks=6, + max_channels=512, + block_args=BlockArgs( + initialization_method='he', + use_spectral_norm=False, + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=False) + ), + output_iris_mouth_grid_change=True, + ) + ) + print("Loading the face morpher ... ", end="") + module = factory.create() + module.load_state_dict(torch_load(file_name)) + print("DONE!!!") + return module + + +def apply_color_change(alpha, color_change, image: Tensor) -> Tensor: + return color_change * alpha + image * (1 - alpha) + + +def load_morpher_00(file_name: str): + unet_args = UnetArgs( + in_channels=4, + out_channels=7, + model_channels=64, + level_channel_multipliers=[1, 2, 4, 4, 4], + level_use_attention=[False, False, False, False, True], + num_res_blocks_per_level=1, + num_middle_res_blocks=4, + time_embedding_channels=None, + cond_input_channels=6, + cond_internal_channels=256, + attention_block_args=AttentionBlockArgs( + num_heads=8, + use_new_attention_order=True), + dropout_prob=0.0) + morpher_00_args = Morpher00Args( + image_size=256, + image_channels=4, + num_pose_parameters=6, + unet_args=unet_args) + morpher_00 = Morpher00(morpher_00_args) + + print("Loading the body morpher ... ", end="") + morpher_00.load_state_dict(torch_load(file_name)) + print("DONE") + + morpher_00.train(False) + return morpher_00 + + +def load_upscaler_02(file_name: str): + unet_args = UnetArgs( + in_channels=4, + out_channels=7, + model_channels=32, + level_channel_multipliers=[1, 2, 4, 8, 8, 8], + level_use_attention=[False, False, False, False, False, True], + num_res_blocks_per_level=1, + num_middle_res_blocks=4, + time_embedding_channels=None, + cond_input_channels=6, + cond_internal_channels=256, + attention_block_args=AttentionBlockArgs( + num_heads=8, + use_new_attention_order=True), + dropout_prob=0.0) + upscaler_02_args = Upscaler02Args( + image_size=512, + image_channels=4, + num_pose_parameters=6, + unet_args=unet_args) + upscaler_02 = Upscaler02(upscaler_02_args) + + print("Loading the upscaler ... ", end="") + upscaler_02.load_state_dict(torch_load(file_name)) + print("DONE") + + upscaler_02.train(False) + return upscaler_02 + + +def create_poser( + device: torch.device, + module_file_names: Optional[Dict[str, str]] = None, + eyebrow_morphed_image_index: int = EyebrowMorphingCombiner00.EYEBROW_IMAGE_NO_COMBINE_ALPHA_INDEX, + default_output_index: int = 0) -> GeneralPoser02: + if module_file_names is None: + module_file_names = {} + if Network.eyebrow_decomposer.name not in module_file_names: + file_name = "data/tha4/eyebrow_decomposer.pt" + module_file_names[Network.eyebrow_decomposer.name] = file_name + if Network.eyebrow_morphing_combiner.name not in module_file_names: + file_name = "data/tha4/eyebrow_morphing_combiner.pt" + module_file_names[Network.eyebrow_morphing_combiner.name] = file_name + if Network.face_morpher.name not in module_file_names: + file_name = "data/tha4/face_morpher.pt" + module_file_names[Network.face_morpher.name] = file_name + if Network.body_morpher.name not in module_file_names: + file_name = "data/tha4/body_morpher.pt" + module_file_names[Network.body_morpher.name] = file_name + if Network.upscaler.name not in module_file_names: + file_name = "data/tha4/upscaler.pt" + module_file_names[Network.upscaler.name] = file_name + + loaders = { + Network.eyebrow_decomposer.name: + lambda: load_eyebrow_decomposer(module_file_names[Network.eyebrow_decomposer.name]), + Network.eyebrow_morphing_combiner.name: + lambda: load_eyebrow_morphing_combiner(module_file_names[Network.eyebrow_morphing_combiner.name]), + Network.face_morpher.name: + lambda: load_face_morpher(module_file_names[Network.face_morpher.name]), + Network.body_morpher.name: + lambda: load_morpher_00(module_file_names[Network.body_morpher.name]), + Network.upscaler.name: + lambda: load_upscaler_02(module_file_names[Network.upscaler.name]), + } + return GeneralPoser02( + image_size=512, + module_loaders=loaders, + pose_parameters=get_pose_parameters().get_pose_parameter_groups(), + output_list_func=FiveStepPoserComputationProtocol(eyebrow_morphed_image_index).compute_func(), + subrect=None, + device=device, + output_length=5 + 1 + 5 + 8 + 8 + 6, + default_output_index=default_output_index) diff --git a/tha4/src/tha4/poser/modes/mode_12.py b/tha4/src/tha4/poser/modes/mode_12.py new file mode 100644 index 0000000..908c1ec --- /dev/null +++ b/tha4/src/tha4/poser/modes/mode_12.py @@ -0,0 +1,202 @@ +from enum import Enum +from typing import List, Dict, Optional, Any + +import torch +from tha4.shion.core.cached_computation import CachedComputationProtocol, ComputationState +from tha4.shion.core.load_save import torch_load +from tha4.nn.eyebrow_decomposer.eyebrow_decomposer_00 import EyebrowDecomposer00, \ + EyebrowDecomposer00Factory, EyebrowDecomposer00Args +from tha4.nn.eyebrow_morphing_combiner.eyebrow_morphing_combiner_00 import \ + EyebrowMorphingCombiner00Factory, EyebrowMorphingCombiner00Args, EyebrowMorphingCombiner00 +from tha4.nn.face_morpher.face_morpher_08 import FaceMorpher08Args, FaceMorpher08Factory +from tha4.nn.nonlinearity_factory import ReLUFactory +from tha4.nn.normalization import InstanceNorm2dFactory +from tha4.nn.util import BlockArgs +from tha4.poser.general_poser_02 import GeneralPoser02 +from tha4.poser.modes.pose_parameters import get_pose_parameters +from torch import Tensor + + +class Network(Enum): + eyebrow_decomposer = 1 + eyebrow_morphing_combiner = 2 + face_morpher = 3 + + @property + def outputs_key(self): + return f"{self.name}_outputs" + + +class Branch(Enum): + face_morphed_half = 1 + face_morphed_full = 2 + all_outputs = 3 + + +NUM_EYEBROW_PARAMS = 12 +NUM_FACE_PARAMS = 27 +NUM_ROTATION_PARAMS = 6 + + +class FiveStepPoserComputationProtocol(CachedComputationProtocol): + def __init__(self, eyebrow_morphed_image_index: int): + super().__init__() + self.eyebrow_morphed_image_index = eyebrow_morphed_image_index + self.cached_batch_0 = None + self.cached_eyebrow_decomposer_output = None + + def compute_func(self): + def func(state: ComputationState) -> List[Tensor]: + if self.cached_batch_0 is None: + new_batch_0 = True + elif state.batch[0].shape[0] != self.cached_batch_0.shape[0]: + new_batch_0 = True + else: + new_batch_0 = torch.max((state.batch[0] - self.cached_batch_0).abs()).item() > 0 + if not new_batch_0: + state.outputs[Network.eyebrow_decomposer.outputs_key] = self.cached_eyebrow_decomposer_output + output = self.get_output(Branch.all_outputs.name, state) + if new_batch_0: + self.cached_batch_0 = state.batch[0] + self.cached_eyebrow_decomposer_output = state.outputs[Network.eyebrow_decomposer.outputs_key] + return output + + return func + + def compute_output(self, key: str, state: ComputationState) -> Any: + if key == Network.eyebrow_decomposer.outputs_key: + input_image = state.batch[0][:, :, 64:192, 64 + 128:192 + 128] + return state.modules[Network.eyebrow_decomposer.name].forward(input_image) + elif key == Network.eyebrow_morphing_combiner.outputs_key: + eyebrow_decomposer_output = self.get_output(Network.eyebrow_decomposer.outputs_key, state) + background_layer = eyebrow_decomposer_output[EyebrowDecomposer00.BACKGROUND_LAYER_INDEX] + eyebrow_layer = eyebrow_decomposer_output[EyebrowDecomposer00.EYEBROW_LAYER_INDEX] + eyebrow_pose = state.batch[1][:, :NUM_EYEBROW_PARAMS] + return state.modules[Network.eyebrow_morphing_combiner.name].forward( + background_layer, + eyebrow_layer, + eyebrow_pose) + elif key == Network.face_morpher.outputs_key: + eyebrow_morphing_combiner_output = self.get_output( + Network.eyebrow_morphing_combiner.outputs_key, state) + eyebrow_morphed_image = eyebrow_morphing_combiner_output[self.eyebrow_morphed_image_index] + input_image = state.batch[0][:, :, 32:32 + 192, (32 + 128):(32 + 192 + 128)].clone() + input_image[:, :, 32:32 + 128, 32:32 + 128] = eyebrow_morphed_image + face_pose = state.batch[1][:, NUM_EYEBROW_PARAMS:NUM_EYEBROW_PARAMS + NUM_FACE_PARAMS] + return state.modules[Network.face_morpher.name].forward(input_image, face_pose) + elif key == Branch.all_outputs.name: + face_morpher_output = self.get_output(Network.face_morpher.outputs_key, state) + eyebrow_morphing_combiner_output = self.get_output(Network.eyebrow_morphing_combiner.outputs_key, state) + eyebrow_decomposer_output = self.get_output(Network.eyebrow_decomposer.outputs_key, state) + output = face_morpher_output \ + + eyebrow_morphing_combiner_output \ + + eyebrow_decomposer_output + return output + else: + raise RuntimeError("Unsupported key: " + key) + + +def load_eyebrow_decomposer(file_name: str): + factory = EyebrowDecomposer00Factory( + EyebrowDecomposer00Args( + image_size=128, + image_channels=4, + start_channels=64, + bottleneck_image_size=16, + num_bottleneck_blocks=6, + max_channels=512, + block_args=BlockArgs( + initialization_method='he', + use_spectral_norm=False, + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=True)))) + print("Loading the eyebrow decomposer ... ", end="") + module = factory.create() + module.load_state_dict(torch_load(file_name)) + print("DONE!!!") + return module + + +def load_eyebrow_morphing_combiner(file_name: str): + factory = EyebrowMorphingCombiner00Factory( + EyebrowMorphingCombiner00Args( + image_size=128, + image_channels=4, + start_channels=64, + num_pose_params=12, + bottleneck_image_size=16, + num_bottleneck_blocks=6, + max_channels=512, + block_args=BlockArgs( + initialization_method='he', + use_spectral_norm=False, + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=True)))) + print("Loading the eyebrow morphing conbiner ... ", end="") + module = factory.create() + module.load_state_dict(torch_load(file_name)) + print("DONE!!!") + return module + + +def load_face_morpher(file_name: str): + factory = FaceMorpher08Factory( + FaceMorpher08Args( + image_size=192, + image_channels=4, + num_expression_params=27, + start_channels=64, + bottleneck_image_size=24, + num_bottleneck_blocks=6, + max_channels=512, + block_args=BlockArgs( + initialization_method='he', + use_spectral_norm=False, + normalization_layer_factory=InstanceNorm2dFactory(), + nonlinearity_factory=ReLUFactory(inplace=False)), + output_iris_mouth_grid_change=True)) + print("Loading the face morpher ... ", end="") + module = factory.create() + module.load_state_dict(torch_load(file_name)) + print("DONE!!!") + return module + + +def apply_color_change(alpha, color_change, image: Tensor) -> Tensor: + return color_change * alpha + image * (1 - alpha) + + +def create_poser( + device: torch.device, + module_file_names: Optional[Dict[str, str]] = None, + eyebrow_morphed_image_index: int = EyebrowMorphingCombiner00.EYEBROW_IMAGE_NO_COMBINE_ALPHA_INDEX, + default_output_index: int = 0) -> GeneralPoser02: + if module_file_names is None: + module_file_names = {} + if Network.eyebrow_decomposer.name not in module_file_names: + file_name = "data/tha4/eyebrow_decomposer.pt" + module_file_names[Network.eyebrow_decomposer.name] = file_name + if Network.eyebrow_morphing_combiner.name not in module_file_names: + file_name = "data/tha4/eyebrow_morphing_combiner.pt" + module_file_names[Network.eyebrow_morphing_combiner.name] = file_name + if Network.face_morpher.name not in module_file_names: + file_name = "data/tha4/face_morpher.pt" + module_file_names[Network.face_morpher.name] = file_name + + loaders = { + Network.eyebrow_decomposer.name: + lambda: load_eyebrow_decomposer(module_file_names[Network.eyebrow_decomposer.name]), + Network.eyebrow_morphing_combiner.name: + lambda: load_eyebrow_morphing_combiner(module_file_names[Network.eyebrow_morphing_combiner.name]), + Network.face_morpher.name: + lambda: load_face_morpher(module_file_names[Network.face_morpher.name]), + } + return GeneralPoser02( + image_size=512, + module_loaders=loaders, + pose_parameters=get_pose_parameters().get_pose_parameter_groups(), + output_list_func=FiveStepPoserComputationProtocol(eyebrow_morphed_image_index).compute_func(), + subrect=None, + device=device, + output_length=5 + 5 + 8, + default_output_index=default_output_index) diff --git a/tha4/src/tha4/poser/modes/mode_14.py b/tha4/src/tha4/poser/modes/mode_14.py new file mode 100644 index 0000000..3fcf610 --- /dev/null +++ b/tha4/src/tha4/poser/modes/mode_14.py @@ -0,0 +1,162 @@ +from dataclasses import dataclass +from typing import List, Optional, Dict, Any + +import torch +from tha4.shion.core.cached_computation import CachedComputationProtocol, ComputationState +from tha4.shion.core.load_save import torch_load +from tha4.nn.siren.face_morpher.siren_face_morpher_00 import SirenFaceMorpher00Args, SirenFaceMorpher00 +from tha4.nn.siren.morpher.siren_morpher_03 import SirenMorpher03, SirenMorpher03Args, SirenMorpherLevelArgs +from tha4.nn.siren.vanilla.siren import SirenArgs +from tha4.poser.general_poser_02 import GeneralPoser02 +from tha4.poser.modes.pose_parameters import get_pose_parameters +from torch import Tensor + +KEY_FACE_MORPHER = "face_morpher" +KEY_BODY_MORPHER = "body_morpher" + + +@dataclass +class Keys: + face_morpher: str = KEY_FACE_MORPHER + face_morpher_output: str = "face_morpher_output" + + face_morpher_input_image: str = "face_morpher_input_image" + face_morpher_input_pose: str = "face_morpher_input_pose" + + body_morpher_input_image: str = "body_morpher_input_image" + + body_morpher: str = KEY_BODY_MORPHER + body_morpher_output: str = "body_morpher_output" + + all_outputs: str = "all_outputs" + + +@dataclass +class Indices: + original_image: int = 0 + original_pose: int = 1 + + +class TwoStepPoserComputationProtocol(CachedComputationProtocol): + def __init__(self, keys: Optional[Keys] = None, indices: Optional[Indices] = None): + super().__init__() + + if keys is None: + keys = Keys() + if indices is None: + indices = Indices() + + self.keys = keys + self.indices = indices + + def compute_func(self): + def func(state: ComputationState) -> List[Tensor]: + return self.get_output(self.keys.all_outputs, state) + + return func + + def compute_output(self, key: str, state: ComputationState) -> Any: + if key == self.keys.face_morpher_input_image: + image = state.batch[self.indices.original_image] + center_x = 256 + center_y = 128 + 16 + return image[:, :, center_y - 64:center_y + 64, center_x - 64:center_x + 64] + elif key == self.keys.face_morpher_input_pose: + pose = state.batch[self.indices.original_pose] + return pose[:, 0:39] + elif key == self.keys.face_morpher_output: + module = state.modules[self.keys.face_morpher] + pose = self.get_output(self.keys.face_morpher_input_pose, state) + with torch.no_grad(): + return module.forward(pose) + elif key == self.keys.body_morpher_input_image: + image = state.batch[self.indices.original_image].clone() + center_x = 256 + center_y = 128 + 16 + face_morphed_image = self.get_output(self.keys.face_morpher_output, state) + image[:, :, center_y - 64:center_y + 64, center_x - 64:center_x + 64] = face_morphed_image + return image + elif key == self.keys.body_morpher_output: + image = self.get_output(self.keys.body_morpher_input_image, state) + pose = state.batch[self.indices.original_pose] + body_morpher = state.modules[self.keys.body_morpher] + with torch.no_grad(): + return body_morpher.forward(image, pose) + elif key == self.keys.all_outputs: + body_morpher_output = self.get_output(self.keys.body_morpher_output, state) + face_morpher_output = self.get_output(self.keys.face_morpher_output, state) + return body_morpher_output + [face_morpher_output] + else: + raise RuntimeError("Unsupported key: " + key) + + +def load_face_morpher(file_name: Optional[str] = None): + module = SirenFaceMorpher00( + SirenFaceMorpher00Args( + image_size=128, + image_channels=4, + pose_size=39, + siren_args=SirenArgs( + in_channels=39 + 2, + out_channels=4, + intermediate_channels=128, + num_sine_layers=8))) + if file_name is not None: + module.load_state_dict(torch_load(file_name)) + return module + + +def load_body_morpher(file_name: Optional[str] = None): + module = SirenMorpher03( + SirenMorpher03Args( + image_size=512, + image_channels=4, + pose_size=45, + level_args=[ + SirenMorpherLevelArgs( + image_size=128, + intermediate_channels=360, + num_sine_layers=3), + SirenMorpherLevelArgs( + image_size=256, + intermediate_channels=180, + num_sine_layers=3), + SirenMorpherLevelArgs( + image_size=512, + intermediate_channels=90, + num_sine_layers=3), + ])) + if file_name is not None: + module.load_state_dict(torch_load(file_name)) + return module + + +def create_poser( + device: torch.device, + module_file_names: Optional[Dict[str, str]] = None, + default_output_index: int = 0) -> GeneralPoser02: + if module_file_names is None: + module_file_names = {} + if KEY_FACE_MORPHER not in module_file_names: + file_name = "data/character_models/lambda_00/face_morpher.pt" + module_file_names[KEY_FACE_MORPHER] = file_name + if KEY_BODY_MORPHER not in module_file_names: + file_name = "data/character_models/lambda_00/body_morpher.pt" + module_file_names[KEY_BODY_MORPHER] = file_name + + loaders = { + KEY_FACE_MORPHER: + lambda: load_face_morpher(module_file_names[KEY_FACE_MORPHER]), + KEY_BODY_MORPHER: + lambda: load_body_morpher(module_file_names[KEY_BODY_MORPHER]), + } + + return GeneralPoser02( + image_size=512, + module_loaders=loaders, + pose_parameters=get_pose_parameters().get_pose_parameter_groups(), + output_list_func=TwoStepPoserComputationProtocol().compute_func(), + subrect=None, + device=device, + output_length=5 + 1, + default_output_index=default_output_index) diff --git a/tha4/src/tha4/poser/modes/pose_parameters.py b/tha4/src/tha4/poser/modes/pose_parameters.py new file mode 100644 index 0000000..8f38aa2 --- /dev/null +++ b/tha4/src/tha4/poser/modes/pose_parameters.py @@ -0,0 +1,36 @@ +from tha4.poser.poser import PoseParameters, PoseParameterCategory + + +def get_pose_parameters(): + return PoseParameters.Builder() \ + .add_parameter_group("eyebrow_troubled", PoseParameterCategory.EYEBROW, arity=2) \ + .add_parameter_group("eyebrow_angry", PoseParameterCategory.EYEBROW, arity=2) \ + .add_parameter_group("eyebrow_lowered", PoseParameterCategory.EYEBROW, arity=2) \ + .add_parameter_group("eyebrow_raised", PoseParameterCategory.EYEBROW, arity=2) \ + .add_parameter_group("eyebrow_happy", PoseParameterCategory.EYEBROW, arity=2) \ + .add_parameter_group("eyebrow_serious", PoseParameterCategory.EYEBROW, arity=2) \ + .add_parameter_group("eye_wink", PoseParameterCategory.EYE, arity=2) \ + .add_parameter_group("eye_happy_wink", PoseParameterCategory.EYE, arity=2) \ + .add_parameter_group("eye_surprised", PoseParameterCategory.EYE, arity=2) \ + .add_parameter_group("eye_relaxed", PoseParameterCategory.EYE, arity=2) \ + .add_parameter_group("eye_unimpressed", PoseParameterCategory.EYE, arity=2) \ + .add_parameter_group("eye_raised_lower_eyelid", PoseParameterCategory.EYE, arity=2) \ + .add_parameter_group("iris_small", PoseParameterCategory.IRIS_MORPH, arity=2) \ + .add_parameter_group("mouth_aaa", PoseParameterCategory.MOUTH, arity=1, default_value=1.0) \ + .add_parameter_group("mouth_iii", PoseParameterCategory.MOUTH, arity=1) \ + .add_parameter_group("mouth_uuu", PoseParameterCategory.MOUTH, arity=1) \ + .add_parameter_group("mouth_eee", PoseParameterCategory.MOUTH, arity=1) \ + .add_parameter_group("mouth_ooo", PoseParameterCategory.MOUTH, arity=1) \ + .add_parameter_group("mouth_delta", PoseParameterCategory.MOUTH, arity=1) \ + .add_parameter_group("mouth_lowered_corner", PoseParameterCategory.MOUTH, arity=2) \ + .add_parameter_group("mouth_raised_corner", PoseParameterCategory.MOUTH, arity=2) \ + .add_parameter_group("mouth_smirk", PoseParameterCategory.MOUTH, arity=1) \ + .add_parameter_group("iris_rotation_x", PoseParameterCategory.IRIS_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("iris_rotation_y", PoseParameterCategory.IRIS_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("head_x", PoseParameterCategory.FACE_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("head_y", PoseParameterCategory.FACE_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("neck_z", PoseParameterCategory.FACE_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("body_y", PoseParameterCategory.BODY_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("body_z", PoseParameterCategory.BODY_ROTATION, arity=1, range=(-1.0, 1.0)) \ + .add_parameter_group("breathing", PoseParameterCategory.BREATHING, arity=1, range=(0.0, 1.0)) \ + .build() \ No newline at end of file diff --git a/tha4/src/tha4/poser/poser.py b/tha4/src/tha4/poser/poser.py new file mode 100644 index 0000000..eb712ba --- /dev/null +++ b/tha4/src/tha4/poser/poser.py @@ -0,0 +1,162 @@ +from abc import ABC, abstractmethod +from enum import Enum +from typing import Tuple, List, Optional + +import torch +from torch import Tensor + + +class PoseParameterCategory(Enum): + EYEBROW = 1 + EYE = 2 + IRIS_MORPH = 3 + IRIS_ROTATION = 4 + MOUTH = 5 + FACE_ROTATION = 6 + BODY_ROTATION = 7 + BREATHING = 8 + + +class PoseParameterGroup: + def __init__(self, + group_name: str, + parameter_index: int, + category: PoseParameterCategory, + arity: int = 1, + discrete: bool = False, + default_value: float = 0.0, + range: Optional[Tuple[float, float]] = None): + assert arity == 1 or arity == 2 + if range is None: + range = (0.0, 1.0) + if arity == 1: + parameter_names = [group_name] + else: + parameter_names = [group_name + "_left", group_name + "_right"] + assert len(parameter_names) == arity + + self.parameter_names = parameter_names + self.range = range + self.default_value = default_value + self.discrete = discrete + self.arity = arity + self.category = category + self.parameter_index = parameter_index + self.group_name = group_name + + def get_arity(self) -> int: + return self.arity + + def get_group_name(self) -> str: + return self.group_name + + def get_parameter_names(self) -> List[str]: + return self.parameter_names + + def is_discrete(self) -> bool: + return self.discrete + + def get_range(self) -> Tuple[float, float]: + return self.range + + def get_default_value(self): + return self.default_value + + def get_parameter_index(self): + return self.parameter_index + + def get_category(self) -> PoseParameterCategory: + return self.category + + +class PoseParameters: + def __init__(self, pose_parameter_groups: List[PoseParameterGroup]): + self.pose_parameter_groups = pose_parameter_groups + + def get_parameter_index(self, name: str) -> int: + index = 0 + for parameter_group in self.pose_parameter_groups: + for param_name in parameter_group.parameter_names: + if name == param_name: + return index + index += 1 + raise RuntimeError("Cannot find parameter with name %s" % name) + + def get_parameter_name(self, index: int) -> str: + assert index >= 0 and index < self.get_parameter_count() + + for group in self.pose_parameter_groups: + if index < group.get_arity(): + return group.get_parameter_names()[index] + index -= group.arity + + raise RuntimeError("Something is wrong here!!!") + + def get_pose_parameter_groups(self): + return self.pose_parameter_groups + + def get_parameter_count(self): + count = 0 + for group in self.pose_parameter_groups: + count += group.arity + return count + + class Builder: + def __init__(self): + self.index = 0 + self.pose_parameter_groups = [] + + def add_parameter_group(self, + group_name: str, + category: PoseParameterCategory, + arity: int = 1, + discrete: bool = False, + default_value: float = 0.0, + range: Optional[Tuple[float, float]] = None): + self.pose_parameter_groups.append( + PoseParameterGroup( + group_name, + self.index, + category, + arity, + discrete, + default_value, + range)) + self.index += arity + return self + + def build(self) -> 'PoseParameters': + return PoseParameters(self.pose_parameter_groups) + + +class Poser(ABC): + @abstractmethod + def get_image_size(self) -> int: + pass + + @abstractmethod + def get_output_length(self) -> int: + pass + + @abstractmethod + def get_pose_parameter_groups(self) -> List[PoseParameterGroup]: + pass + + @abstractmethod + def get_num_parameters(self) -> int: + pass + + @abstractmethod + def pose(self, image: Tensor, pose: Tensor, output_index: int = 0) -> Tensor: + pass + + @abstractmethod + def get_posing_outputs(self, image: Tensor, pose: Tensor) -> List[Tensor]: + pass + + def get_dtype(self) -> torch.dtype: + return torch.float + + @abstractmethod + def to(self, device: torch.device): + pass \ No newline at end of file diff --git a/tha4/src/tha4/pytasuku/__init__.py b/tha4/src/tha4/pytasuku/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/pytasuku/indexed/__init__.py b/tha4/src/tha4/pytasuku/indexed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/pytasuku/indexed/all_tasks.py b/tha4/src/tha4/pytasuku/indexed/all_tasks.py new file mode 100644 index 0000000..11ea938 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/all_tasks.py @@ -0,0 +1,26 @@ +from typing import Iterable + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_tasks import IndexedTasks +from tha4.pytasuku.indexed.no_index_command_tasks import NoIndexCommandTasks + + +class AllTasks(NoIndexCommandTasks): + def __init__( + self, + workspace: Workspace, prefix: str, + tasks: Iterable[IndexedTasks], + command_name: str = "all", + define_tasks_immediately: bool = True): + super().__init__(workspace, prefix, command_name, define_tasks_immediately) + self.tasks = [t for t in tasks] + if define_tasks_immediately: + self.define_tasks() + + def execute_run_command(self): + for task in self.tasks: + self.workspace.run(task.run_command) + + def execute_clean_command(self): + for task in self.tasks: + self.workspace.run(task.clean_command) \ No newline at end of file diff --git a/tha4/src/tha4/pytasuku/indexed/bundled_indexed_file_tasks.py b/tha4/src/tha4/pytasuku/indexed/bundled_indexed_file_tasks.py new file mode 100644 index 0000000..2ac9ab8 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/bundled_indexed_file_tasks.py @@ -0,0 +1,31 @@ +import abc +from typing import Iterable, List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_tasks import IndexedTasks +from tha4.pytasuku.workspace import do_nothing + + +class BundledIndexedTasks: + __metaclass__ = abc.ABCMeta + + @property + @abc.abstractmethod + def indexed_tasks_command_names(self) -> Iterable[str]: + pass + + @abc.abstractmethod + def get_indexed_tasks(self, command_name) -> IndexedTasks: + pass + + +def define_all_tasks_from_list(workspace: Workspace, prefix: str, tasks: List[BundledIndexedTasks]): + for command_name in tasks[0].indexed_tasks_command_names: + workspace.create_command_task( + prefix + "/" + command_name, + [x.get_indexed_tasks(command_name).run_command for x in tasks], + do_nothing) + workspace.create_command_task( + prefix + "/" + command_name + "_clean", + [x.get_indexed_tasks(command_name).clean_command for x in tasks], + do_nothing) diff --git a/tha4/src/tha4/pytasuku/indexed/indexed_file_tasks.py b/tha4/src/tha4/pytasuku/indexed/indexed_file_tasks.py new file mode 100644 index 0000000..3b53cfd --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/indexed_file_tasks.py @@ -0,0 +1,19 @@ +import abc +from typing import List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_tasks import IndexedTasks + + +class IndexedFileTasks(IndexedTasks, abc.ABC): + def __init__(self, workspace: Workspace, prefix: str): + super().__init__(workspace, prefix) + + @property + @abc.abstractmethod + def file_list(self) -> List[str]: + pass + + @abc.abstractmethod + def get_file_name(self, *indices: int) -> str: + pass diff --git a/tha4/src/tha4/pytasuku/indexed/indexed_tasks.py b/tha4/src/tha4/pytasuku/indexed/indexed_tasks.py new file mode 100644 index 0000000..75e6676 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/indexed_tasks.py @@ -0,0 +1,34 @@ +import abc +from typing import List + +from tha4.pytasuku.workspace import Workspace + + +class IndexedTasks(abc.ABC): + def __init__(self, workspace: Workspace, prefix: str): + self.prefix = prefix + self.workspace = workspace + + @property + @abc.abstractmethod + def run_command(self) -> str: + pass + + @property + @abc.abstractmethod + def clean_command(self) -> str: + pass + + @property + @abc.abstractmethod + def shape(self) -> List[int]: + pass + + @property + @abc.abstractmethod + def arity(self) -> int: + pass + + @abc.abstractmethod + def define_tasks(self): + pass diff --git a/tha4/src/tha4/pytasuku/indexed/no_index_command_tasks.py b/tha4/src/tha4/pytasuku/indexed/no_index_command_tasks.py new file mode 100644 index 0000000..6f8c400 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/no_index_command_tasks.py @@ -0,0 +1,41 @@ +import abc +from typing import List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_tasks import IndexedTasks + + +class NoIndexCommandTasks(IndexedTasks, abc.ABC): + def __init__(self, workspace: Workspace, prefix: str, command_name: str, define_tasks_immediately: bool = True): + super().__init__(workspace, prefix) + self.command_name = command_name + if define_tasks_immediately: + self.define_tasks() + + @property + def run_command(self): + return self.prefix + "/" + self.command_name + + @property + def clean_command(self): + return self.prefix + "/" + self.command_name + "_clean" + + @property + def arity(self) -> int: + return 0 + + @property + def shape(self) -> List[int]: + return [] + + @abc.abstractmethod + def execute_run_command(self): + pass + + @abc.abstractmethod + def execute_clean_command(self): + pass + + def define_tasks(self): + self.workspace.create_command_task(self.run_command, [], self.execute_run_command) + self.workspace.create_command_task(self.clean_command, [], self.execute_clean_command) diff --git a/tha4/src/tha4/pytasuku/indexed/no_index_file_tasks.py b/tha4/src/tha4/pytasuku/indexed/no_index_file_tasks.py new file mode 100644 index 0000000..70be842 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/no_index_file_tasks.py @@ -0,0 +1,56 @@ +import abc +from typing import List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_file_tasks import IndexedFileTasks +from tha4.pytasuku.indexed.util import delete_file + + +class NoIndexFileTasks(IndexedFileTasks, abc.ABC): + def __init__(self, workspace: Workspace, prefix: str, command_name: str, define_tasks_immediately: bool = True): + super().__init__(workspace, prefix) + self.command_name = command_name + if define_tasks_immediately: + self.define_tasks() + + @property + @abc.abstractmethod + def file_name(self): + pass + + @abc.abstractmethod + def create_file_task(self): + pass + + def get_file_name(self, *indices: int) -> str: + if len(indices) > 0: + raise IndexError("NoIndexFileTasks has arity 0, but get_file_name is called with an index.") + return self.file_name + + @property + def run_command(self): + return self.prefix + "/" + self.command_name + + @property + def clean_command(self): + return self.prefix + "/" + self.command_name + "_clean" + + @property + def arity(self) -> int: + return 0 + + @property + def shape(self) -> List[int]: + return [] + + @property + def file_list(self) -> List[str]: + return [self.file_name] + + def clean(self): + delete_file(self.file_name) + + def define_tasks(self): + self.create_file_task() + self.workspace.create_command_task(self.run_command, [self.file_name]) + self.workspace.create_command_task(self.clean_command, [], lambda: self.clean()) \ No newline at end of file diff --git a/tha4/src/tha4/pytasuku/indexed/one_index_file_tasks.py b/tha4/src/tha4/pytasuku/indexed/one_index_file_tasks.py new file mode 100644 index 0000000..ca5305c --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/one_index_file_tasks.py @@ -0,0 +1,66 @@ +import abc + +from typing import List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_file_tasks import IndexedFileTasks +from tha4.pytasuku.indexed.util import delete_file + + +class OneIndexFileTasks(IndexedFileTasks, abc.ABC): + def __init__(self, workspace: Workspace, prefix: str, command_name: str, count: int, + define_tasks_immediately: bool = True): + super().__init__(workspace, prefix) + self.command_name = command_name + self.count = count + self.file_list_ = [] + if define_tasks_immediately: + self.define_tasks() + + @property + def run_command(self) -> str: + return self.prefix + "/" + self.command_name + + @property + def clean_command(self) -> str: + return self.prefix + "/" + self.command_name + "_clean" + + @property + def shape(self) -> List[int]: + return [self.count] + + @property + def arity(self) -> int: + return 1 + + @abc.abstractmethod + def file_name(self, index): + pass + + @abc.abstractmethod + def create_file_tasks(self, index): + pass + + def get_file_name(self, *indices: int) -> str: + if len(indices) != 1: + raise IndexError("OneIndexFileTasks has arity 1, but " + "get_file_name does not get the appropriate number of arguments.") + return self.file_name(indices[0]) + + @property + def file_list(self): + if len(self.file_list_) == 0: + for i in range(self.count): + self.file_list_.append(self.file_name(i)) + return self.file_list_ + + def clean(self): + for file in self.file_list: + delete_file(file) + + def define_tasks(self): + for index in range(self.count): + self.create_file_tasks(index) + dependencies = self.file_list + self.workspace.create_command_task(self.run_command, dependencies) + self.workspace.create_command_task(self.clean_command, [], lambda: self.clean()) diff --git a/tha4/src/tha4/pytasuku/indexed/simple_no_index_file_tasks.py b/tha4/src/tha4/pytasuku/indexed/simple_no_index_file_tasks.py new file mode 100644 index 0000000..d4d1ed5 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/simple_no_index_file_tasks.py @@ -0,0 +1,28 @@ +from typing import Callable, List, Optional + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.no_index_file_tasks import NoIndexFileTasks + + +class SimpleNoIndexFileTasks(NoIndexFileTasks): + def __init__(self, + workspace: Workspace, + prefix: str, + command_name: str, + file_name: str, + run_func: Callable[[], None], + dependencies: Optional[List[str]] = None): + super().__init__(workspace, prefix, command_name, define_tasks_immediately=False) + if dependencies is None: + dependencies = [] + self.run_func = run_func + self._file_name = file_name + self.dependencies = dependencies + self.define_tasks() + + @property + def file_name(self): + return self._file_name + + def create_file_task(self): + self.workspace.create_file_task(self.file_name, self.dependencies, self.run_func) diff --git a/tha4/src/tha4/pytasuku/indexed/two_indices_file_tasks.py b/tha4/src/tha4/pytasuku/indexed/two_indices_file_tasks.py new file mode 100644 index 0000000..d3d36e0 --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/two_indices_file_tasks.py @@ -0,0 +1,67 @@ +import abc +from typing import List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.indexed_file_tasks import IndexedFileTasks +from tha4.pytasuku.indexed.util import delete_file + + +class TwoIndicesFileTasks(IndexedFileTasks, abc.ABC): + def __init__(self, workspace: Workspace, prefix: str, command_name: str, + count0: int, count1: int, define_tasks_immediately: bool = True): + super().__init__(workspace, prefix) + self.count1 = count1 + self.count0 = count0 + self.command_name = command_name + self.file_list_ = [] + if define_tasks_immediately: + self.define_tasks() + + @property + def run_command(self) -> str: + return self.prefix + "/" + self.command_name + + @property + def clean_command(self) -> str: + return self.prefix + "/" + self.command_name + "_clean" + + @property + def shape(self) -> List[int]: + return [self.count0, self.count1] + + @property + def arity(self) -> int: + return 2 + + @abc.abstractmethod + def file_name(self, index0: int, index1: int) -> str: + pass + + @property + def file_list(self) -> List[str]: + if len(self.file_list_) == 0: + for i in range(self.count0): + for j in range(self.count1): + self.file_list_.append(self.file_name(i, j)) + return self.file_list_ + + @abc.abstractmethod + def create_file_tasks(self, index0: int, index1: int): + pass + + def get_file_name(self, *indices: int) -> str: + if len(indices) != 2: + raise IndexError("TwoIndicesFileTasks.get_file_name require two indices, " + + "but not exactly 2 indices were provide") + return self.file_name(indices[0], indices[1]) + + def clean(self): + for file in self.file_list: + delete_file(file) + + def define_tasks(self): + for index0 in range(self.count0): + for index1 in range(self.count1): + self.create_file_tasks(index0, index1) + self.workspace.create_command_task(self.run_command, self.file_list) + self.workspace.create_command_task(self.clean_command, [], lambda: self.clean()) diff --git a/tha4/src/tha4/pytasuku/indexed/util.py b/tha4/src/tha4/pytasuku/indexed/util.py new file mode 100644 index 0000000..c1d9b4b --- /dev/null +++ b/tha4/src/tha4/pytasuku/indexed/util.py @@ -0,0 +1,71 @@ +import os +from typing import Iterable, Dict, Callable, List + +from tha4.pytasuku.workspace import Workspace +from tha4.pytasuku.indexed.all_tasks import AllTasks +from tha4.pytasuku.indexed.indexed_tasks import IndexedTasks + + +def delete_file(file_name): + if os.path.exists(file_name): + os.remove(file_name) + print("[delete] " + file_name) + else: + print("[not exist] " + file_name) + + +def all_tasks_from_named_tasks_map( + workspace: Workspace, + prefix: str, + tasks: Iterable[Dict[str, IndexedTasks]], + define_all_tasks: bool = True) \ + -> Dict[str, IndexedTasks]: + subtasks = [x for x in tasks] + name_to_subtask_list = {} + for a_subtasks in subtasks: + for name in a_subtasks: + if not define_all_tasks and name == "all": + continue + if name not in name_to_subtask_list: + name_to_subtask_list[name] = [] + name_to_subtask_list[name].append(a_subtasks[name]) + output = {} + for name in name_to_subtask_list: + output[name] = AllTasks(workspace, prefix, name_to_subtask_list[name], name) + return output + + +def create_tasks_hierarchy_helper( + workspace: Workspace, + prefix: str, + tasks_func: Callable[[Workspace, str, List[str]], Dict[str, IndexedTasks]], + branches: List[List[str]], + path: List[str]): + if len(branches) == 0: + return tasks_func(workspace, prefix, path) + else: + tasks = {} + for branch in branches[0]: + output_tasks = create_tasks_hierarchy_helper( + workspace, + f"{prefix}/{branch}", + tasks_func, + branches[1:], + path + [branch]) + if output_tasks is not None: + tasks[branch] = output_tasks + return all_tasks_from_named_tasks_map(workspace, prefix, tasks.values()) + + +def create_task_hierarchy( + workspace: Workspace, + prefix: str, + tasks_func: Callable[[Workspace, str, List[str]], Dict[str, IndexedTasks]], + branches: List[List[str]]) -> Dict[str, IndexedTasks]: + return create_tasks_hierarchy_helper(workspace, prefix, tasks_func, branches, []) + + +def write_done_file(file_name: str): + os.makedirs(os.path.dirname(file_name), exist_ok=True) + with open(file_name, "wt") as fout: + fout.write("DONE!!!") \ No newline at end of file diff --git a/tha4/src/tha4/pytasuku/task.py b/tha4/src/tha4/pytasuku/task.py new file mode 100644 index 0000000..f6909f9 --- /dev/null +++ b/tha4/src/tha4/pytasuku/task.py @@ -0,0 +1,100 @@ +import os +import logging +from typing import List + + +class Task: + def __init__(self, workspace: 'Workspace', name: str, dependencies: List[str]): + self._workspace = workspace + self._name = name + self._dependencies = dependencies + self._workspace.add_task(self) + + def run(self): + pass + + @property + def can_run(self) -> bool: + return True + + @property + def needs_to_be_run(self) -> bool: + return False + + @property + def name(self) -> str: + return self._name + + @property + def dependencies(self) -> List[str]: + return self._dependencies + + @property + def workspace(self) -> 'Workspace': + return self._workspace + + @property + def timestamp(self) -> float: + return float("inf") + + +class CommandTask(Task): + def __init__(self, workspace, name, dependencies): + super().__init__(workspace, name, dependencies) + + @property + def needs_to_be_run(self): + return True + + +class PlaceholderTask(Task): + def __init__(self, workspace, name): + super().__init__(workspace, name, []) + + @property + def can_run(self): + return False + + def run(self): + raise Exception("A placeholder task cannot be run! (%s)" % self.name) + + @property + def needs_to_be_run(self): + return not os.path.isfile(self.name) + + @property + def timestamp(self) -> float: + if not os.path.isfile(self.name): + return float("inf") + else: + return os.path.getmtime(self.name) + + +class FileTask(Task): + def __init__(self, workspace, name, dependencies): + super().__init__(workspace, name, dependencies) + + @property + def timestamp(self): + return os.path.getmtime(self.name) + + @property + def needs_to_be_run(self): + if not os.path.isfile(self.name): + logging.info("Task %s will be run because the corresponding file does not exist." % self.name) + return True + for dep in self.dependencies: + if self.workspace.needs_to_run(dep): + logging.info("Task %s will be run because dependency %s also needs to be run." % (self.name, dep)) + return True + else: + self_timestamp = self.timestamp + dep_task = self.workspace.get_task(dep) + if dep_task.timestamp > self_timestamp: + if isinstance(dep_task, FileTask) or isinstance(dep_task, PlaceholderTask): + logging.info("Task %s needs to be run because task %s has later timestamp." % + (self.name, dep)) + elif isinstance(dep_task, CommandTask): + logging.info("Task %s needs to be run because task %s is a command." % (self.name, dep)) + return True + return False diff --git a/tha4/src/tha4/pytasuku/task_selector_ui.py b/tha4/src/tha4/pytasuku/task_selector_ui.py new file mode 100644 index 0000000..94ba8b9 --- /dev/null +++ b/tha4/src/tha4/pytasuku/task_selector_ui.py @@ -0,0 +1,113 @@ +from tkinter import Tk, BOTH, Button, RIGHT, Scrollbar +from tkinter.ttk import Frame, Treeview + +from tha4.pytasuku.workspace import Workspace, PlaceholderTask + + +class TaskSelectorUi(Frame): + def __init__(self, root, workspace: Workspace): + super().__init__() + self.root = root + self.workspace = workspace + self.master.title("Tasks") + self.master.geometry("256x512") + + treeview_frame = Frame(self) + treeview_frame.pack(fill=BOTH, expand=True) + + self.treeview = Treeview(treeview_frame) + self.treeview["columns"] = ("task_name") + self.treeview.column("#0", width=256, minwidth=256) + self.treeview.heading("#0", text="Tree") + self.treeview.heading("task_name", text="Task Name") + + treeview_vertical_scroll = Scrollbar(treeview_frame, + orient='vertical', + command=self.treeview.yview) + self.treeview.configure(yscrollcommand=treeview_vertical_scroll.set) + treeview_vertical_scroll.pack(side=RIGHT, fill='y') + self.treeview.pack(fill=BOTH, expand=True) + + treeview_horizontal_scroll = Scrollbar(treeview_frame, + orient='horizontal', + command=self.treeview.xview) + self.treeview.configure(xscrollcommand=treeview_horizontal_scroll.set) + treeview_horizontal_scroll.pack(fill='x') + + self.add_tree_nodes() + + self.execute_button = Button(self, text="Execute!", command=self.run_selected_task) + self.execute_button.pack(side=RIGHT, padx=5, pady=5) + + self.pack(fill=BOTH, expand=True) + + self.selected_task_name = None + + def add_tree_nodes(self): + nodes = {} + + for task in self.workspace._tasks.values(): + if isinstance(task, PlaceholderTask): + continue + comps = task.name.split('/') + for i in range(1, len(comps)): + assert len(comps) > 0 + prefix = "" + index = 0 + for comp in comps: + index = index + 1 + parent = prefix + if prefix == "" and comp == "": + prefix = "/" + elif prefix == "": + prefix = prefix + comp + elif prefix == "/": + prefix = prefix + comp + else: + prefix = prefix + "/" + comp + if prefix in nodes: + continue + if index == len(comps): + data = prefix + else: + data = "" + if prefix == "/": + comp = "/" + nodes[prefix] = { + "name": str(prefix), + "display_name": comp, + "parent": parent, + "data": data + } + + sorted_node_names = sorted(nodes.keys()) + node_index = {} + for name in sorted_node_names: + node = nodes[name] + if node["parent"] == "": + id = self.treeview.insert("", "end", text=node["display_name"], values=node["data"], ) + else: + parent = node_index[node["parent"]] + id = self.treeview.insert(parent, "end", text=node["display_name"], values=node["data"], ) + node_index[node["name"]] = id + + def run_selected_task(self): + selection = self.treeview.selection() + item = self.treeview.item(selection) + if item['values'] == "": + return + task_name = item["values"][0] + self.selected_task_name = task_name + self.root.destroy() + + +def run_task_selector_ui(workspace: Workspace): + root = Tk() + task_selector_ui = TaskSelectorUi(root, workspace=workspace) + root.mainloop() + + task_name = task_selector_ui.selected_task_name + if task_name is not None: + print("Running", task_name, "...") + with workspace.session(): + workspace.run(task_name) diff --git a/tha4/src/tha4/pytasuku/util.py b/tha4/src/tha4/pytasuku/util.py new file mode 100644 index 0000000..24da79b --- /dev/null +++ b/tha4/src/tha4/pytasuku/util.py @@ -0,0 +1,15 @@ +import os.path +from typing import List +import logging + +from tha4.pytasuku.workspace import Workspace + + +def create_delete_all_task(workspace: Workspace, name: str, files: List[str]): + def delete_all(): + for file in files: + if os.path.exists(file): + logging.info("Removing %s ..." % file) + os.remove(file) + + workspace.create_command_task(name, [], delete_all) diff --git a/tha4/src/tha4/pytasuku/workspace.py b/tha4/src/tha4/pytasuku/workspace.py new file mode 100644 index 0000000..856ac0e --- /dev/null +++ b/tha4/src/tha4/pytasuku/workspace.py @@ -0,0 +1,168 @@ +from contextlib import contextmanager +from enum import Enum +from typing import List + +from tha4.pytasuku.task import Task, CommandTask, FileTask, PlaceholderTask + + +class WorkspaceState(Enum): + OUT_OF_SESSION = 1 + IN_SESSION = 2 + + +class NodeState(Enum): + IN_STACK = 1 + VISITED = 2 + + +class FuncCommandTask(CommandTask): + def __init__(self, workspace, name, dependencies, func): + super().__init__(workspace, name, dependencies) + self._func = func + + def run(self): + self._func() + + +class FuncFileTask(FileTask): + def __init__(self, workspace, name, dependencies, func): + super().__init__(workspace, name, dependencies) + self._func = func + + def run(self): + self._func() + + +def do_nothing(): + pass + + +class Workspace: + def __init__(self): + self._tasks = dict() + self._name_to_done = None + self._state = WorkspaceState.OUT_OF_SESSION + self._modified = False + + @property + def modified(self) -> bool: + return self._modified + + @property + def state(self) -> WorkspaceState: + return self._state + + @property + def in_session(self) -> bool: + return self._state == WorkspaceState.IN_SESSION + + def task_exists(self, name: str) -> bool: + return name in self._tasks + + def task_exists_and_not_placeholder(self, name: str) -> bool: + return self.task_exists(name) and not isinstance(self.get_task(name), PlaceholderTask) + + def get_task(self, name: str) -> Task: + return self._tasks[name] + + def add_task(self, task): + if self.in_session: + raise RuntimeError("New tasks can only be created when the workspace is out of session.") + if isinstance(task, PlaceholderTask): + if not self.task_exists(task.name): + self._tasks[task.name] = task + self._modified = True + else: + self._tasks[task.name] = task + for dep in task.dependencies: + PlaceholderTask(self, dep) + self._modified = True + + def start_session(self): + if self.in_session: + raise RuntimeError("A session can only be started when the workspace is out of session.") + if self.modified: + self.check_cycle() + self._state = WorkspaceState.IN_SESSION + self._name_to_done = dict() + self._modified = False + + def end_session(self): + if not self.in_session: + raise RuntimeError("A session can only be ended when the workspace is in session.") + self._state = WorkspaceState.OUT_OF_SESSION + self._name_to_done = None + + @contextmanager + def session(self): + try: + self.start_session() + yield + finally: + self.end_session() + + def check_cycle(self): + node_states = dict() + for name in self._tasks: + if name not in node_states: + self.dfs(name, node_states) + + def dfs(self, name, node_states): + node_states[name] = NodeState.IN_STACK + task = self.get_task(name) + for dep in task.dependencies: + if dep not in node_states: + self.dfs(dep, node_states) + else: + state = node_states[dep] + if state == NodeState.IN_STACK: + raise RuntimeError("Dicovered cyclic dependency!") + node_states[name] = NodeState.VISITED + + def run(self, name): + if not self.in_session: + raise RuntimeError("A task can only be run when the workspace is in session.") + if not self.task_exists(name): + raise RuntimeError("Task %s does not exists" % name) + self.run_helper(name) + + def run_helper(self, name): + task = self.get_task(name) + for dep in task.dependencies: + if self.needs_to_run(dep): + self.run_helper(dep) + if self.needs_to_run(name): + task.run() + self._name_to_done[name] = True + + def needs_to_run(self, name): + if not self.in_session: + raise RuntimeError("You can only check whether a task needs to run when the workspace is in session.") + if name in self._name_to_done: + return not self._name_to_done[name] + task = self.get_task(name) + need_to_run_value = task.needs_to_be_run + self._name_to_done[name] = not need_to_run_value + return need_to_run_value + + def create_command_task(self, name, dependencies, func=do_nothing): + return FuncCommandTask(self, name, dependencies, func) + + def create_file_task(self, name, dependencies, func): + return FuncFileTask(self, name, dependencies, func) + + +def command_task(workspace: Workspace, name: str, dependencies: List[str]): + def func(f): + workspace.create_command_task(name, dependencies, f) + return f + + return func + + +def file_task(workspace: Workspace, name: str, dependencies: List[str]): + def func(f): + workspace.create_file_task(name, dependencies, f) + return f + + return func diff --git a/tha4/src/tha4/sampleoutput/__init__.py b/tha4/src/tha4/sampleoutput/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/sampleoutput/general_sample_output_protocol.py b/tha4/src/tha4/sampleoutput/general_sample_output_protocol.py new file mode 100644 index 0000000..4bf8c2f --- /dev/null +++ b/tha4/src/tha4/sampleoutput/general_sample_output_protocol.py @@ -0,0 +1,142 @@ +import os +from enum import Enum +from typing import List, Dict + +import PIL.Image +import numpy +import torch +from tha4.shion.base.dataset.util import get_indexed_batch +from tha4.shion.base.image_util import pytorch_rgb_to_numpy_image, pytorch_rgba_to_numpy_image +from tha4.shion.core.cached_computation import TensorCachedComputationFunc, ComputationState +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.image_util import grid_change_to_numpy_image +from torch.nn import Module +from torch.nn.functional import interpolate +from torch.utils.data import Dataset + + +class ImageType(Enum): + COLOR = 0 + ALPHA = 1 + GRID_CHANGE = 2 + SIGMOID_LOGIT = 3 + + +class SampleImageSpec: + def __init__(self, value_func: TensorCachedComputationFunc, image_type: ImageType): + self.value_func = value_func + self.image_type = image_type + + +class SampleImageSaver: + def __init__(self, + cell_size: int, + image_channels: int, + sample_image_specs: List[SampleImageSpec]): + super().__init__() + self.sample_image_specs = sample_image_specs + self.cell_size = cell_size + self.image_channels = image_channels + + def save_sample_output_data(self, + state: ComputationState, + prefix: str, + examples_seen_so_far: int): + num_cols = len(self.sample_image_specs) + num_rows = state.batch[0].shape[0] + output_image = numpy.zeros([self.cell_size * num_rows, self.cell_size * num_cols, self.image_channels]) + + for col in range(num_cols): + spec = self.sample_image_specs[col] + images = spec.value_func(state) + start_col = col * self.cell_size + + for image_index in range(num_rows): + image = images[image_index].clone().detach() + row = image_index + start_row = row * self.cell_size + if spec.image_type == ImageType.COLOR: + c, h, w = image.shape + green_screen = torch.ones(3, h, w, device=image.device) * -1.0 + green_screen[1, :, :] = 1.0 + alpha = (image[3:4, :, :] + 1.0) * 0.5 + image[0:3, :, :] = image[0:3, :, :] * alpha + green_screen * (1 - alpha) + image[3:4, :, :] = 1.0 + image = image.cpu() + elif spec.image_type == ImageType.GRID_CHANGE: + image = image.cpu() + elif spec.image_type == ImageType.SIGMOID_LOGIT: + image = torch.sigmoid(image) + image = image.repeat(self.image_channels, 1, 1) + image = image * 2.0 - 1.0 + image = image.cpu() + elif spec.image_type == ImageType.ALPHA: + if image.shape[0] == 1: + image = image.repeat(self.image_channels, 1, 1) + image = image * 2.0 - 1.0 + image = image.cpu() + else: + raise RuntimeError(f"Unsupported image type: {spec.image_type}") + + output_image[start_row:start_row + self.cell_size, start_col:start_col + self.cell_size, :] \ + = self.convert_to_numpy_image(image) + + file_name = "%s/sample_output_%010d.png" % (prefix, examples_seen_so_far) + os.makedirs(os.path.dirname(file_name), exist_ok=True) + if self.image_channels == 3: + mode = 'RGB' + else: + mode = 'RGBA' + pil_image = PIL.Image.fromarray(numpy.uint8(numpy.rint(output_image * 255.0)), mode=mode) + pil_image.save(file_name) + print("Saved %s" % file_name) + + def convert_to_numpy_image(self, image: torch.Tensor): + image_size = image.shape[-1] + if self.cell_size != image_size: + image = interpolate(image.unsqueeze(0), size=self.cell_size).squeeze(0) + if image.shape[0] == 2: + return grid_change_to_numpy_image(image, num_channels=self.image_channels) + elif self.image_channels == 3: + return pytorch_rgb_to_numpy_image(image) + else: + return pytorch_rgba_to_numpy_image(image) + + +class GeneralSampleOutputProtocol(SampleOutputProtocol): + def __init__(self, + sample_image_specs: List[SampleImageSpec], + num_images: int = 8, + cell_size: int = 256, + image_channels: int = 4, + examples_per_sample_output: int = 5000, + random_seed: int = 1203040687): + super().__init__() + self.num_images = num_images + self.random_seed = random_seed + self.examples_per_sample_output = examples_per_sample_output + self.sample_image_saver = SampleImageSaver(cell_size, image_channels, sample_image_specs) + + def get_examples_per_sample_output(self) -> int: + return self.examples_per_sample_output + + def get_random_seed(self) -> int: + return self.random_seed + + def get_sample_output_data(self, validation_dataset: Dataset, device: torch.device) -> dict: + example_indices = torch.randint(0, len(validation_dataset), (self.num_images,)) + example_indices = [example_indices[i].item() for i in range(self.num_images)] + batch = get_indexed_batch(validation_dataset, example_indices, device) + return {'batch': batch} + + def save_sample_output_data(self, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + sample_output_data: dict, prefix: str, + examples_seen_so_far: int, + device: torch.device): + for key in modules: + modules[key].train(False) + batch = sample_output_data['batch'] + state = ComputationState(modules, accumulated_modules, batch, {}) + self.sample_image_saver.save_sample_output_data(state, prefix, examples_seen_so_far) diff --git a/tha4/src/tha4/sampleoutput/poser_sampler_output_protocol.py b/tha4/src/tha4/sampleoutput/poser_sampler_output_protocol.py new file mode 100644 index 0000000..aba1fe9 --- /dev/null +++ b/tha4/src/tha4/sampleoutput/poser_sampler_output_protocol.py @@ -0,0 +1,73 @@ +from typing import Optional, List, Dict + +import torch +from torch.nn import Module +from torch.utils.data import Dataset + +from tha4.shion.base.dataset.util import get_indexed_batch +from tha4.shion.core.cached_computation import CachedComputationFunc, ComputationState +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.sampleoutput.sample_image_creator import SampleImageSpec, ImageSource, ImageType, SampleImageSaver + + +class PoserSampleOutputProtocol(SampleOutputProtocol): + def __init__(self, + output_list_func: Optional[CachedComputationFunc] = None, + num_images: int = 8, + image_size: int = 256, + cell_size: int = 256, + image_channels: int = 4, + examples_per_sample_output: int = 5000, + sample_image_specs: Optional[List[SampleImageSpec]] = None, + random_seed: int = 1203040687): + super().__init__() + self.num_images = num_images + self.random_seed = random_seed + self.examples_per_sample_output = examples_per_sample_output + self.output_list_func = output_list_func + + if sample_image_specs is None: + sample_image_specs = [ + SampleImageSpec(ImageSource.BATCH, 0, ImageType.COLOR), + SampleImageSpec(ImageSource.BATCH, 2, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 0, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 1, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 2, ImageType.ALPHA), + SampleImageSpec(ImageSource.BATCH, 3, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 3, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 4, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 5, ImageType.ALPHA), + SampleImageSpec(ImageSource.OUTPUT, 6, ImageType.COLOR), + SampleImageSpec(ImageSource.BATCH, 4, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 7, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 8, ImageType.COLOR), + SampleImageSpec(ImageSource.OUTPUT, 9, ImageType.ALPHA), + SampleImageSpec(ImageSource.OUTPUT, 10, ImageType.COLOR), + ] + + self.sample_image_saver = SampleImageSaver(image_size, cell_size, image_channels, sample_image_specs) + + def get_examples_per_sample_output(self) -> int: + return self.examples_per_sample_output + + def get_random_seed(self) -> int: + return self.random_seed + + def get_sample_output_data(self, validation_dataset: Dataset, device: torch.device) -> dict: + example_indices = torch.randint(0, len(validation_dataset), (self.num_images,)) + example_indices = [example_indices[i].item() for i in range(self.num_images)] + batch = get_indexed_batch(validation_dataset, example_indices, device) + return {'batch': batch} + + def save_sample_output_data(self, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + sample_output_data: dict, prefix: str, + examples_seen_so_far: int, + device: torch.device): + for key in modules: + modules[key].train(False) + batch = sample_output_data['batch'] + with torch.no_grad(): + outputs = self.output_list_func(ComputationState(modules, accumulated_modules, batch)) + self.sample_image_saver.save_sample_output_data(batch, outputs, prefix, examples_seen_so_far) diff --git a/tha4/src/tha4/sampleoutput/sample_image_creator.py b/tha4/src/tha4/sampleoutput/sample_image_creator.py new file mode 100644 index 0000000..f9504d8 --- /dev/null +++ b/tha4/src/tha4/sampleoutput/sample_image_creator.py @@ -0,0 +1,148 @@ +import math +import os +from enum import Enum +from typing import List + +import numpy +import torch +from matplotlib import cm +from torch import Tensor +from torch.nn.functional import interpolate + +from tha4.shion.base.image_util import save_numpy_image + + +class ImageSource(Enum): + BATCH = 0 + OUTPUT = 1 + + +class ImageType(Enum): + COLOR = 0 + ALPHA = 1 + GRID_CHANGE = 2 + SIGMOID_LOGIT = 3 + + +class SampleImageSpec: + def __init__(self, image_source: ImageSource, index: int, image_type: ImageType): + self.image_type = image_type + self.index = index + self.image_source = image_source + + +def torch_rgb_to_numpy_image(torch_image: Tensor, min_pixel_value=-1.0, max_pixel_value=1.0): + assert torch_image.dim() == 3 + assert torch_image.shape[0] == 3 + height = torch_image.shape[1] + width = torch_image.shape[2] + + reshaped_image = torch_image.numpy().reshape(3, height * width).transpose().reshape(height, width, 3) + numpy_image = (reshaped_image - min_pixel_value) / (max_pixel_value - min_pixel_value) + return numpy_image + + +def torch_rgba_to_numpy_image(torch_image: Tensor, min_pixel_value=-1.0, max_pixel_value=1.0): + assert torch_image.dim() == 3 + assert torch_image.shape[0] == 4 + height = torch_image.shape[1] + width = torch_image.shape[2] + + reshaped_image = torch_image.numpy().reshape(4, height * width).transpose().reshape(height, width, 4) + numpy_image = (reshaped_image - min_pixel_value) / (max_pixel_value - min_pixel_value) + numpy_image = numpy.clip(numpy_image, 0.0, 1.0) + return numpy_image + + +def torch_grid_change_to_numpy_image(torch_image, num_channels=3): + height = torch_image.shape[1] + width = torch_image.shape[2] + size_image = (torch_image[0, :, :] ** 2 + torch_image[1, :, :] ** 2).sqrt().view(height, width, 1).numpy() + hsv = cm.get_cmap('hsv') + angle_image = hsv(((torch.atan2( + torch_image[0, :, :].view(height * width), + torch_image[1, :, :].view(height * width)).view(height, width) + math.pi) / (2 * math.pi)).numpy()) * 3 + numpy_image = size_image * angle_image[:, :, 0:3] + if num_channels == 3: + return numpy_image + elif num_channels == 4: + return numpy.concatenate([numpy_image, numpy.ones_like(size_image)], axis=2) + else: + raise RuntimeError("Unsupported num_channels: " + str(num_channels)) + + +class SampleImageSaver: + def __init__(self, + image_size: int, + cell_size: int, + image_channels: int, + sample_image_specs: List[SampleImageSpec]): + super().__init__() + self.sample_image_specs = sample_image_specs + self.cell_size = cell_size + self.image_channels = image_channels + self.image_size = image_size + + def save_sample_output_image(self, batch: List[Tensor], outputs: List[Tensor], file_name: str): + num_cols = len(self.sample_image_specs) + + num_rows = batch[0].shape[0] + output_image = numpy.zeros([self.cell_size * num_rows, self.cell_size * num_cols, self.image_channels]) + + for image_index in range(num_rows): + row = image_index + start_row = row * self.cell_size + + for col in range(num_cols): + spec = self.sample_image_specs[col] + start_col = col * self.cell_size + + if spec.image_source == ImageSource.BATCH: + image = batch[spec.index][image_index].clone() + else: + image = outputs[spec.index][image_index].clone() + + if spec.image_type == ImageType.COLOR: + c, h, w = image.shape + green_screen = torch.ones(3, h, w, device=image.device) * -1.0 + green_screen[1, :, :] = 1.0 + alpha = (image[3:4, :, :] + 1.0) * 0.5 + image[0:3, :, :] = image[0:3, :, :] * alpha + green_screen * (1 - alpha) + image[3:4, :, :] = 1.0 + image = image.detach().cpu() + elif spec.image_type == ImageType.GRID_CHANGE: + image = image.detach().cpu() + elif spec.image_type == ImageType.SIGMOID_LOGIT: + image = torch.sigmoid(image) + image = image.repeat(self.image_channels, 1, 1) + image = image * 2.0 - 1.0 + image = image.detach().cpu() + else: + if image.shape[0] == 1: + image = image.repeat(self.image_channels, 1, 1) + image = image * 2.0 - 1.0 + image = image.detach().cpu() + + output_image[start_row:start_row + self.cell_size, start_col:start_col + self.cell_size, :] \ + = self.convert_to_numpy_image(image) + + os.makedirs(os.path.dirname(file_name), exist_ok=True) + save_numpy_image(output_image, file_name, save_straight_alpha=True) + + def save_sample_output_data(self, + batch: List[Tensor], + outputs: List[Tensor], + prefix: str, + examples_seen_so_far: int): + file_name = "%s/sample_output_%010d.png" % (prefix, examples_seen_so_far) + self.save_sample_output_image(batch, outputs, file_name) + + def convert_to_numpy_image(self, image: torch.Tensor): + if self.cell_size != self.image_size: + image = interpolate(image.unsqueeze(0), size=self.cell_size).squeeze(0) + if image.shape[0] == 2: + return torch_grid_change_to_numpy_image(image, num_channels=self.image_channels) + elif self.image_channels == 3: + return torch_rgb_to_numpy_image(image) + else: + return torch_rgba_to_numpy_image(image) diff --git a/tha4/src/tha4/shion/__init__.py b/tha4/src/tha4/shion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/base/__init__.py b/tha4/src/tha4/shion/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/base/dataset/__init__.py b/tha4/src/tha4/shion/base/dataset/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/base/dataset/lazy_dataset.py b/tha4/src/tha4/shion/base/dataset/lazy_dataset.py new file mode 100644 index 0000000..2678878 --- /dev/null +++ b/tha4/src/tha4/shion/base/dataset/lazy_dataset.py @@ -0,0 +1,20 @@ +from typing import Callable + +from torch.utils.data import Dataset + + +class LazyDataset(Dataset): + def __init__(self, source_func: Callable[[], Dataset]): + self.source_func = source_func + self.source = None + + def get_source(self): + if self.source is None: + self.source = self.source_func() + return self.source + + def __len__(self): + return len(self.get_source()) + + def __getitem__(self, item): + return self.get_source()[item] diff --git a/tha4/src/tha4/shion/base/dataset/lazy_tensor_dataset.py b/tha4/src/tha4/shion/base/dataset/lazy_tensor_dataset.py new file mode 100644 index 0000000..f330f69 --- /dev/null +++ b/tha4/src/tha4/shion/base/dataset/lazy_tensor_dataset.py @@ -0,0 +1,33 @@ +import torch +from torch.utils.data import Dataset, TensorDataset + +from tha4.shion.core.load_save import torch_load + + +class LazyTensorDataset(Dataset): + def __init__(self, file_name: str): + self.file_name = file_name + self.dataset = None + + def get_dataset(self): + if self.dataset is None: + data = torch_load(self.file_name) + if isinstance(data, torch.Tensor): + self.dataset = TensorDataset(data) + elif isinstance(data, tuple): + self.dataset = TensorDataset(*data) + elif isinstance(data, list): + self.dataset = TensorDataset(*data) + else: + raise RuntimeError("Unsupported data type: " + type(data)) + return self.dataset + + def __len__(self): + dataset = self.get_dataset() + return len(dataset) + + def __getitem__(self, item): + dataset = self.get_dataset() + return dataset.__getitem__(item) + + diff --git a/tha4/src/tha4/shion/base/dataset/png_in_dir_dataset.py b/tha4/src/tha4/shion/base/dataset/png_in_dir_dataset.py new file mode 100644 index 0000000..9d11ef0 --- /dev/null +++ b/tha4/src/tha4/shion/base/dataset/png_in_dir_dataset.py @@ -0,0 +1,53 @@ +import os + +from torch.nn import functional +from torch.utils.data import Dataset +from os import listdir +from os.path import isfile + +from tha4.shion.base.image_util import extract_pytorch_image_from_filelike + + +class PngInDirDataset(Dataset): + def __init__(self, dir: str, + downscale_kernel_size: int = 1, + has_alpha=False, + scale=2.0, + offset=-1.0, + premultiply_alpha=True, + perfrom_srb_to_linear=True): + super().__init__() + self.perfrom_srb_to_linear = perfrom_srb_to_linear + self.premultiply_alpha = premultiply_alpha + self.offset = offset + self.scale = scale + self.has_alpha = has_alpha + self.downscale_kernel_size = downscale_kernel_size + self.dir = dir + self.file_names = None + + def get_file_names(self): + if self.file_names is None: + self.file_names = [os.path.join(self.dir, x) for x in listdir(self.dir)] + self.file_names = [x for x in self.file_names if isfile(x) and x[-4:].lower() == ".png"] + self.file_names = sorted(self.file_names) + return self.file_names + + def __len__(self): + file_names = self.get_file_names() + return len(file_names) + + def __getitem__(self, item): + file_names = self.get_file_names() + file_name = file_names[item] + image = extract_pytorch_image_from_filelike( + file_name, + scale=self.scale, + offset=self.offset, + premultiply_alpha=self.has_alpha and self.premultiply_alpha, + perform_srgb_to_linear=self.perfrom_srb_to_linear) + if self.downscale_kernel_size == 1: + return [image] + else: + image = functional.avg_pool2d(image.unsqueeze(0), kernel_size=self.downscale_kernel_size).squeeze(0) + return [image] diff --git a/tha4/src/tha4/shion/base/dataset/util.py b/tha4/src/tha4/shion/base/dataset/util.py new file mode 100644 index 0000000..77c6be3 --- /dev/null +++ b/tha4/src/tha4/shion/base/dataset/util.py @@ -0,0 +1,29 @@ +from typing import List + +import torch +from torch.utils.data import Dataset + + +def get_indexed_batch(dataset: Dataset, example_indices: List[int], device: torch.device): + if len(example_indices) == 0: + return [] + examples = [] + for index in range(len(example_indices)): + example_index = example_indices[index] + raw_example = dataset[example_index] + example = [] + for x in raw_example: + if isinstance(x, torch.Tensor): + y = x.to(device).unsqueeze(0) + elif isinstance(x, float) or isinstance(x, int): + y = torch.tensor([[x]], device=device) + else: + raise RuntimeError(f"get_indexed_batch: Data of type {type(x)} is not supported.") + example.append(y) + examples.append(example) + k = len(examples[0]) + transposed = [[] for i in range(k)] + for example in examples: + for i in range(k): + transposed[i].append(example[i]) + return [torch.cat(x, dim=0) for x in transposed] diff --git a/tha4/src/tha4/shion/base/dataset/xformed_dataset.py b/tha4/src/tha4/shion/base/dataset/xformed_dataset.py new file mode 100644 index 0000000..169b134 --- /dev/null +++ b/tha4/src/tha4/shion/base/dataset/xformed_dataset.py @@ -0,0 +1,15 @@ +from typing import Any, Callable + +from torch.utils.data import Dataset + + +class XformedDataset(Dataset): + def __init__(self, source: Dataset, xform_func: Callable[[Any], Any]): + self.xform_func = xform_func + self.source = source + + def __len__(self): + return len(self.source) + + def __getitem__(self, item): + return self.xform_func(self.source[item]) diff --git a/tha4/src/tha4/shion/base/image_util.py b/tha4/src/tha4/shion/base/image_util.py new file mode 100644 index 0000000..c89a962 --- /dev/null +++ b/tha4/src/tha4/shion/base/image_util.py @@ -0,0 +1,243 @@ +import os + +import PIL.Image +import numpy +import torch +from matplotlib import pyplot +from torch import Tensor + + +def numpy_srgb_to_linear(x): + x = numpy.clip(x, 0.0, 1.0) + return numpy.where(x <= 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4) + + +def numpy_linear_to_srgb(x): + x = numpy.clip(x, 0.0, 1.0) + return numpy.where(x <= 0.003130804953560372, x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) + + +def numpy_alpha_devide(rgb, a, epsilon=1e-5): + aaa = numpy.repeat(a, 3, axis=2) + aaa_prime = aaa + numpy.where(numpy.abs(aaa) < epsilon, epsilon, 0.0) + return numpy.where(numpy.abs(aaa) < epsilon, 0.0, rgb / aaa_prime) + + +def torch_srgb_to_linear(x: torch.Tensor): + x = torch.clip(x, 0.0, 1.0) + return torch.where(torch.le(x, 0.04045), x / 12.92, ((x + 0.055) / 1.055) ** 2.4) + + +def torch_linear_to_srgb(x): + x = torch.clip(x, 0.0, 1.0) + return torch.where(torch.le(x, 0.003130804953560372), x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) + + +def numpy_image_linear_to_srgb(image): + assert image.shape[2] == 3 or image.shape[2] == 4 + if image.shape[2] == 3: + return numpy_linear_to_srgb(image) + else: + height, width, _ = image.shape + rgb_image = numpy_linear_to_srgb(image[:, :, 0:3]) + a_image = image[:, :, 3:4] + return numpy.concatenate((rgb_image, a_image), axis=2) + + +def numpy_image_srgb_to_linear(image): + assert image.shape[2] == 3 or image.shape[2] == 4 + if image.shape[2] == 3: + return numpy_srgb_to_linear(image) + else: + height, width, _ = image.shape + rgb_image = numpy_srgb_to_linear(image[:, :, 0:3]) + a_image = image[:, :, 3:4] + return numpy.concatenate((rgb_image, a_image), axis=2) + + +def pytorch_rgb_to_numpy_image(torch_image: Tensor, min_pixel_value=-1.0, max_pixel_value=1.0): + assert torch_image.dim() == 3 + assert torch_image.shape[0] == 3 + height = torch_image.shape[1] + width = torch_image.shape[2] + + reshaped_image = torch_image.numpy().reshape(3, height * width).transpose().reshape(height, width, 3) + numpy_image = (reshaped_image - min_pixel_value) / (max_pixel_value - min_pixel_value) + return numpy_linear_to_srgb(numpy_image) + + +def pytorch_rgba_to_numpy_image_greenscreen(torch_image: Tensor, + min_pixel_value=-1.0, + max_pixel_value=1.0, + include_alpha=False): + height = torch_image.shape[1] + width = torch_image.shape[2] + + numpy_image = (torch_image.numpy().reshape(4, height * width).transpose().reshape(height, width, + 4) - min_pixel_value) \ + / (max_pixel_value - min_pixel_value) + rgb_image = numpy_linear_to_srgb(numpy_image[:, :, 0:3]) + a_image = numpy_image[:, :, 3] + rgb_image[:, :, 0:3] = rgb_image[:, :, 0:3] * a_image.reshape(a_image.shape[0], a_image.shape[1], 1) + rgb_image[:, :, 1] = rgb_image[:, :, 1] + (1 - a_image) + + if not include_alpha: + return rgb_image + else: + return numpy.concatenate((rgb_image, numpy.ones_like(numpy_image[:, :, 3:4])), axis=2) + + +def pytorch_rgba_to_numpy_image( + torch_image: Tensor, + min_pixel_value=-1.0, + max_pixel_value=1.0, + perform_linear_to_srb: bool = True): + assert torch_image.dim() == 3 + assert torch_image.shape[0] == 4 + height = torch_image.shape[1] + width = torch_image.shape[2] + + reshaped_image = torch_image.numpy().reshape(4, height * width).transpose().reshape(height, width, 4) + numpy_image = (reshaped_image - min_pixel_value) / (max_pixel_value - min_pixel_value) + if perform_linear_to_srb: + rgb_image = numpy_linear_to_srgb(numpy_image[:, :, 0:3]) + else: + rgb_image = numpy.clip(numpy_image[:, :, 0:3], 0.0, 1.0) + a_image = numpy.clip(numpy_image[:, :, 3], 0.0, 1.0) + rgba_image = numpy.concatenate((rgb_image, a_image.reshape(height, width, 1)), axis=2) + return rgba_image + + +def pil_image_has_transparency(pil_image): + if pil_image.info.get("transparency", None) is not None: + return True + if pil_image.mode == "P": + transparent = pil_image.info.get("transparency", -1) + for _, index in pil_image.getcolors(): + if index == transparent: + return True + elif pil_image.mode == "RGBA": + extrema = pil_image.getextrema() + if extrema[3][0] < 255: + return True + + return False + + +def extract_numpy_image_from_PIL_image(pil_image, scale=2.0, offset=-1.0, + premultiply_alpha=True, + perform_srgb_to_linear=True): + has_alpha = pil_image_has_transparency(pil_image) + if has_alpha and pil_image.mode != 'RGBA': + pil_image = pil_image.convert("RGBA") + if not has_alpha and pil_image.mode != 'RGB': + pil_image = pil_image.convert("RGB") + if has_alpha: + num_channel = 4 + else: + num_channel = 3 + image_width = pil_image.width + image_height = pil_image.height + + raw_image = numpy.asarray(pil_image, dtype=numpy.float32) + image = (raw_image / 255.0).reshape(image_height, image_width, num_channel) + if perform_srgb_to_linear: + image[:, :, 0:3] = numpy_srgb_to_linear(image[:, :, 0:3]) + # Premultiply alpha + if has_alpha and premultiply_alpha: + image[:, :, 0:3] = image[:, :, 0:3] * image[:, :, 3:4] + return image * scale + offset + + +def extract_numpy_image_from_PIL_image_with_pytorch_layout(pil_image, scale=2.0, offset=-1.0, + premultiply_alpha=True, + perform_srgb_to_linear=True): + numpy_image = extract_numpy_image_from_PIL_image( + pil_image, scale, offset, premultiply_alpha, perform_srgb_to_linear) + image_height, image_width, num_channel = numpy_image.shape + image = numpy_image \ + .reshape(image_height * image_width, num_channel) \ + .transpose() \ + .reshape(num_channel, image_height, image_width) + return image + + +def extract_numpy_image_from_filelike_with_pytorch_layout(file, scale=2.0, offset=-1.0, premultiply_alpha=True): + try: + pil_image = PIL.Image.open(file) + except Exception as e: + raise RuntimeError(file) + return extract_numpy_image_from_PIL_image_with_pytorch_layout(pil_image, scale, offset, premultiply_alpha) + + +def extract_numpy_image_from_filelike(file, scale=1.0, offset=0.0, + premultiply_alpha=True, + perform_srgb_to_linear: bool = True): + try: + pil_image = PIL.Image.open(file) + except Exception as e: + raise RuntimeError(file) + return extract_numpy_image_from_PIL_image(pil_image, scale, offset, premultiply_alpha, perform_srgb_to_linear) + + +def extract_pytorch_image_from_filelike(file, scale=2.0, offset=-1.0, premultiply_alpha=True, + perform_srgb_to_linear=True): + try: + pil_image = PIL.Image.open(file) + except Exception as e: + raise RuntimeError(file) + image = extract_numpy_image_from_PIL_image_with_pytorch_layout(pil_image, scale, offset, premultiply_alpha, + perform_srgb_to_linear) + return torch.from_numpy(image).float() + + +def extract_pytorch_image_from_PIL_image(pil_image, scale=2.0, offset=-1.0, premultiply_alpha=True, + perform_srgb_to_linear=True): + image = extract_numpy_image_from_PIL_image_with_pytorch_layout( + pil_image, scale, offset, premultiply_alpha, perform_srgb_to_linear) + return torch.from_numpy(image).float() + + +def convert_pytorch_image_to_zero_to_one_numpy_image( + torch_image: torch.Tensor, + scale: float = 2.0, + offset: float = -1.0): + torch_image = (torch_image - offset) / scale + torch_image = torch.permute(torch_image, (1, 2, 0)) + numpy_image = torch_image.cpu().numpy() + return numpy_image + + +def convert_zero_to_one_numpy_image_to_PIL_image( + numpy_image, use_straight_alpha=True, perform_linear_to_srgb=True): + if numpy_image.shape[2] == 4: + rgb_image = numpy_image[:, :, 0:3] + a_image = numpy.clip(numpy_image[:, :, 3:4], 0.0, 1.0) + if use_straight_alpha: + rgb_image = numpy_alpha_devide(rgb_image, a_image) + if perform_linear_to_srgb: + rgb_image = numpy_linear_to_srgb(rgb_image) + else: + rgb_image = numpy.clip(rgb_image, 0.0, 1.0) + new_numpy_image = numpy.concatenate((rgb_image, a_image), axis=2) + pil_image = PIL.Image.fromarray(numpy.uint8(numpy.rint(new_numpy_image * 255.0)), mode='RGBA') + else: + if perform_linear_to_srgb: + numpy_image = numpy_linear_to_srgb(numpy_image) + else: + numpy_image = numpy.clip(numpy_image, 0.0, 1.0) + pil_image = PIL.Image.fromarray(numpy.uint8(numpy.rint(numpy_image * 255.0)), mode='RGB') + return pil_image + + +def save_numpy_image(numpy_image, file_name: str, save_straight_alpha=True, perform_linear_to_srgb=True): + pil_image = convert_zero_to_one_numpy_image_to_PIL_image(numpy_image, save_straight_alpha, perform_linear_to_srgb) + os.makedirs(os.path.dirname(file_name), exist_ok=True) + pil_image.save(file_name) + + +def resize_PIL_image(pil_image, size=(256, 256)): + w, h = pil_image.size + d = min(w, h) + r = ((w - d) // 2, (h - d) // 2, (w + d) // 2, (h + d) // 2) + return pil_image.resize(size, resample=PIL.Image.LANCZOS, box=r) \ No newline at end of file diff --git a/tha4/src/tha4/shion/base/loss/__init__.py b/tha4/src/tha4/shion/base/loss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/base/loss/computed_scale_loss.py b/tha4/src/tha4/shion/base/loss/computed_scale_loss.py new file mode 100644 index 0000000..18cb115 --- /dev/null +++ b/tha4/src/tha4/shion/base/loss/computed_scale_loss.py @@ -0,0 +1,22 @@ +from typing import Optional, Callable + +from tha4.shion.core.cached_computation import TensorCachedComputationFunc, ComputationState +from tha4.shion.core.loss import Loss + + +class ComputedScaleLoss(Loss): + def __init__(self, + scale_func: TensorCachedComputationFunc, + loss: Loss, + weight: float = 1.0): + self.weight = weight + self.loss = loss + self.scale_func = scale_func + + def compute(self, state: ComputationState, log_func: Optional[Callable[[str, float], None]] = None): + loss = self.loss.compute(state) + scale = self.scale_func(state) + loss = self.weight * scale * loss + if log_func is not None: + log_func("loss", loss.item()) + return loss diff --git a/tha4/src/tha4/shion/base/loss/computed_scaled_l2_loss.py b/tha4/src/tha4/shion/base/loss/computed_scaled_l2_loss.py new file mode 100644 index 0000000..b592b86 --- /dev/null +++ b/tha4/src/tha4/shion/base/loss/computed_scaled_l2_loss.py @@ -0,0 +1,29 @@ +from typing import Callable, Optional + +from tha4.shion.core.cached_computation import TensorCachedComputationFunc, ComputationState +from tha4.shion.core.loss import Loss + + +class ComputedScaledL2Loss(Loss): + def __init__(self, + expected_func: TensorCachedComputationFunc, + actual_func: TensorCachedComputationFunc, + element_scale_func: TensorCachedComputationFunc, + weight: float = 1.0): + self.element_scale_func = element_scale_func + self.actual_func = actual_func + self.expected_func = expected_func + self.weight = weight + + def compute( + self, + state: ComputationState, + log_func: Optional[Callable[[str, float], None]] = None): + element_scale = self.element_scale_func(state) + expected = self.expected_func(state) + actual = self.actual_func(state) + diff = (expected - actual) * element_scale + loss = self.weight * (diff ** 2).mean() + if log_func is not None: + log_func("loss", loss.item()) + return loss diff --git a/tha4/src/tha4/shion/base/loss/l1_loss.py b/tha4/src/tha4/shion/base/loss/l1_loss.py new file mode 100644 index 0000000..1ed7314 --- /dev/null +++ b/tha4/src/tha4/shion/base/loss/l1_loss.py @@ -0,0 +1,67 @@ +from typing import Callable, Optional + +import torch + +from tha4.shion.core.cached_computation import TensorCachedComputationFunc, ComputationState +from tha4.shion.core.loss import Loss + + +class L1Loss(Loss): + def __init__(self, + expected_func: TensorCachedComputationFunc, + actual_func: TensorCachedComputationFunc, + weight: float = 1.0): + self.actual_func = actual_func + self.expected_func = expected_func + self.weight = weight + + def compute(self, state: ComputationState, log_func: Optional[Callable[[str, float], None]] = None): + expected = self.expected_func(state) + actual = self.actual_func(state) + loss = self.weight * (expected - actual).abs().mean() + if log_func is not None: + log_func("loss", loss.item()) + return loss + + +class ListL1Loss(Loss): + def __init__(self, + expected_func: TensorCachedComputationFunc, + actual_func: TensorCachedComputationFunc, + weight: float = 1.0): + self.actual_func = actual_func + self.expected_func = expected_func + self.weight = weight + + def compute(self, state: ComputationState, log_func: Optional[Callable[[str, float], None]] = None): + expected = self.expected_func(state) + actual = self.actual_func(state) + assert len(expected) == len(actual) + loss = torch.zeros(1, device=expected[0].device) + for i in range(len(expected)): + loss += (expected[i] - actual[i]).abs().mean() + loss = self.weight * loss + if log_func is not None: + log_func("loss", loss.item()) + return loss + + +class MaskedL1Loss(Loss): + def __init__(self, + expected_func: TensorCachedComputationFunc, + actual_func: TensorCachedComputationFunc, + mask_func: TensorCachedComputationFunc, + weight: float = 1.0): + self.mask_func = mask_func + self.actual_func = actual_func + self.expected_func = expected_func + self.weight = weight + + def compute(self, state: ComputationState, log_func: Optional[Callable[[str, float], None]] = None): + mask = self.mask_func(state) + expected = self.expected_func(state) + actual = self.actual_func(state) + loss = self.weight * ((expected - actual) * mask).abs().mean() + if log_func is not None: + log_func("loss", loss.item()) + return loss \ No newline at end of file diff --git a/tha4/src/tha4/shion/base/loss/l2_loss.py b/tha4/src/tha4/shion/base/loss/l2_loss.py new file mode 100644 index 0000000..437ac36 --- /dev/null +++ b/tha4/src/tha4/shion/base/loss/l2_loss.py @@ -0,0 +1,25 @@ +from typing import Callable, Optional + +from tha4.shion.core.cached_computation import TensorCachedComputationFunc, ComputationState +from tha4.shion.core.loss import Loss + + +class L2Loss(Loss): + def __init__(self, + expected_func: TensorCachedComputationFunc, + actual_func: TensorCachedComputationFunc, + weight: float = 1.0): + self.actual_func = actual_func + self.expected_func = expected_func + self.weight = weight + + def compute( + self, + state: ComputationState, + log_func: Optional[Callable[[str, float], None]] = None): + expected = self.expected_func(state) + actual = self.actual_func(state) + loss = self.weight * ((expected - actual) ** 2).mean() + if log_func is not None: + log_func("loss", loss.item()) + return loss diff --git a/tha4/src/tha4/shion/base/loss/sum_loss.py b/tha4/src/tha4/shion/base/loss/sum_loss.py new file mode 100644 index 0000000..6dd604a --- /dev/null +++ b/tha4/src/tha4/shion/base/loss/sum_loss.py @@ -0,0 +1,32 @@ +from typing import List, Tuple, Callable, Optional + +import torch +from torch import Tensor + +from tha4.shion.core.cached_computation import ComputationState +from tha4.shion.core.loss import Loss + + +class SumLoss(Loss): + def __init__(self, losses: List[Tuple[str, Loss]]): + self.losses = losses + + def compute(self, + state: ComputationState, + log_func: Optional[Callable[[str, float], None]] = None) -> Tensor: + device = state.batch[0].device + loss_value = torch.zeros(1, device=device) + for loss_spec in self.losses: + loss_name = loss_spec[0] + loss = loss_spec[1] + if log_func is not None: + def loss_log_func(name, value): + log_func(loss_name + "_" + name, value) + else: + loss_log_func = None + loss_value = loss_value + loss.compute(state, loss_log_func) + + if log_func is not None: + log_func("loss", loss_value.item()) + + return loss_value diff --git a/tha4/src/tha4/shion/base/loss/time_dependently_weighted_loss.py b/tha4/src/tha4/shion/base/loss/time_dependently_weighted_loss.py new file mode 100644 index 0000000..b6c6da2 --- /dev/null +++ b/tha4/src/tha4/shion/base/loss/time_dependently_weighted_loss.py @@ -0,0 +1,29 @@ +from typing import Callable, Optional + +from torch import Tensor + +from tha4.shion.core.cached_computation import ComputationState, CachedComputationFunc +from tha4.shion.core.loss import Loss + + +class TimeDependentlyWeightedLoss(Loss): + def __init__(self, + base_loss: Loss, + examples_seen_so_far_func: CachedComputationFunc, + weight_func: Callable[[int], float]): + self.weight_func = weight_func + self.examples_seen_so_far_func = examples_seen_so_far_func + self.base_loss = base_loss + + def compute(self, + state: ComputationState, + log_func: Optional[Callable[[str, float], None]] = None) -> Tensor: + base_value = self.base_loss.compute(state) + examples_seen_so_far = self.examples_seen_so_far_func(state) + weight = self.weight_func(examples_seen_so_far) + loss_value = base_value * weight + + if log_func is not None: + log_func("loss", loss_value.item()) + + return loss_value diff --git a/tha4/src/tha4/shion/base/module_accumulators.py b/tha4/src/tha4/shion/base/module_accumulators.py new file mode 100644 index 0000000..7af98fe --- /dev/null +++ b/tha4/src/tha4/shion/base/module_accumulators.py @@ -0,0 +1,29 @@ +from typing import Optional + +import torch +from torch.nn import Module + +from tha4.shion.core.module_accumulator import ModuleAccumulator + + +# Code from https://github.com/rosinality/style-based-gan-pytorch/blob/8437a8bbd106ad4a4691b798ce35d30b5111990b/train.py +def accumulate_modules(new_module: Module, accumulated_module: Module, beta=0.99): + with torch.no_grad(): + new_module_params = dict(new_module.named_parameters()) + accumulated_module_params = dict(accumulated_module.named_parameters()) + for key in new_module_params.keys(): + accumulated_module_params[key].mul_(beta).add_(new_module_params[key] * (1 - beta)) + + new_module_buffers = dict(new_module.named_buffers()) + accumulated_module_buffers = dict(accumulated_module.named_buffers()) + for key in new_module_buffers.keys(): + accumulated_module_buffers[key].copy_(new_module_buffers[key]) + + +class DecayAccumulator(ModuleAccumulator): + def __init__(self, decay: float = 0.999): + self.decay = decay + + def accumulate(self, module: Module, output: Module, examples_seen_so_far: Optional[int] = None) -> Module: + accumulate_modules(module, output, self.decay) + return output diff --git a/tha4/src/tha4/shion/base/optimizer_factories.py b/tha4/src/tha4/shion/base/optimizer_factories.py new file mode 100644 index 0000000..d2fcbcc --- /dev/null +++ b/tha4/src/tha4/shion/base/optimizer_factories.py @@ -0,0 +1,46 @@ +from typing import Tuple, Iterable + +from torch.nn import Parameter +from torch.optim import Optimizer, Adam, AdamW, SparseAdam, RMSprop + +from tha4.shion.core.optimizer_factory import OptimizerFactory + + +class AdamOptimizerFactory(OptimizerFactory): + def __init__(self, betas: Tuple[float, float] = (0.9, 0.999), epsilon: float = 1e-8, weight_decay: float = 0.0): + super().__init__() + self.weight_decay = weight_decay + self.betas = betas + self.epsilon = epsilon + + def create(self, parameters: Iterable[Parameter]) -> Optimizer: + return Adam(parameters, betas=self.betas, eps=self.epsilon, weight_decay=self.weight_decay) + + +class AdamWOptimizerFactory(OptimizerFactory): + def __init__(self, betas: Tuple[float, float] = (0.9, 0.999), epsilon: float = 1e-8, weight_decay: float = 0.01): + super().__init__() + self.weight_decay = weight_decay + self.betas = betas + self.epsilon = epsilon + + def create(self, parameters: Iterable[Parameter]) -> Optimizer: + return AdamW(parameters, betas=self.betas, eps=self.epsilon, weight_decay=self.weight_decay) + + +class SparseAdamOptimizerFactory(OptimizerFactory): + def __init__(self, betas: Tuple[float, float] = (0.9, 0.999), epsilon: float = 1e-8): + super().__init__() + self.betas = betas + self.epsilon = epsilon + + def create(self, parameters: Iterable[Parameter]) -> Optimizer: + return SparseAdam(list(parameters), betas=self.betas, eps=self.epsilon) + + +class RMSpropOptimizerFactory(OptimizerFactory): + def __init__(self): + super().__init__() + + def create(self, parameters: Iterable[Parameter]) -> Optimizer: + return RMSprop(parameters) diff --git a/tha4/src/tha4/shion/base/protocol/single_network_from_batch_input_computation_protocol.py b/tha4/src/tha4/shion/base/protocol/single_network_from_batch_input_computation_protocol.py new file mode 100644 index 0000000..d52fee1 --- /dev/null +++ b/tha4/src/tha4/shion/base/protocol/single_network_from_batch_input_computation_protocol.py @@ -0,0 +1,29 @@ +from typing import Optional, Any, List + +from tha4.shion.core.cached_computation import CachedComputationProtocol, ComputationState + +KEY_NETWORK = "network" +KEY_NETWORK_OUTPUT = "network_output" + + +class SingleNetworkBatchInputComputationProtocol(CachedComputationProtocol): + def __init__(self, + key_network: str = KEY_NETWORK, + key_network_output: str = KEY_NETWORK_OUTPUT, + input_index_to_batch_index: Optional[List[int]] = None): + if input_index_to_batch_index is None: + input_index_to_batch_index = [0] + + self.input_index_to_batch_index = input_index_to_batch_index + self.key_network_output = key_network_output + self.key_network = key_network + + def compute_output(self, key: str, state: ComputationState) -> Any: + if key == self.key_network_output: + inputs = [] + for batch_index in self.input_index_to_batch_index: + inputs.append(state.batch[batch_index]) + network = state.modules[self.key_network] + return network.forward(*inputs) + else: + raise RuntimeError("Computing output for key " + key + " is not supported!") diff --git a/tha4/src/tha4/shion/base/training/__init__.py b/tha4/src/tha4/shion/base/training/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/base/training/single_network.py b/tha4/src/tha4/shion/base/training/single_network.py new file mode 100644 index 0000000..c0b334b --- /dev/null +++ b/tha4/src/tha4/shion/base/training/single_network.py @@ -0,0 +1,108 @@ +import time +from typing import List, Dict, Callable, Any, Optional + +import torch +from torch.nn import Module +from torch.nn.utils import clip_grad_norm_ +from torch.optim.optimizer import Optimizer + +from tha4.shion.core.cached_computation import ComputationState +from tha4.shion.core.loss import Loss +from tha4.shion.core.optimizer_factory import OptimizerFactory +from tha4.shion.core.training.training_protocol import TrainingProtocol +from tha4.shion.core.training.validation_protocol import ValidationProtocol + +KEY_NETWORK = "network" + + +class SingleNetworkTrainingProtocol(TrainingProtocol): + def __init__(self, + check_point_examples: List[int], + batch_size: int, + learning_rate: Callable[[int], Dict[str, float]], + optimizer_factories: Dict[str, OptimizerFactory], + module_key: str = KEY_NETWORK, + random_seed: int = 39549059840, + max_grad_norm: Optional[float] = None): + super().__init__() + self.max_grad_norm = max_grad_norm + self.module_key = module_key + self.optimizer_factories = optimizer_factories + self.learning_rate = learning_rate + self.batch_size = batch_size + self.random_seed = random_seed + self.check_point_examples = check_point_examples + + def get_optimizer_factories(self) -> Dict[str, OptimizerFactory]: + return self.optimizer_factories + + def get_checkpoint_examples(self) -> List[int]: + return self.check_point_examples + + def get_random_seed(self) -> int: + return self.random_seed + + def get_batch_size(self) -> int: + return self.batch_size + + def get_learning_rate(self, examples_seen_so_far: int) -> Dict[str, float]: + return self.learning_rate(examples_seen_so_far) + + def run_training_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer], + losses: Dict[str, Loss], + create_log_func: Optional[Callable[[str, int], Callable[[str, float], None]]], + device: torch.device): + module = modules[self.module_key] + module.train(True) + optimizers[self.module_key].zero_grad(set_to_none=True) + if create_log_func is not None: + log_func = create_log_func("training_" + self.module_key, examples_seen_so_far) + else: + log_func = None + losses[self.module_key].compute( + ComputationState(modules, accumulated_modules, batch), + log_func).backward() + if self.max_grad_norm is not None: + clip_grad_norm_(module.parameters(), self.max_grad_norm) + optimizers[self.module_key].step() + + +class SingleNetworkValidationProtocol(ValidationProtocol): + def __init__( + self, + example_per_validation_iteration: int, + batch_size: int, + module_key: str = KEY_NETWORK): + super().__init__() + self.module_key = module_key + self.batch_size = batch_size + self.example_per_validation_iteration = example_per_validation_iteration + + def get_batch_size(self, ) -> int: + return self.batch_size + + def get_examples_per_validation_iteration(self) -> int: + return self.example_per_validation_iteration + + def run_validation_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + losses: Dict[str, Loss], + create_log_func: Callable[[str, int], Callable[[str, float], None]], + device: torch.device): + module = modules[self.module_key] + module.train(False) + with torch.no_grad(): + log_func = create_log_func("validation_" + self.module_key, examples_seen_so_far) + losses[self.module_key].compute( + ComputationState(modules, accumulated_modules, batch), + log_func) diff --git a/tha4/src/tha4/shion/base/training/single_network_with_minibatch.py b/tha4/src/tha4/shion/base/training/single_network_with_minibatch.py new file mode 100644 index 0000000..e767b9d --- /dev/null +++ b/tha4/src/tha4/shion/base/training/single_network_with_minibatch.py @@ -0,0 +1,87 @@ +import time +from typing import List, Dict, Callable, Any, Optional + +import torch +from torch.nn import Module +from torch.nn.utils import clip_grad_norm_ +from torch.optim.optimizer import Optimizer + +from tha4.shion.core.cached_computation import ComputationState +from tha4.shion.core.loss import Loss +from tha4.shion.core.optimizer_factory import OptimizerFactory +from tha4.shion.core.training.training_protocol import TrainingProtocol +from tha4.shion.core.training.validation_protocol import ValidationProtocol + +KEY_NETWORK = "network" + + +class SingleNetworkWithMinibatchTrainingProtocol(TrainingProtocol): + def __init__(self, + check_point_examples: List[int], + batch_size: int, + minibatch_size: int, + learning_rate: Callable[[int], Dict[str, float]], + optimizer_factories: Dict[str, OptimizerFactory], + module_key: str = KEY_NETWORK, + random_seed: int = 39549059840, + max_grad_norm: Optional[float] = None): + super().__init__() + assert batch_size % minibatch_size == 0 + self.minibatch_size = minibatch_size + self.max_grad_norm = max_grad_norm + self.module_key = module_key + self.optimizer_factories = optimizer_factories + self.learning_rate = learning_rate + self.batch_size = batch_size + self.random_seed = random_seed + self.check_point_examples = check_point_examples + + def get_optimizer_factories(self) -> Dict[str, OptimizerFactory]: + return self.optimizer_factories + + def get_checkpoint_examples(self) -> List[int]: + return self.check_point_examples + + def get_random_seed(self) -> int: + return self.random_seed + + def get_batch_size(self) -> int: + return self.batch_size + + def get_learning_rate(self, examples_seen_so_far: int) -> Dict[str, float]: + return self.learning_rate(examples_seen_so_far) + + def run_training_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer], + losses: Dict[str, Loss], + create_log_func: Optional[Callable[[str, int], Callable[[str, float], None]]], + device: torch.device): + module = modules[self.module_key] + module.train(True) + optimizers[self.module_key].zero_grad(set_to_none=True) + if create_log_func is not None: + log_func = create_log_func("training_" + self.module_key, examples_seen_so_far) + else: + log_func = None + + num_minibatch = self.batch_size // self.minibatch_size + for minibatch_index in range(num_minibatch): + minibatch = [] + for item in batch: + minibatch.append( + item[minibatch_index * self.minibatch_size:(minibatch_index + 1) * self.minibatch_size]) + loss = losses[self.module_key].compute( + ComputationState(modules, accumulated_modules, minibatch), + log_func if minibatch_index == 0 else None) + loss = loss / num_minibatch + loss.backward() + + if self.max_grad_norm is not None: + clip_grad_norm_(module.parameters(), self.max_grad_norm) + + optimizers[self.module_key].step() diff --git a/tha4/src/tha4/shion/base/training/two_networks_training_protocol.py b/tha4/src/tha4/shion/base/training/two_networks_training_protocol.py new file mode 100644 index 0000000..8acf495 --- /dev/null +++ b/tha4/src/tha4/shion/base/training/two_networks_training_protocol.py @@ -0,0 +1,106 @@ +from typing import List, Dict, Callable, Any, Optional + +import torch +from torch.nn import Module +from torch.nn.utils import clip_grad_norm_ +from torch.optim.optimizer import Optimizer + +from tha4.shion.core.cached_computation import ComputationState +from tha4.shion.core.loss import Loss +from tha4.shion.core.optimizer_factory import OptimizerFactory +from tha4.shion.core.training.training_protocol import TrainingProtocol + + +class TwoNetworksWithMinibatchTrainingProtocol(TrainingProtocol): + def __init__(self, + check_point_examples: List[int], + batch_size: int, + learning_rate: Callable[[int], Dict[str, float]], + optimizer_factories: Dict[str, OptimizerFactory], + key_network_0: str, + key_network_1: str, + train_network_0: bool = False, + random_seed: int = 39549059840, + max_grad_norm: Optional[float] = None, + minibatch_size: Optional[int] = None): + super().__init__() + if minibatch_size is None: + minibatch_size = batch_size + assert batch_size % minibatch_size == 0 + self.train_network_0 = train_network_0 + self.key_network_1 = key_network_1 + self.key_network_0 = key_network_0 + self.minibatch_size = minibatch_size + self.max_grad_norm = max_grad_norm + self.optimizer_factories = optimizer_factories + self.learning_rate = learning_rate + self.batch_size = batch_size + self.random_seed = random_seed + self.check_point_examples = check_point_examples + + def get_optimizer_factories(self) -> Dict[str, OptimizerFactory]: + return self.optimizer_factories + + def get_checkpoint_examples(self) -> List[int]: + return self.check_point_examples + + def get_random_seed(self) -> int: + return self.random_seed + + def get_batch_size(self) -> int: + return self.batch_size + + def get_learning_rate(self, examples_seen_so_far: int) -> Dict[str, float]: + return self.learning_rate(examples_seen_so_far) + + def run_training_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer], + losses: Dict[str, Loss], + create_log_func: Optional[Callable[[str, int], Callable[[str, float], None]]], + device: torch.device): + network_0 = modules[self.key_network_0] + network_0.train(self.train_network_0) + + network_1 = modules[self.key_network_1] + network_1.train(True) + + if self.train_network_0: + optimizers[self.key_network_0].zero_grad(set_to_none=True) + optimizers[self.key_network_1].zero_grad(set_to_none=True) + + if create_log_func is not None: + network_0_log_func = create_log_func("training_" + self.key_network_0, examples_seen_so_far) + network_1_log_func = create_log_func("training_" + self.key_network_1, examples_seen_so_far) + else: + network_0_log_func = None + network_1_log_func = None + + num_minibatch = self.batch_size // self.minibatch_size + for minibatch_index in range(num_minibatch): + minibatch = [] + for item in batch: + minibatch.append( + item[minibatch_index * self.minibatch_size:(minibatch_index + 1) * self.minibatch_size]) + loss = losses[self.key_network_1].compute( + ComputationState(modules, accumulated_modules, minibatch), + network_1_log_func if minibatch_index == 0 else None) + if self.train_network_0 and self.key_network_0 in losses: + loss = loss + losses[self.key_network_0].compute( + ComputationState(modules, accumulated_modules, minibatch), + network_0_log_func if minibatch_index == 0 else None) + loss = loss / num_minibatch + loss.backward() + + if self.max_grad_norm is not None: + clip_grad_norm_(network_1.parameters(), self.max_grad_norm) + if self.train_network_0: + clip_grad_norm_(network_0.parameters(), self.max_grad_norm) + + optimizers[self.key_network_1].step() + if self.train_network_0: + optimizers[self.key_network_0].step() diff --git a/tha4/src/tha4/shion/core/__init__.py b/tha4/src/tha4/shion/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/core/cached_computation.py b/tha4/src/tha4/shion/core/cached_computation.py new file mode 100644 index 0000000..eac0815 --- /dev/null +++ b/tha4/src/tha4/shion/core/cached_computation.py @@ -0,0 +1,112 @@ +from abc import ABC, abstractmethod +from typing import Callable, Dict, Any, Optional + +import torch +from torch import Tensor +from torch.nn import Module + + +class ComputationState: + def __init__(self, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + batch: Any, + outputs: Optional[Dict[str, Any]] = None): + if outputs is None: + outputs = {} + self.outputs = outputs + self.batch = batch + self.accumulated_modules = accumulated_modules + self.modules = modules + + +CachedComputationFunc = Callable[[ComputationState], Any] +TensorCachedComputationFunc = Callable[[ComputationState], Tensor] + + +def create_get_item_func(func: CachedComputationFunc, index): + def _f(state: ComputationState): + output = func(state) + return output[index] + + return _f + + +def create_batch_element_func(index: int) -> TensorCachedComputationFunc: + def _f(state: ComputationState) -> Tensor: + return state.batch[index] + + return _f + + +class CachedComputationProtocol(ABC): + def get_output(self, key: str, state: ComputationState) -> Any: + if key in state.outputs: + return state.outputs[key] + else: + output = self.compute_output(key, state) + state.outputs[key] = output + return state.outputs[key] + + @abstractmethod + def compute_output(self, key: str, state: ComputationState) -> Any: + pass + + def get_output_func(self, key: str) -> CachedComputationFunc: + def func(state: ComputationState): + return self.get_output(key, state) + + return func + + +ComposableCachedComputationStep = Callable[[CachedComputationProtocol, ComputationState], Any] + + +class ComposableCachedComputationProtocol(CachedComputationProtocol): + def __init__(self, computation_steps: Optional[Dict[str, ComposableCachedComputationStep]] = None): + if computation_steps is None: + computation_steps = {} + self.computation_steps = computation_steps + + def compute_output(self, key: str, state: ComputationState) -> Any: + if key in self.computation_steps: + return self.computation_steps[key](self, state) + else: + raise RuntimeError("Computing output for key " + key + " is not supported!") + + +def batch_indexing_func(index: int): + def _f(protocol: CachedComputationProtocol, state: ComputationState): + return state.batch[index] + + return _f + + +def proxy_func(key: str): + def _f(protocol: CachedComputationProtocol, state: ComputationState): + return protocol.get_output(key, state) + + return _f + + +def output_array_indexing_func(key: str, index: int): + def _f(protocol: CachedComputationProtocol, state: ComputationState): + return protocol.get_output(key, state)[index] + + return _f + + +def add_step(step_dict: Dict[str, ComposableCachedComputationStep], name: str): + def _f(func): + step_dict[name] = func + return func + + return _f + + +def zeros_like_func(key: str): + def _f(protocol: CachedComputationProtocol, state: ComputationState): + prototype = protocol.get_output(key, state) + return torch.zeros_like(prototype) + + return _f diff --git a/tha4/src/tha4/shion/core/load_save.py b/tha4/src/tha4/shion/core/load_save.py new file mode 100644 index 0000000..d9f7e72 --- /dev/null +++ b/tha4/src/tha4/shion/core/load_save.py @@ -0,0 +1,14 @@ +import os + +import torch + + +def torch_save(content, file_name): + os.makedirs(os.path.dirname(file_name), exist_ok=True) + with open(file_name, 'wb') as f: + torch.save(content, f) + + +def torch_load(file_name): + with open(file_name, 'rb') as f: + return torch.load(f, map_location=lambda storage, loc: storage) diff --git a/tha4/src/tha4/shion/core/loss.py b/tha4/src/tha4/shion/core/loss.py new file mode 100644 index 0000000..d301051 --- /dev/null +++ b/tha4/src/tha4/shion/core/loss.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from typing import Callable, Optional + +from torch import Tensor + +from tha4.shion.core.cached_computation import ComputationState + + +class Loss(ABC): + @abstractmethod + def compute( + self, + state: ComputationState, + log_func: Optional[Callable[[str, float], None]] = None) -> Tensor: + pass diff --git a/tha4/src/tha4/shion/core/module_accumulator.py b/tha4/src/tha4/shion/core/module_accumulator.py new file mode 100644 index 0000000..e291268 --- /dev/null +++ b/tha4/src/tha4/shion/core/module_accumulator.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from torch.nn import Module + + +class ModuleAccumulator(ABC): + @abstractmethod + def accumulate(self, module: Module, output: Module, examples_seen_so_far: Optional[int] = None) -> Module: + pass diff --git a/tha4/src/tha4/shion/core/module_factory.py b/tha4/src/tha4/shion/core/module_factory.py new file mode 100644 index 0000000..f6231c3 --- /dev/null +++ b/tha4/src/tha4/shion/core/module_factory.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +from torch.nn import Module + + +class ModuleFactory(ABC): + @abstractmethod + def create(self) -> Module: + pass \ No newline at end of file diff --git a/tha4/src/tha4/shion/core/optimizer_factory.py b/tha4/src/tha4/shion/core/optimizer_factory.py new file mode 100644 index 0000000..d5738b4 --- /dev/null +++ b/tha4/src/tha4/shion/core/optimizer_factory.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod +from typing import Iterable + +from torch.nn import Parameter + + +class OptimizerFactory(ABC): + @abstractmethod + def create(self, parameters: Iterable[Parameter]): + pass diff --git a/tha4/src/tha4/shion/core/training/__init__.py b/tha4/src/tha4/shion/core/training/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/core/training/distrib/__init__.py b/tha4/src/tha4/shion/core/training/distrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/core/training/distrib/device_mapper.py b/tha4/src/tha4/shion/core/training/distrib/device_mapper.py new file mode 100644 index 0000000..29673a7 --- /dev/null +++ b/tha4/src/tha4/shion/core/training/distrib/device_mapper.py @@ -0,0 +1,17 @@ +from typing import Dict + +import torch + + +class SimpleCudaDeviceMapper: + def __call__(self, rank, local_rank): + return torch.device("cuda", local_rank) + + +class UserSpecifiedLocalRankToDeviceMapper: + def __init__(self, device_map: Dict[int, torch.device]): + self.device_map = device_map + + def __call__(self, rank, local_rank): + assert local_rank in self.device_map + return self.device_map[local_rank] diff --git a/tha4/src/tha4/shion/core/training/distrib/distributed_trainer.py b/tha4/src/tha4/shion/core/training/distrib/distributed_trainer.py new file mode 100644 index 0000000..988ac27 --- /dev/null +++ b/tha4/src/tha4/shion/core/training/distrib/distributed_trainer.py @@ -0,0 +1,419 @@ +import argparse +import logging +import os.path +import time +from datetime import datetime +from typing import Dict, Optional, Callable, Any + +import torch +import torch.distributed +from torch.nn.parallel import DistributedDataParallel +from torch.utils.data import Dataset, DataLoader, DistributedSampler +from torch.utils.tensorboard import SummaryWriter + +from tha4.shion.core.load_save import torch_save, torch_load +from tha4.shion.core.loss import Loss +from tha4.shion.core.module_accumulator import ModuleAccumulator +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.core.training.distrib.device_mapper import SimpleCudaDeviceMapper +from tha4.shion.core.training.distrib.distributed_training_states import DistributedTrainingState +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.shion.core.training.training_protocol import TrainingProtocol +from tha4.shion.core.training.util import set_learning_rate, create_log_func, get_least_greater_multiple +from tha4.shion.core.training.validation_protocol import ValidationProtocol + +KEY_CHECKPOINT = 'checkpoint' +KEY_SNAPSHOT = 'snapshot' +KEY_VALIDATION = 'validation' +KEY_SAMPLE_OUTPUT = 'sample_output' + + +class DistributedTrainer: + def __init__(self, + prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + losses: Dict[str, Loss], + training_dataset: Dataset, + validation_dataset: Optional[Dataset], + training_protocol: TrainingProtocol, + validation_protocol: Optional[ValidationProtocol], + sample_output_protocol: Optional[SampleOutputProtocol], + pretrained_module_file_names: Dict[str, str], + example_per_snapshot: int, + num_data_loader_workers: int = 8, + distrib_backend: str = 'gloo'): + self.distrib_backend = distrib_backend + self.num_data_loader_workers = num_data_loader_workers + self.accumulators = accumulators + self.sample_output_protocol = sample_output_protocol + self.example_per_snapshot = example_per_snapshot + self.pretrained_module_file_names = pretrained_module_file_names + self.losses = losses + self.validation_protocol = validation_protocol + self.training_protocol = training_protocol + self.module_factories = module_factories + self.prefix = prefix + self.training_dataset = training_dataset + self.validation_dataset = validation_dataset + + self.checkpoint_examples = self.training_protocol.get_checkpoint_examples() + assert len(self.checkpoint_examples) >= 1 + assert self.checkpoint_examples[0] > 0 + self.checkpoint_examples = [0] + self.checkpoint_examples + + self.module_names = self.module_factories.keys() + assert len(self.module_names) > 0 + + self.training_data_loader = None + self.training_data_loader_iter = None + self.training_data_loader_batch_size = None + self.training_data_sampler = None + + self.validation_data_loader = None + self.validation_data_loader_iter = None + self.validation_data_loader_batch_size = None + + self.sample_output_data = None + self.summary_writer = None + self.log_dir = None + self.training_state = None + + def get_sample_output_data_file_name(self): + return self.prefix + "/sample_output_data.pt" + + def save_sample_output_data(self, rank: int, device: torch.device): + if rank != 0: + return + if os.path.exists(self.get_sample_output_data_file_name()): + return + if self.sample_output_protocol is not None: + torch.manual_seed(self.sample_output_protocol.get_random_seed()) + sample_output_data = self.sample_output_protocol.get_sample_output_data(self.validation_dataset, device) + torch_save(sample_output_data, self.get_sample_output_data_file_name()) + else: + torch_save({}, self.get_sample_output_data_file_name()) + + def load_sample_output_data(self, rank: int, device: torch.device): + if rank != 0: + return None + else: + self.save_sample_output_data(rank, device) + return torch_load(self.get_sample_output_data_file_name()) + + def get_snapshot_prefix(self) -> str: + return self.prefix + "/snapshot" + + def can_load_training_state(self, prefix: str, world_size: int) -> bool: + return DistributedTrainingState.can_load( + prefix, + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + world_size) + + def load_training_state(self, prefix, rank: int, local_rank: int, device: torch.device) -> DistributedTrainingState: + return DistributedTrainingState.load( + prefix, + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + rank, + local_rank, + device) + + @staticmethod + def checkpoint_prefix(prefix: str, checkpoint_index: int) -> str: + return "%s/checkpoint/%04d" % (prefix, checkpoint_index) + + def get_checkpoint_prefix(self, checkpoint_index) -> str: + return DistributedTrainer.checkpoint_prefix(self.prefix, checkpoint_index) + + def get_initial_training_state(self, rank: int, local_rank: int, device: torch.device) -> DistributedTrainingState: + training_state = DistributedTrainingState.new( + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + self.training_protocol.get_random_seed(), + rank, + local_rank, + device, + self.pretrained_module_file_names) + logging.info("Created a new initial training state.") + return training_state + + def load_previous_training_state(self, + target_checkpoint_examples: int, + world_size: int, + rank: int, + local_rank: int, + device: torch.device) -> DistributedTrainingState: + if self.can_load_training_state(self.get_snapshot_prefix(), world_size): + examples_seen_so_far = DistributedTrainingState.get_examples_seen_so_far(self.get_snapshot_prefix()) + diff = examples_seen_so_far - target_checkpoint_examples + if diff < self.training_protocol.get_batch_size(): + return self.load_training_state(self.get_snapshot_prefix(), rank, local_rank, device) + num_checkpoints = len(self.checkpoint_examples) + for checkpoint_index in range(num_checkpoints - 1, -1, -1): + if self.can_load_training_state(self.get_checkpoint_prefix(checkpoint_index), world_size): + examples_seen_so_far = DistributedTrainingState.get_examples_seen_so_far( + self.get_checkpoint_prefix(checkpoint_index)) + diff = examples_seen_so_far - target_checkpoint_examples + if diff < self.training_protocol.get_batch_size(): + return self.load_training_state( + self.get_checkpoint_prefix(checkpoint_index), rank, local_rank, device) + + training_state = self.get_initial_training_state(rank, local_rank, device) + training_state.save(self.get_checkpoint_prefix(0), rank, lambda: self.barrier(local_rank)) + training_state = self.load_training_state(self.get_checkpoint_prefix(0), rank, local_rank, device) + return training_state + + def get_log_dir(self): + if self.log_dir is None: + now = datetime.now() + self.log_dir = self.prefix + "/log/" + now.strftime("%Y_%m_%d__%H_%M_%S") + return self.log_dir + + def get_summary_writer(self, rank: int) -> Optional[SummaryWriter]: + if rank != 0: + return None + if self.summary_writer is None: + self.summary_writer = SummaryWriter(log_dir=self.get_log_dir()) + return self.summary_writer + + def get_effective_training_epoch_size(self, world_size: int): + batch_size = self.training_protocol.get_batch_size() + N = len(self.training_dataset) + N = (N // world_size) * world_size + N = (N // batch_size) * batch_size + return N + + def get_training_epoch_index(self, examples_seen_so_far: int, world_size: int): + epoch_size = self.get_effective_training_epoch_size(world_size) + batch_size = self.training_protocol.get_batch_size() + return (examples_seen_so_far + batch_size * world_size) // epoch_size + + def get_next_training_batch(self, examples_seen_so_far: int, world_size: int, device: torch.device): + batch_size = self.training_protocol.get_batch_size() + dataset = self.training_dataset + if self.training_data_loader is None: + self.training_data_sampler = DistributedSampler( + dataset, + shuffle=True, + drop_last=True) + self.training_data_loader = DataLoader( + dataset, + batch_size=batch_size, + sampler=self.training_data_sampler, + shuffle=False, + num_workers=self.num_data_loader_workers, + drop_last=True) + if self.training_data_loader_iter is None: + epoch_index = self.get_training_epoch_index(examples_seen_so_far, world_size) + logging.info(f"Started a new epoch: index = {epoch_index}, examples_seen_so_far = {examples_seen_so_far}") + self.training_data_sampler.set_epoch(epoch_index) + self.training_data_loader_iter = iter(self.training_data_loader) + try: + batch = next(self.training_data_loader_iter) + except StopIteration: + epoch_index = self.get_training_epoch_index(examples_seen_so_far, world_size) + logging.info(f"Started a new epoch: index = {epoch_index}, examples_seen_so_far = {examples_seen_so_far}") + self.training_data_sampler.set_epoch(epoch_index) + self.training_data_loader_iter = iter(self.training_data_loader) + batch = next(self.training_data_loader_iter) + return [x.to(device) for x in batch] + + def get_next_checkpoint_num_examples(self, examples_seen_so_far) -> int: + next_index = next( + (i for i in range(len(self.checkpoint_examples)) if self.checkpoint_examples[i] > examples_seen_so_far), + -1) + return self.checkpoint_examples[next_index] + + def get_next_snapshot_num_examples(self, examples_seen_so_far) -> int: + return get_least_greater_multiple(examples_seen_so_far, self.example_per_snapshot) + + def get_next_validation_num_examples(self, examples_seen_so_far) -> int: + if self.validation_protocol is None: + return -1 + return get_least_greater_multiple(examples_seen_so_far, + self.validation_protocol.get_examples_per_validation_iteration()) + + def get_next_sample_output_num_examples(self, examples_seen_so_far) -> int: + if self.sample_output_protocol is None: + return -1 + return get_least_greater_multiple(examples_seen_so_far, + self.sample_output_protocol.get_examples_per_sample_output()) + + def get_next_num_examples(self, examples_seen_so_far) -> Dict[str, int]: + return { + KEY_CHECKPOINT: self.get_next_checkpoint_num_examples(examples_seen_so_far), + KEY_SNAPSHOT: self.get_next_snapshot_num_examples(examples_seen_so_far), + KEY_VALIDATION: self.get_next_validation_num_examples(examples_seen_so_far), + KEY_SAMPLE_OUTPUT: self.get_next_sample_output_num_examples(examples_seen_so_far) + } + + def get_next_validation_batch(self, device: torch.device): + if self.validation_dataset is None: + return None + if self.validation_data_loader is None: + self.validation_data_loader = DataLoader( + self.validation_dataset, + batch_size=self.validation_protocol.get_batch_size(), + shuffle=True, + num_workers=1, + drop_last=True) + if self.validation_data_loader_iter is None: + self.validation_data_loader_iter = iter(self.validation_data_loader) + try: + batch = next(self.validation_data_loader_iter) + except StopIteration: + self.validation_data_loader_iter = iter(self.validation_data_loader) + batch = next(self.validation_data_loader_iter) + return [x.to(device) for x in batch] + + def get_checkpoint_index_to_save(self, examples_seen_so_far: int) -> int: + checkpoint_index = 0 + for i in range(len(self.checkpoint_examples)): + if self.checkpoint_examples[i] <= examples_seen_so_far: + checkpoint_index = i + return checkpoint_index + + def barrier(self, local_rank: int): + if self.distrib_backend == 'nccl': + torch.distributed.barrier(device_ids=[local_rank]) + else: + torch.distributed.barrier() + + def train(self, + world_size: int, + rank: int, + local_rank: int, + target_checkpoint_examples: Optional[int] = None, + device_mapper: Optional[Callable[[int, int], torch.device]] = None): + if target_checkpoint_examples is None: + target_checkpoint_examples = self.checkpoint_examples[-1] + + if device_mapper is None: + device_mapper = SimpleCudaDeviceMapper() + device = device_mapper(rank, local_rank) + + sample_output_data = self.load_sample_output_data(rank, device) + training_state = self.load_previous_training_state( + target_checkpoint_examples, world_size, rank, local_rank, device) + summary_writer = self.get_summary_writer(rank) + if summary_writer is not None: + log_func_factory = lambda name, num: create_log_func(summary_writer, name, num) + else: + log_func_factory = None + last_time = time.time() + + while training_state.examples_seen_so_far < target_checkpoint_examples: + # Set the learning rate + learning_rate_by_module_name = self.training_protocol.get_learning_rate(training_state.examples_seen_so_far) + for module_name in self.module_factories.keys(): + if module_name not in learning_rate_by_module_name or module_name not in training_state.optimizers: + continue + lr = learning_rate_by_module_name[module_name] + set_learning_rate(training_state.optimizers[module_name], lr) + if summary_writer is not None: + summary_writer.add_scalar( + module_name + "_learning_rate", lr, training_state.examples_seen_so_far) + + # One training iteration + training_batch = self.get_next_training_batch(training_state.examples_seen_so_far, world_size, device) + self.training_protocol.run_training_iteration( + training_batch, + training_state.examples_seen_so_far, + training_state.modules, + training_state.accumulated_modules, + training_state.optimizers, + self.losses, + log_func_factory, + device) + + # Accumulate model data + for module_name in self.accumulators: + new_module = training_state.modules[module_name] + if isinstance(new_module, DistributedDataParallel): + new_module = new_module.module + buffer_module = training_state.accumulated_modules[module_name] + self.accumulators[module_name].accumulate( + new_module, buffer_module, examples_seen_so_far=training_state.examples_seen_so_far) + + # Advance the number of examples seen so far + next_num_examples = self.get_next_num_examples(training_state.examples_seen_so_far) + training_state.examples_seen_so_far += self.training_protocol.get_batch_size() * world_size + + # Validation iteration + if self.validation_protocol is not None \ + and training_state.examples_seen_so_far >= next_num_examples[KEY_VALIDATION] \ + and rank == 0: + validation_batch = self.get_next_validation_batch(device) + self.validation_protocol.run_validation_iteration( + validation_batch, + training_state.examples_seen_so_far, + training_state.modules, + training_state.accumulated_modules, + self.losses, + log_func_factory, + device) + + # Save sample output + if self.sample_output_protocol is not None \ + and training_state.examples_seen_so_far >= next_num_examples[KEY_SAMPLE_OUTPUT]: + if rank == 0: + self.sample_output_protocol.save_sample_output_data( + training_state.modules, + training_state.accumulated_modules, + sample_output_data, + self.prefix + "/sample_outputs", + training_state.examples_seen_so_far, + device) + self.barrier(local_rank) + + # Save checkpoint + if training_state.examples_seen_so_far >= next_num_examples[KEY_CHECKPOINT]: + checkpoint_index = self.get_checkpoint_index_to_save(training_state.examples_seen_so_far) + training_state.save( + self.get_checkpoint_prefix(checkpoint_index), rank, lambda: self.barrier(local_rank)) + if next_num_examples[KEY_CHECKPOINT] != next_num_examples[KEY_SNAPSHOT]: + training_state.save(self.get_snapshot_prefix(), rank, lambda: self.barrier(local_rank)) + + # Save snapshot + if training_state.examples_seen_so_far >= next_num_examples[KEY_SNAPSHOT]: + training_state.save(self.get_snapshot_prefix(), rank, lambda: self.barrier(local_rank)) + + now = time.time() + if now - last_time > 10: + logging.info("Showed %d training examples." % training_state.examples_seen_so_far) + last_time = now + + @staticmethod + def get_default_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description='Training script.') + parser.add_argument("--target_checkpoint_examples", type=int) + return parser + + @staticmethod + def run_with_args(trainer_factory: Callable[[int, str], 'DistributedTrainer'], + args, + backend: str = 'gloo', + device_mapper: Optional[Callable[[int, int], torch.device]] = None): + world_size = int(os.environ['WORLD_SIZE']) + rank = int(os.environ['RANK']) + local_rank = int(os.environ['LOCAL_RANK']) + + torch.distributed.init_process_group(backend) + trainer = trainer_factory(world_size, backend) + trainer.train(world_size, rank, local_rank, args.target_checkpoint_examples, device_mapper) + + @staticmethod + def run(trainer_factory: Callable[[int, str], 'DistributedTrainer'], + backend: str = 'gloo', + device_mapper: Optional[Callable[[int, int], torch.device]] = None, + args: Optional[Any] = None): + if args is None: + parser = DistributedTrainer.get_default_arg_parser() + args = parser.parse_args() + + DistributedTrainer.run_with_args(trainer_factory, args, backend, device_mapper) diff --git a/tha4/src/tha4/shion/core/training/distrib/distributed_training_states.py b/tha4/src/tha4/shion/core/training/distrib/distributed_training_states.py new file mode 100644 index 0000000..8eb1444 --- /dev/null +++ b/tha4/src/tha4/shion/core/training/distrib/distributed_training_states.py @@ -0,0 +1,234 @@ +import copy +import logging +import os +from typing import Dict, Optional, Callable + +import torch +from torch.nn import Module +from torch.nn.parallel import DistributedDataParallel +from torch.optim.optimizer import Optimizer + +from tha4.shion.core.load_save import torch_save, torch_load +from tha4.shion.core.module_accumulator import ModuleAccumulator +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.core.optimizer_factory import OptimizerFactory +from tha4.shion.core.training.util import optimizer_to_device + + +class DistributedTrainingState: + def __init__(self, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer]): + self.accumulated_modules = accumulated_modules + self.optimizers = optimizers + self.modules = modules + self.examples_seen_so_far = examples_seen_so_far + + @staticmethod + def get_examples_seen_so_far_file_name(prefix) -> str: + return prefix + "/examples_seen_so_far.txt" + + @staticmethod + def get_module_file_name(prefix, module_name) -> str: + return "%s/module_%s.pt" % (prefix, module_name) + + @staticmethod + def get_accumulated_module_file_name(prefix, module_name) -> str: + return "%s/accumulated_%s.pt" % (prefix, module_name) + + @staticmethod + def get_optimizer_file_name(prefix, module_name) -> str: + return "%s/optimizer_%s.pt" % (prefix, module_name) + + @staticmethod + def get_rng_state_file_name(prefix, rank: int): + return "%s/rng_state_%08d.pt" % (prefix, rank) + + def mkdir(self, prefix: str): + os.makedirs(prefix, exist_ok=True) + + def save_data(self, prefix: str, rank: int): + assert os.path.exists(prefix) + + torch_save(torch.get_rng_state(), DistributedTrainingState.get_rng_state_file_name(prefix, rank)) + logging.info("Saved %s" % DistributedTrainingState.get_rng_state_file_name(prefix, rank)) + + if rank == 0: + logging.info("Saving training state to %s" % prefix) + with open(DistributedTrainingState.get_examples_seen_so_far_file_name(prefix), "wt") as fout: + fout.write("%d\n" % self.examples_seen_so_far) + logging.info("Saved %s" % DistributedTrainingState.get_examples_seen_so_far_file_name(prefix)) + for module_name in self.modules: + file_name = DistributedTrainingState.get_module_file_name(prefix, module_name) + module = self.modules[module_name] + if isinstance(module, DistributedDataParallel): + state_dict = module.module.state_dict() + else: + state_dict = module.state_dict() + torch_save(state_dict, file_name) + logging.info("Saved %s" % file_name) + for module_name in self.accumulated_modules: + file_name = DistributedTrainingState.get_accumulated_module_file_name(prefix, module_name) + torch_save(self.accumulated_modules[module_name].state_dict(), file_name) + logging.info("Saved %s" % file_name) + for module_name in self.optimizers: + file_name = DistributedTrainingState.get_optimizer_file_name(prefix, module_name) + torch_save(self.optimizers[module_name].state_dict(), file_name) + logging.info("Saved %s" % file_name) + + logging.info("Done saving training state to %s" % prefix) + + def save(self, prefix: str, rank: int, barrier_func: Callable[[], None]): + if rank == 0: + self.mkdir(prefix) + barrier_func() + self.save_data(prefix, rank) + barrier_func() + + @staticmethod + def get_examples_seen_so_far(prefix: str) -> int: + with open(DistributedTrainingState.get_examples_seen_so_far_file_name(prefix)) as fin: + lines = fin.readlines() + return int(lines[0]) + + @staticmethod + def load( + prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + optimizer_factories: Dict[str, OptimizerFactory], + rank: int, + local_rank: int, + device: torch.device) -> 'DistributedTrainingState': + logging.info("Loading training state from %s" % prefix) + + with open(DistributedTrainingState.get_examples_seen_so_far_file_name(prefix)) as fin: + lines = fin.readlines() + examples_seen_so_far = int(lines[0]) + logging.info("Loaded %s" % DistributedTrainingState.get_examples_seen_so_far_file_name(prefix)) + + modules = { + module_name: factory.create() + for (module_name, factory) in module_factories.items() + } + for module_name in modules: + file_name = DistributedTrainingState.get_module_file_name(prefix, module_name) + module = modules[module_name] + state_dict = torch_load(file_name) + module.load_state_dict(state_dict) + module.to(device) + modules[module_name] = DistributedDataParallel( + module, + device_ids=[device.index], + output_device=device.index) + logging.info("Loaded %s" % file_name) + + accumulated_modules = {} + for module_name in accumulators: + module_factory = module_factories[module_name] + module = module_factory.create() + file_name = DistributedTrainingState.get_accumulated_module_file_name(prefix, module_name) + module.load_state_dict(torch_load(file_name)) + module.to(device) + accumulated_modules[module_name] = module + logging.info("Loaded %s" % file_name) + + optimizers = {} + for module_name in optimizer_factories: + optimizer = optimizer_factories[module_name].create(modules[module_name].parameters()) + file_name = DistributedTrainingState.get_optimizer_file_name(prefix, module_name) + optimizer.load_state_dict(torch_load(file_name)) + optimizer_to_device(optimizer, device) + optimizers[module_name] = optimizer + logging.info("Loaded %s" % file_name) + + torch.set_rng_state(torch_load(DistributedTrainingState.get_rng_state_file_name(prefix, rank))) + logging.info("Loaded %s" % DistributedTrainingState.get_rng_state_file_name(prefix, rank)) + + logging.info("Done loading training state from %s" % prefix) + + return DistributedTrainingState(examples_seen_so_far, modules, accumulated_modules, optimizers) + + @staticmethod + def new(module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + optimizer_factories: Dict[str, OptimizerFactory], + random_seed: int, + rank: int, + local_rank: int, + device: torch.device, + pretrained_module_file_names: Optional[Dict[str, str]] = None) -> 'DistributedTrainingState': + examples_seen_so_far = 0 + + modules = { + module_name: factory.create() + for (module_name, factory) in module_factories.items() + } + for module_name in modules: + modules[module_name].to(device) + if pretrained_module_file_names is not None: + for module_name in modules: + if module_name in pretrained_module_file_names: + file_name = pretrained_module_file_names[module_name] + modules[module_name].load_state_dict(torch_load(file_name)) + logging.info("Loaded initial state from %s ..." % file_name) + + accumulated_modules = {} + for module_name in accumulators: + accumulated_modules[module_name] = copy.deepcopy(modules[module_name]) + + for module_name in modules: + module = modules[module_name] + modules[module_name] = DistributedDataParallel( + module, + device_ids=[device.index], + output_device=device.index) + + optimizers = {} + for module_name in optimizer_factories: + module = modules[module_name] + optimizer = optimizer_factories[module_name].create(module.parameters()) + optimizer_to_device(optimizer, device) + optimizers[module_name] = optimizer + + torch.manual_seed(random_seed + rank) + + return DistributedTrainingState(examples_seen_so_far, modules, accumulated_modules, optimizers) + + @staticmethod + def can_load(prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + optimizer_factories: Dict[str, OptimizerFactory], + world_size: int) -> bool: + logging.info(f"Checking directory {prefix}") + if not os.path.isdir(prefix): + logging.info(f"Cannot load files in {prefix} because it is not a directory") + return False + examples_seen_so_far_file_name = DistributedTrainingState.get_examples_seen_so_far_file_name(prefix) + if not os.path.isfile(examples_seen_so_far_file_name): + logging.info(f"Cannot load files in {prefix} because {examples_seen_so_far_file_name} is not a file.") + return False + for module_name in module_factories.keys(): + file_name = DistributedTrainingState.get_module_file_name(prefix, module_name) + if not os.path.isfile(file_name): + logging.info(f"Cannot load files in {prefix} because {file_name} is not a file.") + return False + for module_name in accumulators: + file_name = DistributedTrainingState.get_accumulated_module_file_name(prefix, module_name) + if not os.path.isfile(file_name): + logging.info(f"Cannot load files in {prefix} because {file_name} is not a file.") + return False + for module_name in optimizer_factories: + file_name = DistributedTrainingState.get_optimizer_file_name(prefix, module_name) + if not os.path.isfile(file_name): + logging.info(f"Cannot load files in {prefix} because {file_name} is not a file.") + return False + for rank in range(world_size): + file_name = DistributedTrainingState.get_rng_state_file_name(prefix, rank) + if not os.path.isfile(file_name): + logging.info(f"Cannot load files in {prefix} because {file_name} is not a file.") + return False + return True diff --git a/tha4/src/tha4/shion/core/training/distrib/distributed_training_tasks.py b/tha4/src/tha4/shion/core/training/distrib/distributed_training_tasks.py new file mode 100644 index 0000000..6b65cfc --- /dev/null +++ b/tha4/src/tha4/shion/core/training/distrib/distributed_training_tasks.py @@ -0,0 +1,139 @@ +import logging +import os +import sys +from typing import Callable, List, Optional + +from tha4.pytasuku.workspace import Workspace +from tha4.shion.core.training.distrib.distributed_trainer import DistributedTrainer +from tha4.shion.core.training.distrib.distributed_training_states import DistributedTrainingState + + +def get_torchrun_executable(): + return os.path.dirname(sys.executable) + os.path.sep + "torchrun" + + +def run_distributed_training_script( + training_script_file_name: str, + num_nodes: int, + node_rank: int, + num_proc_per_node: int, + master_addr: int = "127.0.0.1", + master_port: int = 8888): + command = f"{get_torchrun_executable()} " \ + f"--nproc_per_node={num_proc_per_node} " \ + f"--nnodes={num_nodes} " \ + f"--node_rank={node_rank} " \ + f"--master_addr={master_addr} " \ + f"--master_port={master_port} " \ + f"{training_script_file_name}" + logging.info(f"Executing -- {command}") + os.system(command) + + +class RdzvConfig: + def __init__(self, id: int, port: int): + self.port = port + self.id = id + + +def run_standalone_distributed_training_script( + training_script_file_name: str, + num_proc_per_node: int, + target_checkpoint_examples: Optional[int] = None, + rdzv_config: Optional[RdzvConfig] = None): + command = f"{get_torchrun_executable()} " \ + f"--nnodes=1 " \ + f"--nproc_per_node={num_proc_per_node} " + if rdzv_config is not None: + command += f"--rdzv_endpoint=localhost:{rdzv_config.port} " + command += "--rdzv_backend=c10d " + command += f"--rdzv_id={rdzv_config.id} " + else: + command += "--standalone " + command += f"{training_script_file_name} " + if target_checkpoint_examples is not None: + command += f"--target_checkpoint_examples {target_checkpoint_examples} " + logging.info(f"Executing -- {command}") + os.system(command) + + +def define_distributed_training_tasks( + workspace: Workspace, + prefix: str, + training_script_file_name: str, + num_nodes: int, + num_proc_per_node: int, + master_addr: int = "127.0.0.1", + master_port: int = 8888): + def run_training_script_func(rank: int): + def _f(): + run_distributed_training_script( + training_script_file_name, + num_nodes, + rank, + num_proc_per_node, master_addr, + master_port) + + return _f + + for i in range(num_nodes): + workspace.create_command_task(f"{prefix}/train_node_%06d" % i, [], run_training_script_func(i)) + + +def define_standalone_distributed_training_tasks( + workspace: Workspace, + distributed_trainer_func: Callable[[int], DistributedTrainer], + training_script_file_name: str, + num_proc_per_node: int, + dependencies: Optional[List[str]] = None, + rdzv_config: Optional[RdzvConfig] = None): + trainer = distributed_trainer_func(1) + checkpoint_examples = trainer.training_protocol.get_checkpoint_examples() + assert len(checkpoint_examples) >= 1 + assert checkpoint_examples[0] > 0 + checkpoint_examples = [0] + checkpoint_examples + + if dependencies is None: + dependencies = [] + module_file_dependencies = dependencies[:] + for module_name in trainer.pretrained_module_file_names: + module_file_dependencies.append(trainer.pretrained_module_file_names[module_name]) + + def create_train_func(target_checkpoint_examples: int): + return lambda: run_standalone_distributed_training_script( + training_script_file_name, + num_proc_per_node, + target_checkpoint_examples, + rdzv_config=rdzv_config) + + train_tasks = [] + for checkpoint_index in range(0, len(checkpoint_examples)): + for module_name in trainer.module_names: + module_file_name = DistributedTrainingState.get_module_file_name( + trainer.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + module_file_name, + module_file_dependencies, + create_train_func(trainer.checkpoint_examples[checkpoint_index])) + for module_name in trainer.accumulators: + accumulated_module_file_name = DistributedTrainingState.get_accumulated_module_file_name( + trainer.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + accumulated_module_file_name, + module_file_dependencies, + create_train_func(checkpoint_examples[checkpoint_index])) + workspace.create_command_task( + trainer.get_checkpoint_prefix(checkpoint_index) + "/train_standalone", + module_file_dependencies, + create_train_func(checkpoint_examples[checkpoint_index])) + train_tasks.append(trainer.get_checkpoint_prefix(checkpoint_index) + "/train_standlone") + workspace.create_file_task( + trainer.prefix + "/train_standalone", + module_file_dependencies, + create_train_func(checkpoint_examples[-1])) + + +if __name__ == "__main__": + print(os.path.dirname(sys.executable) + os.path.sep + "torchrun") diff --git a/tha4/src/tha4/shion/core/training/sample_output_protocol.py b/tha4/src/tha4/shion/core/training/sample_output_protocol.py new file mode 100644 index 0000000..cc7705d --- /dev/null +++ b/tha4/src/tha4/shion/core/training/sample_output_protocol.py @@ -0,0 +1,43 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any + +import torch +from torch.nn import Module +from torch.utils.data import Dataset + + +class SampleOutputProtocol(ABC): + @abstractmethod + def get_examples_per_sample_output(self) -> int: + pass + + @abstractmethod + def get_random_seed(self) -> int: + pass + + @abstractmethod + def get_sample_output_data(self, validation_dataset: Dataset, device: torch.device) -> Any: + pass + + @abstractmethod + def save_sample_output_data( + self, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + sample_output_data: Any, + prefix: str, + examples_seen_so_far: int, + device: torch.device): + pass + + +class AbstractSampleOutputProtocol(SampleOutputProtocol, ABC): + def __init__(self, examples_per_sample_output: int, random_seed: int): + self.random_seed = random_seed + self.examples_per_sample_output = examples_per_sample_output + + def get_examples_per_sample_output(self) -> int: + return self.examples_per_sample_output + + def get_random_seed(self) -> int: + return self.random_seed diff --git a/tha4/src/tha4/shion/core/training/single/__init__.py b/tha4/src/tha4/shion/core/training/single/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/core/training/single/training_states.py b/tha4/src/tha4/shion/core/training/single/training_states.py new file mode 100644 index 0000000..79c4434 --- /dev/null +++ b/tha4/src/tha4/shion/core/training/single/training_states.py @@ -0,0 +1,182 @@ +import copy +import logging +import os +from typing import Dict, Optional + +import torch +from torch.nn import Module +from torch.optim import Optimizer + +from tha4.shion.core.load_save import torch_save, torch_load +from tha4.shion.core.module_accumulator import ModuleAccumulator +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.core.optimizer_factory import OptimizerFactory +from tha4.shion.core.training.util import optimizer_to_device + + +class TrainingState: + def __init__(self, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer]): + self.accumulated_modules = accumulated_modules + self.optimizers = optimizers + self.modules = modules + self.examples_seen_so_far = examples_seen_so_far + + @staticmethod + def get_examples_seen_so_far_file_name(prefix) -> str: + return prefix + "/examples_seen_so_far.txt" + + @staticmethod + def get_module_file_name(prefix, module_name) -> str: + return "%s/module_%s.pt" % (prefix, module_name) + + @staticmethod + def get_accumulated_module_file_name(prefix, module_name) -> str: + return "%s/accumulated_%s.pt" % (prefix, module_name) + + @staticmethod + def get_optimizer_file_name(prefix, module_name) -> str: + return "%s/optimizer_%s.pt" % (prefix, module_name) + + @staticmethod + def get_rng_state_file_name(prefix): + return "%s/rng_state.pt" % prefix + + def save(self, prefix): + logging.info("Saving training state to %s" % prefix) + os.makedirs(prefix, exist_ok=True) + with open(TrainingState.get_examples_seen_so_far_file_name(prefix), "wt") as fout: + fout.write("%d\n" % self.examples_seen_so_far) + logging.info("Saved %s" % TrainingState.get_examples_seen_so_far_file_name(prefix)) + for module_name in self.modules: + file_name = TrainingState.get_module_file_name(prefix, module_name) + torch_save(self.modules[module_name].state_dict(), file_name) + logging.info("Saved %s" % file_name) + for module_name in self.accumulated_modules: + file_name = TrainingState.get_accumulated_module_file_name(prefix, module_name) + torch_save(self.accumulated_modules[module_name].state_dict(), file_name) + logging.info("Saved %s" % file_name) + for module_name in self.optimizers: + file_name = TrainingState.get_optimizer_file_name(prefix, module_name) + torch_save(self.optimizers[module_name].state_dict(), file_name) + logging.info("Saved %s" % file_name) + torch_save(torch.get_rng_state(), TrainingState.get_rng_state_file_name(prefix)) + logging.info("Saved %s" % TrainingState.get_rng_state_file_name(prefix)) + logging.info("Done saving training state to %s" % prefix) + + @staticmethod + def get_examples_seen_so_far(prefix: str) -> int: + with open(TrainingState.get_examples_seen_so_far_file_name(prefix)) as fin: + lines = fin.readlines() + return int(lines[0]) + + @staticmethod + def load(prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + optimizer_factories: Dict[str, OptimizerFactory], + device: torch.device) -> 'TrainingState': + logging.info("Loading training state from %s" % prefix) + + with open(TrainingState.get_examples_seen_so_far_file_name(prefix)) as fin: + lines = fin.readlines() + examples_seen_so_far = int(lines[0]) + logging.info("Loaded %s" % TrainingState.get_examples_seen_so_far_file_name(prefix)) + + modules = { + module_name: factory.create() + for (module_name, factory) in module_factories.items() + } + for module_name in modules: + file_name = TrainingState.get_module_file_name(prefix, module_name) + modules[module_name].load_state_dict(torch_load(file_name)) + modules[module_name].to(device) + logging.info("Loaded %s" % file_name) + + accumulated_modules = {} + for module_name in accumulators: + module_factory = module_factories[module_name] + module = module_factory.create() + file_name = TrainingState.get_accumulated_module_file_name(prefix, module_name) + module.load_state_dict(torch_load(file_name)) + module.to(device) + accumulated_modules[module_name] = module + logging.info("Loaded %s" % file_name) + + optimizers = {} + for module_name in optimizer_factories: + optimizer = optimizer_factories[module_name].create(modules[module_name].parameters()) + file_name = TrainingState.get_optimizer_file_name(prefix, module_name) + optimizer.load_state_dict(torch_load(file_name)) + optimizer_to_device(optimizer, device) + optimizers[module_name] = optimizer + logging.info("Loaded %s" % file_name) + + torch.set_rng_state(torch_load(TrainingState.get_rng_state_file_name(prefix))) + logging.info("Loaded %s" % TrainingState.get_rng_state_file_name(prefix)) + + logging.info("Done loading training state from %s" % prefix) + + return TrainingState(examples_seen_so_far, modules, accumulated_modules, optimizers) + + @staticmethod + def new(module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + optimizer_factories: Dict[str, OptimizerFactory], + random_seed: int, + device: torch.device, + pretrained_module_file_names: Optional[Dict[str, str]] = None) -> 'TrainingState': + examples_seen_so_far = 0 + + modules = { + module_name: factory.create() + for (module_name, factory) in module_factories.items() + } + for module_name in modules: + modules[module_name].to(device) + if pretrained_module_file_names is not None: + for module_name in modules: + if module_name in pretrained_module_file_names: + file_name = pretrained_module_file_names[module_name] + modules[module_name].load_state_dict(torch_load(file_name)) + logging.info("Loaded initial state from %s ..." % file_name) + + accumulated_modules = {} + for module_name in accumulators: + accumulated_modules[module_name] = copy.deepcopy(modules[module_name]) + + optimizers = {} + for module_name in optimizer_factories: + module = modules[module_name] + optimizer = optimizer_factories[module_name].create(module.parameters()) + optimizer_to_device(optimizer, device) + optimizers[module_name] = optimizer + + torch.manual_seed(random_seed) + + return TrainingState(examples_seen_so_far, modules, accumulated_modules, optimizers) + + @staticmethod + def can_load(prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + optimizer_factories: Dict[str, OptimizerFactory]) -> bool: + if not os.path.isdir(prefix): + return False + if not os.path.isfile(TrainingState.get_examples_seen_so_far_file_name(prefix)): + return False + for module_name in module_factories.keys(): + if not os.path.isfile(TrainingState.get_module_file_name(prefix, module_name)): + return False + for module_name in accumulators: + if not os.path.isfile(TrainingState.get_accumulated_module_file_name(prefix, module_name)): + return False + for module_name in optimizer_factories: + if not os.path.isfile(TrainingState.get_optimizer_file_name(prefix, module_name)): + return False + if not os.path.isfile(TrainingState.get_rng_state_file_name(prefix)): + return False + return True diff --git a/tha4/src/tha4/shion/core/training/single/training_tasks.py b/tha4/src/tha4/shion/core/training/single/training_tasks.py new file mode 100644 index 0000000..78a9c4e --- /dev/null +++ b/tha4/src/tha4/shion/core/training/single/training_tasks.py @@ -0,0 +1,357 @@ +import logging +import time +from datetime import datetime +from typing import Optional, Dict, List + +import torch +from torch.utils.data import Dataset, DataLoader +from torch.utils.tensorboard import SummaryWriter + +from tha4.pytasuku.workspace import Workspace +from tha4.shion.core.load_save import torch_save, torch_load +from tha4.shion.core.loss import Loss +from tha4.shion.core.module_accumulator import ModuleAccumulator +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.shion.core.training.single.training_states import TrainingState +from tha4.shion.core.training.training_protocol import TrainingProtocol +from tha4.shion.core.training.util import get_least_greater_multiple, create_log_func, set_learning_rate +from tha4.shion.core.training.validation_protocol import ValidationProtocol + +KEY_CHECKPOINT = 'checkpoint' +KEY_SNAPSHOT = 'snapshot' +KEY_VALIDATION = 'validation' +KEY_SAMPLE_OUTPUT = 'sample_output' + + +class TrainingTasks: + def __init__( + self, + workspace: Workspace, + prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + losses: Dict[str, Loss], + training_dataset: Dataset, + validation_dataset: Optional[Dataset], + training_protocol: TrainingProtocol, + validation_protocol: Optional[ValidationProtocol], + sample_output_protocol: Optional[SampleOutputProtocol], + pretrained_module_file_names: Dict[str, str], + example_per_snapshot: int, + device: torch.device, + num_data_loader_workers: int = 8, + dependencies: Optional[List[str]] = None): + super().__init__() + self.num_data_loader_workers = num_data_loader_workers + self.accumulators = accumulators + self.device = device + self.sample_output_protocol = sample_output_protocol + self.example_per_snapshot = example_per_snapshot + self.pretrained_module_file_names = pretrained_module_file_names + self.losses = losses + self.validation_protocol = validation_protocol + self.training_protocol = training_protocol + self.module_factories = module_factories + self.prefix = prefix + self.training_dataset = training_dataset + self.validation_dataset = validation_dataset + + self.checkpoint_examples = self.training_protocol.get_checkpoint_examples() + assert len(self.checkpoint_examples) >= 1 + assert self.checkpoint_examples[0] > 0 + self.checkpoint_examples = [0] + self.checkpoint_examples + + self.module_names = self.module_factories.keys() + assert len(self.module_names) > 0 + + self.training_data_loader = None + self.training_data_loader_iter = None + self.training_data_loader_batch_size = None + self.validation_data_loader = None + self.validation_data_loader_iter = None + self.validation_data_loader_batch_size = None + self.sample_output_data = None + self.summary_writer = None + self.log_dir = None + self.training_state = None + + if dependencies is None: + dependencies = [] + self.sample_output_data_task = workspace.create_file_task( + self.get_sample_output_data_file_name(), dependencies, self.save_sample_output_data) + + module_file_dependencies = [self.sample_output_data_task.name] + for module_name in pretrained_module_file_names: + module_file_dependencies.append(self.pretrained_module_file_names[module_name]) + + def create_train_func(target_examples: int): + return lambda: self.train(target_examples) + + train_tasks = [] + for checkpoint_index in range(1, len(self.checkpoint_examples)): + for module_name in self.module_names: + module_file_name = TrainingState.get_module_file_name( + self.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + module_file_name, + module_file_dependencies, + create_train_func(self.checkpoint_examples[checkpoint_index])) + for module_name in self.accumulators: + accumulated_module_file_name = TrainingState.get_accumulated_module_file_name( + self.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + accumulated_module_file_name, + module_file_dependencies, + create_train_func(self.checkpoint_examples[checkpoint_index])) + workspace.create_command_task( + self.get_checkpoint_prefix(checkpoint_index) + "/train", + module_file_dependencies, + create_train_func(self.checkpoint_examples[checkpoint_index])) + train_tasks.append(self.get_checkpoint_prefix(checkpoint_index) + "/train") + + self.train_task = workspace.create_file_task( + self.get_train_command_name(), + module_file_dependencies, + create_train_func(self.checkpoint_examples[-1])) + + def get_sample_output_data_file_name(self): + return self.prefix + "/sample_output_data.pt" + + def save_sample_output_data(self): + if self.sample_output_protocol is not None: + torch.manual_seed(self.sample_output_protocol.get_random_seed()) + sample_output_data = self.sample_output_protocol.get_sample_output_data(self.validation_dataset, + self.device) + torch_save(sample_output_data, self.get_sample_output_data_file_name()) + else: + torch_save({}, self.get_sample_output_data_file_name()) + + def get_module_file_name(self, checkpoint_index, module_name): + return TrainingState.get_module_file_name(self.get_checkpoint_prefix(checkpoint_index), module_name) + + def get_last_module_file_name(self, module_name): + return self.get_module_file_name(len(self.checkpoint_examples) - 1, module_name) + + def get_log_dir(self): + if self.log_dir is None: + now = datetime.now() + self.log_dir = self.prefix + "/log/" + now.strftime("%Y_%m_%d__%H_%M_%S") + return self.log_dir + + def get_summary_writer(self) -> SummaryWriter: + if self.summary_writer is None: + self.summary_writer = SummaryWriter(log_dir=self.get_log_dir()) + return self.summary_writer + + def get_train_command_name(self) -> str: + return self.prefix + "/train" + + def get_snapshot_prefix(self) -> str: + return self.prefix + "/snapshot" + + def get_checkpoint_prefix(self, checkpoint_index) -> str: + return "%s/checkpoint/%04d" % (self.prefix, checkpoint_index) + + def can_load_training_state(self, prefix) -> bool: + return TrainingState.can_load( + prefix, + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories()) + + def load_training_state(self, prefix) -> TrainingState: + return TrainingState.load( + prefix, + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + self.device) + + def get_initial_training_state(self) -> TrainingState: + training_state = TrainingState.new( + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + self.training_protocol.get_random_seed(), + self.device, + self.pretrained_module_file_names) + logging.info("Created a new initial training state.") + return training_state + + def load_previous_training_state(self, target_checkpoint_examples: int) -> TrainingState: + if self.can_load_training_state(self.get_snapshot_prefix()): + examples_seen_so_far = TrainingState.get_examples_seen_so_far(self.get_snapshot_prefix()) + diff = examples_seen_so_far - target_checkpoint_examples + if diff < self.training_protocol.get_batch_size(): + return self.load_training_state(self.get_snapshot_prefix()) + num_checkpoints = len(self.checkpoint_examples) + for checkpoint_index in range(num_checkpoints - 1, -1, -1): + if self.can_load_training_state(self.get_checkpoint_prefix(checkpoint_index)): + examples_seen_so_far = TrainingState.get_examples_seen_so_far( + self.get_checkpoint_prefix(checkpoint_index)) + diff = examples_seen_so_far - target_checkpoint_examples + if diff < self.training_protocol.get_batch_size(): + return self.load_training_state(self.get_checkpoint_prefix(checkpoint_index)) + return self.get_initial_training_state() + + def get_next_checkpoint_num_examples(self, examples_seen_so_far) -> int: + next_index = next( + (i for i in range(len(self.checkpoint_examples)) if self.checkpoint_examples[i] > examples_seen_so_far), + -1) + return self.checkpoint_examples[next_index] + + def get_next_snapshot_num_examples(self, examples_seen_so_far) -> int: + return get_least_greater_multiple(examples_seen_so_far, self.example_per_snapshot) + + def get_next_validation_num_examples(self, examples_seen_so_far) -> int: + if self.validation_protocol is None: + return -1 + return get_least_greater_multiple(examples_seen_so_far, + self.validation_protocol.get_examples_per_validation_iteration()) + + def get_next_sample_output_num_examples(self, examples_seen_so_far) -> int: + if self.sample_output_protocol is None: + return -1 + return get_least_greater_multiple(examples_seen_so_far, + self.sample_output_protocol.get_examples_per_sample_output()) + + def get_next_num_examples(self, examples_seen_so_far) -> Dict[str, int]: + return { + KEY_CHECKPOINT: self.get_next_checkpoint_num_examples(examples_seen_so_far), + KEY_SNAPSHOT: self.get_next_snapshot_num_examples(examples_seen_so_far), + KEY_VALIDATION: self.get_next_validation_num_examples(examples_seen_so_far), + KEY_SAMPLE_OUTPUT: self.get_next_sample_output_num_examples(examples_seen_so_far) + } + + def get_checkpoint_index_to_save(self, examples_seen_so_far: int) -> int: + checkpoint_index = 0 + for i in range(len(self.checkpoint_examples)): + if self.checkpoint_examples[i] <= examples_seen_so_far: + checkpoint_index = i + return checkpoint_index + + def get_next_training_batch(self): + if self.training_data_loader is None: + self.training_data_loader = DataLoader( + self.training_dataset, + batch_size=self.training_protocol.get_batch_size(), + shuffle=True, + num_workers=self.num_data_loader_workers, + drop_last=True) + if self.training_data_loader_iter is None: + self.training_data_loader_iter = iter(self.training_data_loader) + try: + batch = next(self.training_data_loader_iter) + except StopIteration: + self.training_data_loader_iter = iter(self.training_data_loader) + batch = next(self.training_data_loader_iter) + return [x.to(self.device) for x in batch] + + def get_next_validation_batch(self): + if self.validation_dataset is None: + return None + if self.validation_data_loader is None: + self.validation_data_loader = DataLoader( + self.validation_dataset, + batch_size=self.validation_protocol.get_batch_size(), + shuffle=True, + num_workers=self.num_data_loader_workers, + drop_last=True) + if self.validation_data_loader_iter is None: + self.validation_data_loader_iter = iter(self.validation_data_loader) + try: + batch = next(self.validation_data_loader_iter) + except StopIteration: + self.validation_data_loader_iter = iter(self.validation_data_loader) + batch = next(self.validation_data_loader_iter) + return [x.to(self.device) for x in batch] + + def get_checkpoint_index(self, target_checkpoint_examples: int): + return self.checkpoint_examples.index(target_checkpoint_examples) + + def train(self, target_checkpoint_examples: Optional[int] = None): + if target_checkpoint_examples is None: + target_checkpoint_examples = self.checkpoint_examples[-1] + + sample_output_data = torch_load(self.get_sample_output_data_file_name()) + logging.info("Loaded sampled output data from %s", self.get_sample_output_data_file_name()) + training_state = self.load_previous_training_state(target_checkpoint_examples) + summary_writer = self.get_summary_writer() + last_time = time.time() + + while training_state.examples_seen_so_far < target_checkpoint_examples: + # One training iteration + learning_rate = self.training_protocol.get_learning_rate(training_state.examples_seen_so_far) + for module_name in self.module_factories.keys(): + if module_name not in learning_rate or module_name not in training_state.optimizers: + continue + lr = learning_rate[module_name] + set_learning_rate(training_state.optimizers[module_name], lr) + self.get_summary_writer().add_scalar( + module_name + "_learning_rate", lr, training_state.examples_seen_so_far) + training_batch = self.get_next_training_batch() + self.training_protocol.run_training_iteration( + training_batch, + training_state.examples_seen_so_far, + training_state.modules, + training_state.accumulated_modules, + training_state.optimizers, + self.losses, + lambda name, num: create_log_func(summary_writer, name, num), + self.device) + + # Accumulate model data + for module_name in self.accumulators: + new_module = training_state.modules[module_name] + buffer_module = training_state.accumulated_modules[module_name] + self.accumulators[module_name].accumulate( + new_module, + buffer_module, + training_state.examples_seen_so_far) + + # Advance the number of examples seen so far + next_num_examples = self.get_next_num_examples(training_state.examples_seen_so_far) + training_state.examples_seen_so_far += self.training_protocol.get_batch_size() + + # Validation iteration + if self.validation_protocol is not None \ + and training_state.examples_seen_so_far >= next_num_examples[KEY_VALIDATION]: + validation_batch = self.get_next_validation_batch() + self.validation_protocol.run_validation_iteration( + validation_batch, + training_state.examples_seen_so_far, + training_state.modules, + training_state.accumulated_modules, + self.losses, + lambda name, num: create_log_func(summary_writer, name, num), + self.device) + + # Save sample output + if self.sample_output_protocol is not None \ + and training_state.examples_seen_so_far >= next_num_examples[KEY_SAMPLE_OUTPUT]: + self.sample_output_protocol.save_sample_output_data( + training_state.modules, + training_state.accumulated_modules, + sample_output_data, + self.prefix + "/sample_outputs", + training_state.examples_seen_so_far, + self.device) + + # Save checkpoint + if training_state.examples_seen_so_far >= next_num_examples[KEY_CHECKPOINT]: + checkpoint_index = self.get_checkpoint_index_to_save(training_state.examples_seen_so_far) + training_state.save(self.get_checkpoint_prefix(checkpoint_index)) + if next_num_examples[KEY_CHECKPOINT] != next_num_examples[KEY_SNAPSHOT]: + training_state.save(self.get_snapshot_prefix()) + + # Save snapshot + if training_state.examples_seen_so_far >= next_num_examples[KEY_SNAPSHOT]: + training_state.save(self.get_snapshot_prefix()) + + now = time.time() + if now - last_time > 10: + logging.info("Showed %d training examples." % training_state.examples_seen_so_far) + last_time = now diff --git a/tha4/src/tha4/shion/core/training/swarm/__init__.py b/tha4/src/tha4/shion/core/training/swarm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/core/training/swarm/swarm_training_tasks.py b/tha4/src/tha4/shion/core/training/swarm/swarm_training_tasks.py new file mode 100644 index 0000000..06360ef --- /dev/null +++ b/tha4/src/tha4/shion/core/training/swarm/swarm_training_tasks.py @@ -0,0 +1,62 @@ +from typing import Callable, Optional, List + +from tha4.pytasuku.workspace import Workspace +from tha4.shion.core.training.distrib.distributed_training_tasks import RdzvConfig, \ + run_standalone_distributed_training_script +from tha4.shion.core.training.single.training_states import TrainingState +from tha4.shion.core.training.swarm.swarm_unit_trainer import SwarmUnitTrainer + + +def define_standalone_swarm_training_tasks( + workspace: Workspace, + swarm_unit_trainer_func: Callable[[], SwarmUnitTrainer], + training_script_file_name: str, + num_proc_per_node: int, + dependencies: Optional[List[str]] = None, + rdzv_config: Optional[RdzvConfig] = None): + trainer = swarm_unit_trainer_func() + checkpoint_examples = trainer.training_protocol.get_checkpoint_examples() + assert len(checkpoint_examples) >= 1 + assert checkpoint_examples[0] > 0 + checkpoint_examples = [0] + checkpoint_examples + + if dependencies is None: + dependencies = [] + module_file_dependencies = dependencies[:] + for module_name in trainer.pretrained_module_file_names: + module_file_dependencies.append(trainer.pretrained_module_file_names[module_name]) + + def create_train_func(target_checkpoint_examples: int): + return lambda: run_standalone_distributed_training_script( + training_script_file_name, + num_proc_per_node, + target_checkpoint_examples, + rdzv_config=rdzv_config) + + train_tasks = [] + for checkpoint_index in range(0, len(checkpoint_examples)): + for module_name in trainer.module_names: + module_file_name = TrainingState.get_module_file_name( + trainer.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + module_file_name, + module_file_dependencies, + create_train_func(trainer.checkpoint_examples[checkpoint_index])) + for module_name in trainer.accumulators: + accumulated_module_file_name = TrainingState.get_accumulated_module_file_name( + trainer.get_checkpoint_prefix(checkpoint_index), + module_name) + workspace.create_file_task( + accumulated_module_file_name, + module_file_dependencies, + create_train_func(checkpoint_examples[checkpoint_index])) + workspace.create_command_task( + trainer.get_checkpoint_prefix(checkpoint_index) + "/train_standalone", + module_file_dependencies, + create_train_func(checkpoint_examples[checkpoint_index])) + train_tasks.append(trainer.get_checkpoint_prefix(checkpoint_index) + "/train_standlone") + workspace.create_file_task( + trainer.prefix + "/train_standalone", + module_file_dependencies, + create_train_func(checkpoint_examples[-1])) diff --git a/tha4/src/tha4/shion/core/training/swarm/swarm_unit_trainer.py b/tha4/src/tha4/shion/core/training/swarm/swarm_unit_trainer.py new file mode 100644 index 0000000..e5ebc5a --- /dev/null +++ b/tha4/src/tha4/shion/core/training/swarm/swarm_unit_trainer.py @@ -0,0 +1,345 @@ +import argparse +import logging +import os +import time +from datetime import datetime +from typing import Dict, Optional, Callable +import torch.distributed + +import torch +from torch.utils.data import Dataset, DataLoader +from torch.utils.tensorboard import SummaryWriter + +from tha4.shion.core.load_save import torch_save, torch_load +from tha4.shion.core.loss import Loss +from tha4.shion.core.module_accumulator import ModuleAccumulator +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.core.training.distrib.device_mapper import SimpleCudaDeviceMapper +from tha4.shion.core.training.sample_output_protocol import SampleOutputProtocol +from tha4.shion.core.training.single.training_states import TrainingState +from tha4.shion.core.training.single.training_tasks import KEY_CHECKPOINT, KEY_SNAPSHOT, KEY_VALIDATION, KEY_SAMPLE_OUTPUT +from tha4.shion.core.training.training_protocol import TrainingProtocol +from tha4.shion.core.training.util import get_least_greater_multiple, create_log_func, set_learning_rate +from tha4.shion.core.training.validation_protocol import ValidationProtocol + + +class SwarmUnitTrainer: + def __init__(self, + prefix: str, + module_factories: Dict[str, ModuleFactory], + accumulators: Dict[str, ModuleAccumulator], + losses: Dict[str, Loss], + training_dataset: Dataset, + validation_dataset: Optional[Dataset], + training_protocol: TrainingProtocol, + validation_protocol: Optional[ValidationProtocol], + sample_output_protocol: Optional[SampleOutputProtocol], + pretrained_module_file_names: Dict[str, str], + example_per_snapshot: int, + num_data_loader_workers: int = 8): + self.num_data_loader_workers = num_data_loader_workers + self.accumulators = accumulators + self.sample_output_protocol = sample_output_protocol + self.example_per_snapshot = example_per_snapshot + self.pretrained_module_file_names = pretrained_module_file_names + self.losses = losses + self.validation_protocol = validation_protocol + self.training_protocol = training_protocol + self.module_factories = module_factories + self.prefix = prefix + self.training_dataset = training_dataset + self.validation_dataset = validation_dataset + + self.checkpoint_examples = self.training_protocol.get_checkpoint_examples() + assert len(self.checkpoint_examples) >= 1 + assert self.checkpoint_examples[0] > 0 + self.checkpoint_examples = [0] + self.checkpoint_examples + + self.module_names = self.module_factories.keys() + assert len(self.module_names) > 0 + + self.training_data_loader = None + self.training_data_loader_iter = None + self.training_data_loader_batch_size = None + self.training_data_sampler = None + + self.validation_data_loader = None + self.validation_data_loader_iter = None + self.validation_data_loader_batch_size = None + + self.sample_output_data = None + self.summary_writer = None + self.log_dir = None + self.training_state = None + + def get_sample_output_data_file_name(self): + return self.prefix + "/sample_output_data.pt" + + def save_sample_output_data(self, device: torch.device): + if os.path.exists(self.get_sample_output_data_file_name()): + return + if self.sample_output_protocol is not None: + torch.manual_seed(self.sample_output_protocol.get_random_seed()) + sample_output_data = self.sample_output_protocol.get_sample_output_data(self.validation_dataset, device) + torch_save(sample_output_data, self.get_sample_output_data_file_name()) + else: + torch_save({}, self.get_sample_output_data_file_name()) + + def load_sample_output_data(self, device: torch.device): + self.save_sample_output_data(device) + return torch_load(self.get_sample_output_data_file_name()) + + def get_snapshot_prefix(self) -> str: + return self.prefix + "/snapshot" + + def can_load_training_state(self, prefix: str) -> bool: + return TrainingState.can_load( + prefix, + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories()) + + def load_training_state(self, prefix, device: torch.device) -> TrainingState: + return TrainingState.load( + prefix, + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + device) + + @staticmethod + def checkpoint_prefix(prefix: str, checkpoint_index: int) -> str: + return "%s/checkpoint/%04d" % (prefix, checkpoint_index) + + def get_checkpoint_prefix(self, checkpoint_index) -> str: + return SwarmUnitTrainer.checkpoint_prefix(self.prefix, checkpoint_index) + + def get_initial_training_state(self, device: torch.device) -> TrainingState: + training_state = TrainingState.new( + self.module_factories, + self.accumulators, + self.training_protocol.get_optimizer_factories(), + self.training_protocol.get_random_seed(), + device, + self.pretrained_module_file_names) + logging.info("Created a new initial training state.") + return training_state + + def load_previous_training_state(self, + target_checkpoint_examples: int, + device: torch.device) -> TrainingState: + if self.can_load_training_state(self.get_snapshot_prefix()): + examples_seen_so_far = TrainingState.get_examples_seen_so_far(self.get_snapshot_prefix()) + diff = examples_seen_so_far - target_checkpoint_examples + if diff < self.training_protocol.get_batch_size(): + return self.load_training_state(self.get_snapshot_prefix(), device) + num_checkpoints = len(self.checkpoint_examples) + for checkpoint_index in range(num_checkpoints - 1, -1, -1): + if self.can_load_training_state(self.get_checkpoint_prefix(checkpoint_index)): + examples_seen_so_far = TrainingState.get_examples_seen_so_far( + self.get_checkpoint_prefix(checkpoint_index)) + diff = examples_seen_so_far - target_checkpoint_examples + if diff < self.training_protocol.get_batch_size(): + return self.load_training_state( + self.get_checkpoint_prefix(checkpoint_index), device) + + training_state = self.get_initial_training_state(device) + training_state.save(self.get_checkpoint_prefix(0)) + training_state = self.load_training_state(self.get_checkpoint_prefix(0), device) + return training_state + + def get_log_dir(self): + if self.log_dir is None: + now = datetime.now() + self.log_dir = self.prefix + "/log/" + now.strftime("%Y_%m_%d__%H_%M_%S") + return self.log_dir + + def get_summary_writer(self) -> Optional[SummaryWriter]: + if self.summary_writer is None: + self.summary_writer = SummaryWriter(log_dir=self.get_log_dir()) + return self.summary_writer + + def get_next_training_batch(self, device: torch.device): + if self.training_data_loader is None: + self.training_data_loader = DataLoader( + self.training_dataset, + batch_size=self.training_protocol.get_batch_size(), + shuffle=True, + num_workers=self.num_data_loader_workers, + drop_last=True) + if self.training_data_loader_iter is None: + self.training_data_loader_iter = iter(self.training_data_loader) + try: + batch = next(self.training_data_loader_iter) + except StopIteration: + self.training_data_loader_iter = iter(self.training_data_loader) + batch = next(self.training_data_loader_iter) + return [x.to(device) for x in batch] + + def get_next_checkpoint_num_examples(self, examples_seen_so_far) -> int: + next_index = next( + (i for i in range(len(self.checkpoint_examples)) if self.checkpoint_examples[i] > examples_seen_so_far), + -1) + return self.checkpoint_examples[next_index] + + def get_next_snapshot_num_examples(self, examples_seen_so_far) -> int: + return get_least_greater_multiple(examples_seen_so_far, self.example_per_snapshot) + + def get_next_validation_num_examples(self, examples_seen_so_far) -> int: + if self.validation_protocol is None: + return -1 + return get_least_greater_multiple(examples_seen_so_far, + self.validation_protocol.get_examples_per_validation_iteration()) + + def get_next_sample_output_num_examples(self, examples_seen_so_far) -> int: + if self.sample_output_protocol is None: + return -1 + return get_least_greater_multiple(examples_seen_so_far, + self.sample_output_protocol.get_examples_per_sample_output()) + + def get_next_num_examples(self, examples_seen_so_far) -> Dict[str, int]: + return { + KEY_CHECKPOINT: self.get_next_checkpoint_num_examples(examples_seen_so_far), + KEY_SNAPSHOT: self.get_next_snapshot_num_examples(examples_seen_so_far), + KEY_VALIDATION: self.get_next_validation_num_examples(examples_seen_so_far), + KEY_SAMPLE_OUTPUT: self.get_next_sample_output_num_examples(examples_seen_so_far) + } + + def get_next_validation_batch(self, device: torch.device): + if self.validation_dataset is None: + return None + if self.validation_data_loader is None: + self.validation_data_loader = DataLoader( + self.validation_dataset, + batch_size=self.validation_protocol.get_batch_size(), + shuffle=True, + num_workers=1, + drop_last=True) + if self.validation_data_loader_iter is None: + self.validation_data_loader_iter = iter(self.validation_data_loader) + try: + batch = next(self.validation_data_loader_iter) + except StopIteration: + self.validation_data_loader_iter = iter(self.validation_data_loader) + batch = next(self.validation_data_loader_iter) + return [x.to(device) for x in batch] + + def get_checkpoint_index_to_save(self, examples_seen_so_far: int) -> int: + checkpoint_index = 0 + for i in range(len(self.checkpoint_examples)): + if self.checkpoint_examples[i] <= examples_seen_so_far: + checkpoint_index = i + return checkpoint_index + + def train(self, + rank: int, + local_rank: int, + target_checkpoint_examples: Optional[int] = None, + device_mapper: Optional[Callable[[int, int], torch.device]] = None): + if target_checkpoint_examples is None: + target_checkpoint_examples = self.checkpoint_examples[-1] + + if device_mapper is None: + device_mapper = SimpleCudaDeviceMapper() + device = device_mapper(rank, local_rank) + + sample_output_data = self.load_sample_output_data(device) + training_state = self.load_previous_training_state( + target_checkpoint_examples, device) + summary_writer = self.get_summary_writer() + if summary_writer is not None: + log_func_factory = lambda name, num: create_log_func(summary_writer, name, num) + else: + log_func_factory = None + last_time = time.time() + + while training_state.examples_seen_so_far < target_checkpoint_examples: + # Set the learning rate + learning_rate_by_module_name = self.training_protocol.get_learning_rate(training_state.examples_seen_so_far) + for module_name in self.module_factories.keys(): + if module_name not in learning_rate_by_module_name or module_name not in training_state.optimizers: + continue + lr = learning_rate_by_module_name[module_name] + set_learning_rate(training_state.optimizers[module_name], lr) + if summary_writer is not None: + summary_writer.add_scalar( + module_name + "_learning_rate", lr, training_state.examples_seen_so_far) + + # One training iteration + training_batch = self.get_next_training_batch(device) + self.training_protocol.run_training_iteration( + training_batch, + training_state.examples_seen_so_far, + training_state.modules, + training_state.accumulated_modules, + training_state.optimizers, + self.losses, + log_func_factory, + device) + + # Accumulate model data + for module_name in self.accumulators: + new_module = training_state.modules[module_name] + buffer_module = training_state.accumulated_modules[module_name] + self.accumulators[module_name].accumulate( + new_module, buffer_module, examples_seen_so_far=training_state.examples_seen_so_far) + + # Advance the number of examples seen so far + next_num_examples = self.get_next_num_examples(training_state.examples_seen_so_far) + training_state.examples_seen_so_far += self.training_protocol.get_batch_size() + + # Validation iteration + if self.validation_protocol is not None \ + and training_state.examples_seen_so_far >= next_num_examples[KEY_VALIDATION]: + validation_batch = self.get_next_validation_batch(device) + self.validation_protocol.run_validation_iteration( + validation_batch, + training_state.examples_seen_so_far, + training_state.modules, + training_state.accumulated_modules, + self.losses, + log_func_factory, + device) + + # Save sample output + if self.sample_output_protocol is not None \ + and training_state.examples_seen_so_far >= next_num_examples[KEY_SAMPLE_OUTPUT]: + self.sample_output_protocol.save_sample_output_data( + training_state.modules, + training_state.accumulated_modules, + sample_output_data, + self.prefix + "/sample_outputs", + training_state.examples_seen_so_far, + device) + + # Save checkpoint + if training_state.examples_seen_so_far >= next_num_examples[KEY_CHECKPOINT]: + checkpoint_index = self.get_checkpoint_index_to_save(training_state.examples_seen_so_far) + training_state.save(self.get_checkpoint_prefix(checkpoint_index)) + if next_num_examples[KEY_CHECKPOINT] != next_num_examples[KEY_SNAPSHOT]: + training_state.save(self.get_snapshot_prefix()) + + # Save snapshot + if training_state.examples_seen_so_far >= next_num_examples[KEY_SNAPSHOT]: + training_state.save(self.get_snapshot_prefix()) + + now = time.time() + if now - last_time > 10: + logging.info("[Rank %d] Showed %d training examples." % (rank, training_state.examples_seen_so_far)) + last_time = now + + @staticmethod + def run(trainer_factory: Dict[int, Callable[[], 'SwarmUnitTrainer']], + backend: str = 'gloo', + device_mapper: Optional[Callable[[int, int], torch.device]] = None): + parser = argparse.ArgumentParser(description='Training script.') + parser.add_argument("--target_checkpoint_examples", type=int) + args = parser.parse_args() + + rank = int(os.environ['RANK']) + local_rank = int(os.environ['LOCAL_RANK']) + + torch.distributed.init_process_group(backend) + if rank in trainer_factory: + trainer = trainer_factory[rank]() + trainer.train(rank, local_rank, args.target_checkpoint_examples, device_mapper) diff --git a/tha4/src/tha4/shion/core/training/training_protocol.py b/tha4/src/tha4/shion/core/training/training_protocol.py new file mode 100644 index 0000000..da61864 --- /dev/null +++ b/tha4/src/tha4/shion/core/training/training_protocol.py @@ -0,0 +1,73 @@ +from abc import ABC, abstractmethod +from typing import Dict, List, Callable, Any, Optional + +import torch +from torch.nn import Module +from torch.optim.optimizer import Optimizer + +from tha4.shion.core.loss import Loss +from tha4.shion.core.optimizer_factory import OptimizerFactory + + +class TrainingProtocol(ABC): + @abstractmethod + def get_optimizer_factories(self) -> Dict[str, OptimizerFactory]: + pass + + @abstractmethod + def get_checkpoint_examples(self) -> List[int]: + pass + + @abstractmethod + def get_random_seed(self) -> int: + pass + + @abstractmethod + def get_batch_size(self) -> int: + pass + + @abstractmethod + def get_learning_rate(self, examples_seen_so_far: int) -> Dict[str, float]: + pass + + @abstractmethod + def run_training_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + optimizers: Dict[str, Optimizer], + losses: Dict[str, Loss], + create_log_func: Optional[Callable[[str, int], Callable[[str, float], None]]], + device: torch.device): + pass + + +class AbstractTrainingProtocol(TrainingProtocol, ABC): + def __init__(self, + check_point_examples: List[int], + batch_size: int, + learning_rate: Callable[[int], Dict[str, float]], + optimizer_factories: Dict[str, OptimizerFactory], + random_seed: int): + self.random_seed = random_seed + self.optimizer_factories = optimizer_factories + self.learning_rate = learning_rate + self.batch_size = batch_size + self.check_point_examples = check_point_examples + + def get_optimizer_factories(self) -> Dict[str, OptimizerFactory]: + return self.optimizer_factories + + def get_checkpoint_examples(self) -> List[int]: + return self.check_point_examples + + def get_random_seed(self) -> int: + return self.random_seed + + def get_batch_size(self) -> int: + return self.batch_size + + def get_learning_rate(self, examples_seen_so_far: int) -> Dict[str, float]: + return self.learning_rate(examples_seen_so_far) diff --git a/tha4/src/tha4/shion/core/training/util.py b/tha4/src/tha4/shion/core/training/util.py new file mode 100644 index 0000000..862c0ac --- /dev/null +++ b/tha4/src/tha4/shion/core/training/util.py @@ -0,0 +1,41 @@ +from typing import Callable + +import torch +from torch.nn import Module +from torch.optim import Optimizer + + +def optimizer_to_device(optim: Optimizer, device: torch.device): + for state in optim.state.values(): + for k, v in state.items(): + if isinstance(v, torch.Tensor): + state[k] = v.to(device) + + +def zero_module(module: Module): + parameters = dict(module.named_parameters()) + for k in parameters.keys(): + parameters[k].data.zero_() + + +def get_least_greater_multiple(x: int, m: int) -> int: + """ + :param x: a non-negative integer + :param m: a positive integer + :return: the next multiple of m that is greater than x + """ + assert x >= 0 + assert m > 0 + return (x // m + 1) * m + + +def create_log_func(summary_writer, prefix: str, examples_seen_so_far: int) -> Callable[[str, float], None]: + def log_func(tag: str, value: float): + summary_writer.add_scalar(prefix + "_" + tag, value, examples_seen_so_far) + + return log_func + + +def set_learning_rate(module, lr): + for param_group in module.param_groups: + param_group['lr'] = lr diff --git a/tha4/src/tha4/shion/core/training/validation_protocol.py b/tha4/src/tha4/shion/core/training/validation_protocol.py new file mode 100644 index 0000000..806b2da --- /dev/null +++ b/tha4/src/tha4/shion/core/training/validation_protocol.py @@ -0,0 +1,43 @@ +from abc import ABC, abstractmethod +from typing import Dict, Callable, Any + +import torch +from torch.nn import Module + +from tha4.shion.core.loss import Loss + + +class ValidationProtocol(ABC): + @abstractmethod + def get_batch_size(self) -> int: + pass + + @abstractmethod + def get_examples_per_validation_iteration(self) -> int: + pass + + @abstractmethod + def run_validation_iteration( + self, + batch: Any, + examples_seen_so_far: int, + modules: Dict[str, Module], + accumulated_modules: Dict[str, Module], + losses: Dict[str, Loss], + create_log_func: Callable[[str, int], Callable[[str, float], None]], + device: torch.device): + pass + + +class AbstractValidationProtocol(ValidationProtocol, ABC): + def __init__(self, + example_per_validation_iteration: int, + batch_size: int): + self.batch_size = batch_size + self.example_per_validation_iteration = example_per_validation_iteration + + def get_batch_size(self) -> int: + return self.batch_size + + def get_examples_per_validation_iteration(self) -> int: + return self.example_per_validation_iteration diff --git a/tha4/src/tha4/shion/nn00/__init__.py b/tha4/src/tha4/shion/nn00/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tha4/src/tha4/shion/nn00/block_args.py b/tha4/src/tha4/shion/nn00/block_args.py new file mode 100644 index 0000000..f1c39f8 --- /dev/null +++ b/tha4/src/tha4/shion/nn00/block_args.py @@ -0,0 +1,22 @@ +from typing import Optional + +from torch.nn import Module, Sequential + +from tha4.shion.core.module_factory import ModuleFactory +from tha4.shion.nn00.linear_module_args import LinearModuleArgs +from tha4.shion.nn00.nonlinearity_factories import resolve_nonlinearity_factory +from tha4.shion.nn00.normalization_layer_factories import resolve_normalization_layer_factory +from tha4.shion.nn00.normalization_layer_factory import NormalizationLayerFactory + + +class BlockArgs: + def __init__( + self, + linear_module_args: Optional[LinearModuleArgs] = None, + normalization_layer_factory: Optional[NormalizationLayerFactory] = None, + nonlinearity_factory: Optional[ModuleFactory] = None): + if linear_module_args is None: + linear_module_args = LinearModuleArgs() + self.nonlinearity_factory = resolve_nonlinearity_factory(nonlinearity_factory) + self.normalization_layer_factory = resolve_normalization_layer_factory(normalization_layer_factory) + self.linear_module_args = linear_module_args \ No newline at end of file diff --git a/tha4/src/tha4/shion/nn00/conv.py b/tha4/src/tha4/shion/nn00/conv.py new file mode 100644 index 0000000..c1718cc --- /dev/null +++ b/tha4/src/tha4/shion/nn00/conv.py @@ -0,0 +1,102 @@ +from typing import Optional, Union, Callable + +from torch.nn import Conv2d, Module, Sequential, ConvTranspose2d + +from tha4.shion.nn00.block_args import BlockArgs +from tha4.shion.nn00.linear_module_args import LinearModuleArgs, wrap_linear_module + + +def create_conv7( + in_channels: int, + out_channels: int, + bias: bool = False, + linear_module_args: Optional[LinearModuleArgs] = None) -> Module: + return wrap_linear_module( + Conv2d(in_channels, out_channels, kernel_size=7, stride=1, padding=3, bias=bias), + linear_module_args) + + +def create_conv3(in_channels: int, + out_channels: int, + bias: bool = False, + linear_module_args: Optional[LinearModuleArgs] = None) -> Module: + return wrap_linear_module( + Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=bias), + linear_module_args) + + +def create_conv1( + in_channels: int, out_channels: int, + bias: bool = False, + linear_module_args: Optional[LinearModuleArgs] = None) -> Module: + return wrap_linear_module( + Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=bias), + linear_module_args) + + +def create_conv7_block( + in_channels: int, + out_channels: int, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return Sequential( + create_conv7( + in_channels, + out_channels, + bias=False, + linear_module_args=block_args.linear_module_args), + block_args.normalization_layer_factory.create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) + + +def create_conv3_block( + in_channels: int, + out_channels: int, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return Sequential( + create_conv7( + in_channels, + out_channels, + bias=False, + linear_module_args=block_args.linear_module_args), + block_args.normalization_layer_factory.create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) + + +def create_downsample_block( + in_channels: int, + out_channels: int, + is_output_1x1: bool = False, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + if is_output_1x1: + return Sequential( + wrap_linear_module( + Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False), + block_args.linear_module_args), + block_args.nonlinearity_factory.create()) + else: + return Sequential( + wrap_linear_module( + Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False), + block_args.linear_module_args), + block_args.normalization_layer_factory.create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) + + +def create_upsample_block( + in_channels: int, + out_channels: int, + block_args: Optional[BlockArgs] = None) -> Module: + if block_args is None: + block_args = BlockArgs() + return Sequential( + wrap_linear_module( + ConvTranspose2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False), + linear_module_args=block_args.linear_module_args), + block_args.normalization_layer_factory.create(out_channels, affine=True), + block_args.nonlinearity_factory.create()) diff --git a/tha4/src/tha4/shion/nn00/initialization_funcs.py b/tha4/src/tha4/shion/nn00/initialization_funcs.py new file mode 100644 index 0000000..fbf73ae --- /dev/null +++ b/tha4/src/tha4/shion/nn00/initialization_funcs.py @@ -0,0 +1,58 @@ +from typing import Callable, Optional + +import torch +from torch import zero_ +from torch.nn import Module +from torch.nn.init import kaiming_normal_, xavier_normal_, normal_ + + +class HeInitialization: + def __init__(self, a: int = 0, mode: str = 'fan_in', nonlinearity: str = 'leaky_relu'): + self.nonlinearity = nonlinearity + self.mode = mode + self.a = a + + def __call__(self, module: Module) -> Module: + with torch.no_grad(): + kaiming_normal_(module.weight, a=self.a, mode=self.mode, nonlinearity=self.nonlinearity) + return module + + +class NormalInitialization: + def __init__(self, mean: float = 0.0, std: float = 1.0): + self.std = std + self.mean = mean + + def __call__(self, module: Module) -> Module: + with torch.no_grad(): + normal_(module.weight, self.mean, self.std) + return module + + +class XavierInitialization: + def __init__(self, gain: float = 1.0): + self.gain = gain + + def __call__(self, module: Module) -> Module: + with torch.no_grad(): + xavier_normal_(module.weight, self.gain) + return module + + +class ZeroInitialization: + def __call__(self, module: Module) -> Module: + with torch.no_grad: + zero_(module.weight) + return module + + +class NoInitialization: + def __call__(self, module: Module) -> Module: + return module + + +def resolve_initialization_func(initialization: Optional[Callable[[Module], Module]]): + if initialization is None: + return NoInitialization() + else: + return initialization diff --git a/tha4/src/tha4/shion/nn00/linear_module_args.py b/tha4/src/tha4/shion/nn00/linear_module_args.py new file mode 100644 index 0000000..b8bf494 --- /dev/null +++ b/tha4/src/tha4/shion/nn00/linear_module_args.py @@ -0,0 +1,30 @@ +from typing import Optional, Callable + +from torch.nn import Module +from torch.nn.utils import spectral_norm + +from tha4.shion.nn00.initialization_funcs import resolve_initialization_func + + +class LinearModuleArgs: + def __init__( + self, + initialization_func: Optional[Callable[[Module], Module]] = None, + use_spectral_norm: bool = False): + self.use_spectral_norm = use_spectral_norm + self.initialization_func = resolve_initialization_func(initialization_func) + + def wrap_linear_module(self, module: Module) -> Module: + module = self.initialization_func(module) + if self.use_spectral_norm: + module = spectral_norm(module) + return module + + +def wrap_linear_module(module: Module, linear_module_args: Optional[LinearModuleArgs] = None): + if linear_module_args is None: + linear_module_args = LinearModuleArgs() + module = linear_module_args.initialization_func(module) + if linear_module_args.use_spectral_norm: + module = spectral_norm(module) + return module diff --git a/tha4/src/tha4/shion/nn00/nonlinearity_factories.py b/tha4/src/tha4/shion/nn00/nonlinearity_factories.py new file mode 100644 index 0000000..f88dcc8 --- /dev/null +++ b/tha4/src/tha4/shion/nn00/nonlinearity_factories.py @@ -0,0 +1,87 @@ +from typing import Optional + +import torch +from torch import Tensor +from torch.nn import Module, ReLU, LeakyReLU, ELU, ReLU6, Hardswish, SiLU, Tanh, Sigmoid + +from tha4.shion.core.module_factory import ModuleFactory + + +class ReLUFactory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return ReLU(self.inplace) + + +class LeakyReLUFactory(ModuleFactory): + def __init__(self, inplace: bool = False, negative_slope: float = 1e-2): + self.negative_slope = negative_slope + self.inplace = inplace + + def create(self) -> Module: + return LeakyReLU(inplace=self.inplace, negative_slope=self.negative_slope) + + +class ELUFactory(ModuleFactory): + def __init__(self, inplace: bool = False, alpha: float = 1.0): + self.alpha = alpha + self.inplace = inplace + + def create(self) -> Module: + return ELU(inplace=self.inplace, alpha=self.alpha) + + +class ReLU6Factory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return ReLU6(inplace=self.inplace) + + +class SiLUFactory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return SiLU(inplace=self.inplace) + + +class HardswishFactory(ModuleFactory): + def __init__(self, inplace: bool = False): + self.inplace = inplace + + def create(self) -> Module: + return Hardswish(inplace=self.inplace) + + +class TanhFactory(ModuleFactory): + def create(self) -> Module: + return Tanh() + + +class SigmoidFactory(ModuleFactory): + def create(self) -> Module: + return Sigmoid() + + +class Swish(Module): + def __init__(self): + super().__init__() + + def forward(self, x: Tensor): + return x * torch.sigmoid(x) + + +class SwishFactory(ModuleFactory): + def create(self) -> Module: + return Swish() + + +def resolve_nonlinearity_factory(nonlinearity_factory: Optional[ModuleFactory]) -> ModuleFactory: + if nonlinearity_factory is None: + return ReLUFactory(inplace=False) + else: + return nonlinearity_factory \ No newline at end of file diff --git a/tha4/src/tha4/shion/nn00/normalization_layer_factories.py b/tha4/src/tha4/shion/nn00/normalization_layer_factories.py new file mode 100644 index 0000000..61ead7c --- /dev/null +++ b/tha4/src/tha4/shion/nn00/normalization_layer_factories.py @@ -0,0 +1,111 @@ +from typing import Optional + +import torch +from torch.nn import Module, Parameter, BatchNorm2d, InstanceNorm2d, GroupNorm +from torch.nn.functional import layer_norm +from torch.nn.init import normal_, constant_ + +from tha4.shion.nn00.normalization_layer_factory import NormalizationLayerFactory +from tha4.shion.nn00.pass_through import PassThrough + + +class Bias2d(Module): + def __init__(self, num_features: int): + super().__init__() + self.num_features = num_features + self.bias = Parameter(torch.zeros(1, num_features, 1, 1)) + + def forward(self, x): + return x + self.bias + + +class NoNorm2dFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + if affine: + return Bias2d(num_features) + else: + return PassThrough() + + +class BatchNorm2dFactory(NormalizationLayerFactory): + def __init__(self, + weight_mean: Optional[float] = None, + weight_std: Optional[float] = None, + bias: Optional[float] = None): + super().__init__() + self.bias = bias + self.weight_std = weight_std + self.weight_mean = weight_mean + + def get_weight_mean(self): + if self.weight_mean is None: + return 1.0 + else: + return self.weight_mean + + def get_weight_std(self): + if self.weight_std is None: + return 0.02 + else: + return self.weight_std + + def create(self, num_features: int, affine: bool = True) -> Module: + module = BatchNorm2d(num_features=num_features, affine=affine) + if affine: + if self.weight_mean is not None or self.weight_std is not None: + normal_(module.weight, self.get_weight_mean(), self.get_weight_std()) + if self.bias is not None: + constant_(module.bias, self.bias) + return module + + +class InstanceNorm2dFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + return InstanceNorm2d(num_features=num_features, affine=affine) + + +class LayerNorm2d(Module): + def __init__(self, channels: int, affine: bool = True): + super(LayerNorm2d, self).__init__() + self.channels = channels + self.affine = affine + + if self.affine: + self.weight = Parameter(torch.ones(1, channels, 1, 1)) + self.bias = Parameter(torch.zeros(1, channels, 1, 1)) + + def forward(self, x): + shape = x.size()[1:] + y = layer_norm(x, shape) * self.weight + self.bias + return y + + +class LayerNorm2dFactory(NormalizationLayerFactory): + def __init__(self): + super().__init__() + + def create(self, num_features: int, affine: bool = True) -> Module: + return LayerNorm2d(channels=num_features, affine=affine) + + +class GroupNormFactory(NormalizationLayerFactory): + def __init__(self, num_groups: int, eps=1e-6): + super().__init__() + self.eps = eps + self.num_groups = num_groups + + def create(self, num_features: int, affine: bool = True) -> Module: + return GroupNorm(num_channels=num_features, num_groups=self.num_groups, eps=self.eps, affine=affine) + + +def resolve_normalization_layer_factory(factory: Optional['NormalizationLayerFactory']) -> 'NormalizationLayerFactory': + if factory is None: + return InstanceNorm2dFactory() + else: + return factory diff --git a/tha4/src/tha4/shion/nn00/normalization_layer_factory.py b/tha4/src/tha4/shion/nn00/normalization_layer_factory.py new file mode 100644 index 0000000..5ff61cd --- /dev/null +++ b/tha4/src/tha4/shion/nn00/normalization_layer_factory.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + +from torch.nn import Module + + +class NormalizationLayerFactory(ABC): + def __init__(self): + super().__init__() + + @abstractmethod + def create(self, num_features: int, affine: bool = True) -> Module: + pass diff --git a/tha4/src/tha4/shion/nn00/pass_through.py b/tha4/src/tha4/shion/nn00/pass_through.py new file mode 100644 index 0000000..c64d678 --- /dev/null +++ b/tha4/src/tha4/shion/nn00/pass_through.py @@ -0,0 +1,9 @@ +from torch.nn import Module + + +class PassThrough(Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x \ No newline at end of file diff --git a/tha4/src/tha4/shion/nn00/resnet_block.py b/tha4/src/tha4/shion/nn00/resnet_block.py new file mode 100644 index 0000000..7989100 --- /dev/null +++ b/tha4/src/tha4/shion/nn00/resnet_block.py @@ -0,0 +1,55 @@ +from typing import Optional + +import torch +from torch.nn import Module, Sequential, Parameter + +from tha4.shion.nn00.block_args import BlockArgs +from tha4.shion.nn00.conv import create_conv1, create_conv3 + + +class ResnetBlock(Module): + def __init__(self, + num_channels: int, + is1x1: bool = False, + use_scale_parameter: bool = False, + block_args: Optional[BlockArgs] = None): + super().__init__() + if block_args is None: + block_args = BlockArgs() + self.use_scale_parameter = use_scale_parameter + if self.use_scale_parameter: + self.scale = Parameter(torch.zeros(1)) + if is1x1: + self.resnet_path = Sequential( + create_conv1( + num_channels, + num_channels, + bias=True, + linear_module_args=block_args.linear_module_args), + block_args.nonlinearity_factory.create(), + create_conv1( + num_channels, + num_channels, + bias=True, + linear_module_args=block_args.linear_module_args)) + else: + self.resnet_path = Sequential( + create_conv3( + num_channels, + num_channels, + bias=False, + linear_module_args=block_args.linear_module_args), + block_args.normalization_layer_factory.create(num_channels, affine=True), + block_args.nonlinearity_factory.create(), + create_conv3( + num_channels, + num_channels, + bias=False, + linear_module_args=block_args.linear_module_args), + block_args.normalization_layer_factory.create(num_channels, affine=True)) + + def forward(self, x): + if self.use_scale_parameter: + return x + self.scale * self.resnet_path(x) + else: + return x + self.resnet_path(x) From c6d24d6fb28125fb96e8db4589062d3c8774c5d2 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:36:03 +0800 Subject: [PATCH 11/46] edit submodule --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index b1c511b..f9ab4da 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit b1c511bdc617e4186d9fa6e5677d77d1dff3d571 +Subproject commit f9ab4da57678f8484b549b702d351c607f132e6a From 5c463ef174befd9ecefa40880c6f681e530bb31c Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:28:09 +0800 Subject: [PATCH 12/46] submodule update --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index f9ab4da..874a1d9 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit f9ab4da57678f8484b549b702d351c607f132e6a +Subproject commit 874a1d9685539c2b350764a68d7d2ae045028319 From 138766c664edc24ba71cd64792fe457914d77316 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:04:54 +0800 Subject: [PATCH 13/46] tha4half/full ui update --- convert_onnx_to_trt_fp16.py | 51 ------------------------- ezvtb_rt_interface.py | 9 ++--- launcher2.py | 34 +++-------------- main.py | 67 +++++++++++--------------------- onnx_fp32to16.py | 37 ++++++++++++++++++ tha4_convert_onnx_to_trt.py | 76 +++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 129 deletions(-) delete mode 100644 convert_onnx_to_trt_fp16.py create mode 100644 onnx_fp32to16.py create mode 100644 tha4_convert_onnx_to_trt.py diff --git a/convert_onnx_to_trt_fp16.py b/convert_onnx_to_trt_fp16.py deleted file mode 100644 index 3825543..0000000 --- a/convert_onnx_to_trt_fp16.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -将 onnx_model_tha4/fp32 目录中的 ONNX 模型转换为 fp16 精度的 TensorRT 模型 -并保存到 onnx_model_tha4/fp16 目录 -""" -import os -import sys -from pathlib import Path - -# 添加 ezvtuber-rt 到路径 -sys.path.insert(0, str(Path(__file__).parent / 'ezvtuber-rt')) - -from ezvtb_rt.trt_utils import build_engine, save_engine - -def convert_onnx_to_trt(): - # 输入和输出目录 - input_dir = Path('onnx_model_tha4/fp32') - output_dir = Path('onnx_model_tha4/fp16') - - # 创建输出目录 - output_dir.mkdir(parents=True, exist_ok=True) - - # 获取所有 ONNX 文件 - onnx_files = list(input_dir.glob('*.onnx')) - - if not onnx_files: - print(f"在 {input_dir} 中未找到 ONNX 模型文件") - return - - print(f"找到 {len(onnx_files)} 个 ONNX 模型文件") - print("开始转换为 fp16 精度的 TensorRT 模型...\n") - - for onnx_file in onnx_files: - print(f"正在转换: {onnx_file.name}") - - # 构建输出文件路径 - trt_filename = onnx_file.stem + '.trt' - trt_path = output_dir / trt_filename - - try: - # 构建并保存 TensorRT 引擎 - engine = build_engine(str(onnx_file), 'fp16') - save_engine(engine, str(trt_path)) - print(f"✓ 成功转换: {onnx_file.name} -> {trt_path}\n") - except Exception as e: - print(f"✗ 转换失败: {onnx_file.name}") - print(f" 错误信息: {str(e)}\n") - - print("转换完成!") - -if __name__ == '__main__': - convert_onnx_to_trt() diff --git a/ezvtb_rt_interface.py b/ezvtb_rt_interface.py index 4c0b537..8a87c49 100644 --- a/ezvtb_rt_interface.py +++ b/ezvtb_rt_interface.py @@ -61,11 +61,10 @@ def get_core( # Determine model directory based on THA version if use_tha4: - # THA4: Use onnx_model_tha4 directory - if use_tensorrt: - tha_model_dir = os.path.join('.', 'onnx_model_tha4', 'fp16') - else: - tha_model_dir = os.path.join('.', 'onnx_model_tha4', 'fp32') + # THA4: Use data/models/tha4 directory with precision selection + tha_model_dir = os.path.join( + '.', 'data', 'models', 'tha4', + 'fp16' if model_half else 'fp32') print(f'THA4 Mode - Model Path: {tha_model_dir}') else: # THA3: Use data/models/tha3 directory diff --git a/launcher2.py b/launcher2.py index 8e1fcab..7f1505a 100644 --- a/launcher2.py +++ b/launcher2.py @@ -80,9 +80,6 @@ def refreshList(): for item in sorted(os.listdir(dirPath), key=lambda x: -os.path.getmtime(os.path.join(dirPath, x))): if '.png' == item[-4:]: characterList.append(item[:-4]) - elif '.yaml' == item[-5:] or '.yml' == item[-4:]: - # Support THA4 YAML files - characterList.append(item[:-5] if item.endswith('.yaml') else item[:-4]) refreshList() @@ -230,32 +227,9 @@ class LauncherPanel(wx.Panel): choices=['10', '15', '20', '30', '60']) addOption('preset', title='Performance Preset', desc='性能预设,注意修改后会覆盖后续配置', choices=['Low', 'Medium', 'High', 'Ultra', 'Custom']) - model_select_option = addOption('model_select', title='Model Select', desc='选择使用的模型\nStandard Full精度较高性能较低', - choices=['Seperable Half', 'Seperable Full', 'Standard Half', 'Standard Full', 'THA4'], - mapping=['seperable_half', 'seperable_full', 'standard_half', 'standard_full', 'tha4']) - - # Add event handler for character selection to auto-lock THA4 when YAML is selected - def on_character_change(event): - character_name = self.optionDict['character'].GetValue() - # Check if selected character has a YAML file - yaml_exists = False - for ext in ['.yaml', '.yml']: - if os.path.exists(os.path.join(dirPath, character_name + ext)): - yaml_exists = True - break - - if yaml_exists: - # Auto-select THA4 model and disable the control - model_select_option.control.SetSelection(4) # THA4 is index 4 - model_select_option.control.Enable(False) - else: - # Re-enable model selection for non-YAML files - model_select_option.control.Enable(True) - - self.optionDict['character'].control.Bind(wx.EVT_CHOICE, on_character_change) - - # Trigger initial check - on_character_change(None) + addOption('model_select', title='Model Select', desc='选择使用的模型\nStandard Full精度较高性能较低', + choices=['Seperable Half', 'Seperable Full', 'Standard Half', 'Standard Full', 'THA4 Half', 'THA4 Full'], + mapping=['seperable_half', 'seperable_full', 'standard_half', 'standard_full', 'tha4_half', 'tha4_full']) addOption('ram_cache_size', title='RAM Cache Size', desc='分配内存缓存大小\n用于存储最终运算结果', choices=['Off', '1GB', '2GB', '4GB', '8GB', '16GB'], mapping=['0b', '1gb', '2gb', '4gb', '8gb', '16gb']) @@ -439,6 +413,8 @@ class LauncherPanel(wx.Panel): if args['model_select'] is not None: if 'tha4' in args['model_select']: run_args.append('--use_tha4') + if 'half' in args['model_select']: + run_args.append('--model_half') else: if 'seperable' in args['model_select']: run_args.append('--model_seperable') diff --git a/main.py b/main.py index 4bcfd0b..51b84ca 100644 --- a/main.py +++ b/main.py @@ -330,13 +330,12 @@ class MouseClientProcess(Process): class ModelClientProcess(Process): - def __init__(self, input_image, yaml_path=None): + def __init__(self, input_image): super().__init__() self.should_terminate = Value('b', False) self.updated = Value('b', False) self.data = None self.input_image = input_image - self.yaml_path = yaml_path self.output_queue = Queue() self.input_queue = Queue() self.shms = [shared_memory.SharedMemory(create=True, size=args.model_output_size * args.model_output_size * 4) @@ -532,47 +531,27 @@ class ModelClientProcess(Process): @torch.no_grad() def main(): - # Check if character is a YAML file (THA4 model) - yaml_path = None - is_tha4 = False - - # Try to find YAML file - for ext in ['.yaml', '.yml']: - yaml_candidate = f"data/images/{args.character}{ext}" - if os.path.exists(yaml_candidate): - yaml_path = yaml_candidate - is_tha4 = True - print(f"Detected THA4 YAML config: {yaml_path}") - # Override model selection to THA4 - args.model = 'tha4' - break - - # Load character image (only for non-THA4 models) - if not is_tha4: - img = Image.open(f"data/images/{args.character}.png") - img = img.convert('RGBA') - ow, oh = img.size - for i, px in enumerate(img.getdata()): - if px[3] <= 0: - y = i // ow - x = i % ow - img.putpixel((x, y), (0, 0, 0, 0)) - if ow != 512 or oh != 512: - img = resize_to_512_center(img) - if args.alpha_clean: - curves = { - 'a': [ - (60, 0), - (200, 255) - ] - } - img = apply_color_curves(img, curves) - input_image = np.array(img) - input_image = cv2.cvtColor(input_image, cv2.COLOR_RGBA2BGRA) - else: - # For THA4, create a dummy image (will be ignored) - input_image = np.zeros((512, 512, 4), dtype=np.uint8) - print("THA4 mode: Character image will be loaded from YAML") + # Load character image + img = Image.open(f"data/images/{args.character}.png") + img = img.convert('RGBA') + ow, oh = img.size + for i, px in enumerate(img.getdata()): + if px[3] <= 0: + y = i // ow + x = i % ow + img.putpixel((x, y), (0, 0, 0, 0)) + if ow != 512 or oh != 512: + img = resize_to_512_center(img) + if args.alpha_clean: + curves = { + 'a': [ + (60, 0), + (200, 255) + ] + } + img = apply_color_curves(img, curves) + input_image = np.array(img) + input_image = cv2.cvtColor(input_image, cv2.COLOR_RGBA2BGRA) print("Character Image Loaded:", args.character) cap = None @@ -676,7 +655,7 @@ def main(): } model_output = None - model_process = ModelClientProcess(input_image, yaml_path=yaml_path) + model_process = ModelClientProcess(input_image) model_process.daemon = True model_process.start() model_output_nps = [np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, diff --git a/onnx_fp32to16.py b/onnx_fp32to16.py new file mode 100644 index 0000000..eb910b7 --- /dev/null +++ b/onnx_fp32to16.py @@ -0,0 +1,37 @@ +import numpy as np +import onnxruntime as ort +import onnx +from onnxconverter_common import float16 +import os +from onnxsim import simplify + +names = ['body_morpher.onnx', 'combiner.onnx', 'decomposer.onnx', 'morpher.onnx', 'upscaler.onnx'] + +for name in names: + model = onnx.load(os.path.join('data', 'models','tha4', 'fp32', name)) + model_fp16 = float16.convert_float_to_float16(model, min_positive_val=1e-7, max_finite_val=1e4, keep_io_types=False, disable_shape_infer=False, op_block_list=None, node_block_list=None) + + for node in model_fp16.graph.node: + if node.op_type == "Cast": + for attr in node.attribute: + if attr.name == "to": + if attr.i == 1: # 1 means FLOAT (float32) + print(f"Changing {name} Cast node '{node.name}' ({node.output[0]}) from float32 -> float16") + attr.i = 10 # 10 means FLOAT16 + + # Simplify first + model_fp16, check = simplify(model_fp16) + assert check, "Simplified ONNX model could not be validated" + + # Fix input/output types to ensure all are FP16 (after simplify) + for input_tensor in model_fp16.graph.input: + if input_tensor.type.tensor_type.elem_type == 1: # FLOAT32 + print(f"Converting {name} input '{input_tensor.name}' from FP32 to FP16") + input_tensor.type.tensor_type.elem_type = 10 # FLOAT16 + + for output_tensor in model_fp16.graph.output: + if output_tensor.type.tensor_type.elem_type == 1: # FLOAT32 + print(f"Converting {name} output '{output_tensor.name}' from FP32 to FP16") + output_tensor.type.tensor_type.elem_type = 10 # FLOAT16 + + onnx.save(model_fp16, os.path.join('data', 'models', 'tha4', 'fp16', name)) \ No newline at end of file diff --git a/tha4_convert_onnx_to_trt.py b/tha4_convert_onnx_to_trt.py new file mode 100644 index 0000000..4f538a2 --- /dev/null +++ b/tha4_convert_onnx_to_trt.py @@ -0,0 +1,76 @@ +""" +将 ONNX 模型转换为 fp32 和 fp16 精度的 TensorRT 模型 +并保存到对应的 fp32 和 fp16 目录 +""" +import os +import sys +from pathlib import Path + +# 添加 ezvtuber-rt 到路径 +sys.path.insert(0, str(Path(__file__).parent / 'ezvtuber-rt')) + +from ezvtb_rt.trt_utils import build_engine, save_engine + +def convert_onnx_to_trt(): + # 基础目录 + base_dir = Path('data/models/tha4') + onnx_dir = base_dir / 'fp32' + + # 检查 ONNX 文件目录是否存在 + if not onnx_dir.exists(): + print(f"错误: ONNX 模型目录不存在: {onnx_dir}") + return + + # 指定要转换的文件名列表 + target_names = ['body_morpher.onnx', 'combiner.onnx', 'decomposer.onnx', + 'morpher.onnx', 'upscaler.onnx'] + + # 获取指定的 ONNX 文件 + onnx_files = [onnx_dir / name for name in target_names if (onnx_dir / name).exists()] + + if not onnx_files: + print(f"在 {onnx_dir} 中未找到指定的 ONNX 模型文件") + print(f"需要的文件: {target_names}") + return + + print(f"找到 {len(onnx_files)} 个 ONNX 模型文件") + print("开始转换为 fp32 和 fp16 精度的 TensorRT 模型...\n") + + # 定义要转换的精度类型 + precisions = ['fp32', 'fp16'] + + for precision in precisions: + print(f"\n{'='*60}") + print(f"开始转换 {precision.upper()} 精度模型") + print(f"{'='*60}\n") + + # 设置输出目录 + output_dir = base_dir / precision + output_dir.mkdir(parents=True, exist_ok=True) + + for onnx_file in onnx_files: + trt_filename = onnx_file.stem + '.trt' + trt_path = output_dir / trt_filename + + # 检查是否已存在转换好的 TRT 文件 + if trt_path.exists(): + print(f"⊙ 跳过 ({precision}): {onnx_file.name} (TRT文件已存在)") + continue + + print(f"正在转换 ({precision}): {onnx_file.name}") + + try: + # 构建并保存 TensorRT 引擎 + engine = build_engine(str(onnx_file), precision) + save_engine(engine, str(trt_path)) + print(f"✓ 成功转换 ({precision}): {onnx_file.name} -> {trt_filename}\n") + except Exception as e: + print(f"✗ 转换失败 ({precision}): {onnx_file.name}") + print(f" 错误信息: {str(e)}\n") + + print("\n" + "="*60) + print("全部转换完成!") + print("="*60) + +if __name__ == '__main__': + convert_onnx_to_trt() From 06e43f1fcde3e85829c4da8f4d55b65491543148 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:00:39 +0800 Subject: [PATCH 14/46] submodule update --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 874a1d9..9d369c7 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 874a1d9685539c2b350764a68d7d2ae045028319 +Subproject commit 9d369c774ce4ca407552621329a3be517c1068cf From a79d9a76c2058b7b283bab8d5340cfa6728dc4cb Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:36:14 +0800 Subject: [PATCH 15/46] update submodule --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 9d369c7..d23cfb3 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 9d369c774ce4ca407552621329a3be517c1068cf +Subproject commit d23cfb3a3649193e88042df1079ecb33a4cf805a From 6ab72ce20713ce2a221602b58d9b83500f623973 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:37:49 +0800 Subject: [PATCH 16/46] simplify ezvtb-rt calling --- ezvtb_rt_interface.py | 74 ++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/ezvtb_rt_interface.py b/ezvtb_rt_interface.py index 8a87c49..cf7dc0b 100644 --- a/ezvtb_rt_interface.py +++ b/ezvtb_rt_interface.py @@ -17,6 +17,11 @@ else: if project_path not in sys.path: sys.path.append(project_path) +# Initialize model data path to point to data/models directory +import ezvtb_rt +models_path = os.path.join(dir_path, 'data', 'models') +ezvtb_rt.init_model_path(models_path) + def get_core( #Device setting device_id:int = 0, @@ -109,47 +114,36 @@ def get_core( core = None if use_tensorrt: - if use_tha4: - from ezvtb_rt.core_tha4 import CoreTHA4TRT - core = CoreTHA4TRT( - tha_model_dir, - vram_cache_size=model_cache_size, - use_eyebrow=model_use_eyebrow, - rife_dir=rife_model_path if rife_model_path else None, - sr_dir=sr_model_path if sr_model_path else None, - cache_max_volume=cacher_ram_size, - cache_quality=cacher_quality) - else: - from ezvtb_rt.core import CoreTRT - core = CoreTRT( - tha_model_dir, - vram_cache_size=model_cache_size, - use_eyebrow=model_use_eyebrow, - rife_dir=rife_model_path if rife_model_path else None, - sr_dir=sr_model_path if sr_model_path else None, - cache_max_volume=cacher_ram_size, - cache_quality=cacher_quality) + from ezvtb_rt.core import CoreTRT + core = CoreTRT( + tha_model_version='v4' if use_tha4 else 'v3', + tha_model_seperable=model_seperable, + tha_model_fp16=model_half, + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_noise=sr_noise, + sr_model_fp16=sr_half, + vram_cache_size=model_cache_size, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow) else: - if use_tha4: - from ezvtb_rt.core_tha4_ort import CoreTHA4ORT - core = CoreTHA4ORT( - tha_model_dir, - rife_path=rife_model_path if rife_model_path else None, - sr_path=sr_model_path if sr_model_path else None, - device_id=device_id, - cache_max_volume=cacher_ram_size, - cache_quality=cacher_quality, - use_eyebrow=model_use_eyebrow) - else: - from ezvtb_rt.core_ort import CoreORT - core = CoreORT( - tha_model_dir, - rife_path=rife_model_path if rife_model_path else None, - sr_path=sr_model_path if sr_model_path else None, - device_id=device_id, - cache_max_volume=cacher_ram_size, - cache_quality=cacher_quality, - use_eyebrow=model_use_eyebrow) + from ezvtb_rt.core_ort import CoreORT + core = CoreORT( + tha_model_version='v4' if use_tha4 else 'v3', + tha_model_seperable=model_seperable, + tha_model_fp16=model_half, + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_noise=sr_noise, + sr_model_fp16=sr_half, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow) return core From 04eeedcbc081e338462d66401a1d555cd77124c6 Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:03:38 +0800 Subject: [PATCH 17/46] remove fp16 export script --- onnx_fp32to16.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 onnx_fp32to16.py diff --git a/onnx_fp32to16.py b/onnx_fp32to16.py deleted file mode 100644 index eb910b7..0000000 --- a/onnx_fp32to16.py +++ /dev/null @@ -1,37 +0,0 @@ -import numpy as np -import onnxruntime as ort -import onnx -from onnxconverter_common import float16 -import os -from onnxsim import simplify - -names = ['body_morpher.onnx', 'combiner.onnx', 'decomposer.onnx', 'morpher.onnx', 'upscaler.onnx'] - -for name in names: - model = onnx.load(os.path.join('data', 'models','tha4', 'fp32', name)) - model_fp16 = float16.convert_float_to_float16(model, min_positive_val=1e-7, max_finite_val=1e4, keep_io_types=False, disable_shape_infer=False, op_block_list=None, node_block_list=None) - - for node in model_fp16.graph.node: - if node.op_type == "Cast": - for attr in node.attribute: - if attr.name == "to": - if attr.i == 1: # 1 means FLOAT (float32) - print(f"Changing {name} Cast node '{node.name}' ({node.output[0]}) from float32 -> float16") - attr.i = 10 # 10 means FLOAT16 - - # Simplify first - model_fp16, check = simplify(model_fp16) - assert check, "Simplified ONNX model could not be validated" - - # Fix input/output types to ensure all are FP16 (after simplify) - for input_tensor in model_fp16.graph.input: - if input_tensor.type.tensor_type.elem_type == 1: # FLOAT32 - print(f"Converting {name} input '{input_tensor.name}' from FP32 to FP16") - input_tensor.type.tensor_type.elem_type = 10 # FLOAT16 - - for output_tensor in model_fp16.graph.output: - if output_tensor.type.tensor_type.elem_type == 1: # FLOAT32 - print(f"Converting {name} output '{output_tensor.name}' from FP32 to FP16") - output_tensor.type.tensor_type.elem_type = 10 # FLOAT16 - - onnx.save(model_fp16, os.path.join('data', 'models', 'tha4', 'fp16', name)) \ No newline at end of file From 0b6cb9c8d3c7113021de180ffbe2ae84c475580e Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:38:15 +0800 Subject: [PATCH 18/46] export script for student --- onnx_export_tha4_student.py | 455 ++++++++++++++++++++++++++++ tha4_student_convert_onnx_to_trt.py | 285 +++++++++++++++++ 2 files changed, 740 insertions(+) create mode 100644 onnx_export_tha4_student.py create mode 100644 tha4_student_convert_onnx_to_trt.py diff --git a/onnx_export_tha4_student.py b/onnx_export_tha4_student.py new file mode 100644 index 0000000..5d3366b --- /dev/null +++ b/onnx_export_tha4_student.py @@ -0,0 +1,455 @@ +import torch +from os.path import join +from pathlib import Path +import sys +import os +sys.path.append(os.getcwd()) +sys.path.append(join(os.getcwd(), 'tha4', 'src')) +import numpy as np +import PIL.Image +import cv2 +import onnx +from onnxsim import simplify +import onnxruntime as ort +from torch import Tensor +from torch.nn import Module +from typing import List +import matplotlib.pyplot as plt + +# ================================================================================= +# START: Monkey Patch for torch.nn.functional.affine_grid (DEVICE-AWARE) +# ================================================================================= +import torch.nn.functional + +def linspace_from_neg_one(num_steps, dtype=torch.float32, device='cpu'): + r = torch.linspace(-1, 1, num_steps, dtype=torch.float32, device=device) + if num_steps > 1: + r = r * (num_steps - 1) / num_steps + return r + +def create_grid(N, C, H, W, device='cpu'): + grid = torch.empty((N, H, W, 3), dtype=torch.float32, device=device) + grid.select(-1, 0).copy_(linspace_from_neg_one(W, device=device)) + grid.select(-1, 1).copy_(linspace_from_neg_one(H, device=device).unsqueeze_(-1)) + grid.select(-1, 2).fill_(1) + return grid + +def custom_affine_grid(theta, size, align_corners=False): + N, C, H, W = size + device = theta.device + grid = create_grid(N, C, H, W, device=device) + grid = grid.view(N, H * W, 3).bmm(theta.transpose(1, 2)) + grid = grid.view(N, H, W, 2) + return grid + +def apply_affine_grid_patch(): + torch.nn.functional.affine_grid = custom_affine_grid + print("✅ Applied monkey patch to torch.nn.functional.affine_grid (Device-aware version).") + +# ================================================================================= +# END: Monkey Patch +# ================================================================================= + +apply_affine_grid_patch() + +# Parse command line arguments +if len(sys.argv) < 2: + print("Usage: python onnx_export_tha4_student.py [--fp32 or --fp16] [--cpu]") + print("Example: python onnx_export_tha4_student.py kanori_tha4 --fp32") + print(" python onnx_export_tha4_student.py kanori_tha4 --fp32 --cpu") + raise ValueError('Missing model name') + +MODEL_NAME = sys.argv[1] +HALF = '--fp16' in sys.argv +FORCE_CPU = '--cpu' in sys.argv + +# Check CUDA availability +if FORCE_CPU: + DEVICE_NAME = 'cpu' + print("⚠️ Forced CPU mode enabled") +elif not torch.cuda.is_available(): + DEVICE_NAME = 'cpu' + print("⚠️ CUDA not available, automatically using CPU mode") +else: + DEVICE_NAME = 'cuda:0' + +IMAGE_INPUT = os.path.join('.', 'data', 'images', 'character.png') + +# Create output directories +TMP_DIR = join('.', 'onnx_model_tha4_student', MODEL_NAME, 'tmp') +Path(TMP_DIR).mkdir(parents=True, exist_ok=True) +MODEL_DIR = join('.', 'onnx_model_tha4_student', MODEL_NAME, 'fp32' if not HALF else 'fp16') +Path(MODEL_DIR).mkdir(parents=True, exist_ok=True) +TMP_FILE_WRITE = join(TMP_DIR, 'tmp.onnx') + +validation_device = torch.device(DEVICE_NAME) +export_device = torch.device('cpu') +validation_dtype = torch.float16 if (HALF and DEVICE_NAME != 'cpu') else torch.float32 +export_dtype = torch.float32 + +if DEVICE_NAME == 'cpu': + print(f"Using CPU device for both validation and ONNX export, dtype: {export_dtype}") +else: + print(f"Using GPU device for validation: {validation_device}, dtype: {validation_dtype}") + print(f"Using CPU device for ONNX export, dtype: {export_dtype}") + +# Color space conversion utilities +def torch_srgb_to_linear(x: torch.Tensor): + x = torch.clip(x, 0.0, 1.0) + return torch.where(torch.le(x, 0.04045), x / 12.92, ((x + 0.055) / 1.055) ** 2.4) + +def torch_linear_to_srgb(x): + x = torch.clip(x, 0.0, 1.0) + return torch.where(torch.le(x, 0.003130804953560372), x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) + +def numpy_linear_to_srgb(x): + x = np.clip(x, 0.0, 1.0) + return np.where(x <= 0.003130804953560372, x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) + +# Load THA4 Student Model (mode_14: 2-step architecture) +def load_tha4_student_poser(device: torch.device, use_half: bool, model_name: str): + from tha4.poser.modes.mode_14 import create_poser + + # Model file paths + model_base_path = join('.', 'data', 'models', 'custom_tha4_models', model_name) + m_files = { + 'face_morpher': join(model_base_path, 'face_morpher.pt'), + 'body_morpher': join(model_base_path, 'body_morpher.pt') + } + + print(f"Loading THA4 Student poser from {model_base_path}...") + print(f" - Face Morpher: {m_files['face_morpher']}") + print(f" - Body Morpher: {m_files['body_morpher']}") + + poser = create_poser(device=device, module_file_names=m_files) + + if use_half and device.type == 'cuda': + print("Converting poser modules to half precision (FP16)...") + poser.half() + + return poser + +# Load posers for validation and export +validation_poser = load_tha4_student_poser(validation_device, HALF and DEVICE_NAME != 'cpu', MODEL_NAME) +if DEVICE_NAME == 'cpu': + # If using CPU for validation, reuse the same poser for export + export_poser = validation_poser + print("Using same poser for validation and export (CPU mode)") +else: + # If using GPU for validation, load separate CPU poser for export + export_poser = load_tha4_student_poser(export_device, False, MODEL_NAME) + +pose_size = validation_poser.get_num_parameters() +print(f"Pose parameters: {pose_size}") + +# Image loading and preprocessing utilities +def resize_PIL_image(img, size=(512, 512)): + return img.resize(size, PIL.Image.Resampling.LANCZOS) + +def extract_PIL_image_from_filelike(path): + return PIL.Image.open(path) + +def extract_pytorch_image_from_PIL_image(img): + arr = np.array(img) + if arr.shape[2] != 4: + raise ValueError("Input image must be RGBA") + t = torch.from_numpy(arr).float() / 255.0 + t[:, :, :3] = torch_srgb_to_linear(t[:, :, :3]) + t = t.permute(2, 0, 1) * 2.0 - 1.0 + return t + +# Load test image and run reference inference +pil_image = resize_PIL_image(extract_PIL_image_from_filelike(IMAGE_INPUT)) +pt_img_validation = extract_pytorch_image_from_PIL_image(pil_image).to(validation_dtype).unsqueeze(0).to(validation_device) +zero_pose_validation = torch.zeros(1, pose_size, dtype=validation_dtype, device=validation_device) + +with torch.no_grad(): + poser_torch_res = validation_poser.pose(pt_img_validation, zero_pose_validation) +print(f"Reference output shape: {poser_torch_res.shape}") + +print("\n" + "="*80) +print("Starting ONNX export process...") +print("="*80) + +# Get export modules +export_modules = export_poser.get_modules() +cv_img_numpy = cv2.imread(IMAGE_INPUT, cv2.IMREAD_UNCHANGED) +cv_img_tensor_export = torch.from_numpy(cv_img_numpy) + +# ================================================================================= +# Export Face Morpher (Student Model Network 1) +# ================================================================================= +print("\n[1/2] Exporting Face Morpher...") + +class FaceMorpherWrapper(Module): + """ + Face Morpher for Student Model (mode_14) + - Input: 45-dimensional pose (first 39 used for face) + - Output: 128x128 face region + Note: SIREN-based face morpher doesn't need input image, only pose + """ + def __init__(self, face_morpher_module): + super().__init__() + self.face_morpher = face_morpher_module + + def forward(self, pose: Tensor) -> Tensor: + """ + Args: + pose: Full pose parameters [1, 45] + Returns: + face_output: Face morphed image [1, 4, 128, 128] in normalized format + """ + # Extract first 39 dimensions for face morpher + face_pose = pose[:, 0:39] + + # Run face morpher (SIREN generates image from pose only) + face_output = self.face_morpher.forward(face_pose) + + return face_output + +face_morpher_wrapper = FaceMorpherWrapper(export_modules['face_morpher']).eval() + +# Test inputs +face_pose_export = torch.zeros(1, 45, dtype=export_dtype) + +# Export +with torch.no_grad(): + face_morpher_res_export = face_morpher_wrapper(face_pose_export) + +torch.onnx.export( + face_morpher_wrapper, + face_pose_export, + TMP_FILE_WRITE, + opset_version=16, + input_names=['pose'], + output_names=['face_morphed'], + dynamic_axes={ + 'pose': {0: 'batch'} + } +) + +onnx_model = onnx.load(TMP_FILE_WRITE) +onnx.checker.check_model(onnx_model) +print(f" Original model inputs: {[inp.name for inp in onnx_model.graph.input]}") + +# Simplify with input/output names preservation +onnx_model_sim, check = simplify(onnx_model, skip_fuse_bn=True) +if check: + print(f" Simplified model inputs: {[inp.name for inp in onnx_model_sim.graph.input]}") + onnx.save(onnx_model_sim, join(MODEL_DIR, "face_morpher.onnx")) +else: + print(" Simplification failed, using original model") + onnx.save(onnx_model, join(MODEL_DIR, "face_morpher.onnx")) + +print("✓ Face Morpher exported") + +# ================================================================================= +# Export Body Morpher (Student Model Network 2) +# ================================================================================= +print("\n[2/2] Exporting Body Morpher...") + +class BodyMorpherWrapper(Module): + """ + Body Morpher for Student Model (mode_14) + - Input: Full 512x512 image with face morpher output embedded + - Output: Final morphed image + """ + def __init__(self, body_morpher_module): + super().__init__() + self.body_morpher = body_morpher_module + + def forward(self, img: Tensor, face_morphed: Tensor, pose: Tensor) -> List[Tensor]: + """ + Args: + img: Input image [H, W, 4] in uint8 BGR+A format + face_morphed: Face morphed output [1, 4, 128, 128] from face morpher + pose: Full pose parameters [1, 45] + Returns: + result: Final output in [0, 255] uint8 format (OpenCV compatible) + """ + # Convert image to normalized format + img_f = img.to(export_dtype) / 255.0 + img_f[:, :, :3] = torch_srgb_to_linear(img_f[:, :, :3]) + img_f = img_f[:, :, [2, 1, 0, 3]] # BGR to RGB + img_c = img_f.permute(2, 0, 1).unsqueeze(0) * 2.0 - 1.0 + + # Embed face morpher output into the full image + center_x = 256 + center_y = 128 + 16 # 144 + img_with_face = img_c.clone() + img_with_face[:, :, center_y-64:center_y+64, center_x-64:center_x+64] = face_morphed + + # Run body morpher + body_output = self.body_morpher.forward(img_with_face, pose) + + # Take the first output (blended image) + result = body_output[0] + + # Convert back to [0, 1] range + output = result / 2.0 + 0.5 + output[:, :3, :, :] = torch_linear_to_srgb(output[:, :3, :, :]) + + # Convert to OpenCV format (BGR, uint8) + cv_result = (output.squeeze(0).permute(1, 2, 0)[:, :, [2, 1, 0, 3]] * 255).clamp(0, 255).to(torch.uint8) + + return [output, cv_result] + +body_morpher_wrapper = BodyMorpherWrapper(export_modules['body_morpher']).eval() + +# Export +with torch.no_grad(): + body_morpher_res_export = body_morpher_wrapper(cv_img_tensor_export, face_morpher_res_export, face_pose_export) + +torch.onnx.export( + body_morpher_wrapper, + (cv_img_tensor_export, face_morpher_res_export, face_pose_export), + TMP_FILE_WRITE, + opset_version=16, + input_names=['input_image', 'face_morphed', 'pose'], + output_names=['result', 'cv_result'], + dynamic_axes={ + 'input_image': {0: 'height', 1: 'width'}, + 'face_morphed': {0: 'batch'}, + 'pose': {0: 'batch'} + } +) + +onnx_model = onnx.load(TMP_FILE_WRITE) +onnx.checker.check_model(onnx_model) +print(f" Original model inputs: {[inp.name for inp in onnx_model.graph.input]}") + +# Simplify with input/output names preservation +onnx_model_sim, check = simplify(onnx_model, skip_fuse_bn=True) +if check: + print(f" Simplified model inputs: {[inp.name for inp in onnx_model_sim.graph.input]}") + onnx.save(onnx_model_sim, join(MODEL_DIR, "body_morpher.onnx")) +else: + print(" Simplification failed, using original model") + onnx.save(onnx_model, join(MODEL_DIR, "body_morpher.onnx")) + +print("✓ Body Morpher exported") + +# ================================================================================= +# Validation: ONNX Inference +# ================================================================================= +print("\n" + "="*80) +print("Validation: Running ONNX inference...") +print("="*80) + +class THA4StudentONNXRunner: + def __init__(self, model_dir, use_gpu=True): + providers = [("CUDAExecutionProvider", {"device_id": 0})] if use_gpu else ['CPUExecutionProvider'] + + self.face_morpher_sess = self._create_session(join(model_dir, 'face_morpher.onnx'), providers) + self.body_morpher_sess = self._create_session(join(model_dir, 'body_morpher.onnx'), providers) + + # Print actual input names for debugging + face_inputs = [inp.name for inp in self.face_morpher_sess.get_inputs()] + body_inputs = [inp.name for inp in self.body_morpher_sess.get_inputs()] + print(f"✓ All ONNX models loaded") + print(f" Face Morpher inputs: {face_inputs}") + print(f" Body Morpher inputs: {body_inputs}") + + def _create_session(self, path, providers): + try: + return ort.InferenceSession(path, providers=providers) + except Exception as e: + print(f"Failed with {providers}, falling back to CPU for {os.path.basename(path)}") + return ort.InferenceSession(path, providers=['CPUExecutionProvider']) + + def run(self, img_path, pose): + """ + Run inference with student model + + Args: + img_path: Path to input image + pose: Pose parameters [1, 45] + """ + img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED) + + # Step 1: Face Morpher (SIREN only needs pose, generates face from scratch) + face_res = self.face_morpher_sess.run(None, { + 'pose': pose + }) + + # Step 2: Body Morpher (needs original image, face result, and pose) + body_res = self.body_morpher_sess.run(None, { + 'input_image': img, + 'face_morphed': face_res[0], + 'pose': pose + }) + + return body_res[0] + +def save_comparison_images(onnx_img_np, torch_img_np, prefix=""): + onnx_hwc = np.transpose(onnx_img_np.squeeze(0), (1, 2, 0))[:, :, :3] + torch_hwc = np.transpose(torch_img_np.squeeze(0), (1, 2, 0))[:, :, :3] + + onnx_hwc = np.clip(onnx_hwc, 0, 1) + torch_hwc = np.clip(torch_hwc, 0, 1) + + onnx_path = join(MODEL_DIR, f"{prefix}onnx_output.png") + torch_path = join(MODEL_DIR, f"{prefix}pytorch_output.png") + + plt.imsave(onnx_path, onnx_hwc) + plt.imsave(torch_path, torch_hwc) + + print(f"✅ Saved comparison images: {onnx_path} and {torch_path}") + +# Initialize ONNX runner +runner = THA4StudentONNXRunner(MODEL_DIR) +pose_zero = np.zeros((1, 45), dtype=np.float32) + +# Test with zero pose +print("\nRunning ONNX inference with zero pose...") +onnx_output = runner.run(IMAGE_INPUT, pose_zero) + +# Convert reference output to numpy +ref_np = poser_torch_res.cpu().numpy() / 2.0 + 0.5 +ref_np[0, :3, :, :] = numpy_linear_to_srgb(ref_np[0, :3, :, :]) + +# Save comparison images +save_comparison_images(onnx_output, ref_np, "zero_pose_") + +# Calculate MSE +mse = ((onnx_output - ref_np) ** 2).mean() +print(f"\n{'='*80}\nMSE (zero pose): {mse:.10f}\n{'='*80}") + +ACCEPTABLE_MSE_THRESHOLD = 1e-2 +if mse < ACCEPTABLE_MSE_THRESHOLD: + print(f"✅ Validation PASSED! MSE is within the acceptable range of {ACCEPTABLE_MSE_THRESHOLD}.") +else: + print(f"⚠️ Warning: MSE is higher than expected! But this can be normal due to precision differences.") + +# Test with random pose +print("\n" + "="*80) +print("Testing with random poses...") +print("="*80) + +pose_random = np.random.randn(1, 45).astype(np.float32) * 0.3 +onnx_output_r = runner.run(IMAGE_INPUT, pose_random) + +pose_r_validation = torch.from_numpy(pose_random).to(validation_dtype).to(validation_device) +with torch.no_grad(): + ref_output_r = validation_poser.pose(pt_img_validation, pose_r_validation) + +ref_np_r = ref_output_r.cpu().numpy() / 2.0 + 0.5 +ref_np_r[0, :3, :, :] = numpy_linear_to_srgb(ref_np_r[0, :3, :, :]) + +save_comparison_images(onnx_output_r, ref_np_r, "random_pose_") + +mse_r = ((onnx_output_r - ref_np_r) ** 2).mean() +print(f"MSE (random pose): {mse_r:.10f}") + +if mse_r < ACCEPTABLE_MSE_THRESHOLD: + print(f"✅ Random pose test PASSED! (Threshold: {ACCEPTABLE_MSE_THRESHOLD})") +else: + print(f"⚠️ Warning: High MSE with random pose! But this can be normal.") + +print(f"\n{'='*80}") +print("Export and validation completed!") +print(f"Models saved to: {MODEL_DIR}") +print("Generated files:") +print(f" - face_morpher.onnx") +print(f" - body_morpher.onnx") +print(f"{'='*80}") diff --git a/tha4_student_convert_onnx_to_trt.py b/tha4_student_convert_onnx_to_trt.py new file mode 100644 index 0000000..c46a21b --- /dev/null +++ b/tha4_student_convert_onnx_to_trt.py @@ -0,0 +1,285 @@ +""" +将学生模型 ONNX 转换为 fp32 和 fp16 精度的 TensorRT 模型 +学生模型(mode_14)只有 2 个网络:face_morpher 和 body_morpher +""" +import os +import sys +from pathlib import Path + +# 添加 ezvtuber-rt 到路径 +sys.path.insert(0, str(Path(__file__).parent / 'ezvtuber-rt')) + +from ezvtb_rt.trt_utils import save_engine +import tensorrt as trt + + +# TensorRT Logger +TRT_LOGGER = trt.Logger(trt.Logger.INFO) + + +def build_engine_with_dynamic_shape(onnx_file_path: str, precision: str): + """ + 构建支持动态输入形状的 TensorRT 引擎 + 专门用于 body_morpher,它有动态的图像输入 + """ + builder = trt.Builder(TRT_LOGGER) + network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) + config = builder.create_builder_config() + parser = trt.OnnxParser(network, TRT_LOGGER) + + # Parse model file + TRT_LOGGER.log(TRT_LOGGER.INFO, f'Loading ONNX file from path {onnx_file_path}...') + with open(onnx_file_path, 'rb') as model: + TRT_LOGGER.log(TRT_LOGGER.INFO, 'Beginning ONNX file parsing') + parse_res = parser.parse(model.read()) + if not parse_res: + for error in range(parser.num_errors): + TRT_LOGGER.log(TRT_LOGGER.ERROR, parser.get_error(error)) + raise ValueError('Failed to parse the ONNX file.') + + TRT_LOGGER.log(TRT_LOGGER.INFO, 'Completed parsing of ONNX file') + TRT_LOGGER.log(TRT_LOGGER.INFO, f'Input number: {network.num_inputs}') + TRT_LOGGER.log(TRT_LOGGER.INFO, f'Output number: {network.num_outputs}') + + # 检查是否有动态输入 + has_dynamic_shape = False + dynamic_inputs = [] + + for i in range(network.num_inputs): + input_tensor = network.get_input(i) + input_shape = input_tensor.shape + input_name = input_tensor.name + TRT_LOGGER.log(TRT_LOGGER.INFO, f'Input {i} ({input_name}): shape={input_shape}') + # 检查是否有 -1(动态维度) + if any(dim == -1 for dim in input_shape): + has_dynamic_shape = True + dynamic_inputs.append((input_name, input_shape)) + + # 设置优化配置文件(如果有动态输入) + if has_dynamic_shape: + TRT_LOGGER.log(TRT_LOGGER.INFO, 'Detected dynamic shapes, adding optimization profile...') + profile = builder.create_optimization_profile() + + for input_name, input_shape in dynamic_inputs: + input_shape_list = list(input_shape) + + # 为 input_image 设置动态形状配置 + if 'image' in input_name.lower(): + # 图像输入格式为 [H, W, C] + min_shape = [512, 512, 4] + opt_shape = [512, 512, 4] + max_shape = [512, 512, 4] + + TRT_LOGGER.log(TRT_LOGGER.INFO, + f'Setting shape for {input_name}: {min_shape}') + profile.set_shape(input_name, min_shape, opt_shape, max_shape) + + else: + # 对于 pose 和其他输入,固定为 batch_size=1 + # 这避免了条件层(If层)中的秩不匹配问题 + min_shape = [1 if dim == -1 else dim for dim in input_shape_list] + opt_shape = [1 if dim == -1 else dim for dim in input_shape_list] + max_shape = [1 if dim == -1 else dim for dim in input_shape_list] + + TRT_LOGGER.log(TRT_LOGGER.INFO, + f'Setting shape for {input_name}: shape={min_shape}') + profile.set_shape(input_name, min_shape, opt_shape, max_shape) + + config.add_optimization_profile(profile) + + # 设置内存限制 + def GiB(val): + return val * 1 << 30 + config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, GiB(2)) # 2G for complex models + + # 设置精度 + if precision == 'fp32': + config.set_flag(trt.BuilderFlag.TF32) + elif precision == 'fp16': + config.set_flag(trt.BuilderFlag.FP16) + else: + raise ValueError('precision must be one of fp32 or fp16') + + # Build engine + TRT_LOGGER.log(TRT_LOGGER.INFO, f'Building an engine from file {onnx_file_path}; this may take a while...') + serialized_engine = builder.build_serialized_network(network, config) + TRT_LOGGER.log(TRT_LOGGER.INFO, 'Completed creating Engine') + + return serialized_engine + + +def convert_student_onnx_to_trt(model_name: str, precision: str = 'fp32'): + """ + 转换学生模型的 ONNX 文件为 TensorRT 引擎 + + Args: + model_name: 模型名称(例如:kanori_tha4) + precision: 精度类型,'fp32' 或 'fp16' + """ + # 基础目录 + base_dir = Path('data/models/custom_tha4_models') / model_name + onnx_dir = base_dir + + # 检查 ONNX 文件目录是否存在 + if not onnx_dir.exists(): + print(f"错误: ONNX 模型目录不存在: {onnx_dir}") + print(f"请先运行: python onnx_export_tha4_student.py {model_name} --{precision}") + return False + + # 学生模型只需要这两个文件 + target_names = ['face_morpher.onnx', 'body_morpher.onnx'] + + # 获取 ONNX 文件 + onnx_files = [onnx_dir / name for name in target_names if (onnx_dir / name).exists()] + + if len(onnx_files) != 2: + print(f"错误: 在 {onnx_dir} 中未找到所有必需的 ONNX 模型文件") + print(f"需要的文件: {target_names}") + print(f"找到的文件: {[f.name for f in onnx_files]}") + return False + + print(f"\n{'='*70}") + print(f"学生模型 TensorRT 转换工具") + print(f"{'='*70}") + print(f"模型名称: {model_name}") + print(f"精度类型: {precision.upper()}") + print(f"找到 {len(onnx_files)} 个 ONNX 模型文件") + print(f"{'='*70}\n") + + # 设置输出目录(与 ONNX 文件同目录) + output_dir = onnx_dir + success_count = 0 + + for onnx_file in onnx_files: + trt_filename = onnx_file.stem + '.trt' + trt_path = output_dir / trt_filename + + # 检查是否已存在转换好的 TRT 文件 + if trt_path.exists(): + user_input = input(f"⊙ TRT文件已存在: {trt_filename}\n 是否覆盖? (y/N): ") + if user_input.lower() != 'y': + print(f" 跳过: {onnx_file.name}\n") + success_count += 1 + continue + else: + print(f" 将覆盖现有文件\n") + + print(f"{'─'*70}") + print(f"正在转换: {onnx_file.name}") + print(f"{'─'*70}") + + try: + # 构建 TensorRT 引擎 + print(f" → 加载 ONNX 文件...") + + # 使用支持动态输入的构建函数(两个模型都可能有动态输入) + print(f" → 构建 TensorRT 引擎(支持动态输入)...") + engine = build_engine_with_dynamic_shape(str(onnx_file), precision) + + if engine is None: + raise RuntimeError("引擎构建返回 None") + + print(f" → 保存 TensorRT 引擎...") + save_engine(engine, str(trt_path)) + + # 验证文件是否成功保存 + if trt_path.exists(): + file_size = trt_path.stat().st_size / (1024 * 1024) # MB + print(f" ✓ 成功转换: {onnx_file.name} -> {trt_filename}") + print(f" 文件大小: {file_size:.2f} MB") + success_count += 1 + else: + raise RuntimeError("TRT 文件保存失败") + + except Exception as e: + print(f" ✗ 转换失败: {onnx_file.name}") + print(f" 错误信息: {str(e)}") + + print() + + # 输出总结 + print(f"{'='*70}") + if success_count == len(onnx_files): + print(f"✓ 全部转换完成!({success_count}/{len(onnx_files)})") + print(f"\nTensorRT 模型已保存到:") + print(f" {output_dir}") + print(f"\n生成的文件:") + for name in target_names: + trt_name = name.replace('.onnx', '.trt') + trt_file = output_dir / trt_name + if trt_file.exists(): + file_size = trt_file.stat().st_size / (1024 * 1024) + print(f" - {trt_name} ({file_size:.2f} MB)") + return True + else: + print(f"⚠ 部分转换失败 ({success_count}/{len(onnx_files)})") + return False + + +def main(): + """主函数:处理命令行参数并执行转换""" + if len(sys.argv) < 2: + print("用法: python tha4_student_convert_onnx_to_trt.py <模型名称> [精度]") + print("\n参数:") + print(" 模型名称: 自定义模型名称(例如:kanori_tha4)") + print(" 精度: fp32 或 fp16(默认:fp32)") + print("\n示例:") + print(" python tha4_student_convert_onnx_to_trt.py kanori_tha4") + print(" python tha4_student_convert_onnx_to_trt.py kanori_tha4 fp32") + print(" python tha4_student_convert_onnx_to_trt.py kanori_tha4 fp16") + print("\n注意:") + print(" - 请确保已经使用 onnx_export_tha4_student.py 导出了 ONNX 模型") + print(" - TensorRT 转换需要 CUDA 支持") + print(" - 转换过程可能需要几分钟时间") + sys.exit(1) + + model_name = sys.argv[1] + precision = sys.argv[2].lower() if len(sys.argv) > 2 else 'fp32' + + # 验证精度参数 + if precision not in ['fp32', 'fp16']: + print(f"错误: 不支持的精度类型 '{precision}'") + print("支持的精度: fp32, fp16") + sys.exit(1) + + # 检查 TensorRT 是否可用 + try: + import tensorrt as trt + print(f"TensorRT 版本: {trt.__version__}") + except ImportError: + print("错误: 未安装 TensorRT") + print("请安装 TensorRT: pip install tensorrt") + sys.exit(1) + + # 检查 CUDA 是否可用 + try: + import pycuda.driver as cuda + cuda.init() + print(f"CUDA 设备数量: {cuda.Device.count()}") + if cuda.Device.count() > 0: + device = cuda.Device(0) + print(f"GPU: {device.name()}") + except Exception as e: + print(f"警告: CUDA 初始化失败 - {e}") + print("TensorRT 转换需要 CUDA 支持") + sys.exit(1) + + print() + + # 执行转换 + success = convert_student_onnx_to_trt(model_name, precision) + + if success: + print(f"\n{'='*70}") + print("转换成功!可以在启动器中选择使用 TensorRT 模式") + print(f"{'='*70}") + sys.exit(0) + else: + print(f"\n{'='*70}") + print("转换失败,请检查错误信息") + print(f"{'='*70}") + sys.exit(1) + + +if __name__ == '__main__': + main() From daaa8522a09e59ac93394ba1fea5dde09d71978f Mon Sep 17 00:00:00 2001 From: nick-haoran <33177696+nick-haoran@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:23:38 +0800 Subject: [PATCH 19/46] =?UTF-8?q?clear=20and=20add=20bat(TODOFIX:=20tha4?= =?UTF-8?q?=20student=20model=20vram=20cache=200.0%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03.构建tha4的ONNX和TensorRT学生模型.bat | 74 ++++++++++ PerformanceTest.md | 123 ---------------- THA4_SETUP.md | 65 -------- THA4_USAGE.md | 127 ---------------- args.py | 2 + ezvtb_rt_interface.py | 96 ++++++++---- ezvtuber-rt | 2 +- launcher2.py | 102 ++++++++++++- main.py | 2 + onnx_export_tha4_student.py | 21 +-- test_tha4_student.py | 187 ++++++++++++++++++++++++ 11 files changed, 440 insertions(+), 361 deletions(-) create mode 100644 03.构建tha4的ONNX和TensorRT学生模型.bat delete mode 100644 PerformanceTest.md delete mode 100644 THA4_SETUP.md delete mode 100644 THA4_USAGE.md create mode 100644 test_tha4_student.py diff --git a/03.构建tha4的ONNX和TensorRT学生模型.bat b/03.构建tha4的ONNX和TensorRT学生模型.bat new file mode 100644 index 0000000..f7261fa --- /dev/null +++ b/03.构建tha4的ONNX和TensorRT学生模型.bat @@ -0,0 +1,74 @@ +@echo off +setlocal enabledelayedexpansion +cd /D "%~dp0" + +SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% + +echo ================================================================================ +echo THA4 ѧÉúģÐÍ ONNX ºÍ TensorRT ת»»¹¤¾ß +echo ================================================================================ +echo. + +REM ¼ì²éģÐÍĿ¼ÊǷñ´æÔÚ +if not exist "data\models\custom_tha4_models" ( + echo [´íÎó] ÕҲ»µ½ģÐÍĿ¼: data\models\custom_tha4_models + pause + exit /b 1 +) + +echo ¿ÉÓõÄ THA4 ѧÉúģÐÍ: +echo -------------------------------------------------------------------------------- +set count=0 +for /d %%i in (data\models\custom_tha4_models\*) do ( + set /a count+=1 + echo [!count!] %%~nxi +) +echo -------------------------------------------------------------------------------- +echo. + +if %count%==0 ( + echo [´íÎó] δÕҵ½ÈκÎģÐÍÎļþ¼Ð + pause + exit /b 1 +) + +echo ÇëÊäÈëҪת»»µÄģÐÍÎļþ¼ÐÃû³Æ: +set /p MODEL_NAME=^> + +REM ¼ì²éÊäÈëÊǷñΪ¿Õ +if "%MODEL_NAME%"=="" ( + echo [´íÎó] ģÐÍÃû³Ʋ»ÄÜΪ¿Õ + pause + exit /b 1 +) + +REM ¼ì²éģÐÍÎļþ¼ÐÊǷñ´æÔÚ +if not exist "data\models\custom_tha4_models\%MODEL_NAME%" ( + echo [´íÎó] ģÐÍÎļþ¼в»´æÔÚ: data\models\custom_tha4_models\%MODEL_NAME% + pause + exit /b 1 +) + +echo. +echo ================================================================================ +echo ²½Öè 1/2: µ¼³ö ONNX ģÐÍ +echo ================================================================================ +echo ģÐÍ: %MODEL_NAME% +echo ÃüÁî: python onnx_export_tha4_student.py %MODEL_NAME% --cpu +echo -------------------------------------------------------------------------------- +echo. + +python onnx_export_tha4_student.py %MODEL_NAME% --cpu + +echo ================================================================================ +echo ²½Öè 2/2: ת»»Ϊ TensorRT ģÐÍ +echo ================================================================================ +echo ģÐÍ: %MODEL_NAME% +echo ÃüÁî: python tha4_student_convert_onnx_to_trt.py %MODEL_NAME% fp32 +echo -------------------------------------------------------------------------------- +echo. + +python tha4_student_convert_onnx_to_trt.py %MODEL_NAME% fp32 + +pause + diff --git a/PerformanceTest.md b/PerformanceTest.md deleted file mode 100644 index 1d048ea..0000000 --- a/PerformanceTest.md +++ /dev/null @@ -1,123 +0,0 @@ -# 测试环境 -### 基于 -* Release版本v0.3 -* 测试平台为windows10 22H2 -* CPU Intel 10400f -* GPU Nvidia 3060 12G, Nvidia 1660 Super 6G, ARC A770 16G,RX580 8G. - -### 测试定量 -* Jpeg compression 和 Input Simplify 设为 medium -* 使用debug input 和 debug output -* 不使用eyebrow参数 -* 帧数限制60fps -* 英伟达显卡使用TensorRT测试 - -观察开始后约5-10秒的数值记录, 即不考虑长期缓存效果(没时间测) - -# 测试结果 -请注意测试结果仅限参考显卡大概性能,具体性能取决于用户周边配置和显卡驱动。 - -GPU | model | VRAM Cache | RAM Cache | Interpolation | SR | Frame Rate | GPU Usage -:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: -A770 | sepe_half | N/A | No | No | No | 33fps | 97% -A770 | sepe_full | N/A | No | No | No | 48fps | 97% -A770 | stan_half | N/A | No | No | No | 27fps | 97% -A770 | stan_full | N/A | No | No | No | 29fps | 97% -A770 | sepe_half | N/A | 2GB | No | No | 30fps | 82% -A770 | sepe_full | N/A | 2GB | No | No | 43fps | 78% -A770 | stan_half | N/A | 2GB | No | No | 30fps | 82% -A770 | stan_full | N/A | 2GB | No | No | 23fps | 85% -A770 | sepe_full | N/A | No | No | x2_half | 17fps | 97% -A770 | sepe_full | N/A | No | No | x4_half | 29fps | 97% -A770 | sepe_full | N/A | No | No | x4_full | 25fps | 93% -A770 | sepe_full | N/A | No | x2_half | No | 60fps | 93% -A770 | sepe_full | N/A | No | x3_half | No | 60fps | 85% -A770 | sepe_full | N/A | No | x4_half | No | 60fps | 81% --- | -- | --| --|--|--|--|-- -RX580 | sepe_full | N/A | No | No | No | 12fps | 98% -RX580 | stan_full | N/A | No | No | No | 15fps | 98% -RX580 | sepe_full | N/A | 2GB | No | No | 11fps | 93% -RX580 | stan_full | N/A | 2GB | No | No | 13fps | 93% -RX580 | stan_full | N/A | No | No | x2_half | 10fps | 97% -RX580 | stan_full | N/A | No | No | x2_full | 10fps | 97% -RX580 | stan_full | N/A | No | No | x4_half | 11fps | 98% -RX580 | stan_full | N/A | No | No | x4_full | 11fps | 98% -RX580 | stan_full | N/A | No | x2_half | no | 22fps | 98% -RX580 | stan_full | N/A | No | x2_full | no | 22fps | 98% -RX580 | stan_full | N/A | No | x3_half | no | 28fps | 98% -RX580 | stan_full | N/A | No | x3_full | no | 28fps | 98% -RX580 | stan_full | N/A | No | x4_half | no | 33fps | 98% -RX580 | stan_full | N/A | No | x4_full | no | 33fps | 98% --- | -- | --| --|--|--|--|-- -1660s | sepe_half | No | No | No | No | 38fps | 95% -1660s | sepe_full | No | No | No | No | 28fps | 95% -1660s | stan_half | No | No | No | No | 21fps | 96% -1660s | stan_full | No | No | No | No | 12fps | 98% -1660s | sepe_half | 2GB | No | No | No | 39fps | 95% -1660s | sepe_full | 2GB | No | No | No | 28fps | 95% -1660s | stan_half | 2GB | No | No | No | 29fps | 96% -1660s | stan_full | 2GB | No | No | No | 12fps | 96% -1660s | sepe_half | 2GB | 2GB | No | No | 40fps | 75% -1660s | sepe_full | 2GB | 2GB | No | No | 24fps | 80% -1660s | stan_half | 2GB | 2GB | No | No | 26fps | 83% -1660s | stan_full | 2GB | 2GB | No | No | 12fps | 92% -1660s | sepe_half | No | No | No | x2_half | 23fps | 96% -1660s | sepe_half | No | No | No | x2_full | 18fps | 96% -1660s | sepe_half | No | No | No | x4_half | 23fps | 96% -1660s | sepe_half | No | No | No | x4_full | 26fps | 97% -1660s | sepe_half | No | No | x2_half | No | 63fps | 75% -1660s | sepe_half | No | No | x2_full | No | 63fps | 80% -1660s | sepe_half | No | No | x3_half | No | 63fps | 65% -1660s | sepe_half | No | No | x3_full | No | 64fps | 66% -1660s | sepe_half | No | No | x4_half | No | 64fps | 53% -1660s | sepe_half | No | No | x4_full | No | 64fps | 60% --- | -- | --| --|--|--|--|-- -3060 | sepe_half | No | No | No | No | 63fps | 62% -3060 | sepe_full | No | No | No | No | 59fps | 93% -3060 | stan_half | No | No | No | No | 63fps | 80% -3060 | stan_full | No | No | No | No | 31fps | 96% -3060 | sepe_half | 2GB | No | No | No | 63fps | 60% -3060 | sepe_full | 2GB | No | No | No | 60fps | 93% -3060 | stan_half | 2GB | No | No | No | 63fps | 76% -3060 | stan_full | 2GB | No | No | No | 33fps | 96% -3060 | sepe_half | 2GB | 2GB | No | No | 63fps | 58% -3060 | sepe_full | 2GB | 2GB | No | No | 46fps | 70% -3060 | stan_half | 2GB | 2GB | No | No | 43fps | 62% -3060 | stan_full | 2GB | 2GB | No | No | 27fps | 80% -3060 | sepe_half | No | No | No | x2_half | 53fps | 85% -3060 | sepe_half | No | No | No | x2_full | 30fps | 94% -3060 | sepe_half | No | No | No | x4_half | 49fps | 90% -3060 | sepe_half | No | No | No | x4_full | 45fps | 90% -3060 | sepe_half | No | No | x2_half | No | 64fps | 45% -3060 | sepe_half | No | No | x2_full | No | 63fps | 50% -3060 | sepe_half | No | No | x3_half | No | 64fps | 35% -3060 | sepe_half | No | No | x3_full | No | 64fps | 42% -3060 | sepe_half | No | No | x4_half | No | 64fps | 30% -3060 | sepe_half | No | No | x4_full | No | 64fps | 40% - -# 已知问题: -* A770: rife插帧全精度会出现奇怪结果, waifu2x 超分的全精度会输出全绿色 -* RX580 tha3 模型半精度会输出奇怪结果 -* 3060 有时出现奇怪运行时占用上升,猜测为插拔显卡导致的驱动问题。 - -# DirectML 思考 -DirectML 允许所有 Windows 显卡设备以及 AIPC 的 NPU 运行此项目,但由于不同设备的 Kernel 在不同精度下实现区别和驱动兼容问题, -导致 DirectML 的运行结果在某个模型的某些精度出现不可用的扭曲结果,且运行性能与对模型的常识理解不相符。 - -例如适用于本项目插帧的RIFE模型的调用封装经历过一轮迭代, -由原先的n次输入(N=1,C,H,W)独立图像调用RIFE模型生成n个独立结果, -改为输入(N=n,C,H,W)的叠加图像一次性调用RIFE模型得到叠加结果, -此改动下模型获得了稍小的显存占用且在TensorRT实现中获得一定性能提升(虽然改动本意并不在此)。 -接着在笔者环境中的因特尔 A770 显卡 DirectML 在RIFE模型迭代后全精度推理结果由原先的符合预期变为扭曲图像, -半精度迭代后模型不仅保持推理结果符合预期,且相较模型迭代前得到了一定性能提升。 - -再例如笔者测试某 AMD 核显运行时发现其运行THA3的半精度模型时会因为此设备无法运行其中的某个kernel而将kernel运算单独卸载到cpu进行, -但保留了其他大部分能在核显上运行的kernel,由此推理模型需要结果在两个设备间反复,带来了极大的性能倒退。 - -目前阶段,受限于Python OnnxRuntime api 的支持匮乏(相较C++),使用 python 运行 Directml 无法避免上述问题, -在代码层面无法在运行前做出是否适配的条件判断,也无法在出现kernel卸载到cpu性能剧降时raise错误将运行打断。 - -所以,DirectML运行本项目的任意模型出现奇怪结果,或不符合预期的性能倒退,都应该属于意料之中 -换个模型精度,观察调试输出是否有DirectML黄字警告 通常可以解决大部分问题。 -具体某个硬件在DirectML的运行异常修复,已远超本项目的目标范围,请自行验证失效kernel,向硬件厂商和微软求助并期待驱动更新带来解决 -(建议买张N卡吧别折腾自己了)。 diff --git a/THA4_SETUP.md b/THA4_SETUP.md deleted file mode 100644 index a569f99..0000000 --- a/THA4_SETUP.md +++ /dev/null @@ -1,65 +0,0 @@ -# THA4 Model Setup Guide - -## Required Files Structure - -Place your THA4 model files in `data/models/tha4/` with the following structure: - -``` -data/models/tha4/ -├── character_model.yaml # Configuration file (paths to other files) -├── character.png # Character source image (RGBA, 512x512) -├── face_morpher.pt # Face morphing model weights -└── body_morpher.pt # Body morphing model weights -``` - -## Example character_model.yaml - -Create a file named `character_model.yaml` with the following content: - -```yaml -character_image_file_name: character.png -face_morpher_file_name: face_morpher.pt -body_morpher_file_name: body_morpher.pt -``` - -**Note:** All paths in the YAML are relative to the directory containing the YAML file. - -## Alternative Structure - -You can also organize files in subdirectories: - -``` -data/models/tha4/ -├── my_character/ -│ ├── character_model.yaml -│ ├── image.png -│ ├── face_morpher.pt -│ └── body_morpher.pt -``` - -Then update your YAML accordingly: - -```yaml -character_image_file_name: image.png -face_morpher_file_name: face_morpher.pt -body_morpher_file_name: body_morpher.pt -``` - -## Usage in EasyVtuber - -1. Place your files as shown above -2. Launch the EasyVtuber launcher -3. Select "THA4" from the "Model Select" dropdown -4. The system will automatically: - - Load `character_model.yaml` - - Read the PNG character image - - Load both PT model files - - Use the PNG image as the source for animation - -## Differences from THA3 - -- **THA4**: Uses YAML to define character (image + models as a package) -- **THA3**: Uses separate image file selected by user + shared model files - -The THA4 approach bundles character and model together, making it easier to -switch between different characters with their trained models. diff --git a/THA4_USAGE.md b/THA4_USAGE.md deleted file mode 100644 index f3f42a2..0000000 --- a/THA4_USAGE.md +++ /dev/null @@ -1,127 +0,0 @@ -# THA4 模型使用指南 (新版本) - -## 文件结构 - -THA4 模型现在支持直接放在 `data/images/` 目录中,与 THA3 的 PNG 图片一起管理。 - -### 所需文件 - -对于名为 `mycharacter` 的角色,需要在 `data/images/` 中放置以下文件: - -``` -data/images/ -├── mycharacter.yaml # 配置文件 -├── mycharacter.png # 角色源图片 (512x512 RGBA) -├── mycharacter_face_morpher.pt # 面部变形模型权重 -└── mycharacter_body_morpher.pt # 身体变形模型权重 -``` - -### YAML 配置格式 - -YAML 文件 (`mycharacter.yaml`) 应包含相对路径指向其他文件: - -```yaml -character_image_file_name: mycharacter.png -face_morpher_file_name: mycharacter_face_morpher.pt -body_morpher_file_name: mycharacter_body_morpher.pt -``` - -**重要说明:** -- YAML 中的所有路径都是相对于 YAML 文件所在位置的相对路径 -- 由于所有文件都在 `data/images/` 中,只需使用文件名即可 -- 角色图片必须是 512x512 像素的 RGBA 格式 -- 模型文件 (`.pt`) 是从 THA4 训练得到的 PyTorch 权重 - -## 在 EasyVtuber 中使用 - -### 自动检测 - -1. 将所有 4 个文件放入 `data/images/` 目录 -2. 打开启动器 (`02A.启动器.bat`) -3. 在 Character 下拉菜单中选择 `.yaml` 文件(例如 `mycharacter`) -4. 程序将自动: - - 检测这是 THA4 模型 - - 切换到 THA4 推理模式 - - 从 YAML 配置加载角色图片和模型 - -### 模型选择锁定 - -当你选择 YAML 文件时: -- 模型**自动锁定到 THA4** -- 使用 YAML 中的角色图片(启动器中的 Character 选择仅用于选择 YAML) -- 手动的模型选择将被覆盖 - -## 示例设置 - -假设你为名为 "alice" 的角色训练了 THA4 模型: - -1. 将这些文件复制到 `data/images/`: - ``` - alice.yaml - alice.png - alice_face_morpher.pt - alice_body_morpher.pt - ``` - -2. 创建 `alice.yaml`,内容为: - ```yaml - character_image_file_name: alice.png - face_morpher_file_name: alice_face_morpher.pt - body_morpher_file_name: alice_body_morpher.pt - ``` - -3. 在启动器中,从 Character 下拉菜单选择 "alice" -4. 启动 - 程序将检测到 YAML 并自动使用 THA4! - -## 与 THA3 的区别 - -| 特性 | THA3 | THA4 | -|------|------|------| -| 角色选择 | data/images/ 中的任意 PNG | 选择 YAML 文件 | -| 模型文件 | data/models/tha3/ 中的共享模型 | 每个角色独立的 .pt 文件 | -| 图片加载 | 运行时选择 | 与训练模型绑定 | -| 模型类型 | Standard/Separate, Half/Full | 单一类型 (float32) | -| 可互换性 | 可使用任意图片 | 图片与训练模型绑定 | - -## 文件命名建议 - -为了保持清晰,建议使用一致的命名规则: - -``` -<角色名>.yaml # 配置文件 -<角色名>.png # 源图片 -<角色名>_face_morpher.pt # 面部模型 -<角色名>_body_morpher.pt # 身体模型 -``` - -## 故障排除 - -**错误:"THA4 character model YAML not found"** -- 检查 `.yaml` 文件是否存在于 `data/images/` 中 -- 验证文件名是否与你选择的一致 - -**错误:"No such file or directory" 找不到 PNG 或 PT 文件** -- 检查 YAML 中的路径是否正确 -- 确保所有 4 个文件都在同一个 `data/images/` 目录中 -- 验证文件名完全匹配(区分大小写) - -**黑屏 / 无输出** -- 验证角色 PNG 是 512x512 RGBA 格式 -- 检查 .pt 模型文件是否来自正确训练的 THA4 模型 -- 确保模型文件与用于训练的角色图片匹配 - -## 旧文件迁移 - -如果你之前将文件放在 `data/models/tha4/` 中: - -1. 将所有文件移动到 `data/images/`: - ``` - mv data/models/tha4/character_model.yaml data/images/mycharacter.yaml - mv data/models/tha4/character.png data/images/mycharacter.png - mv data/models/tha4/face_morpher.pt data/images/mycharacter_face_morpher.pt - mv data/models/tha4/body_morpher.pt data/images/mycharacter_body_morpher.pt - ``` - -2. 更新 YAML 文件中的路径为新的文件名 - -3. 在启动器中选择新的 YAML 文件名 diff --git a/args.py b/args.py index 28032b1..ab6ae1e 100644 --- a/args.py +++ b/args.py @@ -38,6 +38,8 @@ parser.add_argument('--simplify', type=int, default=1) parser.add_argument('--device_id', type=int, default=0) parser.add_argument('--use_tensorrt', action='store_true') parser.add_argument('--use_tha4', action='store_true') +parser.add_argument('--use_tha4_student', action='store_true') +parser.add_argument('--tha4_student_model', type=str, default=None) parser.add_argument('--model_seperable', action='store_true') parser.add_argument('--model_half', action='store_true') diff --git a/ezvtb_rt_interface.py b/ezvtb_rt_interface.py index cf7dc0b..25d2aa8 100644 --- a/ezvtb_rt_interface.py +++ b/ezvtb_rt_interface.py @@ -44,7 +44,9 @@ def get_core( sr_half:bool = True, sr_noise:int = 1, #THA4 model setting (NEW) - use_tha4:bool = False # Use THA4 instead of THA3 + use_tha4: bool = False, # Use THA4 instead of THA3 + use_tha4_student: bool = False, # Use THA4 Student model + tha4_student_model: str = None # Student model name ): support_trt = False if use_tensorrt: @@ -65,7 +67,14 @@ def get_core( print(f'Using device {pycuda.autoinit.device.name()}') # Determine model directory based on THA version - if use_tha4: + if use_tha4_student and tha4_student_model: + # THA4 Student: Use data/models/custom_tha4_models/{model_name} + tha_model_dir = os.path.join( + '.', 'data', 'models', 'custom_tha4_models', + tha4_student_model) + print(f'THA4 Student Mode - Model: {tha4_student_model}') + print(f'THA4 Student Mode - Model Path: {tha_model_dir}') + elif use_tha4: # THA4: Use data/models/tha4 directory with precision selection tha_model_dir = os.path.join( '.', 'data', 'models', 'tha4', @@ -115,35 +124,64 @@ def get_core( core = None if use_tensorrt: from ezvtb_rt.core import CoreTRT - core = CoreTRT( - tha_model_version='v4' if use_tha4 else 'v3', - tha_model_seperable=model_seperable, - tha_model_fp16=model_half, - rife_model_enable=use_interpolation, - rife_model_scale=interpolation_scale, - rife_model_fp16=interpolation_half, - sr_model_enable=use_sr, - sr_model_scale=4 if sr_x4 else 2, - sr_model_noise=sr_noise, - sr_model_fp16=sr_half, - vram_cache_size=model_cache_size, - cache_max_giga=cacher_ram_size, - use_eyebrow=model_use_eyebrow) + if use_tha4_student and tha4_student_model: + core = CoreTRT( + tha_model_version='v4_student', + tha_model_name=tha4_student_model, + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_noise=sr_noise, + sr_model_fp16=sr_half, + vram_cache_size=model_cache_size, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow) + else: + core = CoreTRT( + tha_model_version='v4' if use_tha4 else 'v3', + tha_model_seperable=model_seperable, + tha_model_fp16=model_half, + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_noise=sr_noise, + sr_model_fp16=sr_half, + vram_cache_size=model_cache_size, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow) else: from ezvtb_rt.core_ort import CoreORT - core = CoreORT( - tha_model_version='v4' if use_tha4 else 'v3', - tha_model_seperable=model_seperable, - tha_model_fp16=model_half, - rife_model_enable=use_interpolation, - rife_model_scale=interpolation_scale, - rife_model_fp16=interpolation_half, - sr_model_enable=use_sr, - sr_model_scale=4 if sr_x4 else 2, - sr_model_noise=sr_noise, - sr_model_fp16=sr_half, - cache_max_giga=cacher_ram_size, - use_eyebrow=model_use_eyebrow) + if use_tha4_student and tha4_student_model: + core = CoreORT( + tha_model_version='v4_student', + tha_model_name=tha4_student_model, + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_noise=sr_noise, + sr_model_fp16=sr_half, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow) + else: + core = CoreORT( + tha_model_version='v4' if use_tha4 else 'v3', + tha_model_seperable=model_seperable, + tha_model_fp16=model_half, + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_noise=sr_noise, + sr_model_fp16=sr_half, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow) return core diff --git a/ezvtuber-rt b/ezvtuber-rt index d23cfb3..b59e1ba 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit d23cfb3a3649193e88042df1079ecb33a4cf805a +Subproject commit b59e1bab05b5c007a09690f83bf656d9c77a420d diff --git a/launcher2.py b/launcher2.py index 7f1505a..5715feb 100644 --- a/launcher2.py +++ b/launcher2.py @@ -64,6 +64,8 @@ finally: p = None dirPath = 'data/images' characterList = [] +studentModelList = [] +studentModelCharacterMap = {} hasRTModel = False try: @@ -82,7 +84,39 @@ def refreshList(): characterList.append(item[:-4]) +def scanStudentModels(): + """Scan custom_tha4_models directory for student models""" + global studentModelList, studentModelCharacterMap + studentModelList = [] + studentModelCharacterMap = {} + + custom_models_path = 'data/models/custom_tha4_models' + if os.path.exists(custom_models_path): + try: + for model_name in os.listdir(custom_models_path): + model_path = os.path.join(custom_models_path, model_name) + if os.path.isdir(model_path): + # Check if it's a valid student model + face_trt = os.path.join(model_path, 'face_morpher.trt') + body_trt = os.path.join(model_path, 'body_morpher.trt') + character_png = os.path.join(model_path, + 'character.png') + has_trt = (os.path.exists(face_trt) and + os.path.exists(body_trt)) + has_character = os.path.exists(character_png) + + if has_trt and has_character: + studentModelList.append(model_name) + studentModelCharacterMap[model_name] = model_name + except Exception: + pass + + # Sort alphabetically + studentModelList.sort() + + refreshList() +scanStudentModels() class OptionPanel(wx.Panel): @@ -227,9 +261,23 @@ class LauncherPanel(wx.Panel): choices=['10', '15', '20', '30', '60']) addOption('preset', title='Performance Preset', desc='性能预设,注意修改后会覆盖后续配置', choices=['Low', 'Medium', 'High', 'Ultra', 'Custom']) - addOption('model_select', title='Model Select', desc='选择使用的模型\nStandard Full精度较高性能较低', - choices=['Seperable Half', 'Seperable Full', 'Standard Half', 'Standard Full', 'THA4 Half', 'THA4 Full'], - mapping=['seperable_half', 'seperable_full', 'standard_half', 'standard_full', 'tha4_half', 'tha4_full']) + + # Build model_select choices + model_choices = ['Seperable Half', 'Seperable Full', 'Standard Half', + 'Standard Full', 'THA4 Half', 'THA4 Full'] + model_mapping = ['seperable_half', 'seperable_full', + 'standard_half', 'standard_full', 'tha4_half', + 'tha4_full'] + + # Add student models if available + for student_model in studentModelList: + model_choices.append(f'THA4 Student ({student_model})') + model_mapping.append(f'tha4_student_{student_model}') + + addOption('model_select', title='Model Select', + desc='选择使用的模型\nStandard Full精度较高性能较低', + choices=model_choices, + mapping=model_mapping) addOption('ram_cache_size', title='RAM Cache Size', desc='分配内存缓存大小\n用于存储最终运算结果', choices=['Off', '1GB', '2GB', '4GB', '8GB', '16GB'], mapping=['0b', '1gb', '2gb', '4gb', '8gb', '16gb']) @@ -307,17 +355,50 @@ class LauncherPanel(wx.Panel): self.optionDict['preset'].Bind(wx.EVT_CHOICE, presetChoice) presetChoice() + def onModelSelect(e=None): + """Handle model selection change""" + model_value = self.optionDict['model_select'].GetValue() + is_student_model = 'tha4_student_' in model_value + + char_ctrl = self.optionDict['character'] + + if is_student_model: + # Disable character selection for student models + # Student models have their own built-in character + char_ctrl.control.Enable(False) + char_ctrl.control.SetToolTip( + 'Locked: Student model includes built-in character') + else: + # Re-enable character selection for non-student models + char_ctrl.control.Enable(True) + char_ctrl.control.SetToolTip( + 'Select a character from data/images') + + self.optionDict['model_select'].Bind( + wx.EVT_CHOICE, onModelSelect) + + # Check initial model selection and lock character if needed + onModelSelect() + def onActivate(e): global characterList tName = self.optionDict['character'].GetValue() refreshList() - self.optionDict['character'].control.SetItems(characterList) - self.optionDict['character'].control.SetSelection(characterList.index(tName)) + self.optionDict['character'].control.SetItems( + characterList) + try: + idx = characterList.index(tName) + self.optionDict['character'].control.SetSelection(idx) + except ValueError: + # Character not found, select first available + if characterList: + self.optionDict['character'].control.SetSelection(0) if not hasRTModel: self.optionDict['use_tensorrt'].control.SetValue(False) self.optionDict['use_tensorrt'].control.Enable(False) - self.optionDict['use_tensorrt'].control.SetToolTip('需要先构建TensorRT模型') + self.optionDict['use_tensorrt'].control.SetToolTip( + '需要先构建TensorRT模型') self.frame.Bind(wx.EVT_ACTIVATE, onActivate) @@ -411,7 +492,14 @@ class LauncherPanel(wx.Panel): run_args.append('4') if args['model_select'] is not None: - if 'tha4' in args['model_select']: + if 'tha4_student_' in args['model_select']: + # Student model: tha4_student_{model_name} + model_name = args['model_select'].replace( + 'tha4_student_', '') + run_args.append('--use_tha4_student') + run_args.append('--tha4_student_model') + run_args.append(model_name) + elif 'tha4' in args['model_select']: run_args.append('--use_tha4') if 'half' in args['model_select']: run_args.append('--model_half') diff --git a/main.py b/main.py index 51b84ca..f4d0f9b 100644 --- a/main.py +++ b/main.py @@ -350,6 +350,8 @@ class ModelClientProcess(Process): self.model = get_core(device_id=args.device_id, use_tensorrt=args.use_tensorrt, use_tha4=args.use_tha4, + use_tha4_student=args.use_tha4_student, + tha4_student_model=args.tha4_student_model, model_seperable = args.model_seperable, model_half=args.model_half, diff --git a/onnx_export_tha4_student.py b/onnx_export_tha4_student.py index 5d3366b..1fe4436 100644 --- a/onnx_export_tha4_student.py +++ b/onnx_export_tha4_student.py @@ -73,13 +73,17 @@ elif not torch.cuda.is_available(): else: DEVICE_NAME = 'cuda:0' -IMAGE_INPUT = os.path.join('.', 'data', 'images', 'character.png') +# Use image from model folder +MODEL_BASE_PATH = join('.', 'data', 'models', 'custom_tha4_models', MODEL_NAME) +IMAGE_INPUT = os.path.join(MODEL_BASE_PATH, 'character.png') -# Create output directories -TMP_DIR = join('.', 'onnx_model_tha4_student', MODEL_NAME, 'tmp') +if not os.path.exists(IMAGE_INPUT): + raise FileNotFoundError(f"Image not found: {IMAGE_INPUT}") + +# Create output directories (save models in the model folder) +TMP_DIR = join(MODEL_BASE_PATH, 'tmp') Path(TMP_DIR).mkdir(parents=True, exist_ok=True) -MODEL_DIR = join('.', 'onnx_model_tha4_student', MODEL_NAME, 'fp32' if not HALF else 'fp16') -Path(MODEL_DIR).mkdir(parents=True, exist_ok=True) +MODEL_DIR = MODEL_BASE_PATH # Save directly in model folder TMP_FILE_WRITE = join(TMP_DIR, 'tmp.onnx') validation_device = torch.device(DEVICE_NAME) @@ -107,11 +111,10 @@ def numpy_linear_to_srgb(x): return np.where(x <= 0.003130804953560372, x * 12.92, 1.055 * (x ** (1.0 / 2.4)) - 0.055) # Load THA4 Student Model (mode_14: 2-step architecture) -def load_tha4_student_poser(device: torch.device, use_half: bool, model_name: str): +def load_tha4_student_poser(device: torch.device, use_half: bool, model_base_path: str): from tha4.poser.modes.mode_14 import create_poser # Model file paths - model_base_path = join('.', 'data', 'models', 'custom_tha4_models', model_name) m_files = { 'face_morpher': join(model_base_path, 'face_morpher.pt'), 'body_morpher': join(model_base_path, 'body_morpher.pt') @@ -130,14 +133,14 @@ def load_tha4_student_poser(device: torch.device, use_half: bool, model_name: st return poser # Load posers for validation and export -validation_poser = load_tha4_student_poser(validation_device, HALF and DEVICE_NAME != 'cpu', MODEL_NAME) +validation_poser = load_tha4_student_poser(validation_device, HALF and DEVICE_NAME != 'cpu', MODEL_BASE_PATH) if DEVICE_NAME == 'cpu': # If using CPU for validation, reuse the same poser for export export_poser = validation_poser print("Using same poser for validation and export (CPU mode)") else: # If using GPU for validation, load separate CPU poser for export - export_poser = load_tha4_student_poser(export_device, False, MODEL_NAME) + export_poser = load_tha4_student_poser(export_device, False, MODEL_BASE_PATH) pose_size = validation_poser.get_num_parameters() print(f"Pose parameters: {pose_size}") diff --git a/test_tha4_student.py b/test_tha4_student.py new file mode 100644 index 0000000..f353120 --- /dev/null +++ b/test_tha4_student.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Test script for THA4 Student model inference +Tests both TensorRT and ONNX Runtime backends +""" +import sys +import os +import numpy as np + +# Add ezvtuber-rt to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'ezvtuber-rt')) + + +def test_tensorrt_backend(): + """Test TensorRT backend""" + print("\n=== Testing TensorRT Backend ===") + try: + from ezvtb_rt import CoreTRT + + # Initialize with student model (kanori_tha4) + core = CoreTRT( + tha_model_version='v4_student', + tha_model_name='kanori_tha4' + ) + + # Create dummy image (512x512 RGBA) + test_image = np.random.randint(0, 256, (512, 512, 4), dtype=np.uint8) + + # Create dummy pose (1x45) + test_pose = np.random.randn(1, 45).astype(np.float32) + + # Run inference + core.setImage(test_image) + result = core.inference(test_pose) + + print("PASS: TensorRT Backend: SUCCESS") + print(f" Input image shape: {test_image.shape}") + print(f" Input pose shape: {test_pose.shape}") + out_shape = (result[0].shape if isinstance(result, list) + else result.shape) + print(f" Output shape: {out_shape}") + return True + + except Exception as e: + print("FAIL: TensorRT Backend: FAILED") + print(f" Error: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def test_onnxruntime_backend(): + """Test ONNX Runtime backend""" + print("\n=== Testing ONNX Runtime Backend ===") + try: + from ezvtb_rt import CoreORT + + # Initialize with student model (kanori_tha4) + core = CoreORT( + tha_model_version='v4_student', + tha_model_name='kanori_tha4' + ) + + # Create dummy image (512x512 RGBA) + test_image = np.random.randint(0, 256, (512, 512, 4), dtype=np.uint8) + + # Create dummy pose (1x45) + test_pose = np.random.randn(1, 45).astype(np.float32) + + # Run inference + core.setImage(test_image) + result = core.inference(test_pose) + + print("PASS: ONNX Runtime Backend: SUCCESS") + print(f" Input image shape: {test_image.shape}") + print(f" Input pose shape: {test_pose.shape}") + out_shape = (result[0].shape if isinstance(result, list) + else result.shape) + print(f" Output shape: {out_shape}") + return True + + except Exception as e: + print("FAIL: ONNX Runtime Backend: FAILED") + print(f" Error: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def test_direct_models(): + """Test direct model classes""" + print("\n=== Testing Direct Model Classes ===") + + success = True + + # Test THA4Student (TensorRT) + try: + from ezvtb_rt.tha4_student import THA4Student + model_dir = os.path.join( + os.path.dirname(__file__), + 'data', 'models', 'custom_tha4_models', 'kanori_tha4' + ) + + if os.path.exists(model_dir): + model = THA4Student(model_dir) + test_image = np.random.randint( + 0, 256, (512, 512, 4), dtype=np.uint8 + ) + test_pose = np.random.randn(1, 45).astype(np.float32) + + model.setImage(test_image) + result = model.inference(test_pose) + + print("PASS: THA4Student (TensorRT): SUCCESS") + out_shape = result[0].shape if isinstance(result, list) else result.shape + print(f" Output shape: {out_shape}") + else: + print(f"SKIP: THA4Student: Model not found at {model_dir}") + + except Exception as e: + print("FAIL: THA4Student (TensorRT): FAILED") + print(f" Error: {str(e)}") + success = False + + # Test THA4StudentORT (ONNX Runtime) + try: + from ezvtb_rt.tha4_student_ort import THA4StudentORT + model_dir = os.path.join( + os.path.dirname(__file__), + 'data', 'models', 'custom_tha4_models', 'kanori_tha4' + ) + + if os.path.exists(model_dir): + model = THA4StudentORT(model_dir, device_id=0) + test_image = np.random.randint( + 0, 256, (512, 512, 4), dtype=np.uint8 + ) + test_pose = np.random.randn(1, 45).astype(np.float32) + + model.setImage(test_image) + result = model.inference(test_pose) + + print("PASS: THA4StudentORT (ONNX Runtime): SUCCESS") + out_shape = result[0].shape if isinstance(result, list) else result.shape + print(f" Output shape: {out_shape}") + else: + print(f"SKIP: THA4StudentORT: Model not found at {model_dir}") + + except Exception as e: + print("FAIL: THA4StudentORT (ONNX Runtime): FAILED") + print(f" Error: {str(e)}") + success = False + + return success + + +def main(): + print("THA4 Student Model Integration Tests") + print("=" * 50) + + results = [] + + # Test direct models first + print("\n--- Direct Model Classes ---") + results.append(("Direct Models", test_direct_models())) + + # Test backends + print("\n--- Framework Backends ---") + results.append(("TensorRT Backend", test_tensorrt_backend())) + results.append(("ONNX Runtime Backend", test_onnxruntime_backend())) + + # Summary + print("\n" + "=" * 50) + print("TEST SUMMARY") + print("=" * 50) + for name, passed in results: + status = "PASS" if passed else "FAIL" + print(f" {name:<30} [{status}]") + + all_passed = all(r[1] for r in results) + overall = "ALL TESTS PASSED" if all_passed else "SOME TESTS FAILED" + print("\nOverall: " + overall) + + return 0 if all_passed else 1 + + +if __name__ == '__main__': + sys.exit(main()) From 4b35d92bc8da1cb3fcf4d20d11abf2728c77ed4a Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Thu, 18 Dec 2025 01:12:07 -0500 Subject: [PATCH 20/46] feat:update tha4 student onnx export --- onnx_export_tha4_student.py | 47 +++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/onnx_export_tha4_student.py b/onnx_export_tha4_student.py index 1fe4436..4109af1 100644 --- a/onnx_export_tha4_student.py +++ b/onnx_export_tha4_student.py @@ -15,6 +15,7 @@ from torch import Tensor from torch.nn import Module from typing import List import matplotlib.pyplot as plt +from onnxconverter_common import float16 # ================================================================================= # START: Monkey Patch for torch.nn.functional.affine_grid (DEVICE-AWARE) @@ -60,7 +61,7 @@ if len(sys.argv) < 2: raise ValueError('Missing model name') MODEL_NAME = sys.argv[1] -HALF = '--fp16' in sys.argv +HALF = False FORCE_CPU = '--cpu' in sys.argv # Check CUDA availability @@ -198,22 +199,19 @@ class FaceMorpherWrapper(Module): def forward(self, pose: Tensor) -> Tensor: """ Args: - pose: Full pose parameters [1, 45] + pose: Full pose parameters [1, 39] Returns: face_output: Face morphed image [1, 4, 128, 128] in normalized format """ - # Extract first 39 dimensions for face morpher - face_pose = pose[:, 0:39] - # Run face morpher (SIREN generates image from pose only) - face_output = self.face_morpher.forward(face_pose) + face_output = self.face_morpher.forward(pose) return face_output face_morpher_wrapper = FaceMorpherWrapper(export_modules['face_morpher']).eval() # Test inputs -face_pose_export = torch.zeros(1, 45, dtype=export_dtype) +face_pose_export = torch.zeros(1, 39, dtype=export_dtype) # Export with torch.no_grad(): @@ -226,15 +224,17 @@ torch.onnx.export( opset_version=16, input_names=['pose'], output_names=['face_morphed'], - dynamic_axes={ - 'pose': {0: 'batch'} - } + external_data =False, + optimize =True, + verify =True, + dynamo =False, ) onnx_model = onnx.load(TMP_FILE_WRITE) onnx.checker.check_model(onnx_model) print(f" Original model inputs: {[inp.name for inp in onnx_model.graph.input]}") +# onnx_model = float16.convert_float_to_float16(onnx_model, keep_io_types=True) # Simplify with input/output names preservation onnx_model_sim, check = simplify(onnx_model, skip_fuse_bn=True) if check: @@ -299,28 +299,39 @@ class BodyMorpherWrapper(Module): body_morpher_wrapper = BodyMorpherWrapper(export_modules['body_morpher']).eval() +full_pose_export = torch.zeros(1, 45, dtype=export_dtype) + # Export with torch.no_grad(): - body_morpher_res_export = body_morpher_wrapper(cv_img_tensor_export, face_morpher_res_export, face_pose_export) + body_morpher_res_export = body_morpher_wrapper(cv_img_tensor_export, face_morpher_res_export, full_pose_export) torch.onnx.export( body_morpher_wrapper, - (cv_img_tensor_export, face_morpher_res_export, face_pose_export), + (cv_img_tensor_export, face_morpher_res_export, full_pose_export), TMP_FILE_WRITE, opset_version=16, input_names=['input_image', 'face_morphed', 'pose'], output_names=['result', 'cv_result'], - dynamic_axes={ - 'input_image': {0: 'height', 1: 'width'}, - 'face_morphed': {0: 'batch'}, - 'pose': {0: 'batch'} - } + external_data =False, + optimize =True, + verify =True, + dynamo =False, ) onnx_model = onnx.load(TMP_FILE_WRITE) onnx.checker.check_model(onnx_model) print(f" Original model inputs: {[inp.name for inp in onnx_model.graph.input]}") +# onnx_model = float16.convert_float_to_float16(onnx_model, keep_io_types=True) +# for node in onnx_model.graph.node: +# if node.name == "/Cast": +# for attr in node.attribute: +# if attr.name == "to": +# attr.i = onnx.TensorProto.FLOAT16 # Set to 10 (FP16) +# break +# if node.name == "/Squeeze": +# node.input[0] = "/ScatterND_14_cast_to_result" + # Simplify with input/output names preservation onnx_model_sim, check = simplify(onnx_model, skip_fuse_bn=True) if check: @@ -372,7 +383,7 @@ class THA4StudentONNXRunner: # Step 1: Face Morpher (SIREN only needs pose, generates face from scratch) face_res = self.face_morpher_sess.run(None, { - 'pose': pose + 'pose': pose[:, :39] }) # Step 2: Body Morpher (needs original image, face result, and pose) From 06f859c67439c1d81c53a832cf7eb8731be4bf0c Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Sat, 27 Dec 2025 23:10:51 -0500 Subject: [PATCH 21/46] feat:restructored the main process --- src/__init__.py | 0 src/args.py | 88 ++++++++++++++ src/debug_input_client.py | 60 ++++++++++ src/ezvtb_rt_interface.py | 75 ++++++++++++ src/face_mesh_client.py | 91 +++++++++++++++ src/i_facial_mocap_client.py | 125 ++++++++++++++++++++ src/main.py | 111 ++++++++++++++++++ src/model_infer_client.py | 200 ++++++++++++++++++++++++++++++++ src/mouse_client.py | 101 ++++++++++++++++ src/open_see_face_client.py | 170 +++++++++++++++++++++++++++ src/utils/__init__.py | 0 src/utils/channel_shared_mem.py | 149 ++++++++++++++++++++++++ src/utils/filter.py | 155 +++++++++++++++++++++++++ src/utils/fps.py | 55 +++++++++ src/utils/pose.py | 183 +++++++++++++++++++++++++++++ src/utils/pose_simplify.py | 130 +++++++++++++++++++++ src/utils/preprocess.py | 114 ++++++++++++++++++ src/utils/timer_wait.py | 57 +++++++++ src/utils/utils.py | 79 +++++++++++++ 19 files changed, 1943 insertions(+) create mode 100644 src/__init__.py create mode 100644 src/args.py create mode 100644 src/debug_input_client.py create mode 100644 src/ezvtb_rt_interface.py create mode 100644 src/face_mesh_client.py create mode 100644 src/i_facial_mocap_client.py create mode 100644 src/main.py create mode 100644 src/model_infer_client.py create mode 100644 src/mouse_client.py create mode 100644 src/open_see_face_client.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/channel_shared_mem.py create mode 100644 src/utils/filter.py create mode 100644 src/utils/fps.py create mode 100644 src/utils/pose.py create mode 100644 src/utils/pose_simplify.py create mode 100644 src/utils/preprocess.py create mode 100644 src/utils/timer_wait.py create mode 100644 src/utils/utils.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/args.py b/src/args.py new file mode 100644 index 0000000..6d2a18f --- /dev/null +++ b/src/args.py @@ -0,0 +1,88 @@ +import argparse +import re + + +def convert_to_byte(size): + result = re.search('(\d+\.?\d*)(b|kb|mb|gb|tb)', size.lower()) + if (result and result.groups()): + unit = result.groups()[1] + amount = float(result.groups()[0]) + index = ['b', 'kb', 'mb', 'gb', 'tb'].index(unit) + return amount * pow(1024, index) + raise ValueError("Invalid size provided, value is " + size) + + +parser = argparse.ArgumentParser() +parser.add_argument('--character', type=str, default='lambda_00') + +parser.add_argument('--debug_input', action='store_true') +parser.add_argument('--cam_input', action='store_true') +parser.add_argument('--mouse_input', type=str) +parser.add_argument('--ifm_input', type=str) +parser.add_argument('--osf_input', type=str) + +parser.add_argument('--output_virtual_cam', action='store_true') +parser.add_argument('--output_spout2', action='store_true') +parser.add_argument('--output_debug', action='store_true') + +parser.add_argument('--alpha_split', action='store_true') +parser.add_argument('--bongo', action='store_true') +parser.add_argument('--extend_movement', action='store_true') + +parser.add_argument('--filter_min_cutoff', type=float, default=1.0) +parser.add_argument('--filter_beta', type=float, default=0.0) + +parser.add_argument('--simplify', type=int, default=1) + +parser.add_argument('--use_tensorrt', action='store_true') +parser.add_argument('--model_version', type=str, default='v3') +parser.add_argument('--model_name', type=str, default='') +parser.add_argument('--model_seperable', action='store_true') +parser.add_argument('--model_half', action='store_true') +parser.add_argument('--gpu_cache', type=str, default='512mb') +parser.add_argument('--eyebrow', action='store_true') + +parser.add_argument('--use_interpolation', action='store_true') +parser.add_argument('--interpolation_scale', type=int, default=1) +parser.add_argument('--interpolation_half', action='store_true') + +parser.add_argument('--use_sr', action='store_true') +parser.add_argument('--sr_x4', action='store_true') +parser.add_argument('--sr_half', action='store_true') +parser.add_argument('--anime4k', action='store_true') + +parser.add_argument('--cache', type=str, default='256mb') + +parser.add_argument('--frame_rate_limit', type=int, default=30) + +parser.add_argument('--alpha_clean', action='store_true') + +args = parser.parse_args() + +if args.cache is not None: + args.max_ram_cache_len = convert_to_byte(args.cache) / pow(1024, 3) # In gigabytes +else: + args.max_ram_cache_len = 0 +if args.gpu_cache is not None: + args.max_gpu_cache_len = convert_to_byte(args.gpu_cache) / pow(1024, 3) # In gigabytes +else: + args.max_gpu_cache_len = 0 + +if args.simplify == 0: + args.max_ram_cache_len = 0 # Disable cacher if simplify is Off + +if not args.output_virtual_cam and not args.output_spout2: + args.output_debug = True # Default to debug output + +if not args.cam_input and args.mouse_input is None \ + and args.ifm_input is None and args.osf_input is None: + args.debug_input = True # Default to debug input + +if args.use_sr: + args.anime4k = False # Disable anime4k if SR is enabled +if args.anime4k: + args.use_sr = False # Disable SR if anime4k is enabled + +args.model_output_size = 1024 if args.use_sr or args.anime4k else 512 + +print("Arguments:", args) \ No newline at end of file diff --git a/src/debug_input_client.py b/src/debug_input_client.py new file mode 100644 index 0000000..37d8136 --- /dev/null +++ b/src/debug_input_client.py @@ -0,0 +1,60 @@ +from multiprocessing import Process, shared_memory, Value +import numpy as np +import time +import math +from .utils.timer_wait import wait_until +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.filter import OneEuroFilterNumpy +from .args import args + +class DebugInputClientProcess(Process): + def __init__(self, pose_position_shm: shared_memory.SharedMemory): + super().__init__() + self.pose_position_shm = pose_position_shm + self.fps = Value('f', 60.0) + + def run(self): + last_time : float = time.perf_counter() + interval : float = 1.0 / 60 # 60 FPS + pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) + np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) + + pose_filter = OneEuroFilterNumpy(freq=60, mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + position_filter = OneEuroFilterNumpy(freq=60, mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + while True: + eyebrow_vector = [0.0] * 12 + mouth_eye_vector = [0.0] * 27 + pose_vector = [0.0] * 6 + position_vector = [0, 0, 0, 1] + + mouth_eye_vector[2] = math.sin(time.perf_counter() * 3) + mouth_eye_vector[3] = math.sin(time.perf_counter() * 3) + + mouth_eye_vector[14] = 0 + + mouth_eye_vector[25] = math.sin(time.perf_counter() * 2.2) * 0.2 + mouth_eye_vector[26] = math.sin(time.perf_counter() * 3.5) * 0.8 + + pose_vector[0] = math.sin(time.perf_counter() * 1.1) + pose_vector[1] = math.sin(time.perf_counter() * 1.2) + pose_vector[2] = math.sin(time.perf_counter() * 1.5) + pose_vector[3] = pose_vector[1] + pose_vector[4] = pose_vector[2] + eyebrow_vector[6] = math.sin(time.perf_counter() * 1.1) + eyebrow_vector[7] = math.sin(time.perf_counter() * 1.1) + + model_input_arr = eyebrow_vector + model_input_arr.extend(mouth_eye_vector) + model_input_arr.extend(pose_vector) + + position_vector[0] = math.sin(time.perf_counter() * 0.5) * 0.1 + position_vector[1] = math.sin(time.perf_counter() * 0.6) * 0.1 + position_vector[2] = math.sin(time.perf_counter() * 0.7) * 0.1 + position_vector[3] = 1 + + with pose_position_shm_channel.lock(): + np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) + wait_until(last_time + interval) + last_time += interval \ No newline at end of file diff --git a/src/ezvtb_rt_interface.py b/src/ezvtb_rt_interface.py new file mode 100644 index 0000000..9d05176 --- /dev/null +++ b/src/ezvtb_rt_interface.py @@ -0,0 +1,75 @@ +import os +import sys + +dir_path = os.path.dirname(os.path.realpath(__file__)) +ezvtb_path = os.path.join(dir_path, "..", "ezvtuber-rt") +ezvtb_main_path = os.path.join(dir_path, "..", 'ezvtuber-rt-main') + +project_path = '' +if os.path.exists(ezvtb_path): + project_path = ezvtb_path +else: + project_path = ezvtb_main_path + +if project_path not in sys.path: + sys.path.append(project_path) + +# Initialize model data path to point to data/models directory +import ezvtb_rt +models_path = os.path.join(dir_path, '..', 'data', 'models') +ezvtb_rt.init_model_path(models_path) + +def get_core( + use_tensorrt:bool = True, + #THA3 model setting + model_version:str = 'v3', #'v3' or 'v4' or 'v4_student' + model_name:str = '', + model_seperable:bool = True, + model_half:bool = True, #If using directml+half, there is small numerical error on Nvidia, and huge numerical error on AMD + model_cache_size:float = 1.0, #unit of GigaBytes, only works for tensorrt + model_use_eyebrow:bool = True, + #RIFE interpolation setting + use_interpolation:bool = True, + interpolation_scale:int = 2, + interpolation_half:bool = True, #If using directml+half, there is small numerical error on Nvidia, and huge numerical error on AMD + #Cacher setting + cacher_ram_size:float = 2.0,#unit of GigaBytes + #SR setting + use_sr:bool = False, + sr_x4:bool = True, + sr_half:bool = True, + ): + if use_tensorrt: + try: + from ezvtb_rt.core_trt import CoreTRT as Core + except: + print("TensorRT is not available, fallback to ONNX Runtime.") + from ezvtb_rt.core_ort import CoreORT as Core + else: + from ezvtb_rt.core_ort import CoreORT as Core + core = Core( + tha_model_version=model_version, + tha_model_seperable=model_seperable, + tha_model_fp16=model_half, + tha_model_name=model_name, + + rife_model_enable=use_interpolation, + rife_model_scale=interpolation_scale, + rife_model_fp16=interpolation_half, + + sr_model_enable=use_sr, + sr_model_scale=4 if sr_x4 else 2, + sr_model_fp16=sr_half, + + vram_cache_size=model_cache_size, + cache_max_giga=cacher_ram_size, + use_eyebrow=model_use_eyebrow, + ) + return core + +if __name__ == '__main__': + try: + from ezvtb_rt.trt_utils import check_build_all_models + check_build_all_models() + except: + print("TensorRT not available, skip building TRT models.") \ No newline at end of file diff --git a/src/face_mesh_client.py b/src/face_mesh_client.py new file mode 100644 index 0000000..387400f --- /dev/null +++ b/src/face_mesh_client.py @@ -0,0 +1,91 @@ +import mediapipe as mp +from multiprocessing import Process, shared_memory, Value +import cv2 +import numpy as np +from .args import args +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.pose import get_pose +from .utils.fps import FPS +from .utils.filter import OneEuroFilterNumpy + + +class FaceMeshClientProcess(Process): + def __init__(self, pose_position_shm: shared_memory.SharedMemory): + super().__init__() + self.pose_position_shm = pose_position_shm + self.fps = Value('f', 0.0) + + def run(self): + facemesh = mp.solutions.face_mesh.FaceMesh(refine_landmarks=True) + pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) + np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) + + cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) + if not cap.isOpened(): + raise Exception("Can't open webcam") + + print("Warming up webcam...") + + frame_count = 0 + input_fps = FPS(60) + while frame_count < 5: + ret, frame = cap.read() + if not ret: + raise Exception("Can't receive frame (stream end?).") + input_fps() + frame_count += 1 + + + pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + + position_offset = None + print("Webcam Input Running at {:.2f} FPS".format(input_fps.view())) + position_vector = np.array([0, 0, 0, 1], dtype=np.float32) + while True: + ret, frame = cap.read() + if not ret: + raise Exception("Can't receive frame (stream end?).") + self.fps.value = input_fps() + rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + results = facemesh.process(rgb_frame) + if results.multi_face_landmarks is None: + continue + facial_landmarks = results.multi_face_landmarks[0].landmark + pose = get_pose(facial_landmarks) + eye_l_h_temp = pose[0] + eye_r_h_temp = pose[1] + mouth_ratio = pose[2] + eye_y_ratio = pose[3] + eye_x_ratio = pose[4] + x_angle = pose[5] + y_angle = pose[6] + z_angle = pose[7] + + eyebrow_vector = [0.0] * 12 + mouth_eye_vector = [0.0] * 27 + pose_vector = [0.0] * 6 + + mouth_eye_vector[2] = (eye_l_h_temp + eye_r_h_temp) / 2.0 + mouth_eye_vector[3] = (eye_l_h_temp + eye_r_h_temp) / 2.0 + + mouth_eye_vector[14] = mouth_ratio * 1.5 + + mouth_eye_vector[25] = (eye_y_ratio + eye_x_ratio) / 2.0 + mouth_eye_vector[26] = (eye_y_ratio + eye_x_ratio) / 2.0 + + if position_offset is None: + position_offset = [(x_angle - 1.5) * 1.6, y_angle * 2.0 , (z_angle + 1.5) * 2] + pose_vector[0] = (x_angle - 1.5) * 1.6 - position_offset[0] + pose_vector[1] = y_angle * 2.0 - position_offset[1] # temp weight + pose_vector[2] = (z_angle + 1.5) * 2 - position_offset[2] # temp weight + pose_vector[3] = pose_vector[1] + pose_vector[4] = pose_vector[2] + + model_input_arr = eyebrow_vector + model_input_arr.extend(mouth_eye_vector) + model_input_arr.extend(pose_vector) + + with pose_position_shm_channel.lock(): + np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + np_position_shm[:] = position_vector \ No newline at end of file diff --git a/src/i_facial_mocap_client.py b/src/i_facial_mocap_client.py new file mode 100644 index 0000000..a48d0b7 --- /dev/null +++ b/src/i_facial_mocap_client.py @@ -0,0 +1,125 @@ +from multiprocessing import Process, shared_memory, Value +from .args import args +from .utils.timer_wait import wait_until +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +import socket +import errno +import numpy as np +import time +import tha2.poser.modes.mode_20_wx +from tha2.mocap.ifacialmocap_constants import * +from .utils.fps import FPS +from .utils.filter import OneEuroFilterNumpy +ifm_converter = tha2.poser.modes.mode_20_wx.IFacialMocapPoseConverter20() + +class IFMClientProcess(Process): + def __init__(self, pose_position_shm: shared_memory.SharedMemory): + super().__init__() + self.pose_position_shm = pose_position_shm + self.address = args.ifm.split(':')[0] + self.port = int(args.ifm.split(':')[1]) + self.fps = Value('f', 0.0) + def run(self): + pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) + np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) + + udpClntSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + data = "iFacialMocap_sahuasouryya9218sauhuiayeta91555dy3719" + + data = data.encode('utf-8') + + udpClntSock.sendto(data, (self.address, self.port)) + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind(("", self.port)) + self.socket.settimeout(5.0) # give ifm app some time to respond + + print("Warming up iFacialMocap connection...") + + frame_count = 0 + input_fps = FPS(60) + while frame_count < 60: + socket_bytes = self.socket.recv(8192) + if not socket_bytes: + raise Exception("Can't receive iFacialMocap data (stream end?).") + input_fps() + frame_count += 1 + + self.socket.settimeout(0.5) # 500 ms + + pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + + print("iFacialMocap Input Running at {:.2f} FPS".format(input_fps.view())) + while True: + socket_bytes = self.socket.recv(8192) + + if not socket_bytes: + raise Exception("Can't receive iFacialMocap data (stream end?).") + + self.fps.value = input_fps() + + socket_string = socket_bytes.decode("utf-8") + + try: + data = self.convert_from_blender_data(socket_string) + except Exception: + print("iFacialMocap data parse error:", socket_string) + continue + + ifacialmocap_pose_converted = ifm_converter.convert(data) + position_vector = data[HEAD_BONE_QUAT] + + eyebrow_vector = [0.0] * 12 + mouth_eye_vector = [0.0] * 27 + pose_vector = [0.0] * 6 + for i in range(0, 12): + eyebrow_vector[i] = ifacialmocap_pose_converted[i] + for i in range(12, 39): + mouth_eye_vector[i - 12] = ifacialmocap_pose_converted[i] + for i in range(39, 42): + pose_vector[i - 39] = ifacialmocap_pose_converted[i] + pose_vector[3] = pose_vector[1] + pose_vector[4] = pose_vector[2] + + mouth_eye_vector[14] = mouth_eye_vector[14] * 1.5 + + model_input_arr = eyebrow_vector + model_input_arr.extend(mouth_eye_vector) + model_input_arr.extend(pose_vector) + + with pose_position_shm_channel.lock(): + np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) + # np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) + # np_position_shm[:] = np.array(position_vector, dtype=np.float32) + + @staticmethod + def convert_from_blender_data(blender_data): + data = {} + + for item in blender_data.split('|'): + if item.find('#') != -1: + k, arr = item.split('#') + arr = [float(n) for n in arr.split(',')] + data[k.replace("_L", "Left").replace("_R", "Right")] = arr + elif item.find('-') != -1: + k, v = item.split("-") + data[k.replace("_L", "Left").replace("_R", "Right")] = float(v) / 100 + + to_rad = 57.3 + data[HEAD_BONE_X] = data["=head"][0] / to_rad + data[HEAD_BONE_Y] = data["=head"][1] / to_rad + data[HEAD_BONE_Z] = data["=head"][2] / to_rad + data[HEAD_BONE_QUAT] = [data["=head"][3], data["=head"][4], data["=head"][5], 1] + # print(data[HEAD_BONE_QUAT][2],min(data[EYE_BLINK_LEFT],data[EYE_BLINK_RIGHT])) + data[RIGHT_EYE_BONE_X] = data["rightEye"][0] / to_rad + data[RIGHT_EYE_BONE_Y] = data["rightEye"][1] / to_rad + data[RIGHT_EYE_BONE_Z] = data["rightEye"][2] / to_rad + data[LEFT_EYE_BONE_X] = data["leftEye"][0] / to_rad + data[LEFT_EYE_BONE_Y] = data["leftEye"][1] / to_rad + data[LEFT_EYE_BONE_Z] = data["leftEye"][2] / to_rad + + return data \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..b775cde --- /dev/null +++ b/src/main.py @@ -0,0 +1,111 @@ +import numpy as np +import time +from .model_infer_client import ModelClientProcess +from .args import args +from .utils.preprocess import resize_to_512_center, apply_color_curves +import cv2 +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from multiprocessing import shared_memory +from .utils.timer_wait import wait_until +from PIL import Image + + + +def main(): + # Load character image + img = Image.open(f"data/images/{args.character}.png") + img = img.convert('RGBA') + ow, oh = img.size + for i, px in enumerate(img.getdata()): + if px[3] <= 0: + y = i // ow + x = i % ow + img.putpixel((x, y), (0, 0, 0, 0)) + if ow != 512 or oh != 512: + img = resize_to_512_center(img) + if args.alpha_clean: + curves = { + 'a': [ + (60, 0), + (200, 255) + ] + } + img = apply_color_curves(img, curves) + input_image = np.array(img) + input_image = cv2.cvtColor(input_image, cv2.COLOR_RGBA2BGRA) + + print("Character Image Loaded:", args.character) + + pose_position_shm = shared_memory.SharedMemory(create=True, size=(45 + 4) * 4) # 45 floats for pose, 4 floats for position + input_process = None + + if args.cam_input: + from .face_mesh_client import FaceMeshClientProcess + input_process = FaceMeshClientProcess(pose_position_shm) + elif args.ifm_input is not None: + from .i_facial_mocap_client import IFMClientProcess + input_process = IFMClientProcess(pose_position_shm) + elif args.osf_input is not None: + from .open_see_face_client import OSFClientProcess + input_process = OSFClientProcess(pose_position_shm) + elif args.mouse_input is not None: + from .mouse_client import MouseClientProcess + input_process = MouseClientProcess(pose_position_shm) + else: + from .debug_input_client import DebugInputClientProcess + input_process = DebugInputClientProcess(pose_position_shm) + + input_fps = input_process.fps + input_process.daemon = True + input_process.start() + + infer_process = ModelClientProcess(input_image, pose_position_shm, input_fps) + infer_process.daemon = True + infer_process.start() + + ret_batch_shm_channels = [ + SharedMemoryExclusiveChannel(infer_process.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") + for i in range(args.interpolation_scale) + ] + np_ret_shms = [ + np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, + buffer=infer_process.ret_shared_mem.buf[i * args.model_output_size * args.model_output_size * 4: + (i + 1) * args.model_output_size * args.model_output_size * 4]) + for i in range(args.interpolation_scale) + ] + + last_time : float = time.perf_counter() + interval : float = 1.0 / args.frame_rate_limit if args.frame_rate_limit > 0 else 0.0 + + from .utils.fps import FPS + pipeline_fps = FPS() + + print("Interval set to {:.3f} seconds".format(interval)) + while True: + infer_process.finish_event.wait() + infer_process.finish_event.clear() + for i in range(args.interpolation_scale): + ret_batch_shm_channels[i].acquire() + for i in range(args.interpolation_scale): + wait_until(last_time + interval) + last_time += interval + cv2.imshow("Output Frame Batch {}".format(i), np_ret_shms[i]) + ret_batch_shm_channels[i].release() + cv2.waitKey(1) + print("pipeline FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, current time {:.5f}".format( + infer_process.pipeline_fps_number.value, + input_fps.value, + infer_process.average_model_interval.value * 1000, + infer_process.cache_hit_ratio.value, + infer_process.gpu_cache_hit_ratio.value, + pipeline_fps() + ), end ='\r', flush=True) + + + + + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/model_infer_client.py b/src/model_infer_client.py new file mode 100644 index 0000000..cddfe06 --- /dev/null +++ b/src/model_infer_client.py @@ -0,0 +1,200 @@ +import math +from multiprocessing import Process, Value, shared_memory, Event +import cv2 +import numpy as np +import time +from .ezvtb_rt_interface import get_core +from .args import args +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.pose_simplify import pose_simplify +from .utils.fps import FPS, Interval +from typing import List +import pyanime4k + +class ModelClientProcess(Process): + def __init__(self, input_image, pose_position_shm: shared_memory.SharedMemory , input_fps): + super().__init__() + self.input_image = input_image + self.pose_position_shm = pose_position_shm # 45 floats for pose, 4 floats for position + + self.ret_shape = (args.interpolation_scale, args.model_output_size, args.model_output_size, 4) + self.ret_nbytes = args.interpolation_scale * args.model_output_size * args.model_output_size * 4 # RGBA + self.ret_shared_mem = shared_memory.SharedMemory(create=True, size=self.ret_nbytes) + + self.last_model_interval = Value('f', 0.0) + self.average_model_interval = Value('f', 0.0) + self.cache_hit_ratio = Value('f', 0.0) + self.gpu_cache_hit_ratio = Value('f', 0.0) + self.pipeline_fps_number = Value('f', 0.0) + self.input_fps = input_fps + + self.finish_event = Event() + + def run(self): + pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) + np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) + + ret_batch_shm_channels = [ + SharedMemoryExclusiveChannel(self.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") + for i in range(args.interpolation_scale) + ] + np_ret_shms = [ + np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, + buffer=self.ret_shared_mem.buf[i * args.model_output_size * args.model_output_size * 4: + (i + 1) * args.model_output_size * args.model_output_size * 4]) + for i in range(args.interpolation_scale) + ] + + if args.anime4k: + self.a4k_processor = pyanime4k.Processor( + processor_type="opencl", + device=0, + model="acnet-gan" + ) + print("Anime4K Loaded") + + model_infer_average_interval: Interval = Interval() + pipeline_fps = FPS() + + # Use unified ezvtb_rt interface for both THA3 and THA4 + model = get_core(use_tensorrt=args.use_tensorrt, + model_version=args.model_version, + model_name=args.model_name, + + model_seperable = args.model_seperable, + model_half=args.model_half, + model_cache_size=args.max_gpu_cache_len, + model_use_eyebrow=args.eyebrow, + + use_interpolation=args.use_interpolation, + interpolation_scale=args.interpolation_scale, + interpolation_half=args.interpolation_half, + + cacher_ram_size=args.max_ram_cache_len, + + use_sr=args.use_sr, + sr_half=args.sr_half, + sr_x4=args.sr_x4) + model.setImage(self.input_image) + model_infer_average_interval.start() + model.inference([np.zeros((1, 45), dtype=np.float32)]) # Warm up + model_infer_average_interval.stop() + self.last_model_interval.value = model_infer_average_interval.last() + + last_pose = np.zeros((45,), dtype=np.float32) + + print("Model Inference Ready") + while True: + with pose_position_shm_channel.lock(): + np_pose = np_pose_shm.copy() + np_position = np_position_shm.copy() + + input_poses = [] + increment = (np_pose - last_pose) / args.interpolation_scale + for i in range(args.interpolation_scale): + input_poses.append(pose_simplify(last_pose + increment * (i + 1))) + last_pose = np_pose + + model_infer_average_interval.start() + output_images = model.inference(input_poses) + + if args.max_ram_cache_len > 0: + hits = model.cacher.hits + miss = model.cacher.miss + if args.use_sr: + hits += model.sr_cacher.hits + miss += model.sr_cacher.miss + total = hits + miss + self.cache_hit_ratio.value = (hits / total) if total > 0 else 0.0 + + if args.use_tensorrt and args.max_gpu_cache_len > 0: + hits = model.tha.cacher.hits + miss = model.tha.cacher.miss + total = hits + miss + self.gpu_cache_hit_ratio.value = (hits / total) if total > 0 else 0.0 + + output_images = self.post_process_ret(np_position, output_images) + + self.average_model_interval.value = model_infer_average_interval.stop() + self.last_model_interval.value = model_infer_average_interval.last() + + self.pipeline_fps_number.value = pipeline_fps() + for i in range(args.interpolation_scale): + with ret_batch_shm_channels[i].lock(): # get pressure from main process if ret not consumed + np_ret_shms[i][:, :, :] = output_images[i] + + self.finish_event.set() # Back pressure main process loop if infer slow + + def post_process_ret(self, np_position: np.ndarray, output_images: np.ndarray) -> List[np.ndarray]: + k_scale = 1 + rotate_angle = 0 + dx = 0 + dy = 0 + if args.extend_movement: + k_scale = np_position[2] * math.sqrt(args.extend_movement) + 1 + rotate_angle = -np_position[0] * 10 * args.extend_movement + dx = np_position[0] * 400 * k_scale * args.extend_movement + dy = -np_position[1] * 600 * k_scale * args.extend_movement + if args.bongo: + rotate_angle -= 5 + + rm = cv2.getRotationMatrix2D((output_images[0].shape[1] / 2, output_images[0].shape[0] / 2), rotate_angle, k_scale) + rm[0, 2] += dx + output_images[0].shape[1] / 2 - output_images[0].shape[1] / 2 + rm[1, 2] += dy + output_images[0].shape[0] / 2 - output_images[0].shape[0] / 2 + + ret = [] + for i in range(output_images.shape[0]): + bgra_image = output_images[i] + bgra_image = cv2.warpAffine( + bgra_image, + rm, + (bgra_image.shape[1], bgra_image.shape[0])) + + if args.debug_input: + cv2.putText(bgra_image, str('OUT_FPS:%.1f' % self.pipeline_fps_number.value), (0, 16), cv2.FONT_HERSHEY_PLAIN, 1, + (0, 255, 0), 1) + cv2.putText(bgra_image, str( + 'GPU_FPS:%.1f' % ( + 1.0 / (self.average_model_interval.value + 1e-6) if not args.use_interpolation else 1.0 / (self.average_model_interval.value + 1e-6) * args.interpolation_scale)), + (0, 32), + cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + cv2.putText(bgra_image, str('INPUT_FPS:%.1f' % self.input_fps.value), (0, 48), + cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + if args.max_ram_cache_len > 0: + cv2.putText(bgra_image, str('MEMCACHED:%.1f%%' % (self.cache_hit_ratio.value * 100)), + (0, 64), + cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + if args.max_gpu_cache_len > 0.0: + cv2.putText(bgra_image, str('GPUCACHED:%.1f%%' % (self.gpu_cache_hit_ratio.value * 100)), + (0, 80), + cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + + rgba_image = None + alpha_channel = None + rgb_channels = None + + if args.anime4k or args.alpha_split: + rgba_image = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGBA) + alpha_channel = np.ascontiguousarray(rgba_image[:, :, 3]) + rgb_channels = np.ascontiguousarray(rgba_image[:,:,:3]) + + if args.anime4k: + rgb_output = self.a4k_processor(rgb_channels) + alpha_output = cv2.resize(alpha_channel, None, fx=2, fy=2) + rgba_image = cv2.merge((rgb_output, alpha_output)) + if args.alpha_split: + alpha_image = cv2.cvtColor(alpha_channel, cv2.COLOR_GRAY2RGB) + rgba_image = cv2.hconcat([rgba_image, alpha_image]) + + + if args.debug_input: + if args.anime4k or args.alpha_split: + bgra_image = cv2.cvtColor(rgba_image, cv2.COLOR_RGBA2BGRA) + ret.append(bgra_image) + else: + ret.append(rgba_image) + return ret + +if __name__ == "__main__": + pass \ No newline at end of file diff --git a/src/mouse_client.py b/src/mouse_client.py new file mode 100644 index 0000000..42e8fb1 --- /dev/null +++ b/src/mouse_client.py @@ -0,0 +1,101 @@ +from multiprocessing import Process, shared_memory, Value +from pynput.mouse import Controller +import numpy as np +import time +from .args import args +from .utils.filter import OneEuroFilterNumpy +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.timer_wait import wait_until + +class MouseClientProcess(Process): + def __init__(self, pose_position_shm: shared_memory.SharedMemory): + super().__init__() + self.pose_position_shm = pose_position_shm + self.fps = Value('f', 60.0) + + def run(self): + mouse = Controller() + pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) + np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) + + # posLimit = [0, 0, 1920, 1080] + posLimit = [int(x) for x in args.mouse_input.split(',')] + prev = { + 'eye_l_h_temp': 0, + 'eye_r_h_temp': 0, + 'mouth_ratio': 0, + 'eye_y_ratio': 0, + 'eye_x_ratio': 0, + 'x_angle': 0, + 'y_angle': 0, + 'z_angle': 0, + } + last_time : float = time.perf_counter() + interval : float = 1.0 / 60 # 60 FPS + + print("Mouse Input Running at 60 FPS") + pose_filter = OneEuroFilterNumpy(freq=60, mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + position_vector = np.array([0, 0, 0, 1], dtype=np.float32) + while True: + pos = mouse.position + # print(pos) + eye_limit = [0.8, 0.5] + head_eye_reduce = 0.6 + head_slowness = 0.2 + mouse_data = { + 'eye_l_h_temp': 0, + 'eye_r_h_temp': 0, + 'mouth_ratio': 0, + 'eye_y_ratio': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]) * eye_limit[1], + 'eye_x_ratio': np.interp(pos[0], [posLimit[0], posLimit[2]], [1, -1]) * eye_limit[0], + 'x_angle': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]), + 'y_angle': np.interp(pos[0], [posLimit[0], posLimit[2]], [1, -1]), + 'z_angle': 0, + } + mouse_data['x_angle'] = np.interp(head_slowness, [0, 1], [prev['x_angle'], mouse_data['x_angle']]) + mouse_data['y_angle'] = np.interp(head_slowness, [0, 1], [prev['y_angle'], mouse_data['y_angle']]) + mouse_data['eye_y_ratio'] -= mouse_data['x_angle'] * eye_limit[1] * head_eye_reduce + mouse_data['eye_x_ratio'] -= mouse_data['y_angle'] * eye_limit[0] * head_eye_reduce + if args.bongo: + mouse_data['y_angle'] += 0.05 + mouse_data['x_angle'] += 0.05 + prev = mouse_data + + eye_l_h_temp = mouse_data['eye_l_h_temp'] + eye_r_h_temp = mouse_data['eye_r_h_temp'] + mouth_ratio = mouse_data['mouth_ratio'] + eye_y_ratio = mouse_data['eye_y_ratio'] + eye_x_ratio = mouse_data['eye_x_ratio'] + x_angle = mouse_data['x_angle'] + y_angle = mouse_data['y_angle'] + z_angle = mouse_data['z_angle'] + + eyebrow_vector = [0.0] * 12 + mouth_eye_vector = [0.0] * 27 + pose_vector = [0.0] * 6 + + mouth_eye_vector[2] = eye_l_h_temp + mouth_eye_vector[3] = eye_r_h_temp + + mouth_eye_vector[14] = mouth_ratio * 1.5 + + mouth_eye_vector[25] = eye_y_ratio + mouth_eye_vector[26] = eye_x_ratio + + pose_vector[0] = x_angle + pose_vector[1] = y_angle + pose_vector[2] = z_angle + pose_vector[3] = pose_vector[1] + pose_vector[4] = pose_vector[2] + + model_input_arr = eyebrow_vector + model_input_arr.extend(mouth_eye_vector) + model_input_arr.extend(pose_vector) + + with pose_position_shm_channel.lock(): + np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + np_position_shm[:] = position_vector + + wait_until(last_time + interval) + last_time += interval \ No newline at end of file diff --git a/src/open_see_face_client.py b/src/open_see_face_client.py new file mode 100644 index 0000000..7b27ea5 --- /dev/null +++ b/src/open_see_face_client.py @@ -0,0 +1,170 @@ +from multiprocessing import Process, Value, shared_memory +from .args import args +import socket +import struct +import numpy as np +from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.fps import FPS +from .utils.filter import OneEuroFilterNumpy + +class OSFClientProcess(Process): + def __init__(self, pose_position_shm: shared_memory.SharedMemory): + super().__init__() + self.pose_position_shm = pose_position_shm + self.address = args.osf.split(':')[0] + self.port = int(args.osf.split(':')[1]) + self.fps = Value('f', 0.0) + + def run(self): + pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) + np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind(("", self.port)) + self.socket.settimeout(None) # wait indefinitely + + print("Warming up OpenSeeFace connection...") + + frame_count = 0 + input_fps = FPS(60) + while frame_count < 5: + socket_bytes = self.socket.recv(8192) + if not socket_bytes: + raise Exception("Can't receive OpenSeeFace data (stream end?).") + input_fps() + frame_count += 1 + + position_vector_0 = None + position_vector = [0, 0, 0, 1] + + pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + + rotation_offset = None + print("OpenSeeFace Input Running at {:.2f} FPS".format(input_fps.view())) + while True: + socket_bytes = self.socket.recv(8192) + + if not socket_bytes: + raise Exception("Can't receive OpenSeeFace data (stream end?).") + + self.fps.value = input_fps() + + try: + osf_raw = (struct.unpack('=di2f2fB1f4f3f3f68f136f210f14f', socket_bytes)) + # print(osf_raw[432:]) + data = {} + OpenSeeDataIndex = [ + 'time', + 'id', + 'cameraResolutionW', + 'cameraResolutionH', + 'rightEyeOpen', + 'leftEyeOpen', + 'got3DPoints', + 'fit3DError', + 'rawQuaternionX', + 'rawQuaternionY', + 'rawQuaternionZ', + 'rawQuaternionW', + 'rawEulerX', + 'rawEulerY', + 'rawEulerZ', + 'translationY', + 'translationX', + 'translationZ', + ] + for i in range(len(OpenSeeDataIndex)): + data[OpenSeeDataIndex[i]] = osf_raw[i] + data['translationY'] *= -1 + data['translationZ'] *= -1 + data['rotationY'] = data['rawEulerY'] - 10 + data['rotationX'] = (-data['rawEulerX'] + 360) % 360 - 180 + data['rotationZ'] = (data['rawEulerZ'] - 90) + OpenSeeFeatureIndex = [ + 'EyeLeft', + 'EyeRight', + 'EyebrowSteepnessLeft', + 'EyebrowUpDownLeft', + 'EyebrowQuirkLeft', + 'EyebrowSteepnessRight', + 'EyebrowUpDownRight', + 'EyebrowQuirkRight', + 'MouthCornerUpDownLeft', + 'MouthCornerInOutLeft', + 'MouthCornerUpDownRight', + 'MouthCornerInOutRight', + 'MouthOpen', + 'MouthWide' + ] + except Exception: + print("OpenSeeFace data parse error:", socket_bytes) + continue + + for i in range(68): + data['confidence' + str(i)] = osf_raw[i + 18] + for i in range(68): + data['pointsX' + str(i)] = osf_raw[i * 2 + 18 + 68] + data['pointsY' + str(i)] = osf_raw[i * 2 + 18 + 68 + 1] + for i in range(70): + data['points3DX' + str(i)] = osf_raw[i * 3 + 18 + 68 + 68 * 2] + data['points3DY' + str(i)] = osf_raw[i * 3 + 18 + 68 + 68 * 2 + 1] + data['points3DZ' + str(i)] = osf_raw[i * 3 + 18 + 68 + 68 * 2 + 2] + + for i in range(len(OpenSeeFeatureIndex)): + data[OpenSeeFeatureIndex[i]] = osf_raw[i + 432] + # print(data['rotationX'],data['rotationY'],data['rotationZ']) + + a = np.array([ + data['points3DX66'] - data['points3DX68'] + data['points3DX67'] - data['points3DX69'], + data['points3DY66'] - data['points3DY68'] + data['points3DY67'] - data['points3DY69'], + data['points3DZ66'] - data['points3DZ68'] + data['points3DZ67'] - data['points3DZ69'] + ]) + a = (a / np.linalg.norm(a)) + data['eyeRotationX'] = a[0] + data['eyeRotationY'] = a[1] + + eyebrow_vector = [0.0] * 12 + mouth_eye_vector = [0.0] * 27 + pose_vector = [0.0] * 6 + + mouth_eye_vector[2] = 1 - data['leftEyeOpen'] + mouth_eye_vector[3] = 1 - data['rightEyeOpen'] + + mouth_eye_vector[14] = max(data['MouthOpen'], 0) * 2 # Open larger mouth + # print(mouth_eye_vector[14]) + + mouth_eye_vector[25] = -data['eyeRotationY'] * 3 - (data['rotationX']) / 57.3 * 1.5 + mouth_eye_vector[26] = data['eyeRotationX'] * 3 + (data['rotationY']) / 57.3 + # print(mouth_eye_vector[25:27]) + eyebrow_vector[6] = data['EyebrowUpDownLeft'] + eyebrow_vector[7] = data['EyebrowUpDownRight'] + # print(data['EyebrowUpDownLeft'],data['EyebrowUpDownRight']) + if rotation_offset is None: + rotation_offset = [data['rotationX'], data['rotationY'], data['rotationZ']] + pose_vector[0] = (data['rotationX'] - rotation_offset[0]) / 57.3 * 3 + pose_vector[1] = -(data['rotationY'] - rotation_offset[1]) / 57.3 * 3 + pose_vector[2] = (data['rotationZ'] - rotation_offset[2]) / 57.3 * 2 + pose_vector[3] = pose_vector[1] + pose_vector[4] = pose_vector[2] + + if position_vector_0 == None: #Provide an initial reference point + position_vector_0 = [0, 0, 0, 1] + position_vector_0[0] = data['translationX'] + position_vector_0[1] = data['translationY'] + position_vector_0[2] = data['translationZ'] + #Compute relative translation + position_vector[0] = -(data['translationX'] - position_vector_0[0]) * 0.1 + position_vector[1] = -(data['translationY'] - position_vector_0[1]) * 0.1 + position_vector[2] = -(data['translationZ'] - position_vector_0[2]) * 0.1 + + model_input_arr = eyebrow_vector + model_input_arr.extend(mouth_eye_vector) + model_input_arr.extend(pose_vector) + + with pose_position_shm_channel.lock(): + np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) + # np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) + # np_position_shm[:] = np.array(position_vector, dtype=np.float32) diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/channel_shared_mem.py b/src/utils/channel_shared_mem.py new file mode 100644 index 0000000..dfecece --- /dev/null +++ b/src/utils/channel_shared_mem.py @@ -0,0 +1,149 @@ +# windows_only_shm_lock.py +from __future__ import annotations +import ctypes +import ctypes.wintypes as wt +import hashlib +import time +from contextlib import contextmanager +from multiprocessing import shared_memory +from typing import Optional, Union + +# ---- WinAPI for Named Mutex ---- +kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + +# HANDLE CreateMutexW(LPSECURITY_ATTRIBUTES, BOOL, LPCWSTR); +_CreateMutexW = kernel32.CreateMutexW +_CreateMutexW.argtypes = [wt.LPVOID, wt.BOOL, wt.LPCWSTR] +_CreateMutexW.restype = wt.HANDLE + +# HANDLE OpenMutexW(DWORD, BOOL, LPCWSTR); +_OpenMutexW = kernel32.OpenMutexW +_OpenMutexW.argtypes = [wt.DWORD, wt.BOOL, wt.LPCWSTR] +_OpenMutexW.restype = wt.HANDLE + +# DWORD WaitForSingleObject(HANDLE, DWORD); +_WaitForSingleObject = kernel32.WaitForSingleObject +_WaitForSingleObject.argtypes = [wt.HANDLE, wt.DWORD] +_WaitForSingleObject.restype = wt.DWORD + +# BOOL ReleaseMutex(HANDLE); +_ReleaseMutex = kernel32.ReleaseMutex +_ReleaseMutex.argtypes = [wt.HANDLE] +_ReleaseMutex.restype = wt.BOOL + +# BOOL CloseHandle(HANDLE); +_CloseHandle = kernel32.CloseHandle +_CloseHandle.argtypes = [wt.HANDLE] +_CloseHandle.restype = wt.BOOL + +# Constants +INFINITE = 0xFFFFFFFF +WAIT_OBJECT_0 = 0x00000000 +WAIT_TIMEOUT = 0x00000102 +WAIT_ABANDONED = 0x00000080 +WAIT_FAILED = 0xFFFFFFFF +MUTEX_ALL_ACCESS = 0x1F0001 +ERROR_ALREADY_EXISTS = 183 + + +class SharedMemoryExclusiveChannel: + """ + Two-process exclusive access wrapper using Windows Named Mutex: + - payload SharedMemory is provided externally + - Uses Windows Named Mutex for cross-process synchronization + - More direct and simpler than manual InterlockedCompareExchange implementation + """ + + def __init__( + self, + payload: shared_memory.SharedMemory, + ctrl_name: str + ): + # 1) attach payload (external) + self.payload_shm = payload + + # 2) create/open named mutex for synchronization + self.mutex_name = f"Global\\{ctrl_name}_mutex" + self._mutex_handle = self._create_or_open_mutex(self.mutex_name) + if self._mutex_handle is None or self._mutex_handle == 0: + error = ctypes.get_last_error() + raise OSError(f"Failed to create/open mutex {self.mutex_name}, error: {error}") + + @staticmethod + def _create_or_open_mutex(name: str) -> wt.HANDLE: + """Create or open a named mutex.""" + # Try to create the mutex + handle = _CreateMutexW(None, False, name) + if handle is None or handle == 0: + # If creation failed, try to open existing mutex + handle = _OpenMutexW(MUTEX_ALL_ACCESS, False, name) + return handle + + # ---- payload view: zero-copy for numpy ---- + def payload_view(self) -> memoryview: + return self.payload_shm.buf # user can slice if needed + + # ---- lock API ---- + def acquire(self, timeout_ms: Optional[int] = None) -> bool: + """ + Acquire exclusive access using Windows Mutex. + - timeout_ms=None => infinite wait + - spin parameter is kept for API compatibility but ignored (Mutex handles waiting efficiently) + + Returns True if lock acquired, False if timeout. + """ + if timeout_ms is None: + wait_time = INFINITE + else: + wait_time = max(0, timeout_ms) + + result = _WaitForSingleObject(self._mutex_handle, wait_time) + + if result == WAIT_OBJECT_0: + # Successfully acquired the mutex + return True + elif result == WAIT_ABANDONED: + # Previous owner died without releasing, but we now own it + # This is still considered a successful acquisition + return True + elif result == WAIT_TIMEOUT: + # Timeout occurred + return False + else: # WAIT_FAILED or other error + error = ctypes.get_last_error() + raise OSError(f"WaitForSingleObject failed with error: {error}") + + def release(self) -> None: + """Release the mutex.""" + if not _ReleaseMutex(self._mutex_handle): + error = ctypes.get_last_error() + raise OSError(f"ReleaseMutex failed with error: {error}") + + @contextmanager + def lock(self, timeout_ms: Optional[int] = None): + """ + Context manager for acquiring and releasing the lock. + - timeout_ms: timeout in milliseconds (None for infinite) + - spin: kept for API compatibility but ignored + """ + if not self.acquire(timeout_ms=timeout_ms): + raise TimeoutError("acquire() timeout") + try: + yield self + finally: + self.release() + + # ---- lifecycle ---- + def close(self) -> None: + """Close handles and shared memory.""" + if self._mutex_handle and self._mutex_handle != 0: + _CloseHandle(self._mutex_handle) + self._mutex_handle = None + self.payload_shm.close() + + def unlink_control_if_owner(self) -> None: + """ + Named mutex is automatically cleaned up by Windows when all handles are closed. + This method is kept for API compatibility but does nothing. + """ + pass diff --git a/src/utils/filter.py b/src/utils/filter.py new file mode 100644 index 0000000..2852c99 --- /dev/null +++ b/src/utils/filter.py @@ -0,0 +1,155 @@ +""" +Numpy-compatible wrapper for OneEuroFilter. + +Provides an adaptive One Euro Filter that automatically handles numpy arrays +by maintaining separate filter instances for each element. +""" + +import numpy as np +from OneEuroFilter import OneEuroFilter +from typing import Optional +import time + +class OneEuroFilterNumpy: + """ + Auto-initializing One Euro Filter for numpy arrays. + + Automatically determines shape from first input and maintains independent + OneEuroFilter instances for each element in the array. + + Example: + >>> filter = AdaptiveOneEuroFilterNumpy(freq=30, mincutoff=1.0, beta=0.01) + >>> noisy_data = np.array([1.0, 2.0, 3.0]) + >>> filtered = filter(noisy_data) + >>> filtered = filter(noisy_data + np.random.randn(3) * 0.1) + """ + + def __init__( + self, + freq: float, + mincutoff: float = 1.0, + beta: float = 0.0, + dcutoff: float = 1.0 + ) -> None: + """ + Initialize an adaptive One Euro Filter. + + Args: + freq: An estimate of the frequency in Hz of the signal (> 0) + mincutoff: Min cutoff frequency in Hz (> 0). Lower values remove more jitter + beta: Parameter to reduce latency (>= 0) + dcutoff: Cutoff frequency for derivatives (> 0) + """ + self.freq = freq + self.mincutoff = mincutoff + self.beta = beta + self.dcutoff = dcutoff + self._filters = None + self._shape = None + + def __call__( + self, + x: np.ndarray, + timestamp: Optional[float] = None + ) -> np.ndarray: + """ + Filter a noisy numpy array. + + On first call, initializes filters based on input shape. + + Args: + x: Noisy array to filter + timestamp: Timestamp in seconds (optional) + + Returns: + Filtered array with same shape as input + """ + # Initialize filters on first call + if self._filters is None: + self._shape = x.shape + size = np.prod(self._shape).astype(int) + self._filters = [ + OneEuroFilter( + freq=self.freq, + mincutoff=self.mincutoff, + beta=self.beta, + dcutoff=self.dcutoff + ) + for _ in range(size) + ] + + # Validate shape + if x.shape != self._shape: + raise ValueError( + f"Input shape {x.shape} doesn't match initialized shape {self._shape}" + ) + + if timestamp is None: + timestamp = time.perf_counter() + + # Flatten, filter, reshape + x_flat = x.flatten() + filtered_flat = np.array([ + self._filters[i](float(val), timestamp) + for i, val in enumerate(x_flat) + ]) + + return filtered_flat.reshape(self._shape) + + def filter( + self, + x: np.ndarray, + timestamp: Optional[float] = None + ) -> np.ndarray: + """Filter a noisy numpy array (alias for __call__).""" + return self.__call__(x, timestamp) + + def reset(self) -> None: + """Reset all internal filter states.""" + if self._filters is not None: + for f in self._filters: + f.reset() + + def setFrequency(self, freq: float) -> None: + """Set the frequency for all filters.""" + self.freq = freq + if self._filters is not None: + for f in self._filters: + f.setFrequency(freq) + + def setMinCutoff(self, mincutoff: float) -> None: + """Set the min cutoff frequency for all filters.""" + self.mincutoff = mincutoff + if self._filters is not None: + for f in self._filters: + f.setMinCutoff(mincutoff) + + def setBeta(self, beta: float) -> None: + """Set the beta parameter for all filters.""" + self.beta = beta + if self._filters is not None: + for f in self._filters: + f.setBeta(beta) + + def setDerivateCutoff(self, dcutoff: float) -> None: + """Set the derivative cutoff frequency for all filters.""" + self.dcutoff = dcutoff + if self._filters is not None: + for f in self._filters: + f.setDerivateCutoff(dcutoff) + + def setParameters( + self, + freq: float, + mincutoff: float = 1.0, + beta: float = 0.0, + dcutoff: float = 1.0 + ) -> None: + """Set all parameters for all filters.""" + self.freq = freq + self.mincutoff = mincutoff + self.beta = beta + self.dcutoff = dcutoff + if self._filters is not None: + for f in self._filters: + f.setParameters(freq, mincutoff, beta, dcutoff) \ No newline at end of file diff --git a/src/utils/fps.py b/src/utils/fps.py new file mode 100644 index 0000000..a7db58f --- /dev/null +++ b/src/utils/fps.py @@ -0,0 +1,55 @@ +import time +import collections + +class FPS: + def __init__(self, avarageof=50): + self.frametimestamps = collections.deque(maxlen=avarageof) + + def __call__(self): + self.frametimestamps.append(time.perf_counter()) + return self.view() + + def view(self): + if len(self.frametimestamps) > 1: + return len(self.frametimestamps) / (self.frametimestamps[-1] - self.frametimestamps[0] + 1e-10) + else: + return 0.0 + + def last(self): + if len(self.frametimestamps): + return self.frametimestamps[-1] + else: + return time.perf_counter() + +class Interval: + def __init__(self, avarageof=50): + self.frame_intervals = collections.deque(maxlen=avarageof) + self.start_time = None + self.interval = 0.0 + self.moving_sum = 0.0 + + def start(self): + self.start_time = time.perf_counter() + + def stop(self): + if self.start_time is not None: + last_interval = time.perf_counter() - self.start_time + + if len(self.frame_intervals) >= self.frame_intervals.maxlen: + self.moving_sum -= self.frame_intervals[0] + self.moving_sum += last_interval + + self.frame_intervals.append(last_interval) + self.start_time = None + + if len(self.frame_intervals) > 0: + self.interval = self.moving_sum / len(self.frame_intervals) + return self.interval + else: + raise RuntimeError("FPSInterval.stop() called before start()") + + def view(self): + return self.interval + + def last(self): + return self.frame_intervals[-1] if len(self.frame_intervals) > 0 else 0.0 \ No newline at end of file diff --git a/src/utils/pose.py b/src/utils/pose.py new file mode 100644 index 0000000..1bc6127 --- /dev/null +++ b/src/utils/pose.py @@ -0,0 +1,183 @@ +import cv2 +import numpy as np +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_CONTOURS +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_FACE_OVAL +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_IRISES +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_EYE +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_EYEBROW +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_IRIS +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LIPS +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYE +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYEBROW +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_IRIS +from mediapipe.python.solutions.face_mesh_connections import FACEMESH_TESSELATION + +from facial_points import MOUTH_TOP, MOUTH_BOTTOM, MOUTH_LEFT1, MOUTH_LEFT2, MOUTH_RIGHT +from facial_points import IRIS_L_TOP, IRIS_L_BOTTOM, IRIS_L_LEFT, IRIS_L_RIGHT +from facial_points import IRIS_R_TOP, IRIS_R_BOTTOM, IRIS_R_LEFT, IRIS_R_RIGHT +from .utils import get_distance + + +# pose vector index ------------------------------------------ +# wink 0, 1 +# happy wink 2, 3 +# surprised 4, 5 +# relaxed 6, 7 +# unimpressed 8, 9 +# iris Shrinkage L 12 +# iris Shrinkage R MOUTH_TOP +# mouth aaa MOUTH_BOTTOM +# mouth iii 15 +# mouth uuu 16 +# mouth eee 17 +# mouth ooo 18 +# mouth delta 19 +# mouth lowered corner 18,19 +# mouth raised corner 20,21 +# mouth smirk 22 +# Iris rotation 25, 26 + +# x,y,z 27, 28, 29 +# ------------------------------------------------------------ + + +class Landmark: + def __init__(self, x=None, y=None, z=None): + self.x = x + self.y = y + self.z = z + + def __repr__(self): + return f'x: {self.x}\ny: {self.y}\nz: {self.z}' + +# def set_pose(pose, pose_vector) + + +def get_iris_center_point(landmarks, side=None): + """ + calculate right or left side iris center 3d coordinate + Args: + landmarks: + side: + + Returns: + iris center 3d coordinate point (Landmark) + + Raises: + if you didn't specify side + + """ + if side is None: + raise 'please choose side `left` or `right`' + elif side.lower() == 'right' or side.lower() == 'r': + landmark_list = FACEMESH_RIGHT_IRIS + elif side.lower() == 'left' or side.lower() == 'l': + landmark_list = FACEMESH_LEFT_IRIS + + temp = np.zeros(3) + for idx in list(map(lambda x: x[0], landmark_list)): + pos_x, pos_y, pos_z = landmarks[idx].x, landmarks[idx].y, landmarks[idx].z + temp[0] += pos_x + temp[1] += pos_y + temp[2] += pos_z + temp = temp / len(landmark_list) + return Landmark(temp[0], temp[1], temp[2]) + + +def get_pose(facial_landmarks, debug_image=None): + """ + extract pose vector from facial landmarks + + Args: + facial_landmarks (landmarks): extracted facial landmarks by mediapipe + debug_image (numpy array): frame image from video or webcam + + Returns: + tuple: pose vector + numpy array(optional): draw features to frame image + """ + iris_r_center = get_iris_center_point(facial_landmarks, 'r') + iris_l_center = get_iris_center_point(facial_landmarks, 'l') + + mouth_h = facial_landmarks[MOUTH_TOP].y - facial_landmarks[MOUTH_BOTTOM].y + mouth_w = facial_landmarks[MOUTH_RIGHT].x - (facial_landmarks[MOUTH_LEFT1].x + facial_landmarks[MOUTH_LEFT2].x) / 2 + mouth_ratio = mouth_h / mouth_w + + x_angle = np.arctan2(facial_landmarks[197].y - facial_landmarks[9].y, facial_landmarks[197].z - facial_landmarks[9].z) + y_angle = np.arctan2(facial_landmarks[IRIS_L_TOP].z - facial_landmarks[IRIS_R_TOP].z, facial_landmarks[IRIS_L_TOP].x - facial_landmarks[IRIS_R_TOP].x) + z_angle = np.arctan2(facial_landmarks[9].y - facial_landmarks[152].y, facial_landmarks[9].x - facial_landmarks[152].x) + + iris_rotation_l_h = get_distance(facial_landmarks[IRIS_L_TOP], facial_landmarks[IRIS_L_BOTTOM]) + iris_rotation_l_w = get_distance(facial_landmarks[IRIS_L_RIGHT], facial_landmarks[IRIS_L_LEFT]) + iris_rotation_r_h = get_distance(facial_landmarks[IRIS_R_TOP], facial_landmarks[IRIS_R_BOTTOM]) + iris_rotation_r_w = get_distance(facial_landmarks[IRIS_R_RIGHT], facial_landmarks[IRIS_R_LEFT]) + + iris_rotation_l_h_temp = np.sqrt( + (iris_l_center.x - facial_landmarks[IRIS_L_TOP].x) ** 2 + (iris_l_center.y - facial_landmarks[IRIS_L_TOP].y) ** 2) + iris_rotation_l_w_temp = np.sqrt( + (iris_l_center.x - facial_landmarks[IRIS_L_RIGHT].x) ** 2 + (iris_l_center.y - facial_landmarks[IRIS_L_RIGHT].y) ** 2) + iris_rotation_r_h_temp = np.sqrt( + (iris_r_center.x - facial_landmarks[IRIS_R_TOP].x) ** 2 + (iris_r_center.y - facial_landmarks[IRIS_R_TOP].y) ** 2) + iris_rotation_r_w_temp = np.sqrt( + (iris_r_center.x - facial_landmarks[IRIS_R_RIGHT].x) ** 2 + (iris_r_center.y - facial_landmarks[IRIS_R_RIGHT].y) ** 2) + + eye_x_ratio = ((iris_rotation_l_w_temp / iris_rotation_l_w + iris_rotation_r_w_temp / iris_rotation_r_w) - 1) * 3 + eye_y_ratio = ((iris_rotation_l_h_temp / iris_rotation_l_h + iris_rotation_r_h_temp / iris_rotation_r_h) - 1) * 3 + + eye_l_h_temp = 1 - 2 * (facial_landmarks[IRIS_R_BOTTOM].y - facial_landmarks[IRIS_R_TOP].y) / ( + facial_landmarks[IRIS_R_LEFT].x - facial_landmarks[IRIS_R_RIGHT].x) + eye_r_h_temp = 1 - 2 * (facial_landmarks[IRIS_L_BOTTOM].y - facial_landmarks[IRIS_L_TOP].y) / ( + facial_landmarks[IRIS_L_LEFT].x - facial_landmarks[IRIS_L_RIGHT].x) + + if debug_image is not None: + h, w, c = debug_image.shape + + # draw iris + cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], + [int(iris_l_center.x * w), int(iris_l_center.y * h)], (0, 128, 128), 3) + cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], + [int(iris_r_center.x * w), int(iris_r_center.y * h)], (255, 128, 0), 3) + + # draw iris connection + debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], + [int(iris_r_center.x * w), int(iris_r_center.y * h)], (0, 128, 255), 1) + + # draw eye edge2iris + debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], + [int(facial_landmarks[IRIS_R_RIGHT].x * w), int(facial_landmarks[IRIS_R_RIGHT].y * h)], (0, 255, 128), 1) + debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], + [int(facial_landmarks[IRIS_R_BOTTOM].x * w), int(facial_landmarks[IRIS_R_BOTTOM].y * h)], (0, 255, 128), 1) + debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], + [int(facial_landmarks[IRIS_R_LEFT].x * w), int(facial_landmarks[IRIS_R_LEFT].y * h)], (0, 255, 128), 1) + debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], + [int(facial_landmarks[IRIS_R_TOP].x * w), int(facial_landmarks[IRIS_R_TOP].y * h)], (0, 255, 128), 1) + + debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], + [int(facial_landmarks[IRIS_L_RIGHT].x * w), int(facial_landmarks[IRIS_L_RIGHT].y * h)], (255, 128, 0), 1) + debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], + [int(facial_landmarks[IRIS_L_TOP].x * w), int(facial_landmarks[IRIS_L_TOP].y * h)], (255, 128, 0), 1) + debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], + [int(facial_landmarks[IRIS_L_LEFT].x * w), int(facial_landmarks[IRIS_L_LEFT].y * h)], (255, 128, 0), 1) + debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], + [int(facial_landmarks[IRIS_L_BOTTOM].x * w), int(facial_landmarks[IRIS_L_BOTTOM].y * h)], (255, 128, 0), 1) + + # draw mouth + debug_image = cv2.line(debug_image, [int((facial_landmarks[MOUTH_LEFT1].x + facial_landmarks[MOUTH_LEFT2].x) / 2 * w), int((facial_landmarks[MOUTH_LEFT1].y + facial_landmarks[MOUTH_LEFT2].y) / 2 * h)], + [int(facial_landmarks[MOUTH_RIGHT].x * w), int(facial_landmarks[MOUTH_RIGHT].y * h)], (255, 64, 0), 1) + + debug_image = cv2.line(debug_image, [int(facial_landmarks[MOUTH_TOP].x * w), int(facial_landmarks[MOUTH_TOP].y * h)], + [int(facial_landmarks[MOUTH_BOTTOM].x * w), int(facial_landmarks[MOUTH_BOTTOM].y * h)], (255, 64, 0), 1) + + # draw mouth + debug_image = cv2.line(debug_image, [int(facial_landmarks[197].x * w), int(facial_landmarks[197].y * h)], + [int(facial_landmarks[9].x * w), int(facial_landmarks[9].y * h)], (0, 0, 255), 1) + + debug_image = cv2.flip(debug_image, 1) + + cv2.putText(debug_image, f'x angle : {x_angle:.2f}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) + cv2.putText(debug_image, f'y angle : {y_angle:.2f}', (10, 45), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) + cv2.putText(debug_image, f'z angle : {z_angle:.2f}', (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) + + return (eye_l_h_temp, eye_r_h_temp, mouth_ratio, eye_y_ratio, eye_x_ratio, x_angle , y_angle, z_angle), debug_image + else: + return eye_l_h_temp, eye_r_h_temp, mouth_ratio, eye_y_ratio, eye_x_ratio,x_angle, y_angle, z_angle \ No newline at end of file diff --git a/src/utils/pose_simplify.py b/src/utils/pose_simplify.py new file mode 100644 index 0000000..4da1798 --- /dev/null +++ b/src/utils/pose_simplify.py @@ -0,0 +1,130 @@ +from ..args import args +import numpy as np +import math + +# Delay import to avoid loading torch during module initialization +ifm_converter = None + +def _get_ifm_converter(): + global ifm_converter + if ifm_converter is None: + import tha2.poser.modes.mode_20_wx + ifm_converter = tha2.poser.modes.mode_20_wx.IFacialMocapPoseConverter20() + return ifm_converter + +def pose_simplify(model_input): + ifm_converter = _get_ifm_converter() + simplify_arr = [1000] * ifm_converter.pose_size + if args.simplify >= 1: + simplify_arr = [200] * ifm_converter.pose_size + simplify_arr[ifm_converter.eye_wink_left_index] = 50 + simplify_arr[ifm_converter.eye_wink_right_index] = 50 + simplify_arr[ifm_converter.eye_happy_wink_left_index] = 50 + simplify_arr[ifm_converter.eye_happy_wink_right_index] = 50 + simplify_arr[ifm_converter.eye_surprised_left_index] = 30 + simplify_arr[ifm_converter.eye_surprised_right_index] = 30 + simplify_arr[ifm_converter.iris_rotation_x_index] = 25 + simplify_arr[ifm_converter.iris_rotation_y_index] = 25 + simplify_arr[ifm_converter.eye_raised_lower_eyelid_left_index] = 10 + simplify_arr[ifm_converter.eye_raised_lower_eyelid_right_index] = 10 + simplify_arr[ifm_converter.mouth_lowered_corner_left_index] = 5 + simplify_arr[ifm_converter.mouth_lowered_corner_right_index] = 5 + simplify_arr[ifm_converter.mouth_raised_corner_left_index] = 5 + simplify_arr[ifm_converter.mouth_raised_corner_right_index] = 5 + if args.simplify >= 2: + simplify_arr[ifm_converter.head_x_index] = 100 + simplify_arr[ifm_converter.head_y_index] = 100 + simplify_arr[ifm_converter.eye_surprised_left_index] = 10 + simplify_arr[ifm_converter.eye_surprised_right_index] = 10 + model_input[ifm_converter.eye_wink_left_index] += model_input[ + ifm_converter.eye_happy_wink_left_index] + model_input[ifm_converter.eye_happy_wink_left_index] = model_input[ + ifm_converter.eye_wink_left_index] / 2 + model_input[ifm_converter.eye_wink_left_index] = model_input[ + ifm_converter.eye_wink_left_index] / 2 + model_input[ifm_converter.eye_wink_right_index] += model_input[ + ifm_converter.eye_happy_wink_right_index] + model_input[ifm_converter.eye_happy_wink_right_index] = model_input[ + ifm_converter.eye_wink_right_index] / 2 + model_input[ifm_converter.eye_wink_right_index] = model_input[ + ifm_converter.eye_wink_right_index] / 2 + + uosum = model_input[ifm_converter.mouth_uuu_index] + \ + model_input[ifm_converter.mouth_ooo_index] + model_input[ifm_converter.mouth_ooo_index] = uosum + model_input[ifm_converter.mouth_uuu_index] = 0 + is_open = (model_input[ifm_converter.mouth_aaa_index] + model_input[ + ifm_converter.mouth_iii_index] + uosum) > 0 + model_input[ifm_converter.mouth_lowered_corner_left_index] = 0 + model_input[ifm_converter.mouth_lowered_corner_right_index] = 0 + model_input[ifm_converter.mouth_raised_corner_left_index] = 0.5 if is_open else 0 + model_input[ifm_converter.mouth_raised_corner_right_index] = 0.5 if is_open else 0 + simplify_arr[ifm_converter.mouth_lowered_corner_left_index] = 0 + simplify_arr[ifm_converter.mouth_lowered_corner_right_index] = 0 + simplify_arr[ifm_converter.mouth_raised_corner_left_index] = 0 + simplify_arr[ifm_converter.mouth_raised_corner_right_index] = 0 + if args.simplify >= 3: + simplify_arr[ifm_converter.iris_rotation_x_index] = 20 + simplify_arr[ifm_converter.iris_rotation_y_index] = 20 + simplify_arr[ifm_converter.eye_wink_left_index] = 32 + simplify_arr[ifm_converter.eye_wink_right_index] = 32 + simplify_arr[ifm_converter.eye_happy_wink_left_index] = 32 + simplify_arr[ifm_converter.eye_happy_wink_right_index] = 32 + if args.simplify >= 4: + simplify_arr[ifm_converter.head_x_index] = 50 + simplify_arr[ifm_converter.head_y_index] = 50 + simplify_arr[ifm_converter.neck_z_index] = 100 + model_input[ifm_converter.eye_raised_lower_eyelid_left_index] = 0 + model_input[ifm_converter.eye_raised_lower_eyelid_right_index] = 0 + simplify_arr[ifm_converter.iris_rotation_x_index] = 10 + simplify_arr[ifm_converter.iris_rotation_y_index] = 10 + simplify_arr[ifm_converter.eye_wink_left_index] = 24 + simplify_arr[ifm_converter.eye_wink_right_index] = 24 + simplify_arr[ifm_converter.eye_happy_wink_left_index] = 24 + simplify_arr[ifm_converter.eye_happy_wink_right_index] = 24 + simplify_arr[ifm_converter.eye_surprised_left_index] = 8 + simplify_arr[ifm_converter.eye_surprised_right_index] = 8 + model_input[ifm_converter.eye_wink_left_index] += model_input[ + ifm_converter.eye_wink_right_index] + model_input[ifm_converter.eye_wink_right_index] = model_input[ + ifm_converter.eye_wink_left_index] / 2 + model_input[ifm_converter.eye_wink_left_index] = model_input[ + ifm_converter.eye_wink_left_index] / 2 + + model_input[ifm_converter.eye_surprised_left_index] += model_input[ + ifm_converter.eye_surprised_right_index] + model_input[ifm_converter.eye_surprised_right_index] = model_input[ + ifm_converter.eye_surprised_left_index] / 2 + model_input[ifm_converter.eye_surprised_left_index] = model_input[ + ifm_converter.eye_surprised_left_index] / 2 + + model_input[ifm_converter.eye_happy_wink_left_index] += model_input[ + ifm_converter.eye_happy_wink_right_index] + model_input[ifm_converter.eye_happy_wink_right_index] = model_input[ + ifm_converter.eye_happy_wink_left_index] / 2 + model_input[ifm_converter.eye_happy_wink_left_index] = model_input[ + ifm_converter.eye_happy_wink_left_index] / 2 + model_input[ifm_converter.mouth_aaa_index] = min( + model_input[ifm_converter.mouth_aaa_index] + + model_input[ifm_converter.mouth_ooo_index] / 2 + + model_input[ifm_converter.mouth_iii_index] / 2 + + model_input[ifm_converter.mouth_uuu_index] / 2, 1 + ) + model_input[ifm_converter.mouth_ooo_index] = 0 + model_input[ifm_converter.mouth_iii_index] = 0 + model_input[ifm_converter.mouth_uuu_index] = 0 + for i in range(4, args.simplify): + simplify_arr = [max(math.ceil(x * 0.8), 5) for x in simplify_arr] + for i in range(0, len(simplify_arr)): + if simplify_arr[i] > 0: + model_input[i] = round(model_input[i] * simplify_arr[i]) / simplify_arr[i] + + input_pose = np.zeros((1, 45), dtype=np.float32) + if args.eyebrow: + for i in range(12): + input_pose[0, i] = model_input[i] + for i in range(27): + input_pose[0, i + 12] = model_input[i + 12] + for i in range(6): + input_pose[0, i + 12 + 27] = model_input[i + 27 + 12] + return input_pose \ No newline at end of file diff --git a/src/utils/preprocess.py b/src/utils/preprocess.py new file mode 100644 index 0000000..ae6bb21 --- /dev/null +++ b/src/utils/preprocess.py @@ -0,0 +1,114 @@ +from PIL import Image +import numpy as np +from scipy.interpolate import interp1d + + +def resize_to_512_center(img, fill_color=(0, 0, 0, 0)): + + # 计算缩放比例 + original_width, original_height = img.size + ratio = min(512 / original_width, 512 / original_height) + + # 等比缩放 + new_size = (int(original_width * ratio), int(original_height * ratio)) + resized_img = img.resize(new_size, Image.LANCZOS) # 高质量缩放算法 + + # 创建新画布 + canvas = Image.new(img.mode, (512, 512), fill_color) + + # 计算粘贴位置(居中) + paste_position = ( + (512 - new_size[0]) // 2, + (512 - new_size[1]) // 2 + ) + + # 合并图像(保持透明度) + canvas.paste(resized_img, paste_position) + + + return canvas + +def apply_color_curves(img, curves): + """ + 对图像应用颜色曲线调整 + :param input_path: 输入图像路径 + :param output_path: 输出图像路径 + :param curves: 曲线配置字典,格式为: + { + 'rgb': [(x1, y1), (x2, y2), ...], # 全局通道曲线 + 'r': [(x1, y1), (x2, y2), ...], # 红色通道曲线 + 'g': [(x1, y1), (x2, y2), ...], # 绿色通道曲线 + 'b': [(x1, y1), (x2, y2), ...] # 蓝色通道曲线 + } + """ + if img.mode == 'P': + img = img.convert('RGB') + + # 分离通道 + channels = list(img.split()) + + # 处理每个颜色通道 + for ch_idx, ch_name in enumerate(['r', 'g', 'b','a']): + # 合并全局和通道特定曲线 + combined_points = [] + if 'rgb' in curves and not ch_name=='a': + combined_points.extend(curves['rgb']) + if ch_name in curves: + combined_points.extend(curves[ch_name]) + + if not combined_points: + continue + + # 生成LUT + points = sorted(combined_points, key=lambda x: x[0]) + xs, ys = zip(*points) + + # 自动选择插值方式 + interpolation_kind = 'quadratic' if len(points) >= 3 else 'linear' + interpolator = interp1d( + xs, ys, + kind=interpolation_kind, + bounds_error=False, + fill_value=(ys[0], ys[-1]) + ) + + # 创建查找表 + x_range = np.arange(256) + y_new = interpolator(x_range) + y_new = np.clip(y_new, 0, 255).astype(np.uint8) + lut = y_new.tolist() + + # 应用LUT + channels[ch_idx] = channels[ch_idx].point(lut) + + # 合并通道并保存 + merged_channels = channels + img_out = Image.merge(img.mode, merged_channels) + + return img_out + + + # 使用示例 +if __name__ == "__main__": + # S型曲线增强对比度 + curves = { + 'a': [ + (60, 0), + (127, 127), + (170, 255) + ] + } + + # 红色通道增强中间调 + # curves = { + # 'r': [ + # (0, 0), + # (127, 160), + # (255, 255) + # ] + # } + img = Image.open('input.png') + # img = apply_color_curves(img, curves) + img = resize_to_512_center(img) + + img.save('output.png') \ No newline at end of file diff --git a/src/utils/timer_wait.py b/src/utils/timer_wait.py new file mode 100644 index 0000000..e966548 --- /dev/null +++ b/src/utils/timer_wait.py @@ -0,0 +1,57 @@ +import ctypes +import time + +ntdll = ctypes.WinDLL("ntdll") + +desired = ctypes.c_ulong(5000) # 0.5 ms = 5000 * 100ns +current = ctypes.c_ulong() + +ntdll.NtSetTimerResolution(desired, True, ctypes.byref(current)) # Set timer resolution to 0.5ms + +# ------------------------------- +# 高精度等待函数 +# ------------------------------- +def wait_until( + target_time: float, + spin_threshold: float = 0.0005, + sleep_min: float = 0.001, +): + """ + 等待直到 perf_counter() >= target_time + + :param target_time: 目标时间(time.perf_counter() 的绝对时间,秒) + :param spin_threshold: 最后 busy-wait 窗口(秒,默认 0.5ms) + :param sleep_min: 允许 sleep 的最小时间(避免 0 sleep) + """ + + # 防止负时间 / NaN + if not isinstance(target_time, (int, float)): + return + + while True: + now = time.perf_counter() + remaining = target_time - now + + # 已经过点 + if remaining <= 0: + return + + # 进入 busy-wait 区间 + if remaining <= spin_threshold: + break + + # 可 sleep 区间 + sleep_time = remaining - spin_threshold + + # 防止 sleep(0) 或极小值 + if sleep_time >= sleep_min: + time.sleep(sleep_time) + else: + # 太小了,直接进入自旋 + break + + # -------- busy-wait 精对齐 -------- + # 使用局部变量减少属性查找开销 + perf = time.perf_counter + while perf() < target_time: + pass diff --git a/src/utils/utils.py b/src/utils/utils.py new file mode 100644 index 0000000..23773d3 --- /dev/null +++ b/src/utils/utils.py @@ -0,0 +1,79 @@ +import numpy as np + + +def linear_rgb2srgb(image): + """ + convert linear rgb image to srgb image + Args: + image (numpy array): linear rgb color space image + + Returns: + image (numpy array): srgb color space image + """ + return np.where(image <= 0.04045, image / 12.92, ((image + 0.055) / 1.055) ** 2.4) + + +def srgb2linear_rgb(image): + """ + convert srgb image to linear rgb image + Args: + image (numpy array): srgb color space image + + Returns: + image (numpy array): linear rgb color space image + """ + return np.where(image <= 0.003130804953560372, image * 12.92, 1.055 * (image ** (1.0 / 2.4)) - 0.055) + + +def preprocessing_image(image): + """ + convert image(numpy array, unit8) to tensor + Args: + image (numpy array): character image (256x256, alpha channel included) + Returns: + tensor + """ + np_image = np.array(image) / 255 + clipped_image = np.clip(np_image, 0, 1) + srgb_image = linear_rgb2srgb(clipped_image) + h, w, c = srgb_image.shape + linear_image = srgb_image.reshape(h * w, c) + for pixel in linear_image: + if pixel[3] == 0.0: + pixel[0:3] = 0.0 + reshaped_image = linear_image.transpose().reshape(c, h, w) + return reshaped_image + + +def postprocessing_image(tensor): + """ + convert tensor to image(numpy array, unit8) + Args: + tensor + Returns: + image (numpy array): character image (256x256, alpha channel included) + """ + tensor = tensor.detach().squeeze(0) + reshaped_tensor = tensor.permute(1, 2, 0) + np_image = reshaped_tensor.numpy() + np_image = (np_image + 1) / 2 + srgb_image = np_image[..., :3] + alpha_image = np_image[..., 3] + clipped_image = np.clip(srgb_image, 0, 1) + rgb_image = srgb2linear_rgb(clipped_image) + rgba_image = np.concatenate([rgb_image, alpha_image[..., np.newaxis]], axis=2) + rgba_image = rgba_image * 255 + return rgba_image.astype(np.uint8) + + +def get_distance(a, b): + """ + calculate euclidean distance a to b + Args: + a (landmark): 3d points + b (landmark): 3d points + + Returns: + L2 distance (float) + """ + return np.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) From 8601645bb0b49fda3d640bd52f2034869192e91a Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Sun, 28 Dec 2025 02:01:19 -0500 Subject: [PATCH 22/46] feat:debug output handle --- src/args.py | 5 +++-- src/face_mesh_client.py | 12 +++++------ src/main.py | 16 ++++++++++----- src/model_infer_client.py | 40 ++++++++++++++++++++++--------------- src/open_see_face_client.py | 15 ++++++++------ 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/args.py b/src/args.py index 6d2a18f..fe52506 100644 --- a/src/args.py +++ b/src/args.py @@ -80,8 +80,9 @@ if not args.cam_input and args.mouse_input is None \ if args.use_sr: args.anime4k = False # Disable anime4k if SR is enabled -if args.anime4k: - args.use_sr = False # Disable SR if anime4k is enabled + +if args.output_spout2: + args.alpha_split = False # Disable alpha split for spout2 output args.model_output_size = 1024 if args.use_sr or args.anime4k else 512 diff --git a/src/face_mesh_client.py b/src/face_mesh_client.py index 387400f..5b67cfa 100644 --- a/src/face_mesh_client.py +++ b/src/face_mesh_client.py @@ -7,6 +7,8 @@ from .utils.channel_shared_mem import SharedMemoryExclusiveChannel from .utils.pose import get_pose from .utils.fps import FPS from .utils.filter import OneEuroFilterNumpy +from OneEuroFilter import OneEuroFilter +import time class FaceMeshClientProcess(Process): @@ -38,7 +40,6 @@ class FaceMeshClientProcess(Process): pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) - position_offset = None print("Webcam Input Running at {:.2f} FPS".format(input_fps.view())) position_vector = np.array([0, 0, 0, 1], dtype=np.float32) @@ -66,14 +67,13 @@ class FaceMeshClientProcess(Process): mouth_eye_vector = [0.0] * 27 pose_vector = [0.0] * 6 - mouth_eye_vector[2] = (eye_l_h_temp + eye_r_h_temp) / 2.0 - mouth_eye_vector[3] = (eye_l_h_temp + eye_r_h_temp) / 2.0 + mouth_eye_vector[2] = min(eye_l_h_temp, eye_r_h_temp) + mouth_eye_vector[3] = min(eye_l_h_temp, eye_r_h_temp) mouth_eye_vector[14] = mouth_ratio * 1.5 - mouth_eye_vector[25] = (eye_y_ratio + eye_x_ratio) / 2.0 - mouth_eye_vector[26] = (eye_y_ratio + eye_x_ratio) / 2.0 - + mouth_eye_vector[25] = 0.0 # keep iris stable for user demo + mouth_eye_vector[26] = 0.0 if position_offset is None: position_offset = [(x_angle - 1.5) * 1.6, y_angle * 2.0 , (z_angle + 1.5) * 2] pose_vector[0] = (x_angle - 1.5) * 1.6 - position_offset[0] diff --git a/src/main.py b/src/main.py index b775cde..d6f28f8 100644 --- a/src/main.py +++ b/src/main.py @@ -63,14 +63,16 @@ def main(): infer_process.daemon = True infer_process.start() + cam_width_scale = 2 if args.alpha_split else 1 + ret_channels = 3 if args.output_virtual_cam or args.output_debug else 4 ret_batch_shm_channels = [ SharedMemoryExclusiveChannel(infer_process.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") for i in range(args.interpolation_scale) ] np_ret_shms = [ - np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, - buffer=infer_process.ret_shared_mem.buf[i * args.model_output_size * args.model_output_size * 4: - (i + 1) * args.model_output_size * args.model_output_size * 4]) + np.ndarray((args.model_output_size, cam_width_scale * args.model_output_size, ret_channels), dtype=np.uint8, + buffer=infer_process.ret_shared_mem.buf[i * cam_width_scale * args.model_output_size * args.model_output_size * ret_channels: + (i + 1) * cam_width_scale * args.model_output_size * args.model_output_size * ret_channels]) for i in range(args.interpolation_scale) ] @@ -89,9 +91,13 @@ def main(): for i in range(args.interpolation_scale): wait_until(last_time + interval) last_time += interval - cv2.imshow("Output Frame Batch {}".format(i), np_ret_shms[i]) + + if args.output_virtual_cam: + pass + else: + cv2.imshow("Output Frame Batch {}".format(i), np_ret_shms[i]) + cv2.waitKey(1) ret_batch_shm_channels[i].release() - cv2.waitKey(1) print("pipeline FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, current time {:.5f}".format( infer_process.pipeline_fps_number.value, input_fps.value, diff --git a/src/model_infer_client.py b/src/model_infer_client.py index cddfe06..4d2a6c0 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -17,8 +17,10 @@ class ModelClientProcess(Process): self.input_image = input_image self.pose_position_shm = pose_position_shm # 45 floats for pose, 4 floats for position - self.ret_shape = (args.interpolation_scale, args.model_output_size, args.model_output_size, 4) - self.ret_nbytes = args.interpolation_scale * args.model_output_size * args.model_output_size * 4 # RGBA + self.alpha_width_scale = 2 if args.alpha_split else 1 + self.ret_channels = 3 if args.output_virtual_cam or args.output_debug else 4 + self.ret_shape = (args.interpolation_scale, args.model_output_size, self.alpha_width_scale * args.model_output_size, self.ret_channels) + self.ret_nbytes = self.alpha_width_scale * args.interpolation_scale * args.model_output_size * args.model_output_size * self.ret_channels # RGBA self.ret_shared_mem = shared_memory.SharedMemory(create=True, size=self.ret_nbytes) self.last_model_interval = Value('f', 0.0) @@ -40,9 +42,9 @@ class ModelClientProcess(Process): for i in range(args.interpolation_scale) ] np_ret_shms = [ - np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, - buffer=self.ret_shared_mem.buf[i * args.model_output_size * args.model_output_size * 4: - (i + 1) * args.model_output_size * args.model_output_size * 4]) + np.ndarray((args.model_output_size, self.alpha_width_scale * args.model_output_size, self.ret_channels), dtype=np.uint8, + buffer=self.ret_shared_mem.buf[i * self.alpha_width_scale * args.model_output_size * args.model_output_size * self.ret_channels: + (i + 1) * self.alpha_width_scale * args.model_output_size * args.model_output_size * self.ret_channels]) for i in range(args.interpolation_scale) ] @@ -151,7 +153,7 @@ class ModelClientProcess(Process): rm, (bgra_image.shape[1], bgra_image.shape[0])) - if args.debug_input: + if args.output_debug: cv2.putText(bgra_image, str('OUT_FPS:%.1f' % self.pipeline_fps_number.value), (0, 16), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) cv2.putText(bgra_image, str( @@ -180,19 +182,25 @@ class ModelClientProcess(Process): rgb_channels = np.ascontiguousarray(rgba_image[:,:,:3]) if args.anime4k: - rgb_output = self.a4k_processor(rgb_channels) - alpha_output = cv2.resize(alpha_channel, None, fx=2, fy=2) - rgba_image = cv2.merge((rgb_output, alpha_output)) + rgb_channels = self.a4k_processor(rgb_channels) + alpha_channel = cv2.resize(alpha_channel, None, fx=2, fy=2) + rgba_image = cv2.merge((rgb_channels, alpha_channel)) if args.alpha_split: alpha_image = cv2.cvtColor(alpha_channel, cv2.COLOR_GRAY2RGB) - rgba_image = cv2.hconcat([rgba_image, alpha_image]) + rgb_channels = cv2.hconcat([rgb_channels, alpha_image]) - - if args.debug_input: - if args.anime4k or args.alpha_split: - bgra_image = cv2.cvtColor(rgba_image, cv2.COLOR_RGBA2BGRA) - ret.append(bgra_image) - else: + if args.output_debug: + if args.alpha_split: + bgr_channels = cv2.cvtColor(rgb_channels, cv2.COLOR_RGB2BGR) + else: + if args.anime4k: + bgr_channels = cv2.cvtColor(rgba_image, cv2.COLOR_RGBA2BGR) + else: + bgr_channels = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2BGR) + ret.append(bgr_channels) + elif args.output_virtual_cam: + ret.append(rgb_channels) + else: ret.append(rgba_image) return ret diff --git a/src/open_see_face_client.py b/src/open_see_face_client.py index 7b27ea5..18a70bb 100644 --- a/src/open_see_face_client.py +++ b/src/open_see_face_client.py @@ -6,13 +6,15 @@ import numpy as np from .utils.channel_shared_mem import SharedMemoryExclusiveChannel from .utils.fps import FPS from .utils.filter import OneEuroFilterNumpy +from OneEuroFilter import OneEuroFilter +import time class OSFClientProcess(Process): def __init__(self, pose_position_shm: shared_memory.SharedMemory): super().__init__() self.pose_position_shm = pose_position_shm - self.address = args.osf.split(':')[0] - self.port = int(args.osf.split(':')[1]) + self.address = args.osf_input.split(':')[0] + self.port = int(args.osf_input.split(':')[1]) self.fps = Value('f', 0.0) def run(self): @@ -40,7 +42,8 @@ class OSFClientProcess(Process): pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) - + iris_x_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.1, beta=0.3) # extra filter for iris movement + iris_y_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.1, beta=0.3) # extra filter for iris movement rotation_offset = None print("OpenSeeFace Input Running at {:.2f} FPS".format(input_fps.view())) while True: @@ -132,11 +135,11 @@ class OSFClientProcess(Process): mouth_eye_vector[2] = 1 - data['leftEyeOpen'] mouth_eye_vector[3] = 1 - data['rightEyeOpen'] - mouth_eye_vector[14] = max(data['MouthOpen'], 0) * 2 # Open larger mouth + mouth_eye_vector[14] = max(data['MouthOpen'], 0) * 1.5 # Open larger mouth # print(mouth_eye_vector[14]) - mouth_eye_vector[25] = -data['eyeRotationY'] * 3 - (data['rotationX']) / 57.3 * 1.5 - mouth_eye_vector[26] = data['eyeRotationX'] * 3 + (data['rotationY']) / 57.3 + mouth_eye_vector[25] = iris_x_filter(-data['eyeRotationY'] * 3 - (data['rotationX']) / 57.3 * 1.5, timestamp=time.perf_counter()) + mouth_eye_vector[26] = iris_y_filter(data['eyeRotationX'] * 3 + (data['rotationY']) / 57.3, timestamp=time.perf_counter()) # print(mouth_eye_vector[25:27]) eyebrow_vector[6] = data['EyebrowUpDownLeft'] eyebrow_vector[7] = data['EyebrowUpDownRight'] From c537839e6c7031b3e044fef9ac5d934d72371d98 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Sun, 28 Dec 2025 02:47:48 -0500 Subject: [PATCH 23/46] feat:obs virtual cam output ready --- src/main.py | 20 +++++++++++++++----- src/model_infer_client.py | 4 ++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main.py b/src/main.py index d6f28f8..381534d 100644 --- a/src/main.py +++ b/src/main.py @@ -8,7 +8,8 @@ from .utils.channel_shared_mem import SharedMemoryExclusiveChannel from multiprocessing import shared_memory from .utils.timer_wait import wait_until from PIL import Image - +from .utils.fps import FPS +import pyvirtualcam def main(): @@ -79,7 +80,16 @@ def main(): last_time : float = time.perf_counter() interval : float = 1.0 / args.frame_rate_limit if args.frame_rate_limit > 0 else 0.0 - from .utils.fps import FPS + if args.output_virtual_cam: + virtual_cam = pyvirtualcam.Camera(width=cam_width_scale * args.model_output_size, + height=args.model_output_size, + fps=args.frame_rate_limit, + backend='obs', + fmt=pyvirtualcam.PixelFormat.RGB) + print(f'Using virtual camera: {virtual_cam.device}') + else: + print("Using OpenCV windows for output display.") + pipeline_fps = FPS() print("Interval set to {:.3f} seconds".format(interval)) @@ -93,7 +103,7 @@ def main(): last_time += interval if args.output_virtual_cam: - pass + virtual_cam.send(np_ret_shms[i]) else: cv2.imshow("Output Frame Batch {}".format(i), np_ret_shms[i]) cv2.waitKey(1) @@ -102,8 +112,8 @@ def main(): infer_process.pipeline_fps_number.value, input_fps.value, infer_process.average_model_interval.value * 1000, - infer_process.cache_hit_ratio.value, - infer_process.gpu_cache_hit_ratio.value, + infer_process.cache_hit_ratio.value * 100, + infer_process.gpu_cache_hit_ratio.value * 100, pipeline_fps() ), end ='\r', flush=True) diff --git a/src/model_infer_client.py b/src/model_infer_client.py index 4d2a6c0..74ccda1 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -199,6 +199,10 @@ class ModelClientProcess(Process): bgr_channels = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2BGR) ret.append(bgr_channels) elif args.output_virtual_cam: + if rgb_channels is None: + rgb_channels = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGB) + if args.anime4k and not args.alpha_split: + rgb_channels = cv2.cvtColor(rgba_image, cv2.COLOR_RGBA2RGB) ret.append(rgb_channels) else: ret.append(rgba_image) From ebe0073ecafc8226d4ba59e8c419fbc91ea8621d Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Sun, 28 Dec 2025 17:53:21 -0500 Subject: [PATCH 24/46] feat:put a4k to rt, all three outputs are working --- ezvtuber-rt | 2 +- src/args.py | 12 ++++++------ src/ezvtb_rt_interface.py | 2 ++ src/main.py | 13 ++++++++++--- src/model_infer_client.py | 39 +++++++++---------------------------- src/open_see_face_client.py | 2 +- 6 files changed, 29 insertions(+), 41 deletions(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index b59e1ba..1bc5fee 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit b59e1bab05b5c007a09690f83bf656d9c77a420d +Subproject commit 1bc5feeb09a12915fd86b6c125aee256b48676bf diff --git a/src/args.py b/src/args.py index fe52506..3f32426 100644 --- a/src/args.py +++ b/src/args.py @@ -29,8 +29,8 @@ parser.add_argument('--alpha_split', action='store_true') parser.add_argument('--bongo', action='store_true') parser.add_argument('--extend_movement', action='store_true') -parser.add_argument('--filter_min_cutoff', type=float, default=1.0) -parser.add_argument('--filter_beta', type=float, default=0.0) +parser.add_argument('--filter_min_cutoff', type=float, default=0.3) +parser.add_argument('--filter_beta', type=float, default=1.0) parser.add_argument('--simplify', type=int, default=1) @@ -49,7 +49,7 @@ parser.add_argument('--interpolation_half', action='store_true') parser.add_argument('--use_sr', action='store_true') parser.add_argument('--sr_x4', action='store_true') parser.add_argument('--sr_half', action='store_true') -parser.add_argument('--anime4k', action='store_true') +parser.add_argument('--sr_a4k', action='store_true') parser.add_argument('--cache', type=str, default='256mb') @@ -78,12 +78,12 @@ if not args.cam_input and args.mouse_input is None \ and args.ifm_input is None and args.osf_input is None: args.debug_input = True # Default to debug input -if args.use_sr: - args.anime4k = False # Disable anime4k if SR is enabled +if args.sr_a4k: + args.use_sr = True if args.output_spout2: args.alpha_split = False # Disable alpha split for spout2 output -args.model_output_size = 1024 if args.use_sr or args.anime4k else 512 +args.model_output_size = 1024 if args.use_sr else 512 print("Arguments:", args) \ No newline at end of file diff --git a/src/ezvtb_rt_interface.py b/src/ezvtb_rt_interface.py index 9d05176..b68ab2b 100644 --- a/src/ezvtb_rt_interface.py +++ b/src/ezvtb_rt_interface.py @@ -38,6 +38,7 @@ def get_core( use_sr:bool = False, sr_x4:bool = True, sr_half:bool = True, + sr_a4k:bool = False, ): if use_tensorrt: try: @@ -60,6 +61,7 @@ def get_core( sr_model_enable=use_sr, sr_model_scale=4 if sr_x4 else 2, sr_model_fp16=sr_half, + sr_a4k=sr_a4k, vram_cache_size=model_cache_size, cache_max_giga=cacher_ram_size, diff --git a/src/main.py b/src/main.py index 381534d..1375c5a 100644 --- a/src/main.py +++ b/src/main.py @@ -10,6 +10,7 @@ from .utils.timer_wait import wait_until from PIL import Image from .utils.fps import FPS import pyvirtualcam +from OpenGL.GL import GL_RGBA def main(): @@ -87,6 +88,10 @@ def main(): backend='obs', fmt=pyvirtualcam.PixelFormat.RGB) print(f'Using virtual camera: {virtual_cam.device}') + elif args.output_spout2: + from PySpout import SpoutSender + spout_sender = SpoutSender("EasyVtuber", cam_width_scale * args.model_output_size, + args.model_output_size, GL_RGBA) else: print("Using OpenCV windows for output display.") @@ -99,14 +104,16 @@ def main(): for i in range(args.interpolation_scale): ret_batch_shm_channels[i].acquire() for i in range(args.interpolation_scale): - wait_until(last_time + interval) - last_time += interval - if args.output_virtual_cam: virtual_cam.send(np_ret_shms[i]) + elif args.output_spout2: + spout_sender.send_image(np_ret_shms[i], False) else: cv2.imshow("Output Frame Batch {}".format(i), np_ret_shms[i]) cv2.waitKey(1) + + wait_until(last_time + interval) + last_time += interval ret_batch_shm_channels[i].release() print("pipeline FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, current time {:.5f}".format( infer_process.pipeline_fps_number.value, diff --git a/src/model_infer_client.py b/src/model_infer_client.py index 74ccda1..9eb5385 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -9,7 +9,6 @@ from .utils.channel_shared_mem import SharedMemoryExclusiveChannel from .utils.pose_simplify import pose_simplify from .utils.fps import FPS, Interval from typing import List -import pyanime4k class ModelClientProcess(Process): def __init__(self, input_image, pose_position_shm: shared_memory.SharedMemory , input_fps): @@ -48,14 +47,6 @@ class ModelClientProcess(Process): for i in range(args.interpolation_scale) ] - if args.anime4k: - self.a4k_processor = pyanime4k.Processor( - processor_type="opencl", - device=0, - model="acnet-gan" - ) - print("Anime4K Loaded") - model_infer_average_interval: Interval = Interval() pipeline_fps = FPS() @@ -77,7 +68,9 @@ class ModelClientProcess(Process): use_sr=args.use_sr, sr_half=args.sr_half, - sr_x4=args.sr_x4) + sr_x4=args.sr_x4, + sr_a4k=args.sr_a4k, + ) model.setImage(self.input_image) model_infer_average_interval.start() model.inference([np.zeros((1, 45), dtype=np.float32)]) # Warm up @@ -172,20 +165,10 @@ class ModelClientProcess(Process): (0, 80), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - rgba_image = None - alpha_channel = None - rgb_channels = None - - if args.anime4k or args.alpha_split: - rgba_image = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGBA) - alpha_channel = np.ascontiguousarray(rgba_image[:, :, 3]) - rgb_channels = np.ascontiguousarray(rgba_image[:,:,:3]) - - if args.anime4k: - rgb_channels = self.a4k_processor(rgb_channels) - alpha_channel = cv2.resize(alpha_channel, None, fx=2, fy=2) - rgba_image = cv2.merge((rgb_channels, alpha_channel)) if args.alpha_split: + rgba_image = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGBA) + alpha_channel = rgba_image[:, :, 3] + rgb_channels = rgba_image[:,:,:3] alpha_image = cv2.cvtColor(alpha_channel, cv2.COLOR_GRAY2RGB) rgb_channels = cv2.hconcat([rgb_channels, alpha_image]) @@ -193,18 +176,14 @@ class ModelClientProcess(Process): if args.alpha_split: bgr_channels = cv2.cvtColor(rgb_channels, cv2.COLOR_RGB2BGR) else: - if args.anime4k: - bgr_channels = cv2.cvtColor(rgba_image, cv2.COLOR_RGBA2BGR) - else: - bgr_channels = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2BGR) + bgr_channels = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2BGR) ret.append(bgr_channels) elif args.output_virtual_cam: - if rgb_channels is None: + if not args.alpha_split: rgb_channels = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGB) - if args.anime4k and not args.alpha_split: - rgb_channels = cv2.cvtColor(rgba_image, cv2.COLOR_RGBA2RGB) ret.append(rgb_channels) else: + rgba_image = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGBA) ret.append(rgba_image) return ret diff --git a/src/open_see_face_client.py b/src/open_see_face_client.py index 18a70bb..68e3070 100644 --- a/src/open_see_face_client.py +++ b/src/open_see_face_client.py @@ -135,7 +135,7 @@ class OSFClientProcess(Process): mouth_eye_vector[2] = 1 - data['leftEyeOpen'] mouth_eye_vector[3] = 1 - data['rightEyeOpen'] - mouth_eye_vector[14] = max(data['MouthOpen'], 0) * 1.5 # Open larger mouth + mouth_eye_vector[14] = max(data['MouthOpen'], 0) * 2 # Open larger mouth # print(mouth_eye_vector[14]) mouth_eye_vector[25] = iris_x_filter(-data['eyeRotationY'] * 3 - (data['rotationX']) / 57.3 * 1.5, timestamp=time.perf_counter()) From 638d6970f77f15f8d0dc85dadbf2663f38dd8f0d Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Mon, 29 Dec 2025 01:54:12 -0500 Subject: [PATCH 25/46] feat:launcher ready --- .gitignore | 2 +- args.py | 85 -- ezvtb_rt_interface.py | 198 ---- ezvtuber-rt | 2 +- launcher2.py | 87 +- main.py | 1019 ----------------- pose.py | 183 --- preprocess.py | 114 -- requirements.txt | 11 +- src/args.py | 6 +- src/debug_input_client.py | 14 +- src/face_mesh_client.py | 8 +- src/i_facial_mocap_client.py | 18 +- src/main.py | 16 +- src/model_infer_client.py | 12 +- src/mouse_client.py | 10 +- src/open_see_face_client.py | 10 +- .../utils/facial_points.py | 0 src/utils/pose.py | 6 +- ...nnel_shared_mem.py => shared_mem_guard.py} | 2 +- utils.py | 80 -- 21 files changed, 84 insertions(+), 1799 deletions(-) delete mode 100644 args.py delete mode 100644 ezvtb_rt_interface.py delete mode 100644 main.py delete mode 100644 pose.py delete mode 100644 preprocess.py rename facial_points.py => src/utils/facial_points.py (100%) rename src/utils/{channel_shared_mem.py => shared_mem_guard.py} (99%) delete mode 100644 utils.py diff --git a/.gitignore b/.gitignore index b983db8..3f5ecf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ pretrained/** !pretrained/PUT_MODEL_HERE character -src +!src !tha4/src tha4/distiller-ui-doc tha4/docs diff --git a/args.py b/args.py deleted file mode 100644 index ab6ae1e..0000000 --- a/args.py +++ /dev/null @@ -1,85 +0,0 @@ -import argparse -import re - - -def convert_to_byte(size): - result = re.search('(\d+\.?\d*)(b|kb|mb|gb|tb)', size.lower()) - if (result and result.groups()): - unit = result.groups()[1] - amount = float(result.groups()[0]) - index = ['b', 'kb', 'mb', 'gb', 'tb'].index(unit) - return amount * pow(1024, index) - raise ValueError("Invalid size provided, value is " + size) - - -parser = argparse.ArgumentParser() -parser.add_argument('--debug', action='store_true') -parser.add_argument('--eyebrow', action='store_true') -parser.add_argument('--extend_movement', type=float) -parser.add_argument('--input', type=str, default='cam') -parser.add_argument('--character', type=str, default='y') -parser.add_argument('--output_dir', type=str) -parser.add_argument('--output_webcam', type=str) -parser.add_argument('--output_size', type=str, default='512x512') -parser.add_argument('--model', type=str, default='standard_float') -parser.add_argument('--debug_input', action='store_true') -parser.add_argument('--mouse_input', type=str) -parser.add_argument('--perf', type=str) -parser.add_argument('--skip_model', action='store_true') -parser.add_argument('--ifm', type=str) -parser.add_argument('--osf', type=str) -parser.add_argument('--anime4k', action='store_true') -parser.add_argument('--alpha_split', action='store_true') -parser.add_argument('--bongo', action='store_true') -parser.add_argument('--cache', type=str, default='256mb') -parser.add_argument('--gpu_cache', type=str, default='512mb') -parser.add_argument('--simplify', type=int, default=1) - -parser.add_argument('--device_id', type=int, default=0) -parser.add_argument('--use_tensorrt', action='store_true') -parser.add_argument('--use_tha4', action='store_true') -parser.add_argument('--use_tha4_student', action='store_true') -parser.add_argument('--tha4_student_model', type=str, default=None) - -parser.add_argument('--model_seperable', action='store_true') -parser.add_argument('--model_half', action='store_true') -parser.add_argument('--model_cache', action='store_true') -parser.add_argument('--model_vram_cache', action='store_true') -# parser.add_argument('--model_cache_size', type=float, default=1.0) - -parser.add_argument('--use_interpolation', action='store_true') -parser.add_argument('--interpolation_scale', type=int, default=3) -parser.add_argument('--interpolation_half', action='store_true') - -parser.add_argument('--use_cacher', action='store_true') -parser.add_argument('--cacher_quality', type=int, default=85) -# parser.add_argument('--cacher_ram_size', type=float, default=1.0) - -parser.add_argument('--use_sr', action='store_true') -parser.add_argument('--sr_x4', action='store_true') -parser.add_argument('--sr_half', action='store_true') -parser.add_argument('--sr_noise', type=int, default=1) - -parser.add_argument('--frame_rate_limit', type=int, default=30) - -parser.add_argument('--alpha_clean', action='store_true') - -args = parser.parse_args() -# args.output_w = int(args.output_size.split('x')[0]) -# args.output_h = int(args.output_size.split('x')[1]) -if args.cache is not None: - args.max_cache_len = convert_to_byte(args.cache) / pow(1024, 3) # In gigabytes -else: - args.max_cache_len = 0 -if args.max_cache_len == 0: - args.use_cacher = False -if args.gpu_cache is not None: - args.max_gpu_cache_len = convert_to_byte(args.gpu_cache) / pow(1024, 3) # In gigabytes -else: - args.max_gpu_cache_len = 0 -if args.output_webcam is None and args.output_dir is None: args.debug = True - -args.model_output_size = 1024 if args.use_sr else 512 - -if args.use_sr and args.use_interpolation: - args.use_interpolation = False diff --git a/ezvtb_rt_interface.py b/ezvtb_rt_interface.py deleted file mode 100644 index 25d2aa8..0000000 --- a/ezvtb_rt_interface.py +++ /dev/null @@ -1,198 +0,0 @@ -import os -import sys -import subprocess -import urllib.request -import shutil - -dir_path = os.path.dirname(os.path.realpath(__file__)) -ezvtb_path = os.path.join(dir_path, 'ezvtuber-rt') -ezvtb_main_path = os.path.join(dir_path, 'ezvtuber-rt-main') - -project_path = '' -if os.path.exists(ezvtb_path): - project_path = ezvtb_path -else: - project_path = ezvtb_main_path - -if project_path not in sys.path: - sys.path.append(project_path) - -# Initialize model data path to point to data/models directory -import ezvtb_rt -models_path = os.path.join(dir_path, 'data', 'models') -ezvtb_rt.init_model_path(models_path) - -def get_core( - #Device setting - device_id:int = 0, - use_tensorrt:bool = True, - #THA3 model setting - model_seperable:bool = True, - model_half:bool = True, #If using directml+half, there is small numerical error on Nvidia, and huge numerical error on AMD - model_cache_size:float = 1.0, #unit of GigaBytes, only works for tensorrt - model_use_eyebrow:bool = True, - #RIFE interpolation setting - use_interpolation:bool = True, - interpolation_scale:int = 2, - interpolation_half:bool = True, #If using directml+half, there is small numerical error on Nvidia, and huge numerical error on AMD - #Cacher setting - cacher_quality:int = 85, - cacher_ram_size:float = 2.0,#unit of GigaBytes - #SR setting - use_sr:bool = False, - sr_x4:bool = True, - sr_half:bool = True, - sr_noise:int = 1, - #THA4 model setting (NEW) - use_tha4: bool = False, # Use THA4 instead of THA3 - use_tha4_student: bool = False, # Use THA4 Student model - tha4_student_model: str = None # Student model name - ): - support_trt = False - if use_tensorrt: - print('Verifying TensorRT') - try: - from ezvtb_rt.trt_utils import cudaSetDevice - cudaSetDevice(device_id) - support_trt = True - except Exception: - support_trt = False - if not support_trt and use_tensorrt: - print('TensorRT option selected but not supported') - use_tensorrt = False - - if use_tensorrt: - os.environ['CUDA_DEVICE'] = str(device_id) - import pycuda.autoinit - print(f'Using device {pycuda.autoinit.device.name()}') - - # Determine model directory based on THA version - if use_tha4_student and tha4_student_model: - # THA4 Student: Use data/models/custom_tha4_models/{model_name} - tha_model_dir = os.path.join( - '.', 'data', 'models', 'custom_tha4_models', - tha4_student_model) - print(f'THA4 Student Mode - Model: {tha4_student_model}') - print(f'THA4 Student Mode - Model Path: {tha_model_dir}') - elif use_tha4: - # THA4: Use data/models/tha4 directory with precision selection - tha_model_dir = os.path.join( - '.', 'data', 'models', 'tha4', - 'fp16' if model_half else 'fp32') - print(f'THA4 Mode - Model Path: {tha_model_dir}') - else: - # THA3: Use data/models/tha3 directory - tha_model_dir = os.path.join( - '.', 'data', 'models', 'tha3', - 'seperable' if model_seperable else 'standard', - 'fp16' if model_half else 'fp32') - print(f'THA3 Mode - Model Path: {tha_model_dir}') - - rife_model_path = '' - if use_interpolation: - rife_model_path = os.path.join( - '.', 'data', 'models', 'rife_512', - f'x{interpolation_scale}', - 'fp16' if interpolation_half else 'fp32') - - sr_model_path = '' - if use_sr: - if sr_x4: - if sr_half: - sr_model_path = os.path.join( - '.', 'data', 'models', 'Real-ESRGAN', - 'exported_256_fp16') - else: - sr_model_path = os.path.join( - '.', 'data', 'models', 'Real-ESRGAN', - 'exported_256') - else: - if sr_half: - sr_model_path = os.path.join( - '.', 'data', 'models', 'waifu2x_upconv', - 'fp16', 'upconv_7', 'art', - f'noise{sr_noise}_scale2x') - else: - sr_model_path = os.path.join( - '.', 'data', 'models', 'waifu2x_upconv', - 'fp32', 'upconv_7', 'art', - f'noise{sr_noise}_scale2x') - - print(f'RIFE Path: {rife_model_path}') - print(f'SR Path: {sr_model_path}') - - core = None - if use_tensorrt: - from ezvtb_rt.core import CoreTRT - if use_tha4_student and tha4_student_model: - core = CoreTRT( - tha_model_version='v4_student', - tha_model_name=tha4_student_model, - rife_model_enable=use_interpolation, - rife_model_scale=interpolation_scale, - rife_model_fp16=interpolation_half, - sr_model_enable=use_sr, - sr_model_scale=4 if sr_x4 else 2, - sr_model_noise=sr_noise, - sr_model_fp16=sr_half, - vram_cache_size=model_cache_size, - cache_max_giga=cacher_ram_size, - use_eyebrow=model_use_eyebrow) - else: - core = CoreTRT( - tha_model_version='v4' if use_tha4 else 'v3', - tha_model_seperable=model_seperable, - tha_model_fp16=model_half, - rife_model_enable=use_interpolation, - rife_model_scale=interpolation_scale, - rife_model_fp16=interpolation_half, - sr_model_enable=use_sr, - sr_model_scale=4 if sr_x4 else 2, - sr_model_noise=sr_noise, - sr_model_fp16=sr_half, - vram_cache_size=model_cache_size, - cache_max_giga=cacher_ram_size, - use_eyebrow=model_use_eyebrow) - else: - from ezvtb_rt.core_ort import CoreORT - if use_tha4_student and tha4_student_model: - core = CoreORT( - tha_model_version='v4_student', - tha_model_name=tha4_student_model, - rife_model_enable=use_interpolation, - rife_model_scale=interpolation_scale, - rife_model_fp16=interpolation_half, - sr_model_enable=use_sr, - sr_model_scale=4 if sr_x4 else 2, - sr_model_noise=sr_noise, - sr_model_fp16=sr_half, - cache_max_giga=cacher_ram_size, - use_eyebrow=model_use_eyebrow) - else: - core = CoreORT( - tha_model_version='v4' if use_tha4 else 'v3', - tha_model_seperable=model_seperable, - tha_model_fp16=model_half, - rife_model_enable=use_interpolation, - rife_model_scale=interpolation_scale, - rife_model_fp16=interpolation_half, - sr_model_enable=use_sr, - sr_model_scale=4 if sr_x4 else 2, - sr_model_noise=sr_noise, - sr_model_fp16=sr_half, - cache_max_giga=cacher_ram_size, - use_eyebrow=model_use_eyebrow) - - return core - -if __name__ == '__main__': - from ezvtb_rt.trt_utils import check_build_all_models, cudaSetDevice - device_id = 0 - import sys - if len(sys.argv) > 1: - device_id = int(sys.argv[1]) - try: - cudaSetDevice(device_id) - check_build_all_models('./data/models') - except: - pass diff --git a/ezvtuber-rt b/ezvtuber-rt index 1bc5fee..2ea780d 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 1bc5feeb09a12915fd86b6c125aee256b48676bf +Subproject commit 2ea780d884afb9740a9c23e6754bc46031eb1e1f diff --git a/launcher2.py b/launcher2.py index 5715feb..24f0a49 100644 --- a/launcher2.py +++ b/launcher2.py @@ -39,14 +39,12 @@ default_arg = { 'is_alpha_clean': False, 'is_eyebrow': False, 'cache_simplify': 'High', - 'cache_compression': 'High', 'ram_cache_size': '2gb', 'vram_cache_size': '2gb', 'model_select': 'seperable_half', 'interpolation': 'x2_half', 'frame_rate_limit': '30', 'sr': 'Off', - 'device_id': '0', 'use_tensorrt': False, 'preset': 'Medium' } @@ -249,10 +247,9 @@ class LauncherPanel(wx.Panel): addOption('osf', title='OpenSeeFace IP:Port', desc='输入OpenSeeFace连接使用的IP:端口号', type=2) addOption('output', title='Output', desc='选择输出目标', - choices=['Spout2', 'OBS VirtualCam', 'UnityCapture', 'Debug Output'], - mapping=[3, 1, 0, 2]) + choices=['Spout2', 'OBS VirtualCam', 'Debug Output'], + mapping=[0, 1, 2]) - addOption('device_id', title='GPU Device', desc='选择需要使用的计算设备(默认0)', choices=['0', '1', '2', '3']) addOption('use_tensorrt', title='TensorRT', desc='运行过预构建脚本后开启TensorRT可进一步提升性能\n(未构建成功时不可用,仅NVIDIA显卡支持)', type=1) @@ -287,9 +284,6 @@ class LauncherPanel(wx.Panel): addOption('cache_simplify', title='Input Simplify', desc='设置输入简化级别\n输入越简化,缓存命中率越高,动作越不平滑', choices=['Off', 'Low', 'Medium', 'High', 'Higher', 'Highest', 'Gaming']) - addOption('cache_compression', title='JPEG Compression', - desc='设置内存缓存压缩等级\n压缩等级越高,缓存命中率越高,输出质量越差', - choices=['Off', 'Low', 'Medium', 'High']) addOption('sr', title='SuperResolution', desc='选择使用的超分模型\n由于性能原因,real-esrgan会进行裁切', choices=['Off', 'anime4k_x2', 'waifu2x_x2_half', 'real-esrgan_x4_half', 'waifu2x_x2_full', @@ -332,13 +326,12 @@ class LauncherPanel(wx.Panel): self.optionDict['ram_cache_size'], self.optionDict['vram_cache_size'], self.optionDict['cache_simplify'], - self.optionDict['cache_compression'], ] presets = { - 'Low': [0, 1, 1, 5, 3], - 'Medium': [1, 1, 1, 4, 2], - 'High': [1, 2, 2, 2, 2], - 'Ultra': [3, 3, 3, 1, 1] + 'Low': [0, 1, 1, 5], + 'Medium': [1, 1, 1, 4], + 'High': [1, 2, 2, 2], + 'Ultra': [3, 3, 3, 1] } if s == 'Custom': @@ -418,18 +411,17 @@ class LauncherPanel(wx.Panel): p = None self.btnLaunch.SetLabelText("Save & Launch") else: - run_args = [sys.executable, 'main.py'] + run_args = [sys.executable, '-m', 'src.main'] if len(args['character']): run_args.append('--character') run_args.append(args['character']) if args['input'] == 0: if len(args['ifm']): - run_args.append('--ifm') + run_args.append('--ifm_input') run_args.append(args['ifm']) elif args['input'] == 1: - run_args.append('--input') - run_args.append('cam') + run_args.append('--cam_input') elif args['input'] == 2: run_args.append('--debug_input') elif args['input'] == 3: @@ -437,38 +429,30 @@ class LauncherPanel(wx.Panel): run_args.append('0,0,' + str(wx.GetDisplaySize().width) + ',' + str(wx.GetDisplaySize().height)) elif args['input'] == 4: if len(args['osf']): - run_args.append('--osf') + run_args.append('--osf_input') run_args.append(args['osf']) if args['output'] == 0: - run_args.append('--output_webcam') - run_args.append('unitycapture') + run_args.append('--output_spout2') elif args['output'] == 1: - run_args.append('--output_webcam') - run_args.append('obs') - elif args['output'] == 3: - run_args.append('--output_webcam') - run_args.append('spout') + run_args.append('--output_virtual_cam') elif args['output'] == 2: - run_args.append('--debug') + run_args.append('--output_debug') + if args['is_alpha_split']: run_args.append('--alpha_split') if args['is_extend_movement']: run_args.append('--extend_movement') - run_args.append('1') if args['is_bongo']: run_args.append('--bongo') if args['is_alpha_clean']: run_args.append('--alpha_clean') if args['is_eyebrow']: run_args.append('--eyebrow') + if args['cache_simplify'] is not None: run_args.append('--simplify') run_args.append(str(cache_simplify_map[args['cache_simplify']])) - run_args.append('--cacher_quality') - run_args.append(str(cache_simplify_quality_map[args['cache_compression']])) - if args['cache_simplify'] != 'Off': - run_args.append('--use_cacher') if args['ram_cache_size'] is not None: run_args.append('--cache') run_args.append(args['ram_cache_size']) @@ -496,44 +480,37 @@ class LauncherPanel(wx.Panel): # Student model: tha4_student_{model_name} model_name = args['model_select'].replace( 'tha4_student_', '') - run_args.append('--use_tha4_student') - run_args.append('--tha4_student_model') + run_args.append('--model_version') + run_args.append('v4_student') + run_args.append('--model_name') run_args.append(model_name) elif 'tha4' in args['model_select']: - run_args.append('--use_tha4') - if 'half' in args['model_select']: - run_args.append('--model_half') + run_args.append('--model_version') + run_args.append('v4') else: - if 'seperable' in args['model_select']: - run_args.append('--model_seperable') - if 'half' in args['model_select']: - run_args.append('--model_half') + run_args.append('--model_version') + run_args.append('v3') + if 'seperable' in args['model_select']: + run_args.append('--model_seperable') + if 'half' in args['model_select']: + run_args.append('--model_half') if args['frame_rate_limit'] is not None: run_args.append('--frame_rate_limit') run_args.append(args['frame_rate_limit']) if args['sr'] is not None and args['sr'] != 'Off': + run_args.append('--use_sr') if 'anime4k' in args['sr']: - run_args.append('--anime4k') - else: - run_args.append('--use_sr') - if 'x4' in args['sr']: - run_args.append('--sr_x4') - if 'half' in args['sr']: - run_args.append('--sr_half') - - if args['device_id'] is not None: - run_args.append('--device_id') - run_args.append(args['device_id']) + run_args.append('--sr_a4k') + if 'x4' in args['sr']: + run_args.append('--sr_x4') + if 'half' in args['sr']: + run_args.append('--sr_half') if args['use_tensorrt'] is not None and args['use_tensorrt']: run_args.append('--use_tensorrt') - run_args.append('--model_cache') - run_args.append('--model_vram_cache') - run_args.append('--output_size') - run_args.append('512x512') print('Launched: ' + ' '.join(run_args)) p = subprocess.Popen(run_args) self.btnLaunch.SetLabelText('Stop') diff --git a/main.py b/main.py deleted file mode 100644 index f4d0f9b..0000000 --- a/main.py +++ /dev/null @@ -1,1019 +0,0 @@ -import struct - -import cv2 -import torch -import pyvirtualcam -import numpy as np -import mediapipe as mp -from PIL import Image - -import tha2.poser.modes.mode_20_wx -from pose import get_pose -from preprocess import resize_to_512_center, apply_color_curves -from utils import preprocessing_image, postprocessing_image -from ezvtb_rt_interface import get_core - -import errno -import json -import os -import queue -import socket -import time -import math -from pynput.mouse import Button, Controller -import re -from collections import OrderedDict -from multiprocessing import Value, Process, Queue, shared_memory - -from pyanime4k import ac - -from tha2.mocap.ifacialmocap_constants import * - -from args import args -import copy - -from tha3.util import torch_linear_to_srgb, resize_PIL_image, extract_PIL_image_from_filelike, \ - extract_pytorch_image_from_PIL_image - -import collections -from PySpout import SpoutSender -from OpenGL.GL import GL_RGBA - - -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 FPS: - def __init__(self, avarageof=50): - self.frametimestamps = collections.deque(maxlen=avarageof) - - def __call__(self): - self.frametimestamps.append(time.time()) - return self.view() - - def view(self): - if len(self.frametimestamps) > 1: - return len(self.frametimestamps) / (self.frametimestamps[-1] - self.frametimestamps[0] + 1e-10) - else: - return 0.0 - - def last(self): - if len(self.frametimestamps): - return self.frametimestamps[-1] - else: - return time.time() - - -device = torch.device('cpu') - - -def create_default_blender_data(): - data = {} - - for blendshape_name in BLENDSHAPE_NAMES: - data[blendshape_name] = 0.0 - - data[HEAD_BONE_X] = 0.0 - data[HEAD_BONE_Y] = 0.0 - data[HEAD_BONE_Z] = 0.0 - data[HEAD_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] - - data[LEFT_EYE_BONE_X] = 0.0 - data[LEFT_EYE_BONE_Y] = 0.0 - data[LEFT_EYE_BONE_Z] = 0.0 - data[LEFT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] - - data[RIGHT_EYE_BONE_X] = 0.0 - data[RIGHT_EYE_BONE_Y] = 0.0 - data[RIGHT_EYE_BONE_Z] = 0.0 - data[RIGHT_EYE_BONE_QUAT] = [0.0, 0.0, 0.0, 1.0] - - return data - - -class OSFClientProcess(Process): - def __init__(self): - super().__init__() - self.queue = Queue() - self.should_terminate = Value('b', False) - self.address = args.osf.split(':')[0] - self.port = int(args.osf.split(':')[1]) - self.ifm_fps_number = Value('f', 0.0) - self.perf_time = 0 - - def run(self): - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.setblocking(False) - self.socket.bind(("", self.port)) - self.socket.settimeout(0.1) - ifm_fps = FPS() - while True: - if self.should_terminate.value: - break - try: - socket_bytes = self.socket.recv(8192) - except socket.error as e: - err = e.args[0] - if err == errno.EAGAIN or err == errno.EWOULDBLOCK or err == 'timed out': - continue - else: - raise e - - # socket_string = socket_bytes.decode("utf-8") - osf_raw = (struct.unpack('=di2f2fB1f4f3f3f68f136f210f14f', socket_bytes)) - # print(osf_raw[432:]) - data = {} - OpenSeeDataIndex = [ - 'time', - 'id', - 'cameraResolutionW', - 'cameraResolutionH', - 'rightEyeOpen', - 'leftEyeOpen', - 'got3DPoints', - 'fit3DError', - 'rawQuaternionX', - 'rawQuaternionY', - 'rawQuaternionZ', - 'rawQuaternionW', - 'rawEulerX', - 'rawEulerY', - 'rawEulerZ', - 'translationY', - 'translationX', - 'translationZ', - ] - for i in range(len(OpenSeeDataIndex)): - data[OpenSeeDataIndex[i]] = osf_raw[i] - data['translationY'] *= -1 - data['translationZ'] *= -1 - data['rotationY'] = data['rawEulerY'] - 10 - data['rotationX'] = (-data['rawEulerX'] + 360) % 360 - 180 - data['rotationZ'] = (data['rawEulerZ'] - 90) - OpenSeeFeatureIndex = [ - 'EyeLeft', - 'EyeRight', - 'EyebrowSteepnessLeft', - 'EyebrowUpDownLeft', - 'EyebrowQuirkLeft', - 'EyebrowSteepnessRight', - 'EyebrowUpDownRight', - 'EyebrowQuirkRight', - 'MouthCornerUpDownLeft', - 'MouthCornerInOutLeft', - 'MouthCornerUpDownRight', - 'MouthCornerInOutRight', - 'MouthOpen', - 'MouthWide' - ] - - for i in range(68): - data['confidence' + str(i)] = osf_raw[i + 18] - for i in range(68): - data['pointsX' + str(i)] = osf_raw[i * 2 + 18 + 68] - data['pointsY' + str(i)] = osf_raw[i * 2 + 18 + 68 + 1] - for i in range(70): - data['points3DX' + str(i)] = osf_raw[i * 3 + 18 + 68 + 68 * 2] - data['points3DY' + str(i)] = osf_raw[i * 3 + 18 + 68 + 68 * 2 + 1] - data['points3DZ' + str(i)] = osf_raw[i * 3 + 18 + 68 + 68 * 2 + 2] - - for i in range(len(OpenSeeFeatureIndex)): - data[OpenSeeFeatureIndex[i]] = osf_raw[i + 432] - # print(data['rotationX'],data['rotationY'],data['rotationZ']) - - a = np.array([ - data['points3DX66'] - data['points3DX68'] + data['points3DX67'] - data['points3DX69'], - data['points3DY66'] - data['points3DY68'] + data['points3DY67'] - data['points3DY69'], - data['points3DZ66'] - data['points3DZ68'] + data['points3DZ67'] - data['points3DZ69'] - ]) - a = (a / np.linalg.norm(a)) - data['eyeRotationX'] = a[0] - data['eyeRotationY'] = a[1] - try: - self.queue.put_nowait(data) - except queue.Full: - pass - self.queue.close() - self.socket.close() - - -ifm_converter = tha2.poser.modes.mode_20_wx.IFacialMocapPoseConverter20() - - -class IFMClientProcess(Process): - def __init__(self): - super().__init__() - self.queue = Queue() - self.should_terminate = Value('b', False) - self.address = args.ifm.split(':')[0] - self.port = int(args.ifm.split(':')[1]) - self.ifm_fps_number = Value('f', 0.0) - self.perf_time = 0 - - def run(self): - - udpClntSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - data = "iFacialMocap_sahuasouryya9218sauhuiayeta91555dy3719" - - data = data.encode('utf-8') - - udpClntSock.sendto(data, (self.address, self.port)) - - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.setblocking(False) - self.socket.bind(("", self.port)) - self.socket.settimeout(0.1) - ifm_fps = FPS() - pre_socket_string = '' - while True: - if self.should_terminate.value: - break - try: - socket_bytes = self.socket.recv(8192) - except socket.error as e: - err = e.args[0] - if err == errno.EAGAIN or err == errno.EWOULDBLOCK or err == 'timed out': - continue - else: - raise e - socket_string = socket_bytes.decode("utf-8") - if args.debug and pre_socket_string != socket_string: - self.ifm_fps_number.value = ifm_fps() - pre_socket_string = socket_string - # print(socket_string) - # blender_data = json.loads(socket_string) - data = self.convert_from_blender_data(socket_string) - - try: - self.queue.put_nowait(data) - except queue.Full: - pass - self.queue.close() - self.socket.close() - - @staticmethod - def convert_from_blender_data(blender_data): - data = {} - - for item in blender_data.split('|'): - if item.find('#') != -1: - k, arr = item.split('#') - arr = [float(n) for n in arr.split(',')] - data[k.replace("_L", "Left").replace("_R", "Right")] = arr - elif item.find('-') != -1: - k, v = item.split("-") - data[k.replace("_L", "Left").replace("_R", "Right")] = float(v) / 100 - - to_rad = 57.3 - data[HEAD_BONE_X] = data["=head"][0] / to_rad - data[HEAD_BONE_Y] = data["=head"][1] / to_rad - data[HEAD_BONE_Z] = data["=head"][2] / to_rad - data[HEAD_BONE_QUAT] = [data["=head"][3], data["=head"][4], data["=head"][5], 1] - # print(data[HEAD_BONE_QUAT][2],min(data[EYE_BLINK_LEFT],data[EYE_BLINK_RIGHT])) - data[RIGHT_EYE_BONE_X] = data["rightEye"][0] / to_rad - data[RIGHT_EYE_BONE_Y] = data["rightEye"][1] / to_rad - data[RIGHT_EYE_BONE_Z] = data["rightEye"][2] / to_rad - data[LEFT_EYE_BONE_X] = data["leftEye"][0] / to_rad - data[LEFT_EYE_BONE_Y] = data["leftEye"][1] / to_rad - data[LEFT_EYE_BONE_Z] = data["leftEye"][2] / to_rad - - return data - - -class MouseClientProcess(Process): - def __init__(self): - super().__init__() - self.queue = Queue() - - def run(self): - mouse = Controller() - posLimit = [int(x) for x in args.mouse_input.split(',')] - prev = { - 'eye_l_h_temp': 0, - 'eye_r_h_temp': 0, - 'mouth_ratio': 0, - 'eye_y_ratio': 0, - 'eye_x_ratio': 0, - 'x_angle': 0, - 'y_angle': 0, - 'z_angle': 0, - } - while True: - pos = mouse.position - # print(pos) - eye_limit = [0.8, 0.5] - head_eye_reduce = 0.6 - head_slowness = 0.2 - mouse_data = { - 'eye_l_h_temp': 0, - 'eye_r_h_temp': 0, - 'mouth_ratio': 0, - 'eye_y_ratio': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]) * eye_limit[1], - 'eye_x_ratio': np.interp(pos[0], [posLimit[0], posLimit[2]], [1, -1]) * eye_limit[0], - 'x_angle': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]), - 'y_angle': np.interp(pos[0], [posLimit[0], posLimit[2]], [1, -1]), - 'z_angle': 0, - } - mouse_data['x_angle'] = np.interp(head_slowness, [0, 1], [prev['x_angle'], mouse_data['x_angle']]) - mouse_data['y_angle'] = np.interp(head_slowness, [0, 1], [prev['y_angle'], mouse_data['y_angle']]) - mouse_data['eye_y_ratio'] -= mouse_data['x_angle'] * eye_limit[1] * head_eye_reduce - mouse_data['eye_x_ratio'] -= mouse_data['y_angle'] * eye_limit[0] * head_eye_reduce - if args.bongo: - mouse_data['y_angle'] += 0.05 - mouse_data['x_angle'] += 0.05 - prev = mouse_data - self.queue.put_nowait(mouse_data) - time.sleep(1 / 60) - - -class ModelClientProcess(Process): - def __init__(self, input_image): - super().__init__() - self.should_terminate = Value('b', False) - self.updated = Value('b', False) - self.data = None - self.input_image = input_image - self.output_queue = Queue() - self.input_queue = Queue() - self.shms = [shared_memory.SharedMemory(create=True, size=args.model_output_size * args.model_output_size * 4) - for _ in range(args.interpolation_scale if args.use_interpolation else 1)] - self.model_fps_number = Value('f', 0.0) - self.gpu_fps_number = Value('f', 0.0) - self.cache_hit_ratio = Value('f', 0.0) - self.gpu_cache_hit_ratio = Value('f', 0.0) - - def run(self): - # Use unified ezvtb_rt interface for both THA3 and THA4 - self.model = get_core(device_id=args.device_id, - use_tensorrt=args.use_tensorrt, - use_tha4=args.use_tha4, - use_tha4_student=args.use_tha4_student, - tha4_student_model=args.tha4_student_model, - - model_seperable = args.model_seperable, - model_half=args.model_half, - model_cache_size=args.max_gpu_cache_len, - model_use_eyebrow=args.eyebrow, - - use_interpolation=args.use_interpolation, - interpolation_scale=args.interpolation_scale, - interpolation_half=args.interpolation_half, - - cacher_quality=args.cacher_quality, - cacher_ram_size=args.max_cache_len, - - use_sr=args.use_sr, - sr_half=args.sr_half, - sr_x4=args.sr_x4, - sr_noise=args.sr_noise) - self.model.setImage(self.input_image) - input_pose = np.zeros((1, 45), dtype=np.float32) - - self.shared_nps = [ - np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, buffer=self.shms[i].buf) for - i in range(len(self.shms))] - - model_fps = FPS() - - last_process_time = time.time() - while True: - model_input = None - try: - model_input = self.input_queue.get(block=True, timeout=1.0) # Save cpu from infinite loop - while not self.input_queue.empty(): - model_input = self.input_queue.get_nowait() - except queue.Empty: - continue - if model_input is None: continue - frame_interval = (1 / args.frame_rate_limit) if not args.use_interpolation else ( - 1 / args.frame_rate_limit * args.interpolation_scale) - now = time.time() - if now < frame_interval + last_process_time - 0.005: continue # - last_process_time = now - simplify_arr = [1000] * ifm_converter.pose_size - if args.simplify >= 1: - simplify_arr = [200] * ifm_converter.pose_size - simplify_arr[ifm_converter.eye_wink_left_index] = 50 - simplify_arr[ifm_converter.eye_wink_right_index] = 50 - simplify_arr[ifm_converter.eye_happy_wink_left_index] = 50 - simplify_arr[ifm_converter.eye_happy_wink_right_index] = 50 - simplify_arr[ifm_converter.eye_surprised_left_index] = 30 - simplify_arr[ifm_converter.eye_surprised_right_index] = 30 - simplify_arr[ifm_converter.iris_rotation_x_index] = 25 - simplify_arr[ifm_converter.iris_rotation_y_index] = 25 - simplify_arr[ifm_converter.eye_raised_lower_eyelid_left_index] = 10 - simplify_arr[ifm_converter.eye_raised_lower_eyelid_right_index] = 10 - simplify_arr[ifm_converter.mouth_lowered_corner_left_index] = 5 - simplify_arr[ifm_converter.mouth_lowered_corner_right_index] = 5 - simplify_arr[ifm_converter.mouth_raised_corner_left_index] = 5 - simplify_arr[ifm_converter.mouth_raised_corner_right_index] = 5 - if args.simplify >= 2: - simplify_arr[ifm_converter.head_x_index] = 100 - simplify_arr[ifm_converter.head_y_index] = 100 - simplify_arr[ifm_converter.eye_surprised_left_index] = 10 - simplify_arr[ifm_converter.eye_surprised_right_index] = 10 - model_input[ifm_converter.eye_wink_left_index] += model_input[ - ifm_converter.eye_happy_wink_left_index] - model_input[ifm_converter.eye_happy_wink_left_index] = model_input[ - ifm_converter.eye_wink_left_index] / 2 - model_input[ifm_converter.eye_wink_left_index] = model_input[ - ifm_converter.eye_wink_left_index] / 2 - model_input[ifm_converter.eye_wink_right_index] += model_input[ - ifm_converter.eye_happy_wink_right_index] - model_input[ifm_converter.eye_happy_wink_right_index] = model_input[ - ifm_converter.eye_wink_right_index] / 2 - model_input[ifm_converter.eye_wink_right_index] = model_input[ - ifm_converter.eye_wink_right_index] / 2 - - uosum = model_input[ifm_converter.mouth_uuu_index] + \ - model_input[ifm_converter.mouth_ooo_index] - model_input[ifm_converter.mouth_ooo_index] = uosum - model_input[ifm_converter.mouth_uuu_index] = 0 - is_open = (model_input[ifm_converter.mouth_aaa_index] + model_input[ - ifm_converter.mouth_iii_index] + uosum) > 0 - model_input[ifm_converter.mouth_lowered_corner_left_index] = 0 - model_input[ifm_converter.mouth_lowered_corner_right_index] = 0 - model_input[ifm_converter.mouth_raised_corner_left_index] = 0.5 if is_open else 0 - model_input[ifm_converter.mouth_raised_corner_right_index] = 0.5 if is_open else 0 - simplify_arr[ifm_converter.mouth_lowered_corner_left_index] = 0 - simplify_arr[ifm_converter.mouth_lowered_corner_right_index] = 0 - simplify_arr[ifm_converter.mouth_raised_corner_left_index] = 0 - simplify_arr[ifm_converter.mouth_raised_corner_right_index] = 0 - if args.simplify >= 3: - simplify_arr[ifm_converter.iris_rotation_x_index] = 20 - simplify_arr[ifm_converter.iris_rotation_y_index] = 20 - simplify_arr[ifm_converter.eye_wink_left_index] = 32 - simplify_arr[ifm_converter.eye_wink_right_index] = 32 - simplify_arr[ifm_converter.eye_happy_wink_left_index] = 32 - simplify_arr[ifm_converter.eye_happy_wink_right_index] = 32 - if args.simplify >= 4: - simplify_arr[ifm_converter.head_x_index] = 50 - simplify_arr[ifm_converter.head_y_index] = 50 - simplify_arr[ifm_converter.neck_z_index] = 100 - model_input[ifm_converter.eye_raised_lower_eyelid_left_index] = 0 - model_input[ifm_converter.eye_raised_lower_eyelid_right_index] = 0 - simplify_arr[ifm_converter.iris_rotation_x_index] = 10 - simplify_arr[ifm_converter.iris_rotation_y_index] = 10 - simplify_arr[ifm_converter.eye_wink_left_index] = 24 - simplify_arr[ifm_converter.eye_wink_right_index] = 24 - simplify_arr[ifm_converter.eye_happy_wink_left_index] = 24 - simplify_arr[ifm_converter.eye_happy_wink_right_index] = 24 - simplify_arr[ifm_converter.eye_surprised_left_index] = 8 - simplify_arr[ifm_converter.eye_surprised_right_index] = 8 - model_input[ifm_converter.eye_wink_left_index] += model_input[ - ifm_converter.eye_wink_right_index] - model_input[ifm_converter.eye_wink_right_index] = model_input[ - ifm_converter.eye_wink_left_index] / 2 - model_input[ifm_converter.eye_wink_left_index] = model_input[ - ifm_converter.eye_wink_left_index] / 2 - - model_input[ifm_converter.eye_surprised_left_index] += model_input[ - ifm_converter.eye_surprised_right_index] - model_input[ifm_converter.eye_surprised_right_index] = model_input[ - ifm_converter.eye_surprised_left_index] / 2 - model_input[ifm_converter.eye_surprised_left_index] = model_input[ - ifm_converter.eye_surprised_left_index] / 2 - - model_input[ifm_converter.eye_happy_wink_left_index] += model_input[ - ifm_converter.eye_happy_wink_right_index] - model_input[ifm_converter.eye_happy_wink_right_index] = model_input[ - ifm_converter.eye_happy_wink_left_index] / 2 - model_input[ifm_converter.eye_happy_wink_left_index] = model_input[ - ifm_converter.eye_happy_wink_left_index] / 2 - model_input[ifm_converter.mouth_aaa_index] = min( - model_input[ifm_converter.mouth_aaa_index] + - model_input[ifm_converter.mouth_ooo_index] / 2 + - model_input[ifm_converter.mouth_iii_index] / 2 + - model_input[ifm_converter.mouth_uuu_index] / 2, 1 - ) - model_input[ifm_converter.mouth_ooo_index] = 0 - model_input[ifm_converter.mouth_iii_index] = 0 - model_input[ifm_converter.mouth_uuu_index] = 0 - for i in range(4, args.simplify): - simplify_arr = [max(math.ceil(x * 0.8), 5) for x in simplify_arr] - for i in range(0, len(simplify_arr)): - if simplify_arr[i] > 0: - model_input[i] = round(model_input[i] * simplify_arr[i]) / simplify_arr[i] - - if args.perf == 'model': - tic = time.perf_counter() - if args.eyebrow: - for i in range(12): - input_pose[0, i] = model_input[i] - for i in range(27): - input_pose[0, i + 12] = model_input[i + 12] - for i in range(6): - input_pose[0, i + 12 + 27] = model_input[i + 27 + 12] - - output_images = self.model.inference(input_pose) - - if args.perf == 'model': - print("postprocess", (time.perf_counter() - tic) * 1000) - tic = time.perf_counter() - - for i in range(len(self.shared_nps)): - np.copyto(self.shared_nps[i], output_images[i]) - self.output_queue.put_nowait(True) - - if args.debug: - self.model_fps_number.value = model_fps() - if self.model.cacher is not None: - self.cache_hit_ratio.value = self.model.cacher.hits / ( - self.model.cacher.hits + self.model.cacher.miss + 1) - try: - if args.use_tensorrt and args.model_cache and args.model_vram_cache: - self.gpu_cache_hit_ratio.value = self.model.tha.morpher_cacher.hits / ( - self.model.tha.morpher_cacher.hits + self.model.tha.morpher_cacher.miss) - except: - pass - - -@torch.no_grad() -def main(): - # Load character image - img = Image.open(f"data/images/{args.character}.png") - img = img.convert('RGBA') - ow, oh = img.size - for i, px in enumerate(img.getdata()): - if px[3] <= 0: - y = i // ow - x = i % ow - img.putpixel((x, y), (0, 0, 0, 0)) - if ow != 512 or oh != 512: - img = resize_to_512_center(img) - if args.alpha_clean: - curves = { - 'a': [ - (60, 0), - (200, 255) - ] - } - img = apply_color_curves(img, curves) - input_image = np.array(img) - input_image = cv2.cvtColor(input_image, cv2.COLOR_RGBA2BGRA) - - print("Character Image Loaded:", args.character) - cap = None - - output_fps = FPS() - - if not args.debug_input: - - if args.ifm is not None: - client_process = IFMClientProcess() - client_process.daemon = True - client_process.start() - print("iFacialMocap Service Running:", args.ifm) - - elif args.osf is not None: - client_process = OSFClientProcess() - client_process.daemon = True - client_process.start() - print("OpenSeeFace Service Running:", args.osf) - - elif args.mouse_input is not None: - client_process = MouseClientProcess() - client_process.daemon = True - client_process.start() - print("Mouse Input Running") - - else: - - if args.input == 'cam': - cap = cv2.VideoCapture(0) - ret, frame = cap.read() - if ret is None: - raise Exception("Can't find Camera") - else: - cap = cv2.VideoCapture(args.input) - frame_count = 0 - os.makedirs(os.path.join('dst', args.character, args.output_dir), exist_ok=True) - print("Webcam Input Running") - - facemesh = mp.solutions.face_mesh.FaceMesh(refine_landmarks=True) - - if args.output_webcam: - cam_scale = 1 - cam_width_scale = 1 - if args.anime4k: - cam_scale = 2 - if args.alpha_split: - cam_width_scale = 2 - - if args.output_webcam == 'spout': - # Create the sender - sender = SpoutSender("EasyVtuber", args.model_output_size * cam_scale * cam_width_scale, - args.model_output_size * cam_scale, GL_RGBA) - else: - cam = pyvirtualcam.Camera(width=args.model_output_size * cam_scale * cam_width_scale, - height=args.model_output_size * cam_scale, - fps=60, - backend=args.output_webcam, - fmt= - {'unitycapture': pyvirtualcam.PixelFormat.RGBA, - 'obs': pyvirtualcam.PixelFormat.RGB}[ - args.output_webcam]) - print(f'Using virtual camera: {cam.device}') - - a = None - - if args.anime4k: - parameters = ac.Parameters() - # enable HDN for ACNet - parameters.HDN = True - - # a = ac.AC( - # managerList=ac.ManagerList([ac.CUDAManager(dID=0)]), - # type=ac.ProcessorType.Cuda_ACNet, - # ) - - a = ac.AC( - managerList=ac.ManagerList([ac.OpenCLACNetManager(pID=0, dID=0)]), - type=ac.ProcessorType.OpenCL_ACNet, - ) - a.set_arguments(parameters) - print("Anime4K Loaded") - - position_vector = [0, 0, 0, 1] - position_vector_0 = None - pose_vector_0 = None - - pose_queue = [] - blender_data = {} - if (args.ifm): - blender_data = create_default_blender_data() - mouse_data = { - 'eye_l_h_temp': 0, - 'eye_r_h_temp': 0, - 'mouth_ratio': 0, - 'eye_y_ratio': 0, - 'eye_x_ratio': 0, - 'x_angle': 0, - 'y_angle': 0, - 'z_angle': 0, - } - - model_output = None - model_process = ModelClientProcess(input_image) - model_process.daemon = True - model_process.start() - model_output_nps = [np.ndarray((args.model_output_size, args.model_output_size, 4), dtype=np.uint8, - buffer=model_process.shms[i].buf) for i in range(len(model_process.shms))] - model_result_read_ptr = 0 - model_return_fps = FPS(5) - need_a_copy = True - - print("Ready. Close this console to exit.") - - mouth_eye_vector_c_hist = [] - - while True: - # ret, frame = cap.read() - # input_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - # results = facemesh.process(input_frame) - - if args.perf == 'main': - tic = time.perf_counter() - if args.debug_input: - eyebrow_vector_c = [0.0] * 12 - mouth_eye_vector_c = [0.0] * 27 - pose_vector_c = [0.0] * 6 - - mouth_eye_vector_c[2] = math.sin(time.perf_counter() * 3) - mouth_eye_vector_c[3] = math.sin(time.perf_counter() * 3) - - mouth_eye_vector_c[14] = 0 - - mouth_eye_vector_c[25] = math.sin(time.perf_counter() * 2.2) * 0.2 - mouth_eye_vector_c[26] = math.sin(time.perf_counter() * 3.5) * 0.8 - - pose_vector_c[0] = math.sin(time.perf_counter() * 1.1) - pose_vector_c[1] = math.sin(time.perf_counter() * 1.2) - pose_vector_c[2] = math.sin(time.perf_counter() * 1.5) - - eyebrow_vector_c[6] = math.sin(time.perf_counter() * 1.1) - eyebrow_vector_c[7] = math.sin(time.perf_counter() * 1.1) - - elif args.osf is not None: - try: - new_blender_data = blender_data - while not client_process.should_terminate.value and not client_process.queue.empty(): - new_blender_data = client_process.queue.get_nowait() - blender_data = new_blender_data - except queue.Empty: - pass - eyebrow_vector_c = [0.0] * 12 - mouth_eye_vector_c = [0.0] * 27 - pose_vector_c = [0.0] * 6 - - if len(blender_data) != 0: - mouth_eye_vector_c[2] = 1 - blender_data['leftEyeOpen'] - mouth_eye_vector_c[3] = 1 - blender_data['rightEyeOpen'] - - mouth_eye_vector_c[14] = max(blender_data['MouthOpen'], 0) - # print(mouth_eye_vector_c[14]) - - mouth_eye_vector_c[25] = -blender_data['eyeRotationY'] * 3 - (blender_data['rotationX']) / 57.3 * 1.5 - mouth_eye_vector_c[26] = blender_data['eyeRotationX'] * 3 + (blender_data['rotationY']) / 57.3 - # print(mouth_eye_vector_c[25:27]) - eyebrow_vector_c[6] = blender_data['EyebrowUpDownLeft'] - eyebrow_vector_c[7] = blender_data['EyebrowUpDownRight'] - # print(blender_data['EyebrowUpDownLeft'],blender_data['EyebrowUpDownRight']) - - # if pose_vector_0==None: - # pose_vector_0=[0,0,0] - # pose_vector_0[0] = blender_data['rotationX'] - # pose_vector_0[1] = blender_data['rotationY'] - # pose_vector_0[2] = blender_data['rotationZ'] - # pose_vector_c[0] = (blender_data['rotationX']-pose_vector_0[0])/57.3*3 - # pose_vector_c[1] = -(blender_data['rotationY']-pose_vector_0[1])/57.3*3 - # pose_vector_c[2] = (blender_data['rotationZ']-pose_vector_0[2])/57.3 - pose_vector_c[0] = (blender_data['rotationX']) / 57.3 * 3 - pose_vector_c[1] = -(blender_data['rotationY']) / 57.3 * 3 - pose_vector_c[2] = (blender_data['rotationZ']) / 57.3 * 2 - # print(pose_vector_c) - - if position_vector_0 == None: - position_vector_0 = [0, 0, 0, 1] - position_vector_0[0] = blender_data['translationX'] - position_vector_0[1] = blender_data['translationY'] - position_vector_0[2] = blender_data['translationZ'] - position_vector[0] = -(blender_data['translationX'] - position_vector_0[0]) * 0.1 - position_vector[1] = -(blender_data['translationY'] - position_vector_0[1]) * 0.1 - position_vector[2] = -(blender_data['translationZ'] - position_vector_0[2]) * 0.1 - - # Apply filter - if len(mouth_eye_vector_c_hist) == 0: - for _ in range(3): - mouth_eye_vector_c_hist.append(copy.deepcopy(mouth_eye_vector_c)) - else: - mouth_eye_vector_c_hist.pop(0) - mouth_eye_vector_c_hist.append(mouth_eye_vector_c) - mouth_eye_vector_c[25] = mouth_eye_vector_c_hist[0][25] * 0.2 + mouth_eye_vector_c_hist[1][25] * 0.5 + mouth_eye_vector_c_hist[2][25] * 0.3 - mouth_eye_vector_c[26] = mouth_eye_vector_c_hist[0][26] * 0.2 + mouth_eye_vector_c_hist[1][26] * 0.5 + mouth_eye_vector_c_hist[2][26] * 0.3 - - elif args.ifm is not None: - # get pose from ifm - try: - new_blender_data = blender_data - while not client_process.should_terminate.value and not client_process.queue.empty(): - new_blender_data = client_process.queue.get_nowait() - blender_data = new_blender_data - except queue.Empty: - pass - - ifacialmocap_pose_converted = ifm_converter.convert(blender_data) - - # ifacialmocap_pose = blender_data - # - # eye_l_h_temp = ifacialmocap_pose[EYE_BLINK_LEFT] - # eye_r_h_temp = ifacialmocap_pose[EYE_BLINK_RIGHT] - # mouth_ratio = (ifacialmocap_pose[JAW_OPEN] - 0.10)*1.3 - # x_angle = -ifacialmocap_pose[HEAD_BONE_X] * 1.5 + 1.57 - # y_angle = -ifacialmocap_pose[HEAD_BONE_Y] - # z_angle = ifacialmocap_pose[HEAD_BONE_Z] - 1.57 - # - # eye_x_ratio = (ifacialmocap_pose[EYE_LOOK_IN_LEFT] - - # ifacialmocap_pose[EYE_LOOK_OUT_LEFT] - - # ifacialmocap_pose[EYE_LOOK_IN_RIGHT] + - # ifacialmocap_pose[EYE_LOOK_OUT_RIGHT]) / 2.0 / 0.75 - # - # eye_y_ratio = (ifacialmocap_pose[EYE_LOOK_UP_LEFT] - # + ifacialmocap_pose[EYE_LOOK_UP_RIGHT] - # - ifacialmocap_pose[EYE_LOOK_DOWN_RIGHT] - # + ifacialmocap_pose[EYE_LOOK_DOWN_LEFT]) / 2.0 / 0.75 - - eyebrow_vector_c = [0.0] * 12 - mouth_eye_vector_c = [0.0] * 27 - pose_vector_c = [0.0] * 6 - for i in range(0, 12): - eyebrow_vector_c[i] = ifacialmocap_pose_converted[i] - for i in range(12, 39): - mouth_eye_vector_c[i - 12] = ifacialmocap_pose_converted[i] - for i in range(39, 42): - pose_vector_c[i - 39] = ifacialmocap_pose_converted[i] - - position_vector = blender_data[HEAD_BONE_QUAT] - - elif args.mouse_input is not None: - - try: - new_blender_data = mouse_data - while not client_process.queue.empty(): - new_blender_data = client_process.queue.get_nowait() - mouse_data = new_blender_data - except queue.Empty: - pass - - eye_l_h_temp = mouse_data['eye_l_h_temp'] - eye_r_h_temp = mouse_data['eye_r_h_temp'] - mouth_ratio = mouse_data['mouth_ratio'] - eye_y_ratio = mouse_data['eye_y_ratio'] - eye_x_ratio = mouse_data['eye_x_ratio'] - x_angle = mouse_data['x_angle'] - y_angle = mouse_data['y_angle'] - z_angle = mouse_data['z_angle'] - - eyebrow_vector_c = [0.0] * 12 - mouth_eye_vector_c = [0.0] * 27 - pose_vector_c = [0.0] * 6 - - mouth_eye_vector_c[2] = eye_l_h_temp - mouth_eye_vector_c[3] = eye_r_h_temp - - mouth_eye_vector_c[14] = mouth_ratio * 1.5 - - mouth_eye_vector_c[25] = eye_y_ratio - mouth_eye_vector_c[26] = eye_x_ratio - - pose_vector_c[0] = x_angle - pose_vector_c[1] = y_angle - pose_vector_c[2] = z_angle - - else: - ret, frame = cap.read() - input_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - results = facemesh.process(input_frame) - - if results.multi_face_landmarks is None: - continue - - facial_landmarks = results.multi_face_landmarks[0].landmark - - if args.debug: - pose, debug_image = get_pose(facial_landmarks, frame) - else: - pose = get_pose(facial_landmarks) - - if len(pose_queue) < 3: - pose_queue.append(pose) - pose_queue.append(pose) - pose_queue.append(pose) - else: - pose_queue.pop(0) - pose_queue.append(pose) - - np_pose = np.average(np.array(pose_queue), axis=0, weights=[0.6, 0.3, 0.1]) - - eye_l_h_temp = np_pose[0] - eye_r_h_temp = np_pose[1] - mouth_ratio = np_pose[2] - eye_y_ratio = np_pose[3] - eye_x_ratio = np_pose[4] - x_angle = np_pose[5] - y_angle = np_pose[6] - z_angle = np_pose[7] - - eyebrow_vector_c = [0.0] * 12 - mouth_eye_vector_c = [0.0] * 27 - pose_vector_c = [0.0] * 6 - - mouth_eye_vector_c[2] = eye_l_h_temp - mouth_eye_vector_c[3] = eye_r_h_temp - - mouth_eye_vector_c[14] = mouth_ratio * 1.5 - - mouth_eye_vector_c[25] = eye_y_ratio - mouth_eye_vector_c[26] = eye_x_ratio - - pose_vector_c[0] = (x_angle - 1.5) * 1.6 - pose_vector_c[1] = y_angle * 2.0 # temp weight - pose_vector_c[2] = (z_angle + 1.5) * 2 # temp weight - - pose_vector_c[3] = pose_vector_c[1] - pose_vector_c[4] = pose_vector_c[2] - - model_input_arr = eyebrow_vector_c - model_input_arr.extend(mouth_eye_vector_c) - model_input_arr.extend(pose_vector_c) - - if args.perf == 'main': - print('===') - print("input", time.perf_counter() - tic) - tic = time.perf_counter() - model_process.input_queue.put_nowait(model_input_arr) - - try: - while not model_process.output_queue.empty(): - model_process.output_queue.get_nowait() - model_result_read_ptr = 0 - model_return_fps() - need_a_copy = True - except queue.Empty: - pass - - if need_a_copy: - model_output = model_output_nps[model_result_read_ptr].copy() - need_a_copy = False - interval = 1 / (model_return_fps.view() + 1e-10) / len(model_output_nps) * 0.8 - if model_result_read_ptr + 1 < len(model_output_nps) and time.time() >= ( - (model_result_read_ptr + 1) * interval + model_return_fps.last()): - model_result_read_ptr += 1 - need_a_copy = True - - postprocessed_image = model_output - if args.perf == 'main': - print("Compute", time.perf_counter() - tic) - tic = time.perf_counter() - - k_scale = 1 - rotate_angle = 0 - dx = 0 - dy = 0 - if args.extend_movement: - k_scale = position_vector[2] * math.sqrt(args.extend_movement) + 1 - rotate_angle = -position_vector[0] * 10 * args.extend_movement - dx = position_vector[0] * 400 * k_scale * args.extend_movement - dy = -position_vector[1] * 600 * k_scale * args.extend_movement - if args.bongo: - rotate_angle -= 5 - - rm = cv2.getRotationMatrix2D((args.model_output_size / 2, args.model_output_size / 2), rotate_angle, k_scale) - rm[0, 2] += dx + args.model_output_size / 2 - args.model_output_size / 2 - rm[1, 2] += dy + args.model_output_size / 2 - args.model_output_size / 2 - - postprocessed_image = cv2.warpAffine( - postprocessed_image, - rm, - (args.model_output_size, args.model_output_size)) - - if args.perf == 'main': - print("postprocess", (time.perf_counter() - tic) * 1000) - tic = time.perf_counter() - - output_fps_number = output_fps() - - if args.anime4k: - alpha_channel = postprocessed_image[:, :, 3] - alpha_channel = cv2.resize(alpha_channel, None, fx=2, fy=2) - - # a.load_image_from_numpy(cv2.cvtColor(postprocessed_image, cv2.COLOR_RGBA2RGB), input_type=ac.AC_INPUT_RGB) - # img = cv2.imread("character/test41.png") - img1 = cv2.cvtColor(postprocessed_image, cv2.COLOR_BGRA2BGR) - # a.load_image_from_numpy(img, input_type=ac.AC_INPUT_BGR) - a.load_image_from_numpy(img1, input_type=ac.AC_INPUT_BGR) - a.process() - postprocessed_image = a.save_image_to_numpy() - postprocessed_image = cv2.merge((postprocessed_image, alpha_channel)) - if args.perf == 'main': - print("anime4k", (time.perf_counter() - tic) * 1000) - tic = time.perf_counter() - if args.alpha_split: - alpha_image = cv2.merge( - [postprocessed_image[:, :, 3], postprocessed_image[:, :, 3], postprocessed_image[:, :, 3]]) - alpha_image = cv2.cvtColor(alpha_image, cv2.COLOR_BGR2BGRA) - postprocessed_image = cv2.hconcat([postprocessed_image, alpha_image]) - - if args.debug: - if args.use_sr: - time.sleep( - 0.000001) # Opencv is using different strategy when putting 512x512 and 1024x1024 image to imshow, - # in a way that 1024x1024 is much faster than 512x512 14msvs0.2ms(maybe using zero-copy?) so when using SR, add a delay to prevent overan - # Due to windows system limitation this actually sleep 16ms - output_frame = postprocessed_image - # resized_frame = cv2.resize(output_frame, (np.min(debug_image.shape[:2]), np.min(debug_image.shape[:2]))) - # output_frame = np.concatenate([debug_image, resized_frame], axis=1) - cv2.putText(output_frame, str('OUT_FPS:%.1f' % output_fps_number), (0, 16), cv2.FONT_HERSHEY_PLAIN, 1, - (0, 255, 0), 1) - cv2.putText(output_frame, str( - 'GPU_FPS:%.1f' % ( - model_process.model_fps_number.value if not args.use_interpolation else model_process.model_fps_number.value * args.interpolation_scale)), - (0, 32), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - if args.ifm is not None: - cv2.putText(output_frame, str('IFM_FPS:%.1f' % client_process.ifm_fps_number.value), (0, 48), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - if args.max_cache_len > 0: - cv2.putText(output_frame, str('MEMCACHED:%.1f%%' % (model_process.cache_hit_ratio.value * 100)), - (0, 64), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - if args.max_gpu_cache_len > 0.0: - cv2.putText(output_frame, str('GPUCACHED:%.1f%%' % (model_process.gpu_cache_hit_ratio.value * 100)), - (0, 80), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - cv2.imshow("frame", output_frame) - # cv2.imshow("camera", debug_image) - cv2.waitKey(1) - if args.output_webcam: - # result_image = np.zeros([720, 1280, 3], dtype=np.uint8) - # result_image[720 - 512:, 1280 // 2 - 256:1280 // 2 + 256] = cv2.resize( - # cv2.cvtColor(postprocessing_image(output_image.cpu()), cv2.COLOR_RGBA2RGB), (512, 512)) - result_image = postprocessed_image - if args.output_webcam == 'spout': - sender.send_image(cv2.cvtColor(postprocessed_image, cv2.COLOR_BGRA2RGBA), False) - time.sleep(0.000001) - else: - if args.output_webcam == 'obs': - result_image = cv2.cvtColor(result_image, cv2.COLOR_BGRA2RGB) - cam.send(result_image) - cam.sleep_until_next_frame() - if args.perf == 'main': - print("output", (time.perf_counter() - tic) * 1000) - tic = time.perf_counter() - - -if __name__ == '__main__': - main() diff --git a/pose.py b/pose.py deleted file mode 100644 index 04074da..0000000 --- a/pose.py +++ /dev/null @@ -1,183 +0,0 @@ -import cv2 -import numpy as np -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_CONTOURS -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_FACE_OVAL -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_IRISES -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_EYE -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_EYEBROW -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_IRIS -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LIPS -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYE -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYEBROW -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_IRIS -from mediapipe.python.solutions.face_mesh_connections import FACEMESH_TESSELATION - -from facial_points import MOUTH_TOP, MOUTH_BOTTOM, MOUTH_LEFT1, MOUTH_LEFT2, MOUTH_RIGHT -from facial_points import IRIS_L_TOP, IRIS_L_BOTTOM, IRIS_L_LEFT, IRIS_L_RIGHT -from facial_points import IRIS_R_TOP, IRIS_R_BOTTOM, IRIS_R_LEFT, IRIS_R_RIGHT -from utils import get_distance - - -# pose vector index ------------------------------------------ -# wink 0, 1 -# happy wink 2, 3 -# surprised 4, 5 -# relaxed 6, 7 -# unimpressed 8, 9 -# iris Shrinkage L 12 -# iris Shrinkage R MOUTH_TOP -# mouth aaa MOUTH_BOTTOM -# mouth iii 15 -# mouth uuu 16 -# mouth eee 17 -# mouth ooo 18 -# mouth delta 19 -# mouth lowered corner 18,19 -# mouth raised corner 20,21 -# mouth smirk 22 -# Iris rotation 25, 26 - -# x,y,z 27, 28, 29 -# ------------------------------------------------------------ - - -class Landmark: - def __init__(self, x=None, y=None, z=None): - self.x = x - self.y = y - self.z = z - - def __repr__(self): - return f'x: {self.x}\ny: {self.y}\nz: {self.z}' - -# def set_pose(pose, pose_vector) - - -def get_iris_center_point(landmarks, side=None): - """ - calculate right or left side iris center 3d coordinate - Args: - landmarks: - side: - - Returns: - iris center 3d coordinate point (Landmark) - - Raises: - if you didn't specify side - - """ - if side is None: - raise 'please choose side `left` or `right`' - elif side.lower() == 'right' or side.lower() == 'r': - landmark_list = FACEMESH_RIGHT_IRIS - elif side.lower() == 'left' or side.lower() == 'l': - landmark_list = FACEMESH_LEFT_IRIS - - temp = np.zeros(3) - for idx in list(map(lambda x: x[0], landmark_list)): - pos_x, pos_y, pos_z = landmarks[idx].x, landmarks[idx].y, landmarks[idx].z - temp[0] += pos_x - temp[1] += pos_y - temp[2] += pos_z - temp = temp / len(landmark_list) - return Landmark(temp[0], temp[1], temp[2]) - - -def get_pose(facial_landmarks, debug_image=None): - """ - extract pose vector from facial landmarks - - Args: - facial_landmarks (landmarks): extracted facial landmarks by mediapipe - debug_image (numpy array): frame image from video or webcam - - Returns: - tuple: pose vector - numpy array(optional): draw features to frame image - """ - iris_r_center = get_iris_center_point(facial_landmarks, 'r') - iris_l_center = get_iris_center_point(facial_landmarks, 'l') - - mouth_h = facial_landmarks[MOUTH_TOP].y - facial_landmarks[MOUTH_BOTTOM].y - mouth_w = facial_landmarks[MOUTH_RIGHT].x - (facial_landmarks[MOUTH_LEFT1].x + facial_landmarks[MOUTH_LEFT2].x) / 2 - mouth_ratio = mouth_h / mouth_w - - x_angle = np.arctan2(facial_landmarks[197].y - facial_landmarks[9].y, facial_landmarks[197].z - facial_landmarks[9].z) - y_angle = np.arctan2(facial_landmarks[IRIS_L_TOP].z - facial_landmarks[IRIS_R_TOP].z, facial_landmarks[IRIS_L_TOP].x - facial_landmarks[IRIS_R_TOP].x) - z_angle = np.arctan2(facial_landmarks[9].y - facial_landmarks[152].y, facial_landmarks[9].x - facial_landmarks[152].x) - - iris_rotation_l_h = get_distance(facial_landmarks[IRIS_L_TOP], facial_landmarks[IRIS_L_BOTTOM]) - iris_rotation_l_w = get_distance(facial_landmarks[IRIS_L_RIGHT], facial_landmarks[IRIS_L_LEFT]) - iris_rotation_r_h = get_distance(facial_landmarks[IRIS_R_TOP], facial_landmarks[IRIS_R_BOTTOM]) - iris_rotation_r_w = get_distance(facial_landmarks[IRIS_R_RIGHT], facial_landmarks[IRIS_R_LEFT]) - - iris_rotation_l_h_temp = np.sqrt( - (iris_l_center.x - facial_landmarks[IRIS_L_TOP].x) ** 2 + (iris_l_center.y - facial_landmarks[IRIS_L_TOP].y) ** 2) - iris_rotation_l_w_temp = np.sqrt( - (iris_l_center.x - facial_landmarks[IRIS_L_RIGHT].x) ** 2 + (iris_l_center.y - facial_landmarks[IRIS_L_RIGHT].y) ** 2) - iris_rotation_r_h_temp = np.sqrt( - (iris_r_center.x - facial_landmarks[IRIS_R_TOP].x) ** 2 + (iris_r_center.y - facial_landmarks[IRIS_R_TOP].y) ** 2) - iris_rotation_r_w_temp = np.sqrt( - (iris_r_center.x - facial_landmarks[IRIS_R_RIGHT].x) ** 2 + (iris_r_center.y - facial_landmarks[IRIS_R_RIGHT].y) ** 2) - - eye_x_ratio = ((iris_rotation_l_w_temp / iris_rotation_l_w + iris_rotation_r_w_temp / iris_rotation_r_w) - 1) * 3 - eye_y_ratio = ((iris_rotation_l_h_temp / iris_rotation_l_h + iris_rotation_r_h_temp / iris_rotation_r_h) - 1) * 3 - - eye_l_h_temp = 1 - 2 * (facial_landmarks[IRIS_R_BOTTOM].y - facial_landmarks[IRIS_R_TOP].y) / ( - facial_landmarks[IRIS_R_LEFT].x - facial_landmarks[IRIS_R_RIGHT].x) - eye_r_h_temp = 1 - 2 * (facial_landmarks[IRIS_L_BOTTOM].y - facial_landmarks[IRIS_L_TOP].y) / ( - facial_landmarks[IRIS_L_LEFT].x - facial_landmarks[IRIS_L_RIGHT].x) - - if debug_image is not None: - h, w, c = debug_image.shape - - # draw iris - cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], - [int(iris_l_center.x * w), int(iris_l_center.y * h)], (0, 128, 128), 3) - cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], - [int(iris_r_center.x * w), int(iris_r_center.y * h)], (255, 128, 0), 3) - - # draw iris connection - debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], - [int(iris_r_center.x * w), int(iris_r_center.y * h)], (0, 128, 255), 1) - - # draw eye edge2iris - debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], - [int(facial_landmarks[IRIS_R_RIGHT].x * w), int(facial_landmarks[IRIS_R_RIGHT].y * h)], (0, 255, 128), 1) - debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], - [int(facial_landmarks[IRIS_R_BOTTOM].x * w), int(facial_landmarks[IRIS_R_BOTTOM].y * h)], (0, 255, 128), 1) - debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], - [int(facial_landmarks[IRIS_R_LEFT].x * w), int(facial_landmarks[IRIS_R_LEFT].y * h)], (0, 255, 128), 1) - debug_image = cv2.line(debug_image, [int(iris_r_center.x * w), int(iris_r_center.y * h)], - [int(facial_landmarks[IRIS_R_TOP].x * w), int(facial_landmarks[IRIS_R_TOP].y * h)], (0, 255, 128), 1) - - debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], - [int(facial_landmarks[IRIS_L_RIGHT].x * w), int(facial_landmarks[IRIS_L_RIGHT].y * h)], (255, 128, 0), 1) - debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], - [int(facial_landmarks[IRIS_L_TOP].x * w), int(facial_landmarks[IRIS_L_TOP].y * h)], (255, 128, 0), 1) - debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], - [int(facial_landmarks[IRIS_L_LEFT].x * w), int(facial_landmarks[IRIS_L_LEFT].y * h)], (255, 128, 0), 1) - debug_image = cv2.line(debug_image, [int(iris_l_center.x * w), int(iris_l_center.y * h)], - [int(facial_landmarks[IRIS_L_BOTTOM].x * w), int(facial_landmarks[IRIS_L_BOTTOM].y * h)], (255, 128, 0), 1) - - # draw mouth - debug_image = cv2.line(debug_image, [int((facial_landmarks[MOUTH_LEFT1].x + facial_landmarks[MOUTH_LEFT2].x) / 2 * w), int((facial_landmarks[MOUTH_LEFT1].y + facial_landmarks[MOUTH_LEFT2].y) / 2 * h)], - [int(facial_landmarks[MOUTH_RIGHT].x * w), int(facial_landmarks[MOUTH_RIGHT].y * h)], (255, 64, 0), 1) - - debug_image = cv2.line(debug_image, [int(facial_landmarks[MOUTH_TOP].x * w), int(facial_landmarks[MOUTH_TOP].y * h)], - [int(facial_landmarks[MOUTH_BOTTOM].x * w), int(facial_landmarks[MOUTH_BOTTOM].y * h)], (255, 64, 0), 1) - - # draw mouth - debug_image = cv2.line(debug_image, [int(facial_landmarks[197].x * w), int(facial_landmarks[197].y * h)], - [int(facial_landmarks[9].x * w), int(facial_landmarks[9].y * h)], (0, 0, 255), 1) - - debug_image = cv2.flip(debug_image, 1) - - cv2.putText(debug_image, f'x angle : {x_angle:.2f}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) - cv2.putText(debug_image, f'y angle : {y_angle:.2f}', (10, 45), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) - cv2.putText(debug_image, f'z angle : {z_angle:.2f}', (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) - - return (eye_l_h_temp, eye_r_h_temp, mouth_ratio, eye_y_ratio, eye_x_ratio, x_angle , y_angle, z_angle), debug_image - else: - return eye_l_h_temp, eye_r_h_temp, mouth_ratio, eye_y_ratio, eye_x_ratio,x_angle, y_angle, z_angle \ No newline at end of file diff --git a/preprocess.py b/preprocess.py deleted file mode 100644 index ae6bb21..0000000 --- a/preprocess.py +++ /dev/null @@ -1,114 +0,0 @@ -from PIL import Image -import numpy as np -from scipy.interpolate import interp1d - - -def resize_to_512_center(img, fill_color=(0, 0, 0, 0)): - - # 计算缩放比例 - original_width, original_height = img.size - ratio = min(512 / original_width, 512 / original_height) - - # 等比缩放 - new_size = (int(original_width * ratio), int(original_height * ratio)) - resized_img = img.resize(new_size, Image.LANCZOS) # 高质量缩放算法 - - # 创建新画布 - canvas = Image.new(img.mode, (512, 512), fill_color) - - # 计算粘贴位置(居中) - paste_position = ( - (512 - new_size[0]) // 2, - (512 - new_size[1]) // 2 - ) - - # 合并图像(保持透明度) - canvas.paste(resized_img, paste_position) - - - return canvas - -def apply_color_curves(img, curves): - """ - 对图像应用颜色曲线调整 - :param input_path: 输入图像路径 - :param output_path: 输出图像路径 - :param curves: 曲线配置字典,格式为: - { - 'rgb': [(x1, y1), (x2, y2), ...], # 全局通道曲线 - 'r': [(x1, y1), (x2, y2), ...], # 红色通道曲线 - 'g': [(x1, y1), (x2, y2), ...], # 绿色通道曲线 - 'b': [(x1, y1), (x2, y2), ...] # 蓝色通道曲线 - } - """ - if img.mode == 'P': - img = img.convert('RGB') - - # 分离通道 - channels = list(img.split()) - - # 处理每个颜色通道 - for ch_idx, ch_name in enumerate(['r', 'g', 'b','a']): - # 合并全局和通道特定曲线 - combined_points = [] - if 'rgb' in curves and not ch_name=='a': - combined_points.extend(curves['rgb']) - if ch_name in curves: - combined_points.extend(curves[ch_name]) - - if not combined_points: - continue - - # 生成LUT - points = sorted(combined_points, key=lambda x: x[0]) - xs, ys = zip(*points) - - # 自动选择插值方式 - interpolation_kind = 'quadratic' if len(points) >= 3 else 'linear' - interpolator = interp1d( - xs, ys, - kind=interpolation_kind, - bounds_error=False, - fill_value=(ys[0], ys[-1]) - ) - - # 创建查找表 - x_range = np.arange(256) - y_new = interpolator(x_range) - y_new = np.clip(y_new, 0, 255).astype(np.uint8) - lut = y_new.tolist() - - # 应用LUT - channels[ch_idx] = channels[ch_idx].point(lut) - - # 合并通道并保存 - merged_channels = channels - img_out = Image.merge(img.mode, merged_channels) - - return img_out - - - # 使用示例 -if __name__ == "__main__": - # S型曲线增强对比度 - curves = { - 'a': [ - (60, 0), - (127, 127), - (170, 255) - ] - } - - # 红色通道增强中间调 - # curves = { - # 'r': [ - # (0, 0), - # (127, 160), - # (255, 255) - # ] - # } - img = Image.open('input.png') - # img = apply_color_curves(img, curves) - img = resize_to_512_center(img) - - img.save('output.png') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9bd7383..ac93f95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ -numpy==1.26.4 +numpy scipy matplotlib protobuf==3.20.1 pynput==1.7.6 -mediapipe +mediapipe==0.10.11 opencv_python==4.5.5.64 -Pillow==9.1.0 -pyanime4k==2.5.2 +Pillow +pyanime4k==3.1.0 pyvirtualcam==0.9.1 pyopengl wxpython @@ -14,4 +14,5 @@ onnx onnxruntime-directml turbojpeg tqdm -torch \ No newline at end of file +torch +OneEuroFilter \ No newline at end of file diff --git a/src/args.py b/src/args.py index 3f32426..7571675 100644 --- a/src/args.py +++ b/src/args.py @@ -29,8 +29,8 @@ parser.add_argument('--alpha_split', action='store_true') parser.add_argument('--bongo', action='store_true') parser.add_argument('--extend_movement', action='store_true') -parser.add_argument('--filter_min_cutoff', type=float, default=0.3) -parser.add_argument('--filter_beta', type=float, default=1.0) +parser.add_argument('--filter_min_cutoff', type=float, default=10.0) +parser.add_argument('--filter_beta', type=float, default=0.3) parser.add_argument('--simplify', type=int, default=1) @@ -85,5 +85,3 @@ if args.output_spout2: args.alpha_split = False # Disable alpha split for spout2 output args.model_output_size = 1024 if args.use_sr else 512 - -print("Arguments:", args) \ No newline at end of file diff --git a/src/debug_input_client.py b/src/debug_input_client.py index 37d8136..09e0323 100644 --- a/src/debug_input_client.py +++ b/src/debug_input_client.py @@ -3,9 +3,7 @@ import numpy as np import time import math from .utils.timer_wait import wait_until -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel -from .utils.filter import OneEuroFilterNumpy -from .args import args +from .utils.shared_mem_guard import SharedMemoryGuard class DebugInputClientProcess(Process): def __init__(self, pose_position_shm: shared_memory.SharedMemory): @@ -16,12 +14,10 @@ class DebugInputClientProcess(Process): def run(self): last_time : float = time.perf_counter() interval : float = 1.0 / 60 # 60 FPS - pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) - pose_filter = OneEuroFilterNumpy(freq=60, mincutoff=args.filter_min_cutoff, beta=args.filter_beta) - position_filter = OneEuroFilterNumpy(freq=60, mincutoff=args.filter_min_cutoff, beta=args.filter_beta) while True: eyebrow_vector = [0.0] * 12 mouth_eye_vector = [0.0] * 27 @@ -53,8 +49,8 @@ class DebugInputClientProcess(Process): position_vector[2] = math.sin(time.perf_counter() * 0.7) * 0.1 position_vector[3] = 1 - with pose_position_shm_channel.lock(): - np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) - np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) + with pose_position_shm_guard.lock(): + np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) + np_position_shm[:] = np.array(position_vector, dtype=np.float32) wait_until(last_time + interval) last_time += interval \ No newline at end of file diff --git a/src/face_mesh_client.py b/src/face_mesh_client.py index 5b67cfa..68a09d8 100644 --- a/src/face_mesh_client.py +++ b/src/face_mesh_client.py @@ -3,7 +3,7 @@ from multiprocessing import Process, shared_memory, Value import cv2 import numpy as np from .args import args -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.shared_mem_guard import SharedMemoryGuard from .utils.pose import get_pose from .utils.fps import FPS from .utils.filter import OneEuroFilterNumpy @@ -19,7 +19,7 @@ class FaceMeshClientProcess(Process): def run(self): facemesh = mp.solutions.face_mesh.FaceMesh(refine_landmarks=True) - pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) @@ -70,7 +70,7 @@ class FaceMeshClientProcess(Process): mouth_eye_vector[2] = min(eye_l_h_temp, eye_r_h_temp) mouth_eye_vector[3] = min(eye_l_h_temp, eye_r_h_temp) - mouth_eye_vector[14] = mouth_ratio * 1.5 + mouth_eye_vector[14] = mouth_ratio * 2.0 mouth_eye_vector[25] = 0.0 # keep iris stable for user demo mouth_eye_vector[26] = 0.0 @@ -86,6 +86,6 @@ class FaceMeshClientProcess(Process): model_input_arr.extend(mouth_eye_vector) model_input_arr.extend(pose_vector) - with pose_position_shm_channel.lock(): + with pose_position_shm_guard.lock(): np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) np_position_shm[:] = position_vector \ No newline at end of file diff --git a/src/i_facial_mocap_client.py b/src/i_facial_mocap_client.py index a48d0b7..d877525 100644 --- a/src/i_facial_mocap_client.py +++ b/src/i_facial_mocap_client.py @@ -1,7 +1,7 @@ from multiprocessing import Process, shared_memory, Value from .args import args from .utils.timer_wait import wait_until -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.shared_mem_guard import SharedMemoryGuard import socket import errno import numpy as np @@ -20,7 +20,7 @@ class IFMClientProcess(Process): self.port = int(args.ifm.split(':')[1]) self.fps = Value('f', 0.0) def run(self): - pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) @@ -49,8 +49,8 @@ class IFMClientProcess(Process): self.socket.settimeout(0.5) # 500 ms - pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) - position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + # pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) + # position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) print("iFacialMocap Input Running at {:.2f} FPS".format(input_fps.view())) while True: @@ -90,11 +90,11 @@ class IFMClientProcess(Process): model_input_arr.extend(mouth_eye_vector) model_input_arr.extend(pose_vector) - with pose_position_shm_channel.lock(): - np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) - np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) - # np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) - # np_position_shm[:] = np.array(position_vector, dtype=np.float32) + with pose_position_shm_guard.lock(): + # np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + # np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) + np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) + np_position_shm[:] = np.array(position_vector, dtype=np.float32) @staticmethod def convert_from_blender_data(blender_data): diff --git a/src/main.py b/src/main.py index 1375c5a..fa09a59 100644 --- a/src/main.py +++ b/src/main.py @@ -4,7 +4,7 @@ from .model_infer_client import ModelClientProcess from .args import args from .utils.preprocess import resize_to_512_center, apply_color_curves import cv2 -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.shared_mem_guard import SharedMemoryGuard from multiprocessing import shared_memory from .utils.timer_wait import wait_until from PIL import Image @@ -68,7 +68,7 @@ def main(): cam_width_scale = 2 if args.alpha_split else 1 ret_channels = 3 if args.output_virtual_cam or args.output_debug else 4 ret_batch_shm_channels = [ - SharedMemoryExclusiveChannel(infer_process.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") + SharedMemoryGuard(infer_process.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") for i in range(args.interpolation_scale) ] np_ret_shms = [ @@ -109,26 +109,20 @@ def main(): elif args.output_spout2: spout_sender.send_image(np_ret_shms[i], False) else: - cv2.imshow("Output Frame Batch {}".format(i), np_ret_shms[i]) + cv2.imshow("EasyVtuber Debug Frame", np_ret_shms[i]) cv2.waitKey(1) wait_until(last_time + interval) last_time += interval ret_batch_shm_channels[i].release() - print("pipeline FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, current time {:.5f}".format( + print("Infer Process FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, Output Pipeline FPS {:.5f}".format( infer_process.pipeline_fps_number.value, input_fps.value, infer_process.average_model_interval.value * 1000, infer_process.cache_hit_ratio.value * 100, infer_process.gpu_cache_hit_ratio.value * 100, - pipeline_fps() + pipeline_fps() * args.interpolation_scale ), end ='\r', flush=True) - - - - - - if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/model_infer_client.py b/src/model_infer_client.py index 9eb5385..97be6f3 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -5,7 +5,7 @@ import numpy as np import time from .ezvtb_rt_interface import get_core from .args import args -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.shared_mem_guard import SharedMemoryGuard from .utils.pose_simplify import pose_simplify from .utils.fps import FPS, Interval from typing import List @@ -32,12 +32,12 @@ class ModelClientProcess(Process): self.finish_event = Event() def run(self): - pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) - ret_batch_shm_channels = [ - SharedMemoryExclusiveChannel(self.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") + ret_batch_shm_guard = [ + SharedMemoryGuard(self.ret_shared_mem, ctrl_name=f"ret_shm_ctrl_batch_{i}") for i in range(args.interpolation_scale) ] np_ret_shms = [ @@ -81,7 +81,7 @@ class ModelClientProcess(Process): print("Model Inference Ready") while True: - with pose_position_shm_channel.lock(): + with pose_position_shm_guard.lock(): np_pose = np_pose_shm.copy() np_position = np_position_shm.copy() @@ -116,7 +116,7 @@ class ModelClientProcess(Process): self.pipeline_fps_number.value = pipeline_fps() for i in range(args.interpolation_scale): - with ret_batch_shm_channels[i].lock(): # get pressure from main process if ret not consumed + with ret_batch_shm_guard[i].lock(): # get pressure from main process if ret not consumed np_ret_shms[i][:, :, :] = output_images[i] self.finish_event.set() # Back pressure main process loop if infer slow diff --git a/src/mouse_client.py b/src/mouse_client.py index 42e8fb1..a61436b 100644 --- a/src/mouse_client.py +++ b/src/mouse_client.py @@ -3,8 +3,7 @@ from pynput.mouse import Controller import numpy as np import time from .args import args -from .utils.filter import OneEuroFilterNumpy -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.shared_mem_guard import SharedMemoryGuard from .utils.timer_wait import wait_until class MouseClientProcess(Process): @@ -15,7 +14,7 @@ class MouseClientProcess(Process): def run(self): mouse = Controller() - pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) @@ -35,7 +34,6 @@ class MouseClientProcess(Process): interval : float = 1.0 / 60 # 60 FPS print("Mouse Input Running at 60 FPS") - pose_filter = OneEuroFilterNumpy(freq=60, mincutoff=args.filter_min_cutoff, beta=args.filter_beta) position_vector = np.array([0, 0, 0, 1], dtype=np.float32) while True: pos = mouse.position @@ -93,8 +91,8 @@ class MouseClientProcess(Process): model_input_arr.extend(mouth_eye_vector) model_input_arr.extend(pose_vector) - with pose_position_shm_channel.lock(): - np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) + with pose_position_shm_guard.lock(): + np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) np_position_shm[:] = position_vector wait_until(last_time + interval) diff --git a/src/open_see_face_client.py b/src/open_see_face_client.py index 68e3070..0be9a01 100644 --- a/src/open_see_face_client.py +++ b/src/open_see_face_client.py @@ -3,7 +3,7 @@ from .args import args import socket import struct import numpy as np -from .utils.channel_shared_mem import SharedMemoryExclusiveChannel +from .utils.shared_mem_guard import SharedMemoryGuard from .utils.fps import FPS from .utils.filter import OneEuroFilterNumpy from OneEuroFilter import OneEuroFilter @@ -18,7 +18,7 @@ class OSFClientProcess(Process): self.fps = Value('f', 0.0) def run(self): - pose_position_shm_channel = SharedMemoryExclusiveChannel(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") + pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) @@ -42,8 +42,8 @@ class OSFClientProcess(Process): pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) - iris_x_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.1, beta=0.3) # extra filter for iris movement - iris_y_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.1, beta=0.3) # extra filter for iris movement + iris_x_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.001, beta=1.0) # extra filter for iris movement + iris_y_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.001, beta=1.0) # extra filter for iris movement rotation_offset = None print("OpenSeeFace Input Running at {:.2f} FPS".format(input_fps.view())) while True: @@ -166,7 +166,7 @@ class OSFClientProcess(Process): model_input_arr.extend(mouth_eye_vector) model_input_arr.extend(pose_vector) - with pose_position_shm_channel.lock(): + with pose_position_shm_guard.lock(): np_pose_shm[:] = pose_filter(np.array(model_input_arr, dtype=np.float32)) np_position_shm[:] = position_filter(np.array(position_vector, dtype=np.float32)) # np_pose_shm[:] = np.array(model_input_arr, dtype=np.float32) diff --git a/facial_points.py b/src/utils/facial_points.py similarity index 100% rename from facial_points.py rename to src/utils/facial_points.py diff --git a/src/utils/pose.py b/src/utils/pose.py index 1bc6127..3fa76d6 100644 --- a/src/utils/pose.py +++ b/src/utils/pose.py @@ -12,9 +12,9 @@ from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYEB from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_IRIS from mediapipe.python.solutions.face_mesh_connections import FACEMESH_TESSELATION -from facial_points import MOUTH_TOP, MOUTH_BOTTOM, MOUTH_LEFT1, MOUTH_LEFT2, MOUTH_RIGHT -from facial_points import IRIS_L_TOP, IRIS_L_BOTTOM, IRIS_L_LEFT, IRIS_L_RIGHT -from facial_points import IRIS_R_TOP, IRIS_R_BOTTOM, IRIS_R_LEFT, IRIS_R_RIGHT +from .facial_points import MOUTH_TOP, MOUTH_BOTTOM, MOUTH_LEFT1, MOUTH_LEFT2, MOUTH_RIGHT +from .facial_points import IRIS_L_TOP, IRIS_L_BOTTOM, IRIS_L_LEFT, IRIS_L_RIGHT +from .facial_points import IRIS_R_TOP, IRIS_R_BOTTOM, IRIS_R_LEFT, IRIS_R_RIGHT from .utils import get_distance diff --git a/src/utils/channel_shared_mem.py b/src/utils/shared_mem_guard.py similarity index 99% rename from src/utils/channel_shared_mem.py rename to src/utils/shared_mem_guard.py index dfecece..c349d3e 100644 --- a/src/utils/channel_shared_mem.py +++ b/src/utils/shared_mem_guard.py @@ -46,7 +46,7 @@ MUTEX_ALL_ACCESS = 0x1F0001 ERROR_ALREADY_EXISTS = 183 -class SharedMemoryExclusiveChannel: +class SharedMemoryGuard: """ Two-process exclusive access wrapper using Windows Named Mutex: - payload SharedMemory is provided externally diff --git a/utils.py b/utils.py deleted file mode 100644 index 30bf9fe..0000000 --- a/utils.py +++ /dev/null @@ -1,80 +0,0 @@ -import torch -import numpy as np - - -def linear_rgb2srgb(image): - """ - convert linear rgb image to srgb image - Args: - image (numpy array): linear rgb color space image - - Returns: - image (numpy array): srgb color space image - """ - return np.where(image <= 0.04045, image / 12.92, ((image + 0.055) / 1.055) ** 2.4) - - -def srgb2linear_rgb(image): - """ - convert srgb image to linear rgb image - Args: - image (numpy array): srgb color space image - - Returns: - image (numpy array): linear rgb color space image - """ - return np.where(image <= 0.003130804953560372, image * 12.92, 1.055 * (image ** (1.0 / 2.4)) - 0.055) - - -def preprocessing_image(image): - """ - convert image(numpy array, unit8) to tensor - Args: - image (numpy array): character image (256x256, alpha channel included) - Returns: - tensor - """ - np_image = np.array(image) / 255 - clipped_image = np.clip(np_image, 0, 1) - srgb_image = linear_rgb2srgb(clipped_image) - h, w, c = srgb_image.shape - linear_image = srgb_image.reshape(h * w, c) - for pixel in linear_image: - if pixel[3] == 0.0: - pixel[0:3] = 0.0 - reshaped_image = linear_image.transpose().reshape(c, h, w) - return reshaped_image - - -def postprocessing_image(tensor): - """ - convert tensor to image(numpy array, unit8) - Args: - tensor - Returns: - image (numpy array): character image (256x256, alpha channel included) - """ - tensor = tensor.detach().squeeze(0) - reshaped_tensor = tensor.permute(1, 2, 0) - np_image = reshaped_tensor.numpy() - np_image = (np_image + 1) / 2 - srgb_image = np_image[..., :3] - alpha_image = np_image[..., 3] - clipped_image = np.clip(srgb_image, 0, 1) - rgb_image = srgb2linear_rgb(clipped_image) - rgba_image = np.concatenate([rgb_image, alpha_image[..., np.newaxis]], axis=2) - rgba_image = rgba_image * 255 - return rgba_image.astype(np.uint8) - - -def get_distance(a, b): - """ - calculate euclidean distance a to b - Args: - a (landmark): 3d points - b (landmark): 3d points - - Returns: - L2 distance (float) - """ - return np.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) From 8882e4af110fc79879b358d9d259f58fb12c4754 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Mon, 29 Dec 2025 15:48:49 -0500 Subject: [PATCH 26/46] feat:add filter args --- launcher2.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++----- src/args.py | 3 ++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/launcher2.py b/launcher2.py index 24f0a49..99db2e1 100644 --- a/launcher2.py +++ b/launcher2.py @@ -29,10 +29,12 @@ cache_simplify_quality_map = { } default_arg = { 'character': 'lambda_00', - 'input': 2, + 'input': 3, 'output': 2, 'ifm': None, 'osf': '127.0.0.1:11573', + 'min_cutoff': 50, + 'beta': 80, 'is_extend_movement': False, 'is_alpha_split': False, 'is_bongo': False, @@ -42,11 +44,11 @@ default_arg = { 'ram_cache_size': '2gb', 'vram_cache_size': '2gb', 'model_select': 'seperable_half', - 'interpolation': 'x2_half', + 'interpolation': 'x3_half', 'frame_rate_limit': '30', - 'sr': 'Off', + 'sr': 'anime4k_x2', 'use_tensorrt': False, - 'preset': 'Medium' + 'preset': 'Low' } try: @@ -116,9 +118,32 @@ def scanStudentModels(): refreshList() scanStudentModels() +def min_cutoff_mapper(value, revert=False): + """ + 非线性映射函数:0-100整数 <-> 0-100浮点数 + 使用平方函数,使得越接近0数字越密集 + """ + if revert: + # 浮点 -> 整数: 使用平方根反向映射 + return int((value / 100.0) ** 0.5 * 100) + # 整数 -> 浮点: 使用平方映射 + return (value / 100.0) ** 2 * 100.0 + + +def beta_mapper(value, revert=False): + """ + 非线性映射函数:0-100整数 <-> 0-1浮点数 + 使用平方函数,使得越接近0数字越密集 + """ + if revert: + # 浮点 -> 整数: 使用平方根反向映射 + return int((value ** 0.5) * 100) + # 整数 -> 浮点: 使用平方映射 + return (value / 100.0) ** 2 + class OptionPanel(wx.Panel): - def __init__(self, parent, title='', desc='', choices=None, mapping=None, type=0, default=None, disabled=False): + def __init__(self, parent, title='', desc='', choices=None, mapping=None, type=0, default=None, disabled=False, mapper=min_cutoff_mapper): wx.Panel.__init__(self, parent) self.type = type if mapping is not None: @@ -172,8 +197,38 @@ class OptionPanel(wx.Panel): self.control.SetValue(default) except: pass + elif self.type == 3: + # Slider type for float values 0.0 to 1.0 + sliderSizer = wx.BoxSizer(wx.HORIZONTAL) + self.control = wx.Slider(self, wx.ID_ANY, value=50, minValue=0, maxValue=100, + style=wx.SL_HORIZONTAL) + self.control.SetMinSize(wx.Size(250, -1)) + + # Add a label to show the float value + self.valueLabel = wx.StaticText(self, wx.ID_ANY, "0.50") + self.valueLabel.SetMinSize(wx.Size(50, -1)) + + try: + if default is not None: + self.control.SetValue(default) + self.valueLabel.SetLabelText(f"{mapper(default):.2f}") + except: + pass + + # Update label when slider changes + def onSliderChange(event): + val = mapper(self.control.GetValue()) + self.valueLabel.SetLabelText(f"{val:.2f}") + self.control.Bind(wx.EVT_SLIDER, onSliderChange) + + sliderSizer.Add(self.control, 1, wx.ALIGN_CENTER_VERTICAL) + sliderSizer.Add(self.valueLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10) + mainSizer.Add(sliderSizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) + # Skip the normal control addition below + self.control._slider_added = True - mainSizer.Add(self.control, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) + if not (self.type == 3 and hasattr(self.control, '_slider_added')): + mainSizer.Add(self.control, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) # self.SetBackgroundColour('#000000') def GetValue(self): @@ -183,6 +238,8 @@ class OptionPanel(wx.Panel): ret = self.control.GetValue() elif self.type == 2: ret = self.control.GetValue() + elif self.type == 3: + ret = self.control.GetValue() if self.mapping is not None: return self.mapping[ret] else: @@ -245,6 +302,10 @@ class LauncherPanel(wx.Panel): addOption('is_eyebrow', title='Eyebrow', desc='使用眉毛输入,对性能有一定影响\n仅支持iFacialMocap', type=1, default=True) addOption('osf', title='OpenSeeFace IP:Port', desc='输入OpenSeeFace连接使用的IP:端口号', type=2) + addOption('min_cutoff', title='Min CutOff', desc='输入滤波频率截断,\n越小越平滑,越大静止时越灵敏', + type=3, mapper=min_cutoff_mapper) + addOption('beta', title='Beta', desc='输入滤波速度补偿,\n越小越平滑,越大快速运动时越灵敏', + type=3, mapper=beta_mapper) addOption('output', title='Output', desc='选择输出目标', choices=['Spout2', 'OBS VirtualCam', 'Debug Output'], @@ -312,6 +373,13 @@ class LauncherPanel(wx.Panel): self.optionSizer.Hide(self.optionDict['osf']) else: self.optionSizer.Show(self.optionDict['osf']) + if s != 1 and s != 4: + self.optionSizer.Hide(self.optionDict['min_cutoff']) + self.optionSizer.Hide(self.optionDict['beta']) + else: + self.optionSizer.Show(self.optionDict['min_cutoff']) + self.optionSizer.Show(self.optionDict['beta']) + self.frame.fSizer.Layout() self.frame.Fit() @@ -340,7 +408,7 @@ class LauncherPanel(wx.Panel): for c in presetControls: self.optionSizer.Hide(c) if s in presets: opt = presets[s] - for i in range(5): presetControls[i].control.SetSelection(opt[i]) + for i in range(4): presetControls[i].control.SetSelection(opt[i]) self.frame.fSizer.Layout() self.frame.Fit() @@ -511,6 +579,12 @@ class LauncherPanel(wx.Panel): if args['use_tensorrt'] is not None and args['use_tensorrt']: run_args.append('--use_tensorrt') + run_args.append('--filter_min_cutoff') + run_args.append(str(min_cutoff_mapper(args['min_cutoff']))) + + run_args.append('--filter_beta') + run_args.append(str(beta_mapper(args['beta']))) + print('Launched: ' + ' '.join(run_args)) p = subprocess.Popen(run_args) self.btnLaunch.SetLabelText('Stop') diff --git a/src/args.py b/src/args.py index 7571675..22ed258 100644 --- a/src/args.py +++ b/src/args.py @@ -74,6 +74,9 @@ if args.simplify == 0: if not args.output_virtual_cam and not args.output_spout2: args.output_debug = True # Default to debug output +if args.output_spout2: + args.alpha_split = False # Disable alpha split for spout2 output + if not args.cam_input and args.mouse_input is None \ and args.ifm_input is None and args.osf_input is None: args.debug_input = True # Default to debug input From e91d42374b4ada93e9aa60075da6cf68bf3a7452 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Mon, 29 Dec 2025 17:16:26 -0500 Subject: [PATCH 27/46] feat: mouse input plus audio mouth --- launcher2.py | 48 +++++++++++++++++++++++++++++++++++----- src/args.py | 4 ++++ src/mouse_client.py | 53 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/launcher2.py b/launcher2.py index 99db2e1..f449e0f 100644 --- a/launcher2.py +++ b/launcher2.py @@ -48,7 +48,10 @@ default_arg = { 'frame_rate_limit': '30', 'sr': 'anime4k_x2', 'use_tensorrt': False, - 'preset': 'Low' + 'preset': 'Low', + 'mouse_audio_input': False, + 'audio_sensitivity': '0.02', + 'audio_threshold': '10.0' } try: @@ -211,14 +214,14 @@ class OptionPanel(wx.Panel): try: if default is not None: self.control.SetValue(default) - self.valueLabel.SetLabelText(f"{mapper(default):.2f}") + self.valueLabel.SetLabelText(f"{mapper(default):.4f}") except: pass # Update label when slider changes def onSliderChange(event): val = mapper(self.control.GetValue()) - self.valueLabel.SetLabelText(f"{val:.2f}") + self.valueLabel.SetLabelText(f"{val:.4f}") self.control.Bind(wx.EVT_SLIDER, onSliderChange) sliderSizer.Add(self.control, 1, wx.ALIGN_CENTER_VERTICAL) @@ -302,9 +305,12 @@ class LauncherPanel(wx.Panel): addOption('is_eyebrow', title='Eyebrow', desc='使用眉毛输入,对性能有一定影响\n仅支持iFacialMocap', type=1, default=True) addOption('osf', title='OpenSeeFace IP:Port', desc='输入OpenSeeFace连接使用的IP:端口号', type=2) + addOption('mouse_audio_input', title='Audio Input', desc='启用WASAPI音频输入控制嘴部动作', type=1) + addOption('audio_sensitivity', title='Audio Sensitivity', desc='音频灵敏度,控制音频对嘴部动作的影响程度', type=2) + addOption('audio_threshold', title='Audio Threshold', desc='音频阈值,低于此值的音频将被忽略', type=2) addOption('min_cutoff', title='Min CutOff', desc='输入滤波频率截断,\n越小越平滑,越大静止时越灵敏', type=3, mapper=min_cutoff_mapper) - addOption('beta', title='Beta', desc='输入滤波速度补偿,\n越小越平滑,越大快速运动时越灵敏', + addOption('beta', title='Beta', desc='输入滤波速度补偿,\n越小越平滑,越大运动时越灵敏', type=3, mapper=beta_mapper) addOption('output', title='Output', desc='选择输出目标', @@ -379,12 +385,35 @@ class LauncherPanel(wx.Panel): else: self.optionSizer.Show(self.optionDict['min_cutoff']) self.optionSizer.Show(self.optionDict['beta']) - + # Show/hide audio input options for Mouse Input (s == 3) + if s != 3: + self.optionSizer.Hide(self.optionDict['mouse_audio_input']) + self.optionSizer.Hide(self.optionDict['audio_sensitivity']) + self.optionSizer.Hide(self.optionDict['audio_threshold']) + else: + self.optionSizer.Show(self.optionDict['mouse_audio_input']) + # Update audio fields based on checkbox state + audioInputChoice() self.frame.fSizer.Layout() self.frame.Fit() + def audioInputChoice(e=None): + """Handle mouse_audio_input checkbox changes""" + enabled = self.optionDict['mouse_audio_input'].GetValue() + if enabled: + self.optionSizer.Show(self.optionDict['audio_sensitivity']) + self.optionSizer.Show(self.optionDict['audio_threshold']) + self.optionDict['audio_sensitivity'].control.Enable(True) + self.optionDict['audio_threshold'].control.Enable(True) + else: + self.optionSizer.Hide(self.optionDict['audio_sensitivity']) + self.optionSizer.Hide(self.optionDict['audio_threshold']) + self.frame.fSizer.Layout() + self.frame.Fit() + self.optionDict['input'].Bind(wx.EVT_CHOICE, inputChoice) + self.optionDict['mouse_audio_input'].Bind(wx.EVT_CHECKBOX, audioInputChoice) inputChoice() def presetChoice(e=None): @@ -495,6 +524,15 @@ class LauncherPanel(wx.Panel): elif args['input'] == 3: run_args.append('--mouse_input') run_args.append('0,0,' + str(wx.GetDisplaySize().width) + ',' + str(wx.GetDisplaySize().height)) + # Add audio input options for mouse input + if args['mouse_audio_input']: + run_args.append('--mouse_audio_input') + if args['audio_sensitivity']: + run_args.append('--audio_sensitivity') + run_args.append(str(args['audio_sensitivity'])) + if args['audio_threshold']: + run_args.append('--audio_threshold') + run_args.append(str(args['audio_threshold'])) elif args['input'] == 4: if len(args['osf']): run_args.append('--osf_input') diff --git a/src/args.py b/src/args.py index 22ed258..d7ad390 100644 --- a/src/args.py +++ b/src/args.py @@ -21,6 +21,10 @@ parser.add_argument('--mouse_input', type=str) parser.add_argument('--ifm_input', type=str) parser.add_argument('--osf_input', type=str) +parser.add_argument('--mouse_audio_input', action='store_true') +parser.add_argument('--audio_sensitivity', type=float, default=0.02) +parser.add_argument('--audio_threshold', type=float, default=10.0) + parser.add_argument('--output_virtual_cam', action='store_true') parser.add_argument('--output_spout2', action='store_true') parser.add_argument('--output_debug', action='store_true') diff --git a/src/mouse_client.py b/src/mouse_client.py index a61436b..c08d38b 100644 --- a/src/mouse_client.py +++ b/src/mouse_client.py @@ -2,15 +2,40 @@ from multiprocessing import Process, shared_memory, Value from pynput.mouse import Controller import numpy as np import time +import sounddevice as sd from .args import args from .utils.shared_mem_guard import SharedMemoryGuard from .utils.timer_wait import wait_until +from OneEuroFilter import OneEuroFilter +from .utils.fps import FPS +from .utils.timer_wait import wait_until class MouseClientProcess(Process): def __init__(self, pose_position_shm: shared_memory.SharedMemory): super().__init__() self.pose_position_shm = pose_position_shm self.fps = Value('f', 60.0) + self.audio_volume = Value('f', 0.0) + self.audio_running = False + self.audio_callback_fps = FPS(60) + + def audio_callback(self, indata, frames, time_info, status): + """音频回调函数,计算音量""" + if status: + print(f"Audio status: {status}") + + self.audio_callback_fps() + # 计算 RMS (均方根) 音量 + volume_norm = np.linalg.norm(indata) * 10 + + # 应用阈值和灵敏度 + if volume_norm < args.audio_threshold: + volume_norm = 0 + else: + volume_norm = (volume_norm - args.audio_threshold) * args.audio_sensitivity + # 限制在 0-1 范围 + volume_norm = np.clip(volume_norm, 0, 1) + self.audio_volume.value = volume_norm def run(self): mouse = Controller() @@ -20,6 +45,29 @@ class MouseClientProcess(Process): # posLimit = [0, 0, 1920, 1080] posLimit = [int(x) for x in args.mouse_input.split(',')] + + if args.mouse_audio_input: + # 启动音频流 (使用 WASAPI) + try: + # 获取默认输入设备(麦克风或系统音频) + # 如果要捕获系统音频输出,需要使用 loopback 设备 + audio_stream = sd.InputStream( + callback=self.audio_callback, + channels=1, + samplerate=16000, + blocksize=1024, + ) + audio_stream.start() + self.audio_running = True + wait_until(time.perf_counter() + 1.0) # 等待一秒以稳定音频输入 + print("Audio capture started (WASAPI)") + except Exception as e: + print(f"Failed to start audio capture: {e}") + print("Continuing without audio input...") + self.audio_running = False + + audio_filter = OneEuroFilter(freq=self.audio_callback_fps.view(), mincutoff=10.0, beta=0.0) + prev = { 'eye_l_h_temp': 0, 'eye_r_h_temp': 0, @@ -41,10 +89,13 @@ class MouseClientProcess(Process): eye_limit = [0.8, 0.5] head_eye_reduce = 0.6 head_slowness = 0.2 + + # 获取音频音量并映射到 mouth_ratio + current_mouth_ratio = audio_filter(self.audio_volume.value, time.perf_counter()) if self.audio_running else 0 mouse_data = { 'eye_l_h_temp': 0, 'eye_r_h_temp': 0, - 'mouth_ratio': 0, + 'mouth_ratio': current_mouth_ratio, 'eye_y_ratio': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]) * eye_limit[1], 'eye_x_ratio': np.interp(pos[0], [posLimit[0], posLimit[2]], [1, -1]) * eye_limit[0], 'x_angle': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]), From 9b0deb105b56e53653a3ec69a6426bb77a0c5563 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Mon, 29 Dec 2025 22:39:53 -0500 Subject: [PATCH 28/46] feat:update requirements --- ezvtuber-rt | 2 +- requirements.txt | 7 +++++-- src/mouse_client.py | 3 +-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 2ea780d..16c6b28 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 2ea780d884afb9740a9c23e6754bc46031eb1e1f +Subproject commit 16c6b289344281a612d038f5fd0f1433d89dc11f diff --git a/requirements.txt b/requirements.txt index ac93f95..ba3c98a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ matplotlib protobuf==3.20.1 pynput==1.7.6 mediapipe==0.10.11 -opencv_python==4.5.5.64 +opencv_python Pillow pyanime4k==3.1.0 pyvirtualcam==0.9.1 @@ -15,4 +15,7 @@ onnxruntime-directml turbojpeg tqdm torch -OneEuroFilter \ No newline at end of file +OneEuroFilter +sounddevice +pycuda +brotli \ No newline at end of file diff --git a/src/mouse_client.py b/src/mouse_client.py index c08d38b..8a0ad75 100644 --- a/src/mouse_client.py +++ b/src/mouse_client.py @@ -65,8 +65,7 @@ class MouseClientProcess(Process): print(f"Failed to start audio capture: {e}") print("Continuing without audio input...") self.audio_running = False - - audio_filter = OneEuroFilter(freq=self.audio_callback_fps.view(), mincutoff=10.0, beta=0.0) + audio_filter = OneEuroFilter(freq=self.audio_callback_fps.view(), mincutoff=10.0, beta=0.0) prev = { 'eye_l_h_temp': 0, From d9bb9d5a62af0f241dbdc92029c1ef65fd1aa08b Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Tue, 30 Dec 2025 11:37:00 -0500 Subject: [PATCH 29/46] feat:rife cached --- ezvtuber-rt | 2 +- src/model_infer_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 16c6b28..add0a87 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 16c6b289344281a612d038f5fd0f1433d89dc11f +Subproject commit add0a87a1a874c6e35881f425286afa453282c9e diff --git a/src/model_infer_client.py b/src/model_infer_client.py index 97be6f3..4f57e88 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -147,7 +147,7 @@ class ModelClientProcess(Process): (bgra_image.shape[1], bgra_image.shape[0])) if args.output_debug: - cv2.putText(bgra_image, str('OUT_FPS:%.1f' % self.pipeline_fps_number.value), (0, 16), cv2.FONT_HERSHEY_PLAIN, 1, + cv2.putText(bgra_image, str('OUT_FPS:%.1f' % (self.pipeline_fps_number.value * args.interpolation_scale)), (0, 16), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) cv2.putText(bgra_image, str( 'GPU_FPS:%.1f' % ( From 90c2749bce2cc2689389ab9fe1df409fbd145597 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Tue, 30 Dec 2025 12:53:17 -0500 Subject: [PATCH 30/46] feat:add blink and breath cycle --- launcher2.py | 22 +++++++++++++++++++++- src/args.py | 2 ++ src/face_mesh_client.py | 4 ++-- src/mouse_client.py | 38 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/launcher2.py b/launcher2.py index f449e0f..e5b6975 100644 --- a/launcher2.py +++ b/launcher2.py @@ -51,7 +51,9 @@ default_arg = { 'preset': 'Low', 'mouse_audio_input': False, 'audio_sensitivity': '0.02', - 'audio_threshold': '10.0' + 'audio_threshold': '10.0', + 'blink_interval': '5.0', + 'breath_cycle': 'inf' } try: @@ -308,11 +310,18 @@ class LauncherPanel(wx.Panel): addOption('mouse_audio_input', title='Audio Input', desc='启用WASAPI音频输入控制嘴部动作', type=1) addOption('audio_sensitivity', title='Audio Sensitivity', desc='音频灵敏度,控制音频对嘴部动作的影响程度', type=2) addOption('audio_threshold', title='Audio Threshold', desc='音频阈值,低于此值的音频将被忽略', type=2) + addOption('blink_interval', title='Blink Interval', desc='设置眨眼间隔时间', + choices=['No Blink', '3 seconds', '5 seconds', '7 seconds'], + mapping=['inf', '3.0', '5.0', '7.0']) addOption('min_cutoff', title='Min CutOff', desc='输入滤波频率截断,\n越小越平滑,越大静止时越灵敏', type=3, mapper=min_cutoff_mapper) addOption('beta', title='Beta', desc='输入滤波速度补偿,\n越小越平滑,越大运动时越灵敏', type=3, mapper=beta_mapper) + addOption('breath_cycle', title='Breath Cycle', desc='设置呼吸循环时间', + choices=['No Breath', '3 seconds', '5 seconds', '7 seconds'], + mapping=['inf', '3.0', '5.0', '7.0']) + addOption('output', title='Output', desc='选择输出目标', choices=['Spout2', 'OBS VirtualCam', 'Debug Output'], mapping=[0, 1, 2]) @@ -390,8 +399,10 @@ class LauncherPanel(wx.Panel): self.optionSizer.Hide(self.optionDict['mouse_audio_input']) self.optionSizer.Hide(self.optionDict['audio_sensitivity']) self.optionSizer.Hide(self.optionDict['audio_threshold']) + self.optionSizer.Hide(self.optionDict['blink_interval']) else: self.optionSizer.Show(self.optionDict['mouse_audio_input']) + self.optionSizer.Show(self.optionDict['blink_interval']) # Update audio fields based on checkbox state audioInputChoice() @@ -533,11 +544,20 @@ class LauncherPanel(wx.Panel): if args['audio_threshold']: run_args.append('--audio_threshold') run_args.append(str(args['audio_threshold'])) + # Add blink interval for mouse input + if args['blink_interval']: + run_args.append('--blink_interval') + run_args.append(str(args['blink_interval'])) elif args['input'] == 4: if len(args['osf']): run_args.append('--osf_input') run_args.append(args['osf']) + # Add breath cycle option + if args['breath_cycle']: + run_args.append('--breath_cycle') + run_args.append(str(args['breath_cycle'])) + if args['output'] == 0: run_args.append('--output_spout2') elif args['output'] == 1: diff --git a/src/args.py b/src/args.py index d7ad390..6800e06 100644 --- a/src/args.py +++ b/src/args.py @@ -24,6 +24,8 @@ parser.add_argument('--osf_input', type=str) parser.add_argument('--mouse_audio_input', action='store_true') parser.add_argument('--audio_sensitivity', type=float, default=0.02) parser.add_argument('--audio_threshold', type=float, default=10.0) +parser.add_argument('--blink_interval', type=float, default=5.0) +parser.add_argument('--breath_cycle', type=float, default=float('inf')) parser.add_argument('--output_virtual_cam', action='store_true') parser.add_argument('--output_spout2', action='store_true') diff --git a/src/face_mesh_client.py b/src/face_mesh_client.py index 68a09d8..a5352f3 100644 --- a/src/face_mesh_client.py +++ b/src/face_mesh_client.py @@ -67,8 +67,8 @@ class FaceMeshClientProcess(Process): mouth_eye_vector = [0.0] * 27 pose_vector = [0.0] * 6 - mouth_eye_vector[2] = min(eye_l_h_temp, eye_r_h_temp) - mouth_eye_vector[3] = min(eye_l_h_temp, eye_r_h_temp) + mouth_eye_vector[2] = max(eye_l_h_temp, eye_r_h_temp) + mouth_eye_vector[3] = max(eye_l_h_temp, eye_r_h_temp) mouth_eye_vector[14] = mouth_ratio * 2.0 diff --git a/src/mouse_client.py b/src/mouse_client.py index 8a0ad75..62c92f8 100644 --- a/src/mouse_client.py +++ b/src/mouse_client.py @@ -79,6 +79,14 @@ class MouseClientProcess(Process): } last_time : float = time.perf_counter() interval : float = 1.0 / 60 # 60 FPS + + # 眨眼相关参数 + blink_interval = args.blink_interval # 眨眼间隔时间(秒) + blink_duration = 0.25 # 眨眼持续时间(秒) + last_blink_time = time.perf_counter() + + # 呼吸循环参数 + breath_start_time = time.perf_counter() print("Mouse Input Running at 60 FPS") position_vector = np.array([0, 0, 0, 1], dtype=np.float32) @@ -89,11 +97,36 @@ class MouseClientProcess(Process): head_eye_reduce = 0.6 head_slowness = 0.2 + # 计算眨眼效果 + current_time = time.perf_counter() + time_since_last_blink = current_time - last_blink_time + + # 每隔 blink_interval 秒眨一次眼 + if time_since_last_blink >= blink_interval: + last_blink_time = current_time + time_since_last_blink = 0 + + # 计算眨眼动画(使用尖锐的 sin 函数) + if time_since_last_blink < blink_duration: + # 在眨眼持续时间内,使用 sin 函数从 0 升到接近 1 再返回 0 + # 使用 sin^2 使曲线更尖锐 + blink_progress = time_since_last_blink / blink_duration * np.pi + blink_value = np.sin(blink_progress) ** 2 + eye_close = blink_value * 1.0 # 升到1,返回到 0 + else: + eye_close = 0.0 + + # 计算呼吸效果(使用 sin 函数,在 breath_cycle 时间内从 0 到 1 再到 0) + breath_elapsed = (current_time - breath_start_time) % args.breath_cycle + # 使用 sin 函数,让值在一个周期内从 0 -> 1 -> 0 + # sin 在 0 到 π 之间从 0 到 1 到 0 + breath_value = np.sin(breath_elapsed / args.breath_cycle * np.pi) + # 获取音频音量并映射到 mouth_ratio current_mouth_ratio = audio_filter(self.audio_volume.value, time.perf_counter()) if self.audio_running else 0 mouse_data = { - 'eye_l_h_temp': 0, - 'eye_r_h_temp': 0, + 'eye_l_h_temp': eye_close, + 'eye_r_h_temp': eye_close, 'mouth_ratio': current_mouth_ratio, 'eye_y_ratio': np.interp(pos[1], [posLimit[1], posLimit[3]], [1, -1]) * eye_limit[1], 'eye_x_ratio': np.interp(pos[0], [posLimit[0], posLimit[2]], [1, -1]) * eye_limit[0], @@ -136,6 +169,7 @@ class MouseClientProcess(Process): pose_vector[2] = z_angle pose_vector[3] = pose_vector[1] pose_vector[4] = pose_vector[2] + pose_vector[5] = breath_value model_input_arr = eyebrow_vector model_input_arr.extend(mouth_eye_vector) From f5eb7b73f5afa2d4522e1fe98ecece698c353762 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Sun, 1 Feb 2026 22:43:47 -0500 Subject: [PATCH 31/46] feat:update trt to trt_rtx to get better performance --- ...ž„建TensorRT模型(N卡开启TensorRT使用).bat | 8 ----- ezvtuber-rt | 2 +- launcher2.py | 34 +++++++++++-------- onnx_export_tha4_student.py | 7 ++-- requirements.txt | 6 ++-- src/debug_input_client.py | 11 +++++- src/face_mesh_client.py | 11 +++++- src/i_facial_mocap_client.py | 14 ++++++-- src/open_see_face_client.py | 10 +++++- 9 files changed, 66 insertions(+), 37 deletions(-) delete mode 100644 02.构建TensorRT模型(N卡开启TensorRT使用).bat diff --git a/02.构建TensorRT模型(N卡开启TensorRT使用).bat b/02.构建TensorRT模型(N卡开启TensorRT使用).bat deleted file mode 100644 index 09a61de..0000000 --- a/02.构建TensorRT模型(N卡开启TensorRT使用).bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo on -cd /D "%~dp0" - -SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% - -python ezvtb_rt_interface.py - -pause \ No newline at end of file diff --git a/ezvtuber-rt b/ezvtuber-rt index add0a87..fb74e42 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit add0a87a1a874c6e35881f425286afa453282c9e +Subproject commit fb74e4224c1e39cef1b757c6da87b82c91a006ad diff --git a/launcher2.py b/launcher2.py index e5b6975..b798028 100644 --- a/launcher2.py +++ b/launcher2.py @@ -72,14 +72,14 @@ characterList = [] studentModelList = [] studentModelCharacterMap = {} -hasRTModel = False -try: - f = open('data/models/tha3/standard/fp16/decomposer.trt') - f.close() - hasRTModel = True -except: - pass - +def is_nvidia_gpu(): + try: + # 获取显卡名称列表 + output = subprocess.check_output("wmic path Win32_VideoController get Name", shell=True).decode('gbk') + return "NVIDIA" in output.upper() + except Exception: + return False +hasTRTSupport = is_nvidia_gpu() def refreshList(): global characterList @@ -303,8 +303,8 @@ class LauncherPanel(wx.Panel): addOption('input', title='Input Device', desc='选择希望使用的面捕数据源', choices=['iFacialMocap', 'OpenSeeFace', 'OpenCV(Webcam)', 'Mouse Input', 'Debug Input'], mapping=[0, 4, 1, 3, 2]) - addOption('ifm', title='iFacialMocap IP:Port', desc='输入iFacialMocap连接使用的IP:端口号', type=2) - addOption('is_eyebrow', title='Eyebrow', desc='使用眉毛输入,对性能有一定影响\n仅支持iFacialMocap', type=1, + addOption('ifm', title='iFacialMocap IP', desc='输入iFacialMocap连接使用的IP地址,默认连接 49983 端口', type=2) + addOption('is_eyebrow', title='Eyebrow', desc='使用眉毛输入,对性能有一定影响', type=1, default=True) addOption('osf', title='OpenSeeFace IP:Port', desc='输入OpenSeeFace连接使用的IP:端口号', type=2) addOption('mouse_audio_input', title='Audio Input', desc='启用WASAPI音频输入控制嘴部动作', type=1) @@ -326,8 +326,8 @@ class LauncherPanel(wx.Panel): choices=['Spout2', 'OBS VirtualCam', 'Debug Output'], mapping=[0, 1, 2]) - addOption('use_tensorrt', title='TensorRT', - desc='运行过预构建脚本后开启TensorRT可进一步提升性能\n(未构建成功时不可用,仅NVIDIA显卡支持)', + addOption('use_tensorrt', title='TensorRT加速', + desc='需要更长启动和预热时间(仅NVIDIA显卡支持)', type=1) addOption('frame_rate_limit', title='FPS Limit', desc='选择帧率限制目标', @@ -388,6 +388,7 @@ class LauncherPanel(wx.Panel): self.optionSizer.Hide(self.optionDict['osf']) else: self.optionSizer.Show(self.optionDict['osf']) + self.optionSizer.Show(self.optionDict['is_eyebrow']) if s != 1 and s != 4: self.optionSizer.Hide(self.optionDict['min_cutoff']) self.optionSizer.Hide(self.optionDict['beta']) @@ -495,11 +496,11 @@ class LauncherPanel(wx.Panel): if characterList: self.optionDict['character'].control.SetSelection(0) - if not hasRTModel: + if not hasTRTSupport: self.optionDict['use_tensorrt'].control.SetValue(False) self.optionDict['use_tensorrt'].control.Enable(False) self.optionDict['use_tensorrt'].control.SetToolTip( - '需要先构建TensorRT模型') + '需要NVIDIA显卡支持才能使用TensorRT') self.frame.Bind(wx.EVT_ACTIVATE, onActivate) @@ -527,7 +528,10 @@ class LauncherPanel(wx.Panel): if args['input'] == 0: if len(args['ifm']): run_args.append('--ifm_input') - run_args.append(args['ifm']) + if ':' in args['ifm']: + run_args.append(args['ifm']) + else: + run_args.append(args['ifm'] + ':49983') elif args['input'] == 1: run_args.append('--cam_input') elif args['input'] == 2: diff --git a/onnx_export_tha4_student.py b/onnx_export_tha4_student.py index 4109af1..b6ca3a7 100644 --- a/onnx_export_tha4_student.py +++ b/onnx_export_tha4_student.py @@ -272,9 +272,9 @@ class BodyMorpherWrapper(Module): """ # Convert image to normalized format img_f = img.to(export_dtype) / 255.0 - img_f[:, :, :3] = torch_srgb_to_linear(img_f[:, :, :3]) - img_f = img_f[:, :, [2, 1, 0, 3]] # BGR to RGB - img_c = img_f.permute(2, 0, 1).unsqueeze(0) * 2.0 - 1.0 + img_f[0, :, :, :3] = torch_srgb_to_linear(img_f[0, :, :, :3]) + img_f = img_f[:, :, :, [2, 1, 0, 3]] # BGR to RGB + img_c = img_f.permute(0, 3, 1, 2) * 2.0 - 1.0 # Embed face morpher output into the full image center_x = 256 @@ -301,6 +301,7 @@ body_morpher_wrapper = BodyMorpherWrapper(export_modules['body_morpher']).eval() full_pose_export = torch.zeros(1, 45, dtype=export_dtype) +cv_img_tensor_export = cv_img_tensor_export.unsqueeze(0) # Export with torch.no_grad(): body_morpher_res_export = body_morpher_wrapper(cv_img_tensor_export, face_morpher_res_export, full_pose_export) diff --git a/requirements.txt b/requirements.txt index ba3c98a..4671f68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ numpy scipy matplotlib -protobuf==3.20.1 -pynput==1.7.6 +protobuf +pynput mediapipe==0.10.11 opencv_python Pillow @@ -12,10 +12,8 @@ pyopengl wxpython onnx onnxruntime-directml -turbojpeg tqdm torch OneEuroFilter sounddevice -pycuda brotli \ No newline at end of file diff --git a/src/debug_input_client.py b/src/debug_input_client.py index 09e0323..48ad7b6 100644 --- a/src/debug_input_client.py +++ b/src/debug_input_client.py @@ -4,6 +4,7 @@ import time import math from .utils.timer_wait import wait_until from .utils.shared_mem_guard import SharedMemoryGuard +from .args import args class DebugInputClientProcess(Process): def __init__(self, pose_position_shm: shared_memory.SharedMemory): @@ -17,13 +18,20 @@ class DebugInputClientProcess(Process): pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) - + # 呼吸循环参数 + breath_start_time = time.perf_counter() while True: eyebrow_vector = [0.0] * 12 mouth_eye_vector = [0.0] * 27 pose_vector = [0.0] * 6 position_vector = [0, 0, 0, 1] + # 计算呼吸效果(使用 sin 函数,在 breath_cycle 时间内从 0 到 1 再到 0) + breath_elapsed = (time.perf_counter() - breath_start_time) % args.breath_cycle + # 使用 sin 函数,让值在一个周期内从 0 -> 1 -> 0 + # sin 在 0 到 π 之间从 0 到 1 到 0 + breath_value = np.sin(breath_elapsed / args.breath_cycle * np.pi) + mouth_eye_vector[2] = math.sin(time.perf_counter() * 3) mouth_eye_vector[3] = math.sin(time.perf_counter() * 3) @@ -37,6 +45,7 @@ class DebugInputClientProcess(Process): pose_vector[2] = math.sin(time.perf_counter() * 1.5) pose_vector[3] = pose_vector[1] pose_vector[4] = pose_vector[2] + pose_vector[5] = breath_value eyebrow_vector[6] = math.sin(time.perf_counter() * 1.1) eyebrow_vector[7] = math.sin(time.perf_counter() * 1.1) diff --git a/src/face_mesh_client.py b/src/face_mesh_client.py index a5352f3..bf752c4 100644 --- a/src/face_mesh_client.py +++ b/src/face_mesh_client.py @@ -38,7 +38,8 @@ class FaceMeshClientProcess(Process): input_fps() frame_count += 1 - + # 呼吸循环参数 + breath_start_time = time.perf_counter() pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) position_offset = None print("Webcam Input Running at {:.2f} FPS".format(input_fps.view())) @@ -52,6 +53,13 @@ class FaceMeshClientProcess(Process): results = facemesh.process(rgb_frame) if results.multi_face_landmarks is None: continue + + # 计算呼吸效果(使用 sin 函数,在 breath_cycle 时间内从 0 到 1 再到 0) + breath_elapsed = (time.perf_counter() - breath_start_time) % args.breath_cycle + # 使用 sin 函数,让值在一个周期内从 0 -> 1 -> 0 + # sin 在 0 到 π 之间从 0 到 1 到 0 + breath_value = np.sin(breath_elapsed / args.breath_cycle * np.pi) + facial_landmarks = results.multi_face_landmarks[0].landmark pose = get_pose(facial_landmarks) eye_l_h_temp = pose[0] @@ -81,6 +89,7 @@ class FaceMeshClientProcess(Process): pose_vector[2] = (z_angle + 1.5) * 2 - position_offset[2] # temp weight pose_vector[3] = pose_vector[1] pose_vector[4] = pose_vector[2] + pose_vector[5] = breath_value model_input_arr = eyebrow_vector model_input_arr.extend(mouth_eye_vector) diff --git a/src/i_facial_mocap_client.py b/src/i_facial_mocap_client.py index d877525..f812a31 100644 --- a/src/i_facial_mocap_client.py +++ b/src/i_facial_mocap_client.py @@ -16,8 +16,8 @@ class IFMClientProcess(Process): def __init__(self, pose_position_shm: shared_memory.SharedMemory): super().__init__() self.pose_position_shm = pose_position_shm - self.address = args.ifm.split(':')[0] - self.port = int(args.ifm.split(':')[1]) + self.address = args.ifm_input.split(':')[0] + self.port = int(args.ifm_input.split(':')[1]) self.fps = Value('f', 0.0) def run(self): pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") @@ -51,7 +51,8 @@ class IFMClientProcess(Process): # pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) # position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) - + # 呼吸循环参数 + breath_start_time = time.perf_counter() print("iFacialMocap Input Running at {:.2f} FPS".format(input_fps.view())) while True: socket_bytes = self.socket.recv(8192) @@ -69,6 +70,12 @@ class IFMClientProcess(Process): print("iFacialMocap data parse error:", socket_string) continue + # 计算呼吸效果(使用 sin 函数,在 breath_cycle 时间内从 0 到 1 再到 0) + breath_elapsed = (time.perf_counter() - breath_start_time) % args.breath_cycle + # 使用 sin 函数,让值在一个周期内从 0 -> 1 -> 0 + # sin 在 0 到 π 之间从 0 到 1 到 0 + breath_value = np.sin(breath_elapsed / args.breath_cycle * np.pi) + ifacialmocap_pose_converted = ifm_converter.convert(data) position_vector = data[HEAD_BONE_QUAT] @@ -83,6 +90,7 @@ class IFMClientProcess(Process): pose_vector[i - 39] = ifacialmocap_pose_converted[i] pose_vector[3] = pose_vector[1] pose_vector[4] = pose_vector[2] + pose_vector[5] = breath_value mouth_eye_vector[14] = mouth_eye_vector[14] * 1.5 diff --git a/src/open_see_face_client.py b/src/open_see_face_client.py index 0be9a01..ff4f42a 100644 --- a/src/open_see_face_client.py +++ b/src/open_see_face_client.py @@ -39,7 +39,8 @@ class OSFClientProcess(Process): position_vector_0 = None position_vector = [0, 0, 0, 1] - + # 呼吸循环参数 + breath_start_time = time.perf_counter() pose_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) position_filter = OneEuroFilterNumpy(freq=input_fps.view(), mincutoff=args.filter_min_cutoff, beta=args.filter_beta) iris_x_filter = OneEuroFilter(freq=input_fps.view(), mincutoff=0.001, beta=1.0) # extra filter for iris movement @@ -119,6 +120,12 @@ class OSFClientProcess(Process): data[OpenSeeFeatureIndex[i]] = osf_raw[i + 432] # print(data['rotationX'],data['rotationY'],data['rotationZ']) + # 计算呼吸效果(使用 sin 函数,在 breath_cycle 时间内从 0 到 1 再到 0) + breath_elapsed = (time.perf_counter() - breath_start_time) % args.breath_cycle + # 使用 sin 函数,让值在一个周期内从 0 -> 1 -> 0 + # sin 在 0 到 π 之间从 0 到 1 到 0 + breath_value = np.sin(breath_elapsed / args.breath_cycle * np.pi) + a = np.array([ data['points3DX66'] - data['points3DX68'] + data['points3DX67'] - data['points3DX69'], data['points3DY66'] - data['points3DY68'] + data['points3DY67'] - data['points3DY69'], @@ -151,6 +158,7 @@ class OSFClientProcess(Process): pose_vector[2] = (data['rotationZ'] - rotation_offset[2]) / 57.3 * 2 pose_vector[3] = pose_vector[1] pose_vector[4] = pose_vector[2] + pose_vector[5] = breath_value if position_vector_0 == None: #Provide an initial reference point position_vector_0 = [0, 0, 0, 1] From 3dbc2c0942c98dfabe7f651dd80c8c56573f468f Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Mon, 2 Feb 2026 00:02:58 -0500 Subject: [PATCH 32/46] update:update documents --- PerformanceTest.md | 123 +++++++ README.md | 243 ++++++-------- assets/complete_building_log.txt | 548 ------------------------------- 3 files changed, 223 insertions(+), 691 deletions(-) create mode 100644 PerformanceTest.md delete mode 100644 assets/complete_building_log.txt diff --git a/PerformanceTest.md b/PerformanceTest.md new file mode 100644 index 0000000..e2eb39d --- /dev/null +++ b/PerformanceTest.md @@ -0,0 +1,123 @@ +# 测试环境 +### 基于 +* Release版本v0.3 +* 测试平台为windows10 22H2 +* CPU Intel 10400f +* GPU Nvidia 3060 12G, Nvidia 1660 Super 6G, ARC A770 16G,RX580 8G. + +### 测试定量 +* Jpeg compression 和 Input Simplify 设为 medium +* 使用debug input 和 debug output +* 不使用eyebrow参数 +* 帧数限制60fps +* 英伟达显卡使用TensorRT测试 + +观察开始后约5-10秒的数值记录, 即不考虑长期缓存效果(没时间测) + +# 测试结果 +请注意测试结果仅限参考显卡大概性能,具体性能取决于用户周边配置和显卡驱动。 + +GPU | model | VRAM Cache | RAM Cache | Interpolation | SR | Frame Rate | GPU Usage +:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: +A770 | sepe_half | N/A | No | No | No | 33fps | 97% +A770 | sepe_full | N/A | No | No | No | 48fps | 97% +A770 | stan_half | N/A | No | No | No | 27fps | 97% +A770 | stan_full | N/A | No | No | No | 29fps | 97% +A770 | sepe_half | N/A | 2GB | No | No | 30fps | 82% +A770 | sepe_full | N/A | 2GB | No | No | 43fps | 78% +A770 | stan_half | N/A | 2GB | No | No | 30fps | 82% +A770 | stan_full | N/A | 2GB | No | No | 23fps | 85% +A770 | sepe_full | N/A | No | No | x2_half | 17fps | 97% +A770 | sepe_full | N/A | No | No | x4_half | 29fps | 97% +A770 | sepe_full | N/A | No | No | x4_full | 25fps | 93% +A770 | sepe_full | N/A | No | x2_half | No | 60fps | 93% +A770 | sepe_full | N/A | No | x3_half | No | 60fps | 85% +A770 | sepe_full | N/A | No | x4_half | No | 60fps | 81% +-- | -- | --| --|--|--|--|-- +RX580 | sepe_full | N/A | No | No | No | 12fps | 98% +RX580 | stan_full | N/A | No | No | No | 15fps | 98% +RX580 | sepe_full | N/A | 2GB | No | No | 11fps | 93% +RX580 | stan_full | N/A | 2GB | No | No | 13fps | 93% +RX580 | stan_full | N/A | No | No | x2_half | 10fps | 97% +RX580 | stan_full | N/A | No | No | x2_full | 10fps | 97% +RX580 | stan_full | N/A | No | No | x4_half | 11fps | 98% +RX580 | stan_full | N/A | No | No | x4_full | 11fps | 98% +RX580 | stan_full | N/A | No | x2_half | no | 22fps | 98% +RX580 | stan_full | N/A | No | x2_full | no | 22fps | 98% +RX580 | stan_full | N/A | No | x3_half | no | 28fps | 98% +RX580 | stan_full | N/A | No | x3_full | no | 28fps | 98% +RX580 | stan_full | N/A | No | x4_half | no | 33fps | 98% +RX580 | stan_full | N/A | No | x4_full | no | 33fps | 98% +-- | -- | --| --|--|--|--|-- +1660s | sepe_half | No | No | No | No | 38fps | 95% +1660s | sepe_full | No | No | No | No | 28fps | 95% +1660s | stan_half | No | No | No | No | 21fps | 96% +1660s | stan_full | No | No | No | No | 12fps | 98% +1660s | sepe_half | 2GB | No | No | No | 39fps | 95% +1660s | sepe_full | 2GB | No | No | No | 28fps | 95% +1660s | stan_half | 2GB | No | No | No | 29fps | 96% +1660s | stan_full | 2GB | No | No | No | 12fps | 96% +1660s | sepe_half | 2GB | 2GB | No | No | 40fps | 75% +1660s | sepe_full | 2GB | 2GB | No | No | 24fps | 80% +1660s | stan_half | 2GB | 2GB | No | No | 26fps | 83% +1660s | stan_full | 2GB | 2GB | No | No | 12fps | 92% +1660s | sepe_half | No | No | No | x2_half | 23fps | 96% +1660s | sepe_half | No | No | No | x2_full | 18fps | 96% +1660s | sepe_half | No | No | No | x4_half | 23fps | 96% +1660s | sepe_half | No | No | No | x4_full | 26fps | 97% +1660s | sepe_half | No | No | x2_half | No | 63fps | 75% +1660s | sepe_half | No | No | x2_full | No | 63fps | 80% +1660s | sepe_half | No | No | x3_half | No | 63fps | 65% +1660s | sepe_half | No | No | x3_full | No | 64fps | 66% +1660s | sepe_half | No | No | x4_half | No | 64fps | 53% +1660s | sepe_half | No | No | x4_full | No | 64fps | 60% +-- | -- | --| --|--|--|--|-- +3060 | sepe_half | No | No | No | No | 63fps | 62% +3060 | sepe_full | No | No | No | No | 59fps | 93% +3060 | stan_half | No | No | No | No | 63fps | 80% +3060 | stan_full | No | No | No | No | 31fps | 96% +3060 | sepe_half | 2GB | No | No | No | 63fps | 60% +3060 | sepe_full | 2GB | No | No | No | 60fps | 93% +3060 | stan_half | 2GB | No | No | No | 63fps | 76% +3060 | stan_full | 2GB | No | No | No | 33fps | 96% +3060 | sepe_half | 2GB | 2GB | No | No | 63fps | 58% +3060 | sepe_full | 2GB | 2GB | No | No | 46fps | 70% +3060 | stan_half | 2GB | 2GB | No | No | 43fps | 62% +3060 | stan_full | 2GB | 2GB | No | No | 27fps | 80% +3060 | sepe_half | No | No | No | x2_half | 53fps | 85% +3060 | sepe_half | No | No | No | x2_full | 30fps | 94% +3060 | sepe_half | No | No | No | x4_half | 49fps | 90% +3060 | sepe_half | No | No | No | x4_full | 45fps | 90% +3060 | sepe_half | No | No | x2_half | No | 64fps | 45% +3060 | sepe_half | No | No | x2_full | No | 63fps | 50% +3060 | sepe_half | No | No | x3_half | No | 64fps | 35% +3060 | sepe_half | No | No | x3_full | No | 64fps | 42% +3060 | sepe_half | No | No | x4_half | No | 64fps | 30% +3060 | sepe_half | No | No | x4_full | No | 64fps | 40% + +# 已知问题: +* A770: rife插帧全精度会出现奇怪结果, waifu2x 超分的全精度会输出全绿色 +* RX580 tha3 模型半精度会输出奇怪结果 +* 3060 有时出现奇怪运行时占用上升,猜测为插拔显卡导致的驱动问题。 + +# DirectML 思考 +DirectML 允许所有 Windows 显卡设备以及 AIPC 的 NPU 运行此项目,但由于不同设备的 Kernel 在不同精度下实现区别和驱动兼容问题, +导致 DirectML 的运行结果在某个模型的某些精度出现不可用的扭曲结果,且运行性能与对模型的常识理解不相符。 + +例如适用于本项目插帧的RIFE模型的调用封装经历过一轮迭代, +由原先的n次输入(N=1,C,H,W)独立图像调用RIFE模型生成n个独立结果, +改为输入(N=n,C,H,W)的叠加图像一次性调用RIFE模型得到叠加结果, +此改动下模型获得了稍小的显存占用且在TensorRT实现中获得一定性能提升(虽然改动本意并不在此)。 +接着在笔者环境中的因特尔 A770 显卡 DirectML 在RIFE模型迭代后全精度推理结果由原先的符合预期变为扭曲图像, +半精度迭代后模型不仅保持推理结果符合预期,且相较模型迭代前得到了一定性能提升。 + +再例如笔者测试某 AMD 核显运行时发现其运行THA3的半精度模型时会因为此设备无法运行其中的某个kernel而将kernel运算单独卸载到cpu进行, +但保留了其他大部分能在核显上运行的kernel,由此推理模型需要结果在两个设备间反复,带来了极大的性能倒退。 + +目前阶段,受限于Python OnnxRuntime api 的支持匮乏(相较C++),使用 python 运行 Directml 无法避免上述问题, +在代码层面无法在运行前做出是否适配的条件判断,也无法在出现kernel卸载到cpu性能剧降时raise错误将运行打断。 + +所以,DirectML运行本项目的任意模型出现奇怪结果,或不符合预期的性能倒退,都应该属于意料之中 +换个模型精度,观察调试输出是否有DirectML黄字警告 通常可以解决大部分问题。 +具体某个硬件在DirectML的运行异常修复,已远超本项目的目标范围,请自行验证失效kernel,向硬件厂商和微软求助并期待驱动更新带来解决 +(建议买张N卡吧别折腾自己了)。 \ No newline at end of file diff --git a/README.md b/README.md index 35d5107..1d4138e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Updates: * 使用 https://github.com/zpeng11/ezvtuber-rt/tree/main 项目转换的ONNX模型,实现TensorRT加速以及非N卡支持,在半精度和全精度下均有加速(具体取决于显卡) * 为A卡和I卡提供DirectML支持,人人都能玩。 * 引入[RIFE](https://github.com/hzwer/ECCV2022-RIFE)模型进行插帧,极限帧数提升达到50%-100%,配合锁帧可以有效降低使用占用。 -* 使用 [TurboJPEG](https://github.com/Dobatymo/turbojpeg-python) 获得成倍缓存命中率提升,在长时间使用上显卡减负效果相当出色。 +* 使用 `brotli` 压缩器获得成倍缓存命中率提升,在长时间使用上显卡减负效果相当出色。 * 使用 [waifu2x](https://github.com/nagadomi/waifu2x) 和 [real-esrgan](https://github.com/xinntao/Real-ESRGAN) 项目带来的输出超分辨率,对比anime4k效果提升显著(也使用更多gpu占用) * 更漂亮的 WxPython 新启动器界面,带中文介绍更加简单易用 * 添加Spout2 支持输出原生透明通道给OBS @@ -24,12 +24,12 @@ Updates: ### 硬件 -- 支持FaceID的iPhone(使用ifacialmocap软件,需购买,需要稳定的WIFI连接)或网络摄像头(使用OpenCV) +- 支持无摄像头鼠标捕获/USB网络摄像头/iPhone结构光面捕(ifacialmocap软件,需购买) - 任意5年内的游戏级显卡,N卡/A卡/I卡均可使用,某些性能强的核显也可尝试,请参考[性能测试结果](PerformanceTest.md) ### 软件 -- 本方案在Windows 10上测试可用 -- OBS或Unity Capture(虚拟摄像头方案) +- 本方案在Windows 10/11上测试可用 +- OBS 使用经验 - Photoshop或其他图片处理软件 - 科学上网方案,看懂英文网站和报错的能力 @@ -40,100 +40,13 @@ Updates: * [谷歌网盘](https://drive.google.com/drive/folders/1cYj18EfVQ2Cl348_rkCu_fgaasHTI_io?usp=drive_link) ### 安装Spout2 OBS插件(可选) -安装整合包附带的 `OBS_Spout2_Plugin_Install_v1.8.exe`, 安装包来自 https://github.com/Off-World-Live/obs-spout2-plugin 项目,可以提供透明通道给OBS。 - -### 构建TensorRT模型(N卡专用,可选) -双击`02.构建TensorRT模型(N卡使用).bat`,若是英伟达用户将会开始TensorRT构建, 每台配置仅需构建一次,更新显卡驱动可能需要重新构建 -额外需要>20min时间(取决于显卡),结束后可以大幅提升运行速度(可简单理解为玩游戏之前编译着色器,和每个人显卡有关) -构建的开头和结束输出如下,请耐心等进度条百分百或完成后自动退出。 -若不慎关掉窗口也可以双击自动从断点重来,可以先放后台运行构建并尝试下一步运行暂时不勾选TensorRT: -``` -Start testing if TensorRT works on this machine - 0%| | 0/38 [00:00conda --version -conda 24.11.3 -``` -以上方式可以保证conda环境被你选定的Ananconda统一管理。若没有此需求可以忽略直接运行下一步,`01X.构建运行环境(XX源).bat`脚本会自动安装miniconda。 - -### 克隆项目和子项目 -``` -git clone https://github.com/zpeng11/EasyVtuber.git -cd EasyVtuber -git submodule init -git submodule update --recursive --remote -``` - -### 安装环境 -根据网络环境双击运行`01A.构建运行环境(默认源).bat`或者`01B.构建运行环境(国内源).bat` -这两个脚本会构建名为`ezvtb_rt_venv`的conda环境 -或可以用如下命令手动创建: -``` -conda create -y -n ezvtb_rt_venv python=3.10 -conda activate ezvtb_rt_venv -conda install -y nvidia/label/cuda-12.6.3::cuda-nvcc-dev_win-64 -conda install -y conda-forge::pycuda -python -m pip install --upgrade pip wheel -python -m pip install nvidia-cudnn-cu12 -pip install tensorrt_cu12_libs==10.6.0 tensorrt_cu12_bindings==10.6.0 tensorrt==10.6.0 --extra-index-url https://pypi.nvidia.com -python -m pip install -r requirements.txt --no-warn-script-location -``` - -### 下载模型 -前往模型 release 地址 https://github.com/zpeng11/ezvtuber-rt/releases/tag/0.0.1 -下载 https://github.com/zpeng11/ezvtuber-rt/releases/download/0.0.1/20241220.zip 并解压到`data/models`文件夹下 - -### Pycharm 配置 -安装完成后,在Pycharm内打开本项目,右下角解释器菜单点开,`Add Interpreter...`->`Conda Environment`->`Existing environment` -选好自己电脑上的`conda.exe`和刚才创建好的`ezvtb_rt_venv`环境内的`python.exe` -点击OK,依赖全亮即可 - -### 检查环境模型,并构建TensorRT -在Conda环境中执行以下命令 -`python ezvtb_rt_interface.py` - -### 运行启动器 -在Conda环境中执行以下命令 -`python launcher2.py` - -### 环境错误排查 -当运行TensorRT构建或启动器出现错误时,请参考如下可能性: -1. 环境安装有错误(一般运行时缺少库99%的原因都是这个,pip下载并不稳定,各种网络问题都可能导致安装失败),检查源,手动删除`envs`文件夹再试, 对照 [此log](assets/complete_building_log.txt) 为成功案例排查原因。 实在不懂排查请使用整合包。 -2. 英伟达显卡但计算架构低于7.5(10系以及之前)不支持TensorRT - ## 输入输出设备 #### SPOUT2 OBS插件输出 @@ -143,7 +56,7 @@ python -m pip install -r requirements.txt --no-warn-script-location #### OBS Virtual Camera 无透明通道 -如果你选择自己进行抠像你可以直接仅输出RGB到OBS,使用补帧时可能也需要此方法 +次选方案,如果你选择自己进行抠像你可以直接仅输出RGB到OBS,使用补帧时可能也需要此方法 使用`视频采集设备`源,设置`滤镜`, 在`效果滤镜`添加`色值`并自定义颜色为纯黑色,多尝试调整颜色参数直到抠图合理 @@ -158,29 +71,21 @@ python -m pip install -r requirements.txt --no-warn-script-location 之后对OBS中的视频采集设备添加滤镜-着色器-选择你下载的`merge alpha2.hlsl`-关闭 这样透明通道就应用回左边的图像了 你可能需要手动调整一下裁剪把右侧的无用画面切掉 -(看不到着色器滤镜的话就是StreamFX没装好或者OBS不是最新版) +(看不到着色器滤镜的话就是StreamFX没装好或者OBS不是最新版)d -#### UnityCapture(Outdated,性能问题) - -存在未查明的性能瓶颈 -如果需要使用透明通道输出,参考 https://github.com/schellingb/UnityCapture#installation 安装好UnityCapture -只需要正常走完Install.bat,在OBS里能看到对应的设备(Unity Video Capture)就行 - -在OBS添加完摄像头以后,还需要手动配置一次摄像头属性才能支持ARGB -右键属性-取消激活-分辨率类型自定义-分辨率512x512(与`--output_size`参数一致)-视频格式ARGB-激活 - -#### iFacialMocap +#### iFacialMocap 面捕 目前最推荐的方案,使用苹果结构光硬件,捕捉效果追踪准确度都最好,demo中使用了此输入。 需要输入`ip:port`,其中`ip`会显示在手机上,`port`使用默认`49983` https://www.ifacialmocap.com/download/ 你大概率需要购买正式版(非广告,只是试用版不太够时长) -购买之前确认好自己的设备支持 -**不需要下载PC软件**,装好iOS端的软件即可,连接信息通过参数传入Python -游戏加速器,梯子等网络工具可能会导致ip失效,请自行解决。 +购买之前确认好自己的设备支持,装好iOS端的软件即可,连接信息通过参数传入Python +一般需要iphone与window在同一局域网下 +且游戏加速器,梯子等网络工具可能会导致ip失效,请自行解决。 -#### OpenSeeFace +#### OpenSeeFace 高精度摄像头面捕 +在没有苹果硬件时最推荐使用的捕捉方案,需要更多配置 https://github.com/emilianavt/OpenSeeFace/releases 直接下载最新版本的Release包并解压,或使用整合包附带的压缩包 之后进入解压目录的Binary文件夹 @@ -189,28 +94,34 @@ https://github.com/emilianavt/OpenSeeFace/releases 之后保存并双击`run.bat`运行,按照提示选择摄像头、分辨率、帧率,捕获正常的话可以看到输出画面 最后在启动器中选择OpenSeeFace输入,或添加启动参数`--osf 127.0.0.1:11573`即可接入OpenSeeFace +#### OpenCV 低精度摄像头面捕 + +此输入不需要配置,会尝试开启默认的USB或笔记本电脑摄像头开始面捕,稳定性和精确度略欠佳。 + +#### 鼠标音频输入 + +隐私考虑不想使用任何摄像头可使用这个输入,定时循环眨眼和呼吸,配合音频音量控制口型,可以在无摄像头时得到不错的动态。 + ## 性能配置(调参必读) ### 概念须知 #### 模型&精度 本项目完全基于AI算法生成,由3类AI模型提供服务(缝合怪): -* THA模型:指本项目的基础模型,输入静态图像和姿态信息计算出新图像 +* THA模型:指本项目的基础模型,输入静态图像和姿态信息计算出新图像,有v3_seperable, v3_standard, v4, v4_student 四种版本。 * 插帧/补帧模型:指RIFE插帧模型,输入第1帧和第2帧,输出第1.5帧,类似DLSS3或小黄鸭,此模型是整体性能提升的关键 -* 超分模型:指waifu2x或realesrgan模型,输入一帧图像,解计算出放大且更清晰的同一帧,显著提升画面效果 +* 超分模型:指Anime4k, waifu2x或realesrgan模型,输入一帧图像,解算出放大且更清晰的同一帧,显著提升画面效果 -每一种模型提供了半精度(标记为half)和全精度(标记为full)选项,半精度通常运行速度比全精度快,但生图效果稍微逊色,需要根据性能进行取舍。 -THA模型有另外的`seperable`和`standard`两个版本,可以简单理解为`seperable`是性能更强但生图效果稍差的版本,所以加上两种精度THA模型一共有4种组合。 +每一种模型提供了半精度(标记为half)和全精度(标记为full)选项,半精度通常运行速度比全精度快,但生图效果稍微逊色,且兼容性可能不好。 #### 缓存 当我们当前的姿态与一秒前的姿态完全相同时,输入给模型的参数也完全相同,没有必要再做一次完全相同的运算得出完全相同的结果 -缓存的原理是将过去一段时间内产生过的结果储存起来。 -倘若发现姿态相同,就使用储存的结果输出而绕过模型运行(又叫缓存命中),从而减轻显卡负担。 -本项目同时使用显存VRAM和内存RAM用作缓存目的。在内存中还使用了JPEG格式对缓存的图片进行压缩。 +倘若发现姿态相同,就使用曾经的结果输出而绕过模型运行(又叫缓存命中),从而减轻显卡负担。 +本项目同时使用显存VRAM和内存RAM用作缓存目的。在内存中还对缓存的图片进行压缩。 缓存大小(单位GB)决定了缓存最多能使用的空间大小。更大的缓存能让缓存命中率更高,长时间使用时显卡占用更小。但也会挤占其他应用或游戏的空间。 需要自行理解并取舍。 -#### 输入简化 +#### 输入简化(量化) 输入简化与上文的缓存相辅相成,我们将姿态参数分入一些区间,这样相近的参数就可以命中同一个缓存。 举个非常简化的例子:缓存中的一个结果对应姿态参数`0.5`, 在不进行任何输入简化时,需要新输入参数完全相同于`0.5`即 `x = 0.5`才能判定命中并绕过GPU运算。 而开启了输入简化后,只需要新输入参数 `x` $\approx$ `0.5` 就可以命中。如`x = 0.49`或 `x = 0.51`. @@ -226,9 +137,9 @@ THA模型有另外的`seperable`和`standard`两个版本,可以简单理解 #### 超分 ![image](https://github.com/user-attachments/assets/afa78440-5122-4b57-b586-4487e125eb2f) -超分由waifu2x和realesrgan模型的两种精度一共4种组合,外加一种Anime4K纯cpu实现(效果比较差)。 +超分由waifu2x和realesrgan模型的两种精度一共4种组合,外加一种Anime4K实现。 通常选用半精度,根据使用半身或大头选择`waifu2x_x2_half`和`realesrgan_x4_half` -超分启动时会显著增加显卡占用,通常建议在低负载游戏或者杂谈状态下使用。 +超分启动时会显著增加显卡占用。 #### 插帧/补帧 ![image](https://github.com/user-attachments/assets/de392b93-139b-455b-b561-7713d26fca20) @@ -240,17 +151,70 @@ RIFE插帧的计算消耗比起一次THA计算要少约50%且画面观感差异 ## 脱离启动器运行本体 -`main.py` 的完全体运行命令请参考 `02B.启动器(调试输出).bat` 的输出以及 `args.py`。 以下为部分基础参数 +`main.py` 的完全体运行命令请参考 `02B.启动器(调试输出).bat` 的输出以及 `args.py`。 -参数名 | 值类型 | 说明 -:---: | :---: | :---: ---character|字符串|`character`目录下的输入图像文件名,不需要带扩展名 ---debug|无|打开OpenCV预览窗口输出渲染结果,如果没有任何输出配置,该参数默认生效 ---input|字符串|不使用iOS面捕时,传入要使用的摄像头设备名称,默认为设备0,有ifm参数时无效 ---ifm|字符串|使用iOS面捕时,传入设备的`IP:端口号`,如`192.168.31.182:49983` ---output_webcam|字符串|可用值为`obs` `unitycapture`,选择对应的输出种类,不传不输出到摄像头 ---extend_movement|浮点数|使用iOS面捕返回的头部位置,对模型输出图像进一步进行移动和旋转使得上半身可动
传入的数值表示移动倍率(建议值为1) ---output_size|字符串|格式为`256x256`,必须是4的倍数。
增大它并不会让图像更清晰,但配合extend_movement会增大可动范围 +## Installation(使用整合包请忽略) +可使用此安装方法对本项目二次开发 + +### 安装Anaconda(可选) +前往https://www.anaconda.com 安装Anaconda 并保证加入环境变量命令行可以找到,如: +``` +C:\Users\Eleven>conda --version +conda 24.11.3 +``` +以上方式可以保证conda环境被你选定的Ananconda统一管理。若没有此需求可以忽略直接运行下一步。 + +### 下载 TensorRT-RTX (Nvidia开发必选) +``` +curl -L -o trt_rtx.zip https://developer.nvidia.com/downloads/trt/rtx_sdk/secure/1.3/TensorRT-RTX-1.3.0.35-win10-amd64-cuda-12.9-Release-external.zip +tar -xf trt_rtx.zip "TensorRT-RTX-1.3.0.35" && ren "TensorRT-RTX-1.3.0.35" "TensorRT-RTX-1.3.0.35_cu129" && del trt_rtx.zip +# Adding the bin folder to PATH +cd TensorRT-RTX-1.3.0.35_cu129\bin && set PATH=%CD%;%PATH% +``` +也可以在Window 环境变量管理器 GUI中将`TensorRT-RTX-1.3.0.35_cu129\bin`在`PATH`环境变量中添加来持久化 + +### 准备Conda 环境 +``` +conda create -y -n easyvtuber python=3.10 +conda activate easyvtuber +conda install conda-forge::pycuda +conda install -c nvidia/label/cuda-12.9.1 cuda-nvcc-dev_win-64 cudnn cuda-runtime +``` + +### 安装 TensorRT-RTX 的 Python Binding +``` +pip install TensorRT-RTX-1.3.0.35_cu129\python\tensorrt_rtx-1.3.0.35-cp310-none-win_amd64.whl +``` + +### 克隆项目和子项目 +``` +git clone https://github.com/yuyuyzl/EasyVtuber.git +cd EasyVtuber +git submodule init +git submodule update --recursive --remote +``` + +### 安装环境 +``` +pip install -r requirements.txt --no-warn-script-location +``` + +### 下载模型 +前往模型 release 地址 https://drive.google.com/file/d/1pWKIpjWeqfpa3Rub185FVvxDr5H09pOi/view?usp=drive_link 并解压到`data/models`文件夹下 + +### Pycharm 配置(可选) +安装完成后,在Pycharm内打开本项目,右下角解释器菜单点开,`Add Interpreter...`->`Conda Environment`->`Existing environment` +选好自己电脑上的`conda.exe`和刚才创建好的`easyvtuber`环境内的`python.exe` +点击OK,依赖全亮即可 + +### 运行启动器 +在Conda环境中执行以下命令 +`python launcher2.py` + +### 环境错误排查 +当运行TensorRT构建或启动器出现错误时,请参考如下可能性: +1. 环境安装有错误(一般运行时缺少库99%的原因都是这个,pip下载并不稳定,各种网络问题都可能导致安装失败),请检查源。 +2. 英伟达显卡但计算架构低于7.5(10系以及之前)不支持TensorRT ## FAQ @@ -258,9 +222,12 @@ RIFE插帧的计算消耗比起一次THA计算要少约50%且画面观感差异 A卡I卡使用 DirectML,这个框架与显卡厂商的显卡驱动及 Direct12 实现相关,很多算子实现存在数值错误或未实现并行优化被迫降级到cpu运行。方案:1. 将显卡驱动更新至最新,2. 打开调试启动器运行并检查模型或算子是否被降级到cpu实现,3.阅读上文的模型调参环节,切换不同模型和精度多尝试。 如有兴趣了解更多请参考[性能测试结果](PerformanceTest.md)末尾对DirectML的思考。 -> Q2: 我开启超分辨率后,为什么补帧没效果了? +> Q2: 为什么无法找到我自己准备的Vtuber图像/使用自己的图像会报错? -v0.5的实现中,补帧和超分只能二选一,开启超分后补帧会自动失效。欢迎重新在上文中更新下载v0.7之后的版本。 +1. 确认图像改为512x512的png,带Alpha透明通道即32比特位宽。 +2. 确认放置位置为`data/images`,在选项中应当能看到文件名。 +3. 仍然无法正确启动请尝试删除`launch.json`文件并重新启动软件。 +4. 99%的错误情况都来自于准备的图像不满足模型需求 > Q3: 我开启补帧后为什么在 Spout2 输出中边缘会有些抖动? @@ -270,16 +237,6 @@ v0.5的实现中,补帧和超分只能二选一,开启超分后补帧会自 请在GPU页面中点击`Video Decode`或`Video Processing`按钮可以看到下拉,请对这里的选项挨个尝试找到正确的程序负载,一般在 `cuda` 或 `3D` 或 `Compute_X` 或 `Graphics_X`。 -> Q5: 我的配置是 CPU带核显加独立显卡,这个核显性能挺不错,打游戏时还用不上,能否把项目跑到这上面? +> Q6: 本机有两张显卡,如何使用第二张副卡使用这个项目? -完全可行,其实界面中留显卡选项有部分原因就是这方面需求。具体操作如下:去除TensorRT勾选,开启设备管理器观察核显占用,从`1`开始挨个尝试`GPU Device`选项并启动,直到观察到核显开始占用上升即找到了核显对应的编号。(os:DirectML的API就是这么难用没办法)接下来有可能出现Q1中提及的问题,请参照上文解决。 -因为性能问题很有可能需要使用插帧来提高输出帧数。目前核显相关测试样本较少,欢迎分享运行效果。 - -> Q6: 我主机上插着两张英伟达显卡,如何使用第二张副卡使用这个项目? -1. 对第二张卡构建TenosrRT:更改`02.构建TensorRT模型(N卡开启TensorRT使用).bat` 中的 `python ezvtb_rt_interface.py` 一行,加上空格接参数cuda设备号,例如我的副卡设备号是`1`所以改成 `python ezvtb_rt_interface.py 1` 然后双击运行。 -2. 在启动器界面中选择相同的设备号。 - -> Q7: 为什么无法使用我自己准备的Vtuber图像/使用自己的图像会报错? -1. 确认图像改为512x512的png,带Alpha透明通道即32比特位宽。 -2. 确认放置位置为`data/images`,在选项中应当能看到文件名。 -3. 仍然无法正确启动请尝试删除`launch.json`文件并重新启动软件。 +在运行前配置环境变量`EZVTB_DEVICE_ID`为你想要运行的GPU ID,此变量缺省为`0` diff --git a/assets/complete_building_log.txt b/assets/complete_building_log.txt deleted file mode 100644 index dcc6465..0000000 --- a/assets/complete_building_log.txt +++ /dev/null @@ -1,548 +0,0 @@ -C:\Users\Eleven\Desktop\EasyVtuber-main>cd /D "C:\Users\Eleven\Desktop\EasyVtuber-main\" - -C:\Users\Eleven\Desktop\EasyVtuber-main>WHERE conda -INFO: Could not find files for the given pattern(s). - -C:\Users\Eleven\Desktop\EasyVtuber-main>IF 1 NEQ 0 ( -if not exist "envs" mkdir envs - if not exist "envs\miniconda3" mkdir envs\miniconda3 - IF not EXIST C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\Scripts ( - - mkdir C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3 - echo "Downloading miniconda..." - powershell -Command "(New-Object Net.WebClient).DownloadFile('https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe', '.\envs\miniconda3.exe')" - echo "Installling minconda..." - start /wait "" C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3 - echo "Successfully install minconda" - del envs\miniconda3.exe -) -) -"Downloading miniconda..." -"Installling minconda..." -"Successfully install minconda" - -C:\Users\Eleven\Desktop\EasyVtuber-main>IF EXIST C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\Scripts SET PATH=C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\Scripts;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\bin;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\libnvvp;;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\Git\cmd;C:\Program Files\Tailscale\;C:\Program Files\NVIDIA Corporation\Nsight Compute 2024.3.2\;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;C:\Program Files\dotnet\;C:\Users\Eleven\AppData\Local\Programs\Python\Python312\Scripts\;C:\Users\Eleven\AppData\Local\Programs\Python\Python312\;C:\Users\Eleven\.cargo\bin;C:\Users\Eleven\AppData\Local\Microsoft\WindowsApps;C:\Users\Eleven\AppData\Local\Programs\Microsoft VS Code\bin;C:\Program Files\Git\bin;D:\ffmpeg\bin; - -C:\Users\Eleven\Desktop\EasyVtuber-main>call activate -# conda environments: -# - C:\EasyVtuber\envs\miniconda3 - C:\EasyVtuber\envs\miniconda3\envs\ezvtb_rt_venv -base * C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3 - -Retrieving notices: ...working... done -Channels: - - defaults -Platform: win-64 -Collecting package metadata (repodata.json): done -Solving environment: done - -## Package Plan ## - - environment location: C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3 - - -The following packages will be downloaded: - - package | build - ---------------------------|----------------- - anaconda-anon-usage-0.5.0 |py310hfc23b7f_100 28 KB - brotli-python-1.0.9 | py310h5da7b33_9 346 KB - ca-certificates-2024.11.26 | haa95532_0 132 KB - certifi-2024.12.14 | py310haa95532_0 162 KB - cffi-1.17.1 | py310h827c3e9_1 254 KB - conda-24.11.2 | py310haa95532_0 948 KB - conda-libmamba-solver-24.11.1| pyhd3eb1b0_0 39 KB - conda-package-handling-2.4.0| py310haa95532_0 297 KB - conda-package-streaming-0.11.0| py310haa95532_0 31 KB - cpp-expected-1.1.0 | h214f63a_0 131 KB - cryptography-43.0.3 | py310hbd6ee87_1 1.3 MB - libarchive-3.7.7 | h9243413_0 1.8 MB - libcurl-8.11.1 | haff574d_0 382 KB - libmamba-2.0.5 | hcd6fe79_1 4.7 MB - libmambapy-2.0.5 | py310h214f63a_1 459 KB - libsolv-0.7.30 | hf2fb9eb_1 473 KB - libssh2-1.11.1 | h2addb87_0 312 KB - libxml2-2.13.5 | h24da03e_0 2.9 MB - menuinst-2.2.0 | py310h5da7b33_0 200 KB - nlohmann_json-3.11.2 | h6c2663c_0 126 KB - packaging-24.2 | py310haa95532_0 175 KB - pluggy-1.5.0 | py310haa95532_0 39 KB - requests-2.32.3 | py310haa95532_1 101 KB - simdjson-3.10.1 | h214f63a_0 275 KB - spdlog-1.11.0 | h59b6b97_0 195 KB - vc-14.40 | haa95532_2 10 KB - vs2015_runtime-14.42.34433 | h9531ae6_2 1.2 MB - zstandard-0.23.0 | py310h4fc1ca9_1 342 KB - ------------------------------------------------------------ - Total: 17.3 MB - -The following NEW packages will be INSTALLED: - - cpp-expected pkgs/main/win-64::cpp-expected-1.1.0-h214f63a_0 - nlohmann_json pkgs/main/win-64::nlohmann_json-3.11.2-h6c2663c_0 - simdjson pkgs/main/win-64::simdjson-3.10.1-h214f63a_0 - spdlog pkgs/main/win-64::spdlog-1.11.0-h59b6b97_0 - -The following packages will be UPDATED: - - anaconda-anon-usa~ 0.4.4-py310hfc23b7f_100 --> 0.5.0-py310hfc23b7f_100 - brotli-python 1.0.9-py310hd77b12b_8 --> 1.0.9-py310h5da7b33_9 - ca-certificates 2024.9.24-haa95532_0 --> 2024.11.26-haa95532_0 - certifi 2024.8.30-py310haa95532_0 --> 2024.12.14-py310haa95532_0 - cffi 1.17.1-py310h827c3e9_0 --> 1.17.1-py310h827c3e9_1 - conda 24.9.2-py310haa95532_0 --> 24.11.2-py310haa95532_0 - conda-libmamba-so~ 24.9.0-pyhd3eb1b0_0 --> 24.11.1-pyhd3eb1b0_0 - conda-package-han~ 2.3.0-py310haa95532_0 --> 2.4.0-py310haa95532_0 - conda-package-str~ 0.10.0-py310haa95532_0 --> 0.11.0-py310haa95532_0 - cryptography 43.0.0-py310h89fc84f_0 --> 43.0.3-py310hbd6ee87_1 - libarchive 3.7.4-h9243413_0 --> 3.7.7-h9243413_0 - libcurl 8.9.1-h0416ee5_0 --> 8.11.1-haff574d_0 - libmamba 1.5.8-h99b1521_3 --> 2.0.5-hcd6fe79_1 - libmambapy 1.5.8-py310h77c03ed_3 --> 2.0.5-py310h214f63a_1 - libsolv 0.7.24-h23ce68f_1 --> 0.7.30-hf2fb9eb_1 - libssh2 1.11.0-h291bd65_0 --> 1.11.1-h2addb87_0 - libxml2 2.13.1-h24da03e_2 --> 2.13.5-h24da03e_0 - menuinst 2.1.2-py310h5da7b33_0 --> 2.2.0-py310h5da7b33_0 - packaging 24.1-py310haa95532_0 --> 24.2-py310haa95532_0 - pluggy 1.0.0-py310haa95532_1 --> 1.5.0-py310haa95532_0 - requests 2.32.3-py310haa95532_0 --> 2.32.3-py310haa95532_1 - vc 14.40-h2eaa2aa_1 --> 14.40-haa95532_2 - vs2015_runtime 14.40.33807-h98bb1dd_1 --> 14.42.34433-h9531ae6_2 - zstandard 0.23.0-py310h4fc1ca9_0 --> 0.23.0-py310h4fc1ca9_1 - - - -Downloading and Extracting Packages: - -Preparing transaction: done -Verifying transaction: done -Executing transaction: done -Channels: - - defaults -Platform: win-64 -Collecting package metadata (repodata.json): done -Solving environment: done - -## Package Plan ## - - environment location: C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\envs\ezvtb_rt_venv - - added / updated specs: - - python=3.10 - - -The following packages will be downloaded: - - package | build - ---------------------------|----------------- - python-3.10.16 | h4607a30_1 16.3 MB - ------------------------------------------------------------ - Total: 16.3 MB - -The following NEW packages will be INSTALLED: - - bzip2 pkgs/main/win-64::bzip2-1.0.8-h2bbff1b_6 - ca-certificates pkgs/main/win-64::ca-certificates-2024.11.26-haa95532_0 - libffi pkgs/main/win-64::libffi-3.4.4-hd77b12b_1 - openssl pkgs/main/win-64::openssl-3.0.15-h827c3e9_0 - pip pkgs/main/win-64::pip-24.2-py310haa95532_0 - python pkgs/main/win-64::python-3.10.16-h4607a30_1 - setuptools pkgs/main/win-64::setuptools-75.1.0-py310haa95532_0 - sqlite pkgs/main/win-64::sqlite-3.45.3-h2bbff1b_0 - tk pkgs/main/win-64::tk-8.6.14-h0416ee5_0 - tzdata pkgs/main/noarch::tzdata-2024b-h04d1e81_0 - vc pkgs/main/win-64::vc-14.40-haa95532_2 - vs2015_runtime pkgs/main/win-64::vs2015_runtime-14.42.34433-h9531ae6_2 - wheel pkgs/main/win-64::wheel-0.44.0-py310haa95532_0 - xz pkgs/main/win-64::xz-5.4.6-h8cc25b3_1 - zlib pkgs/main/win-64::zlib-1.2.13-h8cc25b3_1 - - - -Downloading and Extracting Packages: - -Preparing transaction: done -Verifying transaction: done -Executing transaction: done -# -# To activate this environment, use -# -# $ conda activate ezvtb_rt_venv -# -# To deactivate an active environment, use -# -# $ conda deactivate - - -# conda environments: -# - C:\EasyVtuber\envs\miniconda3 - C:\EasyVtuber\envs\miniconda3\envs\ezvtb_rt_venv -base C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3 -ezvtb_rt_venv * C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\envs\ezvtb_rt_venv - -Channels: - - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - - defaults -Platform: win-64 -Collecting package metadata (repodata.json): done -Solving environment: done - -## Package Plan ## - - environment location: C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\envs\ezvtb_rt_venv - - added / updated specs: - - pycuda - - -The following packages will be downloaded: - - package | build - ---------------------------|----------------- - appdirs-1.4.4 | pyhd8ed1ab_1 14 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - boost-1.78.0 | py310h220cb41_4 555 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - boost-cpp-1.78.0 | h5b4e17d_0 17.0 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - ca-certificates-2024.12.14 | h56e8100_0 154 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - cudatoolkit-11.8.0 | h09e9e62_11 638.9 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - importlib-metadata-8.5.0 | pyha770c72_1 28 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - intel-openmp-2024.2.1 | h57928b3_1083 1.8 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - libblas-3.9.0 | 26_win64_mkl 3.6 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - libcblas-3.9.0 | 26_win64_mkl 3.6 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - libhwloc-2.9.1 | h51c2c0f_0 2.4 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - libiconv-1.17 | h8ffe710_0 698 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - liblapack-3.9.0 | 26_win64_mkl 3.6 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - lz4-c-1.9.4 | hcfcfb64_0 131 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - mako-1.3.8 | pyhd8ed1ab_0 66 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - markupsafe-2.1.2 | py310h8d17308_0 25 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - mkl-2024.2.2 | h66d3029_15 98.3 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - numpy-1.24.3 | py310hd02465a_0 5.7 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - openssl-3.1.0 | hcfcfb64_3 7.1 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - platformdirs-4.3.6 | pyhd8ed1ab_1 20 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - pthreads-win32-2.9.1 | hfa6e2cd_3 141 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - pycuda-2022.2.2 | py310ha2c4f5d_0 378 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - python_abi-3.10 | 2_cp310 4 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - pytools-2024.1.6 | pyhd8ed1ab_0 72 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - tbb-2021.9.0 | h91493d7_0 151 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - typing_extensions-4.12.2 | pyha770c72_1 39 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - ucrt-10.0.22621.0 | h57928b3_1 547 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - zipp-3.21.0 | pyhd8ed1ab_1 21 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - ------------------------------------------------------------ - Total: 784.9 MB - -The following NEW packages will be INSTALLED: - - appdirs anaconda/cloud/conda-forge/noarch::appdirs-1.4.4-pyhd8ed1ab_1 - boost anaconda/cloud/conda-forge/win-64::boost-1.78.0-py310h220cb41_4 - boost-cpp anaconda/cloud/conda-forge/win-64::boost-cpp-1.78.0-h5b4e17d_0 - cudatoolkit anaconda/cloud/conda-forge/win-64::cudatoolkit-11.8.0-h09e9e62_11 - importlib-metadata anaconda/cloud/conda-forge/noarch::importlib-metadata-8.5.0-pyha770c72_1 - intel-openmp anaconda/cloud/conda-forge/win-64::intel-openmp-2024.2.1-h57928b3_1083 - libblas anaconda/cloud/conda-forge/win-64::libblas-3.9.0-26_win64_mkl - libcblas anaconda/cloud/conda-forge/win-64::libcblas-3.9.0-26_win64_mkl - libhwloc anaconda/cloud/conda-forge/win-64::libhwloc-2.9.1-h51c2c0f_0 - libiconv anaconda/cloud/conda-forge/win-64::libiconv-1.17-h8ffe710_0 - liblapack anaconda/cloud/conda-forge/win-64::liblapack-3.9.0-26_win64_mkl - libxml2 pkgs/main/win-64::libxml2-2.13.5-h24da03e_0 - lz4-c anaconda/cloud/conda-forge/win-64::lz4-c-1.9.4-hcfcfb64_0 - mako anaconda/cloud/conda-forge/noarch::mako-1.3.8-pyhd8ed1ab_0 - markupsafe anaconda/cloud/conda-forge/win-64::markupsafe-2.1.2-py310h8d17308_0 - mkl anaconda/cloud/conda-forge/win-64::mkl-2024.2.2-h66d3029_15 - numpy anaconda/cloud/conda-forge/win-64::numpy-1.24.3-py310hd02465a_0 - platformdirs anaconda/cloud/conda-forge/noarch::platformdirs-4.3.6-pyhd8ed1ab_1 - pthreads-win32 anaconda/cloud/conda-forge/win-64::pthreads-win32-2.9.1-hfa6e2cd_3 - pycuda anaconda/cloud/conda-forge/win-64::pycuda-2022.2.2-py310ha2c4f5d_0 - python_abi anaconda/cloud/conda-forge/win-64::python_abi-3.10-2_cp310 - pytools anaconda/cloud/conda-forge/noarch::pytools-2024.1.6-pyhd8ed1ab_0 - tbb anaconda/cloud/conda-forge/win-64::tbb-2021.9.0-h91493d7_0 - typing_extensions anaconda/cloud/conda-forge/noarch::typing_extensions-4.12.2-pyha770c72_1 - ucrt anaconda/cloud/conda-forge/win-64::ucrt-10.0.22621.0-h57928b3_1 - zipp anaconda/cloud/conda-forge/noarch::zipp-3.21.0-pyhd8ed1ab_1 - zstd pkgs/main/win-64::zstd-1.5.6-h8880b57_0 - -The following packages will be UPDATED: - - ca-certificates pkgs/main::ca-certificates-2024.11.26~ --> anaconda/cloud/conda-forge::ca-certificates-2024.12.14-h56e8100_0 - openssl pkgs/main::openssl-3.0.15-h827c3e9_0 --> anaconda/cloud/conda-forge::openssl-3.1.0-hcfcfb64_3 - - - -Downloading and Extracting Packages: - -Preparing transaction: done -Verifying transaction: done -Executing transaction: | "By downloading and using the CUDA Toolkit conda packages, you accept the terms and conditions of the CUDA End User License Agreement (EULA): https://docs.nvidia.com/cuda/eula/index.html" - -done -Channels: - - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - - defaults -Platform: win-64 -Collecting package metadata (repodata.json): done -Solving environment: done - -## Package Plan ## - - environment location: C:\Users\Eleven\Desktop\EasyVtuber-main\envs\miniconda3\envs\ezvtb_rt_venv - - added / updated specs: - - pytorch-cpu - - -The following packages will be downloaded: - - package | build - ---------------------------|----------------- - blas-1.0 | mkl 6 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - filelock-3.13.1 | py310haa95532_0 22 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - fsspec-2024.6.1 | py310haa95532_0 286 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - gmpy2-2.1.2 | py310h7f96b67_0 160 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - intel-openmp-2023.1.0 | h59b6b97_46320 2.7 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - jinja2-3.1.4 | py310haa95532_1 281 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - libuv-1.48.0 | h827c3e9_0 322 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mkl-2023.1.0 | h6b88ed4_46358 155.9 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mkl-service-2.4.0 | py310h2bbff1b_1 44 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mkl_fft-1.3.11 | py310h827c3e9_0 168 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mkl_random-1.2.8 | py310hc64d2fc_0 257 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mpc-1.1.0 | h7edee0f_1 260 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mpfr-4.0.2 | h62dcd97_1 1.5 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mpir-3.0.0 | hec2e145_1 1.3 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - mpmath-1.3.0 | py310haa95532_0 834 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - networkx-3.3 | py310haa95532_0 2.5 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - numpy-1.26.4 | py310h055cbcc_0 11 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - numpy-base-1.26.4 | py310h65a83cf_0 8.6 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - pytorch-2.3.1 |cpu_py310h0ce1571_0 111.2 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - pytorch-cpu-2.3.1 | haa95532_0 21 KB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - sympy-1.13.3 | py310haa95532_0 11.3 MB https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - ------------------------------------------------------------ - Total: 297.6 MB - -The following NEW packages will be INSTALLED: - - blas anaconda/pkgs/main/win-64::blas-1.0-mkl - filelock anaconda/pkgs/main/win-64::filelock-3.13.1-py310haa95532_0 - fsspec anaconda/pkgs/main/win-64::fsspec-2024.6.1-py310haa95532_0 - gmpy2 anaconda/pkgs/main/win-64::gmpy2-2.1.2-py310h7f96b67_0 - jinja2 anaconda/pkgs/main/win-64::jinja2-3.1.4-py310haa95532_1 - libuv anaconda/pkgs/main/win-64::libuv-1.48.0-h827c3e9_0 - mkl-service anaconda/pkgs/main/win-64::mkl-service-2.4.0-py310h2bbff1b_1 - mkl_fft anaconda/pkgs/main/win-64::mkl_fft-1.3.11-py310h827c3e9_0 - mkl_random anaconda/pkgs/main/win-64::mkl_random-1.2.8-py310hc64d2fc_0 - mpc anaconda/pkgs/main/win-64::mpc-1.1.0-h7edee0f_1 - mpfr anaconda/pkgs/main/win-64::mpfr-4.0.2-h62dcd97_1 - mpir anaconda/pkgs/main/win-64::mpir-3.0.0-hec2e145_1 - mpmath anaconda/pkgs/main/win-64::mpmath-1.3.0-py310haa95532_0 - networkx anaconda/pkgs/main/win-64::networkx-3.3-py310haa95532_0 - numpy-base anaconda/pkgs/main/win-64::numpy-base-1.26.4-py310h65a83cf_0 - pytorch anaconda/pkgs/main/win-64::pytorch-2.3.1-cpu_py310h0ce1571_0 - pytorch-cpu anaconda/pkgs/main/win-64::pytorch-cpu-2.3.1-haa95532_0 - sympy anaconda/pkgs/main/win-64::sympy-1.13.3-py310haa95532_0 - -The following packages will be REMOVED: - - libblas-3.9.0-26_win64_mkl - libcblas-3.9.0-26_win64_mkl - liblapack-3.9.0-26_win64_mkl - -The following packages will be UPDATED: - - numpy anaconda/cloud/conda-forge::numpy-1.2~ --> anaconda/pkgs/main::numpy-1.26.4-py310h055cbcc_0 - -The following packages will be SUPERSEDED by a higher-priority channel: - - intel-openmp anaconda/cloud/conda-forge::intel-ope~ --> anaconda/pkgs/main::intel-openmp-2023.1.0-h59b6b97_46320 - mkl anaconda/cloud/conda-forge::mkl-2024.~ --> anaconda/pkgs/main::mkl-2023.1.0-h6b88ed4_46358 - - - -Downloading and Extracting Packages: - -Preparing transaction: done -Verifying transaction: done -Executing transaction: done -Looking in indexes: https://mirrors.aliyun.com/pypi/simple/ -Requirement already satisfied: pip in c:\users\eleven\desktop\easyvtuber-main\envs\miniconda3\envs\ezvtb_rt_venv\lib\site-packages (24.2) -Collecting pip - Downloading https://mirrors.aliyun.com/pypi/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl (1.8 MB) - ---------------------------------------- 1.8/1.8 MB 1.4 MB/s eta 0:00:00 -Requirement already satisfied: wheel in c:\users\eleven\desktop\easyvtuber-main\envs\miniconda3\envs\ezvtb_rt_venv\lib\site-packages (0.44.0) -Collecting wheel - Downloading https://mirrors.aliyun.com/pypi/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl (72 kB) -Installing collected packages: wheel, pip - Attempting uninstall: wheel - Found existing installation: wheel 0.44.0 - Uninstalling wheel-0.44.0: - Successfully uninstalled wheel-0.44.0 - Attempting uninstall: pip - Found existing installation: pip 24.2 - Uninstalling pip-24.2: - Successfully uninstalled pip-24.2 -Successfully installed pip-24.3.1 wheel-0.45.1 -Looking in indexes: https://mirrors.aliyun.com/pypi/simple/ -Collecting nvidia-cudnn-cu12 - Downloading https://mirrors.aliyun.com/pypi/packages/07/1d/3c7055e1d189f55ed25183f50e1d82f477024289f925df22ac89c0f4d387/nvidia_cudnn_cu12-9.6.0.74-py3-none-win_amd64.whl (502.8 MB) - ---------------------------------------- 502.8/502.8 MB 6.3 MB/s eta 0:00:00 -Collecting nvidia-cublas-cu12 (from nvidia-cudnn-cu12) - Downloading https://mirrors.aliyun.com/pypi/packages/84/f7/985e9bdbe3e0ac9298fcc8cfa51a392862a46a0ffaccbbd56939b62a9c83/nvidia_cublas_cu12-12.6.4.1-py3-none-win_amd64.whl (434.5 MB) - ---------------------------------------- 434.5/434.5 MB 6.2 MB/s eta 0:00:00 -Installing collected packages: nvidia-cublas-cu12, nvidia-cudnn-cu12 -Successfully installed nvidia-cublas-cu12-12.6.4.1 nvidia-cudnn-cu12-9.6.0.74 -Looking in indexes: https://pypi.org/simple, https://pypi.nvidia.com -Collecting tensorrt_cu12_libs==10.6.0 - Downloading https://pypi.nvidia.com/tensorrt-cu12-libs/tensorrt_cu12_libs-10.6.0-py2.py3-none-win_amd64.whl (1075.0 MB) - ---------------------------------------- 1.1/1.1 GB 8.2 MB/s eta 0:00:00 -Collecting tensorrt_cu12_bindings==10.6.0 - Downloading https://pypi.nvidia.com/tensorrt-cu12-bindings/tensorrt_cu12_bindings-10.6.0-cp310-none-win_amd64.whl (814 kB) - ---------------------------------------- 814.1/814.1 kB 12.0 MB/s eta 0:00:00 -Collecting tensorrt==10.6.0 - Downloading https://pypi.nvidia.com/tensorrt/tensorrt-10.6.0.tar.gz (16 kB) - Preparing metadata (setup.py) ... done -Collecting nvidia-cuda-runtime-cu12 (from tensorrt_cu12_libs==10.6.0) - Downloading https://pypi.nvidia.com/nvidia-cuda-runtime-cu12/nvidia_cuda_runtime_cu12-12.6.77-py3-none-win_amd64.whl (891 kB) - ---------------------------------------- 891.8/891.8 kB 10.2 MB/s eta 0:00:00 -Collecting tensorrt-cu12==10.6.0 (from tensorrt==10.6.0) - Downloading https://pypi.nvidia.com/tensorrt-cu12/tensorrt-cu12-10.6.0.tar.gz (18 kB) - Preparing metadata (setup.py) ... done -Building wheels for collected packages: tensorrt, tensorrt-cu12 - Building wheel for tensorrt (setup.py) ... done - Created wheel for tensorrt: filename=tensorrt-10.6.0-py2.py3-none-any.whl size=16345 sha256=3fc94cd935389ea9535e279dc81bdf07b97da751150f64a1f45c39ba8b5c2663 - Stored in directory: c:\users\eleven\appdata\local\pip\cache\wheels\42\bd\86\598d26504705081ad82ae23a08a869322ee5a8bdb410d5d298 - Building wheel for tensorrt-cu12 (setup.py) ... done - Created wheel for tensorrt-cu12: filename=tensorrt_cu12-10.6.0-py2.py3-none-any.whl size=17568 sha256=1856795aec69a0f6767ab62c0962d586ea16c3301c0f7040cde242c5f92f9abc - Stored in directory: c:\users\eleven\appdata\local\pip\cache\wheels\13\99\06\326119ed088736cbd6ded866ada42bb29bbb67bed34903e57b -Successfully built tensorrt tensorrt-cu12 -Installing collected packages: tensorrt_cu12_bindings, tensorrt-cu12, nvidia-cuda-runtime-cu12, tensorrt_cu12_libs, tensorrt -Successfully installed nvidia-cuda-runtime-cu12-12.6.77 tensorrt-10.6.0 tensorrt-cu12-10.6.0 tensorrt_cu12_bindings-10.6.0 tensorrt_cu12_libs-10.6.0 -Looking in indexes: https://mirrors.aliyun.com/pypi/simple/ -Requirement already satisfied: numpy==1.26.4 in c:\users\eleven\desktop\easyvtuber-main\envs\miniconda3\envs\ezvtb_rt_venv\lib\site-packages (from -r requirements.txt (line 1)) (1.26.4) -Collecting scipy (from -r requirements.txt (line 2)) - Downloading https://mirrors.aliyun.com/pypi/packages/51/8c/c2d371111961f737ae08881f654cf54eca796c42ec0429add2a06df97049/scipy-1.15.0-cp310-cp310-win_amd64.whl (43.9 MB) - ---------------------------------------- 43.9/43.9 MB 6.5 MB/s eta 0:00:00 -Collecting matplotlib (from -r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/5a/85/b9a54d64585a6b8737a78a61897450403c30f39e0bd3214270bb0b96f002/matplotlib-3.10.0-cp310-cp310-win_amd64.whl (8.0 MB) - ---------------------------------------- 8.0/8.0 MB 6.7 MB/s eta 0:00:00 -Collecting protobuf==3.20.1 (from -r requirements.txt (line 4)) - Downloading https://mirrors.aliyun.com/pypi/packages/c0/9c/bb88091287418ae1cf8af2bb9ed9710748a562b9abc227e4884d687a8650/protobuf-3.20.1-cp310-cp310-win_amd64.whl (903 kB) - ---------------------------------------- 903.8/903.8 kB 6.8 MB/s eta 0:00:00 -Collecting pynput==1.7.6 (from -r requirements.txt (line 5)) - Downloading https://mirrors.aliyun.com/pypi/packages/02/27/4de87850ff87c8dcecaaf8d27f28cec89ef17eeb6938f250449cb2635e06/pynput-1.7.6-py2.py3-none-any.whl (89 kB) -Collecting mediapipe (from -r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/f6/7a/448885fc960a972d8bf1939602919fdaa8de17f505b6c50782a074e7b70e/mediapipe-0.10.20-cp310-cp310-win_amd64.whl (51.0 MB) - ---------------------------------------- 51.0/51.0 MB 6.2 MB/s eta 0:00:00 -Collecting opencv_python==4.5.5.64 (from -r requirements.txt (line 7)) - Downloading https://mirrors.aliyun.com/pypi/packages/48/c3/798bd7b8f78430f82ec0660b753106717e4e4bb8032ce56f77d8577cb24b/opencv_python-4.5.5.64-cp36-abi3-win_amd64.whl (35.4 MB) - ---------------------------------------- 35.4/35.4 MB 6.3 MB/s eta 0:00:00 -Collecting Pillow==9.1.0 (from -r requirements.txt (line 8)) - Downloading https://mirrors.aliyun.com/pypi/packages/a8/df/1177786a2d1c0bf732ba6d5f05a2fa40f016e81e1c16d62f1101e35d271e/Pillow-9.1.0-cp310-cp310-win_amd64.whl (3.3 MB) - ---------------------------------------- 3.3/3.3 MB 6.4 MB/s eta 0:00:00 -Collecting pyanime4k==2.5.2 (from -r requirements.txt (line 9)) - Downloading https://mirrors.aliyun.com/pypi/packages/81/af/25a4a2e9a3d46820c016f6bb2cc235d953f104546a079c8c67bf09548734/pyanime4k-2.5.2-py3-none-win_amd64.whl (35.4 MB) - ---------------------------------------- 35.4/35.4 MB 6.3 MB/s eta 0:00:00 -Collecting pyvirtualcam==0.9.1 (from -r requirements.txt (line 10)) - Downloading https://mirrors.aliyun.com/pypi/packages/ca/59/e245e2d9832c6cefd212133d5c1ef988e66e7ccd93f4381017e844949334/pyvirtualcam-0.9.1-cp310-cp310-win_amd64.whl (158 kB) -Collecting pyopengl (from -r requirements.txt (line 11)) - Downloading https://mirrors.aliyun.com/pypi/packages/99/48/00e31747821d3fc56faddd00a4725454d1e694a8b67d715cf20f531506a5/PyOpenGL-3.1.7-py3-none-any.whl (2.4 MB) - ---------------------------------------- 2.4/2.4 MB 6.3 MB/s eta 0:00:00 -Collecting onnx (from -r requirements.txt (line 12)) - Downloading https://mirrors.aliyun.com/pypi/packages/0e/d3/d26ebf590a65686dde6b27fef32493026c5be9e42083340d947395f93405/onnx-1.17.0-cp310-cp310-win_amd64.whl (14.5 MB) - ---------------------------------------- 14.5/14.5 MB 6.0 MB/s eta 0:00:00 -Collecting onnxruntime-directml (from -r requirements.txt (line 13)) - Downloading https://mirrors.aliyun.com/pypi/packages/3c/4f/f433239b05304aa9af0217da20508abbbcec1dcd58ee821e3dab8939ecfe/onnxruntime_directml-1.20.1-cp310-cp310-win_amd64.whl (22.8 MB) - ---------------------------------------- 22.8/22.8 MB 6.3 MB/s eta 0:00:00 -Collecting turbojpeg (from -r requirements.txt (line 14)) - Downloading https://mirrors.aliyun.com/pypi/packages/a0/06/1a6ca4e7eb9c8342e5991093a36dcde37033a8f8da58dcf680282bb2b733/turbojpeg-0.0.2-cp310-cp310-win_amd64.whl (439 kB) -Collecting tqdm (from -r requirements.txt (line 15)) - Downloading https://mirrors.aliyun.com/pypi/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl (78 kB) -Collecting six (from pynput==1.7.6->-r requirements.txt (line 5)) - Downloading https://mirrors.aliyun.com/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl (11 kB) -Collecting ffmpeg-python>=0.2.0 (from pyanime4k==2.5.2->-r requirements.txt (line 9)) - Downloading https://mirrors.aliyun.com/pypi/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl (25 kB) -Collecting contourpy>=1.0.1 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl (218 kB) -Collecting cycler>=0.10 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl (8.3 kB) -Collecting fonttools>=4.22.0 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/19/d1/4dcd865360fb2c499749a913fe80e41c26e8ae18629d87dfffa3de27e831/fonttools-4.55.3-cp310-cp310-win_amd64.whl (2.2 MB) - ---------------------------------------- 2.2/2.2 MB 6.3 MB/s eta 0:00:00 -Collecting kiwisolver>=1.3.1 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl (71 kB) -Collecting packaging>=20.0 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl (65 kB) -Collecting pyparsing>=2.3.1 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl (107 kB) -Collecting python-dateutil>=2.7 (from matplotlib->-r requirements.txt (line 3)) - Downloading https://mirrors.aliyun.com/pypi/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB) -Collecting absl-py (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/a2/ad/e0d3c824784ff121c03cc031f944bc7e139a8f1870ffd2845cc2dd76f6c4/absl_py-2.1.0-py3-none-any.whl (133 kB) -Collecting attrs>=19.1.0 (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl (63 kB) -Collecting flatbuffers>=2.0 (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/fb/b4/31c461eef98b96b8ab736d97274548eaf2b2e349bf09e4de3902f7d53084/flatbuffers-24.12.23-py2.py3-none-any.whl (30 kB) -Collecting jax (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/22/49/b4418a7a892c0dd64442bbbeef54e1cdfe722dfc5a7bf0d611d3f5f90e99/jax-0.4.38-py3-none-any.whl (2.2 MB) - ---------------------------------------- 2.2/2.2 MB 6.3 MB/s eta 0:00:00 -Collecting jaxlib (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/0d/04/120c4caac6151f7297fedf9dd776362aa2d417d3f87bda826050b4da45e8/jaxlib-0.4.38-cp310-cp310-win_amd64.whl (64.2 MB) - ---------------------------------------- 64.2/64.2 MB 6.2 MB/s eta 0:00:00 -WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ReadTimeoutError("HTTPSConnectionPool(host='mirrors.aliyun.com', port=443): Read timed out. (read timeout=15)")': /pypi/simple/opencv-contrib-python/ -Collecting opencv-contrib-python (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/a7/9e/7110d2c5d543ab03b9581dbb1f8e2429863e44e0c9b4960b766f230c1279/opencv_contrib_python-4.10.0.84-cp37-abi3-win_amd64.whl (45.5 MB) - ---------------------------------------- 45.5/45.5 MB 6.3 MB/s eta 0:00:00 -INFO: pip is looking at multiple versions of mediapipe to determine which version is compatible with other requirements. This could take a while. -Collecting mediapipe (from -r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/d0/ca/4a32e95f1767467f7c461d7d5b4b506faebb1e30e35c1031c1b31cf82846/mediapipe-0.10.18-cp310-cp310-win_amd64.whl (50.9 MB) - ---------------------------------------- 50.9/50.9 MB 6.3 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/ea/1b/79e1ddc041012f5e5a7d60cbfd8cfe6037c5c87f4e77b4fdc0f9dfa2d44e/mediapipe-0.10.14-cp310-cp310-win_amd64.whl (50.8 MB) - ---------------------------------------- 50.8/50.8 MB 6.3 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/bb/b7/ffcdd19de3bf443864d0b0fed2b102275b96cbfbc1ddcf96ad811bbc1af0/mediapipe-0.10.13-cp310-cp310-win_amd64.whl (50.8 MB) - ---------------------------------------- 50.8/50.8 MB 6.4 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/12/50/9c24e158350d3f93be669db291fb452f21a25d874c94c5758374be82fff1/mediapipe-0.10.11-cp310-cp310-win_amd64.whl (50.8 MB) - ---------------------------------------- 50.8/50.8 MB 6.4 MB/s eta 0:00:00 -Collecting sounddevice>=0.4.4 (from mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/af/9b/15217b04f3b36d30de55fef542389d722de63f1ad81f9c72d8afc98cb6ab/sounddevice-0.5.1-py3-none-win_amd64.whl (363 kB) -INFO: pip is looking at multiple versions of onnx to determine which version is compatible with other requirements. This could take a while. -Collecting onnx (from -r requirements.txt (line 12)) - Downloading https://mirrors.aliyun.com/pypi/packages/0c/e4/1bc3ae56e6581587926a50a5c9dce3cfacf510e592e2512c7f5f2a9a4859/onnx-1.16.2-cp310-cp310-win_amd64.whl (14.4 MB) - ---------------------------------------- 14.4/14.4 MB 6.4 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/9c/7c/40fbebcb30f0fb6a773ca36e3a9dd4bdccd4b15455ef6c21a335781db78f/onnx-1.16.1-cp310-cp310-win_amd64.whl (14.4 MB) - ---------------------------------------- 14.4/14.4 MB 6.3 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/3b/e2/471ff83b3862967791d67f630000afce038756afbdf0665a3d767677c851/onnx-1.16.0-cp310-cp310-win_amd64.whl (14.4 MB) - ---------------------------------------- 14.4/14.4 MB 6.3 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/1b/43/6e84bf35a9201fb344b1a98edf7095c4fa1cf1478dfb6638d7b77f5475e6/onnx-1.15.0-cp310-cp310-win_amd64.whl (14.3 MB) - ---------------------------------------- 14.3/14.3 MB 6.3 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/00/36/e7a7e7a85564e7d409e4e8addfa11d41015d2190bfff30064771e7c21ca0/onnx-1.14.1-cp310-cp310-win_amd64.whl (13.3 MB) - ---------------------------------------- 13.3/13.3 MB 6.2 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/22/5c/46298252ea9f92b6b94184e8f001e575f2c346a22011498110fd032fc921/onnx-1.14.0-cp310-cp310-win_amd64.whl (13.3 MB) - ---------------------------------------- 13.3/13.3 MB 6.2 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/bb/cd/e2e63b1f0922dd45f6941a3422ccdb400360955064654a1a3a20e6a613fb/onnx-1.13.1-cp310-cp310-win_amd64.whl (12.2 MB) - ---------------------------------------- 12.2/12.2 MB 6.2 MB/s eta 0:00:00 -INFO: pip is still looking at multiple versions of onnx to determine which version is compatible with other requirements. This could take a while. - Downloading https://mirrors.aliyun.com/pypi/packages/97/e1/bdb42dde046507250083862520428901af3f5182faac50d3df5f13a4ff3d/onnx-1.13.0-cp310-cp310-win_amd64.whl (12.2 MB) - ---------------------------------------- 12.2/12.2 MB 6.5 MB/s eta 0:00:00 - Downloading https://mirrors.aliyun.com/pypi/packages/b0/f0/5e383f64293857e86c300cb70010cce1ca6f038f1563b85501d30404ad2a/onnx-1.12.0-cp310-cp310-win_amd64.whl (11.5 MB) - ---------------------------------------- 11.5/11.5 MB 6.2 MB/s eta 0:00:00 -Requirement already satisfied: typing-extensions>=3.6.2.1 in c:\users\eleven\desktop\easyvtuber-main\envs\miniconda3\envs\ezvtb_rt_venv\lib\site-packages (from onnx->-r requirements.txt (line 12)) (4.12.2) -Collecting coloredlogs (from onnxruntime-directml->-r requirements.txt (line 13)) - Downloading https://mirrors.aliyun.com/pypi/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl (46 kB) -Requirement already satisfied: sympy in c:\users\eleven\desktop\easyvtuber-main\envs\miniconda3\envs\ezvtb_rt_venv\lib\site-packages (from onnxruntime-directml->-r requirements.txt (line 13)) (1.13.3) -Collecting colorama (from tqdm->-r requirements.txt (line 15)) - Downloading https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl (25 kB) -Collecting future (from ffmpeg-python>=0.2.0->pyanime4k==2.5.2->-r requirements.txt (line 9)) - Downloading https://mirrors.aliyun.com/pypi/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl (491 kB) -Collecting CFFI>=1.0 (from sounddevice>=0.4.4->mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl (181 kB) -Collecting humanfriendly>=9.1 (from coloredlogs->onnxruntime-directml->-r requirements.txt (line 13)) - Downloading https://mirrors.aliyun.com/pypi/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl (86 kB) -Collecting ml_dtypes>=0.4.0 (from jax->mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/6b/7e/bc54ae885e4d702e60a4bf50aa9066ff35e9c66b5213d11091f6bffb3036/ml_dtypes-0.5.1-cp310-cp310-win_amd64.whl (209 kB) -Collecting opt_einsum (from jax->mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl (71 kB) -Requirement already satisfied: mpmath<1.4,>=1.1.0 in c:\users\eleven\desktop\easyvtuber-main\envs\miniconda3\envs\ezvtb_rt_venv\lib\site-packages (from sympy->onnxruntime-directml->-r requirements.txt (line 13)) (1.3.0) -Collecting pycparser (from CFFI>=1.0->sounddevice>=0.4.4->mediapipe->-r requirements.txt (line 6)) - Downloading https://mirrors.aliyun.com/pypi/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl (117 kB) -Collecting pyreadline3 (from humanfriendly>=9.1->coloredlogs->onnxruntime-directml->-r requirements.txt (line 13)) - Downloading https://mirrors.aliyun.com/pypi/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl (83 kB) -Installing collected packages: pyopengl, flatbuffers, turbojpeg, six, scipy, pyvirtualcam, pyreadline3, pyparsing, pycparser, protobuf, Pillow, packaging, opt_einsum, opencv_python, opencv-contrib-python, ml_dtypes, kiwisolver, future, fonttools, cycler, contourpy, colorama, attrs, absl-py, tqdm, python-dateutil, pynput, onnx, jaxlib, humanfriendly, ffmpeg-python, CFFI, sounddevice, pyanime4k, matplotlib, jax, coloredlogs, onnxruntime-directml, mediapipe -Successfully installed CFFI-1.17.1 Pillow-9.1.0 absl-py-2.1.0 attrs-24.3.0 colorama-0.4.6 coloredlogs-15.0.1 contourpy-1.3.1 cycler-0.12.1 ffmpeg-python-0.2.0 flatbuffers-24.12.23 fonttools-4.55.3 future-1.0.0 humanfriendly-10.0 jax-0.4.38 jaxlib-0.4.38 kiwisolver-1.4.8 matplotlib-3.10.0 mediapipe-0.10.11 ml_dtypes-0.5.1 onnx-1.12.0 onnxruntime-directml-1.20.1 opencv-contrib-python-4.10.0.84 opencv_python-4.5.5.64 opt_einsum-3.4.0 packaging-24.2 protobuf-3.20.1 pyanime4k-2.5.2 pycparser-2.22 pynput-1.7.6 pyopengl-3.1.7 pyparsing-3.2.1 pyreadline3-3.5.4 python-dateutil-2.9.0.post0 pyvirtualcam-0.9.1 scipy-1.15.0 six-1.17.0 sounddevice-0.5.1 tqdm-4.67.1 turbojpeg-0.0.2 -Press any key to continue . . . \ No newline at end of file From 23fc058ac64d82ab2e6ee7aa7dd19d0ba3a0c0f0 Mon Sep 17 00:00:00 2001 From: zpeng11 Date: Tue, 3 Feb 2026 01:11:08 -0500 Subject: [PATCH 33/46] feat:export release --- 01A.启动器.bat | 6 + 01B.启动器(调试输出).bat | 8 + 02A.启动器.bat | 6 - 02B.启动器(调试输出).bat | 8 - 03.构建tha4的ONNX和TensorRT学生模型.bat | 74 ----- README.md | 27 +- launcher2.py | 2 +- pack_release.bat | 80 +++--- requirements.txt | 1 - src/ezvtb_rt_interface.py | 11 +- test.py | 258 ------------------ .../onnx_export_tha4.py | 0 .../onnx_export_tha4_student.py | 0 .../test_tha4_student.py | 0 tha4_adapter.py => tha4/tha4_adapter.py | 0 .../tha4_convert_onnx_to_trt.py | 0 tha4_core.py => tha4/tha4_core.py | 0 tha4_core_old.py => tha4/tha4_core_old.py | 0 .../tha4_student_convert_onnx_to_trt.py | 0 19 files changed, 78 insertions(+), 403 deletions(-) create mode 100644 01A.启动器.bat create mode 100644 01B.启动器(调试输出).bat delete mode 100644 02A.启动器.bat delete mode 100644 02B.启动器(调试输出).bat delete mode 100644 03.构建tha4的ONNX和TensorRT学生模型.bat delete mode 100644 test.py rename onnx_export_tha4.py => tha4/onnx_export_tha4.py (100%) rename onnx_export_tha4_student.py => tha4/onnx_export_tha4_student.py (100%) rename test_tha4_student.py => tha4/test_tha4_student.py (100%) rename tha4_adapter.py => tha4/tha4_adapter.py (100%) rename tha4_convert_onnx_to_trt.py => tha4/tha4_convert_onnx_to_trt.py (100%) rename tha4_core.py => tha4/tha4_core.py (100%) rename tha4_core_old.py => tha4/tha4_core_old.py (100%) rename tha4_student_convert_onnx_to_trt.py => tha4/tha4_student_convert_onnx_to_trt.py (100%) diff --git a/01A.启动器.bat b/01A.启动器.bat new file mode 100644 index 0000000..8fa5cf0 --- /dev/null +++ b/01A.启动器.bat @@ -0,0 +1,6 @@ +@echo on +cd /D "%~dp0" + +SET PATH=%~dp0envs\TensorRT-RTX-1.3.0.35_cu129\bin;%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% + +pythonw launcher2.py \ No newline at end of file diff --git a/01B.启动器(调试输出).bat b/01B.启动器(调试输出).bat new file mode 100644 index 0000000..453f60c --- /dev/null +++ b/01B.启动器(调试输出).bat @@ -0,0 +1,8 @@ +@echo on +cd /D "%~dp0" + +SET PATH=%~dp0envs\TensorRT-RTX-1.3.0.35_cu129\bin;%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% + +python launcher2.py + +pause \ No newline at end of file diff --git a/02A.启动器.bat b/02A.启动器.bat deleted file mode 100644 index 09c6fc0..0000000 --- a/02A.启动器.bat +++ /dev/null @@ -1,6 +0,0 @@ -@echo on -cd /D "%~dp0" - -SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% - -pythonw launcher2.py \ No newline at end of file diff --git a/02B.启动器(调试输出).bat b/02B.启动器(调试输出).bat deleted file mode 100644 index d35ae38..0000000 --- a/02B.启动器(调试输出).bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo on -cd /D "%~dp0" - -SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% - -python launcher2.py - -pause \ No newline at end of file diff --git a/03.构建tha4的ONNX和TensorRT学生模型.bat b/03.构建tha4的ONNX和TensorRT学生模型.bat deleted file mode 100644 index f7261fa..0000000 --- a/03.构建tha4的ONNX和TensorRT学生模型.bat +++ /dev/null @@ -1,74 +0,0 @@ -@echo off -setlocal enabledelayedexpansion -cd /D "%~dp0" - -SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% - -echo ================================================================================ -echo THA4 ѧÉúģÐÍ ONNX ºÍ TensorRT ת»»¹¤¾ß -echo ================================================================================ -echo. - -REM ¼ì²éģÐÍĿ¼ÊǷñ´æÔÚ -if not exist "data\models\custom_tha4_models" ( - echo [´íÎó] ÕҲ»µ½ģÐÍĿ¼: data\models\custom_tha4_models - pause - exit /b 1 -) - -echo ¿ÉÓõÄ THA4 ѧÉúģÐÍ: -echo -------------------------------------------------------------------------------- -set count=0 -for /d %%i in (data\models\custom_tha4_models\*) do ( - set /a count+=1 - echo [!count!] %%~nxi -) -echo -------------------------------------------------------------------------------- -echo. - -if %count%==0 ( - echo [´íÎó] δÕҵ½ÈκÎģÐÍÎļþ¼Ð - pause - exit /b 1 -) - -echo ÇëÊäÈëҪת»»µÄģÐÍÎļþ¼ÐÃû³Æ: -set /p MODEL_NAME=^> - -REM ¼ì²éÊäÈëÊǷñΪ¿Õ -if "%MODEL_NAME%"=="" ( - echo [´íÎó] ģÐÍÃû³Ʋ»ÄÜΪ¿Õ - pause - exit /b 1 -) - -REM ¼ì²éģÐÍÎļþ¼ÐÊǷñ´æÔÚ -if not exist "data\models\custom_tha4_models\%MODEL_NAME%" ( - echo [´íÎó] ģÐÍÎļþ¼в»´æÔÚ: data\models\custom_tha4_models\%MODEL_NAME% - pause - exit /b 1 -) - -echo. -echo ================================================================================ -echo ²½Öè 1/2: µ¼³ö ONNX ģÐÍ -echo ================================================================================ -echo ģÐÍ: %MODEL_NAME% -echo ÃüÁî: python onnx_export_tha4_student.py %MODEL_NAME% --cpu -echo -------------------------------------------------------------------------------- -echo. - -python onnx_export_tha4_student.py %MODEL_NAME% --cpu - -echo ================================================================================ -echo ²½Öè 2/2: ת»»Ϊ TensorRT ģÐÍ -echo ================================================================================ -echo ģÐÍ: %MODEL_NAME% -echo ÃüÁî: python tha4_student_convert_onnx_to_trt.py %MODEL_NAME% fp32 -echo -------------------------------------------------------------------------------- -echo. - -python tha4_student_convert_onnx_to_trt.py %MODEL_NAME% fp32 - -pause - diff --git a/README.md b/README.md index 1d4138e..989ca5d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Updates: * 使用 [waifu2x](https://github.com/nagadomi/waifu2x) 和 [real-esrgan](https://github.com/xinntao/Real-ESRGAN) 项目带来的输出超分辨率,对比anime4k效果提升显著(也使用更多gpu占用) * 更漂亮的 WxPython 新启动器界面,带中文介绍更加简单易用 * 添加Spout2 支持输出原生透明通道给OBS +* 添加 [THA V4](https://github.com/pkhungurn/talking-head-anime-4-demo) 支持,拥有更好的图形输出 ## Requirements @@ -43,7 +44,7 @@ Updates: 使用Spout2插件可以提供透明通道给OBS,访问 https://github.com/Off-World-Live/obs-spout2-plugin/releases 项目,请选择与你的OBS版本兼容的插件安装exe。 ### 启动项目 -双击`02A.启动器.bat` 或 `02B.启动器(调试输出).bat` 启动器,如下图启动。 +双击`01A.启动器.bat` 或 `01B.启动器(调试输出).bat` 启动器,如下图启动。 ![](assets/02success.png) 之后请移步后文的输入输出&性能配置板块自行调节。如有问题请先参考本页末的FAQ再提出。 @@ -240,3 +241,27 @@ A卡I卡使用 DirectML,这个框架与显卡厂商的显卡驱动及 Direct12 > Q6: 本机有两张显卡,如何使用第二张副卡使用这个项目? 在运行前配置环境变量`EZVTB_DEVICE_ID`为你想要运行的GPU ID,此变量缺省为`0` + +## References +``` +@inproceedings{huang2022rife, + title={Real-Time Intermediate Flow Estimation for Video Frame Interpolation}, + author={Huang, Zhewei and Zhang, Tianyuan and Heng, Wen and Shi, Boxin and Zhou, Shuchang}, + booktitle={Proceedings of the European Conference on Computer Vision (ECCV)}, + year={2022} +} +@misc{Khungurn:2022, + author = {Pramook Khungurn}, + title = {Talking Head(?) Anime from a Single Image 3: Now the Body Too}, + howpublished = {\url{http://pkhungurn.github.io/talking-head-anime-3/}}, + year = 2022, + note = {Accessed: YYYY-MM-DD}, +} + +@InProceedings{wang2021realesrgan, + author = {Xintao Wang and Liangbin Xie and Chao Dong and Ying Shan}, + title = {Real-ESRGAN: Training Real-World Blind Super-Resolution with Pure Synthetic Data}, + booktitle = {International Conference on Computer Vision Workshops (ICCVW)}, + date = {2021} +} +``` \ No newline at end of file diff --git a/launcher2.py b/launcher2.py index b798028..6d0edbb 100644 --- a/launcher2.py +++ b/launcher2.py @@ -318,7 +318,7 @@ class LauncherPanel(wx.Panel): addOption('beta', title='Beta', desc='输入滤波速度补偿,\n越小越平滑,越大运动时越灵敏', type=3, mapper=beta_mapper) - addOption('breath_cycle', title='Breath Cycle', desc='设置呼吸循环时间', + addOption('breath_cycle', title='Breath Cycle', desc='设置呼吸循环时间(会增加占用)', choices=['No Breath', '3 seconds', '5 seconds', '7 seconds'], mapping=['inf', '3.0', '5.0', '7.0']) diff --git a/pack_release.bat b/pack_release.bat index 747e7df..922dd7b 100644 --- a/pack_release.bat +++ b/pack_release.bat @@ -6,61 +6,49 @@ REM 2. Rebase with the latest changes: git rebase main REM ======================================================================== cd /D "%~dp0" -@REM git clean -fdx -@REM git submodule update --init --recursive -@REM if not exist "envs" mkdir envs -@REM if not exist "envs\miniconda3" mkdir envs\miniconda3 +if not exist "envs" mkdir envs +if not exist "envs\miniconda3" mkdir envs\miniconda3 -@REM IF not EXIST %~dp0envs\miniconda3\Scripts ( -@REM @RD /S /Q %~dp0envs\miniconda3 -@REM mkdir %~dp0envs\miniconda3 -@REM echo "Downloading miniconda..." -@REM powershell -Command "Invoke-WebRequest -Uri 'https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe' -OutFile '.\envs\miniconda3.exe' -UseBasicParsing -Headers @{'User-Agent'='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'}" +IF not EXIST %~dp0envs\TensorRT-RTX-1.3.0.35_cu129\bin ( + @RD /S /Q %~dp0envs\TensorRT-RTX-1.3.0.35_cu129 + cd /D "%~dp0\envs" + curl -L -o trt_rtx.zip https://developer.nvidia.com/downloads/trt/rtx_sdk/secure/1.3/TensorRT-RTX-1.3.0.35-win10-amd64-cuda-12.9-Release-external.zip + tar -xf trt_rtx.zip "TensorRT-RTX-1.3.0.35" && ren "TensorRT-RTX-1.3.0.35" "TensorRT-RTX-1.3.0.35_cu129" && del trt_rtx.zip + cd /D "%~dp0" +) -@REM echo "Installling minconda..." -@REM start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 -@REM echo "Successfully install minconda" -@REM del envs\miniconda3.exe -@REM ) +IF not EXIST %~dp0envs\miniconda3\Scripts ( + @RD /S /Q %~dp0envs\miniconda3 + mkdir %~dp0envs\miniconda3 + echo "Downloading miniconda..." + powershell -Command "Invoke-WebRequest -Uri 'https://repo.anaconda.com/miniconda/Miniconda3-py310_24.9.2-0-Windows-x86_64.exe' -OutFile '.\envs\miniconda3.exe' -UseBasicParsing -Headers @{'User-Agent'='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'}" -@REM SET PATH=%~dp0envs\miniconda3\Scripts;%PATH% + echo "Installling minconda..." + start /wait "" %~dp0envs\miniconda3.exe /S /AddToPath=0 /RegisterPython=0 /InstallationType=JustMe /D=%~dp0envs\miniconda3 + echo "Successfully install minconda" + del envs\miniconda3.exe +) -@REM call activate -@REM call conda env list -@REM call conda update -y --all -@REM call conda create -n ezvtb_rt_venv_release -c conda-forge conda-pack python=3.10 -y -@REM call conda activate ezvtb_rt_venv_release -@REM call conda env list +SET PATH=%~dp0envs\TensorRT-RTX-1.3.0.35_cu129\bin;%~dp0envs\miniconda3\Scripts;%PATH% -@REM call conda install -y -c conda-forge cudatoolkit=12.9.1 cudnn=12.9.1 -@REM call conda install -y nvidia/label/cuda-12.9.1::cuda-nvcc-dev_win-64 -@REM call conda install -y pycuda -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ +call activate +call conda env list +call conda update -y --all +call conda create -n ezvtb_rt_venv_release python=3.10 -y +call conda activate ezvtb_rt_venv_release +call conda env list -@REM call conda-pack -n ezvtb_rt_venv_release -o %~dp0envs\python_embedded --format no-archive +call conda install -y conda-pack +call conda install -y conda-forge::pycuda +call conda install -y -c nvidia/label/cuda-12.9.1 cuda-nvcc-dev_win-64 cudnn cuda-runtime + +call conda-pack -n ezvtb_rt_venv_release -o %~dp0envs\python_embedded --format no-archive -@REM SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% +SET PATH=%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% +call python -m pip install %~dp0envs\TensorRT-RTX-1.3.0.35_cu129\python\tensorrt_rtx-1.3.0.35-cp310-none-win_amd64.whl +call python -m pip install -r requirements.txt --no-warn-script-location -@REM call python -m pip install --upgrade pip wheel -i https://mirrors.aliyun.com/pypi/simple/ -@REM echo yes|python -m pip install nvidia-cudnn-cu12 -i https://mirrors.aliyun.com/pypi/simple/ - -@REM echo yes|pip install tensorrt_cu12_libs==10.11.0.33 tensorrt_cu12_bindings==10.11.0.33 tensorrt==10.11.0.33 --extra-index-url https://pypi.nvidia.com - -@REM call python -m pip install -r requirements.txt --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ - -@REM if not exist "data\models" mkdir data\models -@REM echo "Downloading model zip file..." -@REM powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/zpeng11/ezvtuber-rt/releases/download/0.0.1/20241220.zip', '.\data\models\model.zip')" -@REM echo "Unzipping model zip file..." -@REM powershell -Command "Expand-Archive -Path '.\data\models\model.zip' -DestinationPath '.\data\models\temp' -Force; Move-Item -Path '.\data\models\temp\20241220\*' -Destination '.\data\models\' -Force; Remove-Item -Path '.\data\models\temp' -Recurse -Force" -@REM del .\data\models\model.zip - -@REM @REM To verify if environment works -@REM call python ezvtb_rt_interface.py - -@REM Clean up step -echo "Cleaning up .trt files in data\models directory..." -for /R %~dp0data\models %%f in (*.trt) do del "%%f" @RD /S /Q %~dp0envs\miniconda3 pause \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4671f68..1503fd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ numpy scipy -matplotlib protobuf pynput mediapipe==0.10.11 diff --git a/src/ezvtb_rt_interface.py b/src/ezvtb_rt_interface.py index b68ab2b..09204a2 100644 --- a/src/ezvtb_rt_interface.py +++ b/src/ezvtb_rt_interface.py @@ -1,6 +1,6 @@ import os import sys - +from .args import args dir_path = os.path.dirname(os.path.realpath(__file__)) ezvtb_path = os.path.join(dir_path, "..", "ezvtuber-rt") ezvtb_main_path = os.path.join(dir_path, "..", 'ezvtuber-rt-main') @@ -45,6 +45,7 @@ def get_core( from ezvtb_rt.core_trt import CoreTRT as Core except: print("TensorRT is not available, fallback to ONNX Runtime.") + args.use_tensorrt = False from ezvtb_rt.core_ort import CoreORT as Core else: from ezvtb_rt.core_ort import CoreORT as Core @@ -68,10 +69,4 @@ def get_core( use_eyebrow=model_use_eyebrow, ) return core - -if __name__ == '__main__': - try: - from ezvtb_rt.trt_utils import check_build_all_models - check_build_all_models() - except: - print("TensorRT not available, skip building TRT models.") \ No newline at end of file + \ No newline at end of file diff --git a/test.py b/test.py deleted file mode 100644 index 2ae533f..0000000 --- a/test.py +++ /dev/null @@ -1,258 +0,0 @@ -# from pyanime4k import ac -# import pyanime4k -# -# parameters = ac.Parameters() -# # enable HDN for ACNet -# parameters.HDN = True -# -# a = ac.AC( -# type=ac.ProcessorType.CPU_ACNet, -# ) -# a.set_arguments(parameters) -import struct - -import cv2 -# -# img = cv2.imread("character/test41.png") -# -# a.load_image_from_numpy(img,input_type=ac.AC_INPUT_BGR) -# -# a.get_processor_info() -# -# # start processing -# a.process() -# -# a.show_image() - -# import numpy as np -# import zlib -# import bz2 -# import lzma -# import time -# -# postprocessed_image=np.load('out.npy') -# tic=time.perf_counter() -# res=None -# for i in range(100): -# res=zlib.compress(postprocessed_image) -# print("zlib", len(res),(time.perf_counter() - tic) * 1000) -# tic = time.perf_counter() -# for i in range(100): -# zlib.decompress(res) -# print("zlib", len(res),(time.perf_counter() - tic) * 1000) -# tic = time.perf_counter() -# for i in range(100): -# res=bz2.compress(postprocessed_image) -# print("bz2",len(res), (time.perf_counter() - tic) * 1000) -# tic = time.perf_counter() -# for i in range(100): -# bz2.decompress(res) -# print("bz2", len(res),(time.perf_counter() - tic) * 1000) -# tic = time.perf_counter() -# for i in range(100): -# res=lzma.compress(postprocessed_image) -# print("lzma",len(res), (time.perf_counter() - tic) * 1000) -# tic = time.perf_counter() -# for i in range(100): -# lzma.decompress(res) -# print("lzma", len(res),(time.perf_counter() - tic) * 1000) -# tic = time.perf_counter() -# -# -# output_frame = cv2.cvtColor(postprocessed_image, cv2.COLOR_RGBA2BGRA) -# cv2.imshow("frame", output_frame) -# cv2.waitKey(100000) -# preview upscaled image -# img=a.save_image_to_numpy() -# -# print(img) -# import cv2 -# import numpy as np -# -# rgb=[ -# [[1,2,3],[2,2,3],[3,2,3],[4,2,3]], -# [[1,2,3],[2,2,3],[3,2,3],[4,2,3]], -# [[1,2,3],[2,2,3],[3,2,3],[4,2,3]], -# [[1,2,3],[2,2,3],[3,2,3],[4,2,3]] -# ] -# a=[ -# [1,2,3,4], -# [1,2,3,4], -# [1,2,3,4], -# [1,2,3,4], -# ] -# -# print(cv2.merge((np.array(rgb),np.array(a)))) - - -from multiprocessing import Value, Process, Queue -import queue -import socket -import errno -import time -import tha2.poser.modes.mode_20_wx -from tha2.mocap.ifacialmocap_constants import * -import numpy as np - -ifm='127.0.0.1:11573' - - - -class ClientProcess(Process): - def __init__(self): - super().__init__() - self.queue = Queue() - self.should_terminate = Value('b', False) - self.address = ifm.split(':')[0] - self.port = int(ifm.split(':')[1]) - self.perf_time = 0 - # self.ifm_converter = tha2.poser.modes.mode_20_wx.create_ifacialmocap_pose_converter() - self.a_min=None - self.a_max=None - - def run(self): - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.setblocking(False) - self.socket.bind(("", self.port)) - self.socket.settimeout(0.1) - while True: - if self.should_terminate.value: - break - try: - socket_bytes = self.socket.recv(8192) - except socket.error as e: - err = e.args[0] - if err == errno.EAGAIN or err == errno.EWOULDBLOCK or err == 'timed out': - continue - else: - raise e - - # socket_string = socket_bytes.decode("utf-8") - osf_raw=(struct.unpack('=di2f2fB1f4f3f3f68f136f210f14f',socket_bytes)) - # print(osf_raw[432:]) - data={} - OpenSeeDataIndex=[ - 'time', - 'id', - 'cameraResolutionW', - 'cameraResolutionH', - 'rightEyeOpen', - 'leftEyeOpen', - 'got3DPoints', - 'fit3DError', - 'rawQuaternionX', - 'rawQuaternionY', - 'rawQuaternionZ', - 'rawQuaternionW', - 'rawEulerX', - 'rawEulerY', - 'rawEulerZ', - 'translationY', - 'translationX', - 'translationZ', - ] - for i in range(len(OpenSeeDataIndex)): - data[OpenSeeDataIndex[i]]=osf_raw[i] - data['translationY']*=-1 - data['translationZ']*=-1 - data['rotationY']=data['rawEulerY'] - data['rotationX']=(-data['rawEulerX']+180)%360 - data['rotationZ']=(data['rawEulerZ']-90) - OpenSeeFeatureIndex=[ - 'EyeLeft', - 'EyeRight', - 'EyebrowSteepnessLeft', - 'EyebrowUpDownLeft', - 'EyebrowQuirkLeft', - 'EyebrowSteepnessRight', - 'EyebrowUpDownRight', - 'EyebrowQuirkRight', - 'MouthCornerUpDownLeft', - 'MouthCornerInOutLeft', - 'MouthCornerUpDownRight', - 'MouthCornerInOutRight', - 'MouthOpen', - 'MouthWide' - ] - - for i in range(68): - data['confidence'+str(i)]=osf_raw[i+18] - for i in range(68): - data['pointsX'+str(i)]=osf_raw[i*2+18+68] - data['pointsY'+str(i)]=osf_raw[i*2+18+68+1] - for i in range(70): - data['points3DX'+str(i)]=osf_raw[i*3+18+68+68*2] - data['points3DY'+str(i)]=osf_raw[i*3+18+68+68*2+1] - data['points3DZ'+str(i)]=osf_raw[i*3+18+68+68*2+2] - - for i in range(len(OpenSeeFeatureIndex)): - data[OpenSeeFeatureIndex[i]]=osf_raw[i+432] - # print(data['rotationX'],data['rotationY'],data['rotationZ']) - - a=np.array([ - data['points3DX66']-data['points3DX68']+data['points3DX67']-data['points3DX69'], - data['points3DY66']-data['points3DY68']+data['points3DY67']-data['points3DY69'], - data['points3DZ66']-data['points3DZ68']+data['points3DZ67']-data['points3DZ69'] - ]) - a=(a/np.linalg.norm(a)) - data['eyeRotationX']=a[0] - data['eyeRotationY']=a[1] - # blender_data = json.loads(socket_string) - # data = self.convert_from_blender_data(socket_string) - # data=self.ifm_converter.convert(data) - # print(data) - # if(self.a_max is None): - # self.a_max=[x for x in data] - # else: - # self.a_max=[max(data[i],self.a_max[i]) for i in range(len(data))] - # if(self.a_min is None): - # self.a_min=[x for x in data] - # else: - # self.a_min=[min(data[i],self.a_min[i]) for i in range(len(data))] - # print([[self.a_min[i],self.a_max[i]] for i in range(len(data))]) - cur_time = time.perf_counter() - fps = 1 / (cur_time - self.perf_time) - self.perf_time = cur_time - # print(fps) - try: - self.queue.put_nowait(data) - except queue.Full: - pass - self.queue.close() - self.socket.close() - @staticmethod - def convert_from_blender_data(blender_data): - data = {} - - for item in blender_data.split('|'): - if item.find('#') != -1: - k, arr = item.split('#') - arr = [float(n) for n in arr.split(',')] - data[k.replace("_L", "Left").replace("_R", "Right")] = arr - elif item.find('-') != -1: - k, v = item.split("-") - data[k.replace("_L", "Left").replace("_R", "Right")] = float(v) / 100 - - to_rad = 57.3 - data[HEAD_BONE_X] = data["=head"][0] / to_rad - data[HEAD_BONE_Y] = data["=head"][1] / to_rad - data[HEAD_BONE_Z] = data["=head"][2] / to_rad - data[HEAD_BONE_QUAT] = [data["=head"][3], data["=head"][4], data["=head"][5], 1] - # print(data[HEAD_BONE_QUAT][2],min(data[EYE_BLINK_LEFT],data[EYE_BLINK_RIGHT])) - data[RIGHT_EYE_BONE_X] = data["rightEye"][0] / to_rad - data[RIGHT_EYE_BONE_Y] = data["rightEye"][1] / to_rad - data[RIGHT_EYE_BONE_Z] = data["rightEye"][2] / to_rad - data[LEFT_EYE_BONE_X] = data["leftEye"][0] / to_rad - data[LEFT_EYE_BONE_Y] = data["leftEye"][1] / to_rad - data[LEFT_EYE_BONE_Z] = data["leftEye"][2] / to_rad - - return data - - -if __name__ == '__main__': - client_process = ClientProcess() - client_process.daemon = True - client_process.start() - - while True: - pass \ No newline at end of file diff --git a/onnx_export_tha4.py b/tha4/onnx_export_tha4.py similarity index 100% rename from onnx_export_tha4.py rename to tha4/onnx_export_tha4.py diff --git a/onnx_export_tha4_student.py b/tha4/onnx_export_tha4_student.py similarity index 100% rename from onnx_export_tha4_student.py rename to tha4/onnx_export_tha4_student.py diff --git a/test_tha4_student.py b/tha4/test_tha4_student.py similarity index 100% rename from test_tha4_student.py rename to tha4/test_tha4_student.py diff --git a/tha4_adapter.py b/tha4/tha4_adapter.py similarity index 100% rename from tha4_adapter.py rename to tha4/tha4_adapter.py diff --git a/tha4_convert_onnx_to_trt.py b/tha4/tha4_convert_onnx_to_trt.py similarity index 100% rename from tha4_convert_onnx_to_trt.py rename to tha4/tha4_convert_onnx_to_trt.py diff --git a/tha4_core.py b/tha4/tha4_core.py similarity index 100% rename from tha4_core.py rename to tha4/tha4_core.py diff --git a/tha4_core_old.py b/tha4/tha4_core_old.py similarity index 100% rename from tha4_core_old.py rename to tha4/tha4_core_old.py diff --git a/tha4_student_convert_onnx_to_trt.py b/tha4/tha4_student_convert_onnx_to_trt.py similarity index 100% rename from tha4_student_convert_onnx_to_trt.py rename to tha4/tha4_student_convert_onnx_to_trt.py From d623f6648f692e116f291ab199b55878f779af86 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Sun, 8 Feb 2026 21:38:05 +0800 Subject: [PATCH 34/46] fix: update interpolation and sr settings to "Off" in launcher2.py; synchronize character mapping and selection handling --- ezvtuber-rt | 2 +- launcher2.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index fb74e42..3803041 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit fb74e4224c1e39cef1b757c6da87b82c91a006ad +Subproject commit 380304184e367202fee9c3d6550532761281b30a diff --git a/launcher2.py b/launcher2.py index 6d0edbb..15994b3 100644 --- a/launcher2.py +++ b/launcher2.py @@ -44,9 +44,9 @@ default_arg = { 'ram_cache_size': '2gb', 'vram_cache_size': '2gb', 'model_select': 'seperable_half', - 'interpolation': 'x3_half', + 'interpolation': "Off", 'frame_rate_limit': '30', - 'sr': 'anime4k_x2', + 'sr': "Off", 'use_tensorrt': False, 'preset': 'Low', 'mouse_audio_input': False, @@ -484,17 +484,21 @@ class LauncherPanel(wx.Panel): def onActivate(e): global characterList - tName = self.optionDict['character'].GetValue() + # 用控件当前选中字符串,避免 GetValue() 用旧 mapping 按下标取导致 IndexError + char_ctrl = self.optionDict['character'].control + tName = char_ctrl.GetStringSelection() if char_ctrl.GetSelection() >= 0 else '' refreshList() - self.optionDict['character'].control.SetItems( - characterList) + scanStudentModels() + self.optionDict['character'].mapping = characterList # 同步 mapping,后续 GetValue() 才正确 + char_ctrl.SetItems(characterList) try: idx = characterList.index(tName) - self.optionDict['character'].control.SetSelection(idx) - except ValueError: - # Character not found, select first available + char_ctrl.SetSelection(idx) + except (ValueError, TypeError): if characterList: - self.optionDict['character'].control.SetSelection(0) + char_ctrl.SetSelection(0) + # 重新应用“当前模型 → 是否锁定 character”的规则 + onModelSelect() if not hasTRTSupport: self.optionDict['use_tensorrt'].control.SetValue(False) From 0d4907c4d6cf0906a0d7dc9ccec491b24d45f780 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Sun, 8 Feb 2026 21:45:54 +0800 Subject: [PATCH 35/46] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DDML=E6=97=B6?= =?UTF-8?q?=E6=9C=AA=E8=83=BD=E6=AD=A3=E7=A1=AE=E5=85=B3=E9=97=AD=E8=A1=A5?= =?UTF-8?q?=E5=B8=A7=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 3803041..64a3975 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 380304184e367202fee9c3d6550532761281b30a +Subproject commit 64a397543998d26af80c4364e7129c9a9d33505a From 034652338fef7b307a10a03fefbbd8e200af0e76 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Sun, 8 Feb 2026 23:29:44 +0800 Subject: [PATCH 36/46] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B8=A7?= =?UTF-8?q?=E7=8E=87=E9=99=90=E5=88=B6=E5=AE=9E=E7=8E=B0=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ezvtuber-rt | 2 +- src/args.py | 1 + src/main.py | 4 ++++ src/model_infer_client.py | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 64a3975..1118b7d 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 64a397543998d26af80c4364e7129c9a9d33505a +Subproject commit 1118b7d4856830a25f31ba5f491b535305c6b3a7 diff --git a/src/args.py b/src/args.py index 6800e06..4b2d98f 100644 --- a/src/args.py +++ b/src/args.py @@ -51,6 +51,7 @@ parser.add_argument('--eyebrow', action='store_true') parser.add_argument('--use_interpolation', action='store_true') parser.add_argument('--interpolation_scale', type=int, default=1) parser.add_argument('--interpolation_half', action='store_true') +parser.add_argument('--mark_interpolated', action='store_true', help='在插值帧左上角打红点便于确认补帧输出') parser.add_argument('--use_sr', action='store_true') parser.add_argument('--sr_x4', action='store_true') diff --git a/src/main.py b/src/main.py index fa09a59..77c800e 100644 --- a/src/main.py +++ b/src/main.py @@ -111,9 +111,13 @@ def main(): else: cv2.imshow("EasyVtuber Debug Frame", np_ret_shms[i]) cv2.waitKey(1) + # 限速:若 last_time 落后于当前时间则对齐到 now,避免开局成坨送帧 wait_until(last_time + interval) last_time += interval + now = time.perf_counter() + if last_time < now: + last_time = now ret_batch_shm_channels[i].release() print("Infer Process FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, Output Pipeline FPS {:.5f}".format( infer_process.pipeline_fps_number.value, diff --git a/src/model_infer_client.py b/src/model_infer_client.py index 4f57e88..8407a92 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -1,4 +1,5 @@ import math +import os from multiprocessing import Process, Value, shared_memory, Event import cv2 import numpy as np @@ -32,6 +33,9 @@ class ModelClientProcess(Process): self.finish_event = Event() def run(self): + # 插值帧红点标记:由 --mark_interpolated 控制,供 ezvtb_rt 读取 + if getattr(args, 'mark_interpolated', False): + os.environ['EZVTB_MARK_INTERPOLATED'] = '1' pose_position_shm_guard = SharedMemoryGuard(self.pose_position_shm, ctrl_name="pose_position_shm_ctrl") np_pose_shm = np.ndarray((45,), dtype=np.float32, buffer=self.pose_position_shm.buf[:45 * 4]) np_position_shm = np.ndarray((4,), dtype=np.float32, buffer=self.pose_position_shm.buf[45 * 4:45 * 4 + 4 * 4]) From 3abad68de5643a500573d0662bfe9fdd9947fafa Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 00:08:46 +0800 Subject: [PATCH 37/46] feat: add output pipeline FPS tracking and update display format in model inference --- src/main.py | 4 +++- src/model_infer_client.py | 33 ++++++++++++++++----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/main.py b/src/main.py index 77c800e..9d633f2 100644 --- a/src/main.py +++ b/src/main.py @@ -119,13 +119,15 @@ def main(): if last_time < now: last_time = now ret_batch_shm_channels[i].release() + output_pipeline_fps_val = pipeline_fps() * args.interpolation_scale + infer_process.output_pipeline_fps.value = output_pipeline_fps_val print("Infer Process FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, Output Pipeline FPS {:.5f}".format( infer_process.pipeline_fps_number.value, input_fps.value, infer_process.average_model_interval.value * 1000, infer_process.cache_hit_ratio.value * 100, infer_process.gpu_cache_hit_ratio.value * 100, - pipeline_fps() * args.interpolation_scale + output_pipeline_fps_val ), end ='\r', flush=True) if __name__ == "__main__": diff --git a/src/model_infer_client.py b/src/model_infer_client.py index 8407a92..e41566c 100644 --- a/src/model_infer_client.py +++ b/src/model_infer_client.py @@ -28,6 +28,7 @@ class ModelClientProcess(Process): self.cache_hit_ratio = Value('f', 0.0) self.gpu_cache_hit_ratio = Value('f', 0.0) self.pipeline_fps_number = Value('f', 0.0) + self.output_pipeline_fps = Value('f', 0.0) # 由 main 进程更新,与 main print 一致 self.input_fps = input_fps self.finish_event = Event() @@ -151,24 +152,22 @@ class ModelClientProcess(Process): (bgra_image.shape[1], bgra_image.shape[0])) if args.output_debug: - cv2.putText(bgra_image, str('OUT_FPS:%.1f' % (self.pipeline_fps_number.value * args.interpolation_scale)), (0, 16), cv2.FONT_HERSHEY_PLAIN, 1, - (0, 255, 0), 1) - cv2.putText(bgra_image, str( - 'GPU_FPS:%.1f' % ( - 1.0 / (self.average_model_interval.value + 1e-6) if not args.use_interpolation else 1.0 / (self.average_model_interval.value + 1e-6) * args.interpolation_scale)), - (0, 32), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - cv2.putText(bgra_image, str('INPUT_FPS:%.1f' % self.input_fps.value), (0, 48), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + # 与 main.py 输出格式一致 + y = 16 + cv2.putText(bgra_image, 'INFER/S: {:.4f}'.format(self.pipeline_fps_number.value), (0, y), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + y += 16 + cv2.putText(bgra_image, 'INPUT/S: {:.4f}'.format(self.input_fps.value), (0, y), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + y += 16 + cv2.putText(bgra_image, 'OUTPUT/S: {:.4f}'.format(self.output_pipeline_fps.value), (0, y), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + y += 16 + cv2.putText(bgra_image, 'CALC: {:.2f} ms'.format(self.average_model_interval.value * 1000), (0, y), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + y += 16 if args.max_ram_cache_len > 0: - cv2.putText(bgra_image, str('MEMCACHED:%.1f%%' % (self.cache_hit_ratio.value * 100)), - (0, 64), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - if args.max_gpu_cache_len > 0.0: - cv2.putText(bgra_image, str('GPUCACHED:%.1f%%' % (self.gpu_cache_hit_ratio.value * 100)), - (0, 80), - cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) - + cv2.putText(bgra_image, 'MEM CACHE: {:.2f}%'.format(self.cache_hit_ratio.value * 100), (0, y), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + y += 16 + if args.max_gpu_cache_len > 0: + cv2.putText(bgra_image, 'GPU CACHE: {:.2f}%'.format(self.gpu_cache_hit_ratio.value * 100), (0, y), cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) + if args.alpha_split: rgba_image = cv2.cvtColor(bgra_image, cv2.COLOR_BGRA2RGBA) alpha_channel = rgba_image[:, :, 3] From 81280cb879693d0f4b195c2c29b82adf6b174bc2 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 01:07:30 +0800 Subject: [PATCH 38/46] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E8=A1=A5?= =?UTF-8?q?=E5=B8=A7=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index 1118b7d..c6f3fd6 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit 1118b7d4856830a25f31ba5f491b535305c6b3a7 +Subproject commit c6f3fd6f64212a04cfb38ca258296999f1355e2a From efa8534776c7833fce43f79a43a7f72c08c5c1a4 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 01:19:18 +0800 Subject: [PATCH 39/46] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=99=A8=E8=AE=A9=E4=BB=96=E7=9B=B4=E6=8E=A5=E5=85=B3?= =?UTF-8?q?=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01A.启动器.bat | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/01A.启动器.bat b/01A.启动器.bat index 8fa5cf0..08cca03 100644 --- a/01A.启动器.bat +++ b/01A.启动器.bat @@ -3,4 +3,5 @@ cd /D "%~dp0" SET PATH=%~dp0envs\TensorRT-RTX-1.3.0.35_cu129\bin;%~dp0envs\python_embedded;%~dp0envs\python_embedded\Scripts;%~dp0envs\python_embedded\Library\bin;%PATH% -pythonw launcher2.py \ No newline at end of file +start "" pythonw launcher2.py +exit \ No newline at end of file From 03b44f4d4b17c84589a1b012fbe02e46200e4249 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 03:47:54 +0800 Subject: [PATCH 40/46] feat: enhance logging in launcher2.py with important log extraction and status updates --- launcher2.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/launcher2.py b/launcher2.py index 15994b3..ecd3582 100644 --- a/launcher2.py +++ b/launcher2.py @@ -1,6 +1,7 @@ import ctypes import os import subprocess +import threading import wx import json @@ -251,12 +252,89 @@ class OptionPanel(wx.Panel): return ret +def _important_log_line(line): + """从 main 的一行日志中提取“重要”的简短描述,用于状态栏;无关行返回 None。""" + line = line.strip() + if not line: + return None + if line.startswith('Launched:'): + return 'Launched' + if 'Model Inference Ready' in line: + return 'Model Inference Ready' + # TRT: Building engine from ONNX: ...\filename.onnx + if '[TRT]' in line and 'Building engine from ONNX:' in line: + idx = line.find('Building engine from ONNX:') + if idx != -1: + path = line[idx + len('Building engine from ONNX:'):].strip().rstrip('\r\n') + name = os.path.basename(path) + if name: + return f'Building: {name}' + # TRT: Loading ONNX file from path ...\filename.onnx + if '[TRT]' in line and 'Loading ONNX file from path' in line: + idx = line.find('Loading ONNX file from path') + if idx != -1: + path = line[idx + len('Loading ONNX file from path'):].strip().strip('.').strip().rstrip('\r\n') + name = os.path.basename(path) + if name: + return f'Loading: {name}' + # ORT: Loading ONNX model from path ...\filename.onnx + if '[ORT]' in line and 'Loading ONNX model from path' in line: + idx = line.find('Loading ONNX model from path') + if idx != -1: + path = line[idx + len('Loading ONNX model from path'):].strip().strip('.').strip().rstrip('\r\n') + name = os.path.basename(path) + if name: + return f'Loading: {name}' + # ORT: Completed loading session: xxx.onnx + if '[ORT]' in line and 'Completed loading session:' in line: + idx = line.find('Completed loading session:') + if idx != -1: + name = line[idx + len('Completed loading session:'):].strip().rstrip('\r\n') + if name: + return f'Loaded: {name}' + return None + + +def _on_main_log_line(panel, line): + """在子线程中调用:若该行是重要日志,则用 wx.CallAfter 更新 panel 的状态框。""" + display = _important_log_line(line) + if display is not None: + wx.CallAfter(panel.statusCtrl.SetValue, display) + + +def _read_pipe_to_stream(pipe, dest_stream, out_lines=None, on_line_callback=None): + """从 pipe 读行,写回 dest_stream,可选追加到 out_lines,并对每行调用 on_line_callback(line)。""" + if pipe is None: + return + try: + for raw in iter(pipe.readline, b''): + try: + text = raw.decode('utf-8', errors='replace') + except Exception: + text = raw.decode('gbk', errors='replace') + if out_lines is not None: + out_lines.append(text) + if on_line_callback is not None: + on_line_callback(text) + dest_stream.write(text) + dest_stream.flush() + except Exception: + pass + finally: + try: + pipe.close() + except Exception: + pass + + class LauncherPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) self.number_of_buttons = 0 self.frame = parent self.optionDict = {} + self.main_output_lines = [] # main 的 stdout 副本 + self.main_stderr_lines = [] # main 的 stderr 副本 self.mainSizer = wx.BoxSizer(wx.VERTICAL) controlSizer = wx.BoxSizer(wx.HORIZONTAL) self.widgetSizer = wx.BoxSizer(wx.VERTICAL) @@ -272,7 +350,16 @@ class LauncherPanel(wx.Panel): f = f.MakeLarger() stVtuber.SetFont(f) controlSizer.Add(stEasy, 0, wx.ALL | wx.CENTER, 0) - controlSizer.Add(stVtuber, 1, wx.RIGHT | wx.CENTER, 30) + controlSizer.Add(stVtuber, 0, wx.RIGHT | wx.CENTER, 30) + + self.statusCtrl = wx.TextCtrl( + self, wx.ID_ANY, '', + style=wx.TE_READONLY | wx.BORDER_NONE | wx.TE_RIGHT, + ) + self.statusCtrl.SetHint('当前操作') + f = self.statusCtrl.GetFont() + self.statusCtrl.SetFont(f.Smaller()) + controlSizer.Add(self.statusCtrl, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT, 8) self.btnLaunch = wx.Button(self, label="Save && Launch") self.btnLaunch.Bind(wx.EVT_BUTTON, self.OnLaunch) @@ -522,6 +609,7 @@ class LauncherPanel(wx.Panel): subprocess.run(['taskkill', '/F', '/PID', str(p.pid), '/T'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) p = None + self.statusCtrl.Clear() self.btnLaunch.SetLabelText("Save & Launch") else: run_args = [sys.executable, '-m', 'src.main'] @@ -652,7 +740,25 @@ class LauncherPanel(wx.Panel): run_args.append(str(beta_mapper(args['beta']))) print('Launched: ' + ' '.join(run_args)) - p = subprocess.Popen(run_args) + self.main_output_lines.clear() + self.main_stderr_lines.clear() + self.statusCtrl.SetValue('Launched') + on_line = lambda line: _on_main_log_line(self, line) + p = subprocess.Popen( + run_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + threading.Thread( + target=_read_pipe_to_stream, + args=(p.stdout, sys.stdout, self.main_output_lines, on_line), + daemon=True, + ).start() + threading.Thread( + target=_read_pipe_to_stream, + args=(p.stderr, sys.stderr, self.main_stderr_lines, on_line), + daemon=True, + ).start() self.btnLaunch.SetLabelText('Stop') From 9834767397e311c944ada185d92ea501e3f31a27 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 03:48:05 +0800 Subject: [PATCH 41/46] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=99=A8=E8=AE=A9=E4=BB=96=E7=9B=B4=E6=8E=A5=E5=85=B3?= =?UTF-8?q?=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ezvtuber-rt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index c6f3fd6..b84350f 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit c6f3fd6f64212a04cfb38ca258296999f1355e2a +Subproject commit b84350f5cc2087b184d001f9263d288c61dbc4c3 From b0d346a115873e5c16974d7f741e91e92267ba3e Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 05:04:02 +0800 Subject: [PATCH 42/46] feat: improve frame timing and dynamic scheduling in main processing loop --- ezvtuber-rt | 2 +- src/main.py | 45 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/ezvtuber-rt b/ezvtuber-rt index b84350f..07c3029 160000 --- a/ezvtuber-rt +++ b/ezvtuber-rt @@ -1 +1 @@ -Subproject commit b84350f5cc2087b184d001f9263d288c61dbc4c3 +Subproject commit 07c302921e67b483b023a7db5c0fa96ac5fd8e6d diff --git a/src/main.py b/src/main.py index 9d633f2..f75b365 100644 --- a/src/main.py +++ b/src/main.py @@ -96,14 +96,35 @@ def main(): print("Using OpenCV windows for output display.") pipeline_fps = FPS() - + last_frame_time = None # 上一帧输出时间,用于打印帧时间差 + last_batch_start_time = None # 上一批就绪时间,用于周期估计 + n_frames = args.interpolation_scale + min_period = n_frames * interval if interval > 0 else n_frames / 60.0 # 60fps 下本批最少占用时间 + default_period = 1.0 / 15.0 # 约 15fps 推理时的周期,首包无历史时使用 + print("Interval set to {:.3f} seconds".format(interval)) while True: infer_process.finish_event.wait() infer_process.finish_event.clear() - for i in range(args.interpolation_scale): + for i in range(n_frames): ret_batch_shm_channels[i].acquire() - for i in range(args.interpolation_scale): + + # 动态周期:本批就绪与上一批就绪的时间间隔,用于本批内均匀排期 + batch_start_time = time.perf_counter() + if last_batch_start_time is not None: + observed_period = batch_start_time - last_batch_start_time + period = max(min_period, min(observed_period, 1.0)) + else: + period = max(min_period, default_period) + last_batch_start_time = batch_start_time + + for i in range(n_frames): + # 均匀排期 + frame_rate_limit:取两者中较晚的时间发送 + target_send_time = batch_start_time + i * (period / n_frames) + if interval > 0: + target_send_time = max(target_send_time, last_time) + wait_until(target_send_time) + if args.output_virtual_cam: virtual_cam.send(np_ret_shms[i]) elif args.output_spout2: @@ -111,13 +132,15 @@ def main(): else: cv2.imshow("EasyVtuber Debug Frame", np_ret_shms[i]) cv2.waitKey(1) - # 限速:若 last_time 落后于当前时间则对齐到 now,避免开局成坨送帧 - - wait_until(last_time + interval) - last_time += interval - now = time.perf_counter() - if last_time < now: - last_time = now + now_send = time.perf_counter() + if last_frame_time is not None: + # print("帧时间差: {:.1f} ms, period: {:.1f} ms, i: {}".format((now_send - last_frame_time) * 1000, period * 1000, i)) + last_frame_time = now_send + # 限速:下一帧最早在 last_time + interval,若已落后于当前时间则对齐到 now + if interval > 0: + last_time += interval + if last_time < now_send: + last_time = now_send ret_batch_shm_channels[i].release() output_pipeline_fps_val = pipeline_fps() * args.interpolation_scale infer_process.output_pipeline_fps.value = output_pipeline_fps_val @@ -128,7 +151,7 @@ def main(): infer_process.cache_hit_ratio.value * 100, infer_process.gpu_cache_hit_ratio.value * 100, output_pipeline_fps_val - ), end ='\r', flush=True) + ), end='\r', flush=True) if __name__ == "__main__": main() \ No newline at end of file From 54d59846d808092b2e6f774bcd7727a6be9710b4 Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Mon, 9 Feb 2026 05:19:26 +0800 Subject: [PATCH 43/46] fix: update submodule URL for ezvtuber-rt to use SSH instead of HTTPS --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index e1a865b..2abdf60 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ezvtuber-rt"] path = ezvtuber-rt - url = https://github.com/nick-haoran/ezvtuber-rt.git + url = git@github.com:yuyuyzl/ezvtuber-rt.git branch = tha4 \ No newline at end of file From 34296a4978e85b6715e0c6d9b9095e2db263de6b Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Wed, 11 Feb 2026 08:45:15 +0800 Subject: [PATCH 44/46] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=99=A8=E8=AE=A9=E4=BB=96=E7=9B=B4=E6=8E=A5=E5=85=B3?= =?UTF-8?q?=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/main.py b/src/main.py index f75b365..2545b5e 100644 --- a/src/main.py +++ b/src/main.py @@ -38,7 +38,8 @@ def main(): print("Character Image Loaded:", args.character) - pose_position_shm = shared_memory.SharedMemory(create=True, size=(45 + 4) * 4) # 45 floats for pose, 4 floats for position + pose_position_shm = shared_memory.SharedMemory(create=True, + size=(45 + 4) * 4) # 45 floats for pose, 4 floats for position input_process = None if args.cam_input: @@ -73,25 +74,26 @@ def main(): ] np_ret_shms = [ np.ndarray((args.model_output_size, cam_width_scale * args.model_output_size, ret_channels), dtype=np.uint8, - buffer=infer_process.ret_shared_mem.buf[i * cam_width_scale * args.model_output_size * args.model_output_size * ret_channels: - (i + 1) * cam_width_scale * args.model_output_size * args.model_output_size * ret_channels]) + buffer=infer_process.ret_shared_mem.buf[ + i * cam_width_scale * args.model_output_size * args.model_output_size * ret_channels: + (i + 1) * cam_width_scale * args.model_output_size * args.model_output_size * ret_channels]) for i in range(args.interpolation_scale) ] - last_time : float = time.perf_counter() - interval : float = 1.0 / args.frame_rate_limit if args.frame_rate_limit > 0 else 0.0 - + last_time: float = time.perf_counter() + interval: float = 1.0 / args.frame_rate_limit if args.frame_rate_limit > 0 else 0.0 + if args.output_virtual_cam: virtual_cam = pyvirtualcam.Camera(width=cam_width_scale * args.model_output_size, - height=args.model_output_size, - fps=args.frame_rate_limit, - backend='obs', - fmt=pyvirtualcam.PixelFormat.RGB) + height=args.model_output_size, + fps=args.frame_rate_limit, + backend='obs', + fmt=pyvirtualcam.PixelFormat.RGB) print(f'Using virtual camera: {virtual_cam.device}') elif args.output_spout2: from PySpout import SpoutSender spout_sender = SpoutSender("EasyVtuber", cam_width_scale * args.model_output_size, - args.model_output_size, GL_RGBA) + args.model_output_size, GL_RGBA) else: print("Using OpenCV windows for output display.") @@ -133,8 +135,6 @@ def main(): cv2.imshow("EasyVtuber Debug Frame", np_ret_shms[i]) cv2.waitKey(1) now_send = time.perf_counter() - if last_frame_time is not None: - # print("帧时间差: {:.1f} ms, period: {:.1f} ms, i: {}".format((now_send - last_frame_time) * 1000, period * 1000, i)) last_frame_time = now_send # 限速:下一帧最早在 last_time + interval,若已落后于当前时间则对齐到 now if interval > 0: @@ -144,14 +144,16 @@ def main(): ret_batch_shm_channels[i].release() output_pipeline_fps_val = pipeline_fps() * args.interpolation_scale infer_process.output_pipeline_fps.value = output_pipeline_fps_val - print("Infer Process FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, Output Pipeline FPS {:.5f}".format( - infer_process.pipeline_fps_number.value, - input_fps.value, - infer_process.average_model_interval.value * 1000, - infer_process.cache_hit_ratio.value * 100, - infer_process.gpu_cache_hit_ratio.value * 100, - output_pipeline_fps_val - ), end='\r', flush=True) + print( + "Infer Process FPS: {:.2f}, Input FPS: {:.2f}, Model Avg Interval: {:.2f} ms, Cache Hit Ratio: {:.2f}%, GPU Cache Hit Ratio: {:.2f}%, Output Pipeline FPS {:.5f}".format( + infer_process.pipeline_fps_number.value, + input_fps.value, + infer_process.average_model_interval.value * 1000, + infer_process.cache_hit_ratio.value * 100, + infer_process.gpu_cache_hit_ratio.value * 100, + output_pipeline_fps_val + ), end='\r', flush=True) + if __name__ == "__main__": - main() \ No newline at end of file + main() From d79293687ab17bb7b4397bcbe87212c01340e59f Mon Sep 17 00:00:00 2001 From: yuyuyzl Date: Wed, 11 Feb 2026 11:24:39 +0800 Subject: [PATCH 45/46] chore: delete /character --- character/amelria.png | Bin 198070 -> 0 bytes character/ddolsun.png | Bin 111065 -> 0 bytes character/lostark.png | Bin 121021 -> 0 bytes character/luda.png | Bin 50201 -> 0 bytes character/lupy.png | Bin 72276 -> 0 bytes character/overwatch.png | Bin 181662 -> 0 bytes character/y.png | Bin 169603 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 character/amelria.png delete mode 100644 character/ddolsun.png delete mode 100644 character/lostark.png delete mode 100644 character/luda.png delete mode 100644 character/lupy.png delete mode 100644 character/overwatch.png delete mode 100644 character/y.png diff --git a/character/amelria.png b/character/amelria.png deleted file mode 100644 index e5431e868a26e0010829f24dbdbfdcda7b9acb88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 198070 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

u;9h7ouv$-sqmEdt?d*4ZjzjweGv4uD!nJ6_fN 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�!DxD`qgeGHSgMc+T8~~^#9y9KlcUq2XFml zH+=uYu8mpy2CB$76?B-&IY}EqYndRJvq)#sGdm^P_^%lLVANpE? zEy8oDz-3*ku|bllM%enMwkEqR|d zr^_uO;+y?4avv<@;2|N@&zF$;if6 zL_N90u_iO9_r+USFS)rfN*JQ0awhTQe5K=GBx^|1F3IM7uD<}FKwrPH(;a%?k?^+7 zTlSYCftmRRZpOA4XcUx9I!~3HZ+Xk^i|#D@gJSgcMZhYA=0US;DAieYk<$nD{gM<5~rl+!9y}c#Dlu}Ehs>o7x(1>O)sy)DHSFc_*Ha!hDPNlIt zrRmV7wh3N&J<6PUCbp$BdqquzqNHhX**x)`TM-c5e_Cf_# zdx(HT;D>GHHo?+P5lB@*UUl`=Oah=i4@Cdp-}zhaHLrN3yZ`Qc++Y9kpSpklo4-Sx zoRcj!`M8g8yheclN#Xs>ArVu#GYll>=^7|o>8%;6@{+C+0*x}( zht1ztiH|~D&{*KU&DHqT;q^zN*Ayp z+IIf#zmrjEA-%lrwp(v`mB@PXiq|?8x#%gB>N?Ox5RbuT{%stP^{wjgdpVVdhJZ2% z?aF|($cjhT!OyJw0AFxKfoy|6I!3Z{DBurq!H+;(e~F$Q2iViwe`;)U>NuNDCJO;c8*VzaoCnq(_l29z%1i=1u3fw4 z8{^{>NsWoIiwQi01lD0Q+SN&EBSIBKH6l2+p(DOTrM3Os^Re?>q=NQlid8+z979#q z07R!A+`;xAIAH8`{hnY{5wp}^Um)-)K`@~Z0MX(D(uOyrMr9Y9?5keL<4+IV3adgBL*UK2)}gnbQ^2_ zI03Z8m{mKy(NX5n6_ir+UB!}*L8?=5nVJ}NgN$L_{jJ+TDOz2}nhkE2z<+vij*1y( zGw^yjMqpy*8nP7?8}4n^?A zz4}Ejao3%9vHQ?p|D${J%U|v8``Whj=H}uHHVkNFosD`5nNr87h3qsY)?Sqf`>d5yYFOVpl+sAP$wA+publJN(cd7X_Dxi* zsz{rug8LAnX~VPow|?nY-5>w<@4HWZ@Wbw({_M})3~;y-sv-!8M&3dsHX@>_+LvOV z1wMjEChZ-!q+3m8P0`5AXShC$kZEm*Ztn1X_q$(v)6cj^?|;z!&aeEcyJW{s*Ub8c zxTpgpI8oL!~pN3*E2Gloi`zpz*rMk)B7fuVY)ZBPY>nQ;YHNzeFn%k=NmiCM8p#C2a$$M?n!^Vl-qF~E_5cZsme2^89HTe}64qHp zWiZ5jBm~DO9x`?{n^RB6Jxe=_kv!xSB@Afnb)`{oEQL|n(78x(bPvMQm2z+&)~QK( z$p3%1jvsz093D^L&U&*!WEJ)FDG*HXOV>g&Gt=+ToWMD&T!$Hq|& z)ltgKm5+)q&j?o%QJJM8RiQ{p3J?;Sk@Y* zXAF*cXB~sSPEzsZc&3+Y~1=CyWF0`$6W%Acw{~?lOc(q z8b$2ORKpak6cki}E-%c}FQ%F!$(WmBj$c~!s`@zA+C03R=Lpp%J~IHc{M*0&4%Z%Q za=-hI-*o@yJ%5U2>oVEPG`N~5&zPL_)~WvH z7u=`+<3HVJKJ;Pt_BX!4z44W=a084FHgk?KRhS~0@!rxB&+l_oH*J+tx zn&kC#T}#aASLlDA7@KmJp^E1tArZ2&Kr5w(HitCl@+3fE0;PhSP?%BLBWk6{ZO_Pu zNaIR-;4}ovMJ5Q^Nc0RkwcQXwNh$};AUrR`-;QPok%JAu9u1FWmU9*K4~j7O$=Ybh*B{UO6{rXF=lw14 z^cNr!Z#mxCGw}8CiMfSv7?N8$PYgt_b;ZLMXoDaZPHo({>EtXrmV&yaHfHjzY?zs9 zP}l6OUyNqI2YJ@~(6{c|?H)Nuu(fKE{nGrioym9#&MeR{oPyadMz^XvF^*+#DtXsA zl;cwLdzApFm#%)OdYR_<#YW$W4Cp8dViJ+L{>B$!>@nt=fOQK%=>;mhd+xl;oY+cw z?uK>lq0=W_lCKM@#E^+quuc*b)FTxxy=MIe0?wHA+@;Sw3p_3V@d~b5J~8!e9IE0P zF&61-iMxqo=$F$czv?BgaR-KHkO4zWoYAaEl-@kY8^RH@g3KK5+9U-8G-kFXmQz?> zKnfLm&cplBSSR?4ghx+XP`Rh&?)84v9~WGG>F)F0|Mx%tw|m8PH@I(p=8Nv@pZJ{n z>X*L6?Dv9dJu}BCf^4V;)bGW$j`XxORUQHAoq@^!z&CGopZ)uPbT@zEQ*Hwg`Y*oz zjcyQTdKT zZE=k~XrOc5_TF&)T)NHj5-X!z5i-UV9CdJ^gjPhD3w-9dniA-V%DF;4aQY zgli^d!HH8xKx4YngR2LRk(DJwj82RmJ@}Ho_NFL2y4GAS+tl6FbGkJSBXK$Pvy04Tl!?pt5_vKxgU7ePrbz}VXPFMgpLnOku4RPvho4#oscD?#9Nsoqcy5dS;S-d)cKJCxdQ?5*?7THhWof8|c?KSYn7wNWr3ktji4Yd~{-&o{p9-a}>o z(ZBy^*9(jNiXG><9jn&67S1aH^c^SoWz~BoF;iPan&z5!H(4_!3R-h1qOb-{XaNYJ z@bf!~joaKq2adZ;xQ&s#cGti89J9=wqyH_|KvV`L+$VCoOVe{sKt3Fq7=bhxaT|7Q zVOG7>JON5T;in!s=w^n;T^lW&77{BV72+-g2rBrD42GS^HT z2PKJ$dXgAc5Y^XGC~T*yRs$%@_YaaxW^jXRYHwa#y>`_D+-QAweBRb@k%-s7t^T=;@-RQtiu+pi`e9nCst*B#iCcUqWsqt{CRC zpmsBX;r<+9B+(a&&V(8h$9?{|Nw!L+DIAT=>F`I|n?*n`) zC*gZQ;-IHqe?0G3pdQel_qV|JwgAA*sg9ohlc7iqf_ggaD;Ht<1A$#{$PkvH6Z~}AaC^` zX)_v3FB#Y-;AJs~)gzSzsLWH;`xFw8qi?(fRIG|9Pc?4xo+gVLcIy#{8K0eY0s5US zD3u+0^ilH?FOwwv^s8R$zIM}BI2}fc)GDB2+jZ%sZiK!)lT^%@M_n_0@I|Uw7)gxT zMP1jb0k@wS>;^Vz$(D+N7U7uVO|8b}7AX+$->U2q<9UKzV^nK%BrM_1J~x-DxJ3Ut zo*}^;Y+UjGE@BsILV6;$X`Y=mTDPhfrZx$IEMO(OLRBrOSOHaHG9}5js<*7)1IkY$ z!n3@PbZeCyjZazS{f@W&vRl{F=l%g^`tK3ACsC}+Pt3Z?!m|6FU-}idAA;boyz$NM z=U)4hu8oS8aO8S0q|t|jge1_@D5Qedm!+$!TFiJ1+7`h~Zh2;Z%hz*|8x?^h+LN8v z^`d2gcHEZT7np`@hIYcp(MR37z8=Q82y&3~bRJq8wN`Y!HePTpXi5@so;hpXObm}Q zd(Sl{Pz$*3sr^=z+%&r&UqNEcc=@km%{@ zjGWrD2XtfEZP~nuN?i82vV3KDafumr?q|hfx$YHG#edQ_G7R`v-b`r-nzS+SFs;+*t%vUkJoP zlyIn_QKd_);|15hi1DFQ^oxPAt=~*vo4~1g^ZWu6a8yhQ#-0`+h6RA4vtl{3`T42d znqWAef8H*t#H@Q%EOYhB2?Xa}coAb|8JwyXTo1k5BL@hua?p}+i|N7ZWnovdE5=(A zZtJ<{x)jMlOK%s|SlUfYj#;01BjatmAZmT%rki1wC(Y3BHLSSa%}k?!Wm9%unOd z-QMEb2G+PjbEn%kJjq-#fr3DV-Mk?HQfrF?39COXK^9S&5!9z80J6cF$;iVnYK=z` z;B&H&)rpENFCrntIg#g*1g!R^7S|*>-MJ-q;pXk`ci;YN?kzv@I(PqL`jH|C5V-&S&{}20yGQqII;Oj#tt4m z=H^KrR@Xbno7d}G^guHGhAY#D$x-Y=c-Pr8B zyZ3=TZU@)7!~E=NV~_x7a)GSO41gMVo_yfDS8=a__7u2Y1XlN&pZaM~1$Y!q@#cN^ z-Sdt;dmemsM_YTP2_D2Ui5$i(;x)vRbJO7hiN@s63D?MRuUWs=Gl~HTT88Q04nm=M zG6n4bC75rEokkg)0`*X;N?#H!fW4U$bexn8$r;mWrJ5 zE>+l)V;v;8%2HGPc8&F*pY0Yhk}eHX@gh;eUYy1^Jda^VH$i)3WSmV{Zed7Lvmpsy&GO&zB`a` z8Gj?NwBttU0T6>WQ$(r1W*+sXNb!hME2ik6MlT$MuB+cwSj-T$< z&wSMNuojj45|BGpHNSuI#7QdU4%#U6*a_q@-v7jbN3dHxXfnM?t`QO51qiO4%;*nn z+CVTLG!(#3aF>zFG6}~$_dSR*--T|?*6sWoSzKD(O0ewu%M^t~DTdflgz1VD!CWn# zTd3ySNe(F6`F&8#;pL`Us6a2INmcjc^uqjx&AYxWUfXEA1>*HILuAe6~1UQdPI^zg%vES-DaW&h&*F`%FNqrL*hrL8~e zEAYLo0HbCTjO+|4M3##gW|0=IFPCzCmCBq1w7rKflx=QlKRG!wjfomCED;BI812{K zfsGrspPrjt$_s*2U@#XodeV3z**m$}@0(jB<_e$(LA9JFBg01~k;dGnjxakS@A|6q&?6*3Z#CfwLE1oL(4 z)>%I^MG~;);!E5-0W!sLC3;r7IaJLA@wP;LZgor4?fE8D1i|+7axv4!W_6VtTa+(* z%!S*4r`4pUipvRpC&{OBBZ6X0-yiN4fB~0`J zlCREzLFmwLgR#esj(%@{&h=+MhgP5-(4RvW^G9|nejkEui{l*~9m_LQ<1n4Fm$NZbhLl!7 z@4!^2AoqJ9DwK`)bDN+0j+U;VG_u~V6Jq1?=@vRQGm(RI=bS9PQDE;cG) zWhqv}y|&qMHg6c}5%Z2FFao=KqhOmR7z>uyJd?~jd`OO}Uo4{%>AY*`TqAu7#txZ7RCJoykoU313^kdVas)%gB`uxZzXBvGi=AGQ1B(woW%N zI3O1ncVN#0ZgFhb=Csj$ajndxU;E0Ra9tZWyMt7gV~E$x0#h%{O}YNT4piONyTb?f zyJL?Yck8#G=So!h(;9g*JOG0d$Z;3!K;&fXw98GLcC#a=thycCu+^P&>6NaBv99UG zv^#p_1Tb=gi?maXM%4Si5MdqWqnW|?Nt#s#F$84^v*bx?Km_e^ZYzHKjRMWeHeIOoCm63@hfEF)7|N|(GxH7+&o$>W?a+$ZRf^Y04ziz2R6q4YZ#Z$#e45G53-eUW=#ww4oqTM(tWa zUDAjX%}z|hZcjjdX9__mi^dOWo8)K}tAzlZf^cGn+_h{9i89!{AXSbb$M$gyL6_P?(>H;mtXP1j~yQxU7T51 zEVm`vN)Oz7&t<`gf1!P#>!DlkxHA_DM++6XoKR^d@=s9^LCk4?W}-Cr?w6%a*mlY-UqSyvCN@uDx%gn=gjliTRY< zbNsYhB52QHz#=w$l<5I4MUY#$f(Ml~=UpVoE3kXPoPtmZmb6LuP`};eccqhFby~Bp zB}Hm8<+4!Kuku;etRw^GkxoKT;s+J`A!BmV5wP(!fNO%Yf@Utud^`j^gU6}x;|S1* zI5&z+?AER8F)Be8wzb(!pc;46SH8;ohuyaA^tzc-zx%E`Q9N68qs+j!5D;6ChE4RZ zX4H)d3G~;y)@@)&9licI3Vp!d5w$8fUxECO96aDQ;=H$h^HxZZF$kq;?tAbdH#9we>Q%q=>p!#mnistfaJj{ruN&`pPe^TetRfK9}r+%h6Dx4bt7&;_kfX4)=1zcBJDf)1iG2J;0ni3W1os z1W6e8?BDO&2YN{&5I_=>pX36IRGyD;&3tygseAQ(a73mf@s5R+yMeUq$A(5O*t&V! z$8yB>4WH7+^n$LgYh1p!j0}$ac<&STqsM8M7jQjE_mtDg zt(~164+Ei&95{HG@XA0GfkORF^}+AB;|}`U5!Z=s`k|vo4K`1ry46fCRwGdw4;mQg zL-upVdfao&imRD!&^{aY21ba!^3|KIw<%l7ept}bdsk&DC|VY$>KmJuxAfxqcQfpF zNsC^H);o}<>7|S4)(@o1w6EJqu=tOE_-A+Oz)=^6sJA?qbnPGxKKi>o8`rrP{?u#S z%ij1?xV%-|LyzoppZN4A+|2Zt+jIBrZ1nd50~1Wx>{oBz=C+_}*F~@R!TaxVTX9lr zi6WIcbku$MlOJ*4y7?xnXgBkmB5?f#qi0bPhbv$5O7|15`zf$)1uL}Pt<@m4GZEXR zK&FaY!{O?qt7=zJmeL+$RFZL3fEQYBHh1)>ZSvYoUhQWJ+%gT(Ljm6W(d}#RhSqbG zB$VWX&JR3mS$V~?=0yBFZG#Y_iK@2AlXwe&{tB=@*;@A2WMgP z_AOEiaNqdE7u|`&LvGE$Cf=WZ`@mY)vud4dA(#)%%z=ii!YxgXcM^{YyQ!gB7Y{;m z1O3mX3+@2g{yhUdF5C(d=_tL9w;RF`QnMOlREkjzb>50)-w{|2tv&&1@fWy=D_Y$I`$4|O* z&pXd$KwHG6m>>y@ac!Dq^~)NMk;H649#?Cx9=`drgQ}<{(%jaOf8e2qlU-fCpW+HV zCYWoj8MRE%#^~gJPBjC$^ZT;44<0*l@^XJe_}H4&8~!~U?V5VNY&G@seEKU;59m*S zt?JKQxdOTDBK_}g5~apwkn&vH)#neD3L7VekN?EX0dJi zH^PP*zi{`qj(hLA^^FLDJhFA?u8$l!abk9MZZ4;qPuk`&iE$7_#F*A-k(kGelIc`0 zm+&J~jVNL+spTcNee(`;HXEnnEiqapcD&}ncVqG*4e=#;cC!IbAz*Uhg%<*el-FY8Y zpAk3Bc^4qVN#ZmwqYQkyFCkdH{PnMQZ~PgQ)qsV&+T!l$!M*O^|L*VHhyVGX-A5P` zyZx^F+%31>?zV5)>_$!=Awj?)E>LniHm!4)Ty&wK3Nwsu0T8=kMzDG@b&+>>0Ak$u z?2OHvFQ!vgH8%j~Y93ud!{GC3AXgx+1!F<(7L=>k&xl$U$i44d5U+l160{b`dA6!= zB?FcONaYMHz=FS~zktl;h(_z^wTZI0t!n41(KB ztml@^8wt84cg}^Ex}Mc*kizYTNVnScF-gz>G40g&q+5gj{vz#=zP>@)G6?ug&LgAS zNv^<1;B*oa^@&p%YImK!%Z!#XN#IKE0?3gqr)%0w?_Lte&DIjodJJRGBYppzk_Ogg8tsBrxa(y}D02`D8wCH8DG2qL8K2I^ zn_CaYnp+-SvITqWBN%EZ;^-%176knHOQ6qDD3pDvWOlKqr~jt4>(4z<-;C-j@I)(c z_Ak1=KJ^v&9#^2Ubh2;s;rsqf$2YS2Xi;%Zw3fVD#TVl`7?JxX&lr?5$@?Fd( z2a8KHFB*CHzCT#Ix<7u;-o3-;T>HZR6KPJIT$mca{?LODyk_^#ExiN>xLXl}RHqkG zZZpbn%md+w_H7adgg+pk#bG*ovYFhv;byn@k%!$3^XnI1b}3>mR7X@oGVV|y+RLVU z9#zybu)TD_#pKQs_{DNJ9e6o}N#`9a(_&V^U!>~18>TN_VLWS!Q7g&KPS6*X@?0ww zd6-0C@1BQkZaGB1UZY(J=F1a|E)|(kSM^N!DuTMFg-#DXPvC-NWm@7(XoSnQRbzV1 zBqm`nT(Rx*1gwVkZZ}Ksd=VG7Xr$SMPS!GBSENtfKowk~BK_Mx|4a8)5xA&2&4N>b zo?t2ly8f*yTLiGG{ibJo&iCnSC`?y_v-sVf4Z3>w)jHiIW%Z$l>T|PHH4_~#JMcCO zNt=qerv5pdbutKiJUQ*bmlxE~RL~4Y75X9qLuObk92QGIav!yxrJacJ_a3v({(+KITdyvYX*X36r z;Avcsu{*5=`Gmj1nl3T5AW=3r84Dyvv!FDC1O2Xvku#whBCDlx9!%W{4(F+FtT^$+j4_uW@qc0sp4w-lHk8$sh>A60k^#HAIqWs~*b7m&Hl zf}|uQQ_R$crUQb=*~WV@8j`RO$WTxwq?4Tg#@{>%sd`h!Ovy6a88Vm?OISBWJ$j?mwnOH<8W zcHu?E>CurF;Y8No(b6(;_2riiO$?p%OCmMIZrn=MnV}LTIxT=Dm!Q0xy=V(s;^{17 zKU7#EZz*W@!hGIJAN00czon+RF|8GVg23&vPd#zsgc+(RpesnVNa+l8`K;TwXD_fUdp4EY0M2e2lM?A{3}w40Nq`7u z8qzN^#&iG)+aV-FHGVZfrMh;@7K3SZ9y2ifzxK5+8)2}igJ4f3nt=iDhv1dd7#hbf zV-necLWs8BxfuB}0FidPHvnCGuu9&%AFEaSzF>Ai^imkFR`ehM>OC70ueH@{i>PM7 zyy$Xb#&fLAv_^s?NqG{g?lprvFY6XZPPmINJQq7uq-Z1aPcSVfr_@lOtm;+|r&sJvlpi{?#vjPdMIw{PDp& zmr3C}Q*PbA?~$L2H#OyG7qx&$g@ULumdn0mJ(z#rTh;UC7ayy^{k%Et`iVYMD-di> zJiPPbOFw+u*T4L>V5DJf0{xOmzUaT<(sQ@(J9+4BCr_NX2Ak6d*;}`+-neDqi~sqr zgV(?0#-0)$-;LphZ|3Ib&b$Bi+jdk+RODMX7K_PDz(+vuXl~9xi9eYu=DYepS7r2B{07E_R^9sH6!Cv?APk!88dCk@C!3XYhmtA$GF^N-*uWh1&`ou>+Zu8;l zOUvI}<699b$pV{knjq0cr4D{_0?3a^)L@cG$?m(hzLSF6BP-NJY`C$efDU@Z_K z(BAjpU>i|R>T9x`^=mXx_`dqYkp{O42LHZqopK|OJfsON#_`aW7jchkm*qI4RP$~4 z!_U$qSCSwTmlD;qG->1|zlIrh5y%GDZ#0Brnfd$s9(dU8pdHaVkTB7lpp=HlzGO|R zF}j8EIoCqMkYvIku710$V^NqpJ$jnSgFc9N1YnXXaRAgBSmHH(!N~G_HhpyGc^7<; zCi_q*+P-LMP9@(kJ$n3-^=k(jqXE|_Z~J5S-|b%W%9p#7$B*0inXL!Mq5CkxsV8UW z5XEV9r^kog`fb}_*JJ1d0;G}00>|0b(_31~=H}w9Jr6P;INT6w;=(=l0kGcgs}!4- zlc}E8mX^=bW+_Do=J(un*QQIac){Uk>TkCGI?vq|s6+10-D~?JJF}rfw{6KUO}*mL z2kw2-_U@ML@p1t%E~=$i3&tkCd??nmFf}<7YbX~RgQ$dQWT>^f4ZG7*nKi2h8^VEL zNah?PK$-(RYY5UC+)~b0J~lCZdds=H9*H)^lZPI>@5Y$V*D)a3Rf2UyjCfSW^vDtv z9(i~#d@+2=5gh46Q7wWiScQ#rY;@f1dEg;;{`nW6wASm{hgL;pq@Y%2gO9*=7J*JP z-wO89EpL?6De!xd^7PR|kGgh7#|G%T9zJ{&C$4^GwK4U;*-KUC6s~P$pzj#pYoU*- z3J~1i$WaPtSY+A8uiz|w5c!dd2`)KU{ceH45Lf>d{JKQNR|f8vj(wSazdX#xAYAq1 zc-Dhr-QS|a{)@l(i{|(V9nIj}JTQF*n3{DoVHv~H6_}U~&`h+db&aGc$kzh8`F&Jl zy+?kdt6sJ9RL@zSpf3G#BWx`s^#xIa0u}qf2fiq1Ibj})R7ZI z`!2lnh3{!ID~GaC?H(PJlKJ-q{a2Ul;X zo3H%HcC;VgH>yMKKfc%QS##v8wqA93-MJV4+c{TW`)BvLPPr@3*%X~Ub|@619~>kop9`}aLkf52?RRJ_$&W63 z)5kyd$&L9!`JrtWUiMe>%lU;###16bV*Ui|SwRK*IGjM7ANS*+$K2ixvzK zsS8rw4L982zVel?VE39fH#ZN4F5`yQWc>EAKL7y$07*naRL)lt1CeW2jQ%(p^)7*- zhs08(1FuSdCzWv?SpM^$`n21QyPIayJ9wRm;nNnh)@|A7HtyW+eDrho?%QvD@(Sa1 z)RAs+Sw8Eqv=y8QOzPcgK02>JNziWY8o<_-u{K7$!X1+Z{S*1WC}wNH4_=cdD@s&FEAmn z8bVuZdndEy=<}0Ub+vW6W|`K|XD-1fIDX`?8^%$s1SjGY?S~0kEEy6gC0(sd9JIsi z@9XQMr9vWwG%Hh#p6N|Y90iUw$Lf+8L};f>fl{QPDV#!7Ty)p#!H|`c{A5A8&ntgmb7C%qOR(7~#)1mXou)Q9$gu z?aMd04J3)pT<`nuyT@I9^;O(kG_?ulVb-gU`>VwLyO)Hr4{~4s`t`WKmAOuxq&^be zak=4%`Nf0BNA_KL<12n8lIS?JoTDeP;(_*i2;msSboRaG%B!#X57_PFnN+&+@V@=4 zcI?<$59ZIBo$lFxAoU9U*?*0GY>u#!9cn-Q(ETs}`bR(Vwrek5cP=Ak_;GXtOV61x zg-kCx+7rxiBab>eW}+yA>o&S!%uJ;CwZJ%9lnt9+p*uD*7f#G+oiCJTs)yudEy%0d=6P0;>oqxc z1^E!Nts&0E1Zz}510{=mCwtHU{d>usHl*j_0X)PA802i3r;t#niami6++`PEWIbo~ znPol8jF-z3lyk_{Heo#qOu^_|7Q!6xJfC@a;C)a9WNY;gB}iq)Aj(COI5F%wmZ{09 z%GB)Q95%ZX&CTt@YgeziD;Nuvj_k((A5-T|dUe#F8aq z1Na08Y`KS|bN9LD$f(5~I&jFHd&QOX79jMoCS}I#nqqCB2Ibi>#U**s|+$Z~o2S{e{zu#m`OW8g7R@eAwUEGTO6d z?Q$R%%PnQIfZSCzl_VE5g%kjpRQ%k9A z2fokYmafj*QFt3Bf)HltQ6mp4^)vd6Y@T_fW2uUOoF~R6+-ZzH1UyHYfvySO>*%LX z&rF%im1fg5a%Ho@`hB4CD8#!{$4{6`n?~al+|rQeX6b<|2zMYhGf2OFJ#*uS5ACCp zY(oidpIb#gdG!Mi0J)id){`@-FuQS?7qW zaPiH!Bt>9L0|$2i>1T=dM^BBocFbuupL0IH6SwMK5~6d=PUjf0t1;1aUJV3Cn>trD zO^}OHt!tj%Q+Fe{C{PoG%Uv7n`;6k$q+2Tr^#D@)M>|*fZIzAK<=PCWfF`6aDP40k>{A%kW}NQy-gpAfzQ{utsplNy;zk5A7HZP>iy z|M`QBlO%8%P!HgfQV>)_;(dYKae#`?IgN9w`+X>2<6 zr#xl_>J@tZhWMeZKp}ZLQCwc`$j;C9<+JIH%gKcclS^~!v$@4iU!@RdM+-&+;o|hv zEO>OV0wJwq_4>8ZmhRr^w!t+YA~X*1fi?Ej`7^U~YevV0JJ+sTo!hZx(_K@?kNrGE zq}9=~tWVTO75pk;m6BVc3Osc5h&zQz%PwZm2Z6JrK-?b1I>dA`c<4&-&~`6MUV$o>+0VtfLniE z6}mpEGv?mZxp;Wenj`qj7L~xMkv&{LMGJQHp*z=!G|7sIDY91uKSxrq6s&iay~gfwP|{K;;Q`o+|_Hl zy4uS5Qb$9n2njv3G`*af8J}C24K=qO>RY=N=d{JxMUIoo5*LKa@Yv`inj%SD+rypOcsM*>zHt!aR{Pjbp}qf2EKd zWS?&=XLIfTXyQ2ZYz6edqkA9v)vtW*%a^|FwXZw1Vdt*@O;`44xzvyeg`zV=jVuYl zy2FR||MH^;9_fC=YhLrMy|>@-)-C<*8|BI+GMWNRmWnuxTx$__)Wqzx+sn-GX;|T>l>K5k*anMFLkl1;~2{H zZNjufQCHE5icYlg0`Du&@fdQl7J2y@ZDNhH8*weJaWv+y_uhS%Tg%v2g4Tc_d;Lr% zGz#cOUek=Y$aVyFB*!giS6ClG?y0N{T|9R-{n0BC@bUAS!km83FXI~paJkTl$!$fr zN2Jqum<&+lkmqU6kOC0I5nR#GwvEW)xaSUFAG_`1t2sql7ZBbWP;N8DINHp9QnHIqJko&Y|_x~`@Lqr_sF;~gG8h5H&l>Tm|ehvyb|?6}|_*E9Gr zkct!m@W`h>{qf&iy}EZckq9x(H+^YOXB(V*->OQc5)b<$;#n>qJa!`6*wb^%mh&(8 zCrA+}7=;!h@y?}6G1pMcraN-Ext_0o`E!2~$S$o~-4fmq%%mD4!2AOU*66-Cg4X;X z!8`}Tar*R-8w6PzI(h=B+08D_I9`s{M;Wpo1vi2@Sota$s>OwQ6c={F;p$)_+4#(hSJL(C|>>d6!=LSzkkwdL;En zeFeVf6{rXF?|I4Uzwm>sKxS?{h;vpb))@7vhfTnU;0+yNbM5-(&0l^m74o{S?%t^@ zuD<#YqOtgCevX_-#m6YmU{h=JIKI<~PkrL!zxA5ezIuP|#J=}oVHst9*VMV9K(qzS zI+hVKX(3qOb?04fl8Wkb`kP&-xT!x(N8X&#{^9Tc(f#DBU(XEm8uPN2kwyqlZmeNs zon=E*@7IL|>5x#mQ|T@xB?SZohVD|jyIVxMq)R|%UvuVACIm6bi7_r)MfG? zp0ejBW6k?+o2dv!(PXQo0AwY@wzEwto3wp1`6*-p`gCcre0+9b+7UM3^NTRevA3ktP^wacRTfcJgOMxzjByw%uhlb z79pKDT`3h-T>^V47jcXh!+c}pF^v1Czx$UOf-x(3&Bm~u3ysMYwn^W76=ue_(Q3^X zXw4XAc7TJ)Z=YY~d!%$R@h)!}OEvAEsv)#HuJ!^$smmjkor7&#+KHr^z?{5Aqej2AA>C5VR;B7&k7pL{gnMB{ zjZQMa$G@oLVgmox8&=FseHQM(J9f5Q`Ha-v;2%4PK6tn#Pe7x3wcT7LDzvS*(SfMA^1r5&L=qhL<@lhK<#G@8@j zww9qle1o(iM6l@ea#JQ7aw|;K>GMI#riUJDw~E)fSX<*t4?hR6PAEx4&r+NVqSazU zcTnA^<;*naHaa!4Hn;|w5Qr6IwkZKs~$$at}eYi-4)7KWbfGK`|eVIS2Xdk@Ie&*`Oxl%{91?VZH~qJ#=4x( z1jA!$p6m>%EOtzI0elD=cJHa-wE8aXK92>#Yd9lA%EwVpXd#&-sxL9I#rI2TcH?hNAtJX&UD^z3uD^%{QyKR@l5NW;-bhaV9P~H#C<)L{8x@~@EbgN}ceg|;fAospH zF1m~uvWl%Yv)&&hb}AniPSYO0iF$b)$%^3RO3poF4U|KC?@N5QUi}s^q^K<7KmD$Y zwg6eW2V8YhnP7E*M%ZTrWzJvc--P)OaG31)Wx7kc{j3(5Jq`j+#K&`=DLOltuWt(d zDOw;+ou3g%_ZZ@;*Lxz9@oF72#MjgXUNx43JQLkp`SyLuJxa>Q?3ve>L!#QOE)_r_=)fY=KnCS!3Z^V(g$I z1R59iHHHn9<%Q7VMOJ*X4UEfn98XN97NhWaWG(T17ZYgwI_;7SOgg4@|k%UR1Z|~(8vxzEs>HBNV;)EzmMV{}T ztgLKnYm=;I$0!KLccJ>5IJf0Vp<}bxB0iE={d)Aya>9WK!Bvr5fO~^bW%*>&(*yH zg(QEW&#{)+1LGDgHDbZsSj+pOBB%N6 z(r{^Ddn)~KG~UABP$g)n_rky0F;@|d6$;C-J5R&qbNJHywoeiK%){eHP_fgTyUQg0 z^I%za0(vXyr>ws|Y;To4berA~q-ABKE~&r6i=z6;*EnLx>(5Y_NIx<>EUs~EOXft- zSXbvoKKf@w#^Fp0%2RU^OA(Bqsn0eWv;jVm{#O2wRG2Fp9`v;O`aC$A204GYrUGtf z0qdT-?R~f_zl=cL2-)vq3{LuXO z>rdQ#X{f5liB|f)pGSsXzcbg9Wtbjr0Bp$-=TdQDKycoY8wx!&gn%C#%24(?b^z~| z{5CmIo7_+(Mrt=yfja1ox~enXwr{4BH2G>*Fa3|abeCN!tCv)m^|Z7scZ#D4Xkz4w z*SmX5>PEqlnT?T|C5@5RJBWzRBvq{<>u12w5i{Fhe)FW(bkl6I^;zrDu87byrRXhK zraSwmHOJqQjfy+T>$j_CA8h)TvZ3U9e>^6(l$8~tT$VnA7Qli*)TCj?(W8;}QiZ3x zr=M;OnJ4awSDnXAwHTgc!s_bHPzB(#BJ^iDWN|CvRP17%) zR80bj`{UvL05@|Nmy2RuWA{a}?B=HEhooZ|&$My$Q2tzH!K2;s`hUNRs|K@+vZrmG zkOJzEa+*-|#VVNSG6Ad={V(!gY^-}=Rf7A3&)T!G!rp0TXFumcpbr<`?7DVcEOh{R zGinv|gR^Z?a)!DxJ&my3_umEB%{oMQiLnN7J7m{F3Rah*d;cJ-6Mg z%JFSS%gb+V;TzIvPmEFgD(y|Uu$jNukh_J(t(?90m^utbT`PJcqi^)qbmUw~$s3zD zI%5g*K9P4<-nfCPcDy;N_G(a@$iB7Ea~b>J86-YZxXsTgFW5}@;^S^wbn1SczGxPT z78dX>mvopO@i%w2svJy&@Ew%Qvbtp}EC*c$-{<-F*qiKtn^)g&n1{!rE%Ky@@9P_l z=ESetDVVA9gYujm926}3bmfS6_HgU(rLC%>k6LTY5hWQJ?6_TAPGVCT1_m(Jlq`R9 z%jq^x2cJJe*{YF!3TjN0h?TcWOxB4!mAiEB@Ny}mddGQ18oUlRj=Q^k7|1`sybhXP zAM^OxY;7^RPzCr}S!HKsCy!J*G6g<>qV8@n-x7)rUQ;%c1$hJ$U3dc_?OG6Ni2}q? zB3&%WyMHOEqqU4JKeVQunrcFoc^3i{e2QV>l~rl|oT^tDICId<>HEeK3mbRlH?!n8 z!XCGQB-CPqwZ9+YVr$MGeWwR_edN6aJ8TwrPEp1iW|KXmg z6qJvUPQ$%*)r}->@g&AGGh?)aO-&iz;6DerpJfh|pM>N*2~=DuSA<1Znc(O&>yo)%ofraFSGi`cpLQFEox|x&4iEoL-P< z@uZm63oxdrXi@VP`n4BLYYA4eD6m_wG6qdnnY1k```+otaUB7+h29GDgzZU;{>EXI zc4yT+Ms4mm`qRFnpSX9)PfPwI+8jLAsb|XV?NLi7dDR&!MI0}@6k6JTBF+5%AFg0W zmj5PQaWvg26W%oH--+)ABey*_J0R4w!JDKgX{tl7RZam9d8eP+{6bk-hVQ2*Cm+9Y zE(sdpU}%N!A=D>W2ly#8!p=*zSl4B!3uF?pUda94!&H8scT-c@W)X-!3jW{6iQ3x7 z(blx&L-lum4;|;@^K%L&xphL(P_vUF!|$_nzpB=HU!%WFxKQ)z|-Uu5~z zmb0DoKo)W+X=&U)cO9g6lT3~3UsQBw42kC^Zn|R+5ne|8tv8ZZDBO)>Yv`Wq>(i7ErI}kTx@?PcVS6l!PBt5e~+XweGAW# zDz5e-U`r34-B&cef>$?V!8_;j877TeKR6(PrUeYOgg#aXJ6|48~@C7&fD3t?^sM^MqWERt&gEvhIP(V34$UcOnF*3@_4Bi8QPI)Ekt( z*iGUnxWaoi*_&)aKC{Ly{&CV}xYi<`}qjyN|kkqnLaKG!XoYt{atnn zCGJ@L{BGl0SHGH^CCq(v1J^sd{1P%f7ixcWIs~S4=jjF~U8DrE{!g$OfLFB5xZ9xr zsF5`f)3LJkdY{GdMngP~EJ;RQflK4NFw0-jD3Xx76Vbq3ML)5{PYjO+f@{H*dV`L| z^9O2O6_TE7@9HXFkwIxQ@~BHtoxLO;!k4-;Z3eJ6^W3w38wkbs`cO)oqSh?Rg!x7u zWs^3O@4(FtC@Ov18?K@&#{iRt7d#Jq9XVggZPic~P<=0!Z@J zis~<%8EwV9odHV`I$-_A4GQLe^Z8zb{^rPQWLF9TX;0YuuP-L-rGe+i?ufZrF=Hzz77=H+-n9=iw)jFzQ~u_EQ~O-wss0UwxcX~ z!D9pawI`(1x;tnCDU|z|sR#vTkpE;MWA#|CupY-!mu8m1zW5d;r4HrB2u^X(6??Q6 z#ZLBGZr<{$w?6Y)0-CQf(cKIW=e>+(cLo(jVuV_l6nm9C^ybNP5dd!GNbcHJ%Cv&Gce7p+x8u#+k4kks` zHqSnqGcse>xuwuhfPdyg8wgr?Ajs}df78Qh)h_NVVrRAt|-ZmYL z!=v6#Ms2M(07!su7wUtvm6?&QqJRmbPb?Et)+erZurr#4v&bIr<2Sf0}PNyj5tiUs>*e zWoPEdrhc8PG;T@zL3>I!Fo-(n_%zJ+=H{1MD&M(SQ$MS&%Svye@xDwB zx|;5i;=s}hYqoClUm3AK(A7AKomKr!mxhz%*D^Fr)3vO&6HMjM@f5q9wAY$z9 zlkSW4R~F?2HDC+T6jc#fBm1E34$ND1YN^(8KFR~w*64@&&p+d#g^~#J<2zZYhZL#Y zmixa$F9pM!liVFmBzR|8wMZjW2@7QQ(f=$&H!8{jd}(_`*>$koxWxoMksNwYqz7rm zgcm!-+vG_$ql!Id0XyMCQA&NaqM@d*zDh5^aptKfD<&Qqq^!}6aGPlNT8td?vXS!? zd|nUzAn<|yZkxa59xFZv1ND>@365E>Fmb{c4p^^a#!|E1>2u}4{;DCh)$t|!G}mUC z9R_(!QE0KU2>*zB9*~?8<~$dtj^DU+DEZTccdkR0I$w8hOT_>XCiWc59;d|A{T$9T z&=c9yBWMr1heCq6YL8Yz-=VY!eh|sGJ=)E4P-Z5rbDEc|HJ{p4%rZ1&b%9e!eMKCm zB>4m|;O>F>mh>PSd%;&@JO7JXI{#Ohzc8gAsw_!oVwB@dNlQz)VhKN4@57|^wK}Rx zN=iE1*_o@`i8;vwU%qO0HFammjv>r`U@D6$;%PkTU7&k9q3a`LbFHbV5%wdMp7bQ@ z_(kg1KioR6sz+-rLXrQDeh6Rf&}ydyoqeMACbBzg-2W{haSv8tfKJk^&N}qQy?VMOWHbhFx3*c^ z6owZa(Zql*q_6jZdwuFe0^O5u`E!Kgq5V<-3_eJi`7()DDc@2-9KO_5wvPSr8&ByX z8>jBs>`x_f8)mySdMMa5!xZn`ZywT5^!D@l%L3sZ$ zt-*NQnh%7*q8!I`Pmj*o&TSWU97HZ+Ppx-Hjj7(YKYNNK$#+&((iF04S1qc;D#UsJ zdT5dNC9`~E!r&r1ltetNHa|tH2#ORB#e0j6|5X@1z0cA2j`petO!7Zst)r4@EH{Uo z&7LwYh`?rUeo>2!oTV?UG#$X8sA<~s5)F%G-;`dQ3(jD>ph;5{_tzU4oaqQ66efTl zd4wK3nffJcrA_$~OjtVLVL^@pyyd!Qapf2;&l0Met_Gw9?Di^thTR$6gKs%)hj6&0 zYmJ3cyg0PIvVBY>0uRkvqZlDyESX~Jkup^b27$;jV8nG*xi-!0;gsm^Q*P$QC6i@& z;*-@FaILg(FS$>79}ll5)}2V9Er_;Ne5ZpZd?!-2rFX^dSp#FM)~%^(8C})a#BKQ_ zR&H8oH%TB!>n&;6U5f~8TfQvh-`4ns6qDL{SL4(!#K1I^ZsdPm(k6HB=#*1G^!(If z{(d-MB@Ghr&!1OYj0c)ZE&T(^MvAMrWn)9~Wdyy_MrTrbC2`##WH!~BR$MtGMw2Dz zzKhO{=D_iu%JL1nGCDON!Th$1E%hJoOPLWaOBC#|oVbiM?Dj88uD6fGICdVH#ziqJ zS~0H)NybTbwHJx|v9@Um7K<42#oaxaDn4C&PIKid(3OHzj930JCYlBmPAOCk&QJl) z*!>{vSKOn6BhoSkLM^tnRBMNgK=`mJ3}+D+_VEe5OV{dE@h(iWa{R(Qc;ctIRrgw1 z7Pvsn)Vz{?7Kjl(TYE}DzwZEUGXA+ji#`juZAiNgYZXVqbNRS$ZC+c8&B1G^3WSE(YnG!S`I9ux*6@LP z*h)HoPIHRLWUJ*P2^B*U*07=_C9JA^5p%J@4V+@wdc7$1l63{P^2+i$=;yoQjTA7S zWf3JKDF}uK=m!@UFXzIY;z3GgqjuWrXoS-dnVrOWO~$nS@9aDn*LU$r>|xSzPjJ{A z$iWXZ+G6XiNx7GADQ%b=Vkr^HnaKCenAwqPMTn)MSU4izET6eLi69`7xc~}HVTaB6 zOZgkx4*PH6Vo%^XMCfc6qRGts4O{CGYrhzNSfy+2BMcAoI4EDjHR=*vcMCg6^FU7; zg$uo!J<>A`#tsTcy_;=*ylZ~^_m_kFLxa;|Q)6#w@0+?mL#-MWOYToRw;v#F2Uqp~ z%?S^ch7a{YN*pilKD>w(`-94#F;d6ZOf3hl=@<7J8c2ETPeAkD<790iz~0~A-^tgt z8k+>_)0!BkK~sE3jK!Rl^>X4{Z6Q>MC@!Mt^Sw#xZUvE^xUekxjAqxX=1TCSEF>af9O++4g2knV0nt7AWRH$fI{z@Vg z)X1taD`8*2O?S>fI2t5U6oncjXa*!*{{4TYQaHOQP1%fxxoD3G%(E-BiGE{s% zz2e`btQ1%&*xcyN5Ngz~o{yW}?6{@Wbw@k+$k*o^T?q&Qx9GQ}6ZK=uKR9`zw%q!3 zqWbnEsQabMDN|mE>-LJgKO#ilk_aRaDX$iy0W3w=DYpZxoA0ikt_cpqpKcgb{*X>= zs529Dcky9)^5pqCMXG4-Y zH~8FKS1Sp`kK1vhqM~k_{rpj%A+Oh|mJX*UOW?y&5O-TqWlfDhN9pKy_b*pGQWj2l zSq;LD=Im-Wni_3KnT22??fblph=yNS= zE2OoD*F9-j2svM@KTh(r;Oj|jsKw!}$1HIcYm#-{iKl#~>IOZ>i+Q8xS2=u+u|4I; zST{)TE=cA^9`&8z+uGT?y564P!|H&hap3Zzi%posDk-3o^aos|9>S^Yk9Urg9jg~} z2VB8{hRYz*U1)(*wb7^W>6B<(%bW873RugnwD_4dwc(rPim5nXTDgNMKK z*gz~#+nSk|ogIc{Cf@K5@lQy%Rz1AGoO5Oc4B?hW#&wr%E_CH>7nbuj7Z-n?Pj3$B z8tSoXW5XUflD>8V&6hW^XJ=H2*fcLN`tYGO_7_<~2eW`EsPB--!pCUT9H5Q>(-)U7 zPzCy$6%_L!pvFh4?7EzO!7wyP67Ah)PF)ubjQ%8qk z1dI)l%z4{;#QkQd=~f(s*8W9nA5=giJNcZ*$K)jZ(upB4zr4>xNN++fLj-Ko9srF# z_|&}{$f<|JmB8++Qye{l9jJY22kHSKpE)1@mFQEvCmUyN7p z?Wd)R+%`v+_gSZleDK?R64F+RqjI zq+)u%23;>M7J=Q2I4_MT%#+5;Be79k3Yo(PVZwlF+F`WZADQHNUtG>Z9`O3b^4&(lt z{bn=ujf>m!b~&9AoO(osI%v7JeHX9;pWsyyJx@0|LVjT5`7_`DYesqCjm!jQHy4_0 z=+frg1ub9?V46ywSrVN)A)1v;5mqtC#7E9le-ZtI>kd6^ja?zdp^#-+Ghd!=dp zfDjAPy!5q^{P!VU#dF|x1OU+_xy$5|H&DVN=S!Uai|g1^gZ8X_KU&6o>1Mvokx^WE z=APN@OyyHpGb=7{-715Xf^3Ar-sMcXz{ZDeyM3{zn}pz_9Ksh|I0KQR&YAxGm}?dg znJ~5@W<>D{F6IEHve;TsGL)T%HN&PMxHmpm{AQ&i9P_P+Bl5icIjCKn)Yc(qI_@8? z{9fWiLO`GIBg~w}E%?!B@99*ahXWQ$58X9={FlFs6A<)|=V@{tOs`S%R^){~lP%7D zPN6eW!wMDyLHG1WnL6S}LqqorjF>w0!R^9HRK zufNvm$oxx5cfK{UAt&L4&Q1hLT{I|aKT4lnK<|njhni1e)O&8dLM7>A!qzFoXvhP+ z`4&0_z{dR8z8~=F?`no`oH)EWVL6&`FptMM>T7|BiFG?-rR-M!0uEmK>GHtFswv+6r0?GliM-aRizHOL< zW322;oR@?L9%bhcHQAv+*epg}^jeaW8#&4}fXc=^1ujW@MgcO$-_>6thWEN~8?hT3 z%Uz!(+}T&OA9fW|VjC9u?B1nDTWbO!_R*2lZ&4@+P)j_}`yIoqlRqazHEJ76*oFcT zOG|w1gE_c^9_u}1Mv2eDgFmg`lj(hrtss6WqC(&Os*S-GSQtu0SRiz%xkWCXkE$-C6r0E%qRY0NP=5oyV+3 z!lhl#;llBqicMK21Bgw^FJXH-BW`MGj?=m8Yd=NGHVC373Hj5BO(^v+C54a8{#(W! zRMq1HCR#rKpcIvbUmnzh3Jm-Gk#uI0CwOjXfol0$ffs>}SeRHn)`!YT10BQ8(VP4M zO$rF@2QaTt^$(q?^+{W@X~U2&Jow&}kU9Bi|37DB+dUlZ+E0}5JD~;>lIqQRbRylo z_bPV3S<5azrmZ@;NbVI9i3MVX)rB|_EihJJA(xbV;4c3vra^^tb|imr5KTMOoD8Cd zs>nj@rI{lR0pLgz)te)tQMVhGpi*GErIvJAWe69H?H`4D2!b@j+|Dm-?o1fCuOD5M z4OrI?b9!0CkHT)(l0GxT^o*K*wr9tZVR9p}gdk=N`)H;c-3ekKL{!&G7{TmjPf(Z88rRGD= zHhjEhl>UV!m5CqHzfV{qh4y?ozG#HZ7>T*K#K+Of61HA8O5LPLRcD?V1TX#C)xkpC zuA6u|^*L-6|Cr!)khDAd7hubm`sv+;Gp=ohNsb%Q?e0$PGRu1;wgcKrcFBua+;YM1 zFAggvW#5Ki&=>ABvoCgas`U@8 z%p%YE#E#dwCGj(3>(kKgf@6@;R>k^DqK>1*WX{0lWC|6>X&ZWmFQ?B4W-RXr^<9kL zk{?YN%ya?G#6F(+(@b?Jp=P5wY?`R#i$`VV#O%KB1uTw7y0yqkMI`)?Kw@7nv`dKf z56*kc#uKi#V;vQov*Ge+eX?vLUhYUFqjGom*D72z#c<*18QN1CCVOtcU{mmIS@4PA z7F-Hg(=vPiCGb6)V!w z2AxmWlaZ3O26X((phG^}qP43UOVs{=oC~Fn|2we~z#=Zle1IgssA+5;S1_e~R^9A& z4%(jl+wPiB9jp|>w&}{a^gt}^*R!$007n4=xa=Rjz-0F+iLa}~CVjrAF;O?6l;8>xrkvEu!7Ot1*f z-SVkwl6fR$I9TqrAv+oV?hK>dGIyVZ59+vY5P$yyS<+I|veKTEZhNM^2j_lg=Tmqm zy!zav!rd}W9|U3+^W@t|Lm5x?9B5?GW#IM2aX}+b$I0fSik#A;l5+KBkwhbi`xaR) zn$&#uYYXkPNM;#OcmJax$i|_Pa@^4uh#i)%6UrTf{%)@%cBuCQ(Hr+2=E|2H+t1e> zs7=X4!*;6Ry3X;Iu-2f%;ugXXc;3ISVbdS{n->?)h|dU!wrMkp zYHPRo)3eN$Nb=`uJU9^!dOFksOWTT(+|)?DGdlE;Q7mpE>a|wR`2aX6`9(qS!MV2v}sR9tB$N1R<3~oPIVjz|jm;&yj<)glx z|G@F<55Ja%z$;3_<}je+0spgG9EQK7n}#pK<%OsEXEMaC*vZ1FlLh%~OF*Ob6CubP^A^n)!lD!qiV6(pI>B$dSzbN?%O=n^uEjnN6VXALAu~afAvPxw125br98{EEECDs2eI5O8(U~!6*?Ji zlg}G7yXpqP25eMs9A&I}4z_!|c&>FgmiiVdE3ytst3DL$|;EZWW1}+;UO|l~B5)picc=I)HJrlc0PfkpQ6-1l!%48MdRSR6! zLGw7O9p0B~+C+P%H!@$mBI1=Fqxc?Z;m`qn*@)|f;6wCQpyAI`rL27OLK+ZgI)q!& z-vMNXaWrQ3HfOmChyuvV`DdEhX4qVb*S>hFR*sbG`oH0LWe5+bWUtfczWda+m>sQ4 zudDf6h<`HocD9o?LRIe{dwNUj<@t6C^b36WYa(%rs!T`2$4CS7-<+CmDJhm4FDkhw zW;^y|HSmN^+*$0yx&fCuiWkh`veK?(MCt zr~pa-?X*=W$&P2ju4rcf7nv9|Dp`f?_IS1N?y&jlWs26+6L#N*Swl(Jkoev+*c3p( zp#d3hi|zqRUF<;Gbz1|psCf77xnakhi0CT9G_pEjIJph0mL(JTHI|WsYt74&K3fSy z8bMD=na_tkT?k%3M;?e4RB#l1bqK8%%=A6KyhJT4^^O&xzhSoB{u@%^@Jic5K$D*%3l4|8?Rft`@A6OOVU@uZo*Eym)XN};#eX-q zc0FS$Oc)wst@E{7JNAm&qaK^S+sT|CmhpaNYh;x>g-by+a%1Vs$-!o=#D6aXWq;_FQvRgQC520 zeYItugI!TYfeIu0-)!1EK!tZJfVvA@Ki32HnRP74cWL=ZOb$$5pYFO6==h?{>d45b z7zSdtuygx+u;A|9M)JmH{zK3MB3Ph}*IRIUQismf|A$1uWATwuXkQxD12OaL>N5oa+BPEeMFqEGLGXyo9YN zzkH$hUdcrZ4smTX_$3tdq7N=Dw3k93AOEGp)JibSJCU|7OjpGa^U!W8M{$;>lcf?3 z1>928N4>9oZSU5hckqz}*dGJc?$)X*7&!xm&Li}@+TA~N}xcuYauZYHHES6+ULNIv_Cwx?M6wAG;vcELE^^at>ycgHn-)#EBwcm;Ft?tpVrD}H;~ja z_&Q-F;Aw5_(HpaBb;xjjUWZLd>1VMHJnD}c8VPO@^n}-Omke@v2=E`aXI83Mcv zi>LkCV>Yc@!ZTVryblRf0L?EAt?T7R&e;c!BC)jNu(?)~MzTdHERo?V5`1J5Ki>dA zllvc_A%U0b1MWN^^b6z|e9uN!1Web_;~@9IO&Zs1?x0;(#(Dtnez}4s{;ds}E%Zr` zY1;R}7hRPmyLU>^oy(k}8U|ONxz1{B<3HA`el>+ejWrp0Yu_iy|AYx7x2`8wtmr;a8Cy6+LtNYI~zptG;n?(J@SxKC**9XokF zJb+}_8@SN|E_wm~kb$Q@wF+MyBjk=OSvoaA5pa`n=%k&!AUOR`u6GCeD~P{HG$W0{ z)7RcnyQ{b*b*z|MBn~*1Io}77ViYO~!-Q4>@Ms8wHln+-NI9V}pxlkSz+;@_$mXdhmZ9i5&>wAyeHF zJ$`>af40$mYY5W12}BH%8^RL?3UwmC*e%-h$T9WW2K0v=q%8j+jV5_dr(5aI=|nJd zt^kKAxP}h0kJ%qeRndJXo4M`owhRl6mv5O8z>*fawv$Eh2~lo^J0kTBLjVenVr3jlr#(1Og;9-cj>Qz&aInJI4;jg zqukRHyJ?`FRO@Zau20sIF4_m|>R+h2z4aBE0JlOuS)Ph=sKa}HJE5Vj#Wx#h+1ng> z1e8Q8v{*HwBv2E=2>qNq`El!b3z|;y7ZHAsGZlYKhO)lLJst!af0nk3ZTlPTl$j`E zYsQw)k{%Y^?r5d`1yH}QwY9m)>0;+toG9R_8&gBrJu8n;xGHEM^TP-1doQ>~S86d~ z=ZU^#BtselWhYz#QhB>$oy4!2cR<~0i%hI&zxhN`ZOH|uDPzl>_q{)}U*8|lI36JK z#a+XQLjM0}Q;1T7v>l=&U}9+oKS6W{M9VPa@m}zof{LSCAJN6w2o469JB75x*3-(d zsw(t0WZxjuRDOzXrEfxwOY=i)0Rs`SdWI1OH_WtlLWnhM;ksMY zQuS;JWFLTkZLn3o{$-_KC9`s-Uguy)ESs4V4jT#Lko{Y-a*u6BEcu1bMCm&=m~D24 zEeuXFTRuy&;I2J#yFZc=TYgmCCsCA*D%!E|QxE12+8ICqHCrk#HsX%TZUVv@H_)fQ ze1VT#J>)<}{*WaJDW2BF>`f{p?sp~jU?#Lf2q1ewHmYDhfgE|+{d@7Syy(OVz4={V zIFdnc4S8Z8pdK>?&^4Af&0#nE+MZ?I?eMnzG~@*li-f)II>E6JX$!Zi^TSKJ>yQ|v z&rGGVYMR9J!c7A{d#cu2^TAkT|?H-z{j_ArLwK4Yk>h({`WHE zFI$?vN)auX__v}0Ug;T=WL3lFJXrQ_N1TJE3N&(%qe?+DD@Xx>0kr-saC2DD56MfA z>4V+W3q-Z>gsg5s&6alNk{4%!SVAAojiu$aWzZivh4<$(&DKo18eg6oFf%7b_ehEi zUKbTAixJ=%z8>UzN!)|NMCA9k3P{Su_xHMJV|yR*btdX+qIzD*^x}Pv=f|_KWOxpo zcpgay97LZ35!cr#q*3cyH~TyPQQAoRhm^LX>-{!026U^TbsXLsvacA!913=_31hY& zcr&LQ_K&5h$f{G>KCampe)L*^QP=G?ZM`SkCm`#OuoJ+H&}VKGq;&~ z*?tx*IOFQl0cX3ZeE_3?GK^{|xIe;VOuKCs1_vR07W$+fT@iTMlDM_8rFD+yyGeeh zRkRe!eZ>%x{+?2FRZFS#>b71CYC@>YTswI&%bnhMHMvU_u%X{l~hlW7mN{v-sKD50eD)fmqqyHy1#mH8@R#A+YWt&|^$EiFBm}NMvx{tUUHFz#v|?|1Gl;%7Q`sn*uX6X<-+0^$8gQdlcbj=S?`2NVBW}1G zAflP$7!~mCr|Nhk&<&6QoPFAKe(c3k1AY_w%UYymSk$B4ZYQ&7>A@t*h&^TJ5`5LX z?ARe46AJ82dnfC71`WJtd)b8eAo=o(?$v|AxK=R2GET*MG@I&c{J;ePm=D7XW$p{z z?ForkauOax@{sT82Tf?r)dgcUO&b(f+%gl1*Vt2Q$E&NMYhh8d)4niCN@hRbfN>k- z8E@+PDb#4topf0K-Y_#je*uVY0D-SiH2B4T`Es@4c8~wHkIY!#}<}Z*6dU@b~juT%K&2K#JS^bSk;10PHRMOY4TqsMx)Z@M?jb z@z}{Uu(=n11bF!`8z!v@t`sMTb;s-P|VIo@6<+=*600&jmOZEWO%eRptb<`e{9+bY9bgoCDRAX$ymVGN?0M%L^q1ASO!%5TlTA|v zPMzDRikUZZS!#Rc6__cxBe~3*JEOrsy7uf8?HQ^ANzhs@J~HDv{)w%0Jh)GL=W~D- zD21YKes_7`mCseEHyb(-Pb>o!<&@DsnIoi%m${lKPLSN~?dxj`H$Gx<_(*HlcX$4j zXeYC9%|bdYVI{vzb|uqC6aKpBZl#yXdEvb?+#XMS z$lK>FDcW?L5${vTB9vNfL=MBdNK*9S&ArnY7#RCs@zMMo5215geU}fq?T5>b@H~2{ zYF_IHC&;p)meA|{1sPLsQhZmeeWtv>ZvCpl=i;rKy|~L3KrR^O+m*bnk5fT9$jhQKRN#VybQv~7fADjmT0Qb zKSvkZ5h&dm_f^O#KXWV&x={G!Xbyx~9&#^1>l=$^!Y=HSs|!ciBp zye9sqSE--&wC_mlbA6twPE;zxp<%G@`sX2clnqBE7~p0OR*S!L3U&6&Y1%L_+YjywmHl@`5rwE^{xbvvqE&${-Joi1_rnToo^2r@P3G+p>}jHY;@dvGt5wN?2`$(7O5_Oohk ze9P6bJ0Bxn7OWpHwrHA=gn)w=Gh@k&$xszY12tv~>AD)ZzDDJb-7eGyC?7LL z#jeQQ-Kf5a{6XX>nm>;%2;H-}#y%P>uJTkX1j_B3^KaYq zq$9MQI$}h$tz_jjX72b&OSwN^mWt2eOmm#yww%0#yZ~7D)wkY2GmLzDN*uH1D+$M$ zhGofS7d|(H0}*};`)VdXPc}Y1cahLGg>B$7S8NU5%C**;zpoKCN~&_8&Efjum80ox z`Ss1~o9O|q!|@KqmTF!qMa_G>Us6!|bJYEl0xugT5g@mXKrWyvN@P#Sqwzn1!3&u zYJ+j)sdrg`j-E_dnQ1e^LT@Q>at|i!y;vQ{_L0$h0xVaNN}p3R4Dow@X@@=mcE00G zqv_yslZx06+P@dZ{QzI8Ba`L&o~@GG@0myM1SJ~27?%MAk0Q~rW+Z1MTVUG?QR-Rm z)D;mg5IJn9adECrk9N{qoChuY7ei?7lB3kqnbO;HrFn5b6Pi8kV|1X%`cy0V@_Zvb z4>EeeVqL-tFtGV@PJvtu5*9Gms4ET^CECjQ;ImTjnR%P6va&r;Q#qmh;#xoOsSy4@_V@|{aw9CadJS5`}1P7Xvx6a zTK{#b~4|p20MB< zmpA|=prrj{uKn9@oq()lt>!_hHff9Rb8Haf}|F4Ta52b31IIV5%YZ^0J< zPDDFe{|CW9KEDO)TI<1rmjz#MfgZnBkGX>%pHF?4+q?Ff2A^{bYOko(qIX!a~>6eChhrp2sUt59rV1i~mfWItpCf^d&pBQL2A=W{igoS{sz$ zZK0HMz0zjyc~^UiT?WhBcvdwA8H`+IJlDQiontq(GIIc9BQ_6pjT%>&7pEsaHs(J6 zxi8x0D{|W+L^naJdVjr-KJ*r7z2=7FOt z=bsmsHsZCzc!*Be-OxLrOe&xuvfhfO9_-=n&;cHBDC z$Fw%Szq?iNwSu`4JV7#R*RFLlpdYpNPVGI6oXGp>dAf%*uU?ZZZs0torjUWFzSY6b0>ewaEDua+o0@vjd0CU;jN0S;wu4(uOT}wOS9~ z5(tjOT*!ETZPXbyWCi)3{HfO=B*TbZ_5I4GB!?ggaH|=}oT&&OAJ}U>-C7i?2ESUR zUYk*_ZPaH-W@pJzD_-14c+2UW8(h5(IoJ+NOFCR`W(vrfK*YMK-K(~+0;j>_wW_=N z_FjcuUAt;`>GSG0$n*5VKU1fTW_lu*Ev#bC2}`WQbW+$B7YM!y*u=90*Us*4_kaHU z&u#PEv}uz`kIFFPCz%b_#=49YCsbbB;N>h(G`Ik#rrS|ijg^O}(; zGuj+~4x*ft;z~uyht?X)wbeSC|9boL+3?}LhrV@YtFHD%^-YndYR~r|XzyPhp-t;i zVQm7C<1~jHjKs`w?a{-B5V^^@i?6uGWn}$|t6G$EGv0tlk@NTh>KR*qTj5AwR1ZbZ zP>)w}xF#xYu=$Fs5B**2_bTJg({!=^ZY^O^(D#nne(g*$q2k?pUvUCTyl%*ksR`t_ zFxM@gO+xCMaZB^FxZU-*L~A#JFvRsBQ9|@Z*Vc}|DxTxLkl+L_IM$Y{)&hUBYW>U< z!sEzgwdZ@nKy>I?GbL^Ts%yFENnA#OI+a|q#n64Idr-Y`_4n-nzG(DE z_&0-$XPPN1E@1u3`U;#~fqFo%-wi*|3WS2;(!u@v*I#$d6^Z%91!j$zTBE9H1qPS4 zdX7Hi%U|*`_s(~|(_MbWW!AeCG=Bi1(L3MuJ5)>;xyvuT*v7`R*&8WHRlfDr1;-QM za1c|49;ULNf(hQYdNqL;H8-lrrNtzS_WkbksbSj`r|ETW!dOL{_9PaVne4Jv?FxuN zBtaGMMn^f>|-BuJGbw^6sOsaKaV;1_|Jd01VQ&_$r2MLBS->FE5CXSHEzHY{bAL9X04Z6;>C4YIUZ@_ zYqHW+%gRd@DnY~@SeyEO{R8b*&;4Jn-yqNZYy3k!!_;DOJsWmUYg=c?bk%`W#cURI z{f)1G)0oYl{M4rn(v~|{tWlyb1*_Em@~{4iUU$;%dGJAZ@X(_cI;AidqN1IY4_g$pu72NUw3gTu&LQ8cl-!UcdDx_n&Bdw zNi9yvg_40Rz0uRBMmd^schM!6y1DsDH@7fP0?=Z;a}nb_ViM4+`tCA6sRl43$q8~b zDBcT!HXa8viLlYpY=cE#=G5nEjx{{Qo<70bQlrG9n~1 zR7XDiWC1f{ZMa@jSpSzmkA3*4IZ8(na$fq%Ce^e*^1qH0psqD87Ne6d}T zU@azdwMS|M@7mX@U=8*b$xne(W+PtpCiq*MvBy}h8Gv~GVkH0`bnOxGtg^QLF7%Dlzz}bDV|b=__x0lfMgXO+Uhy%<= zC2>X|?n(r{c1KwyxiX+uiECz{!g=Olv73gzImht|5|AOe$z&G^=<*hCAaHgdXv6XH z+yFMI3V8koPn}I^FB?$XR#>O_>cH147o*p{6Q$eG+ascf8{rZk~U4Lwt(^kqc6vAi($Z_rgNP8H<41+S+OXPRC;v zH&Ac^8P%rd7G|O+TuWP*iOw_=%(a;+Fe&NCLwu5yxyW4-L=mJckrFZDO=VRAIaQ{< ziM&u{o5`3gNvqYpdB0oS_x4{^(0VT&rynwvk{3Nh98C1WnePy-yRGj^%yho}%b6cyB%6^hG z5D&St1^HPqvqURn3ZNtH(j-Oe)^6r{Vh5_0LC8xyRGq8sP$^ZN*&ZLro*-mBKG=OP z_py?X0@p_OiCQp5_G6KHNREE%o&Vka!q30i5|dWO1j8hxLR!j_)nlF2XP4D$9G!v~ z_r?7W?xCHtjuy|X8NIYNG#e_UM5QJNyb?P9t}J~^;#@Y+*3#-uA0Nto_A{TKdSuTd zO)q}Q3u&6?n`|bR4SQr{#C2h6@#dd>BSEO(RT_P2>E@Ens2D+3 z#IOiKTqG_V`<94`o*SpK18Q$>YH@G*`CoKD|JGl0?|=Vaxf@^fVyl>t#Uv;v_-Jy= zjH<-J!$B+=Uli;*P_-e&LI*rtd4E8Ax3~*;zJm zk>f;mQbA<%>r~ zU0UznqY9PVCohDr?D2lbMSonVu*$Idq?JL+do@|gdh^&aVhJ_iTu1T~aB0`s(Tu}d zv)g~*ILF*-*Ehcmk+FgZivn0RF)A?u5oi!>?Tz3)+2c%~xeB+NYx;_w^W%^0@vozM zPeiyh{qt_xI5X3;RNJ#?q_4G>Mtf&B*IoArqm5k4VAZ5XwXukD;%*f9-Nxo7BUm;j znn?(UT!i~muMHE_8^c_&R<+~%t^s|;!FuY)LNGk>oZ4ujjeYW00dfUvw8o;SZ(8*g zc;XeP2lV=#@sulY!wokKPtAOM=)NkABF#_NQLS zSkfsoIXOKt;eP3t-iEjMvO*(xq!^6XN?En1F{|E}O3g|Ozwr*Ijzq6%g|tb_4WAW!B+GN=;R^AqWJ=0GoV~;OoJ|90TXA zYEKMksm}>XNE4_$Nn4-$_AR%#^UpiqH8o;$DlKzW^+ul3&#AoWgX?+h%(MoUt@RKf zNQ?XwiiY{QXWC-5eU?OqH#Vh45%dS5#uFTJq;e|%N{ z7*g3|31%VIJ5O?@vB?zoj*y>z2$;40zY)+lAC0L=dqAU&4MAA?+_%%n2hRi1kB(2c z+wZv3ZQr)Toj5_Vr7E81MIm>Vkmx-Fj|(X=LK|1n+Afdd0DQ+fTVD6r^QryLN|K+1 za)ZGLe<$C`(5&xY^%YpL0`-7iza5@(1=zTPm}~e>ojO$Vo@>eg>s?~WWh>VEZC-vQHn9%vi6S729fz}TxIHJ@A?-7zr|Hbt9XR^Pb_YwPE~ z@tf~-fBZ*(1j{>aif)2+^_*T{e8KZWr$-n$YBfc#haP&!ef!(rcJcd}3#QU-Y@~;8 z;wD)O$T52Qc@)cD_Oh3`Ou_Hkd)E+nFK}P_(oMWZoSy2EYiVUh8F|uhoUt*EIUtoV zM%*GXs%kdo9En7jV-X6mM3NDv_g^G%YP4)0ef&ord4%z-1(J`5yXvY-7}+A3AsT6h zSqY3}YJ(Aw1_F`BxSCs=g{cEe!@#HVp8}?CYXazC4YG?V7ix>qc1UyH8Q0X>=HkT= zQn0fw3`;*wrHHP#OLX-*AJ@Xj^gdu;9IitsFzr^Y-p+Mqt*Fi`ByOeBGKo#Wg-ov< zf|^F$tpBcLhF5-)5cSyOspTl1VXGc8<#{5~*-C<)o~K!JLoi4jz1efWyXxv|%$d*n z{v@f#jvaA-_5Q!Ge)?Cxdb8`KYMCg z&w^&RdzNQWTi5SKimNU8nbEddvQI zDOZ3hxcLRwUwiupKJc$eRc>?hOAwvVXJ;PzGheyMwGm{mz5eCy6QBN)d+RU!ip#(f z?(AyuAXYX=&~o~~Y2a6px%d|UR#;$%;?)Qd^2%lxE>ZP_W$M8OcFh%6xq}A}v+2ti zg^HP=D9BU&c#)~{1VVA=e)Cs<#eMP9pKt--(l8r!VQJaTk4?Ghkx3UPs5FM+ZhUAQ zI5-C*{?oJrhTP_D8{HRf`Vz0#g2P$A``zFFBX{EX2=C*Ecs4 zAP|9MC!-YD&uu+!p4sqNV=KhGSt`;BRol3`>Y5t~0BM(6g4h>rV;?4XC}9b(=?6$` z{8e^>XgmJl3W$tW`I-=%8F0sr zoN!r^iUyubW<@{&5_Y_2B1$E!s!;1H6*_D9+qKG*G!z*nEBlefCDDK<&ySJ1l^?lU zC0vv;taDki*33e@%2o1y9Df7v70{R&m8ZT%TxoO(`p2yf7cdt~l-%mgjV@K{a1HT( zS2l;UBy;l|r?>&-xbqcY=39W3IgDUx=C>QTAvl^!g@u-tsYbQ<@J8m87~M%G=p`y6-hkv$}K zyWJm=IPPLJ@U~m;bRYiEC%EPfHu|_Ijs}ptkdY=(m?o0Ezx%&`;o7Lm2l{%A^rr+; z)8QIlvx;2kjuh#HHi4|z_bfT8jxetHTrq(l@1yoG9RY7t!YGasXOg~rC4hh~{R!qM z>J@r@zkbRUs8{Iq8{{chVD;+NGnlOG+r9g|?7M#Fcih|G_6u%lfjMI!;`7cw<b| z@~{625$=69I~|Dz2>J`m@Up=JNvp>_3lq4tHQ~+b*6`9Qkw`G7sx>j()gctTe$~}i zkOVB7H~7%du-md_i|g+0avkji5EBBa5X&=c`xXO*c@IC-0^&#BQT3i@Cc)>D3xRRAIPT8G2KE!%`xk!k7u+o%J8ysc+i>TLy7&LZ|FL^j$kS3f$z(w%;zP*2^3Xjcv`M}4 zN#J*#kB&_|0udTnd^;XHM_XU-_|Kvup6-D6@Q$lCK_GFVl@F`x)q)aiZ`Xjo32EXt zG{mq8_RJCLkNOHc$qLj1di~aT$`xQ^Dol(`wBLQteW91W^koDZ=QeKMgkeO?g4u1i z-{#))Cx47icPE=BdmGh?;P6wYh6q@FHr})ZjI6%0l7KMm=(5zf%<|K#NTmI(%};&n z5D@N~HLKnJg9qI1^UiTsUvq`~%2&SPzH;*y-HTszqiwE|4He1legE@MtY@87j1ox=Sy< z#BJNWg@23qC3ZVZd;;cDl0GpB%jr`iyl%;zM|F48O*gx(TeccgUyq6LargDFeFJ#b zXM6$)=xp7#)otFn(*l!{j+(nyb91W+orqtsj%fMe5C5B6ySm5i+PTd&A&r_}rYd#> zpMY8j6_OG9_?aZ4I_q6>a@q~{_Pg8?FnkOC?S2JT+65?sij|Ly4slKLrRvd!F+r;a zd4r!lF&m<@B`P+rZb49B{n_ARfrRG^B{9<(&B{wXkIJ+{-`G#Z`^cULS!b*R{9b<7Ty4LXZs_dzu162h)sR!Eg6e#(g z;Cmxes*_Wl4p2toH4VJ6J#Vz$OIZ9s<(c%7Yb4prlYmGqP7<&Ig8w<^>~Qb+Z@=!| z^0Pl@Rr5WJ>vc&h>xn;6L2oWsEKdX41rk`dmd zBBwsN0_zBsx_afe@^SUmSGyhCw!0Ve-cdGRZN&L}&Z;I==?VbSV%KJ<>Nw4~*S2lj z+{ZutDc1qhS;!^dqIY_dTzPn zp$b;|_r-k5eeG*sAR!rcZ+XjG%rE@opZb!?)HVXYZ{NIuHJ3GQ*3FKNxq($ZZqMG^ zTw8aG>mFFcjQv`dSz^{&Qn!^Pugl1tB3nT@yOi1p@*1bJC|`wvtCg-7Wfjm@sGad^ z>Q@Ii4ypC&SWB872oPL@B?NEAED2NM5(-8+-+q^&8jcdkOGVNO&7G6XiNWvVeWk<~ z)b*v(4I8PC*W;QQ5}*)gv-BQBZ9Q*Qsl2{|s48cT6zX?1BB%*2p%&hpzetCQ;9_Oy z)VwWCmAk3A37!utk-$X_Z37`Ex+l4iF0}yffq!2HGJ~U;I|>g#+69=XLU_!&C|~Qk z%2HLTefmB9`_rHKG%c1Zc$YF9j0J*wo1A{mj7VB)Q4R1q784Juei)d(qI!SqNS>&aPyY?>w8pv1)gjL>J@tZ-gwd#AV9Xi``z#UZ?E~u z*KY{{^Q!6{rwWy<>c>9(5rkgWx|hE6#RT{?)eNIlRIaK(o0<}A#8eoX9|vwBvxL81bH5TM`7OhuaHKef0TYB4qLB(Q!P5Z0dTYm1%?hBv) zqI=)_-e>A=8Xc+ybN${*z3eo}fSAbRkjUQkyT3!=?nW_=J~k4o5};G%6Qr`#jCfkX zioW)S4G4yC+|$e~Pfty_iP>?~+gjYV9lK!857=z7&a1t>6Ak&zh7dGT^@U;9Cm_k? z;1Fn?=H?ec8zAaY2}cMrQYR}SRwG)MH1BJE?Ls^d^|WB|idViC!ruw^sZW27$$>Y~ zTW>{gejSNI8YR4MlW^o*UstEQc=s;1Fgt9?OG`6QKd+hO9GjZ^EI~5K)+$5XqpM5F zj+Tacw$-cc1wI8&IoJte_o`ZhhOMvbO(W>Iij1TII(TDzBq_6V<0K%oFi52IIvo)7 z9P{Q;plgrtMl!|oln7`oO-Eh+=iDC>4l1%nTJu_9Q!qD#M*Vyn+46{#l3xS@+$%~X{3H~XT(AyWbG4bSy5U;J+0w41v(pu6pBOA)4-MAw z&)V^tCVTa;s)yz!9M!}_Ke8`9bi|M=e&)Hsk?fp7B7Bizr5vkdmi@kH{QH&?sBd@A z?G>m8^yl{V{ejNzjowjoO4OdE8b zN=8zks%Y{y{(}k|edZ-3H8p}J0-PAwA=6ZsY^ef-H^W=9O*vA9ejD6opzMeC?sZpO zeuZtu+T*InoS+pqn)^P30={aghlfz13{`(y*bLqSA6^fbZaZ~yM^+zW1a z5y4KC77TS{RUdfZ5n2LXA3aM2uBxxV+a%h6}9K7*v`-KsX|4p(o8cdA{M=z-98G;!(i| zHa#tg?%s9oO>f%j9^QA*ZP~ulL{S<^mUivF0FC!XmqxYh;r+*5TY^?bD(`yuy>Y7g z{{957QLW0YRqbiLD{=5EU=rDEY*1CY7bPt48A+`Q?HUQr`nCjq^|=`@ODV}B@D*VC zyK@)x_d zR{XnVU}ElQMPqPl?59Xji&Cws64kD&p14XBLCZ3~Q19DDkJ#uu6*sYi=@Uw6jmRYwVQUpAa8PA0~;V8b|aBH=kB}nn{IOW z5U@C8K+5SN!LB|xJXI7zZ|ipb>o#)||9^Yu0ib79rT?2UJ()?H^xh#MkV5a$ zK@claMC@hJzpD$nc3rWqz3igc5bTPmh%_luqz3{az4tmvrl!PJLA0P}9_u+Ieu`!2VH3Zg1;K z40c6jS#&ZB7?w@g46KG#u^3-tK0KmT^5Mdi){{3vM(U5)b=Z!<>Q4}lK`!=n~PD5Om`|$UL0-{8h^RNRD5tuNnYX%m~Ro3@*A4UA!HmIn>W`N&MI#>B8W`}a1I$(#w}X#U-fSCop$ z1jpz$H_?y!2R}|nA9DmEFul?r{%|X7Wefs@s8-q2=A_L#vO#M2qv{t+e9pP&QSLS{ zqHbC08s4p2TGE_(i)rd|NEqeq!0$RD6|^j^gh=Nm8@X?caWk7WTP_B^EtvTC-g|$V ziZRP9X79B}TDD>(*Psark=u4&C!DMkV8k8dJT>9@ZbYilkxd=8*Jt@fa%UP z)w&lM*x9Zmt~J=ZK|5et<8U}8ZM(Jw0?@mEEj8@jQ!QzM)*a1h)VKrF1NYoS1l&ko ze;mzgD2D?n=}9z9QUZhRb#MS+&F>^K_qlbv9Dhtu7$jtHOnXNcJFn**V6syC)=jh! zT|(Jm4XKIUX$>{n!$yn;iYNWhvtL?-ao(_bLsTC0s2c)bVthL6;G-hS)q4?cKl=C= zrK3rcY=ty<=RNnrQy80mdCjlW>8G9&IsA6zaKphfl%~=FB*aujd3ja()7|%kBjbPx zf!lUfr%qzxCY&je630YzTeUY)j~$>QB0q{1MJ>CufXKyUu7IV8@(h&t6-6-tbl4F` z#@NlDKR=uW*QAcy?m$+up_QCx#JqCl$~15Oyc|oj{)bSPZi5%4hIKAP0-R(KpZKp7 zffu^%v3@v@e-~}gA@+VnZZcuAGwf0ZzIphRhw_l1Rgh4%d|8^(bAKDyWz?BZYJVx zBGTLjb9e^~=w`~9gwBnK{VGwhFg#sSBao`4Mv-GCQf%DTN}hHMYHAzO&wuvI^b1+;YL`c ztF34mkDWFkA#^vVAIx&*O)h|a=IOl+G)5A`MO|H9JI^@u!($UAA$m>RfnDmm|&Kf{ihimO%Y;n186naua-#d+;I2nPQ zdYr8b(yxAf9R?6V9Jv!90=s1WYh*nE(7}T}sElXqfo18|CnUIE3vNwM6lr(ny z#9-e)@X#aaK3eZ;c)Q^cxsH^|Rm#<`owDmqzm()^0~OMlBAsIyo+W9OsQzh0*RENc zUVPS>LBPx@mkgzZ5%=ek!`Q-n$Ojok!=VZ2Bgpj#5hPL@Wvmr*?;3U;lT!A+2BEB8@+Su z{__#&H0YhH;(r_gjQHkD-*@RJzVp5BET21nKJ`x;O`<&^OpKoJpygb_*0tss1FNBR zquRW+8AAtS7)V?r+KNF84R3ZkA?0YlsX~;W>d{dq z=4&<~pW3ixQ$%Xrfsm^)Zc)O-dEHdqaF06X*ff}0?cPAN%}Td&zY?V^Gt%&eMmkZE zP_JaS77VVDxe*^iIsuJa;^d}pnO^_;fnof04kZ6sYrk=$aC;q^@K2-$^4nX;eQTH< z4_j*10FP_X9S?KqGQbsjv5s14I2qcONYNNG80b0!j?0D(o8b}EgouuH@D-@F_3hUa zS=B)_p{dS%Hiw#VYUTbOoxLEv6SUJ$9 zR(zRS@r?fta>e*G&ZX&rjT_bwmA2w&w5J<>_Z!snmZe|*C&1a~ayJN5K4()X|W zAQ@+Ce4626U*F5eW|*Iy{^{baRkcCB33Bjd^RXaq1aogkaESCfl9!oBy8An)>M{VK*< z!|K|xRL`|2lf9Me^zGL#&3gEeSQjCx!@M-{`=UvUNR_FnLM+D#45&WZ&$2$`RtT|| z90w}_{I_eyhA|>HF06qAhD`nD<=^@2Ti^DMPd_nx-T|OXRsZ9MxAO)2)ChDM^nGf& zpWVYEtb!ItW4F(L{tNFr>#VaNgWJ}VTsqNBJ4S9t4tq+59aveYFmjIVMD5jS6{T*L zvbDBsr@nk;Xp#rRnAO)xL~JfQHZ2UwCdg}B8?B=Taz`kpfYPmRH!AwouP86F)0qu5 zjLc+76A9`FxRKk%OTy9{UjL@Z*=yXZF|PJOm*8d@TP09*7#-Qto`-8`bhkA%veDA& z6Ju(mPixpPJ`EW3er(zrdtqH6X}^1|{{07%8-64lfM|$Cc&F1LG^?{ZoJVSucHMded9pf+0PVNE$T4(nKO@BYso6 zS*nydm7Bo$hTggBg7pxom4MVk@Hgtby^k}_d`Y_D=3CP3f4n=4XF2mD!Au?Xp~FX} z(UcMX40v9t+?|{&VDyr(OcVMAC!KUMj^BnrSUe=&xh{3I!jJ=Bo1c!fhgApa9!y)c z{@@2+9y)Z`i-=HqoN?w^vEgV4Y-Xa6hq-k(M-6LJvkg@!Z6f;ZRf>|=u33}*^rySi z+u!#3C?hgD*#a9{!zzwhCoyS1n85_}Z7#HNW^7y{jo-W0S68 zqpQXMtXi>}JmwIL6n(fs0^a=Ae@Pc!bP=L3sF=aVZ{%Kj)K;YjW3AA$5{qOrMi=ArmDIiAf%FF7}>0EkULeC93)-rN=f zC4+|yz=)Ek2F|ak(I{(V+M_hCiO8=OqMc5TN^85xXLkVx-;Ggh#W-%>Lh> z&#K5<+Xb!?5}Ryrou?>6bF7H)8>t9bykt=tjUGT9Z1i>-%yb2w_j?0;g?{~LV1ue# zWi@biC5EW7H_B#Uw>|oEUl<$0-fm?u@>@|mLn(YGu)Oo3>bgC@EvKvIe)|m>8hK~4 z&BEfy*WhSCO53A~qOs`)val_jj9OZZx+Wr>5lVxi_QH?Kpbt z*R2m?;)vmcqo(}c`|eL;$L$}z0OmX~Crv(JQtFL5n+}To*PRE?rxa^fxByjfAZ(!< zI{0eIV>q3gkQHVQL{?Us6VGG-B*OkUFvA9MV1>6_oaBE#%DZ=@;yaLX-x26m(u9e-?y`j`_}pI>l5lu4&G z(n3LiGsn+tGOs#dA_{@-l=AHXb}267d3gCo5TM`Rd`CL+sN;AZJNb3l1@Un1>eVam z?AyQRbtj#8+muLU4e0HMT0YConPtQ4E@|4l1o^ooB^UggV zVVNJLb*q7Oqc61DP= zCX9#m=;qtcyWq7LvD(ze?>0l;GAf@te;H9Ky|2l28qK@mZ|6Eat!?L?H*DCHe)^MZ za5(OZs7-Rz@|DYww!JkN>$7IfOuqwye(<3`83(e#t5Yv*+a>y`pZ(HTqz`=HrWbCCq3dD zP#W8iRs#{!&y?qtWCe18XpX(Vw~>OV!WmFLwrT5n#(y{Wu{3Jjj*HI9%!g;D>*(0G&F*-llq6>!eCVNZUmK8*l`Mz$ zI`@J@#^0^kxABdsh;ASmDp=A)<{C(A(2Y)3 z)3D^fzHvo5flo?RT!1jz8`g3^|s9sMGeOvVoOQRep9`Ba)*) z_rmW+NSiTCHVzpFNvX0&ZCXcnv>rg@N|v&}BfK&qedcqYNe5yaMhqQ8L_Y#U-wlG- z6Kue1*)+SPb}fb(`4y)fBh-NXS-^6pS>{ z>A0~I(vN@q6EwyT3M1)e9`)};@S!}05!7f^5P|;bPk%~BAANM7B#u!(?q&JnrD+g- z;)RF%R-oQSS=z?+E7|(r3%!tqe~P4vZ{=sIn(|4ln6_o`mL4xQpU&T*)@6$ku)AbX*7S4DL0 zm|0>*R=iXR(4A7SHRPiEruU)f_og?$A${sopUKTx823<`W1cnCiryIIyY6KEyWXlm zAk1q!AHw>s(O#6yNqt|rWNEq&C-O~idQ%!Sd|1f)T3>F?Ui}1f*^0^J1~}zu5WUjJ zL%Lgs!=p2%K{w(zeUsa-W|1MUXYTzKuR1r*5puMQ&vC~cpKiJFCIpcVBeF+02nEF8 zP~cov%gMkoHPUwsJYT;7NCe{qNgTCgPk3E)suJhVJbqKGdkOm8Qm>x1fq)E0adFN1 zhRD-1o|VIgk2q}Duwm8T`qsDBzz4YsIb;bYoyR^n0-XkZADr~R<&oLkt5>gHd+ZTM z9Ce19^u~?rBcJSsszJH@^2@^zjUPWg>arh%b*up&h`zXe!{<_uETPPe(c1Ot)2F8K z7Zr=W5jbH4DGe+w*Zef2mC0BO{3L9P4$d}4xo9-55gcKa+3yOOq-g! zrrxl?8ydEyRyNns6DGh^-oQRxo|Z0Kkw%OhON5R=fI+=>O+%Xb=p*UnFFPlzdBMz{ zi*9!-dC!6U>tL^skESu1+}ADLamVfCt*g`FM;sgmSHUn--7Oe{!6?HG0~QuKwaw-d zAnz3*5a!Yc(Jk$aGft24C%dJ!0{^yvFs!7V>%6&3B4U)p*9$4xfrIMVme2@0W5@O`Y5szxX(V;~n$mJ=-;oglRe%kzZoCawI3z(<;a~+J(iDpB zN=b@0g4`enZjU+i-|_>L?Ul&r>vbZWgB2?`G1phq_~v&wHq%*ak{X+t=Z!dR8_^bk zR}m39#(McOj40}QbgJ75|3O*eVHmqI47@t+KFeA1Q%NxwGDSB5j=upQi_g zcMb6VPS(yf*Zd+q@x&9+$jEj5+Sk60y#Gm53k<}`LR%i^pd5t4Igymv(6EKnLLUTz zEYI^f5!c~)+tK=Ogj=FR+X}o|-O)nM2>lCj4)CTyoqs4VyQ=c5_;Us*R5MelN6ebAfH-^nwfcY4U$owr9O-4U0!A#Ilnhj zsRib2XA`{l?%UJFZ+#QBzLMV{h{;gbLFhr`i8@)kn(*C`VkK{V)enE3)~=&>Ih$Y& zu)M~jjSbtp^*|hm3hr$SPC##>d?9@|XSrLJh6(@cc+^7#8!~htzXK{}!`G;Hv*w-W z))48;)S@}Bw5a!?WUG*GGqCDXo?!@M5_x%0ikvn!52_kiZW?NI7mRyqyk+axkT0*S zs7Zgg`SyTQFSy_}5t;VsM{jf>>KY8x4hV+jI4gDxyX%fy(#abq=u;biFxzWDW{&A zdh^WId)Gq9Ylm}CRofp2;8x1>nrOgtDlm69Pyj?-R81wXM(0_?MM7bYdh?fLL5*cj)yNAdCr7SWPdUbO?%es5;nl^wRKh+# z^swpV&WTEau;m%ptc3=(ywJUKOuV`H(X3_~t6oyR~>0t8RT&`P)KZSdTUn0 zobJ;vjJc#RH}6W=bH~v1o{&nZ`I}G-ozTmkeDAeVMGIKv_ zo2L+&ve~upZUxefm-|0Fk^5S2{+HpI3E40 z>qEKD2x}WX%14gui(G1B7=f;k(u9c{8W2n&V(wGdD>IaXfi4+Jxl?MIC0OzLty@&V z!f!^-DWjL`9iuIXwiArS!m1)Eps!Y zspHltM-G_#Y-j*+AUx|$L=SZ;qV)H{#=h(JTR<*0#x;AmpJI7LWBYqFpvGUHICUr30o+Ot;;3TN*xUd^jHSK~aA8>1#1S8^~8rh24HI zM*bz~_!pg=KK!9ir0LU+rnyXgn)T=$QW|KtCz_H}LfB(K+JMp3a_`v#<4sH`6dzGH zhMN9&M4zAt7}z$-=RywH>bR1iinaKa7^+srrw@d;HET99PIajI4UA}Z;=~CN^-h{R zfz%DrxW(8tG~~fB>c|CLUSoGOm2tX2T-<+8?RIzOXa^)hRRGnMk4gp{1|#hzb?G^( z5$9=T-IbA}E(02u#3|E$;NW3tD#Ane-g9Tvt5?_5GWJ>J&HLGcBU9I_Z{o8N5p@L) zsudls1&M7oc^Ly@G-AZaP*>c}`V!{%GveBsK7D!!udj6f$TZl9UzzwJTkzg$PV(l;J@D5@cno=Qzrgh$Q) zxw#FZd5IV=!7C9FwYU6U7PgU)8?uU5D$D);`s>mwU-7a~!wNO9=;oChfyjQAO&qM^ zQ2ZkQN$$G`>uQ#O!?!Pbf#Xwb&IJ_D1RYb1(A@& zu(=m6T8hyvC(7;3X1|&I`<66z^zih>&wnP2<28CtP^mvNMT{&Vy-b=c*Yf-@1M|6%FM3t$A-D;)HcU=6=GzWBI8wAN-l+kUZlb(o^X#;fx`qlSNXTR(;TGrMx_h>vY-WOeT zemeZ{!+U<{LmwJeUtfPDL4TjW?GxYm>KDQYbVleegjsx!p8*2U$e;b}XKz34w9^h_ zbEvVz$c=9`(QZVV?A~s!gUJI52^vwZS+hPHFd}0kFx#oR(cOOg1CYedqP3^6vfACb zMmIXNVJOW5TApTWQFD$OWTW$tzceBy5_#m|hvMHx&C%1_+-B5i8!-VJ@cm;S{s_5x zAZEx;%CBz5fC+~W8v-+%6AG(#RTG`aQus9fP4kX*Yga~&eA0g8hzT*Z(Kh$+{uCiG zLiaivH1FGuU4+1>J?Ga&3#>V}OsysiyBoiey3t@eu)bu#Y9dvQ>f^Il(ROxNI_}u% zSsJtprD!UE~lY#xwQQy2SB35hT z`{K|%f@+#ZMk1BW|D9!amdm~2TZD806$TBdPmewR2nL$U0{eVZD%ZPiKzeM}!Zd+rl8P!BDEeXWm>fpS$nA6T><* zoqqahF~*L)L^{>~lC|jq=XuOH$F-F~GI2Jn~ zv0>kFfVP3=pohTNR-)8ru71*#1Je(F@BEK?{psf$_R8u==;h{|CP^);VPf?=&WOoJMOp_!{i>UQdM-kLg>SUs7B3=deGnj zY=#(mHc?By+^{7)2{9XqYDCx5MC8S%p9rg(Xgl($S+hFw%5HqBcCq;x-L;Z~HzKdZ z2znij_b!a?HNX5t2&n|z9>qjijH6ux&qmpVK*>ix@{u%c+Cdo9D$2c3ircz@>+gg- zSBt?SLdEF!1G*Q!uSPwswPiEqThvVhXAU1agg)j9#%T1(4J!ttRge9XlB&p^=A5sQ znr!P3OfkOGc(CD?ne61ysMk>otwT^#BP)A*I5y=Nm({CR0o_)l=~E}Ak%+hGxTw9} z71nkIkym%SgY6;;hFR~u6YHgiAG|kRbm95w+u!=H^ytj{*~FLP+}wxZff&VjjiPm_ z&QODMgDt57>Q{-bCx+2y(a@?^Ct@LV-;UFC-S4jtlGfYac5%A?x|>roWqrz{&wgxv zTCi{_2DLFQB(m>AI;DQ#rGGD6Yt>-llryu^{`{}$_oRe<80nr*t3@ghJ^ud9s zWDMlO$)zZI#`i>ve~$tjrFzW>!Pt$>4tGlE3b*U*)Z?J^>01}hTHn64kOY`4%_?TR;#tqdTxt0$ZrcsUS$X8uxwRwCPbIr>=p9SYezI^&0t%aCrxt;4%l*u_e`=w`%|HNeBcST%3Nq4N}WzT<>c6~-B=Yu*@fW}qLnpmEwF|{vY5K%w4o%b zuLjbb4WVyz7@%GV)ksVV2%pW=doUt57cLrLU2wtJ&JIQXGGde69na3a^u%~dvhquk z)eczG-MXTtC&UVj-n&=NXo=}Kzy%KlyG96dbr=Q#U=_ydAe0T@nXaPF|JrM>H7G-L zWkcGyZfzO`3`}MtBFW*yhez5&6vURRxs1yuEJlc`wKYeHnC(XGe%HQ3K7y%D`xHntGDAU$&Gwe@JyJRxAy}(=z*U^2~iGaU(SmghEQYqGq4HqchT-x_eEf7hX3<>ip`a1;(Y=%BQM<}+Pci>5k! zkI}U{?=>>g>FqB3z6y!jIzAsZg4qS)QYa4=I`Hk=w}zVDrp;SGwJ1|FuP+({T4dT` zFxm=RwlpK8GoI>zjXPUGy+sa$z(YI5;NBz!)tYjDV8eEAqpf( zRYgCEial#rwD?j?3FM9!uK&L@}0Zig^{0Ic7-G*>Z_C=vd zNJQi;cd)72W83nczJ2L}rkD(dyd7g7@~do2Hnq?|Y(!W?CifT=%!FY*+z@TIDq}d9 z@3wU=)mjVLE2SC+5(8{ra1(X5YbZmKP-oY#@@Qz0H+XA}jesgC_c`Ry!=gsG2YKcm zK*si59|l8Rr|Fh`B|Z3c>z8{*7fFPI2tm-&G$+W zQ|M$3b<@B3&2Q4LuKi8CPqlYNcWNPCEu#;&HPaIGtht^$cU~BCH+c=0se@?r!CVp0qseRZXcKj*#LsQ>V=!Kf5ix=DhRM72p1OI2>JhcA*oEo6go|jMZAiTzYYT zeR|cQ4ZjiRg2BCqA8L_1DX5?*k#5NqKLL=Sh zRyy7Bc1>xFWv+`9#A}>)jP^z}33LR7=hf*q=d7`9!ZC_<0^7e4^dNUd16H@9fcvu_ zz;&B9A%j499rqq`!8m%77<*xGC0)Aa9h)lV-|^Ow$g-`BL0q$(bub_zX_+E^$yhH_ zLRh85ja$P3R{q%D2ENDn*-XEJgFp$YSmUAwHVjg0!L@En-Ja+KD4!dtD}2gcahO1*zCZxl%1r-Ec%syxt7j`oxYrlZH*1EpT|XXzLT=NHlppSnK1B4@o71-v#&=e&3QAxjQL4s`HDWQ3}Xu1&>EpFR!N z{s2-Y)_IdkfD~prLquXLq&Y=#8p->cQrJS{n$crNM~cUMgPe$s8)5t7fK=lUcrlTO z&6|(|##np4?VvDSg8!g%*`B)qLb4MU7bcJN3il|VV;7S@3=~8{o5;q_Hk?tMHP?o7 zF5g4vRTjU*IAvtRG1X~dZ99KD!+{8JU8^I?>|c+xZ?w$C)6gI{@)b@Lo`LVLAma9; zVK4askWOuE(?ImzK+_!wmyL}ZQ7~Ma4rZ*%MK7Wp5~q)KQdid}gp0~3odc=q^4;%# zZx{-X?|t3tF1nPI#cI+4ovinN)aLqkx^AaI|2qxzKf`4{^O?^ab@9dT+|{$Tw)-|B zY;)pjO*fKjYyq-0Ixu20(slDG46`Ltn`vYsDQpQ5rcj_pUqc#@y$WG$eC9$uLXgCo zU~Ic&yCBD5D1E(uGkRJ`Db-`MW`&{Esi>*q8bl~UzPo7$DwBI8*O)$IMiA>-nrW;; zLQozzs4t3?vjd$q}m_c(X%g7nH)zA6Ysj z@*c=Tc{wC6%B2P)*rH<)8re?_@ac}lnc;nv_HEThTC!ypB-G3eg=^5x3|J{?MzG4Cg<;y7hN6q}$Jp{u~RJJJkF+)kP_fikA6FBI=>TC#L-;j!O%cF6Q2Pq$VKfEkt-_)WsWFw}Gmx;2sBnHaLc@NU`eRv~ch0 z=v%hd3nJH}k32}b+OyMTpZs*RtyxXwFbErjcxeY-v0O>eCr+SZZqGqoP1q#sfKYRU?iEnk(6Jn|^kEjn`kI#MtN`T?BKSAd^ zUsxmXwzt0d)1Ujyf1X-X)#Cu*?yj6x~(Nx$WtZw{GQ%d6aw%_F;k*VSd1e>)MPWIZD_H|nK`eT*D6GVV(# zx0%Se9mY|Rlh~{`VUT53pYy6$5CsYo1HDr>A8@gvC?!K0C1|DYUT(rR3t0(T;B%UP z7G_oD?yFz@YWn==KO3;AWp@gT>=cT&Ne4=BAA`(iw7eSa^w^j&Y!Ly*{TX#p?~McH z-(K~KSJ1xnw_&tqVbo1~Skh%96B*WR7?cL{=~dlnNqPlMJPt)qelw+Ob18QlMw`{0 zX*KgVdH?ZgGfIA@E;iyg)N-E7KKn_G{;fdb+qp|740NG>OXWt4P@pH*xXTYP?cuy_mvqMbFT?q{{PJ(6*Io3+aFRrO8c~C*;JRHD z@sXFod%8kIury9i$3R40B(I4+4S;3tbLc3#HiP_Tt{f6zjS6lI?Q%5mFS_8|GzL`1 zMk`|YA}4m~Q(PwI94^GT{q|3%oN`J!;;19Z?MskT4`mpR=CSzk5`1FKXnQA{sqPr+T1yF$FEqv{LEvH zI`&`49}j2q>E@CWu(IY&UH|b2k*N(O1fl#M}rR{zHLNZ-70#d<4-s_o%G_< zV?*9T1S@1aYSf4{X7s4&PGxT}^U_N&VzTsA4;zYVS15g9IE4RKt&yFdHOjqaPmH(5 z_>q|pL13#z`D<)6A@O&)jce|(R^(bWzHL00xm*pB|5h6Jrkidd>hG6MJ?-Sk#dnQX zr_|-+7~FZsAJhCf5WgOLD9BWHH|uIC9%JM<*~?3qHJY1<1Td!m^7gl*@OEp+-rBZP z2f|+IO1}!R8~r;DJum_%oq9t0_+=l0Rlkfeq4qjRW3+%JqU%YXw~(2BY6AFoAUAe zk-AYfmi27zUG&N8>lp5`w2nF1Nj|%h^?BuYu1Ggu|2vcmtBI-`O z>%$K}m<~K(V$>4Pn)OI}@1>W()*c=AZ60+8k+D^2iHPAN*e5MGzW;GFV&suWe z$M$%VbeN}qvTi(ol^$Me@5w`@d zk@0xTo8QcHl+liLPQ1U8Xx?aa?8xCUUK*=yhSAeD0VVs@<9+5*G2^0 zKqW#2Wr5ooWpED-5~C5fq_zhhnwh@(jsJqUczBeO*{9wy^9Uw5bFJE4HuSl7xQ1J& z(DP_qEhm(4=o$> z6P0~62aU8Y=ML7prIHwO`{o*rcflA|@N*m08?QO{)u~=She(P!^dPdeNB{J#ASw24 z|H7BQf&}j)X&jHs)LomKmcibvni${RTz~zmYuK2m8wMuLjPM+}cExFEV6&e%X=>z->uVta zQA@5^i2t5(@ZpfbCPF%M1Dumy_qq!MDG-0CnHUB;oB9y;gADx-EV)Fe)iK}P)|H0z4v{WL{uqZ ztO+t-6-Mc^pZ+vZK4^omKgMS_n{YLua~qL;fArQH$9G$K6o z;x}w$oSHWWRJ%VCsWuZS#~8Ozjy9aC02`6iQwv_r7z`OcgnQW?9Dw1}%68&^#*Bj*liJ`%+;-a? zac`0WRa&(6+_tWdJ~lfLq3H9DQ8smC_d*>V5ucMO%@lf4ozKr;=A~fBTcSceerHMo zN2Z-9y_wJ3PEeKY5G-vhy#8PR^<917SUgJVp+7HQ^1s|7I^X_09f3}R{yd$SfAnpk z3BC#z?v3C0#y5_6^{dVqR)LYR4|ynG5w(Q0CEwK`?I>0D8qEC$N!3Q6lndcZ!H@hJ6S!<1QnGmNEpE(tf zJ8g*^sA2d~2z7v!f=kjYl#H1ufdk!!Gn5V#}Ihz0YBhPkHsUW7bgjnfDOBw#D@ zoNmfJ;2nJ9%U?=2(&bFS8l$Vc`C3k9EkA@dBC9GRLRxuB4)ZX2f7iR-m1;pBevA^? zij}a^d2c|i+;as|nb{UhDrwGh!2yX9v?|aGZ?!@5tgv$V5U^xBXyCIB%O5dD0 zkEf%KJ|aEz=(03nGVMV(uV*}aq{l%twzo}yAT~aj+eWQkt7~^ef|Pp22#pv$f7QkAGv%| zH9qDLN5m31^VSmOqCAeZ_NrICno_<6AXH<*@Yf+&bJ8iNh1#ER`htZE(#@n&d|x%r z$dN}L33LG)pY^Loz1{rOB6u^%j(YHAR2o>V;p>MC84&YS!@334!rR*2$hzDNKVc(? z)hgy?*~(SRmp}gat2yGIL441QS;h+{Qr!u|)4qaKK{*-VsFP1|o3>{NP_s8vb+yM}ynbwqXwn*_t0 zAz|O;oH@&6o?3?_%IDbQ%oQ8$WfeR1*CbgHZDN$&w1viHRr}nrp%ZbQNCdBOw!}~4 zXJZ*9YL`>0Hh1C4*jcaQ-%29h;LD`yp&wqFQP2nVU-AV45 z{Q9Nuy@WjP#`J60^0l={*GEUa`uf^vF)GAf32faAp-jiY_bO6U+tc#F#;l2; zP!gqMA~^~U?SOf{lT?6?gT%Nd47<+RIEZL-o|ug@gJ2Jlu~xA=PNtBzVmWQx|4bx` z2M!t>&3dfEuY}{E7J*AlM>CVxaNs1;u_mTYo0b;g7`it7>_}2EvNqI4D4@Jap z7sDWtGS4p0QKJWkZhjqeiulvU?b}-ytXjF^A;#%Z*7MSa&AT`C=ux$H!h~@f4>(}| zR-Uty2=~9=68|aA(3wO3rsFKoX2MaKR-;=@v49& zg|Xdy-CVm+A|xr!y4v{*<^_DdWcj*u=2>S%4YXxWb!?)RBJU9eVPkA-wSL&BusZ$Z z$5*FEAG$Z-=VtQPRpdTlHDgo0nJdLwn*?VL|m{8d-}02u$L5os$$+l8hwnaE{Cip(w&pPQ(=9x`|Y zGNFUh*S`MMbjA0+na(};+^o#k0^1u+_@DjoTSTB6)2q%qJEFfj>e2NGWY901woGdgr^|k^Xr59RW|vbQgg!cdyXP zg{ZonhTgi))D>5JEB%rbL2tUi84HRwm~NR*epqAojmy84`qLF|J=}#FoDwz9LktIG z#^xq!;OoeR+8u^iM!n-*f|R)o`M6V!`T87G=uS4%=ZIhZkF3( z6r*acTR|SjuaeY^Jq0#x+7f&Xp?&90^keAIL7{>uOWoAKZtf#W!uXvq{%c=zK~!+) zxaoXk#K1;9MEM{L&0Ob-Z-19H#=1gY*3v%onbKk^KgNt18|8L#J{%KCphErw2lh?x zx%B-wzAqzPv|pqK3WA%jYs9!q`kOazZaNs0%g^0*)=i(@J$KEV`QVZzixxb3zyTBQ zI_=a`e~lBniaBUvy>~|8f8QyLX@5u@D7 zMk*X^d(4niC2EYkCFih>={LUemGpz}eV>LhTeIkh%3^Hvy%7AcMpz=?@?|R_YC+<& z?IyWY|MvgheMx%l8{ZU0ttaRAy4U~icNrSSVG`EwE}=|OZ~kXQ>)$jElFjzpZ&Iqo zP@i=2$?1{DW^v~bzR;(y?9n@&d*N%-VTT=&=FOf%MAs8JU5H_fyAgC3O3LhI{^c)! zD_!*Zx5I?DexKYhWWg4Ul`LuN`L8EsaKQx^V4$p<&N98$+CPDj8G>x70xUXZZ8#hM za`8LTS18vTKYk)DK1sol-k8JdOuJY*_MZ3rD=k?+K&jpe(jm@C)+(@(i9dUUDjVy= zdhSf5%z`dL%zfb)hz`gDIO(L5Vr)i?$YinGZ@(>!;l_<%%C!C#aFH%Z*}i3h- z8Z!=_7Jc2n|NZZ#U;p~rc!7o7Lsy?WQqCqi%RH0IBree@r3 zC!GfUA2I6B^J^0Qj9}9`khNh%5d)+?O%6E1n z+Nq=__jPZ0OVnHMRxu6ZQ3LsE=T7tIs-^9Q93>lCX0&zZnJlC+w-Jc?+1-ThHFwrP zM|6&X=ilb8)oZtuDoe5w;NYyt%GO{gDC5RnY~-HT`h4@5)+OIfi_td%DgXMKYiRhg zHe_aZ5yjSGR5RX6UQ}DGPzvzyc8pE`fkV;-7haUU`-7{~IrP3(_EX2@kw<2xH^24G zVYF7mIZ(4+bu}BHbmg9^NjX&F5SXv_YaK!*XPo_#D94&~;1rsVw4}w9pp{i4*Sb!_ zrnA*6t;Ug9fKECrgVfbJatwEC<8~sy)#*bY{v2KEYJt-!$?DMuB{J&NaT-jmXh5&J z8ut5?gD{+fKg zF~*D@6VYwfR%d;1FK3)~dN@({-S?+(5-rzi!|Ac?%(%atIj9jKVJ&y(XKUNl8qjg8 zrmRgahz8U-4bFwCfSg~LyOOc6n&a%3zk;0joJfy|tQ6&9Q6$M64TltY;|pjX{Vih()3y)3K_`3qd{!LoS->K8Rflu^@~OlBS(%6f|Q$$tlf!3PSb$~ zM~&-@}4th;Uvf7R^+Dn$Ae#_y^>+S;neHT%z zlA=wJv#N9ya0Y}#qn?>fFK^P-@m=y&v!pwqb}7Qx`VrP}DZB&J7vOSCI46 zpcDv#8T5i5pkdG}Fsd>#w~Q+C?hJ_$oCfN$|M-t|_g!~V($!#26P9!vB}bKM3-Ej$ zI_NUWH=vDf_b%R)&b#o!^qud2pQ!jvX#hr1!>r?R=bd+^_x$U-(gI49wz7#28r&bp zLBX6H6-qX<*13@}GS9DU=txQeCj!+YM3GHe}e?v~K0b zv=c($kH33W+PrFgdJqR}_M>wtulfYe(f~@@Mt}|sOwbY1U?6Y?v~q5t?3 zbJBOd^Zn?ruAHfYKvDI;^V~{g+(blaI>0g6im~m2Q5-vALVEv4Kbqe6!S|<;W5;lR zu-Rdu*TT}j|Ngtvyg852-nM`G&J~v%bsJtc$p z-FtU5@Ue+Z-+nOek?8H)4?-wpw<8WcG#ndAdcyyz(fL_0vKKY5D&VZM&W3G2f@|ED z(VQN2{n5}LdB+`h9*V=78I_&KK0N}R27RBN{y%?s%82?xMw&u?bZ84uc!+rjd&*po zoM1lO(5?9mA{m=q8-}wzl+-M1gX~0GN>#vYQyJ|}<>6XG%!a8{sc^HW6MFaG8z46Y z>Bo{FYrLaZwh@cD+OZfc^465>7zL_rEwLsI14$S4jvZ8h8*fS~nf-Ue$(|?al-I78bJVU*D{P`d;x&+UAieDbNhzf{(T!S6*Xx>r@nUjabq3Tm>sTk#Q4Nc_#X@|??SOKsWa$Afa*Oj216gEx~#R5 zW46nes6xyUb1xySiw0EbUhYjj)2=(p zdQqIn?@N}@LmuN>tT#QsCJ1=@O&Fh+BMPL`W2(n-wWFFygL69Nz{&I#Ku^CUkL>St zbgc;S`CZ345IPl>QhGh^xfaLgSenHg!Me|F3_Fi~dIUNR`aV7VfBx{~9If{m5#k%d zpKQu*T5e2kei~i7O&M)#jNQD1!E;VJ!;o1gJ+sTBWhPOkM$`0bYx5YEO+7a;t1w@{)f-vGwpQ>jP5fWYhL!&TW@7d8Z!LK?=>n$isAfTZ(y{$@mmLO8(JFWgm6f9OmwPb=_9inyaW)A z&?#?Eha5t8GkUyhigxgN5tRd{PNQ_KHvRIK*YaFh61R74FCbwm3KlF$Ey$zJn>~-l zENAd{I1bgdsTMUeow9;IAd_ALQVqmgL$j6z3l;^MaKzz91uwvP&?uQV*GT>HSHIdL z+#(Upj&(Y3A;N=G)(7MD9z=d@BP(odN@UZ<4UreNo0&QE|M&m?Ul>;Ft}p-ZZ-$fL z_qA)!_)|v$X!L^srui6^7Z* ziKS)!&4|7cC+e`n4omYvUxMY&m-p;KH8&sXb09k!a!Hn=L7v<97L^s2TGrKdOK*Ma z+tO^RC=es6U@Z>czGK%h3l=OnZ_C!kVV&FAzC8k+27TY2|0g{?wZK((-F?qV=w0t` zdrJ+ElA`7pLuDm@ob0cykP$R`15WCh#yfxatG8iHKj z=1_5uHv1?s<3IP=&qit0CK%iOQ3R9mFD!1(b&EMvqEH#n2OT^keUp6ht1mb|nEo2T zfcUwOt*GjK{R>}A*I#=L)m%H_#69(U%m( zA%`6r^0T?490WaXy9Va=t6uqX=AkVup!}^7g#O-#A5ZlIMx=2gC#BKDCZ zGsj~aPR&=p@?{8rNZ=ZUgFc8(7{Lo>>zK`$esKE02S1WdAR4zOy9N~EKmOwr=`)}D zG}oYFg0(OaVW9IaJP*g`H8eXp4&}5dDDS;JopLhu+pHPO2(#E+E>m=xYA8vIJiC!N z^Y_IseTkCD#{%^c1~=bro;^C`@y;?F4%e(kyCA+vo-60y67|$B2tEa`H7~CqPzc*_ z%~Q`fgVMVpQNHFi9hMTmE@J#E4bwpU}6_j*Vl~dwKHNuEteK|kZmE5_jYxe^Wm^$r; zKl<6*=Z+7H`2cPktITJ+2x zyzib=VeVVQTGEHN)JS7qk66n9YL_+4Hg+*m(oxWG!SV(ACE{ZvG@=X@H+TDJieXen z+-g)NyV;daxt%rGyZs)K89C4ryFF)av(XNV5jT(NR977%p<^S|?Zz%_Y!B?8QdVXp zR*x~aY|0JaRD{H|R`dN3|33VQ|48ru@Q2e>h=MvZZtU_9gne6xa9@j&e*_}ernRe4 zYa=htm>A_(V<=XyT@TDYD0+Ig67dR&Yn;qscj-zlp7&PC^5;I?+;{I#y*&4PudM8r zUiqq*L0X)}7}TWM%a*0P?tdgz^&SHByB^Zr`m_cLNv%m z)m>qCSAc>v;}I;vNJUzLYfZzT51=2r5%vbU-kB$sJo(7XheJ(G=f^R!LtGHe0&U4H zSEtQ5BwhT@ccuUS_IGf46elWASAPG>^wlro3`j_0eW*olZxaQ3bZ)XPIr9$8Z^Wz9 z!9!&#;;Lci9w{GrD$z1z$4C zHcko7LW%S!{O*ZOLAj9n0fSqDXL+H~y0Cs;i;xu8tPakD&Z3QjbYA<_o1Q3#`9F+j zS|7FTHU#R0u#XjHyLLd{1G%WIfO5bk&j+7I3z85G?cIxA(u9q(tm#jQ zOnOAr>iKgENVVb1&?8@mRP4!FY7_X;FEz!ZMWoR=Te59gjcTybkrU1Lmh-=UMFcLw zU{e*o+@v)GM(i4>Cmn@V@9^sg*dz6jIY-{3KhK;wvm_Ur`(gXLND*LgHg0MNX#6EF zducixnAPhE!)-H(Xu)c}`-&^B2xIN}8!)!6uc1SSM&~h|i@1+c zRIRfhQj$v|>DI>K{bmuG41+sfLfluw&==xYV%G6#poMB5(hK$dfWsNH0=6$wD#aRB zp)gAHcuyG)q#lN7gX(&tK{H&>j<5Hiv*7bt)_1@G2c#>$`K|N~H0*!zi(iB|&=QrYgZ;my&T57vn=lFK3_=CG|#S9X(JBoK7Iscg`~c$c0o z80qGqvGxTsJtD7QQD-K6R=Uf%_CNa)wzt(O?_v3foYSwOR# z_;ZwP{rd+$80Yl9)OlBqR7av7a@c&zy?%4;uQ2YbgNWvPGM@k@JGJQQd-tH;KKF>v z&V=`%VTyWsoF=2!;{G)-;d_BD$S3f*dqUzGJAQ0P!k%;{`EtfeMfQdC@9u^}wWEzr zV14@2Xayvo|DbRVL^J|z;a)X_3d@+o6~0zK-0pLeCr_d&OP(jkI9iHEw75OpP5*bB z&cu2N2QQCx(Pp;{Xj@SoYw=G#_4IhIwQFd)gGkNqP@B_Od3a5ohT%hp?a7ln4>}c& zgMW9tbUNjE)L`)ZS!TS>j0Rt4?5LxTO7DL6yA!$!QBtXfzfrks&~+@K&%u(lo>d9+ zmS{vBz&9xe7nI0|l@;4)`KzGfz|V`F`@*o}2-pju z7drZW7A%uH{q)oOKla#T2jUoYmay%k`+BEA-$y6@sgJ#7>*j&LmXl99>BOND(ljQP zeAzHV!k8)Fu;*-k1sh!0ylXJXa~|4UvyE1?l)J^m`Jb?}vYtkfRvG(8H+nbW?|$do zAxLBHaROVO)*G?Sy4N_cG4A0@W}hxKCg9$xwl_E?-Bb1p_3RtZ)i6 z1m3*HLjz<*K^e_uL^f=(;T2A?$9P1olCtDKU=cOyLik&@K>W*$v0$%%`m>)x6>vc~VdkD| zK@*H>@4oAe!K366?q6ZL8UUt}`gbX7txzgXu5I4&I{O3QXu}cpjrF!$t zx6r!wk*MF7bKsnX)5%&7oq3h&7zgLtb!VDHo8Q>f#`7*tvmSdQ$_2HV^`t56Ti-@x z?K4K>7Vblo<#yV*!09xnHG|-1AfpZZ#(wWa z@Lka5FDu^-DnPFRVD@g^ajq%1>jn?R*B^KM(V#o`63JHtW4=%Cx~{K%?Sen{d%6^|VZ92xTHpWLsMVQ#JyYRIap|w3a-fKBq)(S`e z9`&(ne(}rnhS$FV!*zX{4P3g5=QZL|eM};qhCp?-AOG|xLFV#&mhE{34TJx7EFD|P zaFP&>)^j+PKgtEG-$Bs`hGWIgO4uU`XN4P$VEDr5N4XY1>r|M^un~%5Q2_^{jQNfd zz>;#FQLlCKfhKT{7n4qycF43$IK#RKxeuWen@0SC^UuS$BWo+6%sVsP5$$Vpce-vo ziy}M16$c$SEq#fOY+m<{JMMrK2w@DvC^F&uqa>_UQ=Tgka=p0n!^PZb*d04lD1io$ zpU!Pgb9%*dTFbkg(NZ%9Rr(Z2~zMA`T^DSM#@Uds)MwLWuF+NhH1ths<^B&-7l!v9$EB0+qnAY3=HjrBb}E zyGdo#S5{Pamay%k`+BEA-$y6D<72a-)xl&x6qa#aL@8`$LVj*UGLHkIlqJjtmkn4c zk}Yj!M%8XgDz4>CF~ivQtrpre3UXtWJSAeS`kKmcZpwb`1ja3TjpuTnHUu~7+59jb z#@*)VVf#upiYSbzrjBwi(SVK%*!VkOfjDHX=bs8=?#%~I@DxBY^6pPs2T%T3Xc!Bq zy~tztC!)Rh;)~;5=1$e&w)5F&y0clwL0?S zo-69fg;;qmySjZJ!I?pWh6en-mGZJE#{(q@1BXK+lq`Co8lSv?4AbZP&H4VUT_2@` z(fy2PQ#mh;V7Iu(Wh<7Z?jRd2Xy&VZzZ#Yr5{}N<4AwVJE)|5ZzEL+rbHw`DnTqi zM6zbl7|8jtnT+Uw1jjj)mwC9ZmqAQiiR7(hO!MZxwi?IHaVeY*8vy0Bl4wZ@iC|zV znEN*S$MZfv2X>otp2vs1z@;6O zzl8AJxVo?j+nH)S8|#LR<~5b~B*GP%C4!gyG-k{MU}(FV0p)ViBKooeS~byRbrlE# zt2f9@nyVtRHX!9j0q8Xs%Ixo*xyc-nDzK!;XgfdulcMvE?|m_FclMI(+78o{vG z&h-Km-|j~UWfpR+tJkiI{P=Ca?F|r>j99loPJ8UpN7GzX)>PS3F{}}0d5muil-<|b zQ%|0M5N!6ZP`)=|!hZ4IWlNXmMjnhoVU!||&%G#tI-c?_$5LZr1l(4t|Ig!4BpHi8a%7mY8l7!XdjhH3u&2JhI{n8-KNeEH6DLiKl!cw(7A%;Z z-u|{XgXpY})IjbsM>R<}yya;9ub|fdi0qGs(~^mMHmvbA?WUx3j4UG+Wnt0z>}tOg z%{O0t^;Nf%cFCQCI*)yB1Ue)1eQwI1{J?A1uJ1+7?zEAkMvkUsW%q3u;T^(+L};;T zSh6F-DsP{Wqs<1LjfPFtT3VwiH$rWc#z({6gv@A{QW9GclpL|!^PAdVHut%6W@F6P z!Zz-UGTNFjOlq{#=`F4|aPY8@87+(Y-&`B6_y}7z5hKiRZYf7+*+Lhr`%>VRVR-LY zL+)4HeF>g_N<6+ds?X2Z@hBp9H{VbUE0NqJhe;AU|25~Q!4TeLHy?KR;go|dOF#bM z)#>Ly`)PUrW4{sx{4g4r>{K*_h|SH}b5$E@VRZ%$9fF4XhV(9?-2Q4y7G^4>jCFWK`>s%HFMGk=Jl&v(BSx$RguV$-HFZ0GXy@fgKA3vM# zuf`cz3Qu4VtofyjmqZKMRv`V1>g4;>5Jug+#tNdIrCWde+)P97 zz&!V~X;Ua48^IcGja14&5EIcNOEp`7^SiRHeOB+qI|>0CwHadEfbTi?!w=q1PPxqgH{&RdiXUrY{HPS9uM>q;OLu%GYo%Dg9Cvqmi1PM}K zc2Y;ec)#hU8=9}Y@&^x|cmBCwIq&>e-?NWzYy0fucN+A4cIxwkv$6EL{PN3RLl5&Y z%Crh|8Vy-G710?{Lq7KAX#H1Y{Yu6n-eV+NvVKvkPSRGQSUGjgdrQ%b^xKgd-4s%% zzOR7V%|Dj$+Su|@A?t^cP>oE|RyNIh1!0twEh3n`o4Uw>Iaj_)@wc03DTTh*n3u?L zd#AmGQ$NExD2z!#-XUapNlgq#IC7|Es^r9aM?0ic)fFAX>F12%-QM^65L)x}f zCMG|@FB|48f(Yle2Oui5jXLsb9FUVyoNM&wl2q$p`lOp*R)+SrKl~y3l?M}^_0a@8 z>{v)H6hdzXvAFw>ccw=ko|!h$8np*wVze1m2&QyOi^}L+WF*{_NL**6sQAjc_S_}U zag*F9Gdx2{}A!AQt*xl^3i&W&AiS)@_C1h;wfX48+pe{>Fff)=!Ao_SK}9u!iwu79te|4l^z z?;j{skG6q^+CyQx?%l}mW88P~nerYb>p2EdM#}Z&2V8gEbsI0aaIs>*O4z9wp<`Ogh!F)V1ix0PdH6lRGUZ(&5)UW(Z8It{u;E!PF- z$XY9Id5@)P8iT!&T83Zu!0uj?bL21XQ5fD+|JhhPC7J$oV854Qx!Q6%HeF#}H)qSciVZm6Rbg_YMV{pgU;a`$gd*y9&L>@|Xj%P&ekQTA^cJeYOuY~GQ z^YmM`1zF4Tw@@eZ zUsVr#F}FIZ{!PbK=Oc_Wb8kAJAJq;<>Ep+bNnicym(%gb9Yf>|QV?41SvGkG<(!ee z;DVtE&q+)_S*~XUuCPyDcNsB}iLZXXoeE7ah!Pa@1D~VekAM8*{&jUd*FN1c?)=@p zF#??ieczbpr+vzuJ9hQH`R1Ene%^WKPmIV#Bh5~036%{wJdAW=11@s0Q81l1`r>yH zFlE~jWjvC7S`-5TGIt}|g5mHzxuxkYYJz_ef-rk?pF*QbghIq?qxbsc12L3Y%i2iF zd-J_^u(IAbIAA4cf3FeW3&nPP*f8osy(I|#Z=LR7xtGug-y1#DF+4?|cf~9FbrO=h3gjNJ(a|SYTMg^`$25iZCsazBa9-7 z(%RHcls+c?=C{{JYuOr5hT6V$DEbj?+CtP0ZY74f?Pk6Ixsbdd^LTRGPirFzjjQZ!l(7?)}|}3{C;}%Ip+kgK*?Lv z2d%A57&Y?ooHtbCLRK|H+1)X&0XLWU5*;D;JU2$aR3e~dpfi$d^NUa;F=mdoJ=SghH3?4u*lY0&r4i6@WO<>7~C z4yU=t$Q+$#gDwd!m!%m266r-Qb}x)uLAq9WGkX0XtQlSU=-ux$2o=g!UIDc0ia2t?aYdvweAMotvX^Z|qVXHJ4f zF1pL*^oQkqMJG1T5qWCfvkq8ac~o`E?L_vcyZ#i{*|1@An)Adw$YHPK$?`b{4Mu7e zsM<)@=-+Y8XaS@wCUM@Jxh1ikyjIjSSE_Z)m~rWA-}pLFZ|~fz97jZiLc?ste*b;< zr+@jEx2Iu9`A*({Qkpn%Li)mg{wEE3R)w%oPqgY)mm5KQ&@9$2XM#!r2$zNP*iK1U z5s4R0W-+e?Q@nVsW2a&DJVoi6e{(F2kP9ZcqCgsY*IGNyq>f5Po?z~W3Z6teOUKks zD4bAw!Q)gFg9 zvh)33i*w=cj(^edi0TYaSAOsN=@Xy$6eP(N;mlY&uj65OA(*5$I&O@1qk>B-8Ew2kt%PZEt(qQ3hPCcFWg=%!G;Q;6o1&ft2x- zVy&m%S);I#dvB#F$7^4AQM&jY?}*4QTF7D`jQ$f)z0qVfM5%l3zL)xXBU_xQlIofA zo_+fF3&>a<{2~1Z#`uTvVi1i+l?V;jgIlzW&PXyV>(PT8Ct^p_BWb&lAx7)Wm%cI@ zuk0W`DI%MYCbHPUyd zO*~&eSos&d<&Ej>Z-09_2!h{Qq){zKZt5)J#FbcPFul7}&;AM&w0T7^V0mzg>JJs#ZNc_~srqCKKQ!r?h(la>AgA#%(j z8I$wz8vV+hATE|Bl9JfDy_{b5M8Y%zQj&Px+6~;BPJtTwxz2nS>rRdR9TL=71Kn_Z z@je2c*VuL?l6IVp+O4bK4w3GJ6HZC{PdYFyg(UfnZ(f}S59k|6#xchn6`zgTx_(h( z?q{nzgvT@Qxtbj0MzlgDBFh|Y;VfLyn-xkf3D!ZMPQjA+>W zgdMY}M}{w>{U`;g~`1mI-OXr?@9{JM;qr^zzm5~U|90){f3sIlfJa%qmlL=Yf5OUcaj#5fD z-*glFfj+5)@iKBW((DfOY(CnA zuSiE6aZI}PrawTe8=NLgp_bjULY<(djS!i;hLq4PLv`5P zuyP43$tv5Ok%_`s=VHW;$6m**NIPioOxL&`3wdJS@g$B#iI~x5a~Gc5T>3`fa>;|x z|H9D{MT-08H6f_PIEkF>k~m1NeFBX5Z4H~l*o&_CafmdDq(^n|nNZ1}D%*Kp%Mg1Z z`@8?7$vg&7I^(f{6o^i=61%-{oD^9aiq5@N5F0nGjaFIYtTJF`H-9(xgASUS4nl+8awHpjgo0g(m2A0`hwm}} z={ZbISfjpX`RY&+>qidR!*iQA-$wpCApQ`msidTD(4JLWG!rQi+hc4rE*fItdL4#| zPQ*&E+Pv`+8t);3BLX|&_YX}~ejkk$9qxlxoQ zv*~TWd+1?@r(=#gHVA`Z)JtV(uF2v#_aHIP@VS+sW?DuE$T_Nl7C_|$;!cu{b{W2ec;LG6FQP@*m z?wvp5S8gWLQS9}9Kj}uu#o@gcb?Qo=+D_I7E@xaFL&rvVz8nK8f}t{5;ShKZjrk7X z?4@YKhv5dMj*=!Gb|Vp>Ickl$hQcVXioCtAEAq)tMk%rb6H=(9>VggHHxZE!BjWFy z4w`XTy7b>al7^3@V;IAz=0F+uC7I1@dj2eQLgX#~3yj9=__yME+sKJG(gIY6!QU6g z(x^IMf39E2z50xwxa_h>JNPWF1@B89!B*Zc=}za-dT$N*j6)BJbH$n|iO%@%!XYcv z`krp~ihC%=qUc^%$P^cA`Kj;CebVzaE0_lr2{rH{OFFR<=W?FgN(rTKGi2|Y9Wi1A z>sd4_OCGzP191uhsaM(sdwx~A`IZ~Q30kmVUdX!&!xtrX@ty0)rO=5|61OYH+tNWB zvy2%xHqs~7{SO^JJgtQRuk%wzy2gsm7af1%a!MaFJk)vY^CQq{(D%V5P*qvcMzfDm zY~Jw@&sme>}|bYOsp-n?((H)>qF`ryeqyE37jP!<2Q*`}VC5f>q&=I3&R!hp1Lj z8dsk~cs(L{a@LN;T6$?8c;JD#!&&Jl*zDPeWYSk*po}#Az2vmjYc{48t2d!~hG>b5 z07ZK+9Nt4SLMI{z%9t8iN3Nb{^9Vganc*df-+K}P(1G#X1zAs|A&6)g^=**c92Z4_ zgpO6f`{+kMN_5*Jq-TxvHR@JG7_r+GZt=3E>BN&yN(&Y*iZL@UUmO9q`+R>^c(`|d z_U6y??=-I7k5O~s>^Wye+9(_VxgtS!a~v}JT_dbfkM+*aRaI)*>(u16?HZ49(E-qa zi)54exbg;6LNo{&g?Th z7n^a#!w=6olG^C~3^kfD7DD6E3C*Zj(F$u~OYh%fv;*tRy5ZS~g^h|Db)m&@3RF@n zMRexSjedk#5CtL`gej#}+1Ntn(*Drw!A7tp zrO$j{L4Mn7%!^0;-x=?}Ex(?fiCo&Jq0YDEePL8fLOUI&Kr>KCo%;KNxR;TLoWp)( z=0jmziZn!l0x0q#X;G5f(Z?Q}%M1B08}kA}&hw$6a?Fbe-rqUD<0ej{RBk#=S8@xw zFiid&$VIMM4~=moT>Airm+3@>-@NJST`=Y&HIv3Z^}ldyZ5R? za}@XeJ?8hu8-9-*>Lak*SEu1ohFFvKr|a5F&OSTU_gBB4x^C__Gx*E$SmO@cu?n4LbdGKje@j4!{4tyTAA3 z=j!~|zB&S(27Mo!c{a&iY^Wz)aNz~}OEwd7H9`nYHM?>JD>$>qqn~&fDWeBvL~{qI zN+Je>3jd_>-3Im-4O#phZ0pb%=fB-XF>J_9kh3Z(YNCWEI#Q7*+|BDG+!;05nW_@l zchaN-qQ15XxUsx~wxhhSXLWbSAFlS*Inf3MI($PE#*7vm}A3mhqie{u^k!Tx!lUb9IJMDh5}K)?u~B>n4N_dD2qZX!7t$XLTQfgrhlT}xzFZZ z?8$%oL{HCw*mDo1^_vl-Od`yk)be;02vHb#-Ou0#WP+m45@Sv`GxJ1t1l+x=3`C_5 zPG}R3XK^+&%3UNqxwt}jdF5=oo)AQ!dCDqX%WNKX)b>(Aikt^zS z=WVy8#q;N!CL|g8#L}TX5z;iGQ1?9>Dq-KOEN97-=Wvs3K@g**#d_=9*rm`# zFCr%+e91q>efSy|X9$bfJuGKu^M)LO<8!fea|HU_97PU4pRHqo+*8kAqy_fmhZ!TI z==SKV-X6v<*zdw1ny`S=Gn8%)y6_E(^hU??=O|ss83CSBt$z^rC>;~F?_Ne8U(J19 zV-)H++nAfE)mEVmBVPw8PY1$lI!|85c@K^R|F+wgN5{3$@g9VrokxwShR^GYE(o=I zFV<=M_a>Z-uFBDtM2s4Mpnd+1{Di-CZi+OF-$RkFl1RwvH#G9_e9SNOi7RPKQX9*DVeCCH%n^*b0h36jTu=+ptp%16u&|!{V=0y1INW~7y*XNyTD~4phOP<&JIm+Zp1)9{P4^=)mgcSp~ z@1`7}^VsJ{;JFHM_W6PQXBYsYo*t-4optJIXCA-Z$egb*>Kbb|zDVUoOq5aWlI5_e zN96LzkLG&bEIi{O6ztCtES8LHR}BAdh+ts|l-g|Hoi@|wdn*cFm1Po;Y&Jm@2i$M1 zZa1LoofvcpaYY1WbT8?u76?;vocC3TyhIczG45|Z_U2Ik=C}UUZ^#h(GaXOO@%uWT zb>)iXFo)q;D>K z?B9maY-R;bW<1PimzNB?QEzKoGb(Q|)=O)`bW1?QqSKsEXb?ushXJxPpKHbw zdhL?&)R1ey{hP)rH|SBz0K#PCwVynaMc(`A#RRN|+`qD_1R#{`R}cvu~x;tc3~$qja54(@l;1t%2@r zDjk|Wa6Bz*bj@b%_U#Z5x2Dmc6?N!1%$PnkedGi0PbVQHQ^9)Q3SrKXss^cvp6r>r zLfQ#V0M3>UbY)2rS!a}*_&!^8q5OfE*RRwVL2C3@snZbj!$e3J(o-=@9Mm0*LDD@ zhdnK^)pzVP;v|yh;;Q{wVpj!G&%x~tX)kKHd7lt_j?{OAT(7;`8xV{!H=Y}$Wd1-n zDtxIX7oYRo_6xpAR;;5!K+C{) zgF`K^HxPetEG+%B!4Zyy=Qn+|5fr0OpZ-({v?4}R!{=&4S#53a{T3}*QrBtF|Aqzg z0)6Rog*U!Hhv$X!SZwg>JWnG(H_5%#jm9X>%{8-#iL#zzC5zUiZgy_Sxpiue+}&_m z*pLd?xFh2{!-CeB+u$Wbw@a5T&T_3moU?`;cr&CqbG@~B(~b~x0Wrq@9{J+DDaR3k z_MbQC_-C1|Jy3f%OSyJ=BM3&p94~kbK2Of0>j33uBC^ekbj9BD?0xdWm>G>0^As#~ zBW)pD32+$U?3`)vv&McHKj(q@QPxy3p~4^+`EbAcIwQ{A7&)DeA}^iGu=3g{=CL6D z73qS2+e^`Sp-bPpci(UxYKTa4q_2eSUeTPVODjCnJQq$*5uq0j=pKio^u}z!SwtdB zCBj>APS4Ske78A?T10Oy|L=IBzlX3g3#($?u0X(`E|lHZKvhtd9a>$Zk4_;!S|RB+X*RWY~1^nNz|+fLQ` zc)iY14PEGbPfyC|bc}6Vyp@Ut5w4-bhEAM0^U(u0ZfP%CBmT2|6guzoIUa$|2>m%8 z(C6#v*j!27Rm>m{-g|RHE-NbXm2Pfs;D*5(^{|%#ckV4i%bR0O5gLfo>dU{_nR6A z=ouk|jCe!}fd}!KC4)5=Dt+qcEAYc}?yd2*?wQt_M0HO#PJix?7ro4VT|mr4(MIbr zn?X3s4wiffuV3WIoiBfO+$CidM$z+R!<-#2VREY$qK+PNq|~x}Eqiq5xhsr%$Miy+ zuN1M{cGPRTo2-K>ps?1okcdGMeqlhb-y=RPv?;b6Akdh4y} z-S2*PAP=aprlCUz$C!nq#svz_iuWZ^PrdhT^gg#jpf^r}4wVRqwePLu)2(mMIeiJK zAS9p#D$rF`P|itph~!WkwDjmv+h@+4In$Yin|X>2f4_y^d8Oyu2y{m1&$k)*Ke`__ zw7#&LrvSrtA?KDOT%+<*{@VyW?`nDTj=Z(ZDa&V+;AAt_J_xm&mvs}iGg@>=jm9N~ z1CcJh!$iQ9J~r^!ZU8&dPGQh*dQDGXhAl;nbitlqnS zm&9crG_Dofb?JT!;Pn4!;GWHC|3W@MBT{mic!C@ydKjT)Y1OP)R>0Rr#i8rYm`|EC zDcIpf&N`3q-lH&h1z#}NV3!QLX%sHyxw3IKuN{BPsS?hmRBtz;(7{7!&>|wE!Lw4K zln>XqSWf7CZXr+X5jk*9tbkx{=2Dt?pRz*^JflGQS&c4p7rT6zR`F5P8;EkpJTgx< z>3;efkEZ-lresY>GS4WJMhY$&2lzN>qx=YehL_NsF#~y1|un3e3e-B7wYv+4jI3v&*p}%nE@)@6m&9vLxx${r#)4SiOZCjh7nMl;N zqIQ+@j2SKsgB?v+*rEgG&RcGDsB#muMmC5~7-TlcAUC;TMt3$1M$V%KVXaVR^P~ZcbJM!^VX{5B0i;o;ZKVzLr{cmLl}be9!mz7i{SB zY=$!wo?EhJgKOlrde!PESql?ndKy?;G?TH1v{sspr@o}H{oOkd*Zyqc{)b5}u2GCfktUJ=DpF(zK!Wr=NRuNV(Jz+? z<~F@H3S#M{TPZ862Ik&E)0z5yeQ{#w3`n2&&F}|$A$(NZVSO{c%Ciz&fEDrjzDLG{&yVg zf8eFMH*DTIm5q6TaA`+kRbdeP9x!Mr^34!>hGz@)v4WgsPPb#8_sO!F*%63?-K_b4NJ5fVDfq+kfj#WktDC>LgGCglX(IudHdQAGzl& z-~$paHOl#0uUkBSk!I=m{n?zW<617>|J1cp82k8pXw`}!=NRzF%vlF)YTOM0p!3*QN1)T7 zzrbgnHl1CNh@8lWe>_a}a`S}+xycQokoXJ|RjF>gqrsQy?1mT3Cf&%$^C?Kvkp%Um zAq+!a3Z*Q&Db>0Mdziy~Zgv3JnA$S~IX19h*=I7H@0Z!@AsatOqxqb%A@BLO%lDrr zHqSO>?{gFe%-y_+wxk|KL|%+fVcZr(LJ0(-~f>5N!MWA;qTyCXcEuaUV4s-r1O zTS>`Pkz<#69@P*;mtjmW;#Mta5Tg+(zf<|IWB&Rd`LQaTn=t0W?hN@;;r|%75~;6v zPH_tTr+a8s>XQwl>zVrcZBz(os8=js8L0-F)o6@OJ?Mb-;O8*X$G$oOod*2{KJ)C+waZp4KeSKZ`jOk&tgCCx(b?HaV$|M^!;rd&%EkMH z*3ETVh9opu2y|F#mUEGN32Yr1?g7b`;{1Xo9}v7Ob?eWC=Z!{8(X%0G@QvnM=^5_E zD=c4O!w>#$lpfc`((I|Hj`uyaANJEtqK2>ddj3pu$B4QZx2>BSQUi)%g=mN1Z9UUg`K}WSPj3p7fxU~Z5aecQSqZ(-@7T|na&E9!;tg)p_ zmq%%(&S*2QQ3oK@?^vH((V+Kd8x6@T>B8&Hja$QC4ScS=tx@@a5|L5EU&Ci_GqSl$ zmMjZrK?g}T{D={xQaD`Wg{Y99gDj%t8ZPl#q69IQ0tN}OG{}~e_U+xb2k61rKmF<6 z{{N`4@4W6megrxV`agaS{!i}!n*MA<*Qb$J*cL-=LDel*igEVFWr+zWOuW(l$)I-IFBNt$a)+mqbC|qLON)2y@`K@)&2G3Up zK`%&od)uJq>+i8E|5P&>_g)f@vCEq5ai6g}UC6Blee$zve#wr*k~3 zzlUd;*Jr+me6JnH!!gxS)u`Tm_uWB!vtRp`hHde)d2UE&fmmF7?X@va!3p8@=E(i7 zVb|I5_0>2-=F!)!-H=-oV{q+|X8pd6YIM|u@>TS^=9kyTxs=CkLJiO(ddpieK$$@{ z^jQ>WkK-KW2YD@+XD@Lh)6}WchcPb)@LBJK-JjKB`R9LAr$PVcpTlQ-0ye4McieHu z*+(CJ^uYyzMpB#|o~%hW4;TFAz`wIptNWh@HIw1o(6fMx#=M=4Df4r3)RUp_*lL16 z7gpXsm*Tbf9wH_g+AlVx*sw|vrxCvI_xm}C9$_mcZv z?=|YrY&g8pe{8p|Lc!hu@=MU=>S!C%th~ULSu}whJI0JKXc~HaGXLmm(sy3pIb$}eKiq#?=)@d zjP$B=&P{c_dPnmVBX;Y^J%>5=Ee%`ZT=F4oh~xMbJ?4ejZF*ztS&jVi6-%Q(`k1k! z)7#(v_B3zq{D9!CXD>cKMgRA#M-U&oSOYl?lRvi+&GQ_RD!KPcIu_QiTD7|N|9Y%D z&;R@!flh<|{G6BP)_7{**;mXEA9=6UyOVhii%2)+U}tSU-=0xnmXj_6!?bFbo=M!#!X zv}j?}TMNr8TN)Cg7C4s>g8aK|z?IVUe6rxZw;kK_-?Aa;xUBv8`A44fK9yTlOk~lb z#ku~Pxp90H6v>7;=eLD`)w$2)EjS0IQ8#T8B~B$PzV~}J>>cCgJ%+t_ZAqnW&qC9Y z3qw(Y+$q`e-vv^z*C^&3uEu6Hwev>#qB}*U!#|c7w>JVWp3CJR+LIdwIW8T?E9b3? zbuLo!_^ij$xN+lR@FXIddhpp=Ah?y2eyAc3E@5!Q$kFMZ@3-Inqh`%|JSz}&9{bz~bQ<&*=#;m%?W|#Qn9{p<@9{=h zwYAly%`6LP#AbjI?xvC(n?ykEg4?A{J>WdXMgtv?d5NTx;r6|ddd^Mnh1~OSmWWoo zc5gG63?F6%xE!(jJ*yhX0ssI7L`g(JRF~r9jd;O=`SBW~y)tBDb4h%WCoied#kEVV zFiW675zX#>ZN7&m8}@(vM7!Y@1hm3wk}S0af|W-$$XT(?pCvE)@1k5Pq*G;Pm;NiB zs4(Ou8I`>@x(L_fH<=^hTo({@am|jT#v)HEi@TU3@3qL?W+Rn}b8FVDiF4&S%WI$` z67%;R@u#lAy;twZFX$Mdd(Wz}$zPDMzL?tjVk}I7tX;cykDh)vcmopHG_L*n+dVBS z=HJlD$RQni0D5+pk}r$lHqusG|6y6ONDfj~h2CJ99dG zT+4A5j<>_x-lIg8W9&oH3CCJT(9aR1yn0eJ_0*?#?(Kiy1^B$UTIZxcFQ(zSzX>*y z-fSr2fkw+Li)o@~x$t>J3wC}gv1JQr)Xhz({mJB{ENCJ7StFhy>ugwy4g9GPcLCey z*X7#f0wz>;)XgqO)+o(!FGlul?JePKxPiKXY2<<%#_QZ5Bj0WbPYBUy`ARwU4v}}1 zU4^lK?#J@EJY$gujSZM*(kSM9X?9dJ97T~3IV@4awRLT#>O)C)(Uc-H4~{GRQ(o{2Qr zN{@37$JkOciEzDo*2db?5X&dfaBCk`af4Wp8XM`u0yvt2Cz#P91g=>No%_m^&+4WlmxcruYznI&PxhSHOkjYYoG zs5q10imct<+HL5DZWhlKcmW5pG9*EcTlvw0p_DlKL=yX*wJav6~?}(=iAfYaJ~mP&F{Q z3N7<1f}BS*zBhlKo9oEDH{ujF&L_4;I&&?G>pqhK{ri2~0$=iu;yF5^5XC*nfY(sU zTL1m;e;?m1J8N!f4CJBcbx>JR9p{MdgfT!}Q%#Sow&=5Of(X}y(KQE7zKC-S4I@rX zc$`0kdP;&lj;m6-y?W8$1!F#W@+2JF=0It3eK~#8qX$Cng-}TjKjtXNE21kg$}v|3 zXCTr8H9A-uMB-}Ju3O&+$1sx`I*)y71Ue1+3vjxrH?R2l&wqaQm@#7x6w(gKRScZu z8FRLI56&BXkbuJg?v3~g*ff*SvKm&7zQpN^E@#<12%7wy3Mr!g$ejZR@9m?hJ_l;4~W|L)q< zpiB6SQb5*uRs!sr`q3t}6^UI-+>F%Cuj|O=&usooHG=@;6jkOfEaZ2$lO07*qoM6N<$f}8YEaj)iqU9 z^I=!bn%%3Xd#@FyC@+Zwj}H$30Fb1m#FYU6Fu(sUSm>`O1w)P905AYRT3kfcUH@DU z#!$^{Hj{$6lPhl8>q-Vq1gzFjdSs|34A%bwXJ8ILf5m$Tmo^iva&*4JrTRDJ1AV79-Y5cK>gqN_=hbeE31>9mZtr@ z?lcwodnQjpZ>!7O;v-UFh^Vd6?|-7FN;Bv8Qk7FE5sRaMoQnueZQ*wl;b zLH(n^|9e|s6$Ji|=e;4c+m$w#vlaUf$-T47x6Afw#P0I<^sG1m>)-xV&3?rr&6Zr2 zj5R2l+Y!uN-0f~xn_v^^TvNR}x{7*wg!NW)MN-hg-rLvbq?!ol7}&7u)KPszL5;!~ zIynm2f^)xr{W3imO%$V&&se^BIm*7exN!%Tz*t-F>a9#@A%JWcQLmc?9KE&|!MjcT z^P_lw*>wB7uLgoOV4r8#H`%OVd+*0eb}6Z<&adK6eIX@0AS(RG^z8fFe=l0-BY1Un z)rqO?IM2jBZjh3ava-Ca3>IP~`3nmGgCT@pC(_53HVHX-zSz^{MsprO7lulaA6)oX zz4-*2f$#f6iWo9a!TuEYjgD>Sg?I)3YI36503_tR!{7zVG@H1goMXMY4m8U7=ibUu zM)TtoH+DP9Z+}9@|CXlyZ)sj$Pj7wwO}~tcjD5FFk0b0J5n>cvmL7Jaz93oqSrpEP zFrk-u_BSJg2_evS=NVUp0hjn`K)~haR;T?dL*qrqBWS(e9~~1@6$2e#%NZm+w6)Ue z9g6k-dao~NeqQS#=M8lA`oCZ86BnW5epWW$|K&(NllP%7l7I`jN0bEretK!O$>z;O z1#c(QZS#!Zvin^qEDV;h@B7*P7=|j&jFFDY><#O5d)Ve*_A@%Jv%8YO$A7p{&uC zH*r3z8K(e4UZ zjSVE%>z6rlNfY8Ay4y)fNsSnV5Lr4pMB)4G-oUNSv#R#Vj@was-{s7B-}j4VpQkX-=6yKuf zGydMNN=mC^TJ*SCX;B@wWL~>SNKes$s|Q&aVs+` zh9YqNyxMrC5PbJ2?uJjn`bJ0|djZ|&5h@+o3_sb8A4LS`30N0Y2IY_pm0tBB<6?sE zfH+T3Zb@i@$Z($rlhc_fWvOIW(ufC=yX8QC$Xyygc&Bx)UQWxaFPDSm)|xdDKt)j2 zfEuph&)*3A5A!Oaq7cXJ&6}RGo)4#q5zw_YEAEbPpklE48CJyI~iY)EL&4C^h^hoWNyizs5w`p--pQqp%!p-DBr zq{tiM3S@9m{$0lLq&$f&sg=I!L7f^o4M>sREO^KO80xST7*HFh zv!0R$q0Rz5avPaQEPxV-A-Cr3cKUx0iv(I+BaO%S&IvLEtDh7#p z9@!?8d0_o+Yrb|upw0HH)H?dN`m0)Odq%@Xzw^%P&rNWPZ0|?!cxnY~ZWosi;KS9{ zR=w-RDl1nr6PaEh?2H^5zq1;G5Yqp#5mf z^uAkRb>~g@sCo^hXElHxj@wmK3Y0D?Xowv)MZXzN1_vJ*Q$SuFH($#oWv`09Lxz&^ zxv!M}W8?1g!*~_LS{Y#(FkNuiK(L0xXg?>%B?pI$%p}69fns;L-r9yPEaDC;zI$qv zBk1iZQ15fm7SWHcEmf~_6<;D48eZMUfmmO1*2iH>0!t=;wf#=Ti`ojqp!)gxK2%Pc zlVYgcjnX4*`|fvDD&fCU7)?eN<~~Mg{%Jya@i&yRzy+le&oJ1D0D7~dn^_;i>*re$ zl+P#f23PbNQDS3{t=4u${bXAvnBi)J)hVdAH2J@}=Dgklr!C%jciA4otpCNa+t)PG z_)eWc->(>45-H(NrfV_mG~kQxjC-4Ire1$9Lf|hzx}E_ZCW4}yB+XjkSvZs-Nv*T! z5&J}G;H7M=a)T(EVz7_+nPvk2oqAz2YCF?uDqy(zZardriX$xJ_XypTeL798L@f9n z`NI&Q5-;<`gkp1O&cs?A9O4Y?*mb0)vArsX;Q`x31^s*0rB9qQc_$7-15qlV<^#zX zA;e6cLwvUg&$l`e*P$TbGdpejz3EP#GyROW3ohh+u;?jiX{C3PBqEYW zwTiPyB+wWKwjNl*PJw>UWx`h!->3G8HWQ^N5u0m zKVxQjx@c>!=8cb!`@CI0)Pz3bX!k4v zK1BD{^gds(TMpZPGy6O&Bp;;WWG7A%@{NE6LXXiFXEC{LJu|O7|BJLAGTl+Ewm7Qa zgP89n&>Xf|cg*b0lW_iEfTnkXX;75A&m0;bIVN~QI4>ElqDO&&Uq#bjO>J!8_1{}C z5oIyaBdJ^KowYunu*SyV{_KJUzxxErt_gg65sn7HH?-U|y0(;2ajDjc`o(I|T3YI- zI5f&i@G9@iQme%<_1W*j!IZNBgETu1uj7vQ#_$&y9z^0Lf3836sfa=`+DKLLM;2f( zw3tD{+t_Xd7U$vbd+e`ztUka6BTiu+I7x0>4TX%io$x+LN=RBoi3l7ZqlAinQrfNY z$wKi{u`CWVkAp_D#n#hQjh`MEh`p*_Kz0Z`S>;B}n z?$wPLF&f`-awTPG8c=7~z)~KANP_MDEBPhg*9lydBtTNw1_|UE*~bDRJI4?fAaQ+o z1>8;3-aYwzKTjKy&*aoVk4*2uul?hC%)a)*Nl`IiGGmkOyHsnDQ>MEcl0oJGlRf4E z0e<8wdbQcfC^0VxxAs3pRt%KSaQ%W~kU51B_4AA-JNi@9w&TXL5@H`~e=I-(;h^ly zE)I07)ky5c#ikd`^owdfN2QzTI?kZW4en z8V3avvQV*%lr{U!Ha!iSJOqT$(8cS_dSS0$u9U_%paZ2JRxmx|y^h9kZq};^0wtyz z&b~8wAfBpD0wo+w7Aah)UXhMwo#{v^3^DL}@3REKR^gG0(U3Y|gsrtX!Wdcf)?eX#8Y zbAN3A(S$3j{9$asnE3lh%HtJSTKXX6(Ri|twB8{OGP5N6y@dltCew#X9UvxvK2uRR z;4B~&&_{BQ9ey)Vf;X?82*xN%lEd!C_AHE<;${l{z8cJCP2Tq-3P6a1oxuuWrsQiC zHGm|D=3gWuQ`AK^SFF(en!=jz)b$KXfBRIz*~~hZXEgi`c>MP)sgsJ1~@*wbOGnrH@#1gv8b_1H{VA;g9H=Hw$?bR_%U>g&J zuO(DS)PusP!zp%qkhdHv7q+s7AUpOWKcku@`KmSuR&EI zW?v!1XF8fV_fYYu`@Lx`bjxWf>&DLHi;+IQCyx?Ys%Yl8q`PL@ZZ@1e zH<(ntlh`h6sr>$3CNMZ>&?)5muboxIMa^*~QDa2QB7+u}h@3Qc=eDP_c?^a)I+=wg zfC{M_6{&?ZPUt|bbeY>=l>&M!=hDjYq4do}0+p;1y^pd`En|LXOPkLSy*g6cUpd}@ zsZe4ePD_>cY_|U3?M26QwPT_VsFMg8#1((MNsh{JOWB}B9cDR)BWAv85{ydKcpyO% zc3L4d_y~s;8>EebLd$UQ3%QjOq4{_npk-AJ4te27y-q)v!s?gowjOkYHblthdgNSO z#L~3!Ptfi^@_mstMX8}>WU;@_kv#g~I9fFi)e~f_dz)@8yz9o@dwOewHe*cgyFVC( z4v&hAz0_-mb9SMi=hHYK508MSn4W>i<6d9hAw2NKIcL`6&C6vVg3!G#yXzgx>t9BY zG*G!n{&S29hlrr0-z_vvEwXUmH(>QM$v{nuUWj=tp;#z`ersxjE?CVx0Jk<{J86T` zV|XNP`VMuZ>_&#h2PXu86(q`B`=R~4gww~nmuf)AnqN zNFMw5IMZx#m?(v7{{i(I-n75T_+*_|iMJp`#!?CuenP|%RMRwDDo55t9Ml6i??xx5 z(0U$(H~Q@wru(*U{o*-kPFvkm*29yP8z?zevPo_xEfv7U$^9=d$4 zWj@^Fu|vcLH)}Ro?)*tM@ZGDco88^lHpnclX(>rOdrqkAH)jx~iek5B*y_l1W4zq>4Z@86-%7*GohDdS~xR+Orw8A;$ga~dXf5V`fQR-#$w5P}G^!%-OE6y5ARDv&66!uMuv$Z@H#wAP}T0UxOG`n0|N^E>d||M zzvpiBY}Nq^@Z+vNw*s3F56n@tz9`AZK2D_eW&|A#kCIWxEkVgdAugc-d$erm+KpoY zTAE^>KT$u7VKaiKGow*QOONqPy*kFs$wF5C@%u`HYb)md5pO6VjnTxalx2ID;aCQV z5o4Rts=6S{hg!%dij-7omi07z0C{yOU?86SW7PPDjK#fv$yivn=y{Zs=!jp==K>P? zZ{QANLCOz5D(#3Lbzf5*+G=s7WgY(>6-vTcV>8Vwj@I)5B3NiE^q+Qr@malKCcB@! zJnq&w(f$2zu>Aax<$lJ&@VX>^%&;nhTXfU$OB@b0#o5KTjTFV;u3%^R!I>p$V$Dm_ zbZL@r%NVsM=hKb$oBEBufJkYzc%{<{3mC>mMlTzitMWH4yMF(d{2RaTqWRfqT!;kE zhKMJhZ_DlE{mD^s*{_^>qyY6FJ{Xy?x1L~GSx%6e9m#6nw{)2oiR zuh|K5p7Suw>opiP`N4*V*_&-*CF(7PkFL6z#tx@?x3&{#cL3FRb5AtVg7?NToI4YB3rA9Ee$K3fF;?A8gjb^f8e@W$lCbyQ?gQ>ZY$)H&`+U5&Kb(WvW^tp39=6b`tL>%--m18fkvrmXGTpSOp!8 zEkGL|Vz?vmLr?ZqNZFY8)^K~}kmeSGsagI2ViT5BWt0RP-fL&PnDI!KGFCMrIvcqc^Q&J(sXjW?LK=TF zp4B^<>PQ4P4AC8p5P9!Uo0ls8Rf~c-!tpm$JcLUO*k(y3T2eBnYKZ5Vu_;JNQ2pi= zqml=CM0$#R1sm2#$e~KXjt7@x>&-{b2d*+BMz^CO5?J!&DtU zgIqVhTWyqUGWPWEpr)2;?j$P3j?poMAq*?z+MiF14_b!df8jYYwtN;st#7mbr!_cL z;|U@-K6LsS7FW;W9O*BnYSOirXMO4y`y-d$n5VS8;gpDL*FCk8B!AR)S{B6%h6@Im z*jQ`hEB=Tk*Y$~MURds>v=T29$Gm(HDu?)lkcy!IT^7fQnb)5#F}=jS#=d4==Zs^k z7iCbB+p4{N94k0}~+`62CL)$(i2Z#rR&5@CcdNxNT z&;L=B4i{7G5l+F!^sF@tll_yJOyn(`)&2Q>;`l5^DYW^5o^jG=z-qehS3r66uC6dg z6cA#Ec%vN;QS7`68##?_{2S?rA^O16ad+=4?Is=8nYql%Yb!o1EDThrxD;+%cpw@W zID|kPl6#q%@gD~FJxu8+nE4<-*A;J-{nnc?7X_Q?!f(OGF5ge_Jt!sE*hsaqexM$s z;xS2ZGbJWWSM!Dql)2Opf;(D%Z(DRpR~ zZ8*;{4hcPPzwe!2ynVGZNPy43XB`?-w~uSK0(kDg{dLL+N#Mg^GPQ|24}aEPKBt7O z{qW(E>I5WYfHe=Q?;TiP4}1?0AU{24RU-ZXem~dN+($|3#~P#zhpDPkY=_G~yeH?N zZV+;3t7x12Px@HWIkBf38lVnV3R50zc#?olYJ-aU+9V&-B#nj+4)XQzAO0lr7QMw$ zNE)6Dq^R37gbSA$*x_@5;&yO(fyK{2FvJwi6uD-=)bJoZQw;AHM=fC(no@(MC=n25 z0w2b%dpEp?*mep|RqnD4QMFAeEJw>ifDjQEr#OXgL`Uit`pDX;Gk8)Rh!oap;8%HU zzsAr@VX>@Xa@d{@q+sld1l|s=XBCg_0PC%v?|O!utmsrpMOUK}ES~ z>sq|zcnGBig0ltGh`aaNiULN{o+~gXUjHPOd%qkc^XWOyNDw@o%9(I=Dg2l0-n(od zZ{*!Tl+B2(7EY&SC+1F|q^tT5K7&H8O#=uZo|`g5hR#+B?g=g-H4%5Nhr|n8cL)46 z^l|*~b|ghztduQ3VrHtHX?FjN909o?rs_MEhu@5d17r|7pv?r=D$U08j077A-S$x7 z*esOKeTY^U-&VL^RgbV^nXHG3&h^!EhoZlh?Ktim<3wJ+=}40(ZKbdg7bKpyJtmts zVu%BHyw758QW|ZneYU2^G+ui(;1UKcWHc8f(C+tpOt!G944u=T4yb0#DZ(V!TS8J3 z##UfGfuh`K7Fz{t*^gbhHpVkVOsWxC7H2+TQc#3DPSoK$9+^FsEMzN^BTCJpfkor$ z#(yGj1=HKQfavG{0G~P)^j|ggtP^Z@S~Tf4&9z_Sc<1 z8*>q56CMSrgbw9LGZJ|1Z!=}eBnBPPng%<=S#n}G-1H`5~Cf4rtsy5pjc@$1CNFI?`J)m`X(zZ*r4*utZr-VRK=B@I#k5Lo5EA z2zFb}EU#~#e@izMqGqaEBYG;*#DPH7@fZ=wL?4519lh&} zhF;gXp$&RGo)bs3Gw_YqoP7kz=%?D5aY!8qgqPOazNy$Q8CnGR`0BX!5&F`1Gr4+O z<@dRE<=YpGGLiW*wQ^dWCTpI3V(0Wva)pJjg0*kQ ze}%YV2lREBU;PviSCixDHQ(x7!lOpg<(^}yYXL$vA@%C+eLt(&C*)hOzaM(Ma$yDPJ1p%+!QhKR z?lnSXE^YNl63^a2Lc`&$ayFP>*WVW1m`jY_vD!xM2oX4jd>Y6$U`4~G!UeruGZDks zkPuy#*;an%Pt)_|T8(I8p%3Y|KcLUX|5(_!`ocDIWynC@_WeY|K>v0{k~l*xK6`}? z>pUhuPQ_F;Lfz4E?0bwb^xnD(wa(b+K_gPRnXP&fP%FS1OP`LFhxWb1B+4sSHXZx! z(`hJki`e^qruu7cr*Y&Kis1>GP@wE)QYOl}t~X6q{cVCl$ZznFkG7Ko4lW#N%>k*G zS@Wt8FUL;5>F)?B7(?ph>6xGilaiuYkcT0zE!Y5toL3@R!tBhY#VeE3qCrhT+%( zv}7sw=s-#~S~)r#Z~Am0U-6H`Br1hRJlI9d{W7$5O7V8Eu7~4& z)2pMAV7oS5PG2|eUuxwDXTKjcd>_A}hJXl^c~p0Kj<(jMwts2NY8g1!_kQzccRjzU zem^>uZH`Cf{<=r;6*O9ZfIt!aN#yfkqosA6Slp#gW2W!CPF6B~(h}9~z-VZ({O)Rk z?C_e=XXboGIKweSacL<5jk@Gf{)n`TSnt4l*fVW8H#2r|Ob6i)71fPQk77I5@$IJJ zhkXwKZF*7ZraK)XTvF-B z35)Qn4B-n2wLZ;S*fFGFA}__YC~)w)LlM-8op6bZod z1c}Lu&fo=WTk!G8@5~|+vA{t{os6C+BO+z)=U2tn6g2TNGW=fOIEJiB2oj-|ntWo; zyK6=dJ_o-M`~Ce>|g)61V_w-PxozWyEhGdNEH; z$n6PlD-JC-VVD+dZgZ2GvP@}8MHUhXN zNlsZ_c6ZR9HObwHmAvfeu&}vO<3XNu%DR4kboOUwj;!3_sHc!dOXgfw={UhRu~Np7 zcok1Vl4LXHB>mv@0z@srB{Y9|_Hp`nBF|XT+sn^Y?ZEw4yij*xUsh(9I=^tSK<(Cy-vcd6kpFNcP?P^4c8DN6$~+Z zS-lW)yFd4IrZ1S29&x(BiwVmDH>5FKNQ(@5lo^!sjV96?Ox==%~{*6LR{#{|RV0`!IqRfnvMX-4}F&k2`ZylV7~UTOFhZz@JNNj&z5 z<8Ey%5&N%HlzWec!l|ga4AP=ok%n+8CENp~bFCOzqq!yMlH}tvKohfFTSkuNH&i?p zW6|g+?q#Y+1i0#02BQX!|CHSgM|&Q6QNnqU3W<5D2g#_Q^Nu+CX0Y(ONE;BtiZ{Yc|{}0!Uk>7?i(_&(Z1R@R#25SD^9PFd%oClU| z;9euWH!g#F+!~2tJ*NdbW|Kxk3;!m^ce+B46lQq+L(q1rI!3CcdZVw-o<|Hb+-TbZ zFhUI&b}pa+vvf1&9OjRtG`pdcO$n!w2nDA7qBl>( z+~TDk;Y4}vf1{BIlyPvbNgbFPZX@F=(jXSoqd}CM`~2(1 zC~uLoHbtau&Q8&;NvQl#qRiw`)zADMv`xlGNTEasEklBT{JhCq@vE-~_Lc-z)mF>@ z7MVAGCJ+sFacX2m_F(icF=bwz>TOM@YF%>83EMn>P14Mr@tzhD?oUaE6za2H`L>gP zXG4O|)8rVgN2@DOm+k+7y4CsJ%h}Y8|K;?${nM%6%ceYH_VRQ^LEd&l9>D`@duB+K zu?}DUB}z^9;3bmaRQsIE`mvI0-EqXMn5fm+&F%3^3fI_jUN^RQ#bs)G+{xTQpF1oX zD3X%hz@ei5LaLW13g^Ua5P6*0-cH=*D@pUO5~^k{DJCLkT8HJqhBoCk4*R#5j_4{8 zo~NQ}MUbJ7Fs}XQA11MZ=sOg^0-Im9k`fm;P+8fC*q-jB>f!n{cov_>C)sEKKTv0? z1jW*R`a}5m^I1vTjIcM>aI<#LLt%0QiLA{)R5B>WJ|&?Y7M!BtVk2EKIi>W$y#KPd z)es9~rLH;1jR)5y^ic7S3ArBnN9LbIgv6f?LC0V;A-q=Ex8Hc|E}GeV9+Tt1FxJWhgyl_HWHHL`u;Ab;{!m;DF;6QT$pd)6{fTCcH;vPKM_?0jr$ zmj|9Vm0O&T2yXd&%@^yZcH=xJYgNVJRp~@5N)NXoGtHEH64~7 zD?<|QGE%e0NKZ+Z=yM?gbr>PxY|S}=e$^%DSQoHg)T)dQ=}*#XGy(77x(9u-+B(+! zbvA^*0hv5+yUX14)sOhzKv{-w7y!4*sXN?{7OJ1(%8p@R5aVW!(HVml+?aW9LJQugE&AIJ1(gL@!u z>|i1L&1V$7T! ziNRx+bcUdOp30^U;igU(2$IT$QB-B+ipzc}75CkA*`M94=*^$Fa8VKQ+Pdr)Dm@mwm31$_lZ}$;VNs?{%NrC9`g^-P*_>csX%-jg`({{qR+%E+fwjalP3dHM) zCe2qXZj9Gs9OhdB>RzUNwLPUry1;kYX1(W;?pgFaUtZwfZ+{O-WT5JEl5z2=;%8_* zdm8Kx4tCQrbcnp(YlKcge6Hu*wM9i#H%TyAu}lJQd0Mc+it?=ip%mh=*bo-`!id)E zWLu!<4vs+A6v6eV(7NeZ2i*C(A8h*Vx@0jcO&z&{_gWs@N22ZS>nG1$>U^pI<751S_z4-WelK_uuJ3AMD1m1U z8&?g9I$Y0;Z}YxYBA`E=ucuV$T{rlNL;7&Wi2&=q$dxv9z!0y z;4XC78l@dJ+$mphqFD{D>f~~4rWn^Zlo!i`4y|I4l1U^k>+VjeO0mB0d#A$K$kC@x zI15Q|c#3m^K8PB><1K8A?O^caHzgIK@&K~!#iiTZ`O3j9fmPr|o!RXrEs6*nG>n1< z41#oU%&5-$D7&^lyl2pL4E^-#mgL;VI*KGje0BZVvi#tqBX3Q*l}1qc3Tu!T4zR4h8G?s zy{c?NuMpqNYz3J6OdJA-WSqCiDPS{4!N-Yzxhp5u%GeO};KPIS9Z}x*As%wQBE?lH z#pWO_jWz`E<3%W?eQBogVnD}&*2^`sdd*GVM_s01?cmGFRsWcX#-8n{nsI%EyBRB+ zs?ONm21p(0_6cNS9#qz-zTxRvTh^?%nda(tSc+U+*H?8k&}Mv6ChIyfWqytvedx&Z zKN;i@VlPdta|TNz3WWAwEzyafvj&&LmnDZ(8p9{Q=Fz0zmASPsSS%KkQqi<{^6>5@ z%co2dzb2J+2a+B-E8YYu#O<>#@k|n5+3VITTI5)Yd6Gr@w$oG(5lL7*9muxu2WHG+TYp`d*AMOx10Qa+w|`L?|6n_ z$tXE$H?5tB@9~DO>is@Ly)rQCV#9#v-FaYKPAnGBIF{LB@?E583ECq1?Rw7E1VyOC zKKVO{z%Gr=Or~czmb{$T?Tqktdu@-Bl?m_8D%W;IYR{96dSNk$00tw3gaxVI+8{fX zX(2}|#0dbO$fjk#e^0M=Xx+f3c=hjsN7PO}w3-MzK$67#D5)xqHkEjti8jAFp~c8T zG=ovLkVta?)X#iyLC`V+R^Y1Cwwo+x)ybwl+C zqlYwI^d5O$;e5R?_hYpQ7r~MbC2cJn8%WBtcd23Z=lESA=9sBJkr1oL{~@JSx9yz1 zbXe2y@Eeyo$_}pRu8w82IMQ}r1NMa3$u1?E$1MfVLBgCc;lpst2aFUQO##egDsqul z<jlF`kxlsMne?o}O<{RS8{@|r8r$zec^^Zlujkd8&!ZsPA8m+Dr zHObsoD}i~2=oh21{f5)dn{tP}*T2VWP150xLxjG&-g|;?`|2FYBTCd6jDwU==F2b0 zx*n9L+!m$07Q-?p`(kL2r-}%g4UJMo6%$Dhse^H9%3%OebL7q7IAPe#s8KZXH}NUI z^5YRIX2$2DsrbrjJ%oujCrXETC_mP4IjRapbju>TUrk3;AHjhOxg5O@z&C?4{qOIP zALlBH+@CNgR zwitB#uxKxwRZkyK3Qlg%y`;(+C@Q`%zB<^wt^vao+fS~>$Z@4 zdn#(^x!UTKYvpRM+-T}q%Z@s8UW6N}Y^!uC?(u4S6bYsJhYAiu)lUTvxD>zCT`?s#F^*6D1wZeE@@kv&M2R@MmKhA^*(Hyb6KAM(DibeU?0;! zSDo;d#2nku(D0I)n)-I-|8gPu0sf^jzsAA!)^K}y7_h30;s-v5)N40B^3SzrtAalo z9sVSILe*`>zn+bLGm2xdnsE}y{QNS!FhFoDjIGsQfn`Z73ripsPh#rmh3s3 z!k@ZT;dYN`r&ityd?Uw$v^WYj#6McFmFQo-t)x`e)`B>g{=BSj zWCT_5cXxJFDdIewwkaM@1u;hwRNkHz!T9zx&uuc;ij;nOb80pL$L9-|`OCZ^Sws=8 z^TxzW;Vj`?Vmw*@%?GbdhRSA_r*8(q|}xM=nG`;2}ZRVGzr4zeMTwBEiZLylsm7`l-3eI^pB}&9~98 zHz5B@+x|%C+xTT9c`mQ+sXq7JML~#<#`V4)#mFbeFu3G{1^JB9*;6rP2xX8AAnhk) z`y>$Osj!*<%$W7od}^Tb_jczlEiIL$>-^AX2J%X1H`$U~f@bqU0&<@QbvjK~9cm?g z4yr`%^1KPC?vlfEJ*D$`r4{NjJh=#ByTq-R;Hg~i^j5wXZ`WC9=n-MD7jY~JP_x(z z&%zRs26X;9OV59M^i2MULoEZSIZI-cJ0j&*AVYb1`C)8XQIW50F?pRtLK#3F0jF>e zpr?BDwKnqSYs)zJo02=^+h><7R8f$I#Gd2!8SEkB$xmccTZmxx-3a4S?IQb#H4 ztYy)_i*Q_pzs~C1)M+m(=iQ<56!E?v_w!hEc~DX%R?ajB7rMkh8{up}lNv-u8CFq= zrWZ;p%E-bVyinM!5-8u#Ny<Z%8 zrhk%-HZxGmP6|A&ziqkiMe01w42=;b{iknzoY5hrD4aNCGHb2uB6j`52K+^1;|G(O zYDUvBCN1HJaTl3Z2>FWKuJGOTV?z!d~!doJZ z*YNoDMCx=3scYq~#WM7Sa%i3Ol8&&yD#jb4Ft$!z?<1rRF<-H! ztI4!gJswvnfrXEVH1Wu#j~rj-ct*E!`?|m_RMVnq(&Lvlu=Dvn(h%{s1$&2a?c}Gw zA*jlJawzWZVDI$V4cO}9|5cN{N5AaZZo?3t?Vma6cAaLPZ$aW_o;D-QF$m23pHUk6 z9tW4+_h|K@g$h~T7f#r?UawGYre~16Er>)R@po(aUm%kn$biq)19<`VkiHpvxPFdn z*l0s}KSi*z)*I2hxByz8DuqXEeFbJmN^v?$Z4*%1$67tf5GPGvR|s1UXKVK+dd*y& zgniVzT)N!x)j@XYIru<3Ph!a9elVLTA;W|ZaMT~#G!~&@CL$&-Bvaarmj571SB$xv zO+->>3<~_``#tw)c`sVe=kxt3y5%5Arz;v30p)&Mkdc-)_G@zXu`WsEu|HANCVAnS zwg@gKoM*cV@mg$m5%LWA`Bxl4P!zz8G|Wd6Th0b2KfWeFDGWmQxoFP-Qsui;~Sp*`yq7 zMHK?SEU3N=oPYn|>RL_YC~SObY>N~y-TV4YQYWm^{NUh>_J=cJ*iAT$CC$ra{SyTP zS;i$1)GsMIhz}zZ9aIftSPD>Hw1xE{10?S+QQ`ZgBIq8LU^Q@c6o&h>Rj7}UJM+b@ z@2sNOW|%S}TCKFNY~Oc$Gsbns+hD7}&L~U5Q+T0-B1D0llbo!)s@leF1ZNoSb!;k( zm9qliplV_8FF(lqaUF1-ZS8px6B=ws0wcy=e8*(@=L`ha z)}_bZP~vCiW|$+PhfcT6i!ggtMJR1Y3e&b7(3bmd81)a(T=tpw1IPxj99lI#K+BG< z-*Q#NWp_2Jh8zlS!dgeqi};02@4h?Ks^F(i^Q!Nqjey~ts2#`Z#X2>uf2(5hE-`aP!B@TVmFQFAHoC$3`=q)7@w=9ZB_8rWNO z0zRWH9XIj{gdX(q6c`KYiZrMh+wj5S&hC!uBb}SS8-Q~1R>BE+6!{cn0yKR*oM3yP zM{Mvvxr1j7MYRnnD!0EZcFB94-WQRz#h*}$iL zHh@fI(BC@HMx55?EImgh;}Ps_Jk7Aw>5*ZItu`8NQ|RXgxN{(;VOSzDbSSJbR0(4k zp;4w|gwUmoNr4~NtQz!^rR$+`EU45=zvMvW_?2LCvx>4Xog$b|iHR?w3PIVW+6Ih! zhW}0)9{UiqAfq1znTNe6b_IRF7578`ugOvC)pX^ButH&BA?@Y6!)vNV1?ch2ZWV%T zw6vrI;7=l6)%AKhlI`Uv%bqJf6Fl`%HMn?iJEX zjhG%Nk?^~A1&OeGL0!eq(K{y!3SJ~y?jM3bD5Xrx1wB9Jx-)YmOg`2SsEG-2BCNaQ zyPltsTDPxz9UpgKhRNfOAJOp>9I;<_ANKZ6Z z&rVEy_UTAy>LdZw?Y+ydhU%qa3Ups=}$AHe7Q5lli5j_r( zkkpQ5fIR+Qow-^H!TMpHfXC)7D!^)?LYvTnuo{Z#dZ2Om^}Pp0q0-y-+Hk%I~7?Lbwe8u90qXj`nA&?zkZZj8gWHwx7R|d*Y>4nOMA(y{rTAXxEiPe4qGENje zVZ#%>sY*qdUArp-zK70SouQ)!r;rW*IV!Lv?K)tDjiBxmanQWx#m|^Hg9(P=lXIlU=b5)X%MD z_+E~ES4dGsjg;bV)%c?Qek%aDO<)9(!yc0(dR2OHJOBY8t5;1px=cc8^0Hj|$I_Rbzgu z6Tko=L?W9l1?C*U?7h+F8ixWOsg3wter1)s`rPA^pT0&yQRt2i3zUKF<1ysK^a>b9 zZa%5U)yNPGE`%*AP&Cac24#~v`wF)h$A~Ti78ic`QU{Do7s0lyI7g%n2B|}FR96-u z5jrG3s&#Mu+}s}~%YQCBQQmp}F^Spl1W7vTY8^Z2^@K}6Y;&o>PI90I@AD`LYu zlAd3VVs0hZbI9%;7zP27Qp?(@i<5628!oNlB4iDM#n!wsDJ-j!g5nyv7ZZgT~0}m8%ZF{`zZA-_g)$7F&JSU3U!w z6jaB0!15K#4;wjhxamlsDFRyfUJz8_vlO`&{#Af zGgBVkxUufuho3miCGoJH<~|zCl0Y7M&fZ7#7GW?w@4|Z^a;e+2*!3$R*3n$J`et)NwNCb*%S(;fQ zIi*#~^WR!j1=y!j%2@Qp`4r;s<&|iDt0wOsU%!+2@9ef__U_u>kGa4Ui;L_z+`N^? zPBm0^D$o5;yv5tJKwOodZF(Jcm_xw(g6($FCeF@Hsf_Ifg64P^( zc0My|sa+5_YpY7I5*RN*VKInS)#5A6)sAEnD9kYkx}y0CPE5u7A|!F>=~C;7iWqV5 z@c-@XUPX_@zPu19*tIl70P!7O*RTztH&4z5M#Xwe#ohe_PL9 zkPl$P6zLQcdK0-~(a%zs+Ir1`$)8@-)Q5P^dFMV-F=TkCoEL6$pA_xY{&X> zZwY=1HkLF1Ro8^EEv%~`ehISFEQ1USmceWwNxvrdN&>3Clt4j3rI!XTX7>vvr( zsRte>acw(GNNt&PPK(DxF{*_+jZls7p2nqwI;7zgOA7(-K0O!sd6ggBc#K4ZWP;K` zo#3O#rwbWfix}rc2*&VhWhNLr{rmNjCC~m>7QFqE+;iD^a?I$%?3Smy-_eDyNL>Be5Y~$(i|vZ292>$y_o83x+(M#zokSk_~kJH3&52aQKNaA!yht zmi+P>#q~?dtE3nb4h6vQ^DAm4Gp|4 zTEI|Pt(32sZ7w~E`Uz|TW`GsTQ1gXUrKQ{=|f<{@E{3j-pXtP!KDsP zc+6pg<)~wZqh=5xw_JaXd|b3e;)1g=KX;R?EX)>fTmqbRwS*!2uU2UaKfWUvVl_kQ zij|PVPKiO28&)bT+%g!HL(`}#AVP8rizNTc_mM%LibKjlXD^@`oU_6_S|>2Ux4!Vf zu3rXTW6H_Vtgt!(9L&pGFv z&3=}yzkee39ecw9b_K9Ethx)^eByt5>`-+I zw%i7Pc2@*+CVY|Y-OLBtAbuyz{O=V*ESS(GeG6c0KT75 zP@xpj%%W0B9CWOt&;3erHY|})OxhykQ^o5+$FC9(Y#cLzP;LJUEdF$&5)#)IL*mf`!2s`UpEVQ}kprApx^SG*L=}G0Etujnzk-vY9fWHd#O()1 zN~^I~%ciYa!yb6xp-M{-CcNO>|6GN1#AG=3r5|Q zO}_N%D|hUNqHpi$7&x992_+XBPV#0HB_mpNSoYcz_rJL-+u5D>+;G#4mv7G4(&yj* zxlbjYD^XZ1t%fW!V23>L%}W8JRKSUq*PzRkP9MR5**YC39g(~IH|j11}5TdK>T&RI|lXs1f@$=-qU z&xW(7P7lvzGa9)v3dX6uT&GLezj+c@G1+0*ux9Nfdcd)P58ivn3%_=6{_Cu>=WWc)+IVYxlAPYAoy?s(_w=*Q zI1}y0_Vs65zy^Rn+v3jtR{Z?)(N}ft25Dy_4i+C4B+qVIEz?R$edFJHbMW3ctVbVu z=!ua>ohS#6Itq@e6wbd^7Huj<1J*X}I9-8)WjY-dijcYdf<}J{3zdK^w2%o6{Axf& zwNTMiY9Z;LRHy{T91CX{hG4M`gGTB(p~#BWanPY?lNNX;D4M3HZ;D`_zi5)&SbCa_>b{ZyC(fyD9WI#+<~9%$-tzv_(Oi{@?07X|_b5FhYsZ4%Qj zt{574>w3KS0LhaSHmfYHkl8;@l{QHU5*HJ$`as;L2U)mTWw6Mp2TjbxTn?@vDBTP? zqX!LBwdmYEpm$HmRzye^GWu;`?qoorb4x+FY(hgD3%L|{D+2bv8M6-ZVNXIldH5>C zR|V`^f(d6)eUFfe>lQ79;5J;+th4po-`7oT%(4yUHQ%eM;qGc>q4)5cVM zPbacrPC;vQK08}t4MzhXLxQrfOu?7O69DMV#MVHG8aP&hViQ$FPVLKKK3GMi9CRk= ztd+fQz4g{#T?KR)G2+Xo*RGbAy7!iT=&^YF-FKdU_w9F5cll)O8=J%eHUKn<8Fz0Z zC!T%Ib$HB!1}9MO$0u>fI+q}$ymQ4WIsLqgZ#e9bp-nm1MGtP+uwh?B#w7*aaR1Yq z`4fw#vtNns(yZbNS%V2(iy%+TuN9CQNY5!(2?9DcX4pmG*cYLISzc8K*ae-cH30|) zW;h8Qb4q226a|JzIh;ulCsuLQz`j@)M#6VW9n^WkWYUMPN?mA_v^(%9XwTP3EaVX4 zqC+%W=AjxVi1^0T9Afc;=csiQeNtRY)~HsqX>bB4mc6a zBRas$qH?vBe#a^x8 z9Y|+a8Y*yH-@_$tztLC`;Z}9%uWCaazb7UY^M{WMe&OlISqs?dYt)cIZ#aBv&q&C;sS! z!uMW(@rgayEU=hQ7aPY!L*Q#it}**@;1ST`J47 z3c+BhkhR$*vK~LDBN${Bz`+%lNI^+8v}w?hilSyN9Tr5t;>t=T6O*V|NAQukLimVT zES)a8N~zdM{hM?MuTA;nb!k8NFo{lTj!tLj>mYlN1RE)sI{I*!T6chNqExefXZFu8 z(nk?oxB?-Tzj#}LGHTiZ{t2W*hGV6(=M*jK_V^&3%x+rt1(2#)1BMXQLTiu#Z_U_- zOxP=@9(s`K-D}a84$sxPA)LhQ-(e5Yz>bo%7LNmPybOFnXp-V*b78w|1k!v??xJl# z^(+LO#+3!VXC-Xq`v%GT85uGIgI8|5 z_L}qd#xY!Z>BV348+?!)al#pri@_)GiE!>Eszm7pAH52hIR}fdh{;aVd_Wcy9@`9O zzY%Z-x#{b3N@aZkc=P9fBfOuiUJhF~+asECDG+^g9#<*RSA|tCh3ABb({BBp-hLzS%Lyb9*Eh z;1HA<#PGj5NM(nlbXFLsCSxT6(r8H?dy@nww9xm3#5KnRu`?uk&`IKnYmGNy)j-o= z)u&Jq0;Z1DWQ_p9J}{w;#E&>H=<6@W-TiCFcKqR^ANV*oN9Gsi%fB&*=FApt?R{6=YMdw>US8MMmrk#(vr9iVPLbKd6qQDBWpa9JGStL!q>A z+h*CcZJVsj3zwAWaES~@PXSf}gm}oO(h$Oo6ub7R&OUl8;FS2PM8UlpD9`%!>!l@R zo)dsS*AlM6djha66(e#Sw3FIyr-os&Tnw;B9Y9fXLRQ#c2?BUTYzxTd;Fxtty9pjT zN*)Dp)Y_H4soApHn{xwPu{vbU=%0w~G88k3zmS^TP2x!by9NP_7(IB;Js0kcLwV@_dtW&2^oykb!K0+0s0f{>wJ7$2u>zQ)3eKNJTwUk` zR-x0RK2If}0X8#LzyXzlB|C`p`&V&^)n1Y$O>mBbCpWL9y2tjViV z0kkdomC*Gs1N)^C?3XGGe@8PEg3S-pzmwd&ENOr6abVrRxv6so77ngGkSN8-OYx6V zuxX0QE45jQS#<$cCOn30aOw$NEm}B7W-ncfbEuOxaUs|inzI1EP&+5bS-r3Tn}z+e zn4JT;XVx(?`EM|uw&vza`j*Wye7`=Ne++pAj7Y?~ASe`rhbidhFl-cuH%$=6ao1BS zS(89eAwx!Sa_Y^2Aw*aQC#1t>@ZGEKAI=`ksPeg=OATnG9Kxd8Le|fA2<>g)_JP9( z#igP%_h2bmG6|~@Bgn>rZGA7p4!r+e4GLOD!OJZg^Jh*_^HrAWgfsI;1TsKSLD9{` zTbd=_yv^dx-3DkVL87}4L$g<`)K(M$9f|?z(JfG_x}Yp?rmZ#)lno7-E!27o{Dj(Fku zSGr?&K?4#Kpt}sv(Gv2!61(({KBhm$6VbKb?WN@ z=WGGaU+E)M0*su{3yP--HyFMloElaI)UAP2sK#fsc?qye6;>;ynDkUYd>f9q7*2L* z`p>czFwmT8j}#V_NJa^G?u7^ffQyW#4OlVCeXo*PpSdkvM)7G0EYPkg)_fuRHr zya2(4gU+)chDPU|4Z5WHUJ2)2DS2fGLV%7kw&h4pUWrsDpaT^yHZm*(fuKe|mkZb` z8~fAG;NMx`@n_?+EY9lp5{Rv#h`nmr3dvvolQakHpu#@-(m}z7ZmF%!P6x5^*5p4b z&6Jw?ToMEVwhD46qd{0Tu*RX%bCkArf+^<{)5!~feS_wIXI?(F^^d-O5SZ9w4s5<_S>FS#EE%U|EIt8B~Idhm@mTz6_yKNEI) zHURu(_rSy{-<=VIy&nkbBa4*}uV2N7$3N-oFD~C32ldvQuU~)a1y@ND^lZwI9kW4c z1)8SH;B;${b^9n_g9qP-LaaKMMq2NIuFgDG3_6>DpJ4$L-+jRI^MGf!F5D`;GB?ON zuSZt-gTWF)0~9iBgzR#^tXsQQa@H@GmcviQz?m8yby-fuP|#rt;M{ZI ztn=XD>8OnpFW2p4f>DzN2GUjpkzA|<)~9cj9Q0_F`okm=sVs|*3^3~a@%~&CVYAio z<9Dc0@OODiX{|)@+AFzOMP#GQLZ}-)Yh`~^O5;OlA3z|lwDwR6ri5~Fn8(@5TZKoFXij!sI)Te8RRN-x>Qxd zV0d3>d`nCu>m~Wn;t52rMq?+xyR4rb?NzY`g-D%BMWeyrNG zP+_T>Qj7*=Uk2X?r>-*@5Cj4d1bnD-_$gi9XzCdE={D`vfg0?A##MCxV?v&K{Nej| zc8@>$=%bTxNR^)h)?+hZ1z@}@5SWK0qcHsNqwjw9*=G@ZL*u0aHG*(-PM1MfCX0?ApW#;yK9jE$Ou$80RTQ8Z zD<5BlSY6~}w^Qd-p1>l}R{Way3<7ylWR{-p}aQ;9^hafPJbbmOXKor`2 zm_*0<0nF%e*!o@j1UM5p0yO|+`TB8o4m*L<4OH4mjI1H)z!N2>u;lOuAH08O!rU_a zkdgO&mYpff05gVwobjUG>G@cs}elZ2iCa5=95Oc7QBML4cPOx7wY!wLh=fDXG59m;gzML;07ZP_S! z*}$ixTcAkng(D@u9`XzLDuy#F#&-r32B9+k4s0ZTH{%v@+^VBrC)vy9pzk0?l96SS zl|x-P9fFQ*ozfLfpL0F--*9~#YN$GEP)*^SQ4-(( z`=LoKB@~77vSmMjK?DK-;496hovvH12m;x3g{(X{J`IgoJus&6OF%8rDxRl-xUmd` zBh{cmdWRKD<7>F%^>j6ne2@V#^3{68ICel#B?TH(Jm}G=F3FPWoVA+WR~O}Kp_q4# zc4#Y%MBYFc78C3QR@I`$>Kpnw1`9vfLhKk0XxT6Nt4}}q_s;J11NIv*j=#@FFbTy0 z6`+tlI3Yp)-Jz3g*|KHi)mL6+=2zNRlUu;n0GiySJKx~MZ@yFS&=!Tp1(08!mX|Nb zpK#KBdt>)^-gVz=NhvLZPCoN8G;kq6CPx~o);(#P7@AVJeko`~(HKU4hobtDAM3)knC@7=`0=wr5hBQwW zt1H=@hOa8j66OqH{*J+i=Sv0>kB0?rFIEoOD`!jp-n}r)9felK>#+ieiiYFI5MCDs zYiycE4z`Zbf`4P$i=t&nRGjo`*FtJRuVqgFA#t}L%8%(ldf2ND=NXy|>Ms~I%C&d? zrkON{+H*;QbJEoS=I}=L9t8omb>b__*WlzTueycY`r@&3C|q@@f?K1&V4_jOBd#?J z_E})nLgXnzRpJ@M3& zc7V*@a1?sw?CVdlz+=xmbsKkdbc-~3al=}+vU)%J^yB|F_0A`L_kGe2Gp3BW=lQoJ zDi*4oa6B2n(3e8yp&HJRtA1wm2HG*$?ykJt_iHySoW|&awNSsTmi1xrvLzVI5Y-HV zBtRYHpy^cbZ#rByf91>>FJbNbKm!PR{p^H7zd!&y7*`C{I2OuGkfq6<0Uvn&)i~yA zWYJY{?sVJ=7eQ~QytG7u5>cci1`TJ+_mRq4U4twY{)X@MqOeVFJ7Fq*=kMrz`K6Ey zWh}ti->+>N`VS}{L1sUA>!7q^_-#D{sD6$ljnl|RRL?Y(S+ZtAOu2xQ)98TF0HvfM z{FXooh3phs;*>JLM3J2aN_GAwXbBallmwll(My6;#&Ky;7>+ZD+E6jg!~@!8P1Xh+ z7KHIkG*PI|1Ow9>VXlX%JMb77a2O2qJ&qVt1J2L=Ab=Nah0{k3z&S4s3M~AV=SxUz zGYP^tO#1n%&2yC=T8oU_i(}@stbb|o*SzXD<(_6}6exL|+HH)#5hjZ4F%-iC;$jaO zdC*}yyWd*2Yxm_ku*HgS?vw(l34lBZn(c1sh-1Mg2M-?j@UGma_C3320UH2z&t7}{ z?oFFB`X=F_sDu-KY4b)o>zwnh+vAP>#s~lN-`n46+o@YXpT0wYwWEj!Cdz{JJk_A_ zA|uyE9rv{Zru=K0HI9M;dH6WlRm|pkpu_I>WEa?93Cg3{wjl=(@)W{F%tzu zP`POt=!9D&alfPBbjpmwAPa@vxA;uQ3OO2i6w@YcdXm1eGo!$r#y z3bl0zHgqpaBV`8f{97438hjjR3)>3Bd%X_Ej^+iUYFV^Z+O>lMC`S$B7zkqo2BQZ+ zDLbYXLb+*vHyO9_Y%YebX+8_V7=*%(;7HIN1=GYhMT_GT={*$!BC*Jsl^K9lPXuV5 zK@phwyy_=FJK?5Qpfv>^iuq1WemYLhIF9XgsFbXlrjpREyoGy>RJRDQ4MT!@YYOmD zc7l9-c1+`cY>TK5@GS76=wH1pUHs62qQINmXhz9y+O`%P{~(OIjO>EuUBWySWw?He z;C|8dT$rbY?fU)ib506Oi0mptE(gOj&=)uQ&X8-WRH9Nb{Zn*UF+cx8R&A>4F z1RN{RHy5?4qmz>5oHlLcr=R9t``$b6CjHtyYJb0H7O(+e&+N9_pMs-_z#}IQLh2d1 zzh!T~_VO==Xz%t$clf}EAAa0*-cPefU;E#OG|T5QK0Uu!mP68k{B`%zzzJx^6D;?^b|6+N-v{ z+t_6=a)YUxdMjlkjLr;_c+H{R#Jd*D1JWo%lCTXr6UVzKVaMsl<#(fR!!KmPO8er_SJyXU%7b= zn(xp(jo{+GYIQyWPKmb1Dk~&5WxKwE>ims5CbAB-86Rk>8vIQZ+4+TN2ISe{e=H*7ZI`rYt7y{I*Ihs1k3J*z z^V?g%27o{5jsRmNJPQxntDDm0xFe5vd~fXb@kbwdv|HbSs`gm{+?KiV+>QC*+H*(= z`7M+=Fq$p;P&&iFf9+UY=zx?^zL-^#dq1pb9kzkh3{u>gv8^OTvvANT9lyd=24N2_ z{fm}Om8cH=O}42H!)!~bf^^o-!RYTDCrD660|H?Y;Qckld60OhLMLjn8R(+Y1?14v zziFsYCr`mQRRHA<6b9kHSh!}9(?BAXV867)G^+mX(%_6h0mbh;+SR+Wm*e?e=j=3y zxyR4V`*6~OdUTRzk>~-aDF?F0tekGk6jTB*&=#)_&^ojL8KFWQLk6t?FF{_pdaK*%jcYY^uCKvQ7GQUsSQ z@gpnup-8MX1y>YFS8^_#_{(Cn?m~;#I78SyIHiLGx9J`CJZ{vTSvx5;Wzn1IYZSH% zq{qjbO0fd$hmooOZqpG2fSe(>{pXgQLP)=M58L1Wwgqee_>=CCYOgQe%_PT-8`tF6 z|G4p=xADwHT5YJKQ)h|5II}w7+mUdBY`CgIEg%rm*}5_S9OJ=`JfPUR zcIR;i0DF%`7cLh{73=2c>|V-aD6@x95u9UG|6}0bSD-T&@DrW1w?bE1elVXX^cFy_ zJ_9Nn&p^jDLwX!Ii%n=u0`R%o(rmm3vK%N>hs3tVc^R~Yfk6l9;Pd{9a!^Oh)y9o! z!Y%LJ zEm}(?w*S!MPfK?>0kHVd-p2?hK`gI&%6K( zni75Jr>)Ha^$|REWTXm@xJ^fNoXk#lu>j1+Q1M(ivY`d(5)UUs@wC|x1uF#0Cxo~= zfFPG6?nf|SrdhFWp@b)QkgyaqT%ic6Fo)tj%*GosU0rE7R8$if%<%;wL-qo)s4dQw z@YJ?SK@5b$=YDiZ3j+n4uh>|ns{jM3n4cR~I`xIq02DJfFP9@n9Hfn3b-?8rHK=Q& zfl-+_rg&+%jz;RC{?#}*D3<0hUBV(I0zCwUOQ#^9P$ATTel(jm)JXBtFloH#s{>^mMb3(SmD(Dbzyil3S(!YR2;UhckstE;qeU~uV&kOz_| zXyyzq+@Lx*eq_Dt!jSbk@(C_n#7y49S+k&fmTz1r@%tZv<-gI}H>?=W_-(L&I!J7f zs-DV&#~&&8tj-YoU^7%mY!bBUA^8l49Ry)HZAEh6CVs~7?alAh;W_Lk>fP}7b*MCB z3Z+UDFyrmpD#fWiz?s5v3yQh)X0G197G!r892L`P^aZ2Vg5?s7na-n!9Vj&gS%&e$ zV^YB#1S=jB4+;B4TNNdbB@SJ|l~@6IOL9?sH3L=@v!`=raKtZk1;Blwhaj?je>7xe z8}<;jid4SBIM_Ofr$u%?KoWNSMe$xDZ==VXkp|K&N8`BaM< z5!hQ19r_w2Qm%*G`Vih&RS39Bi_DO`U~++GH55)7isFs>NE)teBbJatb6g}=OCV|t zF(3rI^XA*XVD;R6_uZRujOE#&@CIvOG%ve}?=dwBvIZ=` z>dU#?OfaJ3H?R~}6NZsu-~$E_bvAI8eh9gRH*2eM#fI@>GFz?8l4XN^8z#E(%_{4R zhAw_LWd*WgKx7Gtk-VidB^-leQk%C_DFiK}qU)5ny+R|J4F8Jr|CNiT%%g~K*}q+? z20CWRx*CCzG|Pq|71m;nf`Ja_5sG3qi>>7u;MrsKq0XEzn--7FEM^UW#Opb19!+H7 z&00zEiW$KDIk1L-&1p7c&_ED~fqZjJ@6odLvwNf(tF)l777VYF`MF| zI_-~87})iN7hdA{we4QG{xxgCQYr#rK^Khp;`ObBo%=*b$?2`zNPcc!-`8J%y>(MR zNB_j_*#Ph-UG~h)bO!s-_rRAA zUC&rzn7WPA)b@Kfv?df>8DiVfod)skaUXv0&T%__u32iztQomkl7rwMtj%pKe4C)f zfPHK;tlsc~hR}b0Z=d$>zi$B>0Dj*_P4bSAkdP{F2+pQSZGQJ%x4+!JOAi@&)NxW$ zQiOtJu*_JUjm}wv?f#x#OylgG$497#4p^JH=r-Vl$fHlFEtesso4|T8PHb5e2B{E^ zAYQL5P&zp_Xh9~qvPdd(G9<3+5Ipk@87DJg7av#5U!9~`=o@Frf$b((P!hpkULsK~ zFr>u6f?XI(8qr5b2NZOM`(AjP9THxZiGk~ z=f1185tp$8iHQRS>@OWs6Qn3J9eOrcRWL{?{6j_*S2WCIjp`?};K;U}P}v2QcH|;|v^wuGsm@v@ zITIh3vb8fM0IVKeLGijOUl$$ITUSW-SN9{k&yZ$gZUhvDpryxP>MJ2}?exmHfw3AZ zI&F00@pBb`V`tIW%x`X7DEZ~%V){bN?|H9qm`^zF4N#VUL53J{@W{7|aiOc3P_P$6 zSAD$aAQz5;_Q-^0(myU$8Tpv6ODw&i5cg|aBZT3?J^CI8i*BGJ z0{(%=ZC_a+J<76G27-{nlBiZX;fqa29>5oLaQLRpU*5cJ%G-}<1?P>7&#-b_^w}JV zt^&yNaWeD(LLh{og7JI6(lz*eFbbfdQAyY?vrT02EbcN(*3YrvYdG)5t_HSH7LYn- z{@%bkgrq2IT!%txzdrr3aU2ty$)clC>;weEtlx#o)Jy)hLu+hH?vHbkWz!h$d#|>ix?2sY88-DPD7=suG3Uqg zYzUz{O*ovK$8;@Yg@~SpgsvEVQ{`>_?t33J9>OdkA)ayu6w+OF01$(J{pAS2szBuA zG*y=N{5b1}k-z_x*mwMv1?)tyKk5EB{q)lxo;!E$QN_i@5L%HfO|jc29(nk|)aGrX zjz8}bDage{D&W~uR%PPB@?&ZjA09`d;CFWBcA*-_%w;#dDV#c;B#X$%V2{aLEfL5> z%fQR8#O&XUh-d@BFl%j;F*DmOT{m08T6LA+$apwj$O$`zVr|OOl!J1~EF`lxfyG0)(b+)F6p(_Rk7OYgN!6vE$ z^m6iu1JPs!WjRjdiUb5rr-?3RHl^wN-31yqx+{UeacaO*WbnZwWX4Nx0VhY1l)6Bi zUCi@zDUGy)CiU1h-jllD5wiN@`=u&(Bl!2|aX^Mma7%5vy-@V z)sGUl{}}Xkpti!ypXb64u$BTCuntXRp2#Fg9B`b}^*>s?kbdAWnwmUJ8sqHW_*4mu zjFS*ZPuEqIC_BgR!3HoRLXVB-?Q{zBx`c#B;BYa&p=cY%L84KNUE1h7sKrVz2-n!v zHu~Lrnlc)s*0RV8co479r=NV3Fi^eL4+4%)n*p7U#X3xQQydU44G5q#563(Wf>Ce0 z`S#uT_UZ5L*S_IbEnrswf6~oy_0?C;;4@uNP|#%U?3kK&+1Rr#fI~xS;5iJ0wU+$fq2xp z83}`mO)De{;{~~Y_XY!bN1Z>JIl;_!2}u*O!FR$Xe4ic*;FGGHjY!BLosir@`v^=H z+R!AWP>EvYrm;;Mnn*;&;c9ajS3cTQh4;a^1w&UqW6n3y43x?E7z8EO6QCsOiHpJC z;rtyH&+Y89i!vEH%!QEydOc?xbEInQdod2t&{Y|dP!wj<8douUg<1A)e{s95QN&TYtgxBrtK-~YF}u>s)Ey|n(4l9ITl*yR;hUVC|1RDAf+ zr<^4f6%{(Gm%RTx2)~6Y=iEpkG%oz^tnaHnn|7XK#r|FR0Us{KcKep*NVl>~si0CM z9Bq1dyf%S_2s2i*eh6fi4k)7>Q?F{_cp}<#(oERzOx#i^p6}#50Z0Epi@Np3aGWHd z2WG2L&55?!m=KYfjv@fL+QQn2l^` znL-oO=r>C!es zgUCkG*}vS5Icy_yjj@na7On3nr!99<;?jGpao-15Z4fF-=s8?6vhzkXwl1JjFZVqU z6XRGw*Ytv`LbDdqeg=m7F0sj+vk-RU^*3lm*}nG70yY5bncep2Q=>+WdO0&Qv)vwj zc!v+Y`PQp9k3QiHNd_x|)I;E@GIML_mSlSs%BWT7RoSTM(pCyCfjrX6~*lhPE6XU%fAQkqK=fE zy8Ipt`R$6Vbg9kEke;2XEQ#(|_`aYR#9`vsJ39K^aQv>tU=`3+E2K@Q4syt_fs&oR zy8f6IPSJv+hhx&uK|?M0I-r6sPw*4=8kbIZf^*G*2XsHBy<&k_r-MAUa*w=A9yI* zwIzC1bhmK~I{24agzB2Z)Kjn_9`ER6u&(ezv!Iu!~C$a%&a8L;}W&pPkvf=NxfIg^BiOyExCMPq;KdUgyHoA3Rbqefo zRd$c&nfw>!ikKdlZ3H4B`e(o+d7VbxNB?$r>fk`aRjLNlilQVhW;e8Ryxn)dZqqfsqmx2_Pg zY?=_ulRo?HFBK^4R^P>8*6b zA_RmLrbF3TIp{VBX|Wc7)e*06(XwgcLD#MhkEiQr5OAHj3kB^E+%*X8ufU98cQ&v?xAm4_UIE4Wkfpn7p?7k|L}-;bI9um?;MX;8RmmrB|zFpt@rP5Cz8?iNw)? zyBfTVn-=fp6=EQvv;+#HXP$bB(ih9}G7S8K8VMb}VSkx^2Qq!Mq|N#XviQV~eW7=h zCB=}=h)ilBF&%qL&aBT+ECp191fLnW>LNKoG#DKI`0fXz*J7bUK$OF%Pw2IXYp%7a z3;dD^*0|{J_I>c$^2KYVEGE=V$kx&A0R9OLM;!p$s@k?%>og`wpx|FMXul=(bYH$- zXA|({1bf$-?tV2W;GPY}yo12V1erT~)-CRPApflaAPW=XRK?XzVw@GE9wS!+JU9Gq z;>3wp@8;FAZ*OD)8vy>~+u^m>UVG}vC!cgvtKHuFci&HHzjEd3fu~+{sbc)0o?6WM zEtOT-<%hSX{`-Enw4T-L@5mHL!Utaq(EE0M~36@8+iyG zpFYeiGUjcC=1)sqJrFWsC<-N(KoZOTS5vSFkUR$A)H9^e z_W=!mKbDgX3A?*3vl4Lo1oRp@s}IUac$#&Tuom4g_@}zHdkxA;O10Cg3$%Y~o%@kL zn_IFWK)Wikm@jRxAp6=Q3)ld#M`rtD9|9D#aZk7K(1VXW({s=;=|AvbDFgl*2=T5N zt8+01kuzpNpm!aiX2%X3)aYS#_ZxtW-Uo`N55vE8wkx>o`q<+09CS`lAM%}{_X@< z)7HfG8XY>q7is|s-3MY)SB{itZPfmUW`mECLeM=)wG4$_%I#j5bOyT7LaXeuPo1YHRHb^_r(Q>)2nIj*5VC%@7f%;1CifyU`M10tT z&t;+jT*`5V96X{AARImZPKWC&+y?lfzmvV2Y*%HG`FFLP<6QWAMg2)cK_IHe1$Crmt5a$CpDXVGx3R1YoLp`~}~sd^A;3`Ql6lI$s}V4}`VuB_YuK zDVRS|LXoYz7(Cy}_todiJ6Q#ATZZ%RTp89eC?j1CrVv*EdR*$JA>>N+R|L1vSgFZe zrNVR`1boAuV&4XWUvc3tNC$4cVMojI5;|*9r47>*AV=uzM{jjaT6-|5m|(c~bK^;5 zj?;Ij&cB4W{=ilIM}E`>fWOgYzxTfT&y9?Y4<35RQBqc1qIA!Bn+h=QkyJfOKAQkw zx?r755K*8$M{>G+N&oZJRsuf2ctjK(>rh8fsbVMVQ`LQBZ(YItk0jQw(wi*4)ybxa zSSfN?AErP_e*`CiOtm66U1HKYXra)hCK~4tD5gHR7}ZWPcPPh92T+o}9IJ)|36D=u zsQ~4$@N??8xCdhV^swZ;qcF{2L|8>#oM|en*9mg6ONEetQy1oBO50{h(xrKVRImd# zDPEx;o}V6j1FEp+cLE)u?iUt8M{TRtX)@@b5wd3Dd&rC{RmPdYP6HFab51UgAH{E_ zwsI~pR$DC&8jIrbX4qqy_9>(cI9v*sP1o50NRBn~-VN>@ym>QW4) zr{sh=l!o_hR|D9I!>ljL!;eE1)C-o6N;eZuQ}2LIaI8?6#fOC&?KVHUk{Kjavq^*MGk$?ZhWm+^VEVKl9zF1YA)jEs5kukui9zlHj*Ret!AuFcxD1N=?q1G#&DsoSh5mtf;^Aofhv#soX)!OZ=A^3 zDpKb`{ep+Y0HLqO+SH9O<~_YF2uESZ^6!70hjg|~vgb~gh)87c3gI}b6Vg|;)Q&Pa zVG%+v5PXY>Pn5)7LuK0!pGy$bN~2qLktnRF@)mv%+d}7`Gn6%7rZfh3gX@P?LMYzH zAP|K93?1;U&<(q%O>}daIBu}pSVuBxo-qIHHd8_2kIMsj>6lmqDTTE1*a=UG3^K7s!}6na06 zi45+&Y*tX_d|bGzSFc`g@9DL&pWD7dXzj5NTi}!N6FTSSI%L8)m04RQ2pyWi2rf+H`LJr?SHGCOvya#~e-j+& z%so=FX`Mu+VXP$J8afemSX$K812q{k!3a`7fh{XXiM0pREYyW-O~B>Y8zoH*N`S{I zi!*hcdtnCBJ3;>xW8*;OAR|NmhRY`P92?ja_kX@$dCgL zlEsrhMr{DA0AlEjqudT{!o1eWzTJ8U1=dpgA1sxSU(Nz8lr~Qrcr+-XYcLQdU4p{_ zyYO1n+ZE>uVYcK0PLPtdKTE~tg*p)|m;>(BHU`2mM&*i!*BB1i{S}?KE*Bt`QIrGy zrXu_eq}@jB9Rhtm*v^Mu6E=T^u~R#5gQ*SRvq6Llb#;hhFjfH`^ai+t?oBPm3=`7( zQdf1jhR#ZcrU-4`RfDy;ly!1MQZqTaS*nz>gWFk=y7w+Gm70&^O`ZFB#KFVAGbd<4I}3QKYhUZ20Y0D3T{^R=EK!3UT_KJYR;e>hGj zE7J!MZ}tJAsm@*};b|Rpf)#$F4vAncLjf`W7S%<-|Iz)6!m|aAt zV+@EMP4i6MroQ-R^*UN~FwWckh}xIl&3wcZMUkCi)x>~z@g?U;E$EY5N2yT^f<{`FiC1KWIh}nukn2jk_}%zr*hwJKvZEhet}ID z0)3vCE(b{d?9Zie{+CbyoeO4BA`S}>n**7!aBLhqdnrsm2ea{D=sNkq<*&)j)EHr; zG>lttKdVD%*l~>Cc>n|X%8PVVBv(raw`VkzM&<&IhNkMxT zn91jF(fcG6_rb9#&E@6Z{nhr|wi}zblPjUkaUid4-J-{cI!~#&Q1-Pa7O(+ePptMA zd}iv@smC04+)3(bm_g;2Rm;Lng%VCqHXKwfF>hqZRN?g11+V?+dc_(AOi3yB`;%>9N4#uv}>LsJ~UMYg>rSG9_|-cP(vC$ z90kiX4!LPEx9bE7&3dWhgZD5f@U4#X=g(wYdRa)t+iUZR<+L$J%0VL!l?9)?j7%K^ zxXiZ_Za*EGBJMCx~%} z?B%E~miU3Am7gDkhOPV=??}<&NeDU#%IM-tCwd_`YVn+NBUmrM(bN=dmKqe`X>U(3 zMnTfJ>;-^8o)7ZUdgeIZN?cz;J|PIGbpC|=m}qd|jY^0si2$%0n(lBGxPxfFofm1{ zL`1M!2uXr`#)u2#&@tD`I|B}oC>&?ib~;Q>s!zMu$7R2)EH2_{`Fr>7y;nx8{soWt zU$RRZ0R9rQ@7_)yd+OOEgF`}sMjwBMlmh;V4)@5cKkbEgozF)-lUG6?fdHF@K9F5*_R-5 zH^ZO|>=Mj^GJ+20(1wEk8H5#uW_Iv!It)drfd;d7#n70!mcjW)HfxDY6-7WtG9pCz z<__JFd!U0O{nII4YcL^vK+u6sl+FwdUyK4eFW6bfghEQ99571mn#FSTh(ir!6-8#P zMNo(iG$R8cxZc~WI`mZ!^|;r_bz%+9OaUCyWpUih_Pq$O9J|Tk@4-+Q4UUZ!0UeY= zZ@1ijJDg&kq|clvVNp~(;0drcAQKL^Tv{A_ zifsGoOQr5o3YttJo=>31jD8G6y(0nRH>(RXCGo(MWXnf)N!}0dL3TP`J9Yic+Hne` z=hk9--a=4^H>1Nl4`V4A%WxelA@$&eV`r_T21!Cqw(+gCaMV;P6i;1E8Zt*6a6`LS?e0s%e_RxrTeKIJ!D6KsnpigAD_%9aX_J52gN{Ss_*k zkq9Q8tMbg&RejBTD-Xu3S#`z^*Pb+e&{6%senUXr^8j|C`lflAOqWAOxZhxvOTgL! zU94rB)_{cxO&dr?Q`?4tA{a{pf(%Yg(PY)0gjRpW{3>w zpbR%;7!_^FKq0k4j@ZAKQd&7xjKx_!5qPYIMF6vUI5-znChp7(9_Qi1_G?K-r^iD^ zUI+Ps0Ma@^@eGJd7EeM83BtI>;G`4@#P^DdO4+btsmz%&O_t4_D^K2goBa2hOEueG zI^k8xUB3)N+rgw^UjUC824(Ajm=~9)k zUcAso;%8W!2*AvRnvA8A|KnRyleI<%!&6_XjskxUgjB0ya2zT_#ua1$w=TqCJ4&rO zbe<=GtBqA-Z8>HPGdOYe=mga5U>x^s84D3a{YAM_31-oP*cRrzoK&CR>T6om6VfE2 zPQK62)%)w@6Hl>I!Tz8hvhx7`paJ*x$7jr#5p>YO2MxLBUpHv6mQ$=Y<(JEm|SM?Z#r-C{5ge;e$6cwvmRg?+!UJ7sa2$v0%U-uPb~+|J4eH! zaxMB2=y2D~ohDs6VlYY^Rv3_gRwfj&cf~85Oq52`0yiE5bvzCX#8nOh20Nvxe_E6; zMcKJh4F?#Eb6mB0oy?gxS2m?*NKRfp1<6ob4wn_{*UOp>Tcs!;Nh#(7cw*xvzD-Z* zcFa)O1clJWA3P<)uX{p!0BThtnpiwmN?_nH1ZW6UhY1IdB7chm4wbcEyr2rJQMeTC zjyO*iKYNwrf)$k1f0P7bHJ}iXvwZQhAUxu9+4$zI(rn~8((3qICG$H5fvY95T|bHH zFa*aMp-QNJ26Nm8wUDkZng4;5ulZRbTMdxt{g2n6+|Xo4n9E%48q5n038pLr>Nk+C z;Gt?USf#-6ZyjnAti$eu;Hd|`ogd^eb=)mjX$k~Zo4O~4CjX~fHN)VRe4M{DVxbP4mVEDl_sqvOWbgY?C z+r$H_((Ag4fVcvJL`%R0iOA&2$mr1w+(1m(kRSHp{h?TeXyKWa1$?-wI2%l!L0VAO zj9lRjI9Y1sXve6b3kKomlJq4Ii|Z^=(8A}ic;&S_ML)tgbcl5Rly`HByJnSQ#MBrF zf{}q%;yg+=Zjxs0yBnB+DK3wboy9oMTx7drhx9^ULW+2!P`7a5Eo$r25ord^6qMc= zfe;zKx+uJF!Q45rV8H@pvo(;{E|nkW{v`7kuaaWG4gu80XT1fK%YazWae=GyH+6-qVNX&H$`t=2h!Tf ztMS7Y(aqaQVwXX(aq8RBWy}?-$J6Pg>tw^{&r3qz;ijNZr_1f&V%I{vBNRISt;b#` zo5wvQtxmpOI$!spZ2kNxDPH)slq{Q~!7{vUZ=Fb1yn2?7dGvt38$akY%nLXHK{`Mx zw{0_08GLEY#O(@Zb^)FfJIAYvbkIyNRz4gM!}sXa23&hT0)@96D^cb6Z{M8gK5p)` zItFu|Lnso5jX0)NQEAe-A_v{)hEB@AF$j2Jk8sdXA5C9})k2+|eEcyteDv9;`}otl z?;oBG0Q>kgdj9zr?>O%4izGHKQHn5zudD*|c(cmuF;bUar-fi-_rY;oMw^VOj)gx* zFP^8#B4kzW27OR<7&@2VP8O_Yz{3j>M(K;ukAx`b0$9xTBhZk7nH?I908xj)LuW-8 z2HbIEdm-o$4TB?4p1Q&|Cttb}0jPCzJXB)H}^`GAJ%p#9{4!x517U;wUwL-(N&%s)*uKZVr4opFH_#woX*Qv3FmM5|4rb#YyfBiBkrwDd^cr!^kE|p9&~ZP z0Z{0LbAyvvlu;t-xg|*Q(M07nLr+w?0glWMA+eA|G-4_C4p--l)nk#)@PL9aEPMl) z7p+-olt9o)mBZ~8>aeX))DHTT~sk zKuJ`n1&)Z>t2GtsqRYwe@+ln{UXiw`6r$S9f+w> zaC%h;9MXo_-%5V^8fmlNQ04nWK~mpCDk0^Sv(~MZjBQ($F_X}Dq@=X#<`kB>dVzfbk@*9$f-3hR_&@*+DDHyQbR9fIp`%(fN>!E@%G+-~qlRra>=>!R3c`h+ zI28d~RCD-Mnz>FQpqSZt%q6nolgFeWy|u)n>8|;Z<7D-lw@LKM-V)n+5EM>JusuU> zHACCWBzf4WU|JQ(#;4Dh_Lsk=1?iYRqa=3!qjB!$vDYk|1!6exK%NVBtz3SEm5j4K z>9`}LWookg@6qSgi8HVVBbc+M@y7a-uDUZG7;88CD>rdXlSiU(otW)9GY*iM zz0qmG5x@y}z_2k?O_SB}MF*inVvtpOflU`d>NpWH$6;WYP)b0ZGP7syL%qFm{>qS1 z=!nkDP+pboh(A#tp&HFyi^spG=h&t<>5_n3yrdqofr+uoCrhS$E(3b?m84b}DG6#M zQgdgu;RJztr$xFcXtZ$XJ^J;LW%GZKaTC6j&FLFu_1bkZedfEEw*cpgZFo_5=fQ*% zVyiE|pG4oF>(zSTY;d(kai?jKCx#m5F^wa;ma|HcWXJ0We--qWYovYpiVf?iGpc z(A&UlS~2B^$worfj9{BO^mJtDTO|Fx|48f8|A)VU0ffw)^OYIE>u~P+;zzZJ1Stmy z@v%8D5Z8#;w?)7@8Ip`_o`KRS=rWn$9Dqh`=Xa6qsKF2&~( zct3J+;Xq2f(&jQd`*xUTyjOeyvLv#(PMb@E;wh&EgmTpinCgNRi)9UDD*E*4^8%dz zH~a8o`?oqY8vyp<%Qa!bgmVu$@z76q)<~0>Ke3!sp~Kpn3)H4C^9E?9|UJxA{)PdLpp)4pVE1-l&qXB>EFB|?T)z& zHIYIsz&et18i)O0O0f{U<>PxL_}ptu;=AHff*g)V0x%0(_xZ*%WJ*?}6fL z@umfI)%bTH<|v!aVs+w#a*%Wc<36K$DLN53bJHXke3Aai+Y)y5pve{T8yN*+t<+bklh)ORSc-EkU|!nagP#D{&;{%7%#aoX z)p&j2r6a3XW)SvGwHSp3@HsC3^Hzb?0VpHBO?Pz`Wd6AHKqCu5dMEq_g9|lsD4C$O z2=hLLHlUUn8XYYeOXo`ZydU%!QrdTx)Q(-jM1)c$C7$8T!r_?L&YvL>fQ61f{&+nO z#raKP;@4k%A67U-omJZ&LVFBEQ8CdH17WhH)D$`7h{L4ckOQQ7tJV?&W>GA%{TO7o z5jgiy&_hYp^yh&(i@*_tpcjN7&F>yKe{I^;RT*76+N8k4|KDA=$UXNyAX^syC{rH1 zO!8OFM;CCsp=C0Q*JE;>B?19xwyFD#l1Ox0ubcQHY8>mOBfbq5zNt*fte3X zn;NNZdSV8|=BM0?Ra(5Pee_JJh5%q7>Ob`_USO^W(12ElRRgns&TOv2N{g=noto)& z&@~$VQ;h(wH5!}?S2f!In2rD2AEN{i#X zx$>MF*ACY*T9)SL$ss@eC|_k})b;P(=ke^^oMHR$J89&KSuTe z7r zgV1wYCmoNzLbgtR8_-UnLRH+iE9IbI46|-;i8Md{KNvkZTsFLPndE){njQm(${T1W zP^v(6t%W#XUGY|x`u2m86b3Vl9@0k2LA7-g0^`Iv00=8Y1oQHBvs4Sp)hFV4o4`xc zfZ4rIP`q8J`zI>Kr+_Y|#NBFQ}lq7EAYJR5~$IwSf;b&qB? zfZH9+K%?AqIuE{wBOUcP0I$I5&-?5xNr;V<{rc`F5fBV(-l?18;5h~!+rcWJ92xAU z*)!yX(@w=u*)T0w)^pJtK|n59rNR3+|7r}IX$LY7lm`R+-qixqw{lrz9HGAG@J+!* z_oq5Nbd)aC)M⪚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 DI@*Z9 diff --git a/character/lostark.png b/character/lostark.png deleted file mode 100644 index 6b9ebf566c781486de48cb8a78f5b92df710a1c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121021 zcmeEt^;^@?`~CnCaD1O0;3lVAQmJ;b0W58f^GkW9zfzgZ_ zJ^0Q~y#Ijj&z~Q5?YdsCU1#S!=iE=+&wavQX{*xQV7>tW0BF?Jp1%PANDlvfD6bQr z^i9lvCw`HD->5zVlnt`30|1-=_2*CD`kHMasA@jgdd>TPW!?$!&&<BBv;SlJ zI!RT9IKV}P3=)(Xcf7RvA4*-h3si6#xd- zkDG(!-eXg|ta{KRJg;G_7keyvKd7kyr7%A}Ik6DKZ z?Emwc1ctn3fFubd75t^s@;|R9XJ1u6eRhyn@2aN&OjC86+arud&JAO(a; zKyFG|ArWi3{~0udtbU&Czh?OFKa&AKscaqOi^E6%85Bt}hx@mH|DMvz5OUTb-i3Q- zcm6ZzLdeeX|Gusx=Rp$ZMDKPh@Wy`zbyGU_|KA18lG0m{P#jX#eN!O+&!9vz$p2@? z|7V8(GX4LV;s4C={{;%mHbYK%w*@8dE5fnu!NI|)+acl;1>I3R_ali}rvM}omPlz! zA_l>KpPX>=jFU>v2Y`=q=;m~@GPE30mpr`k4YqW&ydEc%%sAVQ^QZKTZoC}XRxm5} z(vg#r)ZdZ{@O9f=Xu;%7x38`comV;&(Pv~aHJrUhX#!`{*`(ZbASb)vQEtENH}g$0 zqpmz)!_?(Z_tqH(Fb?@|9wo+cGkxGg^uB?vX+03dHzxPU^mxR*geI;Uf0~XhicP|- z&(1e&mNnF`=1T{T{F|?6711+_K1srDfqdTpsg>8$s!va~F{V;_r8b6{h4vVs@0Xtb z-a{qjHa+1Et`JB*pKHM0A~(*-F`sl#yD&lTF+#(&DUEh|eA-hyW1Tk6J0FAdbbwA= z?0cwMo)XSo|L`7c!+YOa^XNG6;k}1T(MkTig#}fE%6?Z1Emsr!o6G-}8cg(`idSWD z)Csk*lI4;APHbV2l&M+JMC!c3!N}5jR9m%SoZQeT(ePcHeF;map2eB?b41OXo_#aS zyb<^M7yDG4T6~@<_j)fUi`>r-em*kgo)L{zFte=7F2h9g*QGiG{ey$r$Mp!6+{*n^ zPvH{{n5^&B#=^<`b`hEdHtP|wzuyEWhLzP&qANyyirCVX;}wYM9d$wL+dfurHuIE` z9g#!SseO0dJU*NG&L6Dz`p%2Ye9iUjlQ+pvGrjrjq49cg>l$9zE`P+%kpoo1xOBYA z2$ASHozY?;EH!KoYl>Mpol6iqH<8>9kTEZnud18ez2Qc#bVz=_19f?cs@ZgR}CMOYT3T1lvf|+t0%O;@Dzvvzf|*S4r-e}Z#b_ag3&{p&jki^8Mu(=R4U^0JDB!snDV zkzwrKrRYM^Mr1y|)jjNd{PS9p?^~dLNCB8v0^G3TN z4*unYi;oVawc<>7#)-#Dm-_{DjX^iT!P*F9XlVAv&*h%>fD2)u($ho&AxXOs&%Ld!rA4z$Kn`MhIZRU2>KyN^ zW7e$yq^*SXzhoxeaa~jz2zn(&*j=1*aIF=U&!7|3jCbPEijO$|booB`Re*7`+H$1% z&pV$WrF!Ls_D|H}#lE#KC-nAP2q$+~MUfn@Q=40QI+KD!Pr`G%CIIyqV?#6MP?$nH z82wBM2nh8ro>D6)ux$+rkL{`m*dKXAeamT^+Fsn6U?zKpq_1?v%`{PPny29(aoU&90qFV%db z49!t)t+N;Tifbt9>o4TX-`W&I9h_}kB<9BCWKxfEF2^3ebV0073WDrPyCQj;>^Bnu zgCQ`&WnH6;nf!T|S)+oY_p+MQ5At&{bz?s$q{{p-E4fT7e*n9$FH<_01=T4C_UmHH zbe#WvTpjKIM1Ytnrl7}dy;Aurt_so${&X1`?~7fP-Q#d#^Nc7pbUvf8Pc?}ldJgXO zY`tO|`_!W|0>(jvaqM{D4nun(LyY77=S}R%OVB0pXfg-CS&{6S7}?;ND`($KFj#QA z5Jln{s32%Ezg{ylXLcMbhT2h^AEW>hG&Zqg%ZW`A6IKaV`w%0s~dvrFsKCwgZ1OI^(%}e$rDk zzAzY-D7nS)rKvKReA^x1B2bK?f&qtMI82I|o*o(H%Ah3sse8UAo%PF+9%2ZuKiryvn-ch4LdE|%5SCVf81C?p0I2z#=>VS) zGxvJL!P#LkpP|C2(02&bC}ldcWRQVy$uI5p0~A(8Wlpv`AzO>7C%-gc?8G7kDqN;& z1=95ZrArRFl!}8X#=FtE*Fa<2A}3KGP9yF$e+k7aZvG3i@_GNdIQ{6m9SeNwsTa;8 zLJAo^(Q_-v9Yp~catca}%u`TU);4Q(NbjJ6A4>jU)NuQc#v6Evyjpo*^@irYL(o)a z;{%-R;+1v-eQ*B+88|ctnILxxv0x8F7~W}*2%XeSL_0^@H+Lml3@znUf3x7S7@+f| zuT4Uu?zKQJMPglMAFyI)BNE0}l*f$-p`JtMg!eXj{Tu25X=!xn`go~W1 zoaJ+inyjX}EzTr?RXmg3@ttH}+8o#0z# zm0;jVN~*k)dddX45wi?%&Y0hEZAq7Y>o}fgRLa8}oy9oCN!zn|g39w&=sSqt#%`G6 z*Hb**>t*eZM})-nzWcCGnZQw-lag3mmujGkF)?<}HYq67kA5KeGNE_`_b>Ym>d}ng z4h%|M(Vr-=X?9hRRXBdDrryM$+OovjiNDFWRE3T@BXh+eYu_1c6OOXkB01_W6!M= z_|$Uy@RB3ehQxGrrpZwi0CtXMm#OKLg^3?{)4JPnxWU-v0L=k^yRVW{GY6{V%-Md! zUnZLKpb%?E!9J+Ojo^czZThQ&1FFmy|03WEtMj74dh!um!{r%#{V8FF?{wis#?#CC z7EZ!ZO3G9R9v~N_$X*HX8l4!(g^_pyv6cD?QCnXNn=q%Ge>(C%$W&TSME5yH-0Whc zQ#?CKg&a)g&esO|Q$l?j=9aq`+j9M;0)qLC=!GDiMdOhG!g9DOU&2K=zrc=0JbyV# z$PB$*8_i2-X{hqPQViUW$0-I);BF}97+XC1=hSl;jsmhKE(sLajp`2IsK&}!P;9{Y zGq2Np{Pfr%gk9QfyKcw&?4=o8&cpe(CkZ17%$<|=WZIF-t-V?(`ZeTq|Lfjlscrvz z!hofHflc&Sz)vvhm7N@K6q|Y# zzicW)$NWnHelxwIx5u8c8M!Y;u9kjf1#seQd;&Lgaqq%XUX!IB5HR@D2J)uYM4JZZ zCLZ82s)^{BORLp-fMAh%u@~s4cizH1AQ&k>ig#$X@|Fzwv+G6WVlMRcI>6Xizb{s85F z@cr74HJ8yB*!{jaf6`jQXfmfWx=BG;_DgfrOfycHQx@I{$e`5Z|NBw3mlSZb#tb^g zdz4PHZ!q*_$pYqn>SJ665~7^oOB)TUH8yH=s|1p_UOXrB-p1TjFf%gJU@ngIBFVTd z%L3t$bWJn1H91nKbbdd|Z@hh6Pe%=zYetQ3eP~7_s$(VQ38$aVlj4@KEpSNiX&tc$ zsQQ;-tQ0R<`A>shN!F-XV_SPUui!zkCRqhj7{*!tHj{-<{CvDj`evZ8ZjuA{5YQ*- zuDi*7Ec#_eNPEq8##{`2=s=Ka@XV`ADcr>vX^vCH?dnzOuL72%{t;=C4~$_hS|5Fy zJfA$jQSy+s>w%&+bp5PMUG%YV^B530))iPSfPH^x|vQsLww6 zpA(i{`Cj0Bt#Z75H48@-e0MGEwA!+tG-JsH&^#@%63J z6+DAPYBA8AS44{hHAeaXKvQA#>e}ZQjPwhJ<+5c==BVfQ3IzoDqddkUA|ljbXg0rg ziEFMc(l*JWW$qt^ye*%7%djHZ<$ozgcWV&@Vu;I+*Hq7)q6Ay9jtz(^wr)LkZi|R! zpm7&#O{^*9RT*`VxZ$^!kazMjj;GjOZ>~@bV$_p(z%Og2bxsGHo->vy9zCA(N{Sdx zUs8O^fsuayuXtX5b|Ty)0&F~kx?<2e2lH#*kohFr)4|g%yDGU!8JS6`3gc*`O-mr^ z1;bY>%6?~iW+X>g*J&Be;^yp3tusrgbfz@2F0)I1P`CC&jZ~b4fNkH%Rx;<|y1XCu zJBU>tA{J6JSK`112DuG>~y9m4`1QGIX~&z-t2YxuVTA^Ptc2x63&nH3qZBziOW;? zxuB!q5B5P%`9p5|L!X2^2)Ddxd>-k+%x+KpJ>(q1B#g)K9Ds#an}WxQ=M3ZLEw%hEP<56|By z$2Dy2-(+fWWXSGH!)T5=sdHu?)gO-)Y((d&PY*mE&rnazd>L`6`rZ5ZsKsJ*Z~Qdg z(AB+iQv*zo2OssM^ZfI^Jj!R5gPG)JUTrML*o2D&L$mYn_c12Z+ulANN4$OUT}a02 zs#{i1NfWF(k+MiL1O}^+Sz|h z-~%(bX((R^ymgyjOo|~yi>BcX`LzB_P8bJO1ea4wtMb(*tKJh((8r^9Y>l;bG>%(E zj#))-77Et9Wut8kr?aF1L<4*C%ubZ4q8kx$X)>^hEyu!sAqig-oL`EPXjc`mVB?fpkLOFA(E)mrg`sNlWSjj_M3 znNsfi`;b5f&#O*S{-tiq)Ze8YQTPEX+$f}FbKGV#S?@+-jKpVArPml~jGzT=#U^9S zXdNS{zo%$s1d6{DRCmfZph2>N0OKCFMC3)~-`6IY9ZlCm&e=M(BkM~$y1d;To|!4! zlOy#*@Z7D+Orp&eJ?_x5Wr|H5OiyPcQ8vZIk(*= zVaRE43R1j8>9|?c?L3bV1r+SB015yyzFW!}o&eMGZCfryfr8Tt+Sv(+u}%P@-qq3a z&GqLbF`nf8(DI*)Z+&Q|P(kx?M|P9%HKzgoAQBPPkQl__qBw`g>Qd{bjL@m)CUi59 zq#oLANJ%}yZ_Y#wxr(8M;QID24n76#Kc8Tl4cuKYPnQbxN#p1ds1P9r_9l_?89(t2 zf+VKKzBcXx5?7%37AIe;8Xwkk`Qd@FW6K|j4eri!JuwyBqY@{1-?WC>)QyR1{YwK* zf$Xt@dKZ!!4B0DzkeO!k%TRj-?xY-6Tjhx*=DZ*DDYpQRtu3CWyD8#2B4|RSkJiGU z$Q?~uId+2PJ^8gCu8%@r8D{QpJKbwDR6@^_Iw{FqzFa7)@Si<4J*d{Q7+ZKyzRU|j zV3a?(tdf1IIr&g5QP+1~nx!9oJ3G7_reE2b(dYz3``Sc%uvX+1O(h1>Y8RCMNtv-@NB-AW6~cA-i-3y*aSKU7W9N58N* zJdID7UI$Y;N|4@coa3t0$}R)n$eZHN4ikB+$s@-aa^4Ivmp;nb5;(WZK{h=H)kF{G zpDM>xuQ={$eRVL|%6Dvb4;eh}s<9Vf+<1p{kM-;muh-eNuU&kUzVu)Iht0opU ze%>vTBcA5?RM?K%`5cSU+>XxO7iYJXLMRlt+*X_fp7syAn`lYq`1qmq#CVQwl=|2+ z+?L_U=D*o>K+_dRQ7`P&GrfKNFH4TSLR(t!*6?i;Dld+pt?<{MIWu*r*Pioy163d^ zLR_f)Vixb*8d4!hZ$TP;8^JG_E!K?7&4G;hbcfA%^TVk;y)TwFPy*RY31kx+pu$rn zqaQd!f4Td-#?)s7p+fb2UP7KPE?jF|$j^R8f=s!oO_rixonpb#SZ5|GgRtA$ z_AB=d+n0mVkqpp_kVX&{=ySMtdx%TQR=+Z5R6l=gk}_SObNhD~8i{eL6^Xpm9<4FF z(;@#U=xQ#b=6E&h6vLpHJi9f$TS%bTpdhlF3n4+;W{C&>ua^5MUU(>%qk&bfwItyJ zia-n9;z)8Ziw#A;v%GHr>;AYz3Ckz={@BCPf&P9T)o9j7R5pkzIW11tw&{sns7LV) zpXm5d0S2CxzZU7t88ls@LCe-FBKJS!TG88}E3b|G5M`bDJOH1$X^$t0;MEe{io~JhCpY@2d}b zRHg7awR;j{Kh=CW{h-#hXMnznhB+C-;t5O=&s^}{9^L#I9m_KBLYpTrpzXi% zV69bixjE1)dm~I)0E$y9SVFzZakVB!# zG9%rUdZdSNePNo+^4+Q7_}bs#$>*(~EX&d9=4O&C_+5!S?MO(>Cvl7{kgRvJ#I|h7 zK8o(Ljm%Msx{|fjx{z#5FCUfodn#zhcg;O2!%vRwmCk6*IL$+}cS<&A85(8aebZWnCVqpl^oX0(kECN3#Y6{L}$uJI~b$tb5Cn^iN&OJL4a$ zHb$BHd1`6^LtIi@+A~G~{xZLw4g%*{u!DyDYr&m>5J$zn)-Egk8&41FubP@UBIlz- z)fQ=|&U*VCD?ftdGX3BWi%KiY&FpAFLwDD|;Sag)JA}S}VZS_8uHWVo{8*%7W>)_E zuDXZ52xFt;Bg50S2E~g--Ft_td#;0p3Vy`x%W8D2U$n_H!*2H$1dA}`8it1-(TpT?dQ@9gvnJ$ui5V&kCFyuH3F<-io9>Ue25VG-V0owz} z2#`8=eEMrG)HKEbH&=BW{`@mDo{MS_+q^x{^{aF7{V=+)U2(6~d#zIg z6W{3_(5nDK>Z@~M)h0(2xp^1)LW5fSvu{f$4`({IO__jJ+zbk3B($&ZoRxM3Q2aa? z3Po3HAx_cio%`@hSVWkVM+i(%M_!AfdEEHVMX=ATyXI(cj7zBcKTnuv(nBLbhZimH z$6YL%#a^f(^nypT6)C@9^-QkeMUkIhm6QgmFn&#!{v~J^(`8fXdO3OtZ<`eFV{Le6={;=;|$SBlQWT<1e{bK+y}|Eiw%&Cq&z75@}fmpb->qopkB@~ z?Fwe?AQDQ0g-4NDq9E&&Z@?^5&>2(Zh0$Xv!LCt9s>R9QU++SxBEqz7Yt}+G0d{0% zHDGJ={nh%3BT+V?!RDDWuNi%|X!$bN=)OhIm#tTyS{C{Y;}A#EkEtw4mRsuC2}B> zL}7-~`~rj9ecIrwNJ*E}0UI;44xeJ0b;9bO`o|#V*WEL<``j;yQMaiu=7PxSI#zeZ zBCOJWuje7h50ByG#P$Umx1BafS#nM%(h0xa|Kgh!tV|B=RI4#TIuVkXFwq=zA{Q!s z%d6US<|Mt|3wWV7q=F;cinLj>3l=`<&AxkF`AJ@NQkyBV;Pl& z*>qvN(@ay~9qS?g&$G_1US+w{WmRr1gG&I@EG~{dL>uC%OU4(?0N#KWtB{ zRD!Ju;H%LR12#pmCXTz1a9%%T2wc_t=<~gvHTxO6sRbg_XYig9rm=DU_K@TK7N*uY zQg8@5vD%*YkC$!kVA`F~kk>%cCLOexk^;aEHWp%=E0`&)m>%LW|0FP5BZOh%dd~VJ$KYGlvR9qvcmzfiEw{j$Jn265QsTwCTt=buN4p@cNv;g z9#JS))9t>$K-JL(LANTMb4ro*&}74&36g@p3=~n({qXSX%Gmk+av*VNJ$Zj;*qow* z#LF!AE$>i4yLRSr(JOyL=q%y90k?0(ep&1Bd+u=KcGJhP931uNr#WNi8H<7K3LZsX13eg>GgSq*c_=lsg71 z1}2X54x~vGu?n_$X=<8Fa7s*6W=6 zsUSVQqQOLls5q$l<3aKuWnz!SYs< z%`1u~*-<;@Y%Az2Edbt`w_T~qL31^ex$$u7QM0q=KzGLdQWlWlsA#^@V6Ovb+un46 z${~#L;2VIPE!1Xdtm)obsYL5vuIrwW1GP5L{HOpM2Jb{+ir0d)&BjV->9kTT1E1ku zJcX-2%KRKRDNr%|j~p8=o4n49i!i^?9Ut)5Gn0Q@UQ-O7K6 z)RTJq$F>&!?XD&jK(wFczCH7^DotM1`fbasoZnPfCUijlwX54FHCIP-vhfK2?kv6I+3ebjEP3B6#UD;#l+=$sK3^G9Q^wqV_igILjS~<6BD17T z*!Yd$OSn;E%GTuo!>^oON_~;Sz}OBuzsxU{`4>m79P6x#UTq!GHJ$I!9D)yW>dEDn zcaD)M)yQ$l#ue#LzfQWq)nM1&1E+|=yxG%m%zMDw<#%N9kV%1(Yrm@!ep{FFg}ro! z(x^Hr9u7EDiu1IRV12#zg~y*pe&tKt{aPqb+mFZOlqbkwKd-HjEJ0^dy?UEZUe3qr zN84)2%S!vLbPD*JX2OXgp5oh2rXC%>=lD9^6WLa4dn;r&vh!D^1FJW+iTh97L$7Ky z*Uwq4By0aFpAx7bvVe6^!t3>iP!uHrxEouUFr7|*E`EAtCb>Ka*Nf(msVR`wDd7?x zG`4j~y!hHlvgiZc*y8ZtuzzrsmYZRvxQ1W1-8o%QRUm-e=m>{1W6{anJ;$t4!ib?F z_HM^foLb!9)`eRrF;xq}=#4l&LGz`Q#T2ra-+nyg;B(bJHDw%SocajIRIjKl>*E#a z+?Ae6`zP2h7Tvg@Oxu(i%Q428;2fJNa1BB+-JGpcKw2pu;R?MZWB;K=m!=7D2Tl?Jy^&M?IGd(xxE@mLtOC;n^U1#P(D;ToPP-%Caa zc#9z5wrX_SVHF&TpxM-HUDR>>3E!fYL0~SU5m(WQ zi;8wIq{_c^R|Wot#V-B7WOm`Gz!kq2g- z;_u|dJ!fu3aV3=na+VolJ!Y%ug=h!d0(Ek{$6Z`!n!u5(8VEnQ&Ld9x5ajZ}qs?E~ zS;*>^W=a}4n4o1~rlj$09=o~nWrpW*Z(1%_`EKL(i4Aml@5H%=Uu#Pul)a%=J=7J@e?U)MmB8Lg&|cF0s~~!p5D;a*X6X(T$*6*_%c!kV38> zcgrk?LH^4ZCV$UeSktU)I{W4D0=>gmqUTRnO?EL zc){~u|J>8~P)-p8EhlCc87{c6213hKy>;INPRjdG?buHi`zP#7H@E(;dx>|7fbU{) zdC@C_}gD<(6&q zvlRd=;o}2I1=94KFxPPJ%B~+}GSDUA)z@CtT|W*EeHR%}Nwc3Q5_PEh*W?k{U`mgl z6^5~C>u;bua|B^@oH|hepEN!S5kbn zy(^mQth3dZ>E#DtOkQ=LH1fvE6P!1DvwXLPV*~iSRwwQYkz{{>%~%<{Vvy>B?of`t zs!}lsG|@KidKL$&O2gCm zs_1(~SoGh2N%h5;tW_);0X_tL`Kv`oKFV9~zEO_xd3@tU;y*)6vC3bx z_hJW(eaz)VDZN%9**D(aim6nIN9Vgao&*?ArT!dx>@4ZW*<~^4zO-9R1C1EghBGLA zXh}bKy_1jjod-al_1-I*s(0C3+>~8HroTb}dawga6UC`Q7A_={Uq|oCgoV=qui&x_ zWXbe;4Bx8P2||kA9%2j_+t~O1jgA4#%^8x`o@eYb`#+KE4SJ!p`LdcrqsP$KHmYW( zqIIeG9UcgNte{L~dU=fkUScNMRBeRc$RYo?A~>V2@0m-l%Q$=NH<^c%)JkagaD zK2I>dwDJL__x&rElFA3KGtc1BS0@35(`T3PFQv^_Yxkr1-#t#qsICT!CVB-ecAQU) zCC}Kfr9QSOYwk)YyhRG7a|F(QevP-7;6Z8mf%lTKO5b4kwi{8$9Hz}2( zpUzE`k!?so=#Qf1Jc2ZqNy) ziD%#%Adz-yoUz&aObK7mG$m=f=l*%HLhT zL^u7`>yKp!JHeNe=aS@f3Aephfkd-O|$Taf_~Jq-alqwHa0U5`T7&*fr@-hHJEL#wSP4H&mU z!m@3Ydq{sFotpQ1g%$YrdVhtS;Z7tt^CE2>{fm`9@*xhM%sPuNaWxxa>zg>5%fYP< z`X{DojbOfS^c?7@wyHCxX3%3Xhnx`rXKe(D-`sa^gUB<$NhDV7W#@NPw=Asw*mv8g zu?Qr zA%q_jhHVEAvKn=+SpRAFq#>3Py%k6Eo}*)9dxL$&U~uzV=+-GtN4P84u&um+9`Cbzu4PLkhn38%r;zn*iUnt}k4Z(5H;XTM zOsK!<2D`B2P=0d~atHii73qq6U;^8$lBv1l@N9r@Ib+k^D9{k+Smx;iV(@8epL(MtiU z2=Si18-?87!>b2^#F*N<1zR6vElnrs6Xie9!=J@u144Zg13b25@;iN zK|y4-EX3^e+6SklJFTaicyo$?o+7K_6DYZ-nhR)%sTh&c;|ToSqpz#zqI+Gh*o<2t z0rfOf8S_r!eso#0C8{ro-%~0K$sm7Oag*_gcmW}5tDH?JaMGD+W)1V&peZ*vLp{cFp`5-=4y>KH&Or)J=KLN3Vj`a>~EpZuDEJ$SP3`JDJSM!j!1{ zDZ67t!&Yc>=OFWX%bnPvrJ263Gvl5%tjWf4Sx&zLf9e)~y}CT#p(7PGN;eeB5SA4` znVfJqQiQAxr8=5Qgv5rK(PWosjo&2M3M*p%#qxP_xEawCETNl}a?Z zxHBFVkN0+%7IF_vU0HohFLf<^SvBrL$nI;&4CjsDI7s_fXGvNi4o(`-tWp*EYwv=& z%3mT*+rx7|!Bb96s}I0;KbTR)-kkBcNg%xXIQeeo z;^p5asCCRz5OzTCTrucyLh)jLa7i^phrn9%&yV}Q9>jC0R}-F>?~llp6O2UE%Iw5h zpi}Tudy+rVD zM2>$qQJW`>BQW6FnjPiO1tXesJ7D01zTPKZ8UxWpM&wHDOkj=htGC_W*y?~EbpZk{ zup82Y63)Z(I&zmMpZpSoP`nC2@VHldxL1WGPt;!10Bty}V5+yOG}-FlnIL(0AA7OLoM zX!ZX7_|5#lhWi#2V{LdQcFlozd*ihHY56^I&LaxG4yHbRlFIjp>*6XPu6><{vl-- zBhI8;S;14yE-oprfG;ND=8sUo1G<+pb*~x%yW?D;)-NiidqzhdwqqNqpupYUrE%qC z*7pq0r6Pqsw}r`flBkJu4%-}T@Kf>^v&s893|g&-F)pww{>V2njxVmoZ+u=XT`q1? zPGRx`1N5_|hU6tlfYdW<4|Y0er}*{0#`~~`czP>*zIu&Pb~V4IBr$S_|JT@(2@BdR z*zt8d+ z;HE@qhgP({#^qW@gPE%ro*Lduzb>Lp{kdgt=mFXxrT2G>z4T+ z4I$w{n6Zpe@gFw#)sp_=XX-ucBZtq%KKcRXb5{AX+coc!nORjo_L^7%DRq4mO9*s;X~CPGv<=^eHr>B}dLFzr$)62o zPiUlhJie)T+T=5T_9N(dQ?>B|^}hf|&E)-tjxPo6`8e2TT1Ze9?)ghpQnpz&v##51 z`llicBW<+pyIFhpu|yRde4a@Y_V=#g3n>J}Ex{vS5)+_at^DtS=%oz4F>TRXt#d2) zEWO_LMJ4s_6_sA^U+a{JqEyugxq!Eh)Y4|abvLfJ1*H49_*XhcN&L{EPfi}{^mArP zy9;6Z(jEp~VvYu}S&u6T@PDnbs5;xYSKs6ET=0;s8@%5NwX5dxa2SvEXB&XeX?WAF z1aG@ixYbH~IiUM)!>Y{dw|m2EwZ|xif*vzjvIqc>0@8vL&!mWVoJNG==C8)WF1NPh z&-{=5ocaGXdP+9&0vy}o<$(u%fADZ0cKM5w!`ZEiFGRRc+UIou z+PNpx{5L=KINx2YqkPyKd0?jsjJM3a4aD;EII(l&OHD(kCGurJ+bY{MA=r8lJu9D} zE(`C0MxB`j+uND1h(T-F&lcCtK2K{JOQ^(N=WBjk?vHDVP~{JH`g7)?K}Q9f!L_)+ z4szPv(*e}yPk+~VR32*a-fnMN+88k8E<>{*Kgz%l@JgnuhNgxtG>;QF&e}_Hmy@uI zu5~gg+&`HVW}Z}M9)LDGPyEv2xsPff{G}<@?29mlIn#B_Zukk#9TEFO%g(`LN*Y;V2h^cdO3>$0 z9{X#EXaM3hmf+D%lNye)NUnXm&?#D=Y{|r`Cis{YqXDP z=gQx~8_Z-6>?JN-s1n`bDF`$G=Ud6BZ}9oW+Xm?M6X!dYe%AQvtdp|hCRz9?w&nTd zZr~f1PGZlD1^Xt@k)r;plST5~D13ese>RWCd6Q$O9!gH}GcDYJ;TjI(%8Oa9PY$DL zIVG}by3BBCPbPuOn36=4=H%-TDEffCkx)a%!ig>aYdU?TiifF^fjY78f(yqTa5Ig52OvJ=)8D2P zBg%$I0h}??QS;kj1y6ae`qC5e^AOlvIi=4=XbEPW)S^+LxAa=$&B<$eq?}NlFWo9G zv!n{v4!%i0p^tpnkwG>R%d3Z<*wSD4bMQ8H zxBphpuzRu)vbU0|rwapj&e9O_n2L>4nB4n4X8$_p;a9nhx~-QHjL!VDYyPyi2C+od zXV>D9dx(N|%YceciL4E>p)LFPi z*+uPshDJ)kmriNv?oMe0>6Dc2j-gRt=w<+=LAo16x;usxn4vqQ&-h*EyUzI+_Vuj2 z*IM`Qz8~1_`ZMBDIBy72=}P{8k3te|1p{m6N^vuT{%)Q}+Zu(4*U?g4F|TcgNXcZ{AJF-F57PAD9bmQ2Py9%iSa%**!2G#*|eJE5D?%v;abc zG>Duf!tSb1XH?_>jzH6G0wPJ_<2L@ZC3)N2kAh%|o`6!wL?=Lqd#u$!6;TIV`GzEB zCq%~ub$qgxpSj)va_STkcN%IzMyJg~Vo@vkmHEU9;Chq$q1e~C~%zsz`7*nWY&sDZ8xN-s51UW;Hw;-)=)zD#lS^$uNW-g8sfIy%e89t6DD%1~ z-}b8Ejl7K%7w+6O$!_9Bxm=Y+F9{3Yz?k<+$-z2)?>PQWsZ9Lx2`SNO9H?{l?g=4U zl$dmRE##>MN!$N}u&HuqkaRcB(gYAbZ6eZ&itGLRtNJE{PnPBtt!E25slH&OD21bs zmX06;Lyvx?S0E#9{Rr8|^_(b?th1d7HmtR@%EZ$~Gqoq)b$2tb3R!!mXz6Ex9DBVH z!t^q!LgbqN>==#M;~@d!cW!JL099x!PT?!N`dWx@Vo|93KD8P}n#_zHT1KY^fr`KM5R?!~FoNkn`XXl_e{s1z(L6C- zjB*s?jLOSLjXt93X*Q_`B%fHdQL*Mjvu9ona5@@OoRH!UL3P_swI5?_&g+y zvg3_0Yqw|zvr97mwp0GbssTmVM7k}oD69N3z*=EiPA2P=l#N|Bou5QKfx-`45Ui=? z#k4X=ISxlQ$ORji@}CmWp)vlr*t*jk!Fz21Je1e#r;!X}Omf=^Ca5G`^y-RlF-Nf! z9o~j66fbdDZ&owv(N-Ih$x6y0+1|X9v`PGtdm^jrp*ruX{8L+f>k2vgRe@LxcQ_P zefpz^_8PC{&nJK!8JEHg?;UldR8MJj!X|_y9Sxg)Sq8u7Z01`}nJFVMz=p}b`#cGG zdh0}_f!r|Cw+*RqC+jH@61!@Qk{?{;>!)mDf0=dFR1S@gmEc5o)65=trx4p;A7RAE5G<@C9DjS)by%HOO<#ZNRP{3mz8=G|4)%$n%n?b_ zxfn`}3eCU_gaFHbvVO{0Tno>ArPy2N|<@ zI8yL^?jc{`)SCdHnPbb^2-2k%a$%xtBYaczej{@II8^hN0#xA<2ev7Yl?^$1vLUP2 z(}*unh@B4g%doF_YJL8AF(Y!)va1bkyZpPnr6uw3Zy)}^zoB8X8oYLx3n`x8C@V~T z8iAw(J~lOKw-~>2_0xEFWZqYBF2jrFjNKb1Sam%Q7p{_&mKCfp7_jd+kW5RmsWe%Y z;UxMS-GfFGlhXuh#;&NQUz`S^$_sB6xdLXc?wd?_u&{;8XnslifpP?KbdeJ9;R1fgYu0hTuTIXx+&88Xi&vn|2%*F78JYwa@Fe{KuyONuDLDz z*Q$WIH@mfk3Tp%gXSRJjCA82MZf?6@7LxJ$M|0UwcN$=Yd)d#4b>*~cH6j5Iypf0I zdWOorKfu?`0P&OD{({BpI-2|4-QB^MhMNl!=k8r)vIB*aFbMI#1@TysC=Y?}ou_kv zeYeEl<#EHNfT#|)t0m7SS5Q?A8+Z0$qja3Y2(kxx1%)}j-XA0v#}vnitc#+eI^U6+ z)>l2zi)?0%YjY_Z3(pN7`jrQet)(P$_2B+{-=hZn;Phdx8YcC4~M2UD+S+!1S)s!CitHvdrciZISSPle&3oRU^Q%#5;A z>|Q$J<_V$InCKhJEv6TU6esZP;0m{7nx*;fyE3~PQ&=ee}MDriH^{hzF{sGs&B zPY-xwMKP(t$ydn&j?mh`Y|0-|RQ{joC<`82MX%046gRanAVoTsSZZda_wTL-(+Iud zTT~UF1tdbTDa;~6v<|B#9Hr4a=^tNoIXVi%d$b}Ooj$lIvHZsQJY_+oSJBCmbu5hAxZe~} z{sTBtjSRbRs}8tvkEAxZk!F_6`trY1gSO>vkd`*obAWezmHRhxR2D64Dc=5^8% z1o-HoRHju9p0DhaNTO`cy~}5nlI5XXXg^Ut(ZpKvIc9y|`11XqT7}*VoAPwH=~u>M z9IM-S`ipL~_*52j`h1u9Z~vHCdnikOZ6(lgm-T}w$b9zKA3SOPAG~p1WuIFLXu!>A z8CqF{i`k=s{81zl;l4|w_AFwT2c=M=x&jdQ`nz1laApc*yf>hEe*pN|+O*#MK6uR>OeRv8E26s^508ih7yu@& zHB5|f^cwJDu(WrpRvHE0YteS&CdMnGTMX)iKQ)4+T;%gUkb(N&Ai8DbH(50FGQ2%$ zWm}H3gsBg`2leO=pUMj`LOX+AT^i09t8@iCPJMS#H2ASXWph+Spr!!x6dTqB7(eK{ zoL;r%WstCTUNk|S?A3qE^f1V4VA=muh0xOc1WX#7H{4Mcq$WIX zCjg`#ddUJ-yC#l2*kou{JKG@96W6Lh$IL8{oC%ZJ&lgmWs|8q~q}&ENFVMb6WG;_` zJ$!33^YBzJE7e7C{pHhS;QEj|%%AlwMx#4Bi%y@OMoxhYqV@~?+_7MaOLe9>0t$0- z@VDD*e$b|Ztb+-Cl>K0k>9#QIe=RhVAdQDIo%%lU?LJ#%EaJZy;jO#IeLFUZNE?#X z)1Eeo=0FH{y{6-nhSda8TTgCj2%*nb+jU#P9A09G8Qj`p&N0pcV-w?1A#qNv!EyMGfC))-308)_w$&=ry(`(qz)@M#=g4b8tZ#wosXP23yDeb5SWS1GOww$GZ}dCA1mL1ZGcJy42{rap zfJ~C7qeZseCRw;|8Oi854ZOFsb)P5N#av?B*KmuZ!-vds`tMM7fv;qhL> zR8!6vO|5U9sfRPckMfP=G#t+7<+cxc7&}^ZD_MM6R}cO>*}WG}h{y~<;Me!dO@GmZ zHs%+*x8l}JUh?R|cIZ>!8cGb#SkgHVFlEAsKGr6crAM-sA?dTAG|W3HY5JYso6>^sZrj45 zB*UpIY@t;bZIZWJg3WCExl_dSf5F}xDZ5=cCX;xYHhj78c&2>Pa|$W3KomuZMbV{0 z9x^)ajskSKy1@`a)_l6;o0+9UT~u~f^y_#=sM+5pRpf)24XZY|t4rck) zw`TCK^?Pw_H&Hn~Xvmt_k!bM~lbB4u%I15sUm}As1g`-fcV8O$E-GI4XjmdoeHr_F zITOCA50LyhixhTKaW9TzBuKUC@YQGOTk-pp{L~(8q%Ox1q&E|hE1v9zN+l6tJ(QhZ z;kG}gz`KN;XxUhdKXLA`-;oss--ARA0S%O`j%ntd?$P?M zp#YPR6i}2##eW}Y;Hss%ltN^?whI%4Q*3~GJS)2f22?uDnEJX}Q}sv|B6M>aZV{>u zhm~CXtR{n#1(Cta;()mGB^Vpm8?^_?Z?hX|%iyV4@kOsadsD0zdvhd$wWJlnJT?)m zo9dJ=n9|$Y)V3YMQI3svsV{p;*6z^o&GQD#ZLFzQJSK}iDC1ACM>VSRVbS3e;EGWl zEq--9&-}Ug?f81|J@yn>=74h75>dSb)xUe{?|%OKu;E(uR23+i?2PynhPwwQ zeIozIlY#y;(VbZ}`S|zf(Wu``iJ(R+7_8uwdF%sEdD`1)QWJ0z8Qy$55LL@sogKwX zDL`3rL-d+?M`=jp9}qhiC6j?{33P|D(vy2LU2)@%SpcxHV&rKPE|BtZvlEt#L_w5` zIXrMZG@VoAou*k*&wbtusW4-!FAh!Vttp4pzrINQvbLY;`(;jy$qWy&V_#ZADk4;$ z%N-R-&+X*@^)eIG{K`2;i{7z8l%liQ5js>~+*uA-Lf{HsWwib{%8S2zRR;w-JR<%! zP7&{(X2&If0dwb5m7q5lHH5$ak(0H-M!&3=PnrVQNZ!6Y^B{J3riw|~qZ(<5!L1suwIU-PlV%Vsjpd|qAg5x#I;W2E+?*ou=mLGxcPcC@vHzU z!35IHmFhJ0`kbKv^o&EF74_{}HVvS!zp;i9fu!gQDhDq?f_JLfG_h{T76P}+ z>Zf|1x7H2ySZDb$}t~70R!b_v`n-f+*AZzh$1tWv%h+sRdtEwL6J5 z{#jAP15h1C{?oT2J3VZiHg&si*ftUg@2DKsJNA_<1uMx*uBW!!E?u8~RH_tH@R>P{ z&M8fiEl}dgX#pI_P7t7_BvTVckv-D#MM!WtdEBj9h*M^Eq>%cW(0Zd)lYM~eK4}bA z1ve4PMCxV>Ke;o>$xzoy@q2#fiC-xB=s!J;pho@W1N%I_Wi|I!GN8A|zRP$*t7awN zA6c3(E$hvXn>*Y`9~U#aeQsaK$<`TkC!{4Qd0V{+t%V6el+8tGXJ-2KaD=GI`cKhe zCGn$$z~-xi9}X`A4!lc>53`5!&^bx{Oh10DSJlTzjf^_2xu(li`PspTJ0;^k=LnTw zf!BJ2k;sr}d2wi*yK)^ZtVJFQrsVaYemxDQpB$SH8g@IA)SF(p#17jv4Hh}O{(Tzm zA>Se%?>}gO@&ZvrZhA!&&DQ!}e~z<*ZGsnAD^CZ0+@OsSa`t8i2yff`IsN$v{>QDf@axW-0Kp_#=GHXI~gx zY1%xvLut0!hNqnl78iZ7KQ}A*FqcE%TK69@7rNzYr_<7zd(6dIMegi2ug?H+zN~4b5`J9acS*WqNf!VKpip!_l9fb3R35;Y}OxmHO5)2MDPx zuK2-M7Sn1{2bm2G{{-%D{qy$H0;D;}-z1;8P(}u`^>E^XUY(NEM2kQWwvs8u0%?q7 z6Or{=HZ9t@vGX#y=9n1nHjRnVq*IJkC^zqMm<{d_577+J&#}`jZj^iBTBy}^+!QV) z3bV-T_D1=&GMRST&p%@yVuuZ@YbNA6Qa1>>zso2c`s*3LDF*eQfsJ{pj2xVFpFV4r zt~aR`#l2^y+T9LwjB51;JGx(OOP{h*=f<8-h?T#*fxOuy@SS-`!{R<^ww5`8cs(dq z>O6F2{=BQkqZBP=*hj?TPJH{#=WDJ`A$NSZqzyN#&}dES5m3I_?%MddI}zeK>atCzH41Q zoElX&1fMIT<5fAtns&##;s4v}V(Omrj&FUc_CNFtle#}d6iM4J?!}@l+e;+Uq)fpjd{PiU(Yzi33(!1o;Rri%xL`iUl%LfnH-!%vVeQ9 znhj59%eyt@ZI(~&bFHS4VfR#VJ{4ru=QWz&BEOMKvhfLl{vvSPI1u^i>Z@T}(*Y)5 zgKL3cvsN;tIZD5?5KSHetXw4BgZ)~bePn+ueQt7QoOd`2X{Er}`eX_I=&L$jW zET)^2HIeeCCLCQ}c3}AU<$&ZX4AhT0bp7S0Z!Vg}2F%U316f4l!J>-fxgS2tO zKCh2I59^9L8)P0V2{XRq@i_yDzbLPN%62e^mV9&u8#6&HjF3Fu{%No!g+pXS8)8U3 zJ?kwy1y7xnS8JHRd-~@H{>VBUXsyZpPQvN&*?exBGj~!`_AWIe_*N`UvEECS*MhogXV4#gxKG8uLEg) zoKJ#cXTy-gkoN<;fUvZ7uzlY!KT7-_mL)eZ=Vo5KY5J9r8StIif5}4*i@X!f#rwl! zM_G~Vq>C*Bto)|zr>hG&hayNLdM~*-{y73Rray2JHuYZXF&nhEL+gcR#hA^|Z&w(A<7FFoUe>@hkn*-Aj_mt1CZ} z?>dBZYm|W{0}+UYJd#vXV$6Ws&e_)HolH;xgYoToF&lINV?smAneRVdT5`M9%(TQ0Ro#C^iJEgUpk-vZ-vZvSpd*KJUtQ$;KmD*{RMUO)Dt za^NFtw)t>VZr2$^lXZOa+ea{MnG#tTP!z>EQ!s*xj27OQh)MrQ=I<-aLI0^X*Ke3N z4|7u~SAU(?+@VM7A+dsR?|oFY>udalr8ki2JBOER;ttjr3Reo~1urPDl5))I66Vvn zG~J|)`tv&T*D=Nr2DWFA!4Cr<`LR7MI_IN#AL;; zvp!5m1AUUXedUn5@k-Wh(!y$)a!24v^-AIX*?<=VbS-4!w-l@huGb`pc1H3zAtEHW z$HkJXAaU;W0QzAsEr@f~3>IafO~ufCK-rTx20g1vWIX~V|NZ1)vPUnu?=F+hw*?{cU^@DUR0A3jo!=(6x2wd>>OoQO}u&mz}(b9W92rm?831v37 z2n$A^XsHMuRm6i3Y#-iFhHfz#7-piVztH#41rPN=Pl>kw3j!RM=94kbw0atCLbUO9;i2 z&+4w?$Hub)rCZ)k39MB>0Fe@Snw4|?#@PS3@p^r%YNOhE{}5;|_VY@T0>(%6rJ3w> zvTkIwVt!?JZWjp=po~zLzuuQ%GyflzyZV~(Y3`0D^S$-ySB##|aW3FZ%n z75tGu4s?<@@@9>QR0;}1GwbEmU4KJlJ>(vMK<7*&9wmbOYd-QKlLxy$q_Gl3tXb9b8s-{snpLQG_g&yjL>b~FyGDt3VpvQ|P z{A5r42p_`ebBcH0UqR=fkfy(ehi|st?95jF{&~Xu1CR4kh%)xJg;n*oA>7(wmZs3u zam^R|05~<<04W|uPyTs5>%D$b*i!#2DH5-~2MVGz4Jht<8OY|JXW3UEB!rm?rnnv> zBFVKiPVD%z^G+KKgR_g4@#ag4y`e7c*R~#_%ADVdO#YK*ld2x`MPg4_(FmWF(cWR` zcn^9dU*BO9u4$IfldhRL_uA#`6>7r2w1pZdMopxU%Wo?OJWoiNXnTib&qP|+8|f%j zXJ`7JWiNRw6UctPH4$3EF3JxCSydZyG5M-L*|5nsm|`PsgJ%5Ip+9;3+6~zvdF9;C z<6b8sMb~2ilh)^wEiChgmQ=RR+5eo-r}$0~f;lJELCCAdSp{nA4*T!SJq$VVYx)gL z931Mok?hd`&6#3<^@rE6=787jij5Qt2&UiVX8t8jImvq2&EV?Af~D-GeUj=C%X#Y0 zTsQuakg=f2|!%6M; z^QUDCc8J~DmrrGsmkfmze!IC!M&ty8lJ*A8>EPHD;;rDC@CV_yi-?(@!swgD+V zs>7*0@d=$Hb3XytV-A7n8g*87;kTImQI}pTv8Tc?oT5xy3BX0GL2BO2KTF!DkB~>}4}8w@(%> z_z8QAFFs-o<)9ubJE>b@opitQP<(peNB=?`^8KrdsJY=94&h+r1%tQyl(Vfxh>PT) zaiBVoRj8`N3-X3*x^C?{a~vW;lFxI>JF0H^;oLAx{$=r`w;iA1?6~#zS45V7v&CW+$-vVRZTzELk-^jR{(h!pe9(JIqE@Dx3C!kg z2$se^Ze)qvej?!;B#%QbD7LI^IjhGzm?uqw^s%Z{uh7z;?qh&jDNVAD9Ta!R*ECHE7{R|3XWO!L!&6~ z4H7En3H{+)VzeOSjUJ?i%JU}U@ujG@7{^v@7A7X`{1MPb%rTCUZ#(v3yh2=^d_s?V z+vdOzeUw;x{Mgl}1JXRbRh=K((hS@sH9h~XuIB`Z z!M~fBRDUTC^-($M#~QmP*%5?#>}r>^edJhQm_1wqwGzL?iy1WG;4FUjvQD|rUj+wr zPasEu_GGbPX?WkaYlrISqy1jAiG%pHDVSEP5Vp~C01VXj>m*^`l85-@RbLEsnLo-e zSS}U^#^Q3)A0LVzVk{Lj?_}wsYiFUx@borvOOei|cRZ7G%FUbWh~p}jyAY|Ezv=dt ze1YYvKOfutD?l6T>aWm;75k04sY=Ff-cNbY!`2mdvL&-ZT%RhsUrVo^BSbX&`4#OX zIyno^yP-d@%Jy8$d_!x0pJ?YbQl);BIuCAI6eSBhzgU@+dPqIMv?n9WP7>veaaDOj z2QEJs9(Z$jd_6>KB`jI$+;vxi&(te@q5U`S^gUQKnR(w6|?MSBm`>^MzDN8;fII;B#7^Ly35f$ugkLv7H2Du9sBUT_%(ck1fxc*F#TWHf9;EQUd3i7Q)xC_8W53pBeFt!CSCCDUwh04$867RL(z)33um1#GNv<64k>Y z^nK1TgtAJr{iqfH>u~sOU5A?>+c$6Fe5h&WOCbM029^J30B+v)!D7cTh1T_b$iTsj z!VGV)POddapNN5p!M$zwhKP2By6OB0uf*c9hxfSJOrRi?q*mO~#gAq$W-O8ERf_Zobj(RUm9&2dSphq0y^ zaqH#XMw>~h%ssdeL@Vjx04Bnsu@_^xMhIss6q4?T@uwQa)+JDn{8TZspCgZ}r`l(b znfmq{D}&?hr_Hx$)e4jN*0p4%$#%r;W`O&y0192V-x6@W`%j=;`X}#eUG$gr53caD zK4+vT7)U&X=z3r63nE|2IEScnBNvkGzjbghLxuNk=Ps4bKceH(x}bGVPw0+p-tJf~ zOloHDwk?m6ZvU_%bwt^s@}@-_r-vv}aaSh1yl9ZpAHE}!N?Wvyc2OO^4x~}`srMXQ z5J|tSn#AB6MC5)BZ9tvub`m|sD>!YWT2QXI3snS3K9Z|=0cqE{u|S$FL9Vlni3j=8Us-&lWhW;5z8{ef zBRY&VH8of)RYVWTKN7qr&`NLBs>TbbCWz#H-2j0eIT8JdE7@_}dVIoeyIZ7`99n4M zYcltr{TOzx^ff+q4IsS@AqkvBYW*Z(DZ0(Odaa)ZS4fgN?e<6>sb2FuTj2Ki|3h33%r?gtJz5vok5M=2`s zN(q_$I!cp^_Xa;z{(g@DsEjMZ2U{v7aW&(YPFS1t-YzkLs+h&*^aa0&D(;Pl-~rnG zxP=xDqq8Nby~*WGFhuvg z^C?l7!hPq7O)~4#_twni(Ny(-KWE#)3H1ABUQS!6aRUb&*KLq$L=>$+h5mD$D+srp z;xu5i$uT5zOh!8lKY#Rc*PImOwzMfDO;fY9?_tdts^AczWLBTvW5`a9A`9|RN!j*c zn(Q~DgvSBxp((QFz8yi51RTrwBZBFK=%Ch5nY6388CnBqm=oN4jfylsV-*~reFmMr z%Y*nQ(XuWS(V_k~$hx?8{I*fo=TJ zuJ28?N{z_cP}5ZLNbuNuUMAPGLCdbGi$B`SCj$JO#*?i+G+7y@Sq;~*#dGI6ViR-9 z5Pw#brWQc<9Ucu7*m>jberZCC&n)cj4@OQL$#~J4?Z@5|~px2X=LDKHA1x*jX zCV0K$?%FrP=-hVhrxk^#gAJ-k3tB?4{3Y-5ou%WAi{^j$I~uu0qEn>54vUMz#NFE7 zB|sk{9B?Q`Wsox39MEato_GGV5|(=y$elO2A)Pf&tAH!MHa#8qJ=G<* z^cuIbd6S7T)kCK!=eMDXq=0JT4wQit36Oz@oy?Sl>q1IAWhN7fk4ZO^GjTZod?Sp* zqS{Lw-#AkS`O!T$p}LuVk-Sof;^wP17SVcgwKzJ*N9e=Vt7&S(ZYgGwysNfPWTQdZ zOK!l^6Sl&46P5>Ya3hXMJg3XtTHV*#XDAA(h8XBMJC?mmnXqFu`i20v3f`riIUiR7Kt+`NA82|!FLM* zh7WL--5D93&1x^PYdki#JCZ5wdoVdOx>Ujko4P7z{g*k@!v~6zj3MjUucv51E)>6M z_y`E5GW`*vii2870vBEu$CoUpJLv_FMMV@;8f^F|2O_w~$Koi=0nND(M zv`nLh%&M(R6J`PgJ7wQzcgEgZa+5-|t7jb8RBvehhKn8p%%`SC6oU*11tkVZ6yH(q z!_%3qlK_$V6>E5&$GPHG0Y4Z-3(ktH!GU!@$cN2rYZF3T>O@&4##8t+37Ob%S{Q8i z*x1A~;!ciMb1sm%`Rh>W z3xQA%@2fGs&BaFF*mnCxb~YJ6Jc=8btzc$60+Wb~TM~OviXT3fx3Q_14gx=w#;e7A zO`$DVrI)M1<}8fuvlp~;Dp9tOUzjn&bvgMN&KwBvB$yf|jiPQ1dq3-=)3Qlhn!Rt& z+n2luSsdTaFRIVLQoSoAEWYb9NwJmvpnW$vq+}11D8<}MvugEv=UNI&d;WPu{}T9Q zA%PKw@`q4)lUEUuI5 z)#@n=50lwRovl%uKLvRrgqH5W+t@U1m`LhJ-^^iSHazFr&V%OK1X?8pLVNU>Lk z_H8@QZkwUhN}g<@4)>If`!JeQ~E{Ow|4`_T_Wtva*BeL zphJNBTdpex{*cVLNGy+}g-*ySgE6`uZw@qZ!AzXRnDHA4xt;BP^$CXW+H{Q{UAKrN z#l{;tg$vS)ntF*w4^U&8CWIbrAjC$%^c#ssj-K=jjsBAE8$^qh|Ct9?KfpSU~163b=DZEy(d0%PhEwm>gwRN|bNwqa zu(NE4uCdr@*wt&e1;rb!!lNqX!Rqr;}SV+o;!?fv3$c&5dep`{3(8D+*wSjc&{hzH|CK6U^VB zAiv$ffyt(elLx|Lt%GL|2C<5d?;o2S>0v(a|C(Y#^#zUCMNKQUajj>UM*}uCv@ahy z7)GOlTKujLR;(7+Ts6hhpPKDjJ1|yBT3(mco{5y2jPH46l@~eBzC{T*O4|Jibi1Y& z4((zO5#lUu=%{~927&LC&_svkRT7g?ja(dK zi3hLUfxqpOy4*>QA}@KQ^Yqc&LVA5?y@P^uqekW`DpONo(z&iYbxccW$XlzSPO%^v2C? zqnZnN_FWGGlM?5OIZ;6a8-C8Yh~s>QwJzQ(IU_oo?nI>$iKDv{b$lf;qNU-7`pmIE z(%&1t>oIrX`%sRZXDT&bq1q2U3=UESn<+9|u_1ocsqD7=*saY%4`}-j6*lEi_>!Yt zwbCGTc3(F&*JkBXiP{knM?AG zds1;QNT^XKsVjpa0Q{!dDPn$!6WwksgSO@OJ`3aCnYa{FktjZTBPMdTs?ZHLb6KHTb#+_-<)DXnAh@m7 zI8-Q}vGn$2UFhA4ancxI4!CCv@I?H>D5IffEVL)k?2N3Q`SlUAu~hVb=EB*z{$-`3 z%}EMC=#Pxe!gkES-_G41+3^svtR~(=)qQ(zBc5|Wa_8qiqhewB(FmLKs~3;F`<3>f zGM;R=uFoPj9^D3a0SSt$>q+)^D6naPr5C=*ibgAfOmFSggZjSyi>O?~ryd<`{r1~+ ze@JCiWV(%3=F73pd!kFZfpkjMM1Y4&a};Qce`b2`vUs=w`05=zXOsC>^RoEvEw#f; zm0euh1R&A^GcEQJ=7T?07O0iXr`mc2*Agi@B?n>lgcirHI-|v80=@kxJ%g?t4NWOD zNp)b!1@XLn7J2$X|G@&Xj)xm3%y^2!Bg{zUvt@W94ngi|LGDOGy}D^1`?*+mq=~+g zpr9`+BT=<|@aaDP3X&2FGFRjfWzStMBlPepwX@Gw!oevIkyUD3EPt=0%s7mFO437t zlTWDv5%{6~9(iYMQ|Y(#P>Le{Bw+J{M-!l`Q`H()t+9h04>nb;{9?k5f z@8b2!=F;7kkABz{kz!+C2lh7QKv}Jf^T`j-d$$hSP4u2RHN-yNLeq`gZ<37IZ#kTb z>X_aOVwmCbFx)ls4OzLzF?n{_TJdsccG3}R=%|RzJd{@zBSr9|UUKmTl0=MelS+An z+C#?NlD4z6#Cf&Rj9jthm9o16!8ocU26c4diBc{AF}pH=g)G>9N-?7K?+n6LS%*^O zdpT+Uw;A)>RW?=mBA3l384|D*VSlgu4tz`5$D{jL?v4iOE>*b>}glG)*v+;ioC$DI`jHu zq`*Bl&7mIUSOxn86pzkMlS_&aSIgW!ud?3cNT1V*H9sd!kkdump*_=oWKN8%0W7;8 zfvtOd%}t{ppCJgkpc-vRzq6%AP>P(D| zy;;Oc38>%?J6&#c|1C5$CT|an`Jt#IN{E`s>RjFN_>mSP!Sj!6P}YjCcxWAKyq$c{ zsJl69eWdUt3!g7LuWc^8ZU%O-h4;H(;-X4=To<$j&5z{&WsPm>AGtN=A%0HYj(#R_D}vMiCNnle_s7B z3($W-=46_yX45S%na;rzDl`%6LA{(o`n$uaOSEW`6nb;KR?+A}(yNl|ZL80E-{++V40mpAD zYr2LBfYE0cQf`CdhQn}uQBmWi(QfSmNi*#{5t8VgE9TpAP~-5kp2#iWbJuZAW_Fr! zRvR~J!?x8CV8g+_!RqD0Mk>^quxJ7F-evfA`7FeuWyAye+KPiLUqapp?eEFViU28~ z`IQ7cyY+=nus+nvz}XdPvl2pF5@k?HV`PvAJDt@oU*~3N@(k}o{I}8`oeJ%du(a(f zMx;|rXcN_i((>u`l_+r?^fy~t4|5u#u`ki<+5YmE%qjTPZSwp{){N~oY;$pTe@u?A z`Xu9eRPy4M96)_hMp+vO+W`>gFaIHP=yD$f?va@;Xx%0u|jtPT9 z0q(`~=3L>DGW-T&XW1&Yt5IEgwwyH@X$e+9^8K^~-#e5mX04J^Ii5aLY~D-$Ns4n! z)lQOpWzNBKfJb2Wfh*CNd=J02%Is0Nr=yX$jE5&@HgHizHSf&@Il=VVj)wr&YdKhM zJa6lE1A@Y6I+hx(7Ry#DT0s8~9^1yli% z;Vb8*CSwfI)4`An??Udo=G4)MYFtA57uPv&cED3{v#Khi)=ODR+X{-uTZ<3#A7qGq zWXn}E?b#V+s2Mo~deys1m#GYRNHbRfp(qwkjOxvEb3USza>v<0I`?X2kYrcz{CbOR zek%pg0sF1RN+#>C(91;PNGJ4SkNpT4h4fr}o*XVy zzz3B2;%1&d$Vy1cj%FZML-FR;HiKmXvjRv+uX2hUUC%B$yUeL$8+5ubh`(9}v8WA2 z!2GZDsQS=R@&%hyWsO&7{Qow zNkg&N>mkcM@Ilh1(&LH|m9;C6I-l0lajEH;X1De5W7*Xxy?S8K8UY?v%Ho^gczTy| zI>hT#mUJXY=Djb1lW zEwJvNqZ7+Jn*K#OceF16_q7#HbGP6$wYA0#gagd8F6k95$ggPB#ezmxPOhqXjg$~LbutRCfu&iQ*Txme>onk>Y@5$Vc`)YrM3N}b9YTaor zN_7EYM}yeN?A91JQ#-CW3>oSNCo={@*^3L`b9$fO7_niiR^^=u9+Tw3$PtEJL+Y}_Mjn;v-F%Q;0U zpDJ}m1HTs5Ph!s5HS0WGxZCSGlh{;;`3MvI+q|hyk=dIp&@ZmfXR7(Jq&NpMq>r0A zEqm2|Bz=zW@tPQIZkV_zPkph8e;;Jg0LVQ&@qsf|_B@kwhHNWKGIkaZcUDT#y$LO_ zzAZLBn;HXX}7fPr$KkH@>bYY5B89?MB=S9 zDBwtJsl^VV^!oEH)cFe?_JdxI{E=R2)ct}0nb6iTl61}Ex)oBD8wY^9e1kHBAV}iz zkRiiD4ItjRR|X$-{|>;(k>yh{wa5XfY+Fp_sKr+pQV!vCTb^k1`Ch!`EC=YqTgxcehA4qdUHUNGX$U>CVw9BGLl}NRAxc-Ov0z$N$}4ZO3u#zOVBPkzD7Rdn_eD8=Y^Y?CCnWmE;++IS=ghM&yWC_{^M zjVrEr1HOzc|5e?i26`~`n$uBlaNMDh?s;8CI6u`#5T_KNzF-L0VDigsgQGpYICxH} zQvygc`vx2J$O{P;+bE&%N7Z0z8^1V#Vk6ga+;rA{jd(&CU3<*_J z><`(q)Xvy{$y7HU{QN70ia;Nwf}rzWJGJ&s`bi&zgA#`uY-oi;y~IZ6Qk@;sm#_AN zd^%2U?1S3ukQ(tq^H=-vdPyhmQ75xN^qK3k%Q<|jzf2QjHl)8jWhr|IzdQo`C}-Sb z?arvLtX+Cs?~;%$CHm;HTGC|RhP!!t)VE$pX^sEF;#Cf`&)#GI@}146C1L)e2~q$@ zMu)wAvJtvdQ;{)`raOa|PLJ2X*ezSLjPXzb)`Cpn!Q~y{p;Ng&b`?|n@ixpluqCPS zPuPY<+|4r`m({Z(NRZPNKMasaWE z9QO0_Pq96MnlgQYvaJ&<^w}&e{x6Q4?KX|n61;I%$pDGl=L%Os3~;o$-J1?@Cd7_Uo#kaZ)(GX_c_e!9TqV~ ztO`^lY-V!;DK$28)Uv`pt{W$D>ap@Drd|-l{Y^G07{uL6kfJ_fM7U{R&iAKZ#1zq)|O=_Gzyb}%DEs{5w^0HofNrt1*8hQSu*!enJU(tAg^4=P(<6)p% zk~$H?5wIe+Q|nbm0Y%4ra9kA@&aQkd#GTWJ^EFT2S2tBiUu={OYI9IEMdR`0J>X=*OiV={6hMroViU@E769%VS^44TZXmOIR4H~80 z$_KvPGvbXkC$u|iD`3cal}?#A*`M;;#-p}v`Bv%SbqSdMXNgB1*ScBStfiFEoAj>_ znE)d0#C+~g17VMVW^oa8!urmc8y0PE@j9AuO5*yjned@6Ts}w@T^Wxm3F+rsz6dj?m_7~Y{a8LbalzeRW(qio zp3caQc~kgX3Bde^M^B|Qhu{@1?(os6E3ZQ4f83irS_1yoE!4w{{^oK($O2$3W!g-8 zdbYn72gN4%m%4_*dKpj4}0&6R9Ul{o1Cj?w&lkFPoWRwQ!Q>+qmX-7|qdQmTslzcXqNPT3db)`g`c2*&F4 z(Te=g#NbL`2icD%5cD>w+tujJcx;$`!5Pl9G&qxhRQ5OY<)&g#n@VbV%j`;6mvG&9 z+R*#HqrwOksVXftTJ^ zjgjoRlGDhex21{E0UVhy%<^-LG)saF$c{U9I zQl`^i?1rB}DZC9I#V_Q~&N8RyyVnJv18*Y_zo_YI#(n47;)xl?%8)xQNr!9=MA6<(ElDz3E3Ir ztB7^*K_Y5$zJJ?S|BsiyP7c^&ssDb?)i+T$KxA3CmBaAvqcDjWoz3^Y>jSb240bX9 z4;^ZH@99ZXy*7~Uq#n4gSzH| z^v!PeDH!$~jmMi*d?#kBs_k|q#r^(xiV;>4YN%zpvvpr1>?oJ%;aJE-N@I7wCV<;@!~ zFJyY~!IR33H$=zT&yGQ$$Y{zeZ&f5EMIaxS zK5vifvb0&Dtio4o1*%G!n`3CRERCD8istA#j&Lm}q~Y5M&Fas_c^5up-eewtW=X+} z;SKbNE_E~I{&x)paMT`Mli;hf(Yo<-^Wkt&i|XE|<`=M zQu_&Wqa9{njJ8av0pHVBkju~`x{-)C7XwnAoXAj|DPesTZ8AIQ%+oN|yafgu%r(u{ z<*IKP8?7=gTKHm|@|Tv7M5{MW{G-i<=FnN_FS}qL@yuc9!T{%bGZl5;K3MEw{T)P| z?Rqju@8PdSK;KCx*+nd)%jl4<+O4G>A9(2Lx5lIftvG9eu?LJZHH;) zq(tj}$)tJ3pH&+n)JZOZ|fco>iAqwKGVdKRFuQ_qnR zH#c`|S7lgx1Oeqx|C8Smr1d;FVY_-c*j%2$je#}Oc}uB=D#JSH&h-0Fh1KR_X&ZS; z+lSFT4waS+tnlT=q35&&DfarG>G)%YgGC2Y&ElHr4x^)A6`};LC&PAaRxngwEgiu% zHln~Y&Jk}LMus&d7FlAfqrG~i#E~%vT&g7*DoK4hbc2I~R4Y>%QUkalWtnp(&Rzo- zj6egmCG~{MA}|)1hD^MIv3*j5)%9PtM7JOh?spqy|h5p&vQOayG$BkaC z-_()@TWX-Sz5R`xC}g(BzYwCAnqam>BH-TUZCNQSKwp^7v#l4(|2v;0ivSnrH?Z2J zJg`6dyU1-4(D`!si#!&oOWDc-Nqb@1Ng+F%i zi>>eCWc5!bxQV||S&Hi`DUR$dCo$UV316vLEd%mTzr-ex-o&X%Op5iq;_-p*`n|(3 z4hQ90S{)VI8?ZWHJ{7=Zx-I=~Iz=ij5C9Dz#>LUHn-VF-#FRl4UU`WdaC>BRc&UHe zH!81LpYCyn3nn0C6>$$fddkBmXL;R~IG4zcIV<7ITI@_InL-xUINjvy#c$88HLd@Q8atgEsEP-vnU8838qO0_9hgvffls+)?fyIo)AuuQ-XSpE zN3_`LL`Hxex; zTAu%?$+7$BX1zp%z5J(tq|FCiV!?Si^4R5=@@r{?;522wgTjQ!Ik0><0`I)?-3RcX zjpuoYrwVspgYj0isob_rF8OVx=kQBu=Al(f1IjFcgcs_9L&L-e(jUSGg$ZPT_r!A& z)VnwYd&^BTOlIxs;^Jt=4jORNmz{$l0pYnJv2cZ};DhFLjy_gqjsA=S--oG_wY<^T zJl%ILE2OjJ~1D6 zk7%(psX0x2n~v2_2+Z8F197>1fBL;9GenKMB5hVg87WNr*&dNHBFjOZqpQEB@uj5qsv_#wqs7(9Uv1 zMDxIV+?!VgM5{G#@GGfuh*Mvc(!>(r8IaDF=8pRLF2Au!2Z+yh>OCt~gtm$$6?!?b z)hpYpwIsv`kN)uASDKC;D&{V`9b1drh|btY{<@-gw3$4b`D5x}Zw`yrw}iWGQAc`A zo~dv)Y2lz{GAQ7Ft1mk3;dd(Lg{QR^T8mY&-p2qq@;&=iEm;qh3h>QBKA&P0 zVqH5pu6_6-jbVcWzGJI0CDzul@x;b#QUJVQmw+^NKEdCxZ%v{3vUa1{zAbG|!?isg zLqIAjOIg!V0Fs-xVw9M3R_nd>Nt^P9_Fj+!6swC4V3cHJgdlgE(IYp!&4lwr%2dL@ zgaQp7p`@8LVPW#S(V?9}5!w!D+U%HVVe)e0+Iw8Qi6PGSPCBae`UKZ0zc~KN?op$Z z7d3LX22-A1)NdRsSm?`Nz*Z{vSW{Y(MqIbuC+EJF=ntK@d5e-Vb+I z@>&?U5SzR2*X#1{{o+^WS?TTbQ#U!^z!0^qZSTj7&(Y` z@Kdd#<&0LxizAl28H#*C${fws1Ulsz`4g6tarK8Z0<6@)cPAzJZi(-OKM}PRzON|a zHfKjyLFd}8PAIRIKBeZd7>4B;Zv01{v3V+K|23YwD6%hn$ho#EMr;gLH%1tAU$suWlNwFape4NJzeS= z?=ZjnB@)%(l4MY~m)M6h7)3907hrFfXxEupJ>IcL{o!(d)HkkY*n?@2>PUn+0`oX# zzqqJzeV%OUvpQE+uzZ3w*eU#N$ZFnTkL^x>r(~gTDM7aVv^2XN@~s;6QKVl)!AJ5a zpmk$xhjXrYzZ2DmuT$CPmyoP7uUX-9U}taJ8cW&2{sqe=ID+#o>AKfIRj$WNQzjnE z-PTPA&H{ia-+8}+I!yH>UG4Ka;%``W2uvhb$42Ae9GGJ!*Fw9o0YHUG;fnlHet)Jx zHAOYk1mY9wO>lBFFiwV>IhBz7`DxH9M&GqFbl!KKNziHWlo(RP3_ z`QmRgqVM4jTtsd+?urT%CdstFfa2 zcV?!Rst;z(2CU#!Ga(FQ8};9VfvW20SpE&)(Z|oDSA}0O6XDZc!Jf!0Nv(cu)=B;)vx?TV{^NM7AK&MMOww*(jEH;yxi-B@3mR+Dl1sdc8nKb%<@u2T(zkYBf-L0 zkk|8LDU}g>{Hr_3&h5XpZ7>25Xx$RQ@ZIU2;=Y&uPs1TFx^naRMSr^f#E z;cXAK6Anz?=RL`}|8~S!-!L=&;;PRW3)N3nr z;mHetZ?oeU!K*wV5`WX*32z%@Q<7`1IJDdo+oiZAGd-=p0hRRUX5Sl-1!O}4k_Iks zsMPm2Mu(ms7zaA;M{-r(l?6mbWFK((M1Kii*7fhi7|lVgFzIRzoYT+b3T<2YiWr3z zjB`9nln&4?U~5e24Yv=NyMHHVeW6sC(vQW#l(X~U#CJ*mdxV6J6mu%LF9GeDWMPQ` zPdL=^m5T}|cM28j*x`m)WP`Q|8IR`VU(@fTT8MVVKGOY!NK@K*+y|c4(vW${mNW4y z?kbY$%-=#RZAygvkA9N%`j1c#q$M|XT<#Vv+6xC!qd_(DXpee>*@C*8x=&%xU%~5x zq7tn=u zTTU@2%|@;h;tTSq&T+T4f(|!{sj(Sx(Z>j{wuW};EmnL97a#62%xi|z*3!IyL==!I zPl#MUMI0GVy-ns?cQMjEaaZJP#nH*VQv9~+Eicv+aGF`yMX*z5Zy8jn@)B`&xt7B z{pHm>{g)e6q29i$|7ef+2`avqMIOVkUZxKEcD7G03Bum3^jsP3u&4d81?oeVxg?>7+uyvlT32S4QYgrzyR7}J`xB=>C zC+l4`Z*MH+OZ^=NFVVbyTLa=gaY}8@)Y(rS0{vqnHyB(MzH+*I0#D5VhSp}Vz`ee9 z{`u^q_sm9iqY31EYn4WMcZvArVT;_F5QvH?e-?S7qKMS;&Cz}OGo~;${`oJFqBp*O z#{e{aWAQ|+dfdNbaW&SmondtsRQ_8ybG|D3vR_&yb z!iQ{)Zee{e;j({CnSQ{H%E4My+kRYhMt%Tn9hc9@(s*uh(Z})GWy0BB z@p&LWTnX!XZ42+GTOVli4Se8e=(Bg>OOl~i&$pVmHd_Z10IP3fqzFA>#7?%azsy1j;E%I!@ z%^-1)eRRWcQANsnvhI*^n>A&M@rW>Q3-DNU&`M`tLx=ILi8f5K_$ zPblBDPn%~^%PG72OUf5~dqx9ZJmMvqwn*8u+!tF`7i$?l%SBAXMK0tH4 z!D04Us2azb1v5R(@c!rBLjBCH_CygoA#oSzteVd@jJ z?#&~bJzu9nL82#>Xv0&9V39770bNeTf*)MnRIBfSBG)Nt&H5_O3104WHOQX9t%55L zl-sGR+= z83paO`}?)!$uiyCwZ#uVZk}GEqKTszxydjGCPBv18&D6bY+i2I`MzgV$v$`X-L!5G z`LMaUo0eya*!nJL`kIw;7Lgl(d&-01W0DK-H_II!9VTIBN$&oMPZ&O_=Wjwj)0YpR zV5Qm@X+|}G6drr4F!flUwU)0+w(+bGnB23t=B1zEU)HsN`=Dc&ZVlV-T#5Wu$2{h6 zf#6Ot*(sSmc-K&e^|Xref`WlY+VwJ(HA5o^I&8$8vR!@n)o4$c);?)sh-O zaxG^^yzY}jWp)Vs$Y9l&)@X0l3i9}XQY^_`)e}qi=iKD#tAqH=KI8C`zxi_SO@_Y- zf;I}58?+Y1REHY5?jr{{l1PX>B=dcq@L`r2rfu3t`) zDlUCe_}2xn3`dD41x&&l7uSWO-iKJw>IACQPLw>1O-=cba8c!qJhn3@iQnDfA7$0N^9vI2pAJM{3`yKN5kD&fvm#U`V0WqA+%Obno~s zldqJfubPrNgo=(Ky4*}!T1Pl-$(Ex_?<>{dj~{@Tm{8mtIa@TMc#I(hI(NS)aW>Gh zLG^%d=o!ag-5u8+NnAz&qkQgddSN=Tuvn>BC@ZjQ0qZsRbEG0QebH?46o8`0>G$3G ztWKA+>SNYTR#?9Lg5nvj8gmXzyz=FfqD9Xh1^9JypCpgb>C4S3ov?SW)fEaf!m9$$ z%HVpU*?+luk32mvg|bnUS7ig+?yuWqV0NNX2CZIHCTsDYn##48q2ZkK@*qqz5*?My z*i=md=L|rB+)pzh77;k=HZq9cmLM_Bze-ZpjDlHZ6giq<>u;r6K*gEK#14p_PrM@Q zR@Q?2XEPf^W0ZU|>t<-C7kt*~&x$G8w+XGse{tpw%Uu1Z7k1Te&znFa{W`@ zf$x3zzV7u11vPWPZh{ZdeIS58x|n-XnRnck2PlR6XQGoG*q(}_o)y~3l97?a{+uUG zCF#9ZjrCbBL@>l)ABJMo+c`=Lh8&37<=5q^5rpDlM5@SfODpVbe307f4eHUk7?HZu zfFyxQSFtQj*fE?U2i5Q?%J&*RJWWWJnfbicEvKq#)D#nlD5Ua`xN56x_V4Wtao+E5 ztAEa>^C=+lL22odqXFit?bGecmU+FGlyu&TksWNV6uj9oKXMaSmnuRtkmCwh;cxzB zN?o^M$+mkv0b!#l^ zoO!bp8NZ9&-qm&1J7rE$Tb_!=h2s$IbPeSfVXvBbTF<5-M<|X)PWm@}W$XCf#CJOz zsd01BY}wi*kPuB{qneQ;TN^l^`CPz#ue@WWh@Of_{{=aog00+3j9Y*Hgg`UOA>TCy z%6#n74vNJ~OOsR|W3tIemePTu02)9XWQ!C5MEf&YHat4O$j)xe;NM90)x-6bV4?!< zpkEv*?bV*c%^M6^sAh}%Tsx1YLDPG;7n3hA+=I2$KkfmTn;lyO%JI`VuM0WwnCm}P z3~{VdfkZWmlngyQI%%FU+s}`P3jq0CGaYIC7ZHb}j33OVzgC>fml9RzQ`l1Dt%xlK ztPcNio>}*aDeQzB$7G(jy3*e!jM7C^YdT+kmOok~gYUq|@#j>CX7kr<8nD%FELZ1v z6VBzdWiP+2FSC9$NTr`Uvc_v~wgB069$}mP$U+ZTndknl=%X|N3hJGVQ$BT)Y?CcP zS?^ipS2$Hk(G~$kBmnp%I?@Rqk3^B|@}Y;uq(-_VutY|9rXN$zZ>x{8X$*LFG-%&6 z1DiHdPB|k>K9VPH)V}?}rob5$nc(-CXMd$~IkGc%?PM{=sK@aPgud zXwd5EV5ZVwG8W1i17|t+(+N@3#3eH-YQ#N4P^jU4jo`{C^tndMlVQE8eu5j_{*{c3 z3>YlPZ(i83e0gV>rDa@~EGR`8cz(6PRXi=Ix_#?lGx=d7)JMn8_XHjONbxTNR||g` z_M6;EK3!@Cu5>{sgc;G(hh34n_`;Fr(F1es%|KZYlmAEtzoGqhH4$KFSuLJ5!(KH zL~*5+@do6&jo_n`bdvQq?k5g z0j$NXZd%OF86aCT^u+~Ym-BI^q?M27G0@O zS(-xOq{NlfG$Eh(k1E&BcLpeO`83MsL(GhG&D0983xL zqbyYRUHt5F)wi;ZX$U)bl#VZ<$wrEkFqXTUZ-c6N1z`Q8fezsKP{=HxXIW^XWh%xX z@QqNZ_Xd~PMv9)>F(_BgJ{NFmOKw!)e&;z2UI4z9GuE79(xe=+{PtYO8TQ_ywwJyX zbb9hd%izuF|I}Yl;dt5FLWXrryCTBDi^QpuFY*h-8Y;0(UgENM>e zV00&b7QOwaA?wK!pU56FZgKL!d&@oW)jn{u-%_ab9WP1RYTZKYfHUuAEZQSR9wlsvmxHUsYTiKH#e-ny|`ONX}d*9;c98keX#&NaxS_u`Mw6IW1z(=PlLkAQap zXS0!$E##)^6%`_hJ84zqI{c=q`XQ3nD<@oz8Cc=EPb2|X4yX+-|@>SI1D5MuCiK80}fbFM8DF zZ=n@4;j7?ZsL}t4jOaf~-l#yFXKK%N60b}u&?esp)`FgCg`#|x!zb@_O@an}IQC#c z+bxqK-aM*lypI268-=~MH>792(+lSGS+I30`mIdHh4VL68~&Gg<9oQFr!y)yg4jWA z0s1yu#NL8_!w@I_ZdZ<2KqE1yT@#86;A_dyDnBODfLeWfW;ek-x+*$!4udj+A?&AE zPr0y8s`Y|r^kB#30p7pD#$3m)O8pjj+ojALy>tR$4&K6_NiHr`V3Hl!h}_h?M7M=B zF820RQj3d&@P?3w2Z-QUaRQ@+RbLp=4$|Js#eyJO%2h%Po`76Cse+NqNjbh^B3E+EF8bMAmD^gP?w@$-x% zUG?o!;bGp!p(#&?fR8*fzQaB6tRAVx28Gw6T;T) z;Mut$deB(D_OvD{;8#UdP{u1v-xe4F3TntaUy0UrXB;01lYZ?Ax0riyYCZ4s^9_Vl zJIoC4HCHxdVi&UvIj3H|o?Lg`$266jQrynHS-56J&@ND0QAIuH{L?Q{0k@Rj@++aA-DH2(eSJ)Z zyW}TFYjY(`+(kZiltLVg9q<*k|1wb9KFfx#UK+c}PgKTJ9PFk!H|aA${>FOh%2)jf z)&~{cx3K3EW-Lqo40mkTirhsdH0paG?^9q(p1h#&h;(6oE?R^F^>VRBe{ZHv)x*=X zIHbG9@O8E>P3E3m&Uj%2k)syJRhvQJ7_Y}dMWZdpm9^wU?)40PnfCM=bh7-4;H>=- zloq}!4`VJ%=Oh;Sf}qXm*y`PQZbUqs#%En>m+4dTRtL~UrJOdOo6C1G4{`^6Sm{`j zfPIP(*-1$Uxvyn2Lpi|FZ@nsWYEv^PT_)lg?AIlmw@b^Wetp}so}+U7I3k2|8s%fE zdacc2fXN49TNl$S(@D-ZH0Y=r*v>;B$hkTMva`S6`;DF~#=}UFmLXrCxM;=#_;CuP z(~DFyf=2?!+lZDTlA;63J1c>eBd@KGtJ4i6G?xIB{5ACJ1k!dl7kj!rHzrz%3fl1D zCaw4=G0T)@U<_VQ0fAH40P&|XU-*&cAMxMQM5HQo95kMlGEVK+4{ zcO=2m2gMCnAY`9s@MhfY=XNm2on6>`%$yHnz2lUV>Xzwgy(K)q~G2L?naN*b|H1{-5R zuFZ0-?KUw{5I@*$xEpC%&^6q1>TJ&+7C3x^a1;2k!c6)X3}d*>v?J6hg9(sRE)t`>?qO-ozuWc!(<@7q>J zMvSby5$QJjsSF(WPGb-$ z&Th9`@cZ6!=;$Ot*qz^b@q@u#eiAY7J7Ld%2>iD*pUwe_Qv3qW@+8zoTWAi+dXQT@e#em}O9K%gCIWNpgALQ)zt9h;<$M z$Sc3KuuPS0?po$?Xb#cR{$?8TgY{})A^h7;CiOwWm*)Zf-9)hzbi?=M+l3*eDhCu; zW7U-jz2P8!-LxY8tnYUebVbm=_D(XIu-Oxps{FhLa$}F=mfhp^*y~MU-Gg&$j{$wR zaUXC6b)XlSmNad?2@R{&*}Mmt)WaAa^e{Q>iqOh|qI=kkE7KwiZ{pa@H}zxN2#eIdp~b)Ft8)#=r9P4bS{%o;n# z%RSuEd@QGco-8LRZK2xJOq5KBfQ%CvQ2iG$115V7A;{Ub+xZi#$b}>b>i|PyTY)3g zhDj!j!GnPqBV?rg5*(6MgzF10l$gX9@Wg_UJ+DS`|IAFH06;{Za0`;T@IGX`*Uyg; zYrM`V)qe05-n!c;#F)O@0jA7j-ZB_ko$DX1-X6pJiz11=bHBr`;5hsp)3hUQw~Wcm z4Zwf1t%;G%seQIKw2B$l&+X8vWXE00AL`URebHySufSZ+unz{?u5+2LF(a0p_^!g~;E`XrW*KR3$ z9K>4-Igu&LXkGrwAgHrCHlESrcE2h6x6hw3nbQ|5roN&hiUuY5^fK2&`*q`><4B3N z^@88a2BO(whQp3vax7!G9X8^K3VJ4$8FhDim3B?@(-aQD=4E7DPJ$;=Q} zQpC|X;|u|iW+V8V0}j<6R<=7P{2a#eBKF>krMZ7GXT)c*nGc!MPcb3^tLiKRZVutg zjnpY6|2zVqTKBy#J{#FR`AseCE#){0z)-ndDGgsZr3}_*y?952x6H$o*w3*iPRz^;+TLmxBEuF z`da|LDl+CsNeUv2!gZ6%dr~es{Mfnu_SS+cj`feXHG9a!|70C`)uU)lj^^80Rc+Qp zRmQrj^7X518sU$q&VZY3l4r@u6-`^ZVyFiVCfMZqbp8xY=QmV^X}!k@-_4WF68&`e zSZ;x$RG({ki>$Bib*m-6_;q*H;H^75sf!b+_<1xV=4<&$9<%v&ODes7YKw(yVR&9@ z=w6~V-i(D&PF2X;W@>k9ok&SLwAkCv6NYl$W_3Dh2I0< zjsBzlTW|>2yn#3e-v6nwD%)SoEisdZ&ITN<&zchDfNT=7X#$e1;<3>C3fkBr0E34A z@23Bf4e&niU#l%OVlQb=7mO9QGSJ99x8SGwaDTygbGM6jg(KR9k7vXfap20* z%xw_OXuwB#3>OjQA*tFw*(H~C!0umXz}i93Ej_8RrOB1Sw^e61SxTb*A-byNNMWwF z6Q)pLo%Xbwpq3&hk4u^+yBry@mp~nHhquY=tcR^!P&!=5KRodemn^7sZ5CbD@@kMg zjX$6#9{llp+&R&tB%+Gwr5F-(A33+~tI*I$a**ImhV;e&2p)Js=K*Ls>TjnNV&qB} zVkq?67taC$J7gwWe>CA!ri>P{>*d}Z9rD==b>D;2$jF#1*eREKvV3Pe?7Zzap|bX= zLGttiG-JD`Fzg|6_qfy4r(NmdZvfC4z%J=-GchKN!g>*MQ7|X!&P^ksF1g@9`V08S zfi$HCEbU{n5OBBsxryjj7Q1UOqDD!oFEA9cLiFKv*q+i!k5_7Kg!S_vCEYyYc5(4y z&C}X#7q#L~9n?+XlG|HbQ6r)bOiTY{um2}JVje(5lbee7suoAK*?F)4+I3hTrl6{l z$BZY}h|&IypnaeH_q?<;cmEzw+o^Y5)CA2>KKEg5DtTEvYh}oP1q3&O_XE`J2f|xY zDg2j6hw}C(a=&Xd8|ip>dDz=c`4FJ(A5SG|*o-iaHsNG-BhQ$Wu1p;B;5zX4Vp1Q5$B2B zt{=c319_ucY=SfROO~e{3Ud`FT9iCs4sHNH?z(SLV5w5maVKT+D;u0`?y!v$A7k`) z@bl5GaoO0^S&@2c!RrXit``^b8qXZCgQvw*GgrgA?QH-K`6am~5dD>Enn2&nEFgNx zR`U3`ZeX#_xGul@G)@Z=EOPaiWGv3gZsRy`tlY%WQL`jhI($^{pm_9U0RA=r%KsE0 z@-oEu9_D@iE(&_)S0A0fW zgjlOfOfeTM?5*eq)mEo=gvdFC|xBl zm23csZeB9O;>S)FraPIw;{AQEbMu+2&N9At17ZhQ-E#+nMixBFzOo zA1$Crg*~w(+UMy%V1*@g;mY1jn?tmh0S+Ds$MQe?oWq$%GJ5C!}K{!)YL$jzjrq zSSsD3U-k0hP6jtoT!g{9`^v{D7*g-!uOPKSeU70(96;`5W~e2i-I(rMcveNoKzyefb}1qhBecqxjy@vZlSm&OGnk0gz^~*$;hh!o+i>;f*%>> z6nITV@K4%%scBS+pF;!i(AdG&_ae$e1-5)1{H-Oa>2xC}Zmk%&H?XQg2CgOZmXbeh z{z{bf5NzcQ@Zxic=gpK8mk~BW!F=gV8=rR+#(_e9UWkXkUFGI{%`FI|A)wOKjHJyB zj*G$3d%HAc_Ki$mZBviZ7j7`Kuk$6_`>Gi(`6Vr=4RbE3|H9C$6^^aPe3HwXuA&oj z?^S=Se20B-tN=ex9;9|Z(ctVnZ>Z8PON{>@B@|_q^KByD$!^O{EcGK(nR!1QQ%qv< z{&=AWPU>g*FC=5GAc9D$k;q|!@=6boX#x1#%uScFD)f3aHym^O2aNlV&U+r# zhe-x*%x2r|Wd^#tj9C*zdVC=pulrhw6G{g9<)QSr;+YMiJRR7G{Cz|dYvpCP(Np4% z{nyraqbV<>^?N(W*daMhq}%X3+17*a~9U3@mn!il^=lc>Nh3Bj<2q~m3PFWc^ex_@ zI`3CN{9R#^ipxe3$Xn#TCab%+*hiCq*+mhMjPfCJ5UqWXqIcm`P{inX*RFc9N3?$b zz720SHi;}(HDe}C9}EkRcks8}d)n7Bo+Wr@NSv#FTQ9B0uwU2e5$j&ed0R)DQJU zI)0NH#-f9Vd<&MC-2+vpr-=pTsShTj7^T4-^nbzNUAs2}P6=5fI1zpEuv81ojRCp)3z6rNA~N57 zw6Lu{WW#jVnOyZzVmlb0#PNbS{&d5q{sE#yRZJcR1KL7Z^6>t2MnLjD&jQ?jI%|l+ zEYhHTC)q)$d@j6JFo9$V$#!rl|1W~)aOII)tAj9;GtGE2%4++?jXa&?R7Nuj*@EF_ zmT)9rs%?0S2JCorLjhiTggLpZ7>Z07XA+rEQtwvwTzdwtPaI|hJ%qyso{^jZ+*1itE!rk#6yA%{UYxvhSm(TzxA;3NR<{ zar$Ytoc^~V>sYA+;ePP?@=sxv3^`ev_9e5NXF39yN0mq(oDv(z{K z(-pk-N=VwsBwNV30|)dJ0W2n68b)CxL<=SOhaV)v&N1t;3Wq04Z1KEB{WpU-e}y_a zYHI-q!j2J4qp4+*($SA)1@JdB&wjF~=lGW2VCp9F@7a$Z20k>!v;^~V*ym$XVgqZ7 zgiE<4b@DLL26=F1003p3s7;MR4l?c+J01f7+#6zHj+q%wnMU@51PfB0gI%+ALJYU9>@lkJc!t zUP71*pl4J)kYekUoX>6X$RllNow|Hsu^Mzz^CQKPsNx8m~97HDxNL0d{G?!~3JQwSDZ3baUYC%C)2 zmg4Rj+#Q1Jm%e9xYn^lcvGRx9xvqQ9o|!#+oWI_&iS=c(+CQ`F&OB2|=#{BIZwE2^ zUT0Mqj3V3zI%Z|&fy7!V)7wLnmsN5g;jwN~err{hwd|6z8CKT_gtqx&Aho_klKiJm z*#gp*`G157+i#hLaC|D2`z?96$Wg{OzSlEXBHAKV-DCJaf&rp>wdz#)u5;*9x;B9u z@d&V`dBWby{2IS=&63by4v%klAQ{GPztEb(<-AE=^I!+S(Bf z+Nww-?CYDcD8hc}iO?V+{ZJi%jvAfoO1pZ36GuZO(rH&{qHQ1#AbIpk1&sy zPmiW`fK;82?zNZZMx$_BSk-zCcnu%2wkARGAjzb2IIoeb6R-2f6o6WOU}Xn~d)_zv%GxnEqc4n!jv5Dp zNR78o2+es^ns^W?P602u{m0a${rwpBql&*{U-Os=Oq|MHj`vZk4^iflNDOyu6=$2i z2#2k3HEhn4AYHEi%zHLzRDHV4-=$ym6oH37^b!lr{O>Nx-LlcXOtRX0+hi6KNW8~J zc_GrcUKOQ+*}#WdR}CED@Kn(`s6)!^J6Qg7DB*%#f<*kYqV=8-bDeW6OkuQ-uLgCv zp~);r$$AUznq@q}gs8vDj!!u;zCF8>M!k%yM%urr24u5INGabJG4i-;^vecgPe|Yc z62xTi6PwDa(vFF|a$izk4pe&Nzrmvw+;PXpot12=y+a3exUhkQW`c(7hq^(Hu7X+7 zI**%yI60ATh&=J%nB!_88uQ+HqQPB)Z|Q4{(?IO%Z;s;a>pgvaG`JR_TNj589jo<6 z;uy-QQe4@?sXTTO&!B{cI4WRqbn?uoyqc%_S}OL|olLC8`q_V?c3yL9_9emeub;K- zZCqP&_*^dIyFgfJZ=#0ARLgazTAM{}7=PQm?P040?WCJ{A&9C{YVp27UX#A{K4O{+ zuA|5w1@~h5l~HIudOpkmz~1A$A`l3m`0s_>HE72*8Q^b0nCCUH#IemtxlxJ8{4ckK zn+-~FaogNr&CGumOAu3YT#ElLnotwbrWI8f`D#i7yvt)1n=MF%@nKqL$^-YzdG|=XBCMJ0;C!K~blXcWi%0j9E)0B0H-X zL7thC!?E|AAm++?q*2o7qtLx42yk?Ia3Vy)C);mjID;-PYtySTg>d{_mxBW#4unXt zrJ#YZuj^m6Yfu$F9po!S5_{kI3cDay%HNF31~+PfwP&$kX9V{aP*abSuuH1o34m)w5c^q>C;- zj=q-SmOG~L{(^d0bw8eC|2^(S+2E@&Hc_2tUVaZ;~BF!q=#9(dr|y2YG`voP_G z)qa`^uc4*(SjD`R-`s^+AdxQ((%a99P;N=yURr`o#Mjx)42GPl%7vJ(_?$%v)TA=G zK8sB*WHmqBU#VB&y*D;`)9sos?U)fq@KN;DF!Is(3IRu>eBj;LStZ1o9&flR@&q+^ zD*0~LWM-Tjf(r>V{w1t}lxQTnSYwRx^jl0*g$fw3IqM*jqHTq8&#QY=ywV+Fc!Ntv zkjc7=FyYUA=z`hIB`7+doI4_VtU>>Tcx0-gVSQ;0M?2T88h45Us~&23I)J^oWzRYB znjJjytj3)LPq7oB(beQ_6@?6-l=KJnsD3`wOEC2N6^~^NpMrGqBJ$p7Q;jrQ=edX~ zZ)CF_nzOGe*4C}5r~~ir0$&HOYWu>ba*BYO+0E(1+URAyd5>Y?KNH4&{E*X9UqyO% zgk`Od3V3fsbw-hNI?oC^N1GR&QJCD8%$DOck(Y`Am>nUvMsuW* z9-*NU_NQIVYC2r2y>|mcCK%UKkv({csRm&7-!ysQV>$i$NRzqRqO+oQ31dE`o3&}U z2uCo%hE3iVnd;V60*{&yvo`+dQL*jCZ2~*5`~5e}CikqGD@(*%P})1aavUnb z)ZCRgE4wutX>sd4qfo>yll#(933e#(!NrfGD=h^uMn;A?y!I`$n!>w? zAa;%ep`v)Xh)1pHBZuirj^qt4PVBucQi%hqJp%%IU~a~n{L+D4!ZC)&JxfI7Msr?r zyiN#914i@G)iyTGM$kv=0A!>P$Dkrqeh z-bGb8t;N23nK73YNX{}WCMdu_zIT~$k?HAonYsxu!{Rsx*yA6Pv9$1}K9j%1LfZcz z+f?R)jea-#o#u-pV|+7xA`#1#(qV>y_v%1+#WbIn2x)et3zrKcHTQ9eR#z8axHBmrhV={?;2CRv$Gi(pAR71Uo%kxq06MDHd^?w5uYVTn+rbl;S73nc@+N(83 zTd$atJ(|9Oe8Ep@;-`RxsgCE-n`bj%@+0k50)juW{SYaTc?o?U-?B=O?hr26ok;`j z9!d^7<jR4qBWt93?4n?Yg+#2 znzk)Kg9&e?WDxx33mEgPPF~;g=2bZHe1)z-K|{5E;cmO+dSz|cq*BE>2t;G|<`V}Z z8h47@x|nj^-GUB2cgb3k;hgM`wY8`b780RSq8XVS%kI0DzB z+&B;rRjy`za2cKPOC-0RF0aU@lQiEAq=I5cYXs@FvHcb$H5y5DGBP=bw8*94raSvf z>2^Z_v3ldA4+X>Xmjgn3__0DY$PFak(h}ce%cvv(atFyqjCLVn%EV?7S%ca66=#?)?~2 zaNzW<{VA~HKy%P{mES4`!x6;;%Qg!e;c;$)jIdBy1ufhiASTAFjpF<$q;9BsURf9% z4xoW38AAAYd6h?}r?(%8NEkMedtdD#ISQ1Bhj&Vcg*tPR%KGS`i z9JZ57*jtAjVH<9l$L8NmNO+ePS*z?8?&zu)6LQE7_ZGSZc#twbS|UWr)U1f^F|RUn zr5qkDqAfa~mw>OF@O)$b(P^ZUc9+lpDdJ1LTD$o{iVp@Z9<9?;UwU-c-t_21234~G z5(NS_ni2EdX4+35D#dOG<&<95)So;4n)DG1{m(VOl{l5*iL-exTfN=xVwbht^LMO# zIXy7#ArO(S6{igjwd}2Us=?6f@>@zCWGStNGX*Ib5Xu&;P?I9Lm{G2fC$-Z?4W*^& z$ts0!-*SpX2e%Y`!RkLBw1yFv!`b>UiDcwdrQPL-8ALO3Y=Z8L1Xw01D=V|MWGSUq zEww3Wtw1seXrvQNoM}Ni@UTzdXDo}@w>STga+j8dEzw7_#v5X;c!SLE%#lvG{=8W* z%dh$tc@52(GVQko&KOw+_tcR+(tYNk$LT`#c@urc;7g+W(f=!si)9r_=xk!42&%(9qC&{AAdlThq}e zA&3H0IN|wi@lJfz z?OG6gGm^Zp5S0!W8x!LN-QPiLc0oR(n^fnDyILGE-WI9LuuO;*1rkuIsoNNSnNMM( z-vUXU;{FU0w%X$vFT$;&pqNvVv=~;WXmK0}IeqrwdU0E+PDgXCLlen7wZ=XTS`r5@ zE=bSU135Cy)6)bU(wl?{&Zo!??$}4ZyJhFHTJ6n?d+k#&jsw0HGfLqZU;uaCT+l|J zSTcwHhYB^~kZwNx`km7GBAf5Mw)r-nGv(e!GnyOG(FK5TxN4=+IYD|~Z81eRUdS2{ zwk;Lw3BZFwe#@V2BXQVlZ!@k|!Sti+zbG@u3U|?DpgAWk^*rw~j=yJ6E0_4n58DoA zBOXuw6Ia20cLt?D9ets7Fic0oi1EX`^nY7RKZ+PVoY)yoHaY&SCcIgr>BWIqqWV!+ zT|^$4ULws;F^VbT)taCeMnxB57N9#pb+ytyesQ4uc^dHOEPFS>?Z8#HT-92d*Mh7+ zJ1QfWD2_~dkrVdp45sSjK+%^U$Z9Ae3*Gz9^q7SS;cG?lwyY_^qfnckpBGj2ufPwHpSCN1MaBlyIJSxXsrIVp&Osa z-z&bzc^9_QUHhJMIyh zQ()-U7@Kuu^DvHQX+U|mEd^YTuJ>(fX%~(R%8hBt)JBgux}lx;h8(qDa48S?MvetC zjrn-Sd(5O1p4L=y8o`EaFFLOdc!uR*!7Rze0s>^H+gpB(UCZt9Xan&}bY2g}wchV} zh1-(G>wyiVDHWZ*9H~p(`nbO~hp^iwkbl(5jO53be!r#A%$)L}VSSNr7<*2e`{csq z7SwQ5o`5(HefIAT4^MA@To2nwn|j95^YaU8{$>d2eTxF6YvsSHlgbREMj4l)XIU&# zy-z933oBE@G;!@~|M1%$6Nwuu`1UW$wr0WpMw4E!RYWl~V=yj|-oIHDK+BZO@NNVao*CpYVq7@ox4WmtUw zifUcB$9_$iO@g*8$Y($L{=cWs@5ocC;7x+WiU-SeIOGW@e>R^^5yvp{?-)|ieKJo^ z0&vPpWQF!4QwgX9aLt{QmXV!!+NLC3DzgnWf zDfwM~Hni^O=`I!j^2O@(@rDlDU1HU5v@t+~GSpZRSpO~shJBR8QBXdR0_@Vm{fRn`Ov=-Q3dwB? z(-^Mwrp$FV*m?Arw97Z-}lof*oebj6;xyuv<+DrgVw(R-Q@yO6w1Kqk5??!%vVlgTV zW2nh$uaQ7^u8aevt+>A2P{~%2%_5d+CzgoF}Y2;7riaQa=H_?O2kTklQ~4Vs|@MUvUACDZEC`1{=Zs)HY^u} zhn&LhM+Rxyhnc%i&w$=&Nt|S43iqt8`)Y!x*cJEcJH6S~o;ZOfkup8abk!--4nS90 z2jOM!LP*%_>fKpq($q3sN?_}1+oz%^cmKC|P`{NrN6`*~?uoDBH7#;>H{r#Hg5I@f zBjR4Fl;A_eRn-1>6>Hw^+Z7mN%XeG`uYUfMwXoGe>d#@l?|$}eZWi&Es}6}j~69eeX%LV*iv%k8JjKMy!yXW=VBa(~68yay=^#0>So%I9|y7GfPQ zyYy^HFWdfG>mxUBD_h>KA$z;YEG{XjpzMyduB+b;TIAH8-c^O};PB z8j>JaTkLoCwY2k-ZXub5dF>}j-z{&L5p|7rO^vUww(3Up8m3 zEkVxjq|@QJpi|!pUaCSqneLYw)kb_)%87x!A=tKz#vE>PwaMH_m%Us!V)x&|7)R== zRr|cI{>g;mQv6c0su4YZ%zv3-egD0VyE_L}I3`J!e7V<_R0DNT;FU^?xoG{~yGh9l zbN?yqhaX1txM$o_|EN1Y}f!K>-nf%T0JGlhXP!{*@*iO2CSSad+jQ@rvDT8 zF*TtFtTv#P@@2;jZLVvYj*7q7md-Qp=6&+W(3Nsx7Q`-Aqq8iA_HODwaqf8~zP2!g zKi%ri#U(E;lK1?tXek_gDCSA3I-|DpIno&t@Jnk;*d5CAvHVGGn=#Za<227!_3o(J ztfj!#DQ~V*_oOo(@t~+Ti>Fn*9hS34MUb5K4`O;|nTXkIi8Ng2gCNDCuYN*S+N9jR z+tE4tIWU0bgGT|m0a87(K54L}2WMX1keMO&Mm6>dJBt2muOcMQ+dx5fZ9UvyB;Gr5 zi(e4PJI?*NQVr)@aW*EXbv_Z=@s~y*eP(usjG#!J^GdeX`MmXWgr2~9?Dfg)WV~Tw zJ5SAp+dX>+#qWt@_unlKL4rdlv}eH>94NF)_WD=nt`dnjgUXl!@^j9c(D3YxhKnc7 zrJH&2*aj0NlmG-F@@(j~PwWIIuIkwoDt65-SUi)q82wIZp&swKDr6EVBk1r1)U@nL zCuS+?03Y{A`4!D{&Hjsj6-x57ZZGBx7S8T{?+YxjaVd0pmj;7b&$1X0xmP9%Ioa)Nra?Tmrvh6^E9bdjE+txb-s|)$Z z>hRxtKI}33CnO{o#YkxemHd(Hsje9B&*G1M5h3MBno}Dfo%>THQlQlvdofqUbaOV5 zb1Q&#kQcD|7nRYs`ACwvk^s#G@D-vuZ(7qrgBxNJ<*#U_1-t0SHXs#}-es^pMDymU z3FBYou2837C}wXHqwvfRn0*PkAjavxfSJ@YRQpP*d^;Jzv|2;1dU4+(Tt?D>vl zf#f~i6>=tpDE@48Ie38ow$*u@d*bd}GmdX#uwTX?)wColG#nH^_ehP_*7A_8G_ybB-OK*hz{dM|5#1B7FVCxjE1Pa}Fo33et+}fz{srssJ6*zkJ zG=l}RYu(5L*aAL}4DS7sAPASjqlIV63`i7&@T*1Msw$Bi~X^^YX6D3OpmWEacbI%+E|h}<;%VeR|$zS>{|Ob#z}oR z%?yXl)`r-7EJePCp(yf51&EUyZ%YgEQ#*Nvxvr;oxkSiJa4@cOE7qq?jBz5|kVOAb zzjRryHx!S7DRb@BKh{Mm(2neVW88(~-mM|ST9~hYp_%JBZi<%_Gg=t!lBRu*&{}LD zWsEB-4A=UBL4e?BQs|#D2V%A9QeJ1L0Gw+8uA2j~vfm)Fb1Bp}`7S5YdTGqhkDms?l`RHWRU#?%$Fe}IpjE;ngWY7w|YK+3lJ z`do1deHpyc4$|3ZlT_i z;`MsB%VB+>m2O~^?scbeJyX~)%Ob23bz9p+L% zkv>|pSK6KY!GEyv_BQt}>L5a}gFEqj;6Iju8RW}wdjCheLBxvU9v7uK$!%rAV7%7I zDYC~{YLz)(Wk}t=W-84$bIpr#!Fy zE#W>MG^Z*+m=;PsF^SiA;UJ&wYQW0ziX*&EZM#7>U}ex>WF21fm1D`FVLM5rk^wF6*w72C_WI^r_YYjh=&k><=b0awHtBFfw(L;jZr&^J_ z(V}PMtFO%B%_b=o!p)$i5Qj0}8;NJ%>rwC1C`3?d+XT}5kmlYDUFeu4z54Q*rrQD% z!bTReFx_0G{5F3qPSMAeXfL}H$;a}7YlPGN4*bmJKqHod+0U=jW_!?Oa<_qWzw_hD zwaYO{I{M^SDCWEr9`yPh#z0f@8Vp$Sn~+?$G&K4wFi6;+E+gAqzVCg2K1MT(Aa}Mm zLmAECz@nf+&gKtzPS^uT_*;rNVLvt^^ExMX86of;t#fMa&vItPXzyND{1}6Z+}7s8 zez@4oe+s9zDX~7VzEaLbULsy9B>vm<_<*u$%iEwe6E6I^4KX2zxh!7g_QwpI~aVlE3cY~E)Vr-rI7P|j(Si`p829Tq4b0uX_>!lhtSetR%S>0xlfRSKX{P*}D zq1T87#U=(#5`ch~Jta5cNT�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˾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

#Vh2s67(otap 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 diff --git a/character/overwatch.png b/character/overwatch.png deleted file mode 100644 index e1f9bbb868c1010b90ec874201f0c1002daf53f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181662 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�#T0*t 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 !tFDGCL64_v(=A4ZYtH%6Y$%sMuDJhd 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 Date: Wed, 11 Feb 2026 19:51:49 +0800 Subject: [PATCH 46/46] fix: improve error handling and process management in launcher2.py --- launcher2.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/launcher2.py b/launcher2.py index ecd3582..c30d4ca 100644 --- a/launcher2.py +++ b/launcher2.py @@ -316,8 +316,14 @@ def _read_pipe_to_stream(pipe, dest_stream, out_lines=None, on_line_callback=Non out_lines.append(text) if on_line_callback is not None: on_line_callback(text) - dest_stream.write(text) - dest_stream.flush() + # 检查dest_stream是否可用(pythonw环境下可能不可用) + if dest_stream is not None: + try: + dest_stream.write(text) + dest_stream.flush() + except (AttributeError, OSError, ValueError): + # pythonw环境下sys.stdout/sys.stderr可能不可用,忽略错误 + pass except Exception: pass finally: @@ -606,13 +612,23 @@ class LauncherPanel(wx.Panel): self.btnLaunch.SetLabelText('Working...') if p is not None: - subprocess.run(['taskkill', '/F', '/PID', str(p.pid), '/T'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + creation_flags = 0 + if sys.platform == 'win32': + # CREATE_NO_WINDOW = 0x08000000 + creation_flags = 0x08000000 + subprocess.run(['taskkill', '/F', '/PID', str(p.pid), '/T'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=creation_flags) p = None self.statusCtrl.Clear() self.btnLaunch.SetLabelText("Save & Launch") else: - run_args = [sys.executable, '-m', 'src.main'] + # 如果启动器是用pythonw启动的,使用python.exe来启动main以便捕获控制台输出 + python_exe = sys.executable + if 'pythonw' in python_exe.lower(): + python_exe = python_exe.replace('pythonw.exe', 'python.exe').replace('pythonw', 'python') + run_args = [python_exe, '-m', 'src.main'] if len(args['character']): run_args.append('--character') run_args.append(args['character']) @@ -744,10 +760,16 @@ class LauncherPanel(wx.Panel): self.main_stderr_lines.clear() self.statusCtrl.SetValue('Launched') on_line = lambda line: _on_main_log_line(self, line) + # 使用CREATE_NO_WINDOW标志隐藏控制台窗口,但仍可捕获输出 + creation_flags = 0 + if sys.platform == 'win32': + # CREATE_NO_WINDOW = 0x08000000 + creation_flags = 0x08000000 p = subprocess.Popen( run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=creation_flags, ) threading.Thread( target=_read_pipe_to_stream, @@ -772,8 +794,14 @@ class MainFrame(wx.Frame): def OnClose(self, e): global p if p is not None: - subprocess.run(['taskkill', '/F', '/PID', str(p.pid), '/T'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + creation_flags = 0 + if sys.platform == 'win32': + # CREATE_NO_WINDOW = 0x08000000 + creation_flags = 0x08000000 + subprocess.run(['taskkill', '/F', '/PID', str(p.pid), '/T'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=creation_flags) e.Skip() def InitUi(self):