1111import skimage .measure
1212from PIL import Image , ImageEnhance
1313from skimage import draw , exposure
14+ from skimage import draw as skdraw
1415from skimage .util import img_as_ubyte
1516
1617
@@ -116,15 +117,15 @@ def draw_outline_on_image_from_outline(
116117 # Create a mask for non-black areas (with threshold)
117118 threshold = 10 # Adjust as needed
118119 # Grayscale
119- if outline_image .ndim == 2 : # noqa: PLR2004
120+ if outline_image .ndim == 2 :
120121 non_black_mask = outline_image > threshold
121122 else : # RGB/RGBA
122123 non_black_mask = np .any (outline_image [..., :3 ] > threshold , axis = - 1 )
123124
124125 # Ensure the original image is RGB
125- if orig_image .ndim == 2 : # noqa: PLR2004
126+ if orig_image .ndim == 2 :
126127 orig_image = np .stack ([orig_image ] * 3 , axis = - 1 )
127- elif orig_image .shape [- 1 ] != 3 : # noqa: PLR2004
128+ elif orig_image .shape [- 1 ] != 3 :
128129 raise ValueError ("Original image must have 3 channels (RGB)." )
129130
130131 # Ensure uint8 data type
@@ -168,14 +169,14 @@ def draw_outline_on_image_from_mask(
168169
169170 # Ensure the original image is RGB
170171 # Grayscale input
171- if orig_image .ndim == 2 : # noqa: PLR2004
172+ if orig_image .ndim == 2 :
172173 orig_image = np .stack ([orig_image ] * 3 , axis = - 1 )
173174 # Unsupported input
174- elif orig_image .shape [- 1 ] != 3 : # noqa: PLR2004
175+ elif orig_image .shape [- 1 ] != 3 :
175176 raise ValueError ("Original image must have 3 channels (RGB)." )
176177
177178 # Ensure the mask is 2D (binary)
178- if mask_image .ndim > 2 : # noqa: PLR2004
179+ if mask_image .ndim > 2 :
179180 mask_image = mask_image [..., 0 ] # Take the first channel if multi-channel
180181
181182 # Detect contours from the mask
@@ -248,17 +249,17 @@ def equalize_and_adjust(channel: np.ndarray) -> np.ndarray:
248249 gamma = 1.0 - brightness_shift * 0.8 # e.g. 1.8 → dark, 0.2 → bright
249250 return exposure .adjust_gamma (eq , gamma = gamma )
250251
251- if image .ndim == 2 : # noqa: PLR2004
252+ if image .ndim == 2 :
252253 result = equalize_and_adjust (image )
253254 return img_as_ubyte (result )
254255
255- elif image .ndim == 3 and image .shape [2 ] == 3 : # noqa: PLR2004
256+ elif image .ndim == 3 and image .shape [2 ] == 3 :
256257 result = np .stack (
257258 [equalize_and_adjust (image [:, :, i ]) for i in range (3 )], axis = - 1
258259 )
259260 return img_as_ubyte (result )
260261
261- elif image .ndim == 3 and image .shape [2 ] == 4 : # noqa: PLR2004
262+ elif image .ndim == 3 and image .shape [2 ] == 4 :
262263 rgb = image [:, :, :3 ]
263264 alpha = image [:, :, 3 ]
264265 result = np .stack (
@@ -322,17 +323,78 @@ def add_image_scale_bar( # noqa: PLR0913
322323 margin_px : int = 10 ,
323324 ** _ : Dict [Any , Any ],
324325) -> np .ndarray :
326+ """
327+ Add a scale bar to the lower or upper corner of an image.
328+
329+ The function overlays a solid rectangular scale bar onto a grayscale,
330+ RGB, or RGBA image. The bar's physical length in micrometers is
331+ converted to pixels using the provided microns-per-pixel value.
332+ Non-finite or out-of-range input values are sanitized before drawing.
333+
334+ Args:
335+ img (np.ndarray):
336+ Input image, either 2-D grayscale or 3-D RGB/RGBA.
337+ um_per_pixel (float):
338+ Micrometers per pixel. If None or ≤ 0, the image is returned
339+ unchanged.
340+ length_um (float, optional):
341+ Desired length of the scale bar in micrometers. Defaults to
342+ 10.0.
343+ thickness_px (int, optional):
344+ Thickness of the bar in pixels. Defaults to 4.
345+ color (Tuple[int, int, int], optional):
346+ RGB color of the bar. Defaults to white ``(255, 255, 255)``.
347+ location (str, optional):
348+ Placement of the bar: ``"lower right"``, ``"lower left"``,
349+ ``"upper right"``, or ``"upper left"``. Defaults to
350+ ``"lower right"``.
351+ margin_px (int, optional):
352+ Distance in pixels between the bar and the image edges.
353+ Defaults to 10.
354+ **_ (Dict[Any, Any]):
355+ Additional keyword arguments ignored for forward
356+ compatibility.
357+
358+ Returns:
359+ np.ndarray:
360+ A new RGB image with the scale bar drawn. The array is always
361+ of type ``uint8`` and has shape ``(H, W, 3)``.
362+
363+ Raises:
364+ ValueError: If the input image has an unsupported number of
365+ channels.
366+
367+ Notes:
368+ The bar length is clamped to fit within image bounds after
369+ margins. The function does not rely on ``skimage.color`` and is
370+ safe against NaN or Inf input values.
371+ """
372+
325373 if um_per_pixel is None or um_per_pixel <= 0 :
326374 return img
327375
328- out = img .copy ()
329- # ensure RGB uint8
330- if out .ndim == 2 : # noqa: PLR2004
331- out = skimage .color .gray2rgb (out )
332- elif out .ndim == 3 and out .shape [2 ] == 4 : # noqa: PLR2004
333- out = out [:, :, :3 ]
376+ # --- Sanitize input first: replace non-finite, clip to valid range, cast to uint8
377+ out = np .nan_to_num (img , nan = 0.0 , posinf = 255.0 , neginf = 0.0 )
378+
379+ # If float image, try to guess range and bring to 0..255
380+ if np .issubdtype (out .dtype , np .floating ):
381+ # If values look like 0..1, scale to 0..255; otherwise clip to 0..255
382+ if out .max () <= 1.0 :
383+ out = out * 255.0
384+ out = np .clip (out , 0 , 255 )
385+
334386 out = out .astype (np .uint8 , copy = False )
335387
388+ # ensure RGB without using skimage.color.* conversions
389+ if out .ndim == 2 : # grayscale -> stack
390+ out = np .dstack ([out , out , out ])
391+ elif out .ndim == 3 and out .shape [2 ] == 4 : # RGBA -> drop alpha for drawing
392+ out = out [:, :, :3 ]
393+ elif out .ndim != 3 or out .shape [2 ] != 3 :
394+ raise ValueError (
395+ "Unsupported image shape for scale bar; expected gray/RGB/RGBA."
396+ )
397+
336398 H , W = out .shape [:2 ]
337399 length_px = max (1 , round (length_um / um_per_pixel ))
338400 thickness_px = max (1 , int (thickness_px ))
@@ -343,8 +405,6 @@ def add_image_scale_bar( # noqa: PLR0913
343405
344406 loc = location .lower ().strip ()
345407
346- # Compute starting corner (y0, x0) and use rectangle
347- # extent = (thickness_px, length_px)
348408 y0 = (
349409 max (0 , H - margin_px - thickness_px )
350410 if "lower" in loc
@@ -358,7 +418,7 @@ def add_image_scale_bar( # noqa: PLR0913
358418 if x0 + length_px > W :
359419 length_px = W - x0
360420
361- rr , cc = skimage . draw .rectangle (
421+ rr , cc = skdraw .rectangle (
362422 start = (y0 , x0 ),
363423 extent = (thickness_px , length_px ),
364424 shape = out .shape [:2 ],
0 commit comments