Files
2026-06-02 21:22:02 +08:00

160 lines
6.0 KiB
Python

"""ComfyUI Node for D-DFFNet Defocus Blur Detection.
This node performs defocus blur detection using the D-DFFNet model.
The model should be placed in <ComfyUI_root>/models/dffnet/ directory.
"""
import os
import numpy as np
import torch
import torch.nn.functional as F
# ComfyUI imports
import folder_paths
import comfy.model_management as model_management
from .model.PDNet_resnest import PDnet_ResNest_4F_SL_LA_D
# Register our custom model folder
folder_paths.folder_names_and_paths["dffnet"] = (
[os.path.join(folder_paths.models_dir, "dffnet")],
[".pth", ".pt", ".pth.tar"],
)
class DDFFNetNode:
"""ComfyUI node for D-DFFNet defocus blur detection."""
CATEGORY = "D-DFFNet"
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "detect"
@classmethod
def INPUT_TYPES(cls):
model_names = cls._get_model_list()
return {
"required": {
"image": ("IMAGE",),
"model_name": (model_names,),
},
"optional": {
"threshold": ("FLOAT", {
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "Threshold for binarization (0 = output raw probability)"
}),
},
}
@classmethod
def _get_model_list(cls):
"""Get list of available model files from ComfyUI/models/dffnet/"""
paths, _ = folder_paths.folder_names_and_paths.get("dffnet", ([], []))
models = []
for folder in paths:
if os.path.isdir(folder):
for f in os.listdir(folder):
if f.endswith((".pth", ".pt", ".pth.tar")):
models.append(f)
return sorted(models) if models else ["[No models found]"]
def detect(self, image, model_name, threshold=0.5):
"""
Run D-DFFNet inference on the input image.
Args:
image: ComfyUI IMAGE tensor, shape (B, H, W, 3), float32, range [0, 1]
model_name: filename of the model weight to load
threshold: if > 0, binarize output at this threshold; if 0, output raw sigmoid probabilities
Returns:
mask: ComfyUI MASK tensor, shape (B, H, W), float32, range [0, 1]
"""
device = self._get_device()
# Load model
model_path = self._find_model_path(model_name)
if model_path is None:
raise FileNotFoundError(f"Model '{model_name}' not found in ComfyUI/models/dffnet/")
model = self._load_model(model_path, device)
# Prepare input: (B, H, W, 3) -> (B, 3, H, W), normalize to [0,1] already
B, H, W, C = image.shape
input_tensor = image.permute(0, 3, 1, 2).to(device) # BHWC -> BCHW
# Preprocess: resize to 320x320 + ImageNet normalization
input_320 = F.interpolate(input_tensor, size=(320, 320), mode="bilinear", align_corners=True)
mean = torch.tensor([0.485, 0.456, 0.406], device=device).view(1, 3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225], device=device).view(1, 3, 1, 1)
input_norm = (input_320 - mean) / std
# Inference
with torch.no_grad():
pred, _, _, _ = model(input_norm)
pred = torch.sigmoid(pred) # (B, 1, 320, 320)
# Post-process: resize back to original image size
pred = F.interpolate(pred, size=(H, W), mode="bilinear", align_corners=True) # (B, 1, H, W)
# Apply threshold if specified
if threshold > 0:
pred = (pred > threshold).float()
# Convert to MASK format: (B, 1, H, W) -> (B, H, W)
mask = pred.squeeze(1) # (B, H, W)
# Move to CPU and ensure correct dtype
mask = mask.cpu().float()
return (mask,)
def _get_device(self):
return model_management.get_torch_device()
def _find_model_path(self, model_name):
"""Find the full path to a model file in ComfyUI/models/dffnet/"""
if model_name.startswith("[") and model_name.endswith("]"):
return None
paths, _ = folder_paths.folder_names_and_paths.get("dffnet", ([], []))
for folder in paths:
candidate = os.path.join(folder, model_name)
if os.path.isfile(candidate):
return candidate
return None
def _load_model(self, model_path, device):
"""Load D-DFFNet model from checkpoint."""
model = PDnet_ResNest_4F_SL_LA_D()
state_dict = torch.load(model_path, map_location=device)
# Handle checkpoint formats: remove 'module.' prefix if present (DataParallel)
new_state_dict = {}
for k, v in state_dict.items():
if k.startswith("module."):
new_state_dict[k[7:]] = v # strip 'module.' prefix
else:
new_state_dict[k] = v
# Load with strict=False: side outputs (side5-side8) are only used during training
# and won't be present if loading a DFFNet checkpoint instead of D-DFFNet.
# Missing depth side outputs (side5-side8) are normal when loading DFFNet weights;
# they don't affect inference quality since only the main output path is used.
missing_keys, unexpected_keys = model.load_state_dict(new_state_dict, strict=False)
if missing_keys:
dffnet_keys = [k for k in missing_keys if 'side5' in k or 'side6' in k or 'side7' in k or 'side8' in k]
other_keys = [k for k in missing_keys if k not in dffnet_keys]
if dffnet_keys:
print(f"[D-DFFNet] Missing depth side outputs (expected if loading DFFNet .pth): {dffnet_keys}")
if other_keys:
print(f"[D-DFFNet] Missing keys: {other_keys}")
if unexpected_keys:
print(f"[D-DFFNet] Unexpected keys: {unexpected_keys}")
model.to(device)
model.eval()
return model