1+ import numpy as np
2+ import matplotlib .pyplot as plt
3+ from skimage .measure import regionprops , label
4+ from skimage .filters import threshold_otsu
5+
6+ # --- 1. Generate a synthetic microscopy image ---
7+ # Create a blank canvas
8+ image_size = 200
9+ image = np .zeros ((image_size , image_size ))
10+
11+ # Create coordinates for the image
12+ x , y = np .mgrid [0 :image_size , 0 :image_size ]
13+
14+ # Define the "cell" using a 2D Gaussian profile
15+ # This is our measurement channel (Channel A)
16+ radius_x , radius_y = 30 , 40
17+ center_x , center_y = image_size // 2 , image_size // 2
18+ intensity_profile = np .exp (- ((x - center_x )** 2 / (2 * radius_x ** 2 ) + (y - center_y )** 2 / (2 * radius_y ** 2 )))
19+
20+ # Add intensity and some random noise
21+ channel_A_measurement = 150 * intensity_profile + 20 * np .random .rand (image_size , image_size )
22+ channel_A_measurement = np .clip (channel_A_measurement , 0 , 255 )
23+
24+ # Create an ideal segmentation channel (Channel B)
25+ # This channel clearly marks the entire object's boundary
26+ # The shape is made slightly larger to simulate a common biological reality (e.g., cytoplasm around a signal)
27+ segment_radius_x , segment_radius_y = 35 , 45
28+ channel_B_segmentation = (((x - center_x )** 2 / segment_radius_x ** 2 + (y - center_y )** 2 / segment_radius_y ** 2 ) < 1 ).astype (float ) * 200
29+ channel_B_segmentation += 20 * np .random .rand (image_size , image_size )
30+ channel_B_segmentation = np .clip (channel_B_segmentation , 0 , 255 )
31+
32+
33+ # --- 2. BIASED APPROACH: Segmenting and measuring in the same channel (Channel A) ---
34+ # Calculate a threshold based on the measurement channel
35+ thresh_A = threshold_otsu (channel_A_measurement )
36+
37+ # Create a mask: only the brightest pixels are selected
38+ biased_mask = channel_A_measurement > thresh_A
39+
40+ # Measure the mean intensity within this biased mask
41+ biased_intensity = np .mean (channel_A_measurement [biased_mask ])
42+
43+
44+ # --- 3. CORRECT APPROACH: Segmenting in Channel B, measuring in Channel A ---
45+ # Calculate a threshold on the ideal segmentation channel
46+ thresh_B = threshold_otsu (channel_B_segmentation )
47+
48+ # Create an unbiased mask that covers the entire object
49+ unbiased_mask = channel_B_segmentation > thresh_B
50+
51+ # Apply the unbiased mask to the measurement channel and measure the intensity
52+ unbiased_intensity = np .mean (channel_A_measurement [unbiased_mask ])
53+
54+
55+ # --- 4. Visualize the results ---
56+ fig , axes = plt .subplots (2 , 3 , figsize = (18 , 12 ))
57+ fig .suptitle ('Comparison of Biased vs. Unbiased Measurement Method' , fontsize = 20 )
58+ plt .subplots_adjust (top = 0.9 )
59+
60+ # Column titles
61+ axes [0 , 0 ].set_title ("Source Images" , fontsize = 14 )
62+ axes [0 , 1 ].set_title ("Problem: Segmenting the Measurement Channel" , fontsize = 14 )
63+ axes [0 , 2 ].set_title ("Solution: Using an Independent Channel" , fontsize = 14 )
64+
65+ # Row 1: Images and mask overlays
66+ ax = axes [0 , 0 ]
67+ im = ax .imshow (channel_A_measurement , cmap = 'viridis' )
68+ ax .set_ylabel ("Channel A (Measurement)" , fontsize = 12 )
69+ ax .axis ('off' )
70+ plt .colorbar (im , ax = ax , fraction = 0.046 , pad = 0.04 )
71+
72+ ax = axes [0 , 1 ]
73+ ax .imshow (channel_A_measurement , cmap = 'viridis' )
74+ ax .contour (biased_mask , colors = 'red' , linewidths = 1.5 ) # Red contour for the biased mask
75+ ax .text (5 , 190 , 'Mask from Channel A' , color = 'white' , backgroundcolor = 'red' )
76+ ax .axis ('off' )
77+
78+ ax = axes [0 , 2 ]
79+ ax .imshow (channel_A_measurement , cmap = 'viridis' )
80+ ax .contour (unbiased_mask , colors = 'cyan' , linewidths = 1.5 ) # Cyan contour for the correct mask
81+ ax .text (5 , 190 , 'Mask from Channel B' , color = 'black' , backgroundcolor = 'cyan' )
82+ ax .axis ('off' )
83+
84+ # Row 2: Segmentation channel and masks
85+ ax = axes [1 , 0 ]
86+ im = ax .imshow (channel_B_segmentation , cmap = 'magma' )
87+ ax .set_ylabel ("Channel B (Segmentation)" , fontsize = 12 )
88+ ax .axis ('off' )
89+ plt .colorbar (im , ax = ax , fraction = 0.046 , pad = 0.04 )
90+
91+ ax = axes [1 , 1 ]
92+ ax .imshow (biased_mask , cmap = 'gray' )
93+ ax .set_title ("Mask selects only the brightest region" , fontsize = 12 )
94+ ax .axis ('off' )
95+
96+ ax = axes [1 , 2 ]
97+ ax .imshow (unbiased_mask , cmap = 'gray' )
98+ ax .set_title ("Mask covers the entire object" , fontsize = 12 )
99+ ax .axis ('off' )
100+
101+ # Add the final measurement results to the bottom of the figure
102+ plt .figtext (0.5 , 0.02 ,
103+ f"BIASED APPROACH: Measured Mean Intensity = { biased_intensity :.2f} \n "
104+ f"CORRECT APPROACH: Measured Mean Intensity = { unbiased_intensity :.2f} \n "
105+ f"DIFFERENCE: The biased measurement is artificially inflated by { ((biased_intensity - unbiased_intensity ) / unbiased_intensity ) * 100 :.1f} %!" ,
106+ ha = "center" , fontsize = 16 , bbox = {"facecolor" :"lightcoral" , "alpha" :0.6 , "pad" :10 })
107+
108+ plt .tight_layout (rect = [0 , 0.08 , 1 , 0.95 ])
109+ plt .show ()
0 commit comments