Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6029ffd2cc | ||
|
|
6944f5d2c0 | ||
|
|
d901609a1d | ||
|
|
54c7c27714 | ||
|
|
9d835780d7 | ||
|
|
289001af81 | ||
|
|
afc62294ac | ||
|
|
b7e30eea5e | ||
|
|
3095e627cd | ||
|
|
e573562a11 | ||
|
|
42cfb3317f | ||
|
|
5944d9a3e1 | ||
|
|
fc69c2232d | ||
|
|
4a7587a3fe | ||
|
|
7b8f84b9c0 |
@@ -7,14 +7,18 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'Gourieff' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github secrets and reference it here.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
|
||||
|
||||

|
||||

|
||||
|
||||
<!--<sup>
|
||||
<font color=brightred>
|
||||
@@ -51,27 +51,33 @@
|
||||
|
||||
## What's new in the latest update
|
||||
|
||||
### 0.5.2 <sub><sup>BETA1</sup></sub>
|
||||
### 0.6.0
|
||||
|
||||
- New Node `ReActorSetWeight` - you can now set the strength of face swap for `source_image` or `face_model` from 0% to 100% (in 12.5% step)
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-02.jpg?raw=true" alt="0.6.0-whatsnew-02" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-alpha1-01.gif?raw=true" alt="0.6.0-whatsnew-03" width="540px"/>
|
||||
</center>
|
||||
|
||||
<details>
|
||||
<summary><a>Previous versions</a></summary>
|
||||
|
||||
### 0.5.2
|
||||
|
||||
- ReSwapper models support. Although Inswapper still has the best similarity, but ReSwapper is evolving - thanks @somanchiu https://github.com/somanchiu/ReSwapper for the ReSwapper models and the ReSwapper project! This is a good step for the Community in the Inswapper's alternative creation!
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-03.jpg?raw=true" alt="0.5.2-whatsnew-03" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-04.jpg?raw=true" alt="0.5.2-whatsnew-04" width="100%"/>
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-03.jpg?raw=true" alt="0.5.2-whatsnew-03" width="75%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-04.jpg?raw=true" alt="0.5.2-whatsnew-04" width="75%"/>
|
||||
</center>
|
||||
|
||||
You can download ReSwapper models here:
|
||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
Just put them into the "models/reswapper" directory.
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA3</sup></sub>
|
||||
|
||||
- NSFW-detector to not violate [GitHub rules](https://docs.github.com/en/site-policy/acceptable-use-policies/github-misinformation-and-disinformation#synthetic--manipulated-media-tools)
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA2</sup></sub>
|
||||
|
||||
- Minor fixes
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA1</sup></sub>
|
||||
|
||||
- New node "Unload ReActor Models" - is useful for complex WFs when you need to free some VRAM utilized by ReActor
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-01.jpg?raw=true" alt="0.5.2-whatsnew-01" width="100%"/>
|
||||
@@ -79,10 +85,12 @@ Just put them into the "models/reswapper" directory.
|
||||
- Support of ORT CoreML and ROCM EPs, just install onnxruntime version you need
|
||||
- Install script improvements to install latest versions of ORT-GPU
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-02.jpg?raw=true" alt="0.5.2-whatsnew-02" width="50%"/>
|
||||
</center>
|
||||
|
||||
- Fixes and improvements
|
||||
|
||||
<details>
|
||||
<summary><a>Previous versions</a></summary>
|
||||
|
||||
### 0.5.1
|
||||
|
||||
|
||||
+22
-15
@@ -2,7 +2,7 @@
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_RU.png?raw=true" alt="logo" width="180px"/>
|
||||
|
||||

|
||||

|
||||
|
||||
<!--<sup>
|
||||
<font color=brightred>
|
||||
@@ -51,27 +51,33 @@
|
||||
|
||||
## Что нового в последнем обновлении
|
||||
|
||||
### 0.5.2 <sub><sup>BETA1</sup></sub>
|
||||
### 0.6.0
|
||||
|
||||
- Новый нод `ReActorSetWeight` - теперь можно установить силу замены лица для `source_image` или `face_model` от 0% до 100% (с шагом 12.5%)
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-02.jpg?raw=true" alt="0.6.0-whatsnew-02" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-alpha1-01.gif?raw=true" alt="0.6.0-whatsnew-03" width="540px"/>
|
||||
</center>
|
||||
|
||||
<details>
|
||||
<summary><a>Предыдущие версии</a></summary>
|
||||
|
||||
### 0.5.2
|
||||
|
||||
- Поддержка моделей ReSwapper. Несмотря на то, что Inswapper по-прежнему даёт лучшее сходство, но ReSwapper развивается - спасибо @somanchiu https://github.com/somanchiu/ReSwapper за эти модели и проект ReSwapper! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-03.jpg?raw=true" alt="0.5.2-whatsnew-03" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-04.jpg?raw=true" alt="0.5.2-whatsnew-04" width="100%"/>
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-03.jpg?raw=true" alt="0.5.2-whatsnew-03" width="75%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-04.jpg?raw=true" alt="0.5.2-whatsnew-04" width="75%"/>
|
||||
</center>
|
||||
|
||||
Скачать модели ReSwapper можно отсюда:
|
||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
Сохраните их в директорию "models/reswapper".
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA3</sup></sub>
|
||||
|
||||
- NSFW-детектор, чтобы не нарушать [правила GitHub](https://docs.github.com/en/site-policy/acceptable-use-policies/github-misinformation-and-disinformation#synthetic--manipulated-media-tools)
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA2</sup></sub>
|
||||
|
||||
- Небольшие исправления
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA1</sup></sub>
|
||||
|
||||
- Новый нод "Unload ReActor Models" - полезен для сложных воркфлоу, когда вам нужно освободить ОЗУ, занятую РеАктором
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-01.jpg?raw=true" alt="0.5.2-whatsnew-01" width="100%"/>
|
||||
@@ -79,10 +85,11 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
- Поддержка ORT CoreML and ROCM EPs, достаточно установить ту версию onnxruntime, которая соответствует вашему GPU
|
||||
- Некоторые улучшения скрипта установки для поддержки последней версии ORT-GPU
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-02.jpg?raw=true" alt="0.5.2-whatsnew-02" width="50%"/>
|
||||
</center>
|
||||
|
||||
<details>
|
||||
<summary><a>Предыдущие версии</a></summary>
|
||||
- Исправления и улучшения
|
||||
|
||||
### 0.5.1
|
||||
|
||||
|
||||
@@ -72,19 +72,6 @@ if not os.path.exists(REACTOR_MODELS_PATH):
|
||||
if not os.path.exists(FACE_MODELS_PATH):
|
||||
os.makedirs(FACE_MODELS_PATH)
|
||||
|
||||
if not os.path.exists(NSFWDET_MODEL_PATH):
|
||||
os.makedirs(NSFWDET_MODEL_PATH)
|
||||
nd_urls = [
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/config.json",
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/confusion_matrix.png",
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/model.safetensors",
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/preprocessor_config.json",
|
||||
]
|
||||
for model_url in nd_urls:
|
||||
model_name = os.path.basename(model_url)
|
||||
model_path = os.path.join(NSFWDET_MODEL_PATH, model_name)
|
||||
download(model_url, model_path, model_name)
|
||||
|
||||
dir_facerestore_models = os.path.join(models_dir, "facerestore_models")
|
||||
os.makedirs(dir_facerestore_models, exist_ok=True)
|
||||
folder_paths.folder_names_and_paths["facerestore_models"] = ([dir_facerestore_models], folder_paths.supported_pt_extensions)
|
||||
@@ -196,7 +183,7 @@ class reactor:
|
||||
global FACE_SIZE, FACE_HELPER
|
||||
|
||||
self.face_helper = FACE_HELPER
|
||||
|
||||
|
||||
faceSize = 512
|
||||
if "1024" in face_restore_model.lower():
|
||||
faceSize = 1024
|
||||
@@ -233,7 +220,7 @@ class reactor:
|
||||
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
|
||||
facerestore_model = model_loading.load_state_dict(sd).eval()
|
||||
facerestore_model.to(device)
|
||||
|
||||
|
||||
if faceSize != FACE_SIZE or self.face_helper is None:
|
||||
self.face_helper = FaceRestoreHelper(1, face_size=faceSize, crop_ratio=(1, 1), det_model=facedetection, save_ext='png', use_parse=True, device=device)
|
||||
FACE_SIZE = faceSize
|
||||
@@ -330,7 +317,7 @@ class reactor:
|
||||
result = restored_img_tensor
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def execute(self, enabled, input_image, swap_model, detect_gender_source, detect_gender_input, source_faces_index, input_faces_index, console_log_level, face_restore_model,face_restore_visibility, codeformer_weight, facedetection, source_image=None, face_model=None, faces_order=None, face_boost=None):
|
||||
|
||||
if face_boost is not None:
|
||||
@@ -356,7 +343,7 @@ class reactor:
|
||||
|
||||
if face_model == "none":
|
||||
face_model = None
|
||||
|
||||
|
||||
script = FaceSwapScript()
|
||||
pil_images = batch_tensor_to_pil(input_image)
|
||||
|
||||
@@ -413,7 +400,7 @@ class reactor:
|
||||
|
||||
if self.restore or not self.face_boost_enabled:
|
||||
result = reactor.restore_face(self,result,face_restore_model,face_restore_visibility,codeformer_weight,facedetection)
|
||||
|
||||
|
||||
else:
|
||||
image_black = Image.new("RGB", (512, 512))
|
||||
result = batched_pil_to_tensor([image_black])
|
||||
@@ -428,7 +415,7 @@ class ReActorPlusOpt:
|
||||
return {
|
||||
"required": {
|
||||
"enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}),
|
||||
"input_image": ("IMAGE",),
|
||||
"input_image": ("IMAGE",),
|
||||
"swap_model": (list(model_names().keys()),),
|
||||
"facedetection": (["retinaface_resnet50", "retinaface_mobile0.25", "YOLOv5l", "YOLOv5n"],),
|
||||
"face_restore_model": (get_model_names(get_restorers),),
|
||||
@@ -462,7 +449,7 @@ class ReActorPlusOpt:
|
||||
self.interpolation = "Bicubic"
|
||||
self.boost_model_visibility = 1
|
||||
self.boost_cf_weight = 0.5
|
||||
|
||||
|
||||
def execute(self, enabled, input_image, swap_model, facedetection, face_restore_model, face_restore_visibility, codeformer_weight, source_image=None, face_model=None, options=None, face_boost=None):
|
||||
|
||||
if options is not None:
|
||||
@@ -472,13 +459,13 @@ class ReActorPlusOpt:
|
||||
self.detect_gender_source = options["detect_gender_source"]
|
||||
self.input_faces_index = options["input_faces_index"]
|
||||
self.source_faces_index = options["source_faces_index"]
|
||||
|
||||
|
||||
if face_boost is not None:
|
||||
self.face_boost_enabled = face_boost["enabled"]
|
||||
self.restore = face_boost["restore_with_main_after"]
|
||||
else:
|
||||
self.face_boost_enabled = False
|
||||
|
||||
|
||||
result = reactor.execute(
|
||||
self,enabled,input_image,swap_model,self.detect_gender_source,self.detect_gender_input,self.source_faces_index,self.input_faces_index,self.console_log_level,face_restore_model,face_restore_visibility,codeformer_weight,facedetection,source_image,face_model,self.faces_order, face_boost=face_boost
|
||||
)
|
||||
@@ -494,7 +481,7 @@ class LoadFaceModel:
|
||||
"face_model": (get_model_names(get_facemodels),),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RETURN_TYPES = ("FACE_MODEL",)
|
||||
FUNCTION = "load_model"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
@@ -510,10 +497,110 @@ class LoadFaceModel:
|
||||
return (out, )
|
||||
|
||||
|
||||
class ReActorWeight:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_image": ("IMAGE",),
|
||||
"faceswap_weight": (["0%", "12.5%", "25%", "37.5%", "50%", "62.5%", "75%", "87.5%", "100%"], {"default": "50%"}),
|
||||
},
|
||||
"optional": {
|
||||
"source_image": ("IMAGE",),
|
||||
"face_model": ("FACE_MODEL",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE","FACE_MODEL")
|
||||
RETURN_NAMES = ("INPUT_IMAGE","FACE_MODEL")
|
||||
FUNCTION = "set_weight"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
CATEGORY = "🌌 ReActor"
|
||||
|
||||
def set_weight(self, input_image, faceswap_weight, face_model=None, source_image=None):
|
||||
|
||||
if input_image is None:
|
||||
logger.error("Please provide `input_image`")
|
||||
return (input_image,None)
|
||||
|
||||
if source_image is None and face_model is None:
|
||||
logger.error("Please provide `source_image` or `face_model`")
|
||||
return (input_image,None)
|
||||
|
||||
weight = float(faceswap_weight.split("%")[0])
|
||||
|
||||
images = []
|
||||
faces = [] if face_model is None else [face_model]
|
||||
embeddings = [] if face_model is None else [face_model.embedding]
|
||||
|
||||
if weight == 0:
|
||||
images = [input_image]
|
||||
faces = []
|
||||
embeddings = []
|
||||
elif weight == 100:
|
||||
if face_model is None:
|
||||
images = [source_image]
|
||||
else:
|
||||
if weight > 50:
|
||||
images = [input_image]
|
||||
count = round(100/(100-weight))
|
||||
else:
|
||||
if face_model is None:
|
||||
images = [source_image]
|
||||
count = round(100/(weight))
|
||||
for i in range(count-1):
|
||||
if weight > 50:
|
||||
if face_model is None:
|
||||
images.append(source_image)
|
||||
else:
|
||||
faces.append(face_model)
|
||||
embeddings.append(face_model.embedding)
|
||||
else:
|
||||
images.append(input_image)
|
||||
|
||||
images_list: List[Image.Image] = []
|
||||
|
||||
apply_patch(1)
|
||||
|
||||
if len(images) > 0:
|
||||
|
||||
for image in images:
|
||||
img = tensor_to_pil(image)
|
||||
images_list.append(img)
|
||||
|
||||
for image in images_list:
|
||||
face = BuildFaceModel.build_face_model(self,image)
|
||||
if isinstance(face, str):
|
||||
continue
|
||||
faces.append(face)
|
||||
embeddings.append(face.embedding)
|
||||
|
||||
if len(faces) > 0:
|
||||
blended_embedding = np.mean(embeddings, axis=0)
|
||||
blended_face = Face(
|
||||
bbox=faces[0].bbox,
|
||||
kps=faces[0].kps,
|
||||
det_score=faces[0].det_score,
|
||||
landmark_3d_68=faces[0].landmark_3d_68,
|
||||
pose=faces[0].pose,
|
||||
landmark_2d_106=faces[0].landmark_2d_106,
|
||||
embedding=blended_embedding,
|
||||
gender=faces[0].gender,
|
||||
age=faces[0].age
|
||||
)
|
||||
if blended_face is None:
|
||||
no_face_msg = "Something went wrong, please try another set of images"
|
||||
logger.error(no_face_msg)
|
||||
|
||||
return (input_image,blended_face)
|
||||
|
||||
|
||||
class BuildFaceModel:
|
||||
def __init__(self):
|
||||
self.output_dir = FACE_MODELS_PATH
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -551,14 +638,14 @@ class BuildFaceModel:
|
||||
face_model = analyze_faces(image, det_size_half)
|
||||
if face_model is not None and len(face_model) > 0:
|
||||
print("...........................................................", end=" ")
|
||||
|
||||
|
||||
if face_model is not None and len(face_model) > 0:
|
||||
return face_model[0]
|
||||
else:
|
||||
no_face_msg = "No face found, please try another image"
|
||||
# logger.error(no_face_msg)
|
||||
return no_face_msg
|
||||
|
||||
|
||||
def blend_faces(self, save_mode, send_only, face_model_name, compute_method, images=None, face_models=None):
|
||||
global BLENDED_FACE_MODEL
|
||||
blended_face: Face = BLENDED_FACE_MODEL
|
||||
@@ -589,7 +676,7 @@ class BuildFaceModel:
|
||||
print(f"{int(((i+1)/n)*100)}%")
|
||||
faces.append(face)
|
||||
embeddings.append(face.embedding)
|
||||
|
||||
|
||||
elif face_models is not None:
|
||||
|
||||
n = len(face_models)
|
||||
@@ -695,7 +782,7 @@ class RestoreFace:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"image": ("IMAGE",),
|
||||
"facedetection": (["retinaface_resnet50", "retinaface_mobile0.25", "YOLOv5l", "YOLOv5n"],),
|
||||
"model": (get_model_names(get_restorers),),
|
||||
"visibility": ("FLOAT", {"default": 1, "min": 0.0, "max": 1, "step": 0.05}),
|
||||
@@ -734,7 +821,7 @@ class MaskHelper:
|
||||
# self.force_resize_width = 0
|
||||
# self.force_resize_height = 0
|
||||
# self.resize_behavior = "source_size"
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
bboxs = ["bbox/"+x for x in folder_paths.get_filename_list("ultralytics_bbox")]
|
||||
@@ -764,7 +851,7 @@ class MaskHelper:
|
||||
"mask_optional": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RETURN_TYPES = ("IMAGE","MASK","IMAGE","IMAGE")
|
||||
RETURN_NAMES = ("IMAGE","MASK","MASK_PREVIEW","SWAPPED_FACE")
|
||||
FUNCTION = "execute"
|
||||
@@ -792,7 +879,7 @@ class MaskHelper:
|
||||
if len(self.labels) > 0:
|
||||
segs, _ = masking_segs.filter(segs, self.labels)
|
||||
# segs, _ = masking_segs.filter(segs, "all")
|
||||
|
||||
|
||||
sam_modelname = folder_paths.get_full_path("sams", sam_model_name)
|
||||
|
||||
if 'vit_h' in sam_model_name:
|
||||
@@ -813,12 +900,12 @@ class MaskHelper:
|
||||
sam.is_auto_mode = self.device_mode == "AUTO"
|
||||
|
||||
combined_mask, _ = core.make_sam_mask_segmented(sam, segs, images, self.detection_hint, sam_dilation, sam_threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative)
|
||||
|
||||
|
||||
else:
|
||||
combined_mask = mask_optional
|
||||
|
||||
# *** MASK TO IMAGE ***:
|
||||
|
||||
|
||||
mask_image = combined_mask.reshape((-1, 1, combined_mask.shape[-2], combined_mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
||||
|
||||
# *** MASK MORPH ***:
|
||||
@@ -835,12 +922,12 @@ class MaskHelper:
|
||||
elif morphology_operation == "close":
|
||||
mask_image = self.dilate(mask_image, morphology_distance)
|
||||
mask_image = self.erode(mask_image, morphology_distance)
|
||||
|
||||
|
||||
# *** MASK BLUR ***:
|
||||
|
||||
|
||||
if len(mask_image.size()) == 3:
|
||||
mask_image = mask_image.unsqueeze(3)
|
||||
|
||||
|
||||
mask_image = mask_image.permute(0, 3, 1, 2)
|
||||
kernel_size = blur_radius * 2 + 1
|
||||
sigma = sigma_factor * (0.6 * blur_radius - 0.3)
|
||||
@@ -849,7 +936,7 @@ class MaskHelper:
|
||||
mask_image_final = mask_image_final[:, :, :, 0]
|
||||
|
||||
# *** CUT BY MASK ***:
|
||||
|
||||
|
||||
if len(swapped_image.shape) < 4:
|
||||
C = 1
|
||||
else:
|
||||
@@ -906,7 +993,7 @@ class MaskHelper:
|
||||
single = (swapped_image[i, ymin:ymax+1, xmin:xmax+1,:]).unsqueeze(0)
|
||||
resized = torch.nn.functional.interpolate(single.permute(0, 3, 1, 2), size=(use_height, use_width), mode='bicubic').permute(0, 2, 3, 1)
|
||||
cutted_image[i] = resized[0]
|
||||
|
||||
|
||||
# Preserve our type unless we were previously RGB and added non-opaque alpha due to the mask size
|
||||
if C == 1:
|
||||
cutted_image = core.tensor2mask(cutted_image)
|
||||
@@ -959,7 +1046,7 @@ class MaskHelper:
|
||||
|
||||
result = image_base.detach().clone()
|
||||
face_segment = mask_image_final
|
||||
|
||||
|
||||
for i in range(0, MB):
|
||||
if is_empty[i]:
|
||||
continue
|
||||
@@ -1038,7 +1125,7 @@ class MaskHelper:
|
||||
face_segment[...,3] = mask[i]
|
||||
|
||||
result = rgba2rgb_tensor(result)
|
||||
|
||||
|
||||
return (result,combined_mask,mask_image_final,face_segment,)
|
||||
|
||||
def gaussian_blur(self, image, kernel_size, sigma):
|
||||
@@ -1067,7 +1154,7 @@ class MaskHelper:
|
||||
output_tensor = output_reshaped.reshape(batch_size, num_channels, height, width)
|
||||
|
||||
return output_tensor
|
||||
|
||||
|
||||
def erode(self, image, distance):
|
||||
return 1. - self.dilate(1. - image, distance)
|
||||
|
||||
@@ -1084,7 +1171,7 @@ class ImageDublicator:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"image": ("IMAGE",),
|
||||
"count": ("INT", {"default": 1, "min": 0}),
|
||||
},
|
||||
}
|
||||
@@ -1096,7 +1183,7 @@ class ImageDublicator:
|
||||
CATEGORY = "🌌 ReActor"
|
||||
|
||||
def execute(self, image, count):
|
||||
images = [image for i in range(count)]
|
||||
images = [image for i in range(count)]
|
||||
return (images,)
|
||||
|
||||
|
||||
@@ -1114,7 +1201,7 @@ class ImageRGBA2RGB:
|
||||
CATEGORY = "🌌 ReActor"
|
||||
|
||||
def execute(self, image):
|
||||
out = rgba2rgb_tensor(image)
|
||||
out = rgba2rgb_tensor(image)
|
||||
return (out,)
|
||||
|
||||
|
||||
@@ -1123,7 +1210,7 @@ class MakeFaceModelBatch:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"face_model1": ("FACE_MODEL",),
|
||||
"face_model1": ("FACE_MODEL",),
|
||||
},
|
||||
"optional": {
|
||||
"face_model2": ("FACE_MODEL",),
|
||||
@@ -1217,7 +1304,7 @@ class ReActorFaceBoost:
|
||||
"restore_with_main_after": restore_with_main_after,
|
||||
}
|
||||
return (face_boost, )
|
||||
|
||||
|
||||
class ReActorUnload:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -1243,6 +1330,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ReActorOptions": ReActorOptions,
|
||||
"ReActorFaceBoost": ReActorFaceBoost,
|
||||
"ReActorMaskHelper": MaskHelper,
|
||||
"ReActorSetWeight": ReActorWeight,
|
||||
# --- Operations with Face Models ---
|
||||
"ReActorSaveFaceModel": SaveFaceModel,
|
||||
"ReActorLoadFaceModel": LoadFaceModel,
|
||||
@@ -1262,6 +1350,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ReActorOptions": "ReActor 🌌 Options",
|
||||
"ReActorFaceBoost": "ReActor 🌌 Face Booster",
|
||||
"ReActorMaskHelper": "ReActor 🌌 Masking Helper",
|
||||
"ReActorSetWeight": "ReActor 🌌 Set Face Swap Weight",
|
||||
# --- Operations with Face Models ---
|
||||
"ReActorSaveFaceModel": "Save Face Model 🌌 ReActor",
|
||||
"ReActorLoadFaceModel": "Load Face Model 🌌 ReActor",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-reactor"
|
||||
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension Node for ComfyUI, based on ReActor SD-WebUI Face Swap Extension"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["insightface==0.7.3", "onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy==1.26.3", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
||||
|
||||
|
||||
+17
-1
@@ -1,14 +1,30 @@
|
||||
from transformers import pipeline
|
||||
from PIL import Image
|
||||
import logging
|
||||
import os
|
||||
from reactor_utils import download
|
||||
from scripts.reactor_logger import logger
|
||||
|
||||
def ensure_nsfw_model(nsfwdet_model_path):
|
||||
"""Download NSFW detection model if it doesn't exist"""
|
||||
if not os.path.exists(nsfwdet_model_path):
|
||||
os.makedirs(nsfwdet_model_path)
|
||||
nd_urls = [
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/config.json",
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/model.safetensors",
|
||||
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/preprocessor_config.json",
|
||||
]
|
||||
for model_url in nd_urls:
|
||||
model_name = os.path.basename(model_url)
|
||||
model_path = os.path.join(nsfwdet_model_path, model_name)
|
||||
download(model_url, model_path, model_name)
|
||||
|
||||
SCORE = 0.96
|
||||
|
||||
logging.getLogger("transformers").setLevel(logging.ERROR)
|
||||
|
||||
def nsfw_image(img_path: str, model_path: str):
|
||||
|
||||
ensure_nsfw_model(model_path)
|
||||
with Image.open(img_path) as img:
|
||||
predict = pipeline("image-classification", model=model_path)
|
||||
result = predict(img)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
app_title = "ReActor Node for ComfyUI"
|
||||
version_flag = "v0.5.2"
|
||||
version_flag = "v0.6.0"
|
||||
|
||||
COLORS = {
|
||||
"CYAN": "\033[0;36m", # CYAN
|
||||
|
||||
Reference in New Issue
Block a user