diff --git a/README.MD b/README.MD index f97a9c1..4c0f2da 100644 --- a/README.MD +++ b/README.MD @@ -17,6 +17,9 @@ Nodes are divided into 5 groups according to their functions: LayerStyle, LayerC ## Update **If the dependency package error after updating, please reinstall the relevant dependency packages. for details, please refer to [here](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5). +* Commit [ImageRemoveAlpha](#ImageRemoveAlpha) and [ImageCombineAlpha](#ImageCombineAlpha) nodes, alpha channel of the image can be removed or merged. +* Commit [ImageScaleRestoreV2](#ImageScaleRestoreV2) and [ImageScaleByAspectRatioV2](#ImageScaleByAspectRatioV2) nodes, supports scaling images to specified long or short edge sizes. +* Commit [PersonMaskUltra](#PersonMaskUltra) node, Generate masks for portrait's face, hair, body skin, clothing, or accessories. the model code for this node comes from [a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator). * Commit [LightLeak](#LightLeak) node, this filter simulate the light leakage effect of the film. * Commit [Film](#Film) node, this filter simulate the grain, dark edge, and blurred edge of the film, support input depth map to simulate defocus. it is reorganize and encapsulate of [digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost). * Commit [ImageAutoCrop](#ImageAutoCrop) node, which is designed to generate image materials for training models. @@ -376,6 +379,15 @@ Outputs: * mask: If have mask input, the scaled mask will be output. * original_size: The original size data of the image is used for subsequent node recovery. +### ImageScaleRestoreV2 +The V2 upgraded version of ImageScaleRestore. + +Node options: +![image](image/image_scale_restore_v2_node.png) +The following changes have been made based on ImageScaleRestore: +* scale_by: Allow scaling by specified size on long or short sides. +* scale_by_length: When scale_by is set to "longest", this will be used as the length of the long edge of the image; When set to "shortest", it serves as the length of the short edge. + ### ImageMaskScaleAs Scale the image or mask to the size of the reference image (or reference mask). ![image](image/image_mask_scale_as_example.png) @@ -417,6 +429,15 @@ The _fill_ mode does not maintain frame ratio and fills the screen with width an * longest_side: When the scale_by_longest_side is set to True, this will be used this value to the long edge of the image. when the original_size have input, this setting will be ignored. +### ImageScaleByAspectRatioV2 +V2 Upgraded Version of ImageScaleByAspectRatio + +Node options: +![image](image/image_scale_by_aspect_ratio_v2_node.png) +The following changes have been made based on ImageScaleByAspectRatio: +* scale_to_side: Allow scaling by specified size on long or short sides. +* scale_to_length: When scale_by_side is set to "longest", this will be used as the length of the long edge of the image; When set to "shortest", it serves as the length of the short edge. + ### ImageShift Shift the image. this node supports the output of displacement seam masks, making it convenient to create continuous textures. ![image](image/image_shift_example.png) @@ -599,6 +620,20 @@ Node options: ![image](image/image_channel_merge_node.png) * mode: Channel mode, include RGBA, YCbCr, LAB adn HSV. +### ImageRemoveAlpha +![image](image/image_remove_alpha_example.png) +Remove the alpha channel from the image and convert it to RGB mode. you can choose to fill the background and set the background color. + +Node options: +![image](image/image_remove_alpha_node.png) +* fill_background: Whether to fill the background. +* background_color4: Color of background. + + +### ImageCombineAlpha +![image](image/image_combine_alpha_node.png) +Merge the image and mask into an RGBA mode image containing an alpha channel. + ### ImageAutoCrop ![image](image/image_auto_crop_example.png) Automatically cutout and crop the image according to the mask. it can specify the background color, aspect ratio, and size for output image. this node is designed to generate the image materials for training models. @@ -711,6 +746,26 @@ Node options: * white_point: Edge white sampling threshold. * process_detail: Set to false here will skip edge processing to save runtime. +### PersonMaskUltra +Generate masks for portrait's face, hair, body skin, clothing, or accessories. Compared to the previous A Person Mask Generator node, this node has ultra-high edge details. +The model code for this node comes from [a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator), edge processing code from [ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters). + +![image](image/person_mask_ultra_example.png) + +Node options: +![image](image/person_mask_ultra_node.png) +* face: Face recognition. +* hair: Hair recognition. +* body: Body skin recognition. +* clothes: Clothing recognition. +* accessories: Identification of accessories (such as backpacks). +* background: Background recognition. +* confidence: Recognition threshold, lower values will output more mask ranges. +* detail_range: Edge detail range. +* black_point: Edge black sampling threshold. +* white_point: Edge white sampling threshold. +* process_detail: Set to false here will skip edge processing to save runtime. + ### PixelSpread Pixel expansion preprocessing on the masked edge of an image can effectively improve the edges of image composit. ![image](image/pixel_spread_example.png) diff --git a/README_CN.MD b/README_CN.MD index 5853283..30e66ec 100644 --- a/README_CN.MD +++ b/README_CN.MD @@ -13,52 +13,55 @@ ## ¸üÐÂ˵Ã÷ **Èç¹û±¾²å¼þ¸üкó³öÏÖÒÀÀµ°ü´íÎó£¬ÇëÖØÐ°²×°Ïà¹ØÒÀÀµ°ü¡£ÏêÇé¼û[ÕâÀï](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5)¡£ -* Ìí¼Ó[LightLeak](#LightLeak) ½Úµã£¬Õâ¸öÂ˾µÄ£Ä⽺Ƭ©¹âЧ¹û¡£ -* Ìí¼Ó[Film](#Film) ½Úµã, Õâ¸öÂ˾µÄ£Ä⽺ƬµÄ¿ÅÁ£¡¢°µ±ßºÍ±ßԵģºý£¬Ö§³ÖÊäÈëÉî¶ÈͼģÄâÐé½¹£¬ÊÇ[digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost)µÄÖØÐ·â×°¡£ -* Ìí¼Ó[ImageAutoCrop](#ImageAutoCrop) ½Úµã, Õâ¸ö½ÚµãÊÇΪÉú³ÉѵÁ·Ä£Ð͵ÄÍ¼Æ¬ËØ²Ä¶øÉè¼ÆµÄ¡£ -* Ìí¼Ó[ImageScaleByAspectRatio](#ImageScaleByAspectRatio) ½Úµã£¬ ¿É°´»­·ù±ÈÀýËõ·ÅͼÏñ¡£ -* ¸ÄÕý[LUT Apply](#LUT) ½ÚµãäÖȾ³öÏÖÉ«½×µÄbug, ²¢Ôö¼ÓlogÉ«²Ê¿Õ¼äÖ§³Ö¡£*logÉ«²Ê¿Õ¼äͼƬÇë¼ÓÔØ×¨ÃŵÄlog lut¡£ -* Ìí¼Ó[CreateGradientMask](#CreateGradientMask) ½Úµã¡£Ìí¼Ó [LayerImageTransform](#LayerImageTransform) ºÍ [LayerMaskTransform](#LayerMaskTransform) ½Úµã¡£ -* Ìí¼Ó[MaskEdgeUltraDetail](#MaskEdgeUltraDetail) ½Úµã£¬¸ø´Ö²ÚµÄÕÚÕÖ½øÐд¦Àí»ñµÃ³¬¾«Ï¸µÄ±ßÔµ¡£Ìí¼Ó [Exposure](#Exposure) ½Úµã£¬µ÷ÕûͼÏñÆØ¹â¡£ -* Ìí¼Ó[Sharp & Soft](#Sharp) ½Úµã£¬¿ÉÌáÉý»òĨƽͼÏñϸ½Ú¡£ÐÂÔö[MaskByDifferent](#MaskByDifferent)½Úµã£¬±È½ÏÁ½ÕÅͼƬ²¢Êä³öMask¡£ÐÂÔö[SegmentAnythingUltra](#SegmentAnythingUltra)½Úµã£¬ÌáÉýÕÚÕÖ±ßÔµÖÊÁ¿¡£*Èç¹ûûÓа²×°SegmentAnything, ÐèÒªÊÖ¶¯ÏÂÔØÄ£ÐÍ¡£ +* Ìí¼Ó [ImageRemoveAlpha](#ImageRemoveAlpha) ºÍ [ImageCombineAlpha](#ImageCombineAlpha) ½Úµã£¬¿ÉÒÆ³ý»òºÏ²¢Í¼Æ¬µÄalphaͨµÀ¡£ +* Ìí¼Ó [ImageScaleRestoreV2](#ImageScaleRestoreV2) ºÍ [ImageScaleByAspectRatioV2](#ImageScaleByAspectRatioV2) ½Úµã¡£Ö§³Ö°´Ö¸¶¨µÄ³¤±ß»ò¶Ì±ß³ß´çËõ·ÅͼÏñ¡£ +* Ìí¼Ó [PersonMaskUltra](#PersonMaskUltra) ½Úµã£¬ÎªÈËÎïÉú³ÉÁ³¡¢Í··¢¡¢ÉíÌ寤·ô¡¢Ò·þ»òÅäÊεÄÕÚÕÖ¡£±¾½ÚµãµÄÄ£ÐÍ´úÂëÀ´×Ô[a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator)¡£ +* Ìí¼Ó [LightLeak](#LightLeak) ½Úµã£¬Õâ¸öÂ˾µÄ£Ä⽺Ƭ©¹âЧ¹û¡£ +* Ìí¼Ó [Film](#Film) ½Úµã, Õâ¸öÂ˾µÄ£Ä⽺ƬµÄ¿ÅÁ£¡¢°µ±ßºÍ±ßԵģºý£¬Ö§³ÖÊäÈëÉî¶ÈͼģÄâÐé½¹£¬ÊÇ[digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost)µÄÖØÐ·â×°¡£ +* Ìí¼Ó [ImageAutoCrop](#ImageAutoCrop) ½Úµã, Õâ¸ö½ÚµãÊÇΪÉú³ÉѵÁ·Ä£Ð͵ÄÍ¼Æ¬ËØ²Ä¶øÉè¼ÆµÄ¡£ +* Ìí¼Ó [ImageScaleByAspectRatio](#ImageScaleByAspectRatio) ½Úµã£¬ ¿É°´»­·ù±ÈÀýËõ·ÅͼÏñ¡£ +* ¸ÄÕý [LUT Apply](#LUT) ½ÚµãäÖȾ³öÏÖÉ«½×µÄbug, ²¢Ôö¼ÓlogÉ«²Ê¿Õ¼äÖ§³Ö¡£*logÉ«²Ê¿Õ¼äͼƬÇë¼ÓÔØ×¨ÃŵÄlog lut¡£ +* Ìí¼Ó [CreateGradientMask](#CreateGradientMask) ½Úµã¡£Ìí¼Ó [LayerImageTransform](#LayerImageTransform) ºÍ [LayerMaskTransform](#LayerMaskTransform) ½Úµã¡£ +* Ìí¼Ó [MaskEdgeUltraDetail](#MaskEdgeUltraDetail) ½Úµã£¬¸ø´Ö²ÚµÄÕÚÕÖ½øÐд¦Àí»ñµÃ³¬¾«Ï¸µÄ±ßÔµ¡£Ìí¼Ó [Exposure](#Exposure) ½Úµã£¬µ÷ÕûͼÏñÆØ¹â¡£ +* Ìí¼Ó [Sharp & Soft](#Sharp) ½Úµã£¬¿ÉÌáÉý»òĨƽͼÏñϸ½Ú¡£ÐÂÔö[MaskByDifferent](#MaskByDifferent)½Úµã£¬±È½ÏÁ½ÕÅͼƬ²¢Êä³öMask¡£ÐÂÔö[SegmentAnythingUltra](#SegmentAnythingUltra)½Úµã£¬ÌáÉýÕÚÕÖ±ßÔµÖÊÁ¿¡£*Èç¹ûûÓа²×°SegmentAnything, ÐèÒªÊÖ¶¯ÏÂÔØÄ£ÐÍ¡£ * ËùÓнڵãÒÑÈ«ÃæÖ§³ÖÅúÁ¿Í¼Æ¬£¬Îª´´×÷ÊÓÆµÌṩ·½±ã¡£( CropByMask ½Úµã½öÖ§³ÖÏàͬ³ß´çµÄÇгý, Èç¹ûÊäÈëÅúÁ¿mask_for_crop£¬½«Ê¹ÓõÚÒ»ÕŵÄÊý¾Ý¡£) -* Ìí¼Ó[RemBgUltra](#RemBgUltra) ºÍ [PixelSpread](#PixelSpread) ½Úµã£¬ÏÔÖøÌáÉýÁËÕÚÕÖÖÊÁ¿¡£*RemBgUltraÐèÊÖ¶¯ÏÂÔØÄ£ÐÍ¡£ -* Ìí¼Ó[TextImage](#TextImage) ½Úµã£¬Éú³ÉÎÄ×ÖͼÏñºÍÕÚÕÖ¡£ +* Ìí¼Ó [RemBgUltra](#RemBgUltra) ºÍ [PixelSpread](#PixelSpread) ½Úµã£¬ÏÔÖøÌáÉýÁËÕÚÕÖÖÊÁ¿¡£*RemBgUltraÐèÊÖ¶¯ÏÂÔØÄ£ÐÍ¡£ +* Ìí¼Ó [TextImage](#TextImage) ½Úµã£¬Éú³ÉÎÄ×ÖͼÏñºÍÕÚÕÖ¡£ * ͼÏñÖ®¼äµÄ[»ìºÏģʽ](#»ìºÏģʽ)Ôö¼ÓÐÂÀàÐÍ£¬ÏÖÔÚÖ§³Ö¶à´ï19ÖÖ»ìºÏģʽ¡£ÐÂÔöcolor_burnÑÕÉ«¼ÓÉî, color_dodgeÑÕÉ«¼õµ­, linear_burnÏßÐÔ¼ÓÉî, linear_dodgeÏßÐÔ¼õµ­, overlayµþ¼Ó, soft_lightÈá¹â, hard_lightÇ¿¹â, vivid_lightÁÁ¹â, pin_lightµã¹â, linear_lightÏßÐÔ¹â, hard_mixʵɫ»ìºÏ¡£ÐÂÔöµÄ»ìºÏģʽÊÊÓÃÓÚËùÓÐÖ§³Ö»ìºÏģʽµÄ½Úµã¡£ -* Ìí¼Ó[ColorMap](#ColorMap) Â˾µ½Úµã£¬ÓÃÓÚÖÆ×÷α²ÊÉ«ÈÈÁ¦Í¼Ð§¹û¡£ -* Ìí¼Ó[WaterColor](#WaterColor) ºÍ [SkinBeauty](#SkinBeauty) ½Úµã¡£ÕâÊÇÁ½¸öͼÏñÂ˾µ£¬Éú³ÉË®²Ê»­ºÍĥƤЧ¹û¡£ -* Ìí¼Ó[ImageShift](#ImageShift) ½Úµã£¬Ê¹Í¼Æ¬²úÉúÎ»ÒÆ£¬¿ÉÊä³öÎ»ÒÆ½Ó·ìÕÚÕÖ£¬·½±ãÖÆ×÷Á¬ÐøÌùͼ¡£ -* Ìí¼Ó[ImageMaskScaleAs](#ImageMaskScaleAs) ½Úµã£¬¿É¸ù¾Ý²Î¿¼Í¼Æ¬µ÷ÕûͼÏñ»òÕÚÕÖ´óС¡£ -* Ìí¼Ó[ImageScaleRestore](#ImageScaleRestore) ½Úµã£¬ÓÃÓÚÅäºÏCropByMask½øÐоֲ¿·Å´óÐÞ¸´¹¤×÷¡£ -* Ìí¼Ó[CropByMask](#CropByMask) ºÍ [RestoreCropBox](#RestoreCropBox)½Úµã¡£´Ë¶þÕßÅäºÏ¿É½«Í¼Æ¬¾Ö²¿²ÃÇÐÖØ»æÈ»ºó»¹Ô­¡£ -* Ìí¼Ó[ColorAdapter](#ColorAdapter) ½Úµã£¬¿É×Ô¶¯µ÷ÕûͼƬɫµ÷¡£ -* Ìí¼Ó[MaskStroke](#MaskStroke) ½Úµã£¬¿É²úÉúmaskÃè±ß¡£ -* Ìí¼Ó[LayerColor](#LayerColor)½Úµã×飬ÓÃÓÚµ÷ÕûͼÏñÑÕÉ«¡£°üÀ¨[LUT Apply](#LUT)£¬[Gamma](#Gamma), [Brightness & Contrast](#Brightness), [RGB](#RGB), [YUV](#YUV), [LAB](#LAB)ºÍ[HSV](#HSV)¡£ -* Ìí¼Ó[ImageChannelSplit](#ImageChannelSplit)ºÍ[ImageChannelMerge](#ImageChannelMerge)½Úµã¡£ -* Ìí¼Ó[MaskMotionBlur](#MaskMotionBlur)½Úµã¡£ -* Ìí¼Ó[SoftLight](#SoftLight)½Úµã¡£ -* Ìí¼Ó[ChannelShake](#ChannelShake)½Úµã£¬ÕâÊÇÒ»¸öÂ˾µ£¬ÄܲúÉúÀàËÆ¶¶ÒôlogoµÄͨµÀ´íλЧ¹û¡£ -* Ìí¼Ó[MaskGradient](#MaskGradient)½Úµã£¬¿Éʹmask²úÉú½¥±ä¡£ -* Ìí¼Ó[GetColorTone](#GetColorTone)½Úµã£¬¿ÉÒÔ»ñȡͼƬµÄÖ÷É«»òƽ¾ùÉ«¡£Ìí¼Ó[MaskGrow](#MaskGrow)ºÍ[MaskEdgeShrink](#MaskEdgeShrink)½Úµã¡£ +* Ìí¼Ó [ColorMap](#ColorMap) Â˾µ½Úµã£¬ÓÃÓÚÖÆ×÷α²ÊÉ«ÈÈÁ¦Í¼Ð§¹û¡£ +* Ìí¼Ó [WaterColor](#WaterColor) ºÍ [SkinBeauty](#SkinBeauty) ½Úµã¡£ÕâÊÇÁ½¸öͼÏñÂ˾µ£¬Éú³ÉË®²Ê»­ºÍĥƤЧ¹û¡£ +* Ìí¼Ó [ImageShift](#ImageShift) ½Úµã£¬Ê¹Í¼Æ¬²úÉúÎ»ÒÆ£¬¿ÉÊä³öÎ»ÒÆ½Ó·ìÕÚÕÖ£¬·½±ãÖÆ×÷Á¬ÐøÌùͼ¡£ +* Ìí¼Ó [ImageMaskScaleAs](#ImageMaskScaleAs) ½Úµã£¬¿É¸ù¾Ý²Î¿¼Í¼Æ¬µ÷ÕûͼÏñ»òÕÚÕÖ´óС¡£ +* Ìí¼Ó [ImageScaleRestore](#ImageScaleRestore) ½Úµã£¬ÓÃÓÚÅäºÏCropByMask½øÐоֲ¿·Å´óÐÞ¸´¹¤×÷¡£ +* Ìí¼Ó [CropByMask](#CropByMask) ºÍ [RestoreCropBox](#RestoreCropBox)½Úµã¡£´Ë¶þÕßÅäºÏ¿É½«Í¼Æ¬¾Ö²¿²ÃÇÐÖØ»æÈ»ºó»¹Ô­¡£ +* Ìí¼Ó [ColorAdapter](#ColorAdapter) ½Úµã£¬¿É×Ô¶¯µ÷ÕûͼƬɫµ÷¡£ +* Ìí¼Ó [MaskStroke](#MaskStroke) ½Úµã£¬¿É²úÉúmaskÃè±ß¡£ +* Ìí¼Ó [LayerColor](#LayerColor)½Úµã×飬ÓÃÓÚµ÷ÕûͼÏñÑÕÉ«¡£°üÀ¨[LUT Apply](#LUT)£¬[Gamma](#Gamma), [Brightness & Contrast](#Brightness), [RGB](#RGB), [YUV](#YUV), [LAB](#LAB)ºÍ[HSV](#HSV)¡£ +* Ìí¼Ó [ImageChannelSplit](#ImageChannelSplit)ºÍ[ImageChannelMerge](#ImageChannelMerge)½Úµã¡£ +* Ìí¼Ó [MaskMotionBlur](#MaskMotionBlur)½Úµã¡£ +* Ìí¼Ó [SoftLight](#SoftLight)½Úµã¡£ +* Ìí¼Ó [ChannelShake](#ChannelShake)½Úµã£¬ÕâÊÇÒ»¸öÂ˾µ£¬ÄܲúÉúÀàËÆ¶¶ÒôlogoµÄͨµÀ´íλЧ¹û¡£ +* Ìí¼Ó [MaskGradient](#MaskGradient)½Úµã£¬¿Éʹmask²úÉú½¥±ä¡£ +* Ìí¼Ó [GetColorTone](#GetColorTone)½Úµã£¬¿ÉÒÔ»ñȡͼƬµÄÖ÷É«»òƽ¾ùÉ«¡£Ìí¼Ó[MaskGrow](#MaskGrow)ºÍ[MaskEdgeShrink](#MaskEdgeShrink)½Úµã¡£ -* Ìí¼Ó[MaskBoxDetect](#MaskBoxDetect)½Úµã£¬¿ÉÒÔͨ¹ýmask×Ô¶¯Ì½²âλÖò¢Êä³öµ½ºÏ³É½Úµã¡£Ìí¼Ó[XY to Percent](#Percent)½Úµã£¬½«¾ø¶Ô×ø±êת»»Îªpercent×ø±ê¡£Ìí¼Ó[GaussianBlur](#GaussianBlur)½Úµã¡£Ìí¼Ó[GetImageSize](#GetImageSize)½Úµã¡£ -* Ìí¼Ó[ExtendCanvas](#ExtendCanvas)½Úµã¡£ -* Ìí¼Ó[ImageBlendAdvance](#ImageBlendAdvance)½Úµã¡£Õâ¸ö½ÚµãÔÊÐíºÏ³É³ß´ç²»Í¬µÄ±³¾°Í¼ºÍͼ²ã£¬ÌṩÁ˸ü¼Ó×ÔÓɵĺϳÉÌåÑé¡£ - Ìí¼Ó[PrintInfo](#PrintInfo)½Úµã×÷Ϊ¹¤×÷Á÷µ÷ÊÔ¸¨Öú¹¤¾ß¡£ -* Ìí¼Ó[ColorImage](#ColorImage)ºÍ[GradientImage](#GradientImage)½Úµã£¬ÓÃÓÚÉú³É´¿É«ºÍ½¥±äɫͼÏñ¡£ -* Ìí¼Ó[GradientOverlay](#GradientOverlay),[ColorOverlay](#ColorOverlay)½Úµã¡£Ôö¼ÓÎÞЧmaskÊäÈëÅжϣ¬µ±ÊäÈëÎÞЧmaskʱ½«ÆäºöÂÔ¡£ -* Ìí¼Ó[InnerGlow](#InnerGlow), [InnerShadow](#InnerShadow), [MotionBlur](#MotionBlur)½Úµã¡£ +* Ìí¼Ó [MaskBoxDetect](#MaskBoxDetect)½Úµã£¬¿ÉÒÔͨ¹ýmask×Ô¶¯Ì½²âλÖò¢Êä³öµ½ºÏ³É½Úµã¡£Ìí¼Ó[XY to Percent](#Percent)½Úµã£¬½«¾ø¶Ô×ø±êת»»Îªpercent×ø±ê¡£Ìí¼Ó[GaussianBlur](#GaussianBlur)½Úµã¡£Ìí¼Ó[GetImageSize](#GetImageSize)½Úµã¡£ +* Ìí¼Ó [ExtendCanvas](#ExtendCanvas)½Úµã¡£ +* Ìí¼Ó [ImageBlendAdvance](#ImageBlendAdvance)½Úµã¡£Õâ¸ö½ÚµãÔÊÐíºÏ³É³ß´ç²»Í¬µÄ±³¾°Í¼ºÍͼ²ã£¬ÌṩÁ˸ü¼Ó×ÔÓɵĺϳÉÌåÑé¡£ + Ìí¼Ó [PrintInfo](#PrintInfo)½Úµã×÷Ϊ¹¤×÷Á÷µ÷ÊÔ¸¨Öú¹¤¾ß¡£ +* Ìí¼Ó [ColorImage](#ColorImage)ºÍ[GradientImage](#GradientImage)½Úµã£¬ÓÃÓÚÉú³É´¿É«ºÍ½¥±äɫͼÏñ¡£ +* Ìí¼Ó [GradientOverlay](#GradientOverlay),[ColorOverlay](#ColorOverlay)½Úµã¡£Ôö¼ÓÎÞЧmaskÊäÈëÅжϣ¬µ±ÊäÈëÎÞЧmaskʱ½«ÆäºöÂÔ¡£ +* Ìí¼Ó [InnerGlow](#InnerGlow), [InnerShadow](#InnerShadow), [MotionBlur](#MotionBlur)½Úµã¡£ * ËùÓÐÒÑÍê³ÉµÄ½ÚµãÖØÐÂÃüÃû£¬½Úµã·ÖΪ4×飺LayerStyle, LayerMask, LayerUtility, LayerFilter¡£ ÒòÎªÖØÐÂÃüÃû£¬°üº¬¾É°æ½ÚµãµÄ¹¤×÷Á÷ÐèÊÖ¶¯Ì滻аæ½Úµã¡£ * [OuterGlow](#OuterGlow)½ÚµãÐ޸ģ¬Ôö¼ÓÁÁ¶È¡¢µÆ¹âÑÕÉ«¡¢»Ô¹âÑÕɫѡÏî¡£ -* Ìí¼Ó[MaskInvert](#MaskInvert)½Úµã¡£ -* Ìí¼Ó[Stroke](#Stroke)½Úµã¡£ -* Ìí¼Ó[MaskPreview](#MaskPreview)½Úµã¡£ -* Ìí¼Ó[ImageOpacity](#ImageOpacity)½Úµã¡£ +* Ìí¼Ó [MaskInvert](#MaskInvert)½Úµã¡£ +* Ìí¼Ó [Stroke](#Stroke)½Úµã¡£ +* Ìí¼Ó [MaskPreview](#MaskPreview)½Úµã¡£ +* Ìí¼Ó [ImageOpacity](#ImageOpacity)½Úµã¡£ * layer_maskÐÞ¸ÄΪ·Ç±ØÑ¡, ĬÈÏʹÓÃlayer_imageµÄalphaͨµÀ£¬ÔÊÐíͨ¹ýÊäÈëmask¸Ä±äÖ®£¬µ«ÊÇ³ß´ç±ØÐëÒ»Ö¡£ -* Ìí¼Ó[ImageBlend](#ImageBlend)½Úµã¡£ -* Ìí¼Ó[OuterGlow](#OuterGlow)½Úµã¡£ -* Ê׸ö½Úµã[DropShadow](#DropShadow)Ìá½»¡£ +* Ìí¼Ó [ImageBlend](#ImageBlend)½Úµã¡£ +* Ìí¼Ó [OuterGlow](#OuterGlow)½Úµã¡£ +* Ê׸ö½Úµã [DropShadow](#DropShadow)Ìá½»¡£ # LayerStyle @@ -365,6 +368,16 @@ * mask: Èç¹ûÓÐmaskÊäÈ룬½«Êä³öËõ·ÅºóµÄmask¡£ * original_size: ͼÏñµÄԭʼ´óСÊý¾Ý£¬ÓÃÓÚºóÐø½Úµã½øÐлָ´¡£ +### ImageScaleRestoreV2 +ImageScaleRestoreµÄV2Éý¼¶°æ¡£ + +½ÚµãÑ¡Ïî˵Ã÷: +![image](image/image_scale_restore_v2_node.png) +ÔÚImageScaleRestore»ù´¡ÉÏ×öÁËÈçϸıä: +* scale_by: ÔÊÐí°´³¤±ß»ò¶Ì±ßÖ¸¶¨³ß´çËõ·Å¡£ +* scale_by_length: scale_by±»ÉèÖÃΪ"longest"ʱ£¬´ËÏ×÷ΪÊÇͼÏñ³¤±ßµÄ³¤¶È; ÉèÖÃΪ"shortest"ʱ£¬×÷Ϊ¶Ì±ßµÄ³¤¶È¡£ + + ### ImageMaskScaleAs ½«Í¼Ïñ»òÕÚÕÖËõ·Åµ½²Î¿¼Í¼Ïñ£¨»òÕÚÕÖ£©µÄ´óС¡£ ![image](image/image_mask_scale_as_example.png) @@ -406,6 +419,15 @@ * mask: Èç¹ûÓÐmaskÊäÈ룬½«Êä³öËõ·ÅºóµÄÕÚÕÖ¡£ * original_size: ͼÏñµÄԭʼ´óСÊý¾Ý£¬ÓÃÓÚºóÐø½Úµã½øÐлָ´¡£ +### ImageScaleByAspectRatioV2 +ImageScaleByAspectRatioµÄV2Éý¼¶°æ + +½ÚµãÑ¡Ïî˵Ã÷: +![image](image/image_scale_by_aspect_ratio_v2_node.png) +ÔÚImageScaleByAspectRatio»ù´¡ÉÏ×öÁËÈçϸıä: +* scale_to_side: ÔÊÐí°´³¤±ß»ò¶Ì±ßÖ¸¶¨³ß´çËõ·Å¡£ +* scale_to_length: scale_by_side±»ÉèÖÃΪ"longest"ʱ£¬´ËÏ×÷ΪÊÇͼÏñ³¤±ßµÄ³¤¶È; ÉèÖÃΪ"shortest"ʱ£¬×÷Ϊ¶Ì±ßµÄ³¤¶È¡£ + ### ImageShift ʹͼƬ²úÉúÎ»ÒÆ¡£´Ë½ÚµãÖ§³ÖÎ»ÒÆ½Ó·ìÕÚÕÖµÄÊä³ö£¬·½±ãÖÆ×÷Á¬ÐøÌùͼ¡£ ![image](image/image_shift_example.png) @@ -586,6 +608,21 @@ ![image](image/image_channel_merge_node.png) * mode: ͨµÀģʽ¡£°üº¬RGBA, YCbCr, LABºÍHSV¡£ +### ImageRemoveAlpha +![image](image/image_remove_alpha_example.png) +ÒÆ³ýͼƬµÄalphaͨµÀ£¬½«Í¼Æ¬×ª»»ÎªRGBģʽ¡£¿ÉÑ¡ÔñÌî³ä±³¾°ÒÔ¼°ÉèÖñ³¾°ÑÕÉ«¡£ + +½ÚµãÑ¡Ïî˵Ã÷: +![image](image/image_remove_alpha_node.png) +* fill_background: ÊÇ·ñÌî³ä±³¾°¡£ +* background_color4: ±³¾°ÑÕÉ«¡£ + + +### ImageCombineAlpha +![image](image/image_combine_alpha_node.png) +½«Í¼Æ¬ÓëÕÚÕֺϲ¢Îª°üº¬alphaͨµÀµÄRGBAģʽµÄͼƬ¡£ + + ### ImageAutoCrop ![image](image/image_auto_crop_example.png) ×Ô¶¯¿Ùͼ²¢°´ÕÕÕÚÕÖ²ÃÇÐͼƬ¡£¿ÉÖ¸¶¨Éú³ÉͼƬµÄ±³¾°ÑÕÉ«¡¢³¤¿í±ÈºÍ´óС¡£Õâ¸ö½ÚµãÊÇΪÉú³ÉѵÁ·Ä£Ð͵ÄÍ¼Æ¬ËØ²Ä¶øÉè¼ÆµÄ¡£ @@ -700,6 +737,27 @@ cropped_mask: * white_point: ±ßÔµºÚÉ«²ÉÑùãÐÖµ¡£ * process_detail: ´Ë´¦ÉèΪFalse½«Ìø¹ý±ßÔµ´¦ÀíÒÔ½ÚÊ¡ÔËÐÐʱ¼ä¡£ +### PersonMaskUltra +ΪÈËÎïÉú³ÉÁ³¡¢Í··¢¡¢ÉíÌ寤·ô¡¢Ò·þ»òÅäÊεÄÕÚÕÖ¡£Óë֮ǰµÄA Person Mask Generator½ÚµãÏà±È£¬Õâ¸ö½Úµã¾ßÓг¬¸ßµÄ±ßԵϸ½Ú¡£ +±¾½ÚµãµÄÄ£ÐÍ´úÂëÀ´×Ô[a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator)£¬±ßÔµ´¦Àí´úÂëÀ´×ÔspacepxlµÄ[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)¡£ + +![image](image/person_mask_ultra_example.png) + +½ÚµãÑ¡Ïî˵Ã÷: +![image](image/person_mask_ultra_node.png) +* face: Á³²¿Ê¶±ð¡£ +* hair: Í··¢Ê¶±ð¡£ +* body: ÉíÌ寤·ôʶ±ð¡£ +* clothes: Ò·þʶ±ð¡£ +* accessories: ÅäÊÎ(ÀýÈç±³°ü)ʶ±ð¡£ +* background: ±³¾°Ê¶±ð¡£ +* confidence: ʶ±ðãÐÖµ£¬¸üµÍµÄÖµ½«Êä³ö¸ü¶àµÄÕÚÕÖ·¶Î§¡£ +* detail_range: ±ßԵϸ½Ú·¶Î§¡£ +* black_point: ±ßÔµºÚÉ«²ÉÑùãÐÖµ¡£ +* white_point: ±ßÔµºÚÉ«²ÉÑùãÐÖµ¡£ +* process_detail: ´Ë´¦ÉèΪFalse½«Ìø¹ý±ßÔµ´¦ÀíÒÔ½ÚÊ¡ÔËÐÐʱ¼ä¡£ + + ### PixelSpread ¶ÔͼÏñµÄÕÚÕÖ±ßÔµ²¿·Ö½øÐÐÏñËØÀ©ÕÅÔ¤´¦Àí£¬¿ÉÓÐЧ¸ÄÉÆÍ¼ÏñºÏ³ÉµÄ±ßÔµ¡£ ![image](image/pixel_spread_example.png) diff --git a/image/image_combine_alpha_node.png b/image/image_combine_alpha_node.png new file mode 100644 index 0000000..3da0655 Binary files /dev/null and b/image/image_combine_alpha_node.png differ diff --git a/image/image_remove_alpha_example.png b/image/image_remove_alpha_example.png new file mode 100644 index 0000000..b5c542d Binary files /dev/null and b/image/image_remove_alpha_example.png differ diff --git a/image/image_remove_alpha_node.png b/image/image_remove_alpha_node.png new file mode 100644 index 0000000..0e48d71 Binary files /dev/null and b/image/image_remove_alpha_node.png differ diff --git a/image/image_scale_by_aspect_ratio_v2_node.png b/image/image_scale_by_aspect_ratio_v2_node.png new file mode 100644 index 0000000..6f4a68e Binary files /dev/null and b/image/image_scale_by_aspect_ratio_v2_node.png differ diff --git a/image/image_scale_restore_v2_node.png b/image/image_scale_restore_v2_node.png new file mode 100644 index 0000000..8730117 Binary files /dev/null and b/image/image_scale_restore_v2_node.png differ diff --git a/image/person_mask_ultra_example.png b/image/person_mask_ultra_example.png new file mode 100644 index 0000000..0e64bc2 Binary files /dev/null and b/image/person_mask_ultra_example.png differ diff --git a/image/person_mask_ultra_node.png b/image/person_mask_ultra_node.png new file mode 100644 index 0000000..88d2df5 Binary files /dev/null and b/image/person_mask_ultra_node.png differ diff --git a/py/image_combine_alpha.py b/py/image_combine_alpha.py new file mode 100644 index 0000000..bac7e34 --- /dev/null +++ b/py/image_combine_alpha.py @@ -0,0 +1,59 @@ +from .imagefunc import * + +NODE_NAME = 'ImageCombineAlpha' + +class ImageCombineAlpha: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV'] + return { + "required": { + "RGB_image": ("IMAGE", ), # + "mask": ("MASK",), # + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("RGBA_image",) + FUNCTION = 'image_combine_alpha' + CATEGORY = '😺dzNodes/LayerUtility' + OUTPUT_NODE = True + + def image_combine_alpha(self, RGB_image, mask): + + ret_images = [] + input_images = [] + input_masks = [] + + for i in RGB_image: + input_images.append(torch.unsqueeze(i, 0)) + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + input_masks.append(torch.unsqueeze(m, 0)) + + max_batch = max(len(input_images), len(input_masks)) + for i in range(max_batch): + _image = input_images[i] if i < len(input_images) else input_images[-1] + _mask = input_masks[i] if i < len(input_masks) else input_masks[-1] + r, g, b, _ = image_channel_split(tensor2pil(_image).convert('RGB'), 'RGB') + ret_image = image_channel_merge((r, g, b, tensor2pil(_mask).convert('L')), 'RGBA') + + ret_images.append(pil2tensor(ret_image)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageCombineAlpha": ImageCombineAlpha +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageCombineAlpha": "LayerUtility: ImageCombineAlpha" +} \ No newline at end of file diff --git a/py/image_remove_alpha.py b/py/image_remove_alpha.py new file mode 100644 index 0000000..70cc713 --- /dev/null +++ b/py/image_remove_alpha.py @@ -0,0 +1,56 @@ +from .imagefunc import * + +NODE_NAME = 'ImageRemoveAlpha' + +class ImageRemoveAlpha: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "RGBA_image": ("IMAGE", ), # + "fill_background": ("BOOLEAN", {"default": False}), + "background_color": ("STRING", {"default": "#000000"}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("RGB_image", ) + FUNCTION = 'image_remove_alpha' + CATEGORY = '😺dzNodes/LayerUtility' + OUTPUT_NODE = True + + def image_remove_alpha(self, RGBA_image, fill_background, background_color): + + ret_images = [] + + for i in RGBA_image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i) + if _image.mode != "RGBA": + log(f"Error: {NODE_NAME} skipped, because the input image is not RGBA.", message_type='error') + return (RGBA_image) + if fill_background: + alpha = _image.split()[-1] + ret_image = Image.new('RGB', size=_image.size, color=background_color) + ret_image.paste(_image, mask=alpha) + ret_images.append(pil2tensor(ret_image)) + else: + ret_images.append(pil2tensor(tensor2pil(i).convert('RGB'))) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), ) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageRemoveAlpha": ImageRemoveAlpha +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageRemoveAlpha": "LayerUtility: ImageRemoveAlpha" +} \ No newline at end of file diff --git a/py/image_scale_by_aspect_ratio_v2.py b/py/image_scale_by_aspect_ratio_v2.py new file mode 100644 index 0000000..5f40b66 --- /dev/null +++ b/py/image_scale_by_aspect_ratio_v2.py @@ -0,0 +1,157 @@ +import torch + +from .imagefunc import * + +NODE_NAME = 'ImageScaleByAspectRatio V2' + +class ImageScaleByAspectRatioV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16'] + fit_mode = ['letterbox', 'crop', 'fill'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + multiple_list = ['8', '16', 'None'] + scale_to_list = ['None', 'longest', 'shortest'] + return { + "required": { + "aspect_ratio": (ratio_list,), + "proportional_width": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}), + "proportional_height": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}), + "fit": (fit_mode,), + "method": (method_mode,), + "round_to_multiple": (multiple_list,), + "scale_to_side": (scale_to_list,), # 是å�¦æŒ‰é•¿è¾¹ç¼©æ”¾ + "scale_to_length": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}), + }, + "optional": { + "image": ("IMAGE",), # + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX",) + RETURN_NAMES = ("image", "mask", "original_size") + FUNCTION = 'image_scale_by_aspect_ratio' + CATEGORY = '😺dzNodes/LayerUtility' + OUTPUT_NODE = True + + def image_scale_by_aspect_ratio(self, aspect_ratio, proportional_width, proportional_height, + fit, method, round_to_multiple, scale_to_side, scale_to_length, + image=None, mask = None, + ): + orig_images = [] + orig_masks = [] + orig_width = 0 + orig_height = 0 + target_width = 0 + target_height = 0 + ratio = 1.0 + ret_images = [] + ret_masks = [] + if image is not None: + for i in image: + i = torch.unsqueeze(i, 0) + orig_images.append(i) + orig_width, orig_height = tensor2pil(orig_images[0]).size + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + m = torch.unsqueeze(m, 0) + orig_masks.append(m) + _width, _height = tensor2pil(orig_masks[0]).size + if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height): + log(f"Error: {NODE_NAME} skipped, because the mask is does'nt match image.", message_type='error') + return (None, None,) + elif orig_width + orig_height == 0: + orig_width = _width + orig_height = _height + + if orig_width + orig_height == 0: + log(f"Error: {NODE_NAME} skipped, because the image or mask at least one must be input.", message_type='error') + return (None, None,) + + if aspect_ratio == 'original': + ratio = orig_width / orig_height + elif aspect_ratio == 'custom': + ratio = proportional_width / proportional_height + else: + s = aspect_ratio.split(":") + ratio = int(s[0]) / int(s[1]) + + # calculate target width and height + if orig_width > orig_height: + if scale_to_side == 'longest': + target_width = scale_to_length + target_height = int(target_width / ratio) + elif scale_to_side == 'shortest': + target_height = scale_to_length + target_width = int(target_height * ratio) + else: + target_width = orig_width + target_height = int(target_width / ratio) + else: + if scale_to_side == 'longest': + target_height = scale_to_length + target_width = int(target_height * ratio) + elif scale_to_side == 'shortest': + target_width = scale_to_length + target_height = int(target_width / ratio) + else: + target_height = orig_height + target_width = int(target_height * ratio) + + if round_to_multiple != 'None': + multiple = int(round_to_multiple) + target_width = num_round_to_multiple(target_width, multiple) + target_height = num_round_to_multiple(target_height, multiple) + + _mask = Image.new('L', size=(target_width, target_height), color='black') + _image = Image.new('RGB', size=(target_width, target_height), color='black') + + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + if len(orig_images) > 0: + for i in orig_images: + _image = tensor2pil(i).convert('RGB') + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler) + ret_images.append(pil2tensor(_image)) + if len(orig_masks) > 0: + for m in orig_masks: + _mask = tensor2pil(m).convert('L') + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L') + ret_masks.append(image2mask(_mask)) + if len(ret_images) > 0 and len(ret_masks) >0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + elif len(ret_images) > 0 and len(ret_masks) == 0: + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), None,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (None, torch.cat(ret_masks, dim=0),) + else: + log(f"Error: {NODE_NAME} skipped, because the available image or mask is not found.", message_type='error') + return (None, None,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageScaleByAspectRatio V2": ImageScaleByAspectRatioV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageScaleByAspectRatio V2": "LayerUtility: ImageScaleByAspectRatio V2" +} \ No newline at end of file diff --git a/py/image_scale_restore_v2.py b/py/image_scale_restore_v2.py new file mode 100644 index 0000000..721e900 --- /dev/null +++ b/py/image_scale_restore_v2.py @@ -0,0 +1,119 @@ +from .imagefunc import * + +NODE_NAME = 'ImageScaleRestore V2' + +class ImageScaleRestoreV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + scale_by_list = ['by_scale', 'longest', 'shortest'] + return { + "required": { + "image": ("IMAGE", ), # + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "method": (method_mode,), + "scale_by": (scale_by_list,), # 是å�¦æŒ‰é•¿è¾¹ç¼©æ”¾ + "scale_by_length": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + "original_size": ("BOX",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX",) + RETURN_NAMES = ("image", "mask", "original_size") + FUNCTION = 'image_scale_restore' + CATEGORY = '😺dzNodes/LayerUtility' + OUTPUT_NODE = True + + def image_scale_restore(self, image, scale, method, + scale_by, scale_by_length, + mask = None, original_size = None + ): + + l_images = [] + l_masks = [] + ret_images = [] + ret_masks = [] + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(l_images), len(l_masks)) + + orig_width, orig_height = tensor2pil(l_images[0]).size + if original_size is not None: + target_width = original_size[0] + target_height = original_size[1] + else: + target_width = int(orig_width * scale) + target_height = int(orig_height * scale) + if scale_by == 'longest': + if orig_width > orig_height: + target_width = scale_by_length + target_height = int(target_width * orig_height / orig_width) + else: + target_height = scale_by_length + target_width = int(target_height * orig_width / orig_height) + if scale_by == 'shortest': + if orig_width < orig_height: + target_width = scale_by_length + target_height = int(target_width * orig_height / orig_width) + else: + target_height = scale_by_length + target_width = int(target_height * orig_width / orig_height) + if target_width < 4: + target_width = 4 + if target_height < 4: + target_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + for i in range(max_batch): + + _image = l_images[i] if i < len(l_images) else l_images[-1] + + _canvas = tensor2pil(_image).convert('RGB') + ret_image = _canvas.resize((target_width, target_height), resize_sampler) + ret_mask = Image.new('L', size=ret_image.size, color='white') + if mask is not None: + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + ret_mask = _mask.resize((target_width, target_height), resize_sampler) + + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height],) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageScaleRestore V2": ImageScaleRestoreV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageScaleRestore V2": "LayerUtility: ImageScaleRestore V2" +} \ No newline at end of file diff --git a/py/mask_edge_ultrl_detail.py b/py/mask_edge_ultrl_detail.py index 6311864..041dad2 100644 --- a/py/mask_edge_ultrl_detail.py +++ b/py/mask_edge_ultrl_detail.py @@ -17,7 +17,7 @@ class MaskEdgeUltraDetail: "mask_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), "fix_gap": ("INT", {"default": 0, "min": 0, "max": 32, "step": 1}), "fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}), - "detail_range": ("INT", {"default": 12, "min": 0, "max": 256, "step": 1}), + "detail_range": ("INT", {"default": 12, "min": 1, "max": 256, "step": 1}), "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}), "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), }, diff --git a/py/person_mask_Ultra.py b/py/person_mask_Ultra.py new file mode 100644 index 0000000..f50d5e2 --- /dev/null +++ b/py/person_mask_Ultra.py @@ -0,0 +1,159 @@ +from .imagefunc import * +from functools import reduce +import wget +import mediapipe as mp +import folder_paths +from .segment_anything_func import * + +NODE_NAME = 'PersonMaskUltra' + +def get_a_person_mask_generator_model_path() -> str: + model_folder_name = 'mediapipe' + model_name = 'selfie_multiclass_256x256.tflite' + + model_folder_path = os.path.join(folder_paths.models_dir, model_folder_name) + model_file_path = os.path.join(model_folder_path, model_name) + + if not os.path.exists(model_file_path): + model_url = f'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/{model_name}' + print(f"Downloading '{model_name}' model") + os.makedirs(model_folder_path, exist_ok=True) + wget.download(model_url, model_file_path) + + return model_file_path + + +class PersonMaskUltra: + + def __init__(self): + # download the model if we need it + get_a_person_mask_generator_model_path() + + @classmethod + def INPUT_TYPES(self): + return { + "required": + { + "images": ("IMAGE",), + "face": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}), + "hair": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "body": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "clothes": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "accessories": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "background": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},), + "detail_range": ("INT", {"default": 16, "min": 1, "max": 256, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), + "process_detail": ("BOOLEAN", {"default": True}), + }, + "optional": + { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = 'person_mask_ultra' + CATEGORY = '😺dzNodes/LayerMask' + OUTPUT_NODE = True + + def get_mediapipe_image(self, image: Image) -> mp.Image: + # Convert image to NumPy array + numpy_image = np.asarray(image) + image_format = mp.ImageFormat.SRGB + # Convert BGR to RGB (if necessary) + if numpy_image.shape[-1] == 4: + image_format = mp.ImageFormat.SRGBA + elif numpy_image.shape[-1] == 3: + image_format = mp.ImageFormat.SRGB + numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB) + return mp.Image(image_format=image_format, data=numpy_image) + + def person_mask_ultra(self, images, face, hair, body, clothes, + accessories, background, confidence, + detail_range, black_point, white_point, process_detail): + + a_person_mask_generator_model_path = get_a_person_mask_generator_model_path() + a_person_mask_generator_model_buffer = None + with open(a_person_mask_generator_model_path, "rb") as f: + a_person_mask_generator_model_buffer = f.read() + image_segmenter_base_options = mp.tasks.BaseOptions(model_asset_buffer=a_person_mask_generator_model_buffer) + options = mp.tasks.vision.ImageSegmenterOptions( + base_options=image_segmenter_base_options, + running_mode=mp.tasks.vision.RunningMode.IMAGE, + output_category_mask=True) + # Create the image segmenter + ret_images = [] + ret_masks = [] + with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter: + for image in images: + # image = torch.unsqueeze(image, 0) + orig_image = tensor2pil(image.unsqueeze(0)).convert('RGB') + # Convert the Tensor to a PIL image + i = 255. * image.cpu().numpy() + image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + # create our foreground and background arrays for storing the mask results + mask_background_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + # Retrieve the masks for the segmented image + media_pipe_image = self.get_mediapipe_image(image=image_pil) + segmented_masks = segmenter.segment(media_pipe_image) + masks = [] + if background: + masks.append(segmented_masks.confidence_masks[0]) + if hair: + masks.append(segmented_masks.confidence_masks[1]) + if body: + masks.append(segmented_masks.confidence_masks[2]) + if face: + masks.append(segmented_masks.confidence_masks[3]) + if clothes: + masks.append(segmented_masks.confidence_masks[4]) + if accessories: + masks.append(segmented_masks.confidence_masks[5]) + image_data = media_pipe_image.numpy_view() + image_shape = image_data.shape + # convert the image shape from "rgb" to "rgba" aka add the alpha channel + if image_shape[-1] == 3: + image_shape = (image_shape[0], image_shape[1], 4) + mask_background_array = np.zeros(image_shape, dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros(image_shape, dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + mask_arrays = [] + if len(masks) == 0: + mask_arrays.append(mask_background_array) + else: + for i, mask in enumerate(masks): + condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence + mask_array = np.where(condition, mask_foreground_array, mask_background_array) + mask_arrays.append(mask_array) + # Merge our masks taking the maximum from each + merged_mask_arrays = reduce(np.maximum, mask_arrays) + # Create the image + mask_image = Image.fromarray(merged_mask_arrays) + # convert PIL image to tensor image + tensor_mask = mask_image.convert("RGB") + tensor_mask = np.array(tensor_mask).astype(np.float32) / 255.0 + tensor_mask = torch.from_numpy(tensor_mask)[None,] + tensor_mask = tensor_mask.squeeze(3)[..., 0] + _mask = tensor2pil(tensor_mask).convert('L') + if process_detail: + _mask = tensor2pil(mask_edge_detail(image.unsqueeze(0), pil2tensor(_mask), detail_range, black_point, white_point)) + ret_image = RGB2RGBA(orig_image, _mask) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerMask: PersonMaskUltra": PersonMaskUltra +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: PersonMaskUltra": "LayerMask: PersonMaskUltra" +} \ No newline at end of file diff --git a/py/rembg_ultra.py b/py/rembg_ultra.py index cb697f2..3e22798 100644 --- a/py/rembg_ultra.py +++ b/py/rembg_ultra.py @@ -12,7 +12,7 @@ class RemBgUltra: return { "required": { "image": ("IMAGE",), - "detail_range": ("INT", {"default": 8, "min": 0, "max": 256, "step": 1}), + "detail_range": ("INT", {"default": 8, "min": 1, "max": 256, "step": 1}), "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}), "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), "process_detail": ("BOOLEAN", {"default": True}), diff --git a/py/segment_anything_ultra.py b/py/segment_anything_ultra.py index e9c487c..3d7a51f 100644 --- a/py/segment_anything_ultra.py +++ b/py/segment_anything_ultra.py @@ -3,6 +3,9 @@ from .segment_anything_func import * NODE_NAME = 'SegmentAnythingUltra' +SAM_MODEL = None +DINO_MODEL = None + class SegmentAnythingUltra: def __init__(self): pass @@ -16,7 +19,7 @@ class SegmentAnythingUltra: "sam_model": (list_sam_model(), ), "grounding_dino_model": (list_groundingdino_model(),), "threshold": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.01}), - "detail_range": ("INT", {"default": 16, "min": 0, "max": 256, "step": 1}), + "detail_range": ("INT", {"default": 16, "min": 1, "max": 256, "step": 1}), "black_point": ("FLOAT", {"default": 0.15, "min": 0.01, "max": 0.98, "step": 0.01}), "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}), "process_detail": ("BOOLEAN", {"default": True}), @@ -30,23 +33,25 @@ class SegmentAnythingUltra: RETURN_NAMES = ("image", "mask", ) FUNCTION = "segment_anything_ultra" CATEGORY = '😺dzNodes/LayerMask' - + + def segment_anything_ultra(self, image, sam_model, grounding_dino_model, threshold, detail_range, black_point, white_point, process_detail, prompt, ): - - sam_model = load_sam_model(sam_model) - dino_model = load_groundingdino_model(grounding_dino_model) + global SAM_MODEL + global DINO_MODEL + if SAM_MODEL is None: SAM_MODEL = load_sam_model(sam_model) + if DINO_MODEL is None: DINO_MODEL = load_groundingdino_model(grounding_dino_model) ret_images = [] ret_masks = [] for i in image: i = torch.unsqueeze(i, 0) item = tensor2pil(i).convert('RGBA') - boxes = groundingdino_predict(dino_model, item, prompt, threshold) + boxes = groundingdino_predict(DINO_MODEL, item, prompt, threshold) if boxes.shape[0] == 0: break - (_, _mask) = sam_segment(sam_model, item, boxes) + (_, _mask) = sam_segment(SAM_MODEL, item, boxes) _mask = _mask[0] if process_detail: _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range, black_point, white_point)) diff --git a/workflow/image_remove_alpha & image_combine_alpha_example.json b/workflow/image_remove_alpha & image_combine_alpha_example.json new file mode 100644 index 0000000..d9516e5 --- /dev/null +++ b/workflow/image_remove_alpha & image_combine_alpha_example.json @@ -0,0 +1,361 @@ +{ + "last_node_id": 41, + "last_link_id": 53, + "nodes": [ + { + "id": 4, + "type": "LoadImage", + "pos": [ + 432, + 374 + ], + "size": [ + 318.6980670166016, + 271.3352774047852 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 39 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "1344x768_hair (31).png", + "image" + ] + }, + { + "id": 26, + "type": "PreviewImage", + "pos": [ + 796, + 662 + ], + "size": [ + 346.13137573242193, + 223.26862304687506 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 40 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 31, + "type": "LayerMask: RemBgUltra", + "pos": [ + 811, + 431 + ], + "size": { + "0": 315, + "1": 150 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 39 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 40, + 42 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "mask", + "type": "MASK", + "links": [ + 52 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LayerMask: RemBgUltra" + }, + "widgets_values": [ + 8, + 0.01, + 0.99, + false + ] + }, + { + "id": 36, + "type": "PreviewImage", + "pos": [ + 1197, + 661 + ], + "size": [ + 343.2526589385732, + 223.19213123163888 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 50 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 38, + "type": "PreviewImage", + "pos": [ + 1605, + 655 + ], + "size": [ + 344.6191846358872, + 226.4373271090924 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 51 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 32, + "type": "LayerUtility: ImageRemoveAlpha", + "pos": [ + 1242, + 478 + ], + "size": { + "0": 254.40000915527344, + "1": 82 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "RGBA_image", + "type": "IMAGE", + "link": 42 + } + ], + "outputs": [ + { + "name": "RGB_image", + "type": "IMAGE", + "links": [ + 45, + 50 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "LayerUtility: ImageRemoveAlpha" + }, + "widgets_values": [ + true, + "#0000F0" + ] + }, + { + "id": 39, + "type": "Reroute", + "pos": [ + 1339, + 381 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 52 + } + ], + "outputs": [ + { + "name": "", + "type": "MASK", + "links": [ + 53 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 34, + "type": "LayerUtility: ImageCombineAlpha", + "pos": [ + 1642, + 487 + ], + "size": { + "0": 260.3999938964844, + "1": 46 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "RGB_image", + "type": "IMAGE", + "link": 45 + }, + { + "name": "mask", + "type": "MASK", + "link": 53 + } + ], + "outputs": [ + { + "name": "RGBA_image", + "type": "IMAGE", + "links": [ + 51 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "LayerUtility: ImageCombineAlpha" + } + } + ], + "links": [ + [ + 39, + 4, + 0, + 31, + 0, + "IMAGE" + ], + [ + 40, + 31, + 0, + 26, + 0, + "IMAGE" + ], + [ + 42, + 31, + 0, + 32, + 0, + "IMAGE" + ], + [ + 45, + 32, + 0, + 34, + 0, + "IMAGE" + ], + [ + 50, + 32, + 0, + 36, + 0, + "IMAGE" + ], + [ + 51, + 34, + 0, + 38, + 0, + "IMAGE" + ], + [ + 52, + 31, + 1, + 39, + 0, + "*" + ], + [ + 53, + 39, + 0, + 34, + 1, + "MASK" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file