diff --git a/README.md b/README.md index 0304d2ed..55078fc2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ - 在线体验: [![SwanHub Demo](https://img.shields.io/static/v1?label=Demo&message=SwanHub%20Demo&color=blue)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)、[![Spaces](https://img.shields.io/badge/🤗-Open%20in%20Spaces-blue)](https://huggingface.co/spaces/TheEeeeLin/HivisionIDPhotos) -- 2024.09.09: 增加新的**抠图模型** [BiRefNet-v1-lite](https://github.com/ZhengPeng7/BiRefNet) | Gradio增加高级参数设置选项卡 +- 2024.09.09: 增加新的**抠图模型** [BiRefNet-v1-lite](https://github.com/ZhengPeng7/BiRefNet) | Gradio增加高级参数设置选项卡 | Gradio增加**水印**选项卡 - 2024.09.08: 增加新的**抠图模型** [RMBG-1.4](https://huggingface.co/briaai/RMBG-1.4) | **ComfyUI工作流** - [HivisionIDPhotos-ComfyUI](https://github.com/AIFSH/HivisionIDPhotos-ComfyUI) 贡献 by [AIFSH](https://github.com/AIFSH/HivisionIDPhotos-ComfyUI) - 2024.09.07: 增加**人脸检测API选项** [Face++](docs/face++_CN.md),实现更高精度的人脸检测 - 2024.09.06: 增加新的抠图模型 [modnet_photographic_portrait_matting.onnx](https://github.com/ZHKKKe/MODNet) diff --git a/app.py b/app.py index 06ac1101..926676f7 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,9 @@ import numpy as np from demo.utils import csv_to_size_list import argparse -from PIL import Image, ImageDraw, ImageFont +from PIL import Image +from hivision.plugin.watermark import Watermarker, WatermarkerStyles + # 获取尺寸列表 root_dir = os.path.dirname(os.path.abspath(__file__)) @@ -63,6 +65,11 @@ def idphoto_inference( matting_model_option, watermark_option, watermark_text, + watermark_text_color, + watermark_text_size, + watermark_text_opacity, + watermark_text_angle, + watermark_text_space, face_detect_option, head_measure_ratio=0.2, # head_height_ratio=0.45, @@ -237,11 +244,13 @@ def idphoto_inference( ) ) + # 如果只换底,就不生成排版照 if ( idphoto_json["size_mode"] == text_lang_map[language]["Only Change Background"] ): result_layout_image = gr.update(visible=False) + # 如果是尺寸列表或自定义尺寸,则生成排版照 else: typography_arr, typography_rotate = generate_layout_photo( input_height=idphoto_json["size"][0], @@ -251,7 +260,15 @@ def idphoto_inference( if watermark_option == "添加" or watermark_option == "Add": result_layout_image = gr.update( value=generate_layout_image( - add_watermark(result_image_standard, watermark_text), + add_watermark( + image=result_image_standard, + text=watermark_text, + size=watermark_text_size, + opacity=watermark_text_opacity, + angle=watermark_text_angle, + space=watermark_text_space, + color=watermark_text_color, + ), typography_arr, typography_rotate, height=idphoto_json["size"][0], @@ -289,8 +306,24 @@ def idphoto_inference( # Add watermark if selected if watermark_option == "添加" or watermark_option == "Add": - result_image_standard = add_watermark(result_image_standard, watermark_text) - result_image_hd = add_watermark(result_image_hd, watermark_text) + result_image_standard = add_watermark( + image=result_image_standard, + text=watermark_text, + size=watermark_text_size, + opacity=watermark_text_opacity, + angle=watermark_text_angle, + space=watermark_text_space, + color=watermark_text_color, + ) + result_image_hd = add_watermark( + image=result_image_hd, + text=watermark_text, + size=watermark_text_size, + opacity=watermark_text_opacity, + angle=watermark_text_angle, + space=watermark_text_space, + color=watermark_text_color, + ) if output_image_path: result_message = { @@ -313,51 +346,23 @@ def idphoto_inference( # Add this function to handle watermark addition -def add_watermark(image, text): - if not text.strip(): - return image - - img = Image.fromarray(image) - img = img.convert("RGBA") - - font_size = 30 - try: - font = ImageFont.truetype("arial.ttf", font_size) - except IOError: - font = ImageFont.load_default() - - # 计算水印文本的大小 - bbox = font.getbbox(text) - text_width = bbox[2] - bbox[0] - text_height = bbox[3] - bbox[1] - - # 创建一个新的图像来绘制倾斜的水印 - watermark = Image.new("RGBA", (img.width * 3, img.height * 3), (0, 0, 0, 0)) - watermark_draw = ImageDraw.Draw(watermark) - - # 在更大的画布上重复绘制水印文本 - for y in range(-img.height, watermark.height, text_height * 3): - for x in range(-img.width, watermark.width, text_width * 3): - watermark_draw.text( - (x, y), text, font=font, fill=(255, 255, 255, 64) - ) # 半透明白色 (255, 255, 255, 64) - - # 旋转水印 - rotated_watermark = watermark.rotate(45, expand=True) - - # 将旋转后的水印裁剪到原始图像大小 - crop_left = (rotated_watermark.width - img.width) // 2 - crop_top = (rotated_watermark.height - img.height) // 2 - crop_right = crop_left + img.width - crop_bottom = crop_top + img.height - cropped_watermark = rotated_watermark.crop( - (crop_left, crop_top, crop_right, crop_bottom) +def add_watermark( + image: np.ndarray, text, size=50, opacity=0.5, angle=45, color="#8B8B1B", space=75 +): + image = Image.fromarray(image) + # 创建 Watermarker 实例 + watermarker = Watermarker( + input_image=image, + text=text, + style=WatermarkerStyles.STRIPED, + angle=angle, + color=color, + opacity=opacity, + size=size, + space=space, ) - - # 将水印叠加到原始图像上 - img = Image.alpha_composite(img, cropped_watermark) - - return np.array(img.convert("RGB")) + # 返回带水印的图片 + return np.array(watermarker.image.convert("RGB")) if __name__ == "__main__": @@ -546,7 +551,7 @@ def load_description(fp): interactive=True, ) - with gr.Tab("水印") as key_parameter_tab: + with gr.Tab("水印") as watermark_parameter_tab: # 左: 水印 watermark_options = gr.Radio( choices=watermark_CN, @@ -555,22 +560,79 @@ def load_description(fp): elem_id="watermark", ) - # 左:水印文字 - watermark_text_options = gr.Text( - max_length=5, - label="水印文字(最多5)", - value="", - elem_id="watermark_text", - visible=False, + with gr.Row(): + # 左:水印文字 + watermark_text_options = gr.Text( + max_length=10, + label="水印文字", + value="Hello", + placeholder="请输入水印文字(最多10个字符)", + elem_id="watermark_text", + interactive=False, + ) + # 水印颜色 + watermark_text_color = gr.ColorPicker( + label="水印颜色", + interactive=False, + value="#FFFFFF", + ) + + # 文字大小 + watermark_text_size = gr.Slider( + minimum=10, + maximum=100, + value=20, + label="文字大小", + interactive=False, + step=1, + ) + + # 文字透明度 + watermark_text_opacity = gr.Slider( + minimum=0, + maximum=1, + value=0.15, + label="水印透明度", + interactive=False, + step=0.01, + ) + + # 文字角度 + watermark_text_angle = gr.Slider( + minimum=0, + maximum=360, + value=30, + label="水印角度", + interactive=False, + step=1, + ) + + # 文字间距 + watermark_text_space = gr.Slider( + minimum=10, + maximum=200, + value=25, + label="水印间距", + interactive=False, + step=1, ) def update_watermark_text_visibility(choice): - return gr.update(visible=choice == "添加") + return [ + gr.update(interactive=(choice == "添加" or choice == "Add")) + ] * 6 watermark_options.change( fn=update_watermark_text_visibility, inputs=[watermark_options], - outputs=[watermark_text_options], + outputs=[ + watermark_text_options, + watermark_text_color, + watermark_text_size, + watermark_text_opacity, + watermark_text_angle, + watermark_text_space, + ], ) img_but = gr.Button("开始制作") @@ -646,6 +708,16 @@ def change_language(language): top_distance_option: gr.update(label="头距顶距离"), key_parameter_tab: gr.update(label="核心参数"), advance_parameter_tab: gr.update(label="高级参数"), + watermark_parameter_tab: gr.update(label="水印"), + watermark_text_options: gr.update(label="水印文字"), + watermark_text_color: gr.update(label="水印颜色"), + watermark_text_size: gr.update(label="文字大小"), + watermark_text_opacity: gr.update(label="水印透明度"), + watermark_text_angle: gr.update(label="水印角度"), + watermark_text_space: gr.update(label="水印间距"), + watermark_options: gr.update( + label="水印", value="不添加", choices=watermark_CN + ), } elif language == "English": @@ -690,6 +762,16 @@ def change_language(language): top_distance_option: gr.update(label="Top distance"), key_parameter_tab: gr.update(label="Key Parameters"), advance_parameter_tab: gr.update(label="Advance Parameters"), + watermark_parameter_tab: gr.update(label="Watermark"), + watermark_text_options: gr.update(label="Text"), + watermark_text_color: gr.update(label="Color"), + watermark_text_size: gr.update(label="Size"), + watermark_text_opacity: gr.update(label="Opacity"), + watermark_text_angle: gr.update(label="Angle"), + watermark_text_space: gr.update(label="Space"), + watermark_options: gr.update( + label="Watermark", value="Not Add", choices=watermark_EN + ), } def change_color(colors): @@ -750,6 +832,14 @@ def change_image_kb(image_kb_option): top_distance_option, key_parameter_tab, advance_parameter_tab, + watermark_parameter_tab, + watermark_text_options, + watermark_text_color, + watermark_text_size, + watermark_text_opacity, + watermark_text_angle, + watermark_text_space, + watermark_options, ], ) @@ -786,6 +876,11 @@ def change_image_kb(image_kb_option): matting_model_options, watermark_options, watermark_text_options, + watermark_text_color, + watermark_text_size, + watermark_text_opacity, + watermark_text_angle, + watermark_text_space, face_detect_model_options, head_measure_ratio_option, top_distance_option, diff --git "a/hivision/plugin/font/\351\235\222\351\270\237\345\215\216\345\205\211\347\256\200\347\220\245\347\217\200.ttf" "b/hivision/plugin/font/\351\235\222\351\270\237\345\215\216\345\205\211\347\256\200\347\220\245\347\217\200.ttf" new file mode 100644 index 00000000..a1a8b8db Binary files /dev/null and "b/hivision/plugin/font/\351\235\222\351\270\237\345\215\216\345\205\211\347\256\200\347\220\245\347\217\200.ttf" differ diff --git a/hivision/plugin/watermark.py b/hivision/plugin/watermark.py new file mode 100644 index 00000000..99995c3b --- /dev/null +++ b/hivision/plugin/watermark.py @@ -0,0 +1,228 @@ +import enum +import os +import math +import textwrap +from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageChops +import os + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +class WatermarkerStyles(enum.Enum): + """水印样式""" + + STRIPED = 1 # 斜向重复 + CENTRAL = 2 # 居中 + + +class Watermarker(object): + """图片水印工具""" + + def __init__( + self, + input_image: Image.Image, + text: str, + style: WatermarkerStyles, + angle=30, + color="#8B8B1B", + font_file="青鸟华光简琥珀.ttf", + opacity=0.15, + size=50, + space=75, + chars_per_line=8, + font_height_crop=1.2, + ): + """_summary_ + + Parameters + ---------- + input_image : Image.Image + PIL图片对象 + text : str + 水印文字 + style : WatermarkerStyles + 水印样式 + angle : int, optional + 水印角度, by default 30 + color : str, optional + 水印颜色, by default "#8B8B1B" + font_file : str, optional + 字体文件, by default "青鸟华光简琥珀.ttf" + font_height_crop : float, optional + 字体高度裁剪比例, by default 1.2 + opacity : float, optional + 水印透明度, by default 0.15 + size : int, optional + 字体大小, by default 50 + space : int, optional + 水印间距, by default 75 + chars_per_line : int, optional + 每行字符数, by default 8 + """ + self.input_image = input_image + self.text = text + self.style = style + self.angle = angle + self.color = color + self.font_file = os.path.join(base_path, "font", font_file) + self.font_height_crop = font_height_crop + self.opacity = opacity + self.size = size + self.space = space + self.chars_per_line = chars_per_line + self._result_image = None + + @staticmethod + def set_image_opacity(image: Image, opacity: float): + alpha = image.split()[3] + alpha = ImageEnhance.Brightness(alpha).enhance(opacity) + image.putalpha(alpha) + return image + + @staticmethod + def crop_image_edge(image: Image): + bg = Image.new(mode="RGBA", size=image.size) + diff = ImageChops.difference(image, bg) + bbox = diff.getbbox() + if bbox: + return image.crop(bbox) + return image + + def _add_mark_striped(self): + origin_image = self.input_image.convert("RGBA") + width = len(self.text) * self.size + height = round(self.size * self.font_height_crop) + watermark_image = Image.new(mode="RGBA", size=(width, height)) + draw_table = ImageDraw.Draw(watermark_image) + draw_table.text( + (0, 0), + self.text, + fill=self.color, + font=ImageFont.truetype(self.font_file, size=self.size), + ) + watermark_image = Watermarker.crop_image_edge(watermark_image) + Watermarker.set_image_opacity(watermark_image, self.opacity) + + c = int(math.sqrt(origin_image.size[0] ** 2 + origin_image.size[1] ** 2)) + watermark_mask = Image.new(mode="RGBA", size=(c, c)) + y, idx = 0, 0 + while y < c: + x = -int((watermark_image.size[0] + self.space) * 0.5 * idx) + idx = (idx + 1) % 2 + while x < c: + watermark_mask.paste(watermark_image, (x, y)) + x += watermark_image.size[0] + self.space + y += watermark_image.size[1] + self.space + + watermark_mask = watermark_mask.rotate(self.angle) + origin_image.paste( + watermark_mask, + (int((origin_image.size[0] - c) / 2), int((origin_image.size[1] - c) / 2)), + mask=watermark_mask.split()[3], + ) + return origin_image + + def _add_mark_central(self): + origin_image = self.input_image.convert("RGBA") + text_lines = textwrap.wrap(self.text, width=self.chars_per_line) + text = "\n".join(text_lines) + width = len(text) * self.size + height = round(self.size * self.font_height_crop * len(text_lines)) + watermark_image = Image.new(mode="RGBA", size=(width, height)) + draw_table = ImageDraw.Draw(watermark_image) + draw_table.text( + (0, 0), + text, + fill=self.color, + font=ImageFont.truetype(self.font_file, size=self.size), + ) + watermark_image = Watermarker.crop_image_edge(watermark_image) + Watermarker.set_image_opacity(watermark_image, self.opacity) + + c = int(math.sqrt(origin_image.size[0] ** 2 + origin_image.size[1] ** 2)) + watermark_mask = Image.new(mode="RGBA", size=(c, c)) + watermark_mask.paste( + watermark_image, + ( + int((watermark_mask.width - watermark_image.width) / 2), + int((watermark_mask.height - watermark_image.height) / 2), + ), + ) + watermark_mask = watermark_mask.rotate(self.angle) + + origin_image.paste( + watermark_mask, + ( + int((origin_image.width - watermark_mask.width) / 2), + int((origin_image.height - watermark_mask.height) / 2), + ), + mask=watermark_mask.split()[3], + ) + return origin_image + + @property + def image(self): + if not self._result_image: + if self.style == WatermarkerStyles.STRIPED: + self._result_image = self._add_mark_striped() + elif self.style == WatermarkerStyles.CENTRAL: + self._result_image = self._add_mark_central() + return self._result_image + + def save(self, file_path: str, image_format: str = "png"): + with open(file_path, "wb") as f: + self.image.save(f, image_format) + + +# Gradio 接口 +def watermark_image( + image, + text, + style, + angle, + color, + opacity, + size, + space, +): + # 创建 Watermarker 实例 + watermarker = Watermarker( + input_image=image, + text=text, + style=( + WatermarkerStyles.STRIPED + if style == "STRIPED" + else WatermarkerStyles.CENTRAL + ), + angle=angle, + color=color, + opacity=opacity, + size=size, + space=space, + ) + + # 返回带水印的图片 + return watermarker.image + + +if __name__ == "__main__": + import gradio as gr + + iface = gr.Interface( + fn=watermark_image, + inputs=[ + gr.Image(type="pil", label="上传图片", height=400), + gr.Textbox(label="水印文字"), + gr.Radio(choices=["STRIPED", "CENTRAL"], label="水印样式"), + gr.Slider(minimum=0, maximum=360, value=30, label="水印角度"), + gr.ColorPicker(label="水印颜色"), + gr.Slider(minimum=0, maximum=1, value=0.15, label="水印透明度"), + gr.Slider(minimum=10, maximum=100, value=50, label="字体大小"), + gr.Slider(minimum=10, maximum=200, value=75, label="水印间距"), + ], + outputs=gr.Image(type="pil", label="带水印的图片", height=400), + title="图片水印工具", + description="上传一张图片,添加水印并下载。", + ) + + iface.launch()