From 70458878d7c84c7f2cb23eb203cc321aa119aa6e Mon Sep 17 00:00:00 2001 From: emiglietta Date: Thu, 15 Jan 2026 10:11:52 -0800 Subject: [PATCH 1/2] Create brightfieldprojectchannels.py working plugin that aims to reproduce the 'Brightfield' projection of the MakeProjection module but on different images (z-planes that have been stored as different channels) --- active_plugins/brightfieldprojectchannels.py | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 active_plugins/brightfieldprojectchannels.py diff --git a/active_plugins/brightfieldprojectchannels.py b/active_plugins/brightfieldprojectchannels.py new file mode 100644 index 0000000..c5e3330 --- /dev/null +++ b/active_plugins/brightfieldprojectchannels.py @@ -0,0 +1,113 @@ +import numpy +import scipy.ndimage +from cellprofiler_core.image import Image +from cellprofiler_core.module import Module +from cellprofiler_core.setting import SettingsGroup, HiddenCount +from cellprofiler_core.setting.do_something import DoSomething, RemoveSettingButton +from cellprofiler_core.setting.subscriber import ImageSubscriber +from cellprofiler_core.setting.text import ImageName + +class BrightfieldProjectChannels(Module): + module_name = "BrightfieldProjectChannels" + variable_revision_number = 1 + category = "Image Processing" + + def create_settings(self): + self.output_image_name = ImageName( + "Name the output image", + "BrightfieldProjected", + doc="Enter a name for the resulting projected image." + ) + + self.stack_channels = [] + self.stack_channel_count = HiddenCount(self.stack_channels) + self.add_stack_channel_cb(can_remove=False) + + self.add_stack_channel = DoSomething( + "Add another image", + "Add another image", + self.add_stack_channel_cb + ) + + def add_stack_channel_cb(self, can_remove=True): + group = SettingsGroup() + group.append("image_name", ImageSubscriber("Select image", "None")) + if can_remove: + group.append("remover", RemoveSettingButton("", "Remove", self.stack_channels, group)) + self.stack_channels.append(group) + + def settings(self): + result = [self.output_image_name, self.stack_channel_count] + for stack_channel in self.stack_channels: + result += [stack_channel.image_name] + return result + + def visible_settings(self): + result = [self.output_image_name] + for sc_group in self.stack_channels: + result.append(sc_group.image_name) + if hasattr(sc_group, "remover"): + result.append(sc_group.remover) + result.append(self.add_stack_channel) + return result + + def prepare_settings(self, setting_values): + try: + num_stack_images = int(setting_values[1]) + except (ValueError, IndexError): + num_stack_images = 1 + del self.stack_channels[num_stack_images:] + while len(self.stack_channels) < num_stack_images: + self.add_stack_channel_cb() + + def run(self, workspace): + image_list = [] + for group in self.stack_channels: + img_name = group.image_name.value + data = workspace.image_set.get_image(img_name).pixel_data + image_list.append(data) + + if not image_list: + return + + stack = numpy.array(image_list) + + # 1. Local Variance (3x3) + def get_cp_variance(img): + mean = scipy.ndimage.uniform_filter(img, size=3) + sq_mean = scipy.ndimage.uniform_filter(img**2, size=3) + return sq_mean - mean**2 + + variances = numpy.array([get_cp_variance(img) for img in stack]) + + # 2. Gaussian Smoothing (Sigma=1.0) + smoothed_variances = numpy.array([ + scipy.ndimage.gaussian_filter(v, sigma=1.0) for v in variances + ]) + + # 3. Find best focus indices + best_indices = numpy.argmax(smoothed_variances, axis=0) + + # 4. Extract winning pixels + height, width = best_indices.shape + ii, jj = numpy.ogrid[:height, :width] + output_pixels = stack[best_indices, ii, jj] + + # Save to workspace + new_image = Image(output_pixels) + workspace.image_set.add(self.output_image_name.value, new_image) + + # Store for display + if self.show_window: + workspace.display_data.output_pixels = output_pixels + + def display(self, workspace, figure): + """Displays the resulting projection in a CellProfiler window.""" + pixels = workspace.display_data.output_pixels + + figure.set_subplots((1, 1)) + # Use sharexy=True so zooming/panning works smoothly + figure.subplot_imshow(0, 0, pixels, title=self.output_image_name.value, colormap="gray") #viridis is the default colormap, or fire + + def upgrade_settings(self, setting_values, variable_revision_number, module_name): + return setting_values, variable_revision_number \ No newline at end of file From e643caf659f22482b12ec7f5fa0682f05257cb5a Mon Sep 17 00:00:00 2001 From: emiglietta Date: Fri, 16 Jan 2026 18:59:27 -0800 Subject: [PATCH 2/2] Update brightfieldprojectchannels.py currently it works correctly to emulate the Brightfield projection --- active_plugins/brightfieldprojectchannels.py | 103 ++++++++++--------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/active_plugins/brightfieldprojectchannels.py b/active_plugins/brightfieldprojectchannels.py index c5e3330..7d42fc9 100644 --- a/active_plugins/brightfieldprojectchannels.py +++ b/active_plugins/brightfieldprojectchannels.py @@ -1,5 +1,4 @@ import numpy -import scipy.ndimage from cellprofiler_core.image import Image from cellprofiler_core.module import Module from cellprofiler_core.setting import SettingsGroup, HiddenCount @@ -15,8 +14,8 @@ class BrightfieldProjectChannels(Module): def create_settings(self): self.output_image_name = ImageName( "Name the output image", - "BrightfieldProjected", - doc="Enter a name for the resulting projected image." + "ProjectionBlue", + doc="Enter the name for the projected image." ) self.stack_channels = [] @@ -61,53 +60,63 @@ def prepare_settings(self, setting_values): self.add_stack_channel_cb() def run(self, workspace): - image_list = [] - for group in self.stack_channels: - img_name = group.image_name.value - data = workspace.image_set.get_image(img_name).pixel_data - image_list.append(data) - - if not image_list: - return - - stack = numpy.array(image_list) - - # 1. Local Variance (3x3) - def get_cp_variance(img): - mean = scipy.ndimage.uniform_filter(img, size=3) - sq_mean = scipy.ndimage.uniform_filter(img**2, size=3) - return sq_mean - mean**2 - - variances = numpy.array([get_cp_variance(img) for img in stack]) - - # 2. Gaussian Smoothing (Sigma=1.0) - smoothed_variances = numpy.array([ - scipy.ndimage.gaussian_filter(v, sigma=1.0) for v in variances - ]) + bright_max = None + bright_min = None + norm0 = None + reference_image = None + final_mask = None - # 3. Find best focus indices - best_indices = numpy.argmax(smoothed_variances, axis=0) - - # 4. Extract winning pixels - height, width = best_indices.shape - ii, jj = numpy.ogrid[:height, :width] - output_pixels = stack[best_indices, ii, jj] - - # Save to workspace - new_image = Image(output_pixels) - workspace.image_set.add(self.output_image_name.value, new_image) - - # Store for display - if self.show_window: - workspace.display_data.output_pixels = output_pixels + for i, group in enumerate(self.stack_channels): + img_name = group.image_name.value + if img_name == "None": + continue + + image_obj = workspace.image_set.get_image(img_name) + pixels = image_obj.pixel_data.copy() + mask = image_obj.mask if image_obj.has_mask else numpy.ones(pixels.shape[:2], bool) + + if bright_max is None: + # Initialization (replicates set_image) + reference_image = image_obj + bright_max = pixels.copy() + bright_min = pixels.copy() + norm0 = numpy.mean(pixels) + final_mask = mask.copy() + else: + # Accumulation (replicates accumulate_image for P_BRIGHTFIELD) + norm = numpy.mean(pixels) + # Normalize pixels relative to the first image + rescaled_pixels = pixels * (norm0 / norm) if norm != 0 else pixels + + # Identify where new pixels are higher/lower + max_mask = (bright_max < rescaled_pixels) & mask + min_mask = (bright_min > rescaled_pixels) & mask + + bright_min[min_mask] = rescaled_pixels[min_mask] + bright_max[max_mask] = rescaled_pixels[max_mask] + + # This specific line from your source ensures min follows max if max is updated + bright_min[max_mask] = bright_max[max_mask] + + # Combine masks (replicates P_MASK or standard mask handling) + final_mask = final_mask & mask + + if bright_max is not None: + # Replicates provide_image: result = max - min + output_pixels = bright_max - bright_min + + # CRITICAL: Setting parent_image ensures SaveImages/FlagImages works + new_image = Image(output_pixels, mask=final_mask, parent_image=reference_image) + workspace.image_set.add(self.output_image_name.value, new_image) + + if self.show_window: + workspace.display_data.output_pixels = output_pixels def display(self, workspace, figure): - """Displays the resulting projection in a CellProfiler window.""" - pixels = workspace.display_data.output_pixels - - figure.set_subplots((1, 1)) - # Use sharexy=True so zooming/panning works smoothly - figure.subplot_imshow(0, 0, pixels, title=self.output_image_name.value, colormap="gray") #viridis is the default colormap, or fire + if hasattr(workspace.display_data, 'output_pixels'): + pixels = workspace.display_data.output_pixels + figure.set_subplots((1, 1)) + figure.subplot_imshow(0, 0, pixels, title=self.output_image_name.value, colormap="gray") def upgrade_settings(self, setting_values, variable_revision_number, module_name): return setting_values, variable_revision_number \ No newline at end of file