deepfacelab中文网

 找回密码
 立即注册(仅限QQ邮箱)
12
返回列表 发新帖
楼主: day270010678

SelfSwapper(SAMAE,ECCV2024)

[复制链接]

44

主题

275

帖子

2228

积分

初级丹圣

Rank: 8Rank: 8

积分
2228
 楼主| 发表于 昨天 23:24 | 显示全部楼层
caiji132156 发表于 2026-9-29 23:19
捏脸是指调五官大小和形态吧,你要传统图形学的还是深度学习的。捏脸的目的是什么
传统图形学就基于lmk液 ...
  1. """
  2. face_sculpt_tab.py - 捏脸Tab面板

  3. 共享组件,集成到step8数字人面板。
  4. 布局:上方SD配置(prompt/负向词/推理参数),下方五官参数滑块。
  5. 五官参数不映射到prompt,而是对已生成图片做landmark位移+图像warp实时变形。
  6. """

  7. import gc
  8. import threading
  9. import logging
  10. import random
  11. from pathlib import Path

  12. import cv2
  13. import numpy as np
  14. from PyQt6.QtCore import Qt, pyqtSignal, QObject, QTimer
  15. from PyQt6.QtWidgets import (
  16.     QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
  17.     QSpinBox, QDoubleSpinBox, QTextEdit, QGroupBox, QGridLayout,
  18.     QMessageBox, QFileDialog, QSizePolicy,
  19. )

  20. import orjson

  21. from faceswap.setting import LORA_MODEL_DIR
  22. from faceswap.gui_app.widgets.convert_model_selector import ConvertModelSelector
  23. from faceswap.gui_app.widgets.face_param_sliders import FaceParamSliders


  24. class _SculptSignal(QObject):
  25.     log = pyqtSignal(str)
  26.     done = pyqtSignal(bool, object)


  27. class _QtLogHandler(logging.Handler):
  28.     def __init__(self, emit_fn):
  29.         super().__init__()
  30.         self._emit_fn = emit_fn

  31.     def emit(self, record):
  32.         try:
  33.             self._emit_fn(self.format(record))
  34.         except Exception:
  35.             pass


  36. class FaceSculptTab(QWidget):
  37.     """捏脸Tab面板。"""

  38.     PREVIEW_SIZE = 512

  39.     def __init__(self, parent=None):
  40.         super().__init__(parent)
  41.         self._sculpt_sig = _SculptSignal()
  42.         self._sculpt_sig.log.connect(self._on_sculpt_log)
  43.         self._sculpt_sig.done.connect(self._on_sculpt_done)
  44.         self._sculpt_ref_image_path = None
  45.         self._sculpt_stop_event = threading.Event()
  46.         self._sculpt_running = False
  47.         self._sculpt_base_image = None
  48.         self._sculpt_current_image = None
  49.         self._sculpt_base_landmarks = None
  50.         self._warp_timer = QTimer()
  51.         self._warp_timer.setSingleShot(True)
  52.         self._warp_timer.timeout.connect(self._do_warp)
  53.         self._build_ui()
  54.         from faceswap.gui_app.widgets.image_utils import attach_save_image_menu
  55.         attach_save_image_menu(
  56.             self._sculpt_result_preview,
  57.             lambda: cv2.cvtColor(self._sculpt_current_image, cv2.COLOR_RGB2BGR) if self._sculpt_current_image is not None else None,
  58.         )

  59.     def _build_ui(self):
  60.         lay = QVBoxLayout(self)
  61.         lay.setContentsMargins(4, 8, 4, 4)
  62.         lay.setSpacing(8)

  63.         # 模型选择 GroupBox(与转换数字人面板样式一致)
  64.         mod_grp = QGroupBox("模型选择")
  65.         mod_lay = QVBoxLayout(mod_grp)
  66.         mod_lay.setSpacing(6)
  67.         self._sculpt_model_selector = ConvertModelSelector(self)
  68.         self._sculpt_model_selector.model_changed.connect(self._on_sculpt_model_changed)
  69.         mod_lay.addWidget(self._sculpt_model_selector)
  70.         info_row = QHBoxLayout()
  71.         self._sculpt_base_label = QLabel("底座: 自动检测")
  72.         self._sculpt_base_label.setStyleSheet("color: #666; font-size: 11px;")
  73.         info_row.addWidget(self._sculpt_base_label)
  74.         self._sculpt_arch_label = QLabel("架构: -")
  75.         self._sculpt_arch_label.setStyleSheet("color: #666; font-size: 11px;")
  76.         info_row.addWidget(self._sculpt_arch_label)
  77.         self._sculpt_output_label = QLabel("输出: -")
  78.         self._sculpt_output_label.setStyleSheet("color: #666; font-size: 11px;")
  79.         info_row.addWidget(self._sculpt_output_label)
  80.         info_row.addStretch()
  81.         mod_lay.addLayout(info_row)
  82.         lay.addWidget(mod_grp)

  83.         content_row = QHBoxLayout()
  84.         content_row.setSpacing(8)

  85.         left_lay = QVBoxLayout()
  86.         left_lay.setSpacing(6)

  87.         base_prompt_grp = QGroupBox("基础 Prompt(配合SD生成图片)")
  88.         base_prompt_lay = QVBoxLayout(base_prompt_grp)
  89.         base_prompt_row = QHBoxLayout()
  90.         self._sculpt_base_prompt = QTextEdit("a photo of a person")
  91.         self._sculpt_base_prompt.setFixedHeight(50)
  92.         self._sculpt_base_prompt.setStyleSheet("font-family: Consolas, monospace; font-size: 11px;")
  93.         base_prompt_row.addWidget(self._sculpt_base_prompt)
  94.         self._sculpt_prompt_translate_btn = QPushButton("翻译")
  95.         self._sculpt_prompt_translate_btn.setFixedWidth(90)
  96.         self._sculpt_prompt_translate_btn.setToolTip("将中文翻译为英文")
  97.         self._sculpt_prompt_translate_btn.clicked.connect(lambda: self._do_translate(self._sculpt_base_prompt, self._sculpt_prompt_translate_btn))
  98.         base_prompt_row.addWidget(self._sculpt_prompt_translate_btn)
  99.         base_prompt_lay.addLayout(base_prompt_row)
  100.         left_lay.addWidget(base_prompt_grp)
  101.         base_prompt_grp.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)

  102.         neg_grp = QGroupBox("负向词")
  103.         neg_lay = QVBoxLayout(neg_grp)
  104.         neg_row = QHBoxLayout()
  105.         self._sculpt_negative = QTextEdit("ugly, deformed, blurry, low quality")
  106.         self._sculpt_negative.setFixedHeight(50)
  107.         self._sculpt_negative.setStyleSheet("font-family: Consolas, monospace; font-size: 11px;")
  108.         neg_row.addWidget(self._sculpt_negative)
  109.         self._sculpt_neg_translate_btn = QPushButton("翻译")
  110.         self._sculpt_neg_translate_btn.setFixedWidth(90)
  111.         self._sculpt_neg_translate_btn.setToolTip("将中文翻译为英文")
  112.         self._sculpt_neg_translate_btn.clicked.connect(lambda: self._do_translate(self._sculpt_negative, self._sculpt_neg_translate_btn))
  113.         neg_row.addWidget(self._sculpt_neg_translate_btn)
  114.         neg_lay.addLayout(neg_row)
  115.         left_lay.addWidget(neg_grp)
  116.         neg_grp.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)

  117.         param_grp = QGroupBox("推理参数")
  118.         param_lay = QGridLayout(param_grp)
  119.         param_lay.setSpacing(6)
  120.         param_lay.setColumnStretch(0, 0)
  121.         param_lay.setColumnStretch(1, 1)
  122.         param_lay.setColumnStretch(2, 0)
  123.         param_lay.setColumnStretch(3, 1)
  124.         param_lay.addWidget(QLabel("分辨率:"), 0, 0)
  125.         self._sculpt_resolution = QSpinBox()
  126.         self._sculpt_resolution.setRange(256, 2048)
  127.         self._sculpt_resolution.setSingleStep(64)
  128.         self._sculpt_resolution.setValue(512)
  129.         param_lay.addWidget(self._sculpt_resolution, 0, 1)
  130.         param_lay.addWidget(QLabel("强度:"), 0, 2)
  131.         self._sculpt_strength = QDoubleSpinBox()
  132.         self._sculpt_strength.setRange(0.1, 0.9)
  133.         self._sculpt_strength.setSingleStep(0.05)
  134.         self._sculpt_strength.setValue(0.5)
  135.         param_lay.addWidget(self._sculpt_strength, 0, 3)
  136.         param_lay.addWidget(QLabel("步数:"), 1, 0)
  137.         self._sculpt_steps = QSpinBox()
  138.         self._sculpt_steps.setRange(10, 100)
  139.         self._sculpt_steps.setValue(30)
  140.         param_lay.addWidget(self._sculpt_steps, 1, 1)
  141.         param_lay.addWidget(QLabel("CFG:"), 1, 2)
  142.         self._sculpt_cfg = QDoubleSpinBox()
  143.         self._sculpt_cfg.setRange(1.0, 20.0)
  144.         self._sculpt_cfg.setSingleStep(0.5)
  145.         self._sculpt_cfg.setValue(7.5)
  146.         param_lay.addWidget(self._sculpt_cfg, 1, 3)
  147.         param_lay.addWidget(QLabel("LoRA强度:"), 2, 0)
  148.         self._sculpt_lora_scale = QDoubleSpinBox()
  149.         self._sculpt_lora_scale.setRange(0.0, 2.0)
  150.         self._sculpt_lora_scale.setSingleStep(0.1)
  151.         self._sculpt_lora_scale.setValue(1.0)
  152.         param_lay.addWidget(self._sculpt_lora_scale, 2, 1)
  153.         param_lay.addWidget(QLabel("种子:"), 3, 0)
  154.         self._sculpt_seed = QSpinBox()
  155.         self._sculpt_seed.setRange(0, 999999)
  156.         self._sculpt_seed.setValue(42)
  157.         param_lay.addWidget(self._sculpt_seed, 3, 1)
  158.         self._sculpt_random_seed_btn = QPushButton("随机种子")
  159.         self._sculpt_random_seed_btn.clicked.connect(self._on_random_seed)
  160.         param_lay.addWidget(self._sculpt_random_seed_btn, 3, 2, 1, 2)
  161.         left_lay.addWidget(param_grp)
  162.         param_grp.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)

  163.         sculpt_grp = QGroupBox("五官参数(实时改变已生成图片,无需重新生成)")
  164.         sculpt_lay = QVBoxLayout(sculpt_grp)
  165.         self._sculpt_param_sliders = FaceParamSliders()
  166.         self._sculpt_param_sliders.params_changed.connect(self._on_sculpt_params_changed)
  167.         self._sculpt_param_sliders.setEnabled(False)
  168.         
  169.         # 设置推荐配置回调:根据当前 landmarks 计算黄金比例
  170.         def _calc_recommend():
  171.             if self._sculpt_base_landmarks is None:
  172.                 QMessageBox.warning(self, "无参考图", "请先上传参考图或生成图片后再使用推荐配置")
  173.                 return {}
  174.             return self.calc_golden_ratio_params(self._sculpt_base_landmarks)
  175.         
  176.         self._sculpt_param_sliders.recommend_callback = _calc_recommend
  177.         sculpt_lay.addWidget(self._sculpt_param_sliders)
  178.         reset_row = QHBoxLayout()
  179.         reset_row.addStretch()
  180.         self._sculpt_reset_btn = QPushButton("重置全部")
  181.         self._sculpt_reset_btn.clicked.connect(self._sculpt_param_sliders.reset)
  182.         reset_row.addWidget(self._sculpt_reset_btn)
  183.         sculpt_lay.addLayout(reset_row)
  184.         left_lay.addWidget(sculpt_grp)
  185.         sculpt_grp.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)

  186.         left_lay.addStretch()

  187.         content_row.addLayout(left_lay, stretch=1)

  188.         right_widget = QWidget()
  189.         right_widget.setFixedWidth(560)
  190.         right_lay = QVBoxLayout(right_widget)
  191.         right_lay.setContentsMargins(2, 2, 2, 2)
  192.         right_lay.setSpacing(6)

  193.         ref_grp = QGroupBox("参考图(可选,上传后基于参考图捏脸)")
  194.         ref_lay = QVBoxLayout(ref_grp)
  195.         self._sculpt_ref_preview = QLabel("点击上传参考图\n或留空随机生成")
  196.         self._sculpt_ref_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
  197.         self._sculpt_ref_preview.setFixedSize(512, 256)
  198.         self._sculpt_ref_preview.setStyleSheet("border: 1px solid #CCC; background: #F0F0F0; color: #999;")
  199.         self._sculpt_ref_preview.mousePressEvent = lambda e: self._on_upload_ref()
  200.         ref_lay.addWidget(self._sculpt_ref_preview)
  201.         self._sculpt_clear_ref_btn = QPushButton("清除参考图")
  202.         self._sculpt_clear_ref_btn.clicked.connect(self._on_clear_ref)
  203.         ref_lay.addWidget(self._sculpt_clear_ref_btn)
  204.         right_lay.addWidget(ref_grp)

  205.         result_grp = QGroupBox("生成结果")
  206.         result_lay = QVBoxLayout(result_grp)
  207.         self._sculpt_result_preview = QLabel("点击生成按钮创建人脸")
  208.         self._sculpt_result_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
  209.         self._sculpt_result_preview.setFixedSize(512, 420)
  210.         self._sculpt_result_preview.setStyleSheet("border: 1px solid #CCC; background: #F0F0F0; color: #999;")
  211.         result_lay.addWidget(self._sculpt_result_preview)
  212.         right_lay.addWidget(result_grp)

  213.         content_row.addWidget(right_widget)
  214.         lay.addLayout(content_row)

  215.         run_row = QHBoxLayout()
  216.         run_row.addStretch()
  217.         self._sculpt_start_btn = QPushButton("生成")
  218.         self._sculpt_start_btn.setFixedWidth(120)
  219.         self._sculpt_start_btn.setStyleSheet(
  220.             "QPushButton { background-color: #0078D4; color: white; font-weight: bold; "
  221.             "padding: 6px 16px; border-radius: 3px; }"
  222.             "QPushButton:hover { background-color: #106EBE; }"
  223.             "QPushButton:disabled { background-color: #A0A0A0; color: #E0E0E0; }")
  224.         self._sculpt_start_btn.clicked.connect(self._on_start_sculpt)
  225.         run_row.addWidget(self._sculpt_start_btn)
  226.         self._sculpt_stop_btn = QPushButton("停止")
  227.         self._sculpt_stop_btn.setFixedWidth(80)
  228.         self._sculpt_stop_btn.setEnabled(False)
  229.         self._sculpt_stop_btn.setProperty("busy_keep", True)
  230.         self._sculpt_stop_btn.setStyleSheet(
  231.             "QPushButton { background-color: #C42B1C; color: white; font-weight: bold; "
  232.             "padding: 6px 16px; border-radius: 3px; }"
  233.             "QPushButton:hover { background-color: #D43B2C; }"
  234.             "QPushButton:disabled { background-color: #A0A0A0; color: #E0E0E0; }")
  235.         self._sculpt_stop_btn.clicked.connect(self._on_stop_sculpt)
  236.         run_row.addWidget(self._sculpt_stop_btn)
  237.         lay.addLayout(run_row)

  238.         self._sculpt_log_label = QLabel("")
  239.         self._sculpt_log_label.setStyleSheet(
  240.             "background: #1E1E1E; color: #00FF00; font-family: Consolas, monospace; "
  241.             "font-size: 11px; padding: 2px 8px; border-radius: 2px;")
  242.         self._sculpt_log_label.setWordWrap(False)
  243.         self._sculpt_log_label.setVisible(False)
  244.         lay.addWidget(self._sculpt_log_label)

  245.     def _on_sculpt_model_changed(self, kind, lora_path, base_model_path, arch, extra):
  246.         # 更新信息标签(与转换数字人面板一致)
  247.         self._sculpt_base_label.setText(f"底座: {Path(base_model_path).name if base_model_path else '未知'}")
  248.         self._sculpt_arch_label.setText(f"架构: {arch}")
  249.         if kind == "lora" and lora_path:
  250.             self._sculpt_output_label.setText(f"输出: LoRA")
  251.             lora_dir = Path(lora_path).parent
  252.             if lora_dir.name == "final":
  253.                 lora_dir = lora_dir.parent
  254.             cfg_path = lora_dir / "lora_training_config.json"
  255.             if cfg_path.exists():
  256.                 try:
  257.                     cfg = orjson.loads(cfg_path.read_bytes())
  258.                     res = cfg.get("resolution", 512)
  259.                     pc = cfg.get("prompt_config", {})
  260.                     self._sculpt_resolution.setValue(res)
  261.                     neg = pc.get("negative_prompt", "")
  262.                     if neg:
  263.                         self._sculpt_negative.setPlainText(neg)
  264.                     if "gen_steps" in pc:
  265.                         self._sculpt_steps.setValue(int(pc["gen_steps"]))
  266.                     if "gen_cfg" in pc:
  267.                         self._sculpt_cfg.setValue(float(pc["gen_cfg"]))
  268.                     if "gen_lora_scale" in pc:
  269.                         self._sculpt_lora_scale.setValue(float(pc["gen_lora_scale"]))
  270.                     if "gen_seed" in pc:
  271.                         self._sculpt_seed.setValue(int(pc["gen_seed"]))
  272.                 except Exception:
  273.                     pass
  274.         elif kind == "base_only":
  275.             self._sculpt_output_label.setText("输出: 仅底座")
  276.         else:
  277.             self._sculpt_output_label.setText("输出: -")

  278.     def _on_sculpt_params_changed(self, params):
  279.         if self._sculpt_base_image is None:
  280.             QMessageBox.warning(self, "无参考图", "请先上传参考图或生成图片后再调节参数")
  281.             return
  282.         self._warp_timer.start(50)

  283.     def _do_warp(self):
  284.         if self._sculpt_base_image is None:
  285.             return
  286.         if self._sculpt_base_landmarks is None:
  287.             self.window().statusBar().showMessage("未检测到人脸landmark,无法捏脸", 3000)
  288.             return
  289.         params = self._sculpt_param_sliders.get_params()
  290.         if not any(abs(v) > 0.01 for v in params.values()):
  291.             self._sculpt_current_image = self._sculpt_base_image
  292.             self._display_image(self._sculpt_base_image, self._sculpt_result_preview)
  293.             return
  294.         try:
  295.             warped = self._warp_face(self._sculpt_base_image, self._sculpt_base_landmarks, params)
  296.             warped = self._widen_forehead(warped, self._sculpt_base_landmarks)
  297.             if warped is not None and warped.size > 0:
  298.                 self._sculpt_current_image = warped
  299.                 self._display_image(warped, self._sculpt_result_preview)
  300.             else:
  301.                 self.window().statusBar().showMessage("变形结果为空", 3000)
  302.         except Exception as e:
  303.             self.window().statusBar().showMessage(f"变形失败: {e}", 3000)

  304.     def _widen_forehead(self, img: np.ndarray, landmarks: np.ndarray) -> np.ndarray:
  305.         """用cv2.remap平滑拉伸额头x,确保额头≥颧骨。单图变形无重影。"""
  306.         import cv2
  307.         h, w = img.shape[:2]
  308.         if len(landmarks) <= 101:
  309.             return img
  310.         brow_y = int((landmarks[43, 1] + landmarks[101, 1]) / 2)
  311.         brow_width = abs(landmarks[101, 0] - landmarks[43, 0])
  312.         face_cx = float((landmarks[43, 0] + landmarks[101, 0]) / 2.0)

  313.         from faceswap.core.landmarks106 import LANDMARK_GROUPS_106
  314.         groups = dict(LANDMARK_GROUPS_106)
  315.         jaw_idx = groups.get("jaw_cheek", [])
  316.         valid_jaw = [i for i in jaw_idx if i < len(landmarks)]
  317.         if not valid_jaw:
  318.             return img
  319.         cheek_width = float(landmarks[valid_jaw, 0].max() - landmarks[valid_jaw, 0].min())

  320.         target_width = max(brow_width, cheek_width * 1.05)
  321.         if brow_width <= 0.5 or target_width <= brow_width or brow_y <= 2:
  322.             return img

  323.         s = target_width / brow_width
  324.         map_x = np.tile(np.arange(w, dtype=np.float32), (h, 1))
  325.         map_y = np.tile(np.arange(h, dtype=np.float32).reshape(-1, 1), (1, w))

  326.         fh = min(brow_y, h)
  327.         ys = np.arange(fh, dtype=np.float32)
  328.         t = 1.0 - ys / fh
  329.         smooth = t * t * (3.0 - 2.0 * t)
  330.         stretch = (s - 1.0) * smooth
  331.         offset = stretch[:, None] * (np.arange(w, dtype=np.float32)[None, :] - face_cx)
  332.         map_x[:fh, :] -= offset

  333.         return cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR,
  334.                          borderMode=cv2.BORDER_REFLECT_101)

  335.     @staticmethod
  336.     def _warp_face(base_img: np.ndarray, landmarks: np.ndarray, params: dict) -> np.ndarray:
  337.         import cv2
  338.         from faceswap.core.landmarks106 import LANDMARK_GROUPS_106
  339.         groups = dict(LANDMARK_GROUPS_106)
  340.         h, w = base_img.shape[:2]
  341.         src_pts = landmarks.astype(np.float32).copy()
  342.         dst_pts = src_pts.copy()

  343.         def scale_region(idx_list, scale_x, scale_y):
  344.             if not idx_list:
  345.                 return
  346.             valid = [i for i in idx_list if i < len(dst_pts)]
  347.             if not valid:
  348.                 return
  349.             local_cx = float(np.mean(dst_pts[valid, 0]))
  350.             local_cy = float(np.mean(dst_pts[valid, 1]))
  351.             for idx in valid:
  352.                 dx = dst_pts[idx, 0] - local_cx
  353.                 dy = dst_pts[idx, 1] - local_cy
  354.                 dst_pts[idx, 0] = local_cx + dx * scale_x
  355.                 dst_pts[idx, 1] = local_cy + dy * scale_y

  356.         def shift_region(idx_list, shift_x, shift_y):
  357.             for idx in idx_list:
  358.                 if idx >= len(dst_pts):
  359.                     continue
  360.                 dst_pts[idx, 0] += shift_x
  361.                 dst_pts[idx, 1] += shift_y

  362.         # ===== 骨骼结构参数 =====
  363.         face_w = 1.0 + params.get("face_width", 0) * 0.15
  364.         face_h = 1.0 + params.get("face_height", 0) * 0.1
  365.         cheekbone_w = 1.0 + params.get("cheekbone_width", 0) * 0.1
  366.         jaw_w = 1.0 + params.get("jaw_width", 0) * 0.12
  367.         chin_scale_y = 1.0 + params.get("chin_shape", 0) * 0.2
  368.         face_round = 1.0 + params.get("face_roundness", 0) * 0.1
  369.         
  370.         # ===== 五官细节参数 =====
  371.         eye_scale = 1.0 + params.get("eye_size", 0) * 0.3
  372.         mouth_scale = 1.0 + params.get("mouth_size", 0) * 0.3
  373.         nose_scale = 1.0 + params.get("nose_size", 0) * 0.25
  374.         brow_scale = 1.0 + params.get("eyebrow_thickness", 0) * 0.2
  375.         lip_scale = 1.0 + params.get("lip_thickness", 0) * 0.25
  376.         eye_dist_shift = params.get("eye_distance", 0) * 15.0

  377.         # ===== 骨骼结构变形 =====
  378.         # 分层独立控制:face_w只拉宽最高1/3(额头),cheekbone_w缩窄中1/3(颧骨),jaw_w缩窄下1/3(下颌)
  379.         jaw_cheek = groups.get("jaw_cheek", [])
  380.         cheekbone_pts = []
  381.         if jaw_cheek:
  382.             valid_jaw = [i for i in jaw_cheek if i < len(dst_pts)]
  383.             if valid_jaw:
  384.                 sorted_by_y = sorted(valid_jaw, key=lambda i: src_pts[i, 1])
  385.                 n = len(sorted_by_y)
  386.                 temple_pts = sorted_by_y[:n // 3]
  387.                 cheekbone_pts = sorted_by_y[n // 3: 2 * n // 3]
  388.                 jaw_pts = sorted_by_y[2 * n // 3:]

  389.                 if temple_pts:
  390.                     t_cx = float(np.mean(dst_pts[temple_pts, 0]))
  391.                     t_cy = float(np.mean(dst_pts[temple_pts, 1]))
  392.                     for idx in temple_pts:
  393.                         dx = dst_pts[idx, 0] - t_cx
  394.                         dy = dst_pts[idx, 1] - t_cy
  395.                         dst_pts[idx, 0] = t_cx + dx * face_w
  396.                         dst_pts[idx, 1] = t_cy + dy * face_h

  397.                 if cheekbone_pts:
  398.                     cheek_cx = float(np.mean(dst_pts[cheekbone_pts, 0]))
  399.                     for idx in cheekbone_pts:
  400.                         dx = dst_pts[idx, 0] - cheek_cx
  401.                         dst_pts[idx, 0] = cheek_cx + dx * cheekbone_w

  402.                 if jaw_pts:
  403.                     jaw_cx = float(np.mean(dst_pts[jaw_pts, 0]))
  404.                     for idx in jaw_pts:
  405.                         dx = dst_pts[idx, 0] - jaw_cx
  406.                         dst_pts[idx, 0] = jaw_cx + dx * jaw_w

  407.                 face_cy = float(np.mean(dst_pts[valid_jaw, 1]))
  408.                 for idx in valid_jaw:
  409.                     dy = dst_pts[idx, 1] - face_cy
  410.                     dst_pts[idx, 1] = face_cy + dy * face_round

  411.         # ===== 五官细节变形 =====
  412.         scale_region(groups.get("right_eye", []), eye_scale, eye_scale)
  413.         scale_region(groups.get("left_eye", []), eye_scale, eye_scale)
  414.         scale_region(groups.get("outer_lip", []), mouth_scale, mouth_scale)
  415.         scale_region(groups.get("inner_lip", []), lip_scale, lip_scale)
  416.         scale_region(groups.get("nose", []), nose_scale, nose_scale)
  417.         scale_region(groups.get("right_eyebrow", []), brow_scale, 1.0)
  418.         scale_region(groups.get("left_eyebrow", []), brow_scale, 1.0)

  419.         jaw = groups.get("jaw_cheek", [])
  420.         chin_idx = [i for i in jaw if i < len(dst_pts)]
  421.         if chin_idx:
  422.             chin_cy = float(np.mean(dst_pts[chin_idx, 1]))
  423.             for idx in chin_idx:
  424.                 dy = dst_pts[idx, 1] - chin_cy
  425.                 dst_pts[idx, 1] = chin_cy + dy * chin_scale_y

  426.         shift_region(groups.get("left_eye", []), -eye_dist_shift, 0)
  427.         shift_region(groups.get("right_eye", []), eye_dist_shift, 0)

  428.         displacement = dst_pts - src_pts
  429.         moved = np.any(np.abs(displacement) > 0.01, axis=1)
  430.         if not np.any(moved):
  431.             return base_img

  432.         src_moved = src_pts[moved]
  433.         disp_moved = displacement[moved]

  434.         bbox_w = float(src_pts[:, 0].max() - src_pts[:, 0].min())
  435.         bbox_h = float(src_pts[:, 1].max() - src_pts[:, 1].min())
  436.         sigma = max(bbox_w, bbox_h) * 0.2

  437.         step = 4
  438.         yy, xx = np.mgrid[0:h:step, 0:w:step].astype(np.float32)
  439.         map_x = xx.copy()
  440.         map_y = yy.copy()
  441.         for i in range(len(src_moved)):
  442.             sx, sy = src_moved[i]
  443.             dx, dy = disp_moved[i]
  444.             dist_sq = (xx - sx) ** 2 + (yy - sy) ** 2
  445.             weight = np.exp(-dist_sq / (2 * sigma * sigma))
  446.             map_x += dx * weight
  447.             map_y += dy * weight

  448.         map_x_full = cv2.resize(map_x, (w, h), interpolation=cv2.INTER_LINEAR)
  449.         map_y_full = cv2.resize(map_y, (w, h), interpolation=cv2.INTER_LINEAR)
  450.         warped = cv2.remap(base_img, map_x_full, map_y_full,
  451.                            cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
  452.         return warped

  453.     def calc_golden_ratio_params(self, landmarks: np.ndarray) -> dict:
  454.         """根据landmarks实测比例与黄金比例的偏差,按滑块灵敏度换算推荐值。"""
  455.         if len(landmarks) < 10:
  456.             return {}

  457.         from faceswap.core.landmarks106 import LANDMARK_GROUPS_106
  458.         groups = dict(LANDMARK_GROUPS_106)

  459.         def _idx(name):
  460.             return [i for i in groups.get(name, []) if i < len(landmarks)]

  461.         # ===== 测量 =====
  462.         face_top = landmarks[:, 1].min()
  463.         face_bottom = landmarks[:, 1].max()
  464.         H = face_bottom - face_top
  465.         if H < 1:
  466.             return {}

  467.         # jaw_cheek 按y排序分三层
  468.         jaw_cheek = _idx("jaw_cheek")
  469.         if not jaw_cheek:
  470.             return {}
  471.         sorted_jaw = sorted(jaw_cheek, key=lambda i: landmarks[i, 1])
  472.         n = len(sorted_jaw)
  473.         temple_pts = sorted_jaw[:n // 3]
  474.         cheek_pts = sorted_jaw[n // 3: 2 * n // 3]
  475.         jaw_pts = sorted_jaw[2 * n // 3:]

  476.         W_temple = landmarks[temple_pts, 0].max() - landmarks[temple_pts, 0].min()
  477.         W_cheek = landmarks[cheek_pts, 0].max() - landmarks[cheek_pts, 0].min()
  478.         W_jaw = landmarks[jaw_pts, 0].max() - landmarks[jaw_pts, 0].min()

  479.         # 三庭
  480.         brow_idx = _idx("right_eyebrow") + _idx("left_eyebrow")
  481.         nose_idx = _idx("nose")
  482.         eye_idx = _idx("right_eye") + _idx("left_eye")
  483.         brow_y = landmarks[brow_idx, 1].mean() if brow_idx else face_top
  484.         nose_bottom_y = landmarks[nose_idx, 1].max() if nose_idx else face_bottom
  485.         upper = brow_y - face_top
  486.         middle = nose_bottom_y - brow_y
  487.         lower = face_bottom - nose_bottom_y

  488.         # 眼睛位置
  489.         eye_y_ratio = 0.5
  490.         if eye_idx:
  491.             eye_y = landmarks[eye_idx, 1].mean()
  492.             eye_y_ratio = (eye_y - face_top) / H

  493.         # ===== 计算差值 → 按灵敏度换算滑块值 =====
  494.         # 灵敏度: face_width=15%, face_height=10%, cheekbone=10%, jaw=12%, chin=20%, roundness=10%
  495.         params = {}

  496.         # 1. 颧骨: W_cheek/H 理想≈0.618
  497.         if W_cheek > 0:
  498.             cheek_ratio = W_cheek / H
  499.             cheek_pct = (cheek_ratio - 0.618) / cheek_ratio
  500.             params["cheekbone_width"] = max(-50, min(50, int(-cheek_pct / 0.10 * 100)))

  501.         # 2. 下颌: W_jaw/W_cheek 理想≈0.62
  502.         if W_jaw > 0 and W_cheek > 0:
  503.             jaw_ratio = W_jaw / W_cheek
  504.             jaw_pct = (jaw_ratio - 0.62) / jaw_ratio
  505.             params["jaw_width"] = max(-50, min(50, int(-jaw_pct / 0.12 * 100)))

  506.         # 3. 太阳穴: W_temple/W_cheek 理想≥1.0(额头应≥颧骨)
  507.         if W_temple > 0 and W_cheek > 0:
  508.             temple_ratio = W_temple / W_cheek
  509.             if temple_ratio < 1.0:
  510.                 temple_pct = (1.0 - temple_ratio) / temple_ratio
  511.                 params["face_width"] = max(-30, min(30, int(temple_pct / 0.15 * 100)))
  512.             else:
  513.                 params["face_width"] = 0

  514.         # 4. 下庭: lower 理想=H/3,过长→chin_shape负
  515.         lower_pct = (lower - H / 3) / H
  516.         params["chin_shape"] = max(-40, min(40, int(-lower_pct / 0.20 * 100)))

  517.         # 5. 中庭: middle 理想=H/3,过长→face_roundness负(纵向压缩)
  518.         middle_pct = (middle - H / 3) / H
  519.         params["face_roundness"] = max(-30, min(30, int(-middle_pct / 0.10 * 100)))

  520.         # 6. 眼睛位置: eye_y_ratio 理想=0.5,太高→拉长上庭
  521.         eye_diff = 0.5 - eye_y_ratio
  522.         params["face_height"] = max(-20, min(20, int(eye_diff / 0.10 * 100)))

  523.         # 五官不自动推荐(landmark均匀缩放破坏比例)
  524.         params.setdefault("eye_size", 0)
  525.         params.setdefault("eye_distance", 0)
  526.         params.setdefault("nose_size", 0)
  527.         params.setdefault("mouth_size", 0)
  528.         params.setdefault("lip_thickness", 0)
  529.         params.setdefault("eyebrow_thickness", 0)

  530.         return params

  531.     def _on_random_seed(self):
  532.         self._sculpt_seed.setValue(random.randint(0, 999999))

  533.     def _on_upload_ref(self):
  534.         path, _ = QFileDialog.getOpenFileName(self, "选择参考图", "", "图片 (*.jpg *.jpeg *.png *.bmp *.webp)")
  535.         if path:
  536.             self._sculpt_ref_image_path = path
  537.             from PyQt6.QtGui import QPixmap
  538.             pm = QPixmap(path)
  539.             lw = self._sculpt_ref_preview.width()
  540.             lh = self._sculpt_ref_preview.height()
  541.             pm = pm.scaled(lw, lh,
  542.                            Qt.AspectRatioMode.KeepAspectRatio,
  543.                            Qt.TransformationMode.SmoothTransformation)
  544.             self._sculpt_ref_preview.setPixmap(pm)
  545.             self._sculpt_ref_preview.setText("")
  546.             self._sculpt_result_preview.setText("正在加载并检测人脸...")

  547.             class _RefSig(QObject):
  548.                 done = pyqtSignal(bool, object, object)

  549.             self._ref_load_sig = _RefSig()
  550.             self._ref_load_sig.done.connect(self._on_ref_loaded)

  551.             def _worker():
  552.                 try:
  553.                     import cv2
  554.                     img = cv2.imread(path)
  555.                     if img is None:
  556.                         self._ref_load_sig.done.emit(False, None, None)
  557.                         return
  558.                     img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  559.                     landmarks = FaceSculptTab._detect_landmarks(img_rgb)
  560.                     self._ref_load_sig.done.emit(True, img_rgb, landmarks)
  561.                 except Exception:
  562.                     self._ref_load_sig.done.emit(False, None, None)

  563.             threading.Thread(target=_worker, daemon=True).start()

  564.     def _on_ref_loaded(self, success, img_rgb, landmarks):
  565.         if success and img_rgb is not None:
  566.             self._sculpt_base_image = img_rgb.copy()
  567.             self._sculpt_current_image = img_rgb.copy()
  568.             self._sculpt_base_landmarks = landmarks
  569.             self._display_image(img_rgb, self._sculpt_result_preview)
  570.             if landmarks is not None:
  571.                 self._sculpt_param_sliders.setEnabled(True)
  572.                 self.window().statusBar().showMessage(
  573.                     f"参考图已加载,检测到 {len(landmarks)} 个landmark点", 3000)
  574.             else:
  575.                 self._sculpt_param_sliders.setEnabled(False)
  576.                 QMessageBox.warning(self, "Landmark检测失败",
  577.                                     "未检测到人脸landmark,请确认图片中包含清晰人脸。")
  578.         else:
  579.             self._sculpt_result_preview.setText("点击上传参考图\n或点击生成")
  580.             QMessageBox.warning(self, "加载失败", "图片加载失败,请重试。")

  581.     def _on_clear_ref(self):
  582.         self._sculpt_ref_image_path = None
  583.         self._sculpt_ref_preview.clear()
  584.         self._sculpt_ref_preview.setText("点击上传参考图\n或留空随机生成")
  585.         self._sculpt_base_image = None
  586.         self._sculpt_current_image = None
  587.         self._sculpt_base_landmarks = None
  588.         self._sculpt_param_sliders.setEnabled(False)
  589.         self._sculpt_result_preview.clear()
  590.         self._sculpt_result_preview.setText("点击上传参考图\n或点击生成")

  591.     def _on_start_sculpt(self):
  592.         sel = self._sculpt_model_selector.get_selection()
  593.         if not sel.get("base_model_path"):
  594.             QMessageBox.warning(self, "未选择模型", "请先选择底座模型或LoRA模型")
  595.             return
  596.         self._sculpt_running = True
  597.         self._sculpt_stop_event.clear()
  598.         self.set_busy(True)
  599.         self._sculpt_stop_btn.setEnabled(True)
  600.         self._sculpt_log_label.setVisible(True)
  601.         self._sculpt_log_label.setText("准备中...")

  602.         params = {
  603.             "lora_path": sel.get("lora_path", ""),
  604.             "base_model": sel.get("base_model_path", ""),
  605.             "arch": sel.get("arch", ""),
  606.             "ref_image_path": self._sculpt_ref_image_path or "",
  607.             "prompt": self._sculpt_base_prompt.toPlainText().strip(),
  608.             "negative_prompt": self._sculpt_negative.toPlainText().strip(),
  609.             "resolution": self._sculpt_resolution.value(),
  610.             "strength": self._sculpt_strength.value(),
  611.             "steps": self._sculpt_steps.value(),
  612.             "cfg": self._sculpt_cfg.value(),
  613.             "lora_scale": self._sculpt_lora_scale.value(),
  614.             "seed": self._sculpt_seed.value(),
  615.         }

  616.         stop_event = self._sculpt_stop_event

  617.         def _worker_safe():
  618.             _qt_handler = None
  619.             try:
  620.                 _qt_handler = _QtLogHandler(self._sculpt_sig.log.emit)
  621.                 _sculpt_logger = logging.getLogger("faceswap")
  622.                 _sculpt_logger.addHandler(_qt_handler)
  623.                 self._sculpt_worker(params, stop_event)
  624.             except Exception as e:
  625.                 import traceback
  626.                 self._sculpt_sig.done.emit(False, traceback.format_exc())
  627.             finally:
  628.                 if _qt_handler:
  629.                     _sculpt_logger = logging.getLogger("faceswap")
  630.                     _sculpt_logger.removeHandler(_qt_handler)

  631.         threading.Thread(target=_worker_safe, daemon=True).start()

  632.     def _sculpt_worker(self, params, stop_event):
  633.         from faceswap.setting import init_vram_manager, get_torch_device
  634.         from faceswap.business.vram_manager import run_with_oom_fallback, clear_cuda_cache
  635.         import torch
  636.         from PIL import Image as PILImage
  637.         from pathlib import Path as _P
  638.         from diffusers import (
  639.             DiffusionPipeline, StableDiffusionPipeline, StableDiffusionXLPipeline,
  640.             StableDiffusion3Pipeline, AutoPipelineForImage2Image,
  641.         )

  642.         init_vram_manager()
  643.         device = get_torch_device()
  644.         load_dtype = torch.float16 if device.type == "cuda" else torch.float32
  645.         base_path = _P(params["base_model"])
  646.         self._sculpt_sig.log.emit("加载模型...")

  647.         _steps = [
  648.             ("sequential_cpu_offload", {"offload": "sequential"}),
  649.             ("model_cpu_offload",      {"offload": "model"}),
  650.             ("direct",                 {"offload": "direct"}),
  651.         ]
  652.         _pipe_holder = [None]

  653.         def _cleanup():
  654.             if _pipe_holder[0] is not None:
  655.                 del _pipe_holder[0]
  656.                 _pipe_holder[0] = None
  657.             gc.collect()
  658.             clear_cuda_cache()

  659.         def _execute(step_params):
  660.             offload = step_params["offload"]
  661.             if stop_event.is_set():
  662.                 raise RuntimeError("用户停止")

  663.             if (base_path / "model_index.json").exists():
  664.                 pipe = DiffusionPipeline.from_pretrained(
  665.                     str(base_path), torch_dtype=load_dtype,
  666.                     requires_safety_checker=False, local_files_only=True)
  667.             else:
  668.                 arch = params["arch"]
  669.                 sf_files = list(base_path.glob("*.safetensors")) + list(base_path.glob("*.ckpt"))
  670.                 if not sf_files:
  671.                     raise FileNotFoundError(f"底座目录无模型文件: {base_path}")
  672.                 pipe_cls = StableDiffusionXLPipeline if arch == "sdxl" else (
  673.                     StableDiffusion3Pipeline if arch in ("sd3", "sd35") else StableDiffusionPipeline)
  674.                 _sf_kwargs = dict(torch_dtype=load_dtype, requires_safety_checker=False,
  675.                                   local_files_only=True)
  676.                 sibling_dirs = [d for d in base_path.parent.iterdir()
  677.                                 if d.is_dir() and (d / "model_index.json").exists()]
  678.                 for d in sibling_dirs:
  679.                     try:
  680.                         test_pipe = DiffusionPipeline.from_pretrained(
  681.                             str(d), torch_dtype=load_dtype, local_files_only=True)
  682.                         if type(test_pipe).__name__ == type(pipe_cls).__name__:
  683.                             _sf_kwargs["config"] = str(d)
  684.                             del test_pipe
  685.                             break
  686.                         del test_pipe
  687.                     except Exception:
  688.                         pass
  689.                 pipe = pipe_cls.from_single_file(str(sf_files[0]), **_sf_kwargs)

  690.             if stop_event.is_set():
  691.                 raise RuntimeError("用户停止")
  692.             _pipe_holder[0] = pipe

  693.             if params.get("lora_path"):
  694.                 self._sculpt_sig.log.emit("加载LoRA权重...")
  695.                 pipe.load_lora_weights(params["lora_path"])

  696.             has_ref = bool(params.get("ref_image_path"))
  697.             if has_ref:
  698.                 pipe = AutoPipelineForImage2Image.from_pipe(pipe)
  699.             for attr in ("tokenizer", "tokenizer_2", "tokenizer_3"):
  700.                 tok = getattr(pipe, attr, None)
  701.                 if tok is not None and hasattr(tok, "model_max_length") and tok.model_max_length > 512:
  702.                     tok.model_max_length = 77

  703.             from faceswap.business.vram_manager import safe_to_device
  704.             pipe, offload = safe_to_device(pipe, offload, device)

  705.             gen_device = "cpu" if offload == "sequential" else str(device)
  706.             generator = torch.Generator(device=gen_device).manual_seed(params["seed"])

  707.             call_kwargs = dict(
  708.                 prompt=params["prompt"],
  709.                 negative_prompt=params["negative_prompt"],
  710.                 num_inference_steps=params["steps"],
  711.                 guidance_scale=params["cfg"],
  712.                 generator=generator,
  713.             )
  714.             if params.get("lora_path"):
  715.                 pipe_cls_name = type(pipe).__name__
  716.                 if "SD3" in pipe_cls_name or "StableDiffusion3" in pipe_cls_name:
  717.                     call_kwargs["joint_attention_kwargs"] = {"scale": params["lora_scale"]}
  718.                 else:
  719.                     call_kwargs["cross_attention_kwargs"] = {"scale": params["lora_scale"]}

  720.             if has_ref:
  721.                 ref_img = PILImage.open(params["ref_image_path"]).convert("RGB")
  722.                 res = params["resolution"]
  723.                 ref_img = ref_img.resize((res, res), PILImage.LANCZOS)
  724.                 call_kwargs["image"] = ref_img
  725.                 call_kwargs["strength"] = params["strength"]
  726.                 self._sculpt_sig.log.emit("基于参考图生成...")
  727.             else:
  728.                 call_kwargs["width"] = params["resolution"]
  729.                 call_kwargs["height"] = params["resolution"]
  730.                 self._sculpt_sig.log.emit("随机生成人脸...")

  731.             result = pipe(**call_kwargs)
  732.             out_img = result.images[0]
  733.             pipe.remove_all_hooks()
  734.             del pipe, result
  735.             _pipe_holder[0] = None
  736.             gc.collect()
  737.             return out_img

  738.         try:
  739.             out_img = run_with_oom_fallback(_steps, _execute, _cleanup)
  740.             self._sculpt_sig.done.emit(True, out_img)
  741.         except Exception as e:
  742.             import traceback
  743.             self._sculpt_sig.log.emit(f"[错误] {e}")
  744.             self._sculpt_sig.done.emit(False, traceback.format_exc())

  745.     def _on_stop_sculpt(self):
  746.         self._sculpt_stop_event.set()
  747.         self._sculpt_log_label.setText("停止中...")

  748.     def _on_sculpt_log(self, msg):
  749.         self._sculpt_log_label.setText(msg)

  750.     def _on_sculpt_done(self, success, result):
  751.         self._sculpt_running = False
  752.         self.set_busy(False)
  753.         self._sculpt_stop_btn.setEnabled(False)
  754.         self._sculpt_log_label.setVisible(False)
  755.         if success and result is not None:
  756.             img_array = np.array(result.convert("RGB"))
  757.             self._sculpt_base_image = img_array.copy()
  758.             self._sculpt_base_landmarks = self._detect_landmarks(img_array)
  759.             if self._sculpt_base_landmarks is not None:
  760.                 self._sculpt_param_sliders.setEnabled(True)
  761.                 QMessageBox.information(self, "生成完成", f"已检测到 {len(self._sculpt_base_landmarks)} 个landmark点,可拖动滑块实时捏脸")
  762.             else:
  763.                 self._sculpt_param_sliders.setEnabled(False)
  764.                 QMessageBox.warning(self, "Landmark检测失败", "生成完成但未检测到人脸landmark,捏脸滑块不可用。\n请确认生成的图片中包含清晰人脸。")
  765.             self._sculpt_current_image = img_array
  766.             self._display_image(img_array, self._sculpt_result_preview)

  767.             from PyQt6.QtGui import QPixmap, QImage
  768.             h, w = img_array.shape[:2]
  769.             qimg = QImage(img_array.tobytes(), w, h, w * 3, QImage.Format.Format_RGB888)
  770.             pm = QPixmap.fromImage(qimg)
  771.             lw = self._sculpt_ref_preview.width()
  772.             lh = self._sculpt_ref_preview.height()
  773.             pm = pm.scaled(lw, lh,
  774.                            Qt.AspectRatioMode.KeepAspectRatio,
  775.                            Qt.TransformationMode.SmoothTransformation)
  776.             self._sculpt_ref_preview.setPixmap(pm)
  777.             self._sculpt_ref_preview.setText("")
  778.         else:
  779.             QMessageBox.critical(self, "生成失败", str(result))

  780.     @staticmethod
  781.     def _detect_landmarks(img_array: np.ndarray) -> np.ndarray | None:
  782.         try:
  783.             from faceswap.core.insightface_adapter import InsightFaceAdapter
  784.             adapter = InsightFaceAdapter.get_instance()
  785.             detected = adapter.detect_faces(img_array, max_num=1)
  786.             if not detected:
  787.                 return None
  788.             lm = detected[0].landmarks_106
  789.             if lm is not None and lm.size > 0:
  790.                 return lm.astype(np.float32)
  791.             return None
  792.         except Exception as e:
  793.             import traceback
  794.             traceback.print_exc()
  795.             return None

  796.     def _display_image(self, img_array: np.ndarray, label: QLabel):
  797.         from PyQt6.QtGui import QPixmap, QImage
  798.         h, w = img_array.shape[:2]
  799.         qimg = QImage(img_array.tobytes(), w, h, 3 * w, QImage.Format.Format_RGB888)
  800.         pm = QPixmap.fromImage(qimg)
  801.         lw = max(label.width(), 64)
  802.         lh = max(label.height(), 64)
  803.         pm = pm.scaled(lw, lh,
  804.                        Qt.AspectRatioMode.KeepAspectRatio,
  805.                        Qt.TransformationMode.SmoothTransformation)
  806.         label.setPixmap(pm)
  807.         label.setText("")

  808.     def _do_translate(self, target_edit: QTextEdit, btn: QPushButton):
  809.         text = target_edit.toPlainText().strip()
  810.         if not text:
  811.             return
  812.         self.set_busy(True)
  813.         btn.setEnabled(False)
  814.         btn.setText("翻译中...")

  815.         class _Sig(QObject):
  816.             done = pyqtSignal(str)
  817.             error = pyqtSignal(str)
  818.         sig = _Sig()
  819.         sig.done.connect(lambda r: self._on_translate_done(target_edit, btn, r))
  820.         sig.error.connect(lambda m: self._on_translate_error(btn, m))

  821.         def _worker():
  822.             try:
  823.                 from faceswap.business.translator import translate_zh_to_en
  824.                 translated = translate_zh_to_en(text)
  825.                 sig.done.emit(translated or text)
  826.             except Exception as e:
  827.                 sig.error.emit(str(e))

  828.         threading.Thread(target=_worker, daemon=True).start()

  829.     def _on_translate_done(self, target_edit: QTextEdit, btn: QPushButton, result: str):
  830.         self.set_busy(False)
  831.         target_edit.setPlainText(result)
  832.         btn.setEnabled(True)
  833.         btn.setText("翻译")

  834.     def _on_translate_error(self, btn: QPushButton, msg: str):
  835.         self.set_busy(False)
  836.         btn.setEnabled(True)
  837.         btn.setText("翻译")
  838.         QMessageBox.warning(self, "翻译失败", msg)

  839.     def set_busy(self, busy: bool):
  840.         win = self.window()
  841.         if hasattr(win, 'set_busy'):
  842.             win.set_busy(busy)
  843.         self._sculpt_model_selector.set_enabled(not busy)
  844.         self._sculpt_param_sliders.setEnabled(not busy)
  845.         self._sculpt_start_btn.setEnabled(not busy)
  846.         self._sculpt_random_seed_btn.setEnabled(not busy)

  847.     def release(self):
  848.         """只在有资源时才执行释放,避免无谓的gc+cleanup。"""
  849.         if self._sculpt_base_image is None and not getattr(self, '_sculpt_running', False):
  850.             return
  851.         try:
  852.             if hasattr(self, "_sculpt_stop_event"):
  853.                 self._sculpt_stop_event.set()
  854.         except Exception:
  855.             pass
  856.         self._sculpt_base_image = None
  857.         self._sculpt_base_landmarks = None
复制代码
回复 支持 反对

使用道具 举报

44

主题

275

帖子

2228

积分

初级丹圣

Rank: 8Rank: 8

积分
2228
 楼主| 发表于 昨天 23:26 | 显示全部楼层
caiji132156 发表于 2026-9-29 23:19
捏脸是指调五官大小和形态吧,你要传统图形学的还是深度学习的。捏脸的目的是什么
传统图形学就基于lmk液 ...

很简单,就是不依靠生成,依靠landmarks去造人脸,我这里支持sd,但是我想依靠手动修改重构下sd生成的人脸,也就是具备两个功能,一个是依靠模型生成,一个是靠手工编辑人脸,你看我代码里面,反复折腾这个人脸黄金比例,反复计算。就是跳不出较好的比例
回复 支持 反对

使用道具 举报

44

主题

275

帖子

2228

积分

初级丹圣

Rank: 8Rank: 8

积分
2228
 楼主| 发表于 昨天 23:28 | 显示全部楼层
caiji132156 发表于 2026-9-29 23:19
捏脸是指调五官大小和形态吧,你要传统图形学的还是深度学习的。捏脸的目的是什么
传统图形学就基于lmk液 ...

我也知道液化僵硬,但是我是想通过设定黄金比例,去塑造一个完美的
回复 支持 反对

使用道具 举报

19

主题

188

帖子

1401

积分

初级丹圣

Rank: 8Rank: 8

积分
1401
发表于 昨天 23:32 | 显示全部楼层
day270010678 发表于 2026-9-29 23:26
很简单,就是不依靠生成,依靠landmarks去造人脸,我这里支持sd,但是我想依靠手动修改重构下sd生成的人脸 ...

五官是单纯拉伸形变导致看起来奇怪吧,比如鼻子,看起来和黄金比例没有什么关系
B站菜级玩家
回复 支持 反对

使用道具 举报

19

主题

188

帖子

1401

积分

初级丹圣

Rank: 8Rank: 8

积分
1401
发表于 昨天 23:34 | 显示全部楼层
day270010678 发表于 2026-9-29 23:28
我也知道液化僵硬,但是我是想通过设定黄金比例,去塑造一个完美的

思路应该是没什么问题的,就是你得去调一下重绘的逻辑,比如说你这个嘴巴,看起来就是单纯放大了
B站菜级玩家
回复 支持 反对

使用道具 举报

19

主题

188

帖子

1401

积分

初级丹圣

Rank: 8Rank: 8

积分
1401
发表于 昨天 23:38 | 显示全部楼层
day270010678 发表于 2026-9-29 23:28
我也知道液化僵硬,但是我是想通过设定黄金比例,去塑造一个完美的

嘴巴好像是等比缩放
B站菜级玩家
回复 支持 反对

使用道具 举报

19

主题

188

帖子

1401

积分

初级丹圣

Rank: 8Rank: 8

积分
1401
发表于 昨天 23:41 | 显示全部楼层
day270010678 发表于 2026-9-29 23:28
我也知道液化僵硬,但是我是想通过设定黄金比例,去塑造一个完美的

我建议捏脸后处理也有diffusion参与比较好
B站菜级玩家
回复 支持 反对

使用道具 举报

44

主题

275

帖子

2228

积分

初级丹圣

Rank: 8Rank: 8

积分
2228
 楼主| 发表于 昨天 23:42 | 显示全部楼层
caiji132156 发表于 2026-9-29 23:38
嘴巴好像是等比缩放

脸型都没搞定,我就没折腾五官,嘴巴是五官,我暂时不折腾这个,我先把脸型,比方说方脸,瓜子脸,娃娃脸,这种脸型相关的搞定了,再说折腾五官,关键是你发现了没有,脸型都有问题,也就是脸部骨骼有问题,
回复 支持 反对

使用道具 举报

44

主题

275

帖子

2228

积分

初级丹圣

Rank: 8Rank: 8

积分
2228
 楼主| 发表于 昨天 23:50 | 显示全部楼层
caiji132156 发表于 2026-9-29 23:41
我建议捏脸后处理也有diffusion参与比较好

用df他们都是为了生成,这里的生成没问题,我就是想折腾下手动调节,主要是生成的对于小显卡不支持,虽然我都优化了支持cpu_offload,但是毕竟对小显卡不友好,小显卡可以尝鲜,毕竟不适合靠大模型啊。得了我明天继续折腾,休息了哈。好梦
回复 支持 反对

使用道具 举报

19

主题

188

帖子

1401

积分

初级丹圣

Rank: 8Rank: 8

积分
1401
发表于 昨天 23:51 | 显示全部楼层
day270010678 发表于 2026-9-29 23:42
脸型都没搞定,我就没折腾五官,嘴巴是五官,我暂时不折腾这个,我先把脸型,比方说方脸,瓜子脸,娃娃脸 ...

下巴和下颚角分开算,颧骨的判定逻辑太夸张了,就是导致看起来人脸方方的了,我觉得这个你用一个真黄金比例的人脸去调一下这些硬编码的比例会更好
B站菜级玩家
回复 支持 反对

使用道具 举报

QQ|Archiver|手机版|deepfacelab中文网 |网站地图

GMT+8, 2026-9-30 17:30 , Processed in 0.095002 second(s), 26 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表