Skip to content
Snippets Groups Projects
Commit f5516fa1 authored by fima's avatar fima :beers:
Browse files

Annotation tool 4 0

parent defd3d14
No related branches found
No related tags found
1 merge request!59Annotation tool 4 0
%% Cell type:markdown id: tags:
# Image annotation tool
This notebook shows how the annotation interface can be used to create masks for images
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
import qim3d import qim3d
from scipy import ndimage
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib as mpl qim3d.io.logger.level("info")
import numpy as np
%matplotlib inline
```
%% Cell type:code id: tags:
``` python # Load example image
# Load 2D example image img = qim3d.examples.bone_128x128x128
img = qim3d.examples.blobs_256x256
# Display image
plt.imshow(img)
plt.show()
```
%% Cell type:code id: tags:
``` python
# Start annotation tool # Start annotation tool
interface = qim3d.gui.annotation_tool.Interface() interface = qim3d.gui.annotation_tool.Interface()
interface.max_masks = 4
# We can directly pass the image we loaded to the interface # We can directly pass the image we loaded to the interface
interface.launch(img=img) #interface.launch()
``` interface.launch(ndimage.zoom(img[0], 3, order=0))
%% Cell type:code id: tags:
``` python
# When 'prepare mask for download' is pressed once, the mask can be retrieved with the get_result() method
mask = interface.get_result()
```
%% Cell type:markdown id: tags:
## Check the obtained mask
%% Cell type:code id: tags:
``` python
print (f"Original image shape..: {img.shape}")
print (f"Mask image shape......: {mask.shape}")
print (f"\nNumber of masks: {np.max(mask)}")
```
%% Cell type:markdown id: tags:
## Show the masked regions
%% Cell type:code id: tags:
``` python
%matplotlib inline
nmasks = np.max(mask)
fig, axs = plt.subplots(nrows=1, ncols=nmasks+2, figsize=(12,3))
# Show original image
axs[0].imshow(img)
axs[0].set_title("Original")
axs[0].axis('off')
# Show masks
cmap = mpl.colormaps["rainbow"].copy()
cmap.set_under(color='black') # Sets the background to black
axs[1].imshow(mask, interpolation='none', cmap=cmap, vmin=1, vmax=nmasks+1)
axs[1].set_title("Masks")
axs[1].axis('off')
# Show masked regions
for idx in np.arange(2, nmasks+2):
mask_id = idx-1
submask = mask.copy()
submask[submask != mask_id] = 0
masked_img = img.copy()
masked_img[submask==0] = 0
axs[idx].imshow(masked_img)
axs[idx].set_title(f"Mask {mask_id}")
axs[idx].axis('off')
plt.show()
``` ```
......
...@@ -6,6 +6,11 @@ icon: fontawesome/solid/screwdriver-wrench ...@@ -6,6 +6,11 @@ icon: fontawesome/solid/screwdriver-wrench
A set of tools to ease managment of the system, with the common needs for large data in mind. A set of tools to ease managment of the system, with the common needs for large data in mind.
::: qim3d.utils.img
options:
members:
- overlay_rgb_images
::: qim3d.utils.system ::: qim3d.utils.system
options: options:
members: members:
......
/* Override for dark mode */ /* Override for dark mode */
.dark{ .dark {
--name: default; --name: default;
--primary-50: #fff7ed; --primary-50: #fff7ed;
--primary-100: #ffedd5; --primary-100: #ffedd5;
...@@ -73,9 +73,9 @@ ...@@ -73,9 +73,9 @@
--link-text-color-hover: var(--secondary-700); --link-text-color-hover: var(--secondary-700);
--link-text-color-visited: var(--secondary-500); --link-text-color-visited: var(--secondary-500);
--body-text-color-subdued: var(--neutral-400); --body-text-color-subdued: var(--neutral-400);
--shadow-drop: rgba(0,0,0,0.05) 0px 1px 2px 0px; --shadow-drop: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
--shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-inset: rgba(0,0,0,0.05) 0px 2px 4px 0px inset; --shadow-inset: rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset;
--shadow-spread: 3px; --shadow-spread: 3px;
--block-background-fill: var(--background-fill-primary); --block-background-fill: var(--background-fill-primary);
--block-border-color: var(--border-color-primary); --block-border-color: var(--border-color-primary);
...@@ -203,6 +203,7 @@ ...@@ -203,6 +203,7 @@
--button-small-text-weight: 400; --button-small-text-weight: 400;
--button-transition: none; --button-transition: none;
} }
/* Up here is the override for dark mode */ /* Up here is the override for dark mode */
/* Now comes our custom CSS */ /* Now comes our custom CSS */
...@@ -228,9 +229,10 @@ h2 { ...@@ -228,9 +229,10 @@ h2 {
white-space: pre !important; white-space: pre !important;
} }
.no-border{ .no-border {
border-width: 0px !important; border-width: 0px !important;
} }
/* Hides Gradio footer */ /* Hides Gradio footer */
footer { footer {
visibility: hidden; visibility: hidden;
...@@ -341,9 +343,9 @@ input[type="range"]::-webkit-slider-thumb { ...@@ -341,9 +343,9 @@ input[type="range"]::-webkit-slider-thumb {
} }
/* Buttons */ /* Buttons */
.btn-html{ .btn-html {
position: sticky; position: sticky;
display:flex; display: flex;
justify-content: center; justify-content: center;
background: white !important; background: white !important;
border-color: #6c757d !important; border-color: #6c757d !important;
...@@ -372,7 +374,7 @@ input[type="range"]::-webkit-slider-thumb { ...@@ -372,7 +374,7 @@ input[type="range"]::-webkit-slider-thumb {
text-align: left !important; text-align: left !important;
} }
.btn-spinner::after{ .btn-spinner::after {
content: ""; content: "";
position: absolute; position: absolute;
width: 16px; width: 16px;
...@@ -547,7 +549,7 @@ div.svelte-1frtwj3 { ...@@ -547,7 +549,7 @@ div.svelte-1frtwj3 {
.w-64 { .w-64 {
width: 64px !important; width: 64px !important;
min-width: 64px !important; min-width: 64px !important;
} }
...@@ -558,7 +560,7 @@ div.svelte-1frtwj3 { ...@@ -558,7 +560,7 @@ div.svelte-1frtwj3 {
.w-36 { .w-36 {
width: 36px !important; width: 36px !important;
min-width: 36px !important; min-width: 36px !important;
} }
...@@ -569,7 +571,7 @@ div.svelte-1frtwj3 { ...@@ -569,7 +571,7 @@ div.svelte-1frtwj3 {
.w-32 { .w-32 {
width: 32px !important; width: 32px !important;
min-width: 32px !important; min-width: 32px !important;
} }
...@@ -579,7 +581,7 @@ div.svelte-1frtwj3 { ...@@ -579,7 +581,7 @@ div.svelte-1frtwj3 {
.w-16 { .w-16 {
width: 16px !important; width: 16px !important;
min-width: 16px !important; min-width: 16px !important;
} }
.h-0 { .h-0 {
...@@ -590,33 +592,124 @@ div.svelte-1frtwj3 { ...@@ -590,33 +592,124 @@ div.svelte-1frtwj3 {
width: 0px !important; width: 0px !important;
} }
.options{ .options {
border: 1px solid #bababa !important; border: 1px solid #bababa !important;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.15) !important; box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.15) !important;
} }
.mt-32{ .mt-32 {
margin-top: 32px; margin-top: 32px;
} }
.mt-48{ .mt-48 {
margin-top: 48px; margin-top: 48px;
} }
.mt-64{ .mt-64 {
margin-top: 64px; margin-top: 64px;
} }
.mt-128{ .mt-128 {
margin-top: 128px; margin-top: 128px;
} }
.hide-overflow{ .hide-overflow {
overflow: hidden !important; overflow: hidden !important;
} }
.wrap.default{ .wrap.default {
visibility: hidden !important; visibility: hidden !important;
} }
\ No newline at end of file
/* Annotation tool CSS */
.annotation-tool .layer-wrap {
display: none !important;
}
.annotation-tool button[title="Image button"] {
display: none !important;
}
.annotation-tool:not(.no-img) button[title="Upload button"] {
display: none !important;
}
.annotation-tool button[title="Clear canvas"] {
display: none !important;
}
.annotation-tool button[title="Color button"] {
color: var(--block-label-text-color) !important;
margin-inline-end: 32px !important;
}
.annotation-tool button[title="Color button"]::after {
content: "Select mask";
}
.annotation-tool button[title="Size button"] {
color: var(--block-label-text-color) !important;
}
.annotation-tool button[title="Size button"]::after {
content: "Brush size";
}
.annotation-tool .row-wrap {
justify-content: flex-start !important;
}
.annotation-tool .controls-wrap .small {
width: 20px !important;
height: 20px !important;
}
.annotation-tool .controls-wrap {
top: 0px !important;
left: 0px !important;
}
.annotation-tool .controls-wrap .padded {
padding: 8px !important;
border: 0px !important;
background-color: rgba(255, 255, 255, 0.8);
}
.annotation-tool .stage-wrap {
margin-top: 0px !important;
margin-bottom: 16px !important;
}
.annotation-tool .svelte-b3dw9m {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
}
.annotation-tool .bottom {
bottom: 8px !important;
left: 0px !important;
position: absolute !important;
}
.annotation-tool .download {
min-width: 4rem !important;
width: 10%;
white-space: nowrap;
text-align: right;
}
.annotation-tool .stage-wrap canvas {
border: 0px !important;
max-width: 512px !important;
max-height: 512px !important;
height: 512px !important;
width: fit-content !important;
}
import tifffile import tifffile
import os import os
import time
import getpass
import numpy as np import numpy as np
import gradio as gr import gradio as gr
from qim3d.io import load # load or DataLoader? import qim3d.utils
from qim3d.utils import internal_tools from qim3d.io import load, save
from qim3d.io.logger import log
class Session:
def __init__(self):
self.n_masks = 3
self.img_editor = None
self.masks_rgb = None
self.mask_names = {0: "red", 1: "green", 2: "blue"}
self.temp_files = []
self.gradio_temp = None
self.username = getpass.getuser()
class Interface: class Interface:
def __init__(self): def __init__(self):
self.verbose = False self.verbose = False
self.title = "Annotation tool" self.title = "Annotation tool"
# self.plot_height = 768 self.height = 768
self.height = 1024 self.interface = None
# self.width = 960 self.username = getpass.getuser()
self.max_masks = 3
self.mask_opacity = 0.5
self.cmy_hex = ["#00ffff", "#ff00ff", "#ffff00"] # Colors for max_masks>3?
# CSS path # CSS path
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
...@@ -23,7 +34,9 @@ class Interface: ...@@ -23,7 +34,9 @@ class Interface:
def launch(self, img=None, **kwargs): def launch(self, img=None, **kwargs):
# Create gradio interfaces # Create gradio interfaces
self.interface = self.create_interface(img=img)
self.interface = self.create_interface(img)
self.gradio_temp = self.interface.GRADIO_CACHE
# Set gradio verbose level # Set gradio verbose level
if self.verbose: if self.verbose:
...@@ -35,7 +48,6 @@ class Interface: ...@@ -35,7 +48,6 @@ class Interface:
quiet=quiet, quiet=quiet,
height=self.height, height=self.height,
# width=self.width, # width=self.width,
show_tips=False,
**kwargs, **kwargs,
) )
...@@ -43,322 +55,203 @@ class Interface: ...@@ -43,322 +55,203 @@ class Interface:
def get_result(self): def get_result(self):
# Get the temporary files from gradio # Get the temporary files from gradio
temp_sets = self.interface.temp_file_sets base = os.path.join(self.gradio_temp, "qim3d", self.username)
for temp_set in temp_sets: temp_path_list = []
if "mask" in str(temp_set): for filename in os.listdir(base):
if "mask" in str(filename):
# Get the list of the temporary files # Get the list of the temporary files
temp_path_list = list(temp_set) temp_path_list.append(os.path.join(base, filename))
# Files are not in creation order,
# so we need to get find the latest
creation_time_list = []
for path in temp_path_list:
creation_time_list.append(os.path.getctime(path))
# Get index for the latest file # Make dictionary of maks
file_idx = np.argmax(creation_time_list) masks = {}
for temp_file in temp_path_list:
mask_file = os.path.basename(temp_file)
mask_name = os.path.splitext(mask_file)[0]
masks[mask_name] = load(temp_file)
# Load the temporary file return masks
mask = load(temp_path_list[file_idx])
return mask def set_visible(self):
return gr.update(visible=True)
def create_interface(self, img=None): def create_interface(self, img=None):
if img is not None:
custom_css = "annotation-tool"
else:
custom_css = "annotation-tool no-img"
with gr.Blocks(css=self.css_path) as gradio_interface: with gr.Blocks(css=self.css_path) as gradio_interface:
masks_state = gr.State(value={})
counts = gr.Number(value=1, visible=False)
brush = gr.Brush(
colors=[
"rgb(255,50,100)",
"rgb(50,250,100)",
"rgb(50,100,255)",
],
color_mode="fixed",
default_size=10,
)
with gr.Row(): with gr.Row():
with gr.Column(scale=1, min_width=320):
upload_img_btn = gr.UploadButton( with gr.Column(scale=6):
label="Upload image", img_editor = gr.ImageEditor(
file_types=["image"], value=img,
interactive=True if img is None else False, type="numpy",
) image_mode="RGB",
clear_img_btn = gr.Button( brush=brush,
value="Clear image", interactive=False if img is None else True sources="upload",
interactive=True,
show_download_button=True,
container=False,
transforms=[""],
elem_classes=custom_css,
) )
with gr.Column(scale=1, min_width=256):
with gr.Row(): with gr.Row():
with gr.Column(scale=2, min_width=32): btn_update = gr.Button(
selected_mask = gr.Radio( value="Update", elem_classes="btn btn-html btn-run"
choices=["Mask 1"], )
value="Mask 1",
label="Choose which mask to draw",
scale=1,
)
with gr.Column(scale=1, min_width=64):
add_mask_btn = gr.Button(
value="Add mask",
scale=2,
)
with gr.Row(): with gr.Row():
prep_dl_btn = gr.Button( overlay_img = gr.Image(
value="Prepare mask for download", show_download_button=False, show_label=False, visible=False
visible=False if img is None else True,
) )
with gr.Row(): with gr.Row():
save_output = gr.File( masks_download = gr.File(
show_label=True, label="Download masks",
label="Output file",
visible=False, visible=False,
elem_classes=custom_css,
) )
with gr.Column(scale=4): temp_path = gr.Textbox(value=gradio_interface.GRADIO_CACHE, visible=False)
with gr.Row(): session = gr.State([])
input_img = gr.Image( inputs = [img_editor]
label="Input", operations = Operations()
tool="sketch", # fmt: off
value=img, btn_update.click(
height=600, fn=operations.start_session, inputs=[img_editor,temp_path] , outputs=session).then(
width=600, fn=operations.preview, inputs=session, outputs=overlay_img).then(
brush_color="#00ffff", fn=self.set_visible, inputs=None, outputs=overlay_img).then(
mask_opacity=self.mask_opacity, fn=operations.separate_masks, inputs=session, outputs=[session, masks_download]).then(
interactive=False if img is None else True, fn=self.set_visible, inputs=None, outputs=masks_download)
)
# fmt: on
return gradio_interface
output_masks = []
for mask_idx in range(self.max_masks):
with gr.Row(): # make a new row for every mask
output_mask = gr.Image(
label=f"Mask {mask_idx+1}",
visible=True if mask_idx == 0 else False,
image_mode="L",
height=600,
width=600,
interactive=False
if img is None
else True, # If statement added bc of bug after Gradio 3.44.x
show_download_button=False,
)
output_masks.append(output_mask)
# Operations
operations = Operations(max_masks=self.max_masks, cmy_hex=self.cmy_hex)
# Update component configuration when image is uploaded
upload_img_btn.upload(
fn=operations.upload_img_update,
inputs=upload_img_btn,
outputs=[input_img, clear_img_btn, upload_img_btn, prep_dl_btn]
+ output_masks,
)
# Add mask below when 'add mask' button is clicked class Operations:
add_mask_btn.click(
fn=operations.increment_mask,
inputs=counts,
outputs=[counts, selected_mask] + output_masks,
)
# Draw mask when input image is edited def start_session(self, *args):
input_img.edit( session = Session()
fn=operations.update_masks, session.img_editor = args[0]
inputs=[input_img, selected_mask, masks_state, upload_img_btn], session.gradio_temp = args[1]
outputs=output_masks,
)
# Update brush color according to radio setting # Clean temp files
selected_mask.change( base = os.path.join(session.gradio_temp, "qim3d", session.username)
fn=operations.update_brush_color,
inputs=selected_mask,
outputs=input_img,
)
# Make file download visible try:
prep_dl_btn.click( files = os.listdir(base)
fn=operations.save_mask, for filename in files:
inputs=output_masks, # Check if "mask" is in the filename
outputs=[save_output, save_output], if "mask" in filename:
).success( file_path = os.path.join(base, filename)
fn=lambda: os.remove('mask.tif') os.remove(file_path)
) # Remove mask file from working directory immediately after sending it to /tmp/gradio
# Update 'Add mask' button interactivit according to the current count
counts.change(
fn=operations.set_add_mask_btn_interactivity,
inputs=counts,
outputs=add_mask_btn,
)
# Reset component configuration when image is cleared except FileNotFoundError:
clear_img_btn.click( files = None
fn=operations.clear_img_update,
inputs=None,
outputs=[
selected_mask,
prep_dl_btn,
save_output,
counts,
input_img,
upload_img_btn,
clear_img_btn,
]
+ output_masks,
)
return gradio_interface return session
def overlay_images(self, background, masks, alpha=0.5):
"""Overlay multiple RGB masks onto an RGB background image using alpha blending.
class Operations: Args:
def __init__(self, max_masks, cmy_hex): background (numpy.ndarray): The background RGB image with shape (height, width, 3).
self.max_masks = max_masks masks (numpy.ndarray): The RGB mask images with shape (num_masks, height, width, 3).
self.cmy_hex = cmy_hex alpha (float, optional): The alpha value for blending. Defaults to 0.5.
def update_masks(self, input_img, selected_mask, masks_state, file): Returns:
# Binarize mask (it is not per default due to anti-aliasing) numpy.ndarray: The composite image with overlaid masks.
input_mask = input_img["mask"]
input_mask[input_mask > 0] = 255
try: Raises:
file_name = file.name ValueError: If input images have different shapes.
except AttributeError:
file_name = "nb_img"
# Add new file to state dictionary when this function sees it first time
if file_name not in masks_state.keys():
masks_state[file_name] = [[] for _ in range(self.max_masks)]
# Get index of currently selected and non-selected masks
sel_mask_idx = int(selected_mask[-1]) - 1
nonsel_mask_idxs = [
mask_idx
for mask_idx in list(range(self.max_masks))
if mask_idx != sel_mask_idx
]
# Add background to state first time function is invoked in current session
if len(masks_state[file_name][0]) == 0:
for i in range(len(masks_state[file_name])):
masks_state[file_name][i].append(input_mask)
# Check for discrepancy between what is drawn and what is shown as output masks
masks_state_combined = 0
for i in range(len(masks_state[file_name])):
masks_state_combined += masks_state[file_name][i][-1]
discrepancy = masks_state_combined != input_mask
if np.any(discrepancy): # Correct discrepancy in output masks
for i in range(self.max_masks):
masks_state[file_name][i][-1][discrepancy] = 0
# Add most recent change in input to currently selected mask
mask2append = input_mask
for mask_idx in nonsel_mask_idxs:
mask2append -= masks_state[file_name][mask_idx][-1]
masks_state[file_name][sel_mask_idx].append(mask2append)
return [masks_state[file_name][i][-1] for i in range(self.max_masks)]
def save_mask(self, *masks):
# Go from multi-channel to single-channel mask
stacked_masks = np.stack(masks, axis=-1)
final_mask = np.zeros_like(masks[0])
final_mask[np.where(stacked_masks == 255)[:2]] = (
np.where(stacked_masks == 255)[-1] + 1
)
# Save output image in a temp space (and to current directory which is a bug) Note:
filename = "mask.tif" - The function performs alpha blending to overlay the masks onto the background.
tifffile.imwrite(filename, final_mask) - It ensures that the background and masks have the same shape before blending.
- It calculates the maximum projection of the masks and blends them onto the background.
- Brightness outside the masks is adjusted to maintain consistency with the background.
"""
save_output_update = gr.File(visible=True) # Igonore alpha in case its there
background = background[..., :3]
masks = masks[..., :3]
return save_output_update, filename # Ensure both images have the same shape
if background.shape != masks.shape:
raise ValueError("Input images must have the same shape")
def increment_mask(self, counts): # Perform alpha blending
# increment count by 1 masks_max_projection = np.amax(masks, axis=2)
counts += 1 masks_max_projection = np.stack((masks_max_projection,) * 3, axis=-1)
counts = int(counts)
counts_update = gr.Number(value=counts) # Normalize if we have something
selected_mask_update = gr.Radio( if np.max(masks_max_projection) > 0:
value=f"Mask {counts}", choices=[f"Mask {i+1}" for i in range(counts)] masks_max_projection = masks_max_projection / np.max(masks_max_projection)
)
output_masks_update = [gr.Image(visible=True)] * counts + [
gr.Image(visible=False)
] * (self.max_masks - counts)
return [counts_update, selected_mask_update] + output_masks_update composite = background * (1 - alpha) + masks * alpha
composite = np.clip(composite, 0, 255).astype("uint8")
def update_brush_color(self, selected_mask): # Adjust brightness outside masks
sel_mask_idx = int(selected_mask[-1]) - 1 composite = composite + (background * (1 - alpha)) * (1 - masks_max_projection)
if sel_mask_idx < len(self.cmy_hex):
input_img_update = gr.Image(brush_color=self.cmy_hex[sel_mask_idx])
else:
input_img_update = gr.Image(brush_color="#000000") # Return black brush
return input_img_update return composite.astype("uint8")
def set_add_mask_btn_interactivity(self, counts): def preview(self, session):
add_mask_btn_update = ( background = session.img_editor["background"]
gr.Button(interactive=True) masks = session.img_editor["layers"][0]
if counts < self.max_masks overlay_image = qim3d.utils.img.overlay_rgb_images(background, masks)
else gr.Button(interactive=False)
) return overlay_image
return add_mask_btn_update
def separate_masks(self, session):
def clear_img_update(self):
selected_mask_update = gr.Radio( masks_rgb = session.img_editor["layers"][0]
choices=["Mask 1"], value="Mask 1" mask_threshold = 200 # This value is based
) # Reset radio component to only show 'Mask 1'
prep_dl_btn_update = gr.Button( mask_list = []
visible=False files_list = []
) # Make 'Prepare mask for download' button invisible
save_output_update = gr.File(visible=False) # Make File save box invisible # Go through each channel
counts_update = gr.Number(value=1) # Reset invisible counter to 1 for idx in np.arange(session.n_masks):
input_img_update = gr.Image(
value=None, interactive=False mask_grayscale = masks_rgb[:, :, idx]
) # Set input image component to non-interactive (so a new image cannot be uploaded directly in the component) mask = mask_grayscale > mask_threshold
upload_img_btn_update = gr.Button(
interactive=True # Save only if we have a mask
) # Make 'Upload image' button interactive if np.sum(mask) > 0:
clear_img_btn_update = gr.Button( mask_list.append(mask)
interactive=False filename = f"mask_{session.mask_names[idx]}.tif"
) # Make 'Clear image' button non-interactive base = os.path.join(session.gradio_temp, "qim3d", session.username)
output_masks_update = [ if not os.path.exists(base):
gr.Image(value=None, visible=True if i == 0 else False, interactive=False) os.makedirs(base)
for i in range(self.max_masks) filepath = os.path.join(base, filename)
] # Remove drawn masks and set as invisible except mask 1. 'interactive=False' added bc of bug after Gradio 3.44.x files_list.append(filepath)
return [ save(filepath, mask, replace=True)
selected_mask_update, session.temp_files.append(filepath)
prep_dl_btn_update,
save_output_update, return session, files_list
counts_update,
input_img_update,
upload_img_btn_update, def run_interface(host="0.0.0.0"):
clear_img_btn_update,
] + output_masks_update
def upload_img_update(self, file):
input_img_update = gr.Image(
value=load(file.name), interactive=True
) # Upload image from button to Image components
clear_img_btn_update = gr.Button(
interactive=True
) # Make 'Clear image' button interactive
upload_img_btn_update = gr.Button(
interactive=False
) # Make 'Upload image' button non-interactive
prep_dl_btn_update = gr.Button(
visible=True
) # Make 'Prepare mask for download' button visible
output_masks_update = [
gr.Image(interactive=True)
] * self.max_masks # This line is added bc of bug in Gradio after 3.44.x
return [
input_img_update,
clear_img_btn_update,
upload_img_btn_update,
prep_dl_btn_update,
] + output_masks_update
def run_interface(host = "0.0.0.0"):
gradio_interface = Interface().create_interface() gradio_interface = Interface().create_interface()
internal_tools.run_gradio_app(gradio_interface,host) qim3d.utils.internal_tools.run_gradio_app(gradio_interface, host)
if __name__ == "__main__": if __name__ == "__main__":
# Creates interface # Creates interface
run_interface() run_interface()
\ No newline at end of file
...@@ -4,4 +4,5 @@ from .augmentations import Augmentation ...@@ -4,4 +4,5 @@ from .augmentations import Augmentation
from .data import Dataset, prepare_datasets, prepare_dataloaders from .data import Dataset, prepare_datasets, prepare_dataloaders
#from .doi import get_bibtex, get_reference #from .doi import get_bibtex, get_reference
from . import doi from . import doi
from .system import Memory from .system import Memory
\ No newline at end of file from .img import overlay_rgb_images
\ No newline at end of file
import numpy as np
def overlay_rgb_images(background, foreground, alpha=0.5):
"""Overlay multiple RGB foreground onto an RGB background image using alpha blending.
Args:
background (numpy.ndarray): The background RGB image.
foreground (numpy.ndarray): The foreground RGB image (usually masks).
alpha (float, optional): The alpha value for blending. Defaults to 0.5.
Returns:
numpy.ndarray: The composite RGB image with overlaid foreground.
Raises:
ValueError: If input images have different shapes.
Note:
- The function performs alpha blending to overlay the foreground onto the background.
- It ensures that the background and foreground have the same shape before blending.
- It calculates the maximum projection of the foreground and blends them onto the background.
- Brightness outside the foreground is adjusted to maintain consistency with the background.
"""
# Igonore alpha in case its there
background = background[..., :3]
foreground = foreground[..., :3]
# Ensure both images have the same shape
if background.shape != foreground.shape:
raise ValueError("Input images must have the same shape")
# Perform alpha blending
foreground_max_projection = np.amax(foreground, axis=2)
foreground_max_projection = np.stack((foreground_max_projection,) * 3, axis=-1)
# Normalize if we have something
if np.max(foreground_max_projection) > 0:
foreground_max_projection = foreground_max_projection / np.max(foreground_max_projection)
composite = background * (1 - alpha) + foreground * alpha
composite = np.clip(composite, 0, 255).astype("uint8")
# Adjust brightness outside foreground
composite = composite + (background * (1 - alpha)) * (1 - foreground_max_projection)
return composite.astype("uint8")
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment