From 54783a2991f34f62810d6e97a10c4c3455eae661 Mon Sep 17 00:00:00 2001 From: New Bing Date: Thu, 14 Dec 2023 01:02:11 +0800 Subject: [PATCH 1/6] Update Inference.py add black and white arguments. --- Inference.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Inference.py b/Inference.py index 61b70fc..3e7604f 100644 --- a/Inference.py +++ b/Inference.py @@ -68,6 +68,9 @@ def parse_args(): parser.add_argument( "--withContours", type=bool, default=False, help="draw the edges of the masks" ) + parser.add_argument( + "--bwMask", type=str, default="", help="black or white mask mode, reverse for background and foreground" + ) return parser.parse_args() @@ -112,6 +115,7 @@ def main(args): point_label = point_label, withContours=args.withContours, better_quality=args.better_quality, + bwMask=args.bwMask ) From fcc9f97f07654d3e8bf12b882fc9374d1c359483 Mon Sep 17 00:00:00 2001 From: New Bing Date: Thu, 14 Dec 2023 01:03:05 +0800 Subject: [PATCH 2/6] Update prompt.py support output white/black mask image --- fastsam/prompt.py | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/fastsam/prompt.py b/fastsam/prompt.py index dde50ac..26a49bc 100644 --- a/fastsam/prompt.py +++ b/fastsam/prompt.py @@ -6,6 +6,7 @@ import torch from .utils import image_to_np_ndarray from PIL import Image +import logging try: import clip # for linear_assignment @@ -100,7 +101,8 @@ def plot_to_result(self, mask_random_color=True, better_quality=True, retina=False, - withContours=True) -> np.ndarray: + withContours=True, + bwMask="") -> np.ndarray: if isinstance(annotations[0], dict): annotations = [annotation['segmentation'] for annotation in annotations] image = self.img @@ -109,14 +111,17 @@ def plot_to_result(self, original_w = image.shape[1] if sys.platform == "darwin": plt.switch_backend("TkAgg") - plt.figure(figsize=(original_w / 100, original_h / 100)) + bgColor="white" + if bwMask == "white": + bgColor="black" + plt.figure(figsize=(original_w / 100, original_h / 100), facecolor=bgColor, edgecolor=bgColor) # Add subplot with no margin. plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) plt.margins(0, 0) plt.gca().xaxis.set_major_locator(plt.NullLocator()) plt.gca().yaxis.set_major_locator(plt.NullLocator()) - - plt.imshow(image) + if bwMask == "": + plt.imshow(image) if better_quality: if isinstance(annotations[0], torch.Tensor): annotations = np.array(annotations.cpu()) @@ -135,6 +140,7 @@ def plot_to_result(self, retinamask=retina, target_height=original_h, target_width=original_w, + bwMask=bwMask, ) else: if isinstance(annotations[0], np.ndarray): @@ -149,6 +155,7 @@ def plot_to_result(self, retinamask=retina, target_height=original_h, target_width=original_w, + bwMask=bwMask, ) if isinstance(annotations, torch.Tensor): annotations = annotations.cpu().numpy() @@ -198,7 +205,8 @@ def plot(self, mask_random_color=True, better_quality=True, retina=False, - withContours=True): + withContours=True, + bwMask=""): if len(annotations) == 0: return None result = self.plot_to_result( @@ -210,6 +218,7 @@ def plot(self, better_quality, retina, withContours, + bwMask, ) path = os.path.dirname(os.path.abspath(output_path)) @@ -230,6 +239,7 @@ def fast_show_mask( retinamask=True, target_height=960, target_width=960, + bwMask="", ): msak_sum = annotation.shape[0] height = annotation.shape[1] @@ -239,12 +249,19 @@ def fast_show_mask( sorted_indices = np.argsort(areas) annotation = annotation[sorted_indices] + opacity=0.6 index = (annotation != 0).argmax(axis=0) - if random_color: + if bwMask == "white": + color = np.ones((msak_sum, 1, 1, 3)) + opacity=1 + elif bwMask == "black": + color = np.zeros((msak_sum, 1, 1, 3)) + opacity=1 + elif random_color: color = np.random.random((msak_sum, 1, 1, 3)) else: color = np.ones((msak_sum, 1, 1, 3)) * np.array([30 / 255, 144 / 255, 255 / 255]) - transparency = np.ones((msak_sum, 1, 1, 1)) * 0.6 + transparency = np.ones((msak_sum, 1, 1, 1)) * opacity visual = np.concatenate([color, transparency], axis=-1) mask_image = np.expand_dims(annotation, -1) * visual @@ -287,6 +304,7 @@ def fast_show_mask_gpu( retinamask=True, target_height=960, target_width=960, + bwMask="", ): msak_sum = annotation.shape[0] height = annotation.shape[1] @@ -295,13 +313,20 @@ def fast_show_mask_gpu( sorted_indices = torch.argsort(areas, descending=False) annotation = annotation[sorted_indices] # Find the index of the first non-zero value at each position. + opacity=0.6 index = (annotation != 0).to(torch.long).argmax(dim=0) - if random_color: + if bwMask == "white": + color = torch.ones((msak_sum, 1, 1, 3)).to(annotation.device) + opacity=1 + elif bwMask == "black": + color = torch.zeros((msak_sum, 1, 1, 3)).to(annotation.device) + opacity=1 + elif random_color: color = torch.rand((msak_sum, 1, 1, 3)).to(annotation.device) else: color = torch.ones((msak_sum, 1, 1, 3)).to(annotation.device) * torch.tensor([ 30 / 255, 144 / 255, 255 / 255]).to(annotation.device) - transparency = torch.ones((msak_sum, 1, 1, 1)).to(annotation.device) * 0.6 + transparency = torch.ones((msak_sum, 1, 1, 1)).to(annotation.device) * opacity visual = torch.cat([color, transparency], dim=-1) mask_image = torch.unsqueeze(annotation, -1) * visual # Select data according to the index. The index indicates which batch's data to choose at each position, converting the mask_image into a single batch form. From f3143aeddeb4931872bacd98e063ea1402f6b3f5 Mon Sep 17 00:00:00 2001 From: New Bing Date: Thu, 14 Dec 2023 01:05:35 +0800 Subject: [PATCH 3/6] Update prompt.py remove useless logging import --- fastsam/prompt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastsam/prompt.py b/fastsam/prompt.py index 26a49bc..4bc34c9 100644 --- a/fastsam/prompt.py +++ b/fastsam/prompt.py @@ -6,7 +6,6 @@ import torch from .utils import image_to_np_ndarray from PIL import Image -import logging try: import clip # for linear_assignment From 8768b4254aa65c17a413be0444dc0fdc724510a1 Mon Sep 17 00:00:00 2001 From: New Bing Date: Thu, 14 Dec 2023 15:57:48 +0800 Subject: [PATCH 4/6] upgrade gradio.app to fix js error --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 03b8c97..e2514f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ tqdm>=4.64.0 pandas>=1.1.4 seaborn>=0.11.0 -gradio==3.35.2 +gradio==3.41.0 # Ultralytics----------------------------------- ultralytics == 8.0.120 From 894170272c1aa1bd8e9bf4e8a2b9df5fd6e2db21 Mon Sep 17 00:00:00 2001 From: New Bing Date: Thu, 14 Dec 2023 16:39:11 +0800 Subject: [PATCH 5/6] gradio and replicate support create white and black masks out. --- app_gradio.py | 16 ++++++++++++++-- predict.py | 13 ++++++++++++- utils/tools.py | 36 +++++++++++++++++++++++++++++------- utils/tools_gradio.py | 35 ++++++++++++++++++++++++++++++----- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/app_gradio.py b/app_gradio.py index 5682bc5..b4238db 100644 --- a/app_gradio.py +++ b/app_gradio.py @@ -80,7 +80,11 @@ def segment_everything( text="", wider=False, mask_random_color=True, + bw_mask="", ): + # Reset the default value of bw_mask + if bw_mask=="None": + bw_mask="" input_size = int(input_size) # 确保 imgsz 是整数 # Thanks for the suggestion by hysts in HuggingFace. w, h = input.size @@ -111,7 +115,8 @@ def segment_everything( mask_random_color=mask_random_color, bbox=None, use_retina=use_retina, - withContours=withContours,) + withContours=withContours, + bwMask=bw_mask) return fig @@ -124,6 +129,7 @@ def segment_with_points( withContours=True, use_retina=True, mask_random_color=True, + bw_mask="", ): global global_points global global_point_label @@ -157,7 +163,8 @@ def segment_with_points( mask_random_color=mask_random_color, bbox=None, use_retina=use_retina, - withContours=withContours,) + withContours=withContours, + bwMask=bw_mask) global_points = [] global_point_label = [] @@ -342,6 +349,9 @@ def get_points_with_draw(image, label, evt: gr.SelectData): mor_check = gr.Checkbox(value=False, label='better_visual_quality', info='better quality using morphologyEx') retina_check = gr.Checkbox(value=True, label='use_retina', info='draw high-resolution segmentation masks') wider_check = gr.Checkbox(value=False, label='wider', info='wider result') + with gr.Row(): + bw_mask = gr.Radio(["None", "black", "white"], value="None", label="Output result image as white or black masks") + rand_color = gr.Checkbox(value=False, label='random', info='mask with random color') # Description gr.Markdown(description_e) @@ -357,6 +367,8 @@ def get_points_with_draw(image, label, evt: gr.SelectData): retina_check, text_box, wider_check, + rand_color, + bw_mask, ], outputs=segm_img_t) diff --git a/predict.py b/predict.py index 06c955c..f1ace0a 100644 --- a/predict.py +++ b/predict.py @@ -43,6 +43,10 @@ def predict( better_quality: bool = Input( description="better quality using morphologyEx", default=False ), + bw_mask: str = Input( + default="", + description="empty value to disable, set white or black to get white or black mask" + ), ) -> Path: """Run a single prediction on the model""" @@ -77,6 +81,7 @@ def predict( retina=retina, text_prompt=text_prompt, withContours=withContours, + bw_mask=bw_mask ) args.point_prompt = ast.literal_eval(args.point_prompt) args.box_prompt = ast.literal_eval(args.box_prompt) @@ -102,6 +107,7 @@ def predict( args=args, mask_random_color=args.randomcolor, bbox=convert_box_xywh_to_xyxy(args.box_prompt), + bwMask=args.bw_mask, ) elif args.text_prompt != None: @@ -109,7 +115,10 @@ def predict( annotations = prompt(results, args, text=True) annotations = np.array([annotations]) fast_process( - annotations=annotations, args=args, mask_random_color=args.randomcolor + annotations=annotations, + args=args, + mask_random_color=args.randomcolor, + bwMask=args.bw_mask, ) elif args.point_prompt[0] != [0, 0]: @@ -122,6 +131,7 @@ def predict( args=args, mask_random_color=args.randomcolor, points=args.point_prompt, + bwMask=args.bw_mask, ) else: @@ -129,6 +139,7 @@ def predict( annotations=results[0].masks.data, args=args, mask_random_color=args.randomcolor, + bwMask=args.bw_mask, ) out = "/tmp.out.png" diff --git a/utils/tools.py b/utils/tools.py index 8b80b4a..50f7ba3 100644 --- a/utils/tools.py +++ b/utils/tools.py @@ -93,7 +93,7 @@ def get_bbox_from_mask(mask): def fast_process( - annotations, args, mask_random_color, bbox=None, points=None, edges=False + annotations, args, mask_random_color, bbox=None, points=None, edges=False, bwMask="" ): if isinstance(annotations[0], dict): annotations = [annotation["segmentation"] for annotation in annotations] @@ -104,13 +104,17 @@ def fast_process( original_w = image.shape[1] if sys.platform == "darwin": plt.switch_backend("TkAgg") - plt.figure(figsize=(original_w/100, original_h/100)) + bgColor="white" + if bwMask == "white": + bgColor="black" + plt.figure(figsize=(original_w/100, original_h/100), facecolor=bgColor, edgecolor=bgColor) # Add subplot with no margin. plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) plt.margins(0, 0) plt.gca().xaxis.set_major_locator(plt.NullLocator()) plt.gca().yaxis.set_major_locator(plt.NullLocator()) - plt.imshow(image) + if bwMask == "": + plt.imshow(image) if args.better_quality == True: if isinstance(annotations[0], torch.Tensor): annotations = np.array(annotations.cpu()) @@ -133,6 +137,7 @@ def fast_process( retinamask=args.retina, target_height=original_h, target_width=original_w, + bwMask=bwMask, ) else: if isinstance(annotations[0], np.ndarray): @@ -147,6 +152,7 @@ def fast_process( retinamask=args.retina, target_height=original_h, target_width=original_w, + bwMask=bwMask, ) if isinstance(annotations, torch.Tensor): annotations = annotations.cpu().numpy() @@ -202,6 +208,7 @@ def fast_show_mask( retinamask=True, target_height=960, target_width=960, + bwMask="", ): msak_sum = annotation.shape[0] height = annotation.shape[1] @@ -211,14 +218,21 @@ def fast_show_mask( sorted_indices = np.argsort(areas) annotation = annotation[sorted_indices] + opacity=0.6 index = (annotation != 0).argmax(axis=0) - if random_color == True: + if bwMask == "white": + color = np.ones((msak_sum, 1, 1, 3)) + opacity=1 + elif bwMask == "black": + color = np.zeros((msak_sum, 1, 1, 3)) + opacity=1 + elif random_color == True: color = np.random.random((msak_sum, 1, 1, 3)) else: color = np.ones((msak_sum, 1, 1, 3)) * np.array( [30 / 255, 144 / 255, 255 / 255] ) - transparency = np.ones((msak_sum, 1, 1, 1)) * 0.6 + transparency = np.ones((msak_sum, 1, 1, 1)) * opacity visual = np.concatenate([color, transparency], axis=-1) mask_image = np.expand_dims(annotation, -1) * visual @@ -268,6 +282,7 @@ def fast_show_mask_gpu( retinamask=True, target_height=960, target_width=960, + bwMask="", ): msak_sum = annotation.shape[0] height = annotation.shape[1] @@ -276,14 +291,21 @@ def fast_show_mask_gpu( sorted_indices = torch.argsort(areas, descending=False) annotation = annotation[sorted_indices] # 找每个位置第一个非零值下标 + opacity=0.6 index = (annotation != 0).to(torch.long).argmax(dim=0) - if random_color == True: + if bwMask == "white": + color = torch.ones((msak_sum, 1, 1, 3)).to(annotation.device) + opacity=1 + elif bwMask == "black": + color = torch.zeros((msak_sum, 1, 1, 3)).to(annotation.device) + opacity=1 + elif random_color == True: color = torch.rand((msak_sum, 1, 1, 3)).to(annotation.device) else: color = torch.ones((msak_sum, 1, 1, 3)).to(annotation.device) * torch.tensor( [30 / 255, 144 / 255, 255 / 255] ).to(annotation.device) - transparency = torch.ones((msak_sum, 1, 1, 1)).to(annotation.device) * 0.6 + transparency = torch.ones((msak_sum, 1, 1, 1)).to(annotation.device) * opacity visual = torch.cat([color, transparency], dim=-1) mask_image = torch.unsqueeze(annotation, -1) * visual # 按index取数,index指每个位置选哪个batch的数,把mask_image转成一个batch的形式 diff --git a/utils/tools_gradio.py b/utils/tools_gradio.py index 31ec5d1..5e17a88 100644 --- a/utils/tools_gradio.py +++ b/utils/tools_gradio.py @@ -15,6 +15,7 @@ def fast_process( bbox=None, use_retina=True, withContours=True, + bwMask="" ): if isinstance(annotations[0], dict): annotations = [annotation['segmentation'] for annotation in annotations] @@ -37,6 +38,7 @@ def fast_process( retinamask=use_retina, target_height=original_h, target_width=original_w, + bwMask=bwMask, ) else: if isinstance(annotations[0], np.ndarray): @@ -49,6 +51,7 @@ def fast_process( retinamask=use_retina, target_height=original_h, target_width=original_w, + bwMask=bwMask, ) if isinstance(annotations, torch.Tensor): annotations = annotations.cpu().numpy() @@ -73,7 +76,13 @@ def fast_process( color = np.array([0 / 255, 0 / 255, 255 / 255, 0.9]) contour_mask = temp / 255 * color.reshape(1, 1, -1) - image = image.convert('RGBA') + if bwMask=="": + image = image.convert('RGBA') + elif bwMask=="white": + image = Image.new('1', (original_w, original_h), 0) + else: + image = Image.new('1', (original_w, original_h), 1) + overlay_inner = Image.fromarray((inner_mask * 255).astype(np.uint8), 'RGBA') image.paste(overlay_inner, (0, 0), overlay_inner) @@ -93,6 +102,7 @@ def fast_show_mask( retinamask=True, target_height=960, target_width=960, + bwMask="", ): mask_sum = annotation.shape[0] height = annotation.shape[1] @@ -102,12 +112,19 @@ def fast_show_mask( sorted_indices = np.argsort(areas)[::1] annotation = annotation[sorted_indices] + opacity=0.6 index = (annotation != 0).argmax(axis=0) - if random_color: + if bwMask == "white": + color = np.ones((mask_sum, 1, 1, 3)) + opacity=1 + elif bwMask == "black": + color = np.zeros((mask_sum, 1, 1, 3)) + opacity=1 + elif random_color: color = np.random.random((mask_sum, 1, 1, 3)) else: color = np.ones((mask_sum, 1, 1, 3)) * np.array([30 / 255, 144 / 255, 255 / 255]) - transparency = np.ones((mask_sum, 1, 1, 1)) * 0.6 + transparency = np.ones((mask_sum, 1, 1, 1)) * opacity visual = np.concatenate([color, transparency], axis=-1) mask_image = np.expand_dims(annotation, -1) * visual @@ -135,6 +152,7 @@ def fast_show_mask_gpu( retinamask=True, target_height=960, target_width=960, + bwMask="", ): device = annotation.device mask_sum = annotation.shape[0] @@ -144,14 +162,21 @@ def fast_show_mask_gpu( sorted_indices = torch.argsort(areas, descending=False) annotation = annotation[sorted_indices] # 找每个位置第一个非零值下标 + opacity=0.6 index = (annotation != 0).to(torch.long).argmax(dim=0) - if random_color: + if bwMask == "white": + color = torch.ones((mask_sum, 1, 1, 3)).to(device) + opacity=1 + elif bwMask == "black": + color = torch.zeros((mask_sum, 1, 1, 3)).to(device) + opacity=1 + elif random_color: color = torch.rand((mask_sum, 1, 1, 3)).to(device) else: color = torch.ones((mask_sum, 1, 1, 3)).to(device) * torch.tensor( [30 / 255, 144 / 255, 255 / 255] ).to(device) - transparency = torch.ones((mask_sum, 1, 1, 1)).to(device) * 0.6 + transparency = torch.ones((mask_sum, 1, 1, 1)).to(device) * opacity visual = torch.cat([color, transparency], dim=-1) mask_image = torch.unsqueeze(annotation, -1) * visual # 按index取数,index指每个位置选哪个batch的数,把mask_image转成一个batch的形式 From d68e576f341bc4990861898d73d82490159f7f33 Mon Sep 17 00:00:00 2001 From: New Bing Date: Thu, 14 Dec 2023 16:51:55 +0800 Subject: [PATCH 6/6] add document to MORE_USAGES.md --- MORE_USAGES.md | 24 ++++++++++++++++++++++++ assets/more_usages/dogs_black.jpeg | Bin 0 -> 8213 bytes assets/more_usages/dogs_white.jpeg | Bin 0 -> 8222 bytes 3 files changed, 24 insertions(+) create mode 100644 assets/more_usages/dogs_black.jpeg create mode 100644 assets/more_usages/dogs_white.jpeg diff --git a/MORE_USAGES.md b/MORE_USAGES.md index 9fdfbfe..11919ee 100644 --- a/MORE_USAGES.md +++ b/MORE_USAGES.md @@ -57,3 +57,27 @@ python Inference.py --model_path ./weights/FastSAM.pt \ --withContours True ``` ![text prompt](assets/more_usages/text_prompt_cat.png) + +### use text prompt and output black mask +Use `--bwMask "black"` to specify the mask color +```shell +python Inference.py --model_path ./weights/FastSAM.pt \ + --img_path ./images/dogs.jpg \ + --text_prompt "the black dog" \ + --better_quality True \ + --withContours True \ + --bwMask "black" +``` +![text prompt](assets/more_usages/dogs_black.jpeg) + +### use text prompt and output white mask +Use `--bwMask "white"` to specify the mask color +```shell +python Inference.py --model_path ./weights/FastSAM.pt \ + --img_path ./images/dogs.jpg \ + --text_prompt "the black dog" \ + --better_quality True \ + --withContours True \ + --bwMask "white" +``` +![text prompt](assets/more_usages/dogs_white.jpeg) diff --git a/assets/more_usages/dogs_black.jpeg b/assets/more_usages/dogs_black.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d3d0d2ec38b1b8ca122393e6a81a1bf9413db112 GIT binary patch literal 8213 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 T(D9V=EXV($INnqLsTlkp1Cv;# literal 0 HcmV?d00001 diff --git a/assets/more_usages/dogs_white.jpeg b/assets/more_usages/dogs_white.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8856220b0afae547b6e9eaae13185b215b0d4711 GIT binary patch literal 8222 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 literal 0 HcmV?d00001