diff --git a/launcher2.py b/launcher2.py index c30d4cae..2858d024 100644 --- a/launcher2.py +++ b/launcher2.py @@ -1,3 +1,4 @@ +import atexit import ctypes import os import subprocess @@ -124,6 +125,44 @@ def scanStudentModels(): refreshList() scanStudentModels() + +def _run_hidden_taskkill(pid): + creation_flags = 0 + if sys.platform == 'win32': + creation_flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0x08000000) + subprocess.run( + ['taskkill', '/F', '/PID', str(pid), '/T'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=creation_flags, + check=False + ) + + +def stop_inference_process(): + global p + if p is None: + return False + + try: + if p.poll() is None: + _run_hidden_taskkill(p.pid) + try: + p.wait(timeout=1.5) + except Exception: + pass + except Exception: + try: + _run_hidden_taskkill(p.pid) + except Exception: + pass + finally: + p = None + return True + + +atexit.register(stop_inference_process) + def min_cutoff_mapper(value, revert=False): """ 非线性映射函数:0-100整数 <-> 0-100浮点数 @@ -611,20 +650,19 @@ def OnLaunch(self, e): f.close() self.btnLaunch.SetLabelText('Working...') - if p is not None: - 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 + if p is not None and p.poll() is None: + self.btnLaunch.Disable() self.statusCtrl.Clear() self.btnLaunch.SetLabelText("Save & Launch") + def _stop_async(): + stop_inference_process() + wx.CallAfter(self.btnLaunch.Enable) + + threading.Thread(target=_stop_async, daemon=True).start() + return else: # 如果启动器是用pythonw启动的,使用python.exe来启动main以便捕获控制台输出 + p = None python_exe = sys.executable if 'pythonw' in python_exe.lower(): python_exe = python_exe.replace('pythonw.exe', 'python.exe').replace('pythonw', 'python') @@ -764,7 +802,7 @@ def OnLaunch(self, e): creation_flags = 0 if sys.platform == 'win32': # CREATE_NO_WINDOW = 0x08000000 - creation_flags = 0x08000000 + creation_flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0x08000000) | getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0x00000200) p = subprocess.Popen( run_args, stdout=subprocess.PIPE, @@ -792,16 +830,7 @@ def __init__(self, *args, **kw): self.Bind(wx.EVT_CLOSE, self.OnClose) def OnClose(self, e): - global p - if p is not None: - 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) + stop_inference_process() e.Skip() def InitUi(self): diff --git a/src/main.py b/src/main.py index 2545b5ec..ae8dc09d 100644 --- a/src/main.py +++ b/src/main.py @@ -13,8 +13,34 @@ from OpenGL.GL import GL_RGBA +def _terminate_process(process): + if process is None: + return + try: + if process.is_alive(): + process.terminate() + except Exception: + pass + try: + process.join(timeout=1.0) + except Exception: + pass + + +def _cleanup_shared_memory(shm): + if shm is None: + return + try: + shm.close() + except Exception: + pass + try: + shm.unlink() + except Exception: + pass + + def main(): - # Load character image img = Image.open(f"data/images/{args.character}.png") img = img.convert('RGBA') ow, oh = img.size @@ -38,121 +64,191 @@ 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) 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() - - 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 = [ - SharedMemoryGuard(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, 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) - ] - - 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) - 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.") - - 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(n_frames): - ret_batch_shm_channels[i].acquire() - - # 动态周期:本批就绪与上一批就绪的时间间隔,用于本批内均匀排期 - 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)) + infer_process = None + ret_batch_shm_channels = [] + virtual_cam = None + spout_sender = None + + try: + 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() + + 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 = [ + SharedMemoryGuard(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, 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) + ] + + last_time = time.perf_counter() + interval = 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 + ) + 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: - 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: - spout_sender.send_image(np_ret_shms[i], False) - else: - cv2.imshow("EasyVtuber Debug Frame", np_ret_shms[i]) - cv2.waitKey(1) - now_send = time.perf_counter() - 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 - 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("Using OpenCV windows for output display.") + + pipeline_fps = FPS() + last_batch_start_time = None + n_frames = args.interpolation_scale + min_period = n_frames * interval if interval > 0 else n_frames / 60.0 + default_period = 1.0 / 15.0 + + print("Interval set to {:.3f} seconds".format(interval)) + should_exit = False + + while True: + if not infer_process.finish_event.wait(timeout=1.0): + if not infer_process.is_alive(): + print("\nInference process exited.") + break + continue + + infer_process.finish_event.clear() + + acquired_count = 0 + for i in range(n_frames): + ret_batch_shm_channels[i].acquire() + acquired_count += 1 + + try: + 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): + 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: + spout_sender.send_image(np_ret_shms[i], False) + else: + cv2.imshow("EasyVtuber Debug Frame", np_ret_shms[i]) + key = cv2.waitKey(1) & 0xFF + if key == 27 or key == ord('q'): + should_exit = True + else: + try: + if cv2.getWindowProperty("EasyVtuber Debug Frame", cv2.WND_PROP_VISIBLE) < 1: + should_exit = True + except cv2.error: + should_exit = True + + now_send = time.perf_counter() + if interval > 0: + last_time += interval + if last_time < now_send: + last_time = now_send + + if should_exit: + break + + if should_exit: + break + + 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 + ) + finally: + for i in range(acquired_count): + try: + ret_batch_shm_channels[i].release() + except Exception: + pass + + finally: + _terminate_process(input_process) + _terminate_process(infer_process) + + if virtual_cam is not None: + try: + virtual_cam.close() + except Exception: + pass + + if spout_sender is not None: + try: + for close_fn_name in ('release', 'ReleaseSender', 'close'): + close_fn = getattr(spout_sender, close_fn_name, None) + if callable(close_fn): + close_fn() + break + except Exception: + pass + + cv2.destroyAllWindows() + + if infer_process is not None: + _cleanup_shared_memory(infer_process.ret_shared_mem) + _cleanup_shared_memory(pose_position_shm) if __name__ == "__main__":