diff --git a/example_workflows/Projection_MultiView_with_Hunyuan3D2.0.json b/example_workflows/Projection_MultiView_with_Hunyuan3D2.0.json index d29dc7c..755af65 100644 --- a/example_workflows/Projection_MultiView_with_Hunyuan3D2.0.json +++ b/example_workflows/Projection_MultiView_with_Hunyuan3D2.0.json @@ -789,54 +789,6 @@ 1024 ] }, - { - "id": 250, - "type": "Hy3DDiffusersSchedulerConfig", - "pos": [ - -314.72770034835025, - 2141.9033917044144 - ], - "size": [ - 390.5999755859375, - 82 - ], - "flags": { - "collapsed": true - }, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "pipeline", - "type": "HY3DDIFFUSERSPIPE", - "link": 450 - } - ], - "outputs": [ - { - "name": "diffusers_scheduler", - "type": "NOISESCHEDULER", - "slot_index": 0, - "links": [ - 455 - ] - } - ], - "properties": { - "aux_id": "visualbruno/ComfyUI-Hunyuan3DWrapper", - "ver": "6090a9051f109d2774d309cf6a8a22d4a11a1b15", - "Node name for S&R": "Hy3DDiffusersSchedulerConfig", - "widget_ue_connectable": { - "scheduler": true, - "sigmas": true - }, - "cnr_id": "comfyui-hunyan3dwrapper" - }, - "widgets_values": [ - "Euler", - "default" - ] - }, { "id": 248, "type": "Hy3DRenderMultiView", @@ -1857,8 +1809,8 @@ "cnr_id": "comfy-core", "ver": "0.16.4", "Node name for S&R": "Preview3D", - "Last Time Model File": "C:/Git/ComfyUI/output/3D/Pistol_MV/Pistol_MV_Textured_MV_00001_.glb", - "Resource Folder": "Git/ComfyUI/output/3D/Pistol_MV", + "Last Time Model File": "C:/Git/ComfyUI/output/3D/AnimeGirl/AnimeGirl_Textured_MV_00002_.glb", + "Resource Folder": "Git/ComfyUI/output/3D/AnimeGirl", "Scene Config": { "showGrid": true, "backgroundColor": "#282828", @@ -1870,13 +1822,13 @@ "fov": 35, "state": { "position": { - "x": -9.285925508472754, - "y": 6.135860681339741, - "z": -3.084115266640774 + "x": -2.7373252313099936, + "y": 5.5744471940243105, + "z": 7.192391392495912 }, "target": { "x": 0, - "y": 2.3565700410649804, + "y": 2.5, "z": 0 }, "zoom": 1, @@ -1888,10 +1840,221 @@ } }, "widgets_values": [ - "C:/Git/ComfyUI/output/3D/Pistol_MV/Pistol_MV_Textured_MV_00001_.glb", + "C:/Git/ComfyUI/output/3D/AnimeGirl/AnimeGirl_Textured_MV_00002_.glb", "" ] }, + { + "id": 209, + "type": "PrimitiveInt", + "pos": [ + -1941.870656950478, + 277.2558588777398 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 400 + ] + } + ], + "title": "Target Face Number", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.16.4", + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 500000, + "fixed" + ] + }, + { + "id": 6, + "type": "Trellis2LoadImageWithTransparency", + "pos": [ + -1948.5694294982507, + 430.5866991705322 + ], + "size": [ + 631.9393495485403, + 765.9737728955563 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": null + }, + { + "name": "mask", + "type": "MASK", + "links": null + }, + { + "name": "image_with_alpha", + "type": "IMAGE", + "links": [ + 344 + ] + } + ], + "title": "Normal Image", + "properties": { + "aux_id": "visualbruno/ComfyUI-Trellis2", + "ver": "e7f9b30df7a09bcedf1c955e176754c73f983254", + "Node name for S&R": "Trellis2LoadImageWithTransparency", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "AnimeGirl_1024_00001_.png", + "image" + ] + }, + { + "id": 239, + "type": "Trellis2LoadImageWithTransparency", + "pos": [ + -1939.1565291278416, + 1247.0590522961702 + ], + "size": [ + 627.638044195148, + 672.4931923313359 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 483 + ] + }, + { + "name": "mask", + "type": "MASK", + "links": null + }, + { + "name": "image_with_alpha", + "type": "IMAGE", + "links": [ + 440 + ] + } + ], + "title": "Color Image", + "properties": { + "aux_id": "visualbruno/ComfyUI-Trellis2", + "ver": "e8098f5641702e74133c2dea0b45a13a3ab19985", + "Node name for S&R": "Trellis2LoadImageWithTransparency" + }, + "widgets_values": [ + "AnimeGirl_808_00001_.png", + "image" + ] + }, + { + "id": 219, + "type": "PrimitiveString", + "pos": [ + -1941.3621908316666, + 157.6667334107927 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 464, + 465 + ] + } + ], + "title": "Name -> output in 3D folder", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.16.4", + "Node name for S&R": "PrimitiveString" + }, + "widgets_values": [ + "AnimeGirl" + ] + }, + { + "id": 250, + "type": "Hy3DDiffusersSchedulerConfig", + "pos": [ + -308.8898175615872, + 2137.2330213377168 + ], + "size": [ + 390.5999755859375, + 82 + ], + "flags": { + "collapsed": true + }, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "pipeline", + "type": "HY3DDIFFUSERSPIPE", + "link": 450 + } + ], + "outputs": [ + { + "name": "diffusers_scheduler", + "type": "NOISESCHEDULER", + "slot_index": 0, + "links": [ + 455 + ] + } + ], + "properties": { + "aux_id": "visualbruno/ComfyUI-Hunyuan3DWrapper", + "ver": "6090a9051f109d2774d309cf6a8a22d4a11a1b15", + "Node name for S&R": "Hy3DDiffusersSchedulerConfig", + "widget_ue_connectable": { + "scheduler": true, + "sigmas": true + }, + "cnr_id": "comfyui-hunyan3dwrapper" + }, + "widgets_values": [ + "Euler", + "default" + ] + }, { "id": 264, "type": "Trellis2MultiViewTexturing", @@ -1901,7 +2064,7 @@ ], "size": [ 363.3228515625, - 386 + 410 ], "flags": {}, "order": 38, @@ -1991,173 +2154,11 @@ 2, 1.1, 1.15, - "", + true, + 10, "", "" ] - }, - { - "id": 209, - "type": "PrimitiveInt", - "pos": [ - -1941.870656950478, - 277.2558588777398 - ], - "size": [ - 270, - 82 - ], - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 400 - ] - } - ], - "title": "Target Face Number", - "properties": { - "cnr_id": "comfy-core", - "ver": "0.16.4", - "Node name for S&R": "PrimitiveInt" - }, - "widgets_values": [ - 500000, - "fixed" - ] - }, - { - "id": 6, - "type": "Trellis2LoadImageWithTransparency", - "pos": [ - -1948.5694294982507, - 430.5866991705322 - ], - "size": [ - 631.9393495485403, - 765.9737728955563 - ], - "flags": {}, - "order": 5, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "image", - "type": "IMAGE", - "links": null - }, - { - "name": "mask", - "type": "MASK", - "links": null - }, - { - "name": "image_with_alpha", - "type": "IMAGE", - "links": [ - 344 - ] - } - ], - "title": "Normal Image", - "properties": { - "aux_id": "visualbruno/ComfyUI-Trellis2", - "ver": "e7f9b30df7a09bcedf1c955e176754c73f983254", - "Node name for S&R": "Trellis2LoadImageWithTransparency", - "widget_ue_connectable": {} - }, - "widgets_values": [ - "Image_1024_00081_.png", - "image" - ] - }, - { - "id": 239, - "type": "Trellis2LoadImageWithTransparency", - "pos": [ - -1939.1565291278416, - 1247.0590522961702 - ], - "size": [ - 627.638044195148, - 672.4931923313359 - ], - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "image", - "type": "IMAGE", - "links": [ - 483 - ] - }, - { - "name": "mask", - "type": "MASK", - "links": null - }, - { - "name": "image_with_alpha", - "type": "IMAGE", - "links": [ - 440 - ] - } - ], - "title": "Color Image", - "properties": { - "aux_id": "visualbruno/ComfyUI-Trellis2", - "ver": "e8098f5641702e74133c2dea0b45a13a3ab19985", - "Node name for S&R": "Trellis2LoadImageWithTransparency" - }, - "widgets_values": [ - "Image_1024_00065_.png", - "image" - ] - }, - { - "id": 219, - "type": "PrimitiveString", - "pos": [ - -1941.3621908316666, - 157.6667334107927 - ], - "size": [ - 270, - 58 - ], - "flags": {}, - "order": 7, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 464, - 465 - ] - } - ], - "title": "Name -> output in 3D folder", - "properties": { - "cnr_id": "comfy-core", - "ver": "0.16.4", - "Node name for S&R": "PrimitiveString" - }, - "widgets_values": [ - "Pistol_MV" - ] } ], "links": [ @@ -2722,10 +2723,10 @@ "workflowRendererVersion": "LG", "ue_links": [], "ds": { - "scale": 0.3899868676879694, + "scale": 0.835971485622887, "offset": [ - 2537.916276869781, - -243.0909834796268 + 1391.1352648291552, + -2311.024721130755 ] }, "links_added_by_ue": [], diff --git a/nodes.py b/nodes.py index 98c4972..7ddb829 100644 --- a/nodes.py +++ b/nodes.py @@ -3852,7 +3852,8 @@ class Trellis2MultiViewTexturing: "blend_exponent": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 8.0, "step": 0.5}), "ortho_scale": ("FLOAT", {"default": 1.0, "min": 0.05, "max": 10.0, "step": 0.01}), "norm_size": ("FLOAT",{"default":1.15, "min":0.0, "max":9.99, "step":0.01}), - "fill_holes": ("BOOLEAN",{"default":False}), + "fill_holes": ("BOOLEAN",{"default":True}), + "max_hole_size": ("INT",{"default":10,"min":0,"max":99999,"step":1}), }, "optional": { # Standard views @@ -3885,6 +3886,7 @@ class Trellis2MultiViewTexturing: ortho_scale, norm_size, fill_holes, + max_hole_size, baseColorTexture = None, front_image=None, back_image=None, @@ -3964,7 +3966,8 @@ class Trellis2MultiViewTexturing: ortho_scale=ortho_scale, blend_texture=blend_texture, fill_holes=fill_holes, - norm_size=norm_size + norm_size=norm_size, + max_hole_size=max_hole_size ) return (trimesh_obj, pil2tensor(base_color), pil2tensor(mr)) diff --git a/texture_projection_multiview.py b/texture_projection_multiview.py index 2f77381..a07eddc 100644 --- a/texture_projection_multiview.py +++ b/texture_projection_multiview.py @@ -53,6 +53,7 @@ import math import cumesh as CuMesh +from tqdm import tqdm # --------------------------------------------------------------------------- # Camera helpers @@ -138,6 +139,7 @@ def texture_mesh_with_multiview( norm_size: float = 1.15, fill_holes: bool = False, blend_texture: bool = True, + max_hole_size: int = 10, ): if not (len(images) == len(azimuths) == len(elevations)): raise ValueError("images, azimuths, and elevations must have the same length") @@ -339,36 +341,96 @@ def texture_mesh_with_multiview( n_projected = int(composite_mask.sum().item()) print(f" Projected texels: {n_projected} / {texture_size*texture_size}" f" ({100.0*n_projected/(texture_size*texture_size):.1f}%)") - - if fill_holes: - print('Filling holes ...') - - hole_mask = (~valid_mask.cpu().numpy()).astype(np.uint8) - n_holes = int(hole_mask.sum()) - print(f" Inpainting {n_holes} hole texels ({100.0*n_holes/hole_mask.size:.1f}%)...") - if n_holes > 0: - for c in range(3): - color_np[..., c] = cv2.inpaint(color_np[..., c], hole_mask, 3, cv2.INPAINT_NS) - alpha_np = cv2.inpaint(alpha_np, hole_mask, 3, cv2.INPAINT_NS) else: # No existing texture found – fall back to projection-only output print(" WARNING: No existing PBR baseColorTexture found on mesh, " "outputting projection-only texture (holes will be transparent).") color_np = (projected_color.cpu().numpy() * 255).clip(0, 255).astype(np.uint8) alpha_np = composite_mask.cpu().numpy().astype(np.uint8) * 255 - - if fill_holes: - print('Filling holes ...') - - hole_mask = (~valid_mask.cpu().numpy()).astype(np.uint8) - n_holes = int(hole_mask.sum()) - print(f" Inpainting {n_holes} hole texels ({100.0*n_holes/hole_mask.size:.1f}%)...") - if n_holes > 0: - for c in range(3): - color_np[..., c] = cv2.inpaint(color_np[..., c], hole_mask, 3, cv2.INPAINT_NS) - alpha_np = cv2.inpaint(alpha_np, hole_mask, 3, cv2.INPAINT_NS) + if fill_holes: + print('Filling holes and padding UV seams ...') + + # Get numpy arrays + uv_mask_np = uv_hit_mask.cpu().numpy().astype(np.uint8) + valid_mask_np = valid_mask.cpu().numpy().astype(np.uint8) + + # 1. INTERNAL HOLES: Pixels inside the UV map that didn't get colored + internal_hole_mask = cv2.bitwise_and(cv2.bitwise_not(valid_mask_np), uv_mask_np) + + if max_hole_size > 0: + # Filter internal holes by size to avoid filling massive intentional gaps + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(internal_hole_mask, connectivity=8) + areas = stats[:, cv2.CC_STAT_AREA] + valid_labels_mask = (areas <= max_hole_size) + valid_labels_mask[0] = False # Ignore background + filtered_internal_holes = valid_labels_mask[labels].astype(np.uint8) + else: + filtered_internal_holes = internal_hole_mask + + # 2. UV SEAM PADDING: A small band of pixels just outside the UV islands + # We dilate the UV mask by a few pixels to create a "bleed" area + pad_radius = 5 # You can increase this if the black lines persist at long distances + kernel = np.ones((3, 3), np.uint8) + dilated_uv = cv2.dilate(uv_mask_np, kernel, iterations=pad_radius) + + # The padding mask is the dilated area MINUS the original UV area + seam_padding_mask = cv2.bitwise_and(dilated_uv, cv2.bitwise_not(uv_mask_np)) + + # 3. COMBINE AND INPAINT + # We want to inpaint both the internal dots and the external seams simultaneously + final_inpaint_mask = cv2.bitwise_or(filtered_internal_holes, seam_padding_mask) + + n_holes = int(filtered_internal_holes.sum()) + n_pad = int(seam_padding_mask.sum()) + print(f" Inpainting {n_holes} internal texels and {n_pad} seam padding texels...") + + if (n_holes + n_pad) > 0: + for c in range(3): + color_np[..., c] = cv2.inpaint(color_np[..., c], final_inpaint_mask, 3, cv2.INPAINT_NS) + alpha_np = cv2.inpaint(alpha_np, final_inpaint_mask, 3, cv2.INPAINT_NS) + + # if fill_holes: + # print('Filling holes ...') + + # # 1. Get the raw mask of all holes (1 for hole, 0 for valid) + # raw_hole_mask = (~valid_mask.cpu().numpy()).astype(np.uint8) + + # # 2. Filter by size if a limit is set + # if max_hole_size > 0: + # filtered_hole_mask = np.zeros_like(raw_hole_mask) + # # Find connected components (connectivity=8 handles diagonals) + # num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(raw_hole_mask, connectivity=8) + + # print(f"Num Labels: {num_labels}") + # # Label 0 is the background (non-holes), so we start checking from Label 1 + + # if num_labels>0: + # progress_bar = tqdm(total=num_labels, desc="Filling holes") + + # for label_id in range(1, num_labels): + # area = stats[label_id, cv2.CC_STAT_AREA] + # if area <= max_hole_size: + # # If the hole is small enough, add it to our filtered mask + # filtered_hole_mask[labels == label_id] = 1 + # progress_bar.update(1) + + # progress_bar.close() + + # print(f"Number of filtered holes: {len(filtered_hole_mask)}") + + # hole_mask = filtered_hole_mask + # else: + # hole_mask = raw_hole_mask + + # n_holes = int(hole_mask.sum()) + # print(f" Inpainting {n_holes} hole texels ({100.0*n_holes/hole_mask.size:.1f}%)...") + + # if n_holes > 0: + # for c in range(3): + # color_np[..., c] = cv2.inpaint(color_np[..., c], hole_mask, 3, cv2.INPAINT_NS) + # alpha_np = cv2.inpaint(alpha_np, hole_mask, 3, cv2.INPAINT_NS) # =========================================================================