From 714e76c80c46a23a38972c0b1861774d7ff3de88 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 21 Sep 2026 14:38:51 -0400 Subject: [PATCH 1/2] Simplify alpha_blend/alpha_blend_masked and overlay_segmentations - Split alpha_blend into a plain alpha_blend (no masks) and a new alpha_blend_masked (mask1/mask2) that reuses it, reducing temporaries and avoiding unnecessary casts. - alpha_blend now blends vector images per-channel with in-place operators instead of allocating a full mask3-image intersection. - overlay_segmentations now uses ToScalarImage + BinaryProjectionImageFilter plus masked assignment instead of NaryAdd + mask_image_multiply, removing a redundant comparison and temporary images. --- Python/05_Results_Visualization.ipynb | 121 +++++++++++++++----------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/Python/05_Results_Visualization.ipynb b/Python/05_Results_Visualization.ipynb index d44b3fdf..a9e1209f 100644 --- a/Python/05_Results_Visualization.ipynb +++ b/Python/05_Results_Visualization.ipynb @@ -292,59 +292,80 @@ "metadata": {}, "outputs": [], "source": [ - "def mask_image_multiply(mask, image):\n", - " components_per_pixel = image.GetNumberOfComponentsPerPixel()\n", - " if components_per_pixel == 1:\n", - " return mask * image\n", + "def alpha_blend(image1, image2, alpha=0.5):\n", + " \"\"\"\n", + " Alpha blend two images, pixels can be scalars or vectors.\n", + " The alpha blending factor can be either a scalar or an image whose\n", + " pixel type is sitkFloat32 and values are in [0,1].\n", + " \"\"\"\n", + " components_per_pixel = image1.GetNumberOfComponentsPerPixel()\n", + " if components_per_pixel > 1:\n", + " # MultiplyImageFilter doesn't support a vector image times a scalar\n", + " # (per-pixel) image, so blend one channel at a time. The 1-alpha\n", + " # factor doesn't depend on the channel, so it's only computed once.\n", + " one_minus_alpha = 1 - alpha\n", + " channels = []\n", + " for channel in range(components_per_pixel):\n", + " scaled1 = sitk.VectorIndexSelectionCast(image1, channel, sitk.sitkFloat32)\n", + " scaled1 *= alpha\n", + " blended_channel = sitk.VectorIndexSelectionCast(\n", + " image2, channel, sitk.sitkFloat32\n", + " )\n", + " blended_channel *= one_minus_alpha\n", + " blended_channel += scaled1\n", + " channels.append(blended_channel)\n", + " return sitk.Compose(channels)\n", " else:\n", - " return sitk.Compose(\n", - " [\n", - " mask * sitk.VectorIndexSelectionCast(image, channel)\n", - " for channel in range(components_per_pixel)\n", - " ]\n", - " )\n", + " scaled1 = sitk.Cast(image1, sitk.sitkFloat32)\n", + " scaled1 *= alpha\n", + " blended = sitk.Cast(image2, sitk.sitkFloat32)\n", + " blended *= 1 - alpha\n", + " blended += scaled1\n", + " return blended\n", "\n", "\n", - "def alpha_blend(image1, image2, alpha=0.5, mask1=None, mask2=None):\n", + "def alpha_blend_masked(image1, image2, alpha=0.5, mask1=None, mask2=None):\n", " \"\"\"\n", - " Alaph blend two images, pixels can be scalars or vectors.\n", - " The alpha blending factor can be either a scalar or an image whose\n", - " pixel type is sitkFloat32 and values are in [0,1].\n", - " The region that is alpha blended is controled by the given masks.\n", + " Alpha blend two images as done by alpha_blend, restricted to the region(s)\n", + " indicated by the given masks. Masks are treated as binary indicators\n", + " (non-zero selects the region), as returned by, e.g., OtsuThreshold.\n", " \"\"\"\n", + " if mask1 is None and mask2 is None:\n", + " return alpha_blend(image1, image2, alpha)\n", "\n", - " if not mask1:\n", - " mask1 = sitk.Image(image1.GetSize(), sitk.sitkFloat32) + 1.0\n", - " mask1.CopyInformation(image1)\n", - " else:\n", - " mask1 = sitk.Cast(mask1, sitk.sitkFloat32)\n", - " if not mask2:\n", - " mask2 = sitk.Image(image2.GetSize(), sitk.sitkFloat32) + 1\n", - " mask2.CopyInformation(image2)\n", - " else:\n", - " mask2 = sitk.Cast(mask2, sitk.sitkFloat32)\n", - " # if we received a scalar, convert it to an image\n", + " # Build a per-pixel alpha image so the mask1/mask2-only regions can be\n", + " # expressed as alpha=1/alpha=0 and blended by reusing alpha_blend. The\n", + " # neither-mask region can't be expressed this way (it's 0, not image1\n", + " # or image2), so it's overwritten separately below.\n", " if type(alpha) != sitk.SimpleITK.Image:\n", - " alpha = sitk.Image(image1.GetSize(), sitk.sitkFloat32) + alpha\n", - " alpha.CopyInformation(image1)\n", - " components_per_pixel = image1.GetNumberOfComponentsPerPixel()\n", - " if components_per_pixel > 1:\n", - " img1 = sitk.Cast(image1, sitk.sitkVectorFloat32)\n", - " img2 = sitk.Cast(image2, sitk.sitkVectorFloat32)\n", + " alpha_image = sitk.Image(image1.GetSize(), sitk.sitkFloat32) + alpha\n", + " alpha_image.CopyInformation(image1)\n", " else:\n", - " img1 = sitk.Cast(image1, sitk.sitkFloat32)\n", - " img2 = sitk.Cast(image2, sitk.sitkFloat32)\n", - "\n", - " intersection_mask = mask1 * mask2\n", - "\n", - " intersection_image = mask_image_multiply(\n", - " alpha * intersection_mask, img1\n", - " ) + mask_image_multiply((1 - alpha) * intersection_mask, img2)\n", - " return (\n", - " intersection_image\n", - " + mask_image_multiply(mask2 - intersection_mask, img2)\n", - " + mask_image_multiply(mask1 - intersection_mask, img1)\n", - " )" + " # Copy so the caller's alpha image isn't mutated by the in-place\n", + " # mask edits below.\n", + " alpha_image = sitk.Image(alpha)\n", + "\n", + " # Comparison operators always return a sitkUInt8 image, regardless of the\n", + " # input pixel type, so there's no need to cast beforehand.\n", + " if mask1 is not None:\n", + " mask1 = mask1 != 0\n", + " if mask2 is not None:\n", + " mask2 = mask2 != 0\n", + "\n", + " if mask2 is None: # only mask1 given, outside it show image2 as is\n", + " alpha_image[mask1 == 0] = 0.0\n", + " elif mask1 is None: # only mask2 given, outside it show image1 as is\n", + " alpha_image[mask2 == 0] = 1.0\n", + " else: # both given, only the intersection is blended\n", + " alpha_image[mask1 * (mask2 == 0)] = 1.0\n", + " alpha_image[mask2 * (mask1 == 0)] = 0.0\n", + "\n", + " blended = alpha_blend(image1, image2, alpha_image)\n", + "\n", + " if mask1 is not None and mask2 is not None:\n", + " neither = (mask1 == 0) * (mask2 == 0)\n", + " blended[neither] = 0\n", + " return blended" ] }, { @@ -363,10 +384,10 @@ "# Combine the two volumes\n", "images_list = [\n", " (alpha_blend(img1_255, img2_255), \"alpha_blend_standard\"),\n", - " (alpha_blend(img1_255, img2_255, mask1=msk1), \"alpha_blend_mask1\"),\n", - " (alpha_blend(img1_255, img2_255, mask2=msk2), \"alpha_blend_mask2\"),\n", + " (alpha_blend_masked(img1_255, img2_255, mask1=msk1), \"alpha_blend_mask1\"),\n", + " (alpha_blend_masked(img1_255, img2_255, mask2=msk2), \"alpha_blend_mask2\"),\n", " (\n", - " alpha_blend(img1_255, img2_255, mask1=msk1, mask2=msk2),\n", + " alpha_blend_masked(img1_255, img2_255, mask1=msk1, mask2=msk2),\n", " \"alpha_blend_mask1_mask2\",\n", " ),\n", "]\n", @@ -557,7 +578,7 @@ "source": [ "gui.MultiImageDisplay(\n", " image_list=[\n", - " alpha_blend(img1_255, img2_255, mask2=msk2),\n", + " alpha_blend_masked(img1_255, img2_255, mask2=msk2),\n", " multiresolution_blend(img1_255, img2_255, msk2),\n", " ],\n", " title_list=[\"alpha_blend_mask2\", \"multiresolution_blend_mask2\"],\n", @@ -718,7 +739,7 @@ " )\n", "elif blending_approach == \"mask\":\n", " combined_volume = sitk.Cast(\n", - " alpha_blend(\n", + " alpha_blend_masked(\n", " sitk.Compose(img1_255, img1_255, img1_255), overlay_color_img, mask2=roi\n", " ),\n", " sitk.sitkVectorUInt8,\n", From 874d27b5e49c0bf3c735b5203b3ea3944a04a9c3 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Wed, 23 Sep 2026 11:26:34 -0400 Subject: [PATCH 2/2] Adding missing words to additional_dictionary --- tests/additional_dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/additional_dictionary.txt b/tests/additional_dictionary.txt index ca4c45a7..8e7c5616 100644 --- a/tests/additional_dictionary.txt +++ b/tests/additional_dictionary.txt @@ -200,6 +200,7 @@ MeanSquares MetaDataDictionaryArrayUpdateOn MetaImageIO MetricEvaluate +MultiplyImageFilter Multiresolution NamedTemporaryFile Nanometers