@@ -297,21 +297,22 @@ def set_fluence_nparray(beam, shaped_fluence, beamlet_size_mm=2.5):
297297 fluence = Fluence (_buffer , x_origin , y_origin )
298298 beam .SetOptimalFluence (fluence )
299299
300- def make_contour_mask_fast (structure_set , structure_obj , image_like ):
300+ def make_contour_mask_fast (structure_set , structure_obj , image_like , super_sample = 8 ):
301301 '''Generate a 3D mask from structure contours on an image-like object grid (e.g., Dose or Image).
302302 This function uses OpenCV to efficiently create a mask from the contours of a structure set.
303303 However, it does not exactly match the segment volume represented in Eclipse. For a more accurate representation,
304- use the `np_mask_like` method of the structure object.
304+ use the `np_mask_like` method of the structure object. Built with the help of Google Gemini and Claude.
305305
306306 Args:
307307 structure_set (pyesapi.StructureSet): The structure set containing the contours.
308308 structure_obj (pyesapi.Structure): The specific structure to create a mask for.
309309 image_like (pyesapi.Image or pyesapi.Dose): The image-like object that defines the grid for the mask.
310+ super_sample (int): The intermediate super-sampling factor to increase mask accuracy. Default is 8, where 1 means no super-sampling (but fastest).
310311 Returns:
311312 np.ndarray: A 3D boolean mask where True indicates the presence of the structure.
312313 '''
313314
314- # Import cv2 only when needed for mask generation
315+ # Import cv2 when needed for mask generation
315316 try :
316317 import cv2
317318 except ImportError :
@@ -332,9 +333,9 @@ def make_contour_mask_fast(structure_set, structure_obj, image_like):
332333 total_volume_mm3 = 0
333334 mask_3d = np .zeros ((nz , ny , nx ), dtype = bool )
334335
335- all_contrours_per_source_slice = []
336+ all_contrours_per_source_slice = []
336337
337- # note: the CT data (source) can be different from the requested mask geometry (sample), so we use the StructureSet's Image geo directly to extract contours
338+ # The CT data (source) can be different from the requested mask geometry (sample), so we use the StructureSet's Image geo directly to extract contours
338339
339340 # first loop over the Z slices of the structure set image and grab contours and signed areas
340341 for source_z_idx in range (structure_set .Image .ZSize ):
@@ -353,67 +354,67 @@ def make_contour_mask_fast(structure_set, structure_obj, image_like):
353354 print (f"WARNING: Skipping contour with < 3 points on slice index { source_z_idx } " )
354355 continue
355356
356- # contour_xy = ]contour_xyz[:, :2] # Extract X, Y coordinates
357+ # Extract X, Y coordinates
357358 x = np .array ([v [0 ] for v in contour_xyz ])
358359 y = np .array ([v [1 ] for v in contour_xyz ])
359360
360361 # Calculate SIGNED area using Shoelace formula
361- signed_area = - 0.5 * (np .dot (x , np .roll (y , 1 )) - np .dot (y , np .roll (x , 1 ))) # sign is flipped relative to this formulation
362+ signed_area = - 0.5 * (np .dot (x , np .roll (y , 1 )) - np .dot (y , np .roll (x , 1 ))) # sign is flipped relative to this formulation (coordinate system?)
362363 total_signed_area_on_slice_mm2 += signed_area
363364
364365 # Store polygon for mask generation
365366 slice_polygons .append ((x , y , signed_area ))
366367
367368 all_contrours_per_source_slice .append (slice_polygons )
368369
369- # Loop over the Z slices of the requested mask geometry
370+ # Now, loop over the Z slices of the requested mask geometry
370371 for sample_z_idx in range (nz ):
371- sample_z_position = sample_z_idx * z_res + origin_z
372+
373+ sample_z_position = sample_z_idx * z_res + origin_z - z_res / 2 # Center the sample slice position
372374
373375 # Find the closest Z slice in the source image index space
374- source_z_idx = np . floor ((sample_z_position - structure_set .Image .Origin .z )/ structure_set .Image .ZRes ).astype (int )
376+ source_z_idx = ((sample_z_position - structure_set .Image .Origin .z + structure_set . Image . ZRes / 2 )/ structure_set .Image .ZRes + np . finfo ( np . float64 ). tiny ).astype (int )
375377
376- # Only process if this Z sample slice is within the source z slices
378+ # Only process if this Z sample slice is within the source Z slices
377379 if source_z_idx < structure_set .Image .ZSize and source_z_idx >= 0 :
378380 # Create mask for this slice
379381 slice_mask = np .zeros ((ny , nx ), dtype = bool )
382+
380383 slice_polygons_list = all_contrours_per_source_slice [source_z_idx ]
381384 if slice_polygons_list is not None :
382- try :
383- for x , y , signed_area in slice_polygons_list : # Direct unpacking
384- if len (x ) == 0 :
385- continue
386- # Convert contour to pixel coordinates using non-isotropic resolution
387- contour_pixels = np .zeros ((len (x ), 2 ), dtype = np .int32 )
388- contour_pixels [:, 0 ] = np .round ((x - origin_x ) / x_res ) # x pixel coords
389- contour_pixels [:, 1 ] = np .round ((y - origin_y ) / y_res ) # y pixel coords
385+ for x , y , signed_area in slice_polygons_list :
386+ if len (x ) == 0 :
387+ continue
388+ # Convert contour to pixel coordinates using non-isotropic resolution
389+ contour_pixels = np .zeros ((len (x ), 2 ), dtype = np .int32 )
390+
391+ if super_sample > 1 :
392+ # Apply super-sampling to improve contour resolution
393+ contour_pixels [:, 0 ] = np .round ((x - origin_x + x_res / 2 ) / x_res * super_sample ) # x pixel coords
394+ contour_pixels [:, 1 ] = np .round ((y - origin_y + y_res / 2 ) / y_res * super_sample ) # y pixel coords
390395
396+ # Create a temporary mask for this contour using cv2.fillPoly
397+ temp_mask_hires = np .zeros ((ny * super_sample , nx * super_sample ), dtype = np .uint8 )
398+ cv2 .fillPoly (temp_mask_hires , [contour_pixels ], 1 , lineType = cv2 .LINE_8 )
399+ temp_mask = cv2 .resize (temp_mask_hires , (nx , ny ), interpolation = cv2 .INTER_AREA )
400+ temp_mask_bool = temp_mask .astype (bool )
401+ else :
402+ # Convert to pixel coordinates without super-sampling
403+ contour_pixels [:, 0 ] = np .round ((x - origin_x + x_res / 2 ) / x_res ).astype (np .int32 ) # x pixel coords
404+ contour_pixels [:, 1 ] = np .round ((y - origin_y + y_res / 2 ) / y_res ).astype (np .int32 ) # y pixel coords
391405 # Create a temporary mask for this contour using cv2.fillPoly
392406 temp_mask = np .zeros ((ny , nx ), dtype = np .uint8 )
393407 cv2 .fillPoly (temp_mask , [contour_pixels ], 1 , lineType = cv2 .LINE_8 )
394408 temp_mask_bool = temp_mask .astype (bool )
395-
396- # Add or subtract based on winding direction (signed area)
397- if signed_area > 0 : # Positive area - add to mask
398- slice_mask |= temp_mask_bool
399- else : # Negative area - subtract from mask (hole)
400- slice_mask &= ~ temp_mask_bool
401- except Exception as e :
402- # print(f"Error processing contour on slice {sample_z_idx} with source index {source_z_idx}: {e}")
403- print (f"Contour data: { slice_polygons_list } " )
404- raise Exception (f"Failed to process contour on slice { sample_z_idx } with source index { source_z_idx } : { e } " )
409+
410+ # Add or subtract based on winding direction (signed area)
411+ if signed_area > 0 : # Positive area - add to mask
412+ slice_mask |= temp_mask_bool
413+ else : # Negative area - subtract from mask (hole)
414+ slice_mask &= ~ temp_mask_bool
405415
406416 mask_3d [sample_z_idx ] = slice_mask
407417
408- # The final net area for the slice is the absolute value of the sum of signed areas
409- # total_area_on_slice_mm2 = np.abs(total_signed_area_on_slice_mm2)
410- total_area_on_slice_mm2 = total_signed_area_on_slice_mm2
411-
412- total_volume_mm3 += total_area_on_slice_mm2 * structure_set .Image .ZRes
413-
414- # Convert total volume from mm^3 to cm^3
415- total_volume_cc = total_volume_mm3 / 1000.0
416-
417418 return mask_3d .T
418419
419420## where the magic happens ##
0 commit comments