Skip to content

Shape Analysis#

The functions used to calculate morphological parameters of the bacteria.

extend_medial_axis(medial_axis: np.array, boundary: np.array, max_iter: int = 50, error_threshold: float = 0.05, min_distance_to_boundary: int = 1, step_size: float or int = 1) -> np.array #

Extend the medial axis to the boundary, using the boundary as a reference, then smooth it to get the final result.

Parameters:

Name Type Description Default
medial_axis np.array

The medial axis.

required
boundary np.array

The boundary.

required
max_iter int

The maximum number of iterations for extending the medial axis(default is 50).

50
min_distance_to_boundary int

The minimum distance to the boundary (default is 1). This determines when the extension stops.

1
step_size float or int

The step size to take when extending the medial axis (default is 1 px).

1

Returns:

Name Type Description
extended_medial_axis np.array

The extended and smoothed medial axis.

Source code in src/micromorph/bacteria/shape_analysis.py
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
def extend_medial_axis(medial_axis: np.array, boundary: np.array, max_iter: int = 50,
                       error_threshold: float = 0.05, min_distance_to_boundary: int = 1, step_size: float or int = 1) -> np.array:
    """
    Extend the medial axis to the boundary, using the boundary as a reference, then smooth it to get the final result.

    Parameters
    ----------
    medial_axis : np.array
        The medial axis.
    boundary : np.array
        The boundary.
    max_iter : int, optional
        The maximum number of iterations for extending the medial axis(default is 50).
    min_distance_to_boundary : int, optional
        The minimum distance to the boundary (default is 1). This determines when the extension stops.
    step_size : float or int, optional
        The step size to take when extending the medial axis (default is 1 px).

    Returns
    -------
    extended_medial_axis: np.array
        The extended and smoothed medial axis.
    """

    medial_axis_extended_roughly = extend_medial_axis_roughly(medial_axis, boundary, step_size=0.05)


    # medial_axis_extended = smooth_medial_axis(medial_axis_extended_roughly, boundary, error_threshold=0.0005)

    return medial_axis_extended_roughly

extend_medial_axis_roughly(medax, bnd, max_iter=500, min_distance_to_boundary=1, step_size=1) #

Extend the medial axis to the boundary, without any smoothing.

Parameters:

Name Type Description Default
medax np.array

The medial axis.

required
bnd np.array

The boundary.

required
max_iter int

The maximum number of iterations (default is 50).

500
min_distance_to_boundary int

The minimum distance to the boundary (default is 1).

1
step_size int

The step size to take (default is 1).

1

Returns:

Name Type Description
extended_medial_axis np.array

The extended medial axis.

Source code in src/micromorph/bacteria/shape_analysis.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
def extend_medial_axis_roughly(medax, bnd, max_iter=500, min_distance_to_boundary=1, step_size=1):
    """
    Extend the medial axis to the boundary, without any smoothing.

    Parameters
    ----------
    medax : np.array
        The medial axis.
    bnd : np.array
        The boundary.
    max_iter : int, optional
        The maximum number of iterations (default is 50).
    min_distance_to_boundary : int, optional
        The minimum distance to the boundary (default is 1).
    step_size : int, optional
        The step size to take (default is 1).

    Returns
    -------
    extended_medial_axis : np.array
        The extended medial axis.
    """
    medax_ext = np.copy(medax)

    # Generate a polygon to use for checking if the points are inside the boundary
    boundary_polygon = Polygon(bnd)
    # medial_axis = np.array([point for point in medial_axis if Point(point).within(boundary_polygon)])

    # We need to check that there's at least two points in the medial axis
    if medax.shape[0] < 2:
        return medax_ext

    # Direction second to last to last point
    direction = np.arctan2(medax[-1, 1] - medax[-2, 1], medax[-1, 0] - medax[-2, 0])

    dist_to_bound = np.inf
    N = 0
    while N < max_iter: #dist_to_bound > min_distance_to_boundary and 
        step_vector = step_size * np.array([np.cos(direction), np.sin(direction)])
        next_point = medax_ext[-1, :] + step_vector
        distances = np.sqrt((next_point[0] - bnd[:, 0]) ** 2 + (next_point[1] - bnd[:, 1]) ** 2)

        # check if the next point is outside the boundary
        if Point(next_point[0], next_point[1]).within(boundary_polygon):
            # logging.info("Next point is inside. Continuing")
            medax_ext = np.vstack((medax_ext, next_point))  
        else:
            # logging.info("Next point is outside. Stopping")
            medax_ext = np.vstack((medax_ext, next_point))  
            break


        N += 1

    # FIXME: UGLY - probably needs a second function which is called twice, not this ...
    # Repeat in the opposite direction

    # Direction second to first point
    direction = np.arctan2(medax[1, 1] - medax[0, 1], medax[1, 0] - medax[0, 0])

    dist_to_bound = np.inf
    N = 0
    while N < max_iter: # dist_to_bound > min_distance_to_boundary and 
        step_vector = -step_size * np.array([np.cos(direction), np.sin(direction)]) * 2
        next_point = medax_ext[0, :] + step_vector
    #     dist_to_bound = np.min(np.sqrt((next_point[0] - bnd[:, 0]) ** 2 + (next_point[1] - bnd[:, 1]) ** 2))
    #     # check if the next point is outside the boundaryp
    #     logging.info(f"Next point: {next_point}, distance to boundary: {dist_to_bound}")
        # check if the next point is outside the boundary#
        if Point(next_point[0], next_point[1]).within(boundary_polygon):
            medax_ext = np.vstack((next_point, medax_ext))
        else:
            logging.debug("Next point is outside. Stopping")
            break

        N += 1

    # remove first and last point
    medax_ext = np.delete(medax_ext, 0, axis=0)
    medax_ext = np.delete(medax_ext, -1, axis=0)

    return medax_ext

get_bacteria_boundary(mask: np.array, boundary_smoothing_factor: int or None = 7) -> np.array #

Get the (smoothed) boundary of a segmented bacterium.

Parameters:

Name Type Description Default
mask np.array

The binary image (with a single bacterium only).

required
boundary_smoothing_factor int or None

The number of frequencies to use for FFT smoothing (default is 7).

7

Returns:

Name Type Description
boundary np.array

XY coordinates of the boundary (smoothed).

Source code in src/micromorph/bacteria/shape_analysis.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def get_bacteria_boundary(mask: np.array, boundary_smoothing_factor: int or None = 7) -> np.array:
    """
    Get the (smoothed) boundary of a segmented bacterium.

    Parameters
    ----------
    mask : np.array
        The binary image (with a single bacterium only).
    boundary_smoothing_factor : int or None, optional
        The number of frequencies to use for FFT smoothing (default is 7).

    Returns
    -------
    boundary: np.array
        XY coordinates of the boundary (smoothed).
    """

    # Get initial coordinates for boundary coords
    boundary_xy = get_boundary_coords(mask)
    # boundary_xy = fix_coordinates_order(boundary_xy) # Performance of this may be questionable.

    if boundary_smoothing_factor is None:
        return boundary_xy
    else:
        x = boundary_xy[:, 0]
        y = boundary_xy[:, 1]

        x_fft = np.fft.rfft(x)
        x_fft[boundary_smoothing_factor:] = 0

        y_fft = np.fft.rfft(y)
        y_fft[boundary_smoothing_factor:] = 0

        x_smoothed = np.fft.irfft(x_fft)
        y_smoothed = np.fft.irfft(y_fft)

        boundary_xy_smoothed = np.column_stack((x_smoothed, y_smoothed))
        return np.fliplr(boundary_xy_smoothed)

get_bacteria_length(medial_axis_extended: np.array, pxsize: float or int = 1.0) -> float #

Calculate the length of a segmented bacterium.

Parameters:

Name Type Description Default
medial_axis_extended np.array

The extended medial axis of the bacterium, whose arclength is calculated.

required
pxsize float

The pixel size of the image. The final distance will be multiplied by this value (default is 1.0).

1.0

Returns:

Name Type Description
total_distance float

Total distance covered by the extended medial axis (in px).

Source code in src/micromorph/bacteria/shape_analysis.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def get_bacteria_length(medial_axis_extended: np.array, pxsize: float or int = 1.0) -> float:
    """
    Calculate the length of a segmented bacterium.

    Parameters
    ----------
    medial_axis_extended : np.array
        The extended medial axis of the bacterium, whose arclength is calculated.
    pxsize : float, optional
        The pixel size of the image. The final distance will be multiplied by this value (default is 1.0).

    Returns
    -------
    total_distance : float
        Total distance covered by the extended medial axis (in px).
    """

    # Get distance vectors
    distance_vectors = np.diff(medial_axis_extended, axis=0)
    distance_vectors = np.concatenate((np.array([[0, 0]]), distance_vectors))

    # From the vectors, calculate the sums
    distances_individual = np.sqrt(np.sum(distance_vectors ** 2, axis=1))

    # now get total distance
    total_distance = np.sum(distances_individual)*pxsize

    return total_distance

get_bacteria_widths(img: np.array, med_ax: np.array, n_lines: int = 5, pxsize: float or int = 1.0, psfFWHM: float or int = 0.25, fit_type: str or None = None, line_magnitude: float or int = 20) -> np.array #

Calculate the width of a segmented bacterium.

Parameters:

Name Type Description Default
img np.array

The original image.

required
med_ax np.array

The medial axis which will be used to find profile lines to fit.

required
n_lines int

The number of lines to fit (default is 5).

5
pxsize float or int

The pixel size of the image (default is 1.0).

1.0
psfFWHM float or int

The FWHM of the PSF (default is 5).

0.25
fit_type str or None

The type of fit to use (default is None).

None
line_magnitude float or int

The length of the line used to measure the profile (default is 20).

20

Returns:

Name Type Description
all_widths np.array

Array of widths.

Source code in src/micromorph/bacteria/shape_analysis.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def get_bacteria_widths(img: np.array, med_ax: np.array, n_lines: int = 5, pxsize: float or int = 1.0,
                        psfFWHM: float or int = 0.250, fit_type: str or None = None,
                        line_magnitude: float or int = 20) -> np.array:
    """
    Calculate the width of a segmented bacterium.

    Parameters
    ----------
    img : np.array
        The original image.
    med_ax : np.array
        The medial axis which will be used to find profile lines to fit.
    n_lines : int, optional
        The number of lines to fit (default is 5).
    pxsize : float or int, optional
        The pixel size of the image (default is 1.0).
    psfFWHM : float or int, optional
        The FWHM of the PSF (default is 5).
    fit_type : str or None, optional
        The type of fit to use (default is None).
    line_magnitude : float or int, optional
        The length of the line used to measure the profile (default is 20).

    Returns
    -------
    all_widths: np.array
        Array of widths.
    """
    xh, yh, xl, yl = get_width_profile_lines(med_ax, n_points=n_lines, line_magnitude=line_magnitude)

    profile_points = np.column_stack((xh, yh, xl, yl))

    all_widths = np.array([])

    for points in profile_points:
        # img or bound_transform
        current_profile = profile_line(img, (points[1], points[0]), (points[3], points[2]))

        x = np.arange(0, len(current_profile)) * pxsize

        try:
            if fit_type == 'fluorescence':
                result = fit_ring_profile(x, current_profile, psfFWHM)
                width = result.params['R'] * 2
            elif fit_type == 'phase':
                result = fit_phase_contrast_profile(x, current_profile)
                width = result.params['width'].value * 2 * (np.log(2)/2) ** (0.25)
            elif fit_type == 'tophat':
                result = fit_top_hat_profile(x, current_profile)
                width = result.params['width'].value * 2 * (np.log(2)/2) ** (0.25)
            else:
                raise ValueError('Invalid fit type - please choose "fluorescence" or "phase".')

            all_widths = np.append(all_widths, width)
        except:
            # logging.info("Error fitting profile, skipping this one.")
            pass

    return all_widths

get_medial_axis(mask: np.array) -> np.array #

Get the medial axis of a segmented bacterium.

Parameters:

Name Type Description Default
mask np.array

The binary image (with a single bacterium only).

required

Returns:

Name Type Description
medial_axis np.array

The medial axis of the bacterium.

Source code in src/micromorph/bacteria/shape_analysis.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def get_medial_axis(mask: np.array) -> np.array:
    """
    Get the medial axis of a segmented bacterium.

    Parameters
    ----------
    mask : np.array
        The binary image (with a single bacterium only).

    Returns
    -------
    medial_axis : np.array
        The medial axis of the bacterium.
    """
    bacteria_skeleton = skeletonize(mask)

    # Get the medial axis
    bacteria_skeleton = prune_short_branches(bacteria_skeleton)

    # Get the medial axis
    medial_axis = trace_axis(bacteria_skeleton)

    return medial_axis

smooth_medial_axis(medial_axis: np.array, boundary: np.array, error_threshold: float = 0.05, max_iter: int = 50, spline_val: int = 1, spline_spacing=0.25) -> np.array #

Smooth the medial axis of a segmented bacterium, using the boundary as a reference.

Parameters:

Name Type Description Default
medial_axis np.array

The medial axis of the bacterium.

required
boundary np.array

The boundary of the bacterium.

required
error_threshold float

The threshold for the error in the smoothing (default is 0.05).

0.05
max_iter int

The maximum number of iterations for the smoothing (default is 50).

50

Returns:

Name Type Description
smoothed_medial_axis np.array

The smoothed medial axis.

Source code in src/micromorph/bacteria/shape_analysis.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def smooth_medial_axis(medial_axis: np.array, boundary: np.array, error_threshold: float = 0.05, max_iter: int = 50, spline_val: int = 1, spline_spacing = 0.25) -> np.array:
    """
    Smooth the medial axis of a segmented bacterium, using the boundary as a reference.

    Parameters
    ----------
    medial_axis : np.array
        The medial axis of the bacterium.
    boundary : np.array
        The boundary of the bacterium.
    error_threshold : float, optional
        The threshold for the error in the smoothing (default is 0.05).
    max_iter : int, optional
        The maximum number of iterations for the smoothing (default is 50).

    Returns
    -------
    smoothed_medial_axis: np.array
        The smoothed medial axis.
    """
    # Sanity check that all points in the medial axis are INSIDE the boundary.
    # This is due to the fact that we use the smoothed boundary for this calculation, but the medial axis is
    # calculated from the mask, which can lead to points extending outside the boundary

    boundary_polygon = Polygon(boundary)
    medial_axis = np.array([point for point in medial_axis if Point(point).within(boundary_polygon)])

    # smooth medial axis before starting
    # smooth the points
    x = medial_axis[:, 0]
    y = medial_axis[:, 1]

    # need to calculate arclength of the medial axis, so we can use it for smoothing
    distances = np.sqrt(np.sum(np.diff(medial_axis, axis=0) ** 2, axis=1))
    distances = np.concatenate((np.array([0]), distances))  # add 0 for the first point
    total_arclength_distance = np.cumsum(distances)  # cumulative sum to get the arclength

    # n_spline_points is such that each point should be spaced px_spacing apart
    n_spline_points = int(np.ceil(total_arclength_distance[-1] / spline_spacing))

    # we are fitting x and y independently of each other
    try:
        indexes = np.arange(0, len(x), 1)

        tck_s = splrep(indexes, x, s=len(x), k=spline_val)
        x_smooth_s = BSpline(*tck_s)

        tck_sy = splrep(indexes, y, s=len(y), k=spline_val)
        y_smooth_s = BSpline(*tck_sy)
    except:
        return medial_axis

    calculation_values = np.linspace(0, len(x) - 1, n_spline_points)

    medial_axis = np.array([x_smooth_s(calculation_values), y_smooth_s(calculation_values)]).T

    iter_n = 0
    previous_error = np.inf
    while iter_n < max_iter:
        # logging.info(f"Starting iteration {iter_n}")
        middle_points = []
        all_points = []
        perpendicular_points = []

        for i in range(medial_axis.shape[0] - 1):
            # calculate the angle between the first and second point of the medial axis
            angle = np.arctan2(medial_axis[i + 1, 1] - medial_axis[i, 1], medial_axis[i + 1, 0] - medial_axis[i, 0])

            # now find the angle perpendicular to this angle
            angle_perpendicular = angle + np.pi / 2

            # calculate the middle point between the first and second point of the medial axis
            middle_point = (medial_axis[i + 1] + medial_axis[i]) / 2


            # get the closest point on the boundary to the perpendicular line
            distances = np.abs((boundary[:, 1] - middle_point[1]) * np.cos(angle_perpendicular) - (
                        boundary[:, 0] - middle_point[0]) * np.sin(angle_perpendicular))



            # get the index of the closest point
            # FIXME: this is ugly, but it works
            idx = np.argmin(distances)

            closest_point = boundary[idx]

            # get the second closest
            distances[idx] = np.inf
            idx = np.argmin(distances)
            closest_point2 = boundary[idx]

            perpendicular_points.append(closest_point)
            perpendicular_points.append(closest_point2)

            # get the middle point between the two closest points
            middle_point = (closest_point + closest_point2) / 2
            middle_points.append(middle_point)

            all_points.append(medial_axis[i])
            all_points.append(middle_point)
            all_points.append(medial_axis[i + 1])

        # convert to numpy array
        all_points = np.array(all_points)
        all_points = np.array([point for point in all_points if Point(point).within(boundary_polygon)])


        # smooth the points
        x = all_points[:, 0]
        y = all_points[:, 1]

        # we are fitting x and y independently of each other
        indexes = np.arange(0, len(x), 1)

        tck_s = splrep(indexes, x, s=len(x), k=spline_val)
        x_smooth_s = BSpline(*tck_s)

        tck_sy = splrep(indexes, y, s=len(y), k=spline_val)
        y_smooth_s = BSpline(*tck_sy)

        calculation_values = np.linspace(0, len(x) - 1, n_spline_points)

        all_points = np.array([x_smooth_s(calculation_values), y_smooth_s(calculation_values)]).T

        # check if there are any large deviations in the smoothed points
        # if there are, we need to stop the smoothing
        delta_x = np.abs(np.diff(all_points[:, 0]))
        if np.max(delta_x) > 100:
            # logging.info(f"Large deviation in x, stopping smoothing here. Delta is {np.max(delta_x)}")
            break

        if iter_n >= 0:
            current_error = np.abs(np.mean(all_points - medial_axis))
            # logging.info(f"Current error: {current_error}, iteration: {iter_n}/{max_iter}")
            if current_error > previous_error:
                pass
                # logging.info("Error increased, stopping smoothing here.")
                # break
            elif current_error < error_threshold:
                medial_axis = all_points
                # logging.info("Error below threshold, stopping smoothing here.")
                break
            else:
                medial_axis = all_points
                previous_error = current_error

        iter_n += 1
    return medial_axis

Utility Functions#

Collection of utility functions used to calculate morphological parameters of the bacteria, mostly to do with binary image processing.

apply_mask_to_image(img: np.array, mask: np.array, method: str = 'min') -> np.array #

A function to apply a dilation to a mask, and then apply the mask to the image. This makes all the points outside the region of interest black.

Parameters:

Name Type Description Default
img np.array

The image to be masked

required
mask np.array

The mask to be applied to the image

required

Returns:

Name Type Description
img_masked np.array

The masked image

Source code in src/micromorph/bacteria/utilities.py
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def apply_mask_to_image(img: np.array, mask: np.array, method: str = 'min') -> np.array:
    """
    A function to apply a dilation to a mask, and then apply the mask to the image.
    This makes all the points outside the region of interest black.

    Parameters
    ----------
    img : np.array
        The image to be masked
    mask : np.array
        The mask to be applied to the image

    Returns
    -------
    img_masked : np.array
        The masked image
    """

    mask_inverted = np.invert(np.copy(mask))

    img_masked = np.copy(img)

    # Below is to avoid problems with different data types
    if mask_inverted.dtype == 'bool':
        idx = mask_inverted == True
    else:
        idx = mask_inverted == 255

    # img_masked[idx] = np.mean(img)
    if method == 'min':
        img_masked[idx] = np.min(img)
    elif method == 'max':
        img_masked[idx] = np.max(img)
    elif method == 'mean':
        img_masked[idx] = np.mean(img)
    else:
        # raise a warning
        Warning('Method not recognised, using min instead')
        img_masked[idx] = np.min(img)

    return img_masked

calculate_distance_along_line(coordinates: np.array) -> np.array #

Function to calculate the distance along a line defined by a set of coordinates.

Parameters:

Name Type Description Default
coordinates np.array

An np.array with 2 columns and N rows (N being the number of points in the line).

required

Returns:

Name Type Description
distances_cumulative np.array

An np.array with 1 column and N rows (N being the number of points in the line).

Source code in src/micromorph/bacteria/utilities.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def calculate_distance_along_line(coordinates: np.array) -> np.array:
    """
    Function to calculate the distance along a line defined by a set of coordinates.

    Parameters
    ----------
    coordinates : np.array
        An np.array with 2 columns and N rows (N being the number of points in the line).

    Returns
    -------
    distances_cumulative : np.array
        An np.array with 1 column and N rows (N being the number of points in the line).
    """
    # Get distance vectors
    distance_vectors = np.diff(coordinates, axis=0)
    distance_vectors = np.concatenate((np.array([[0, 0]]), distance_vectors))

    # From the vectors, calculate the sums
    distances_individual = np.sqrt(np.sum(distance_vectors ** 2, axis=1))
    distances_cumulative = np.cumsum(distances_individual)

    return distances_cumulative

find_branchpoints(skeleton: np.array) -> np.array #

Function to find the branchpoints of a binary image containing a skeleton.

Parameters:

Name Type Description Default
skeleton np.array

The skeletonised binary image

required

Returns:

Name Type Description
branch_points np.array

A binary image with only the branchpoints of the skeleton

Source code in src/micromorph/bacteria/utilities.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def find_branchpoints(skeleton: np.array) -> np.array:
    """
    Function to find the branchpoints of a binary image containing a skeleton.

    Parameters
    ----------
    skeleton : np.array
        The skeletonised binary image

    Returns
    -------
    branch_points : np.array
        A binary image with only the branchpoints of the skeleton
    """
    selems = list()
    selems.append(np.array([[0, 1, 0], [1, 1, 1], [0, 0, 0]]))
    selems.append(np.array([[1, 0, 1], [0, 1, 0], [1, 0, 0]]))
    selems.append(np.array([[1, 0, 1], [0, 1, 0], [0, 1, 0]]))
    selems.append(np.array([[0, 1, 0], [1, 1, 0], [0, 0, 1]]))
    selems.append(np.array([[0, 0, 1], [1, 1, 1], [0, 1, 0]]))
    selems = [np.rot90(selems[i], k=j) for i in range(5) for j in range(4)]

    selems.append(np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]))
    selems.append(np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]]))

    branch_points = np.zeros_like(skeleton, dtype=bool)
    for selem in selems:
        branch_points |= binary_hit_or_miss(skeleton, selem)

    return branch_points

find_closest_boundary_to_axis(medial_axis_coords: np.array, boundary_coords: np.array, start_position: int) -> np.array #

A function to find the closest point on the boundary to the medial axis.

Parameters:

Name Type Description Default
medial_axis_coords np.array

An np.array with 2 columns and N rows (N being the number of points in the medial axis).

required
boundary_coords np.array

An np.array with 2 columns and M rows (M being the number of points in the boundary).

required
start_position int

Either 1 or -1. If 1, the medial axis will be ordered from the first point to the last. If -1, the medial axis will be ordered from the last point to the first.

required

Returns:

Name Type Description
closest_on_boundary np.array

An np.array with 2 columns and 1 row, the coordinates of the closest point on the boundary.

Source code in src/micromorph/bacteria/utilities.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
def find_closest_boundary_to_axis(medial_axis_coords: np.array, boundary_coords: np.array,
                                  start_position: int) -> np.array:
    """
    A function to find the closest point on the boundary to the medial axis.

    Parameters
    ----------
    medial_axis_coords : np.array
        An np.array with 2 columns and N rows (N being the number of points in the medial axis).
    boundary_coords : np.array
        An np.array with 2 columns and M rows (M being the number of points in the boundary).
    start_position : int
        Either 1 or -1. If 1, the medial axis will be ordered from the first point to the last. If -1, the medial axis will be ordered from the last point to the first.

    Returns
    -------
    closest_on_boundary : np.array
        An np.array with 2 columns and 1 row, the coordinates of the closest point on the boundary.
    """
    # Start position can be either 1 or -1
    if start_position == 1:
        pass
    else:
        medial_axis_coords = np.flipud(medial_axis_coords)

    endpoint = medial_axis_coords[0, :]

    if len(medial_axis_coords) > 10:
        points_of_interest = medial_axis_coords[1:10, :]
    else:
        points_of_interest = medial_axis_coords[1::, :]

    search_vectors = points_of_interest - endpoint
    # This is to fix bug that happens when medial_axis_coords is only two points.
    if len(search_vectors) == 2:
        search_vectors = np.reshape(search_vectors, [1, 2])

    search_angles = np.arctan2(search_vectors[:, 1], search_vectors[:, 0])

    mean_search_angle = np.mean(search_angles)

    # Get angles from endpoint to all the boundary points.
    vectors_to_boundary_points = endpoint - boundary_coords
    angles_to_boundary_points = np.arctan2(vectors_to_boundary_points[:, 1], vectors_to_boundary_points[:, 0])

    selected_points = np.where((angles_to_boundary_points > mean_search_angle - 0.3) &
                               (angles_to_boundary_points < mean_search_angle + 0.3))

    # Filter the boundary points
    filtered_boundary_points = boundary_coords[selected_points, :]

    # closest_on_boundary = find_closest_point(endpoint, filtered_boundary_points)
    closest_on_boundary = find_furthest_point(endpoint, filtered_boundary_points)
    closest_on_boundary = np.array(closest_on_boundary[0][0])
    return closest_on_boundary

find_closest_point(point: np.array, candidates: np.array) -> tuple[np.array, int] #

Find the closest point to a given point, from a list of candidates.

Parameters:

Name Type Description Default
point np.array

A np.array with 2 columns and 1 row (x, y)

required
candidates np.array

An np.array with 2 columns and N rows (N being the number of candidates)

required

Returns:

Name Type Description
closest_point np.array

The closest point

min_index int

The index of the closest point in the candidates array

Source code in src/micromorph/bacteria/utilities.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def find_closest_point(point: np.array, candidates: np.array) -> tuple[np.array, int]:
    """
    Find the closest point to a given point, from a list of candidates.

    Parameters
    ----------
    point : np.array
        A np.array with 2 columns and 1 row (x, y)
    candidates : np.array
        An np.array with 2 columns and N rows (N being the number of candidates)

    Returns
    -------
    closest_point : np.array
        The closest point
    min_index : int
        The index of the closest point in the candidates array
    """

    distances_xy = candidates - point

    distances = np.sum(np.square(distances_xy), 1)

    min_index = np.where(distances == np.min(distances))

    min_index = min_index[0][0]  # Ensures we take the first one if there's two points with same distance

    closest_point = candidates[min_index, :]

    return closest_point, min_index

find_endpoints(img: np.array) -> np.array #

A function to find the endpoints of a binary image containing a line (i.e., a medial axis).

Parameters:

Name Type Description Default
img np.array

The binary image.

required

Returns:

Name Type Description
endpoints np.array

A binary image with only the endpoints of the line.

Source code in src/micromorph/bacteria/utilities.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def find_endpoints(img: np.array) -> np.array:
    """
        A function to find the endpoints of a binary image containing a line (i.e., a medial axis).

        Parameters
        ----------
        img : np.array
            The binary image.

        Returns
        -------
        endpoints : np.array
            A binary image with only the endpoints of the line.
    """
    endpoints = np.zeros(np.shape(img))

    endpoint_options = np.array([[1, 0, 0],
                                 [0, 1, 0],
                                 [0, 0, 0],
                                 [0, 1, 0],
                                 [0, 1, 0],
                                 [0, 0, 0],
                                 [0, 0, 1],
                                 [0, 1, 0],
                                 [0, 0, 0],
                                 [0, 0, 0],
                                 [0, 1, 1],
                                 [0, 0, 0],
                                 [0, 0, 0],
                                 [0, 1, 0],
                                 [0, 0, 1],
                                 [0, 0, 0],
                                 [0, 1, 0],
                                 [0, 1, 0],
                                 [0, 0, 0],
                                 [0, 1, 0],
                                 [1, 0, 0],
                                 [0, 0, 0],
                                 [1, 1, 0],
                                 [0, 0, 0],
                                 ])

    endpoint_structures = np.reshape(endpoint_options, (8, 3, 3))

    for box in endpoint_structures:
        current_attempt = binary_hit_or_miss(img, box)

        endpoints = endpoints + current_attempt
    return endpoints

find_furthest_point(point: np.array, candidates: np.array) -> tuple[np.array, int] #

Find the furthest point from a given point, from a list of candidates.

Parameters:

Name Type Description Default
point np.array

A np.array with 2 columns and 1 row (x, y)

required
candidates np.array

An np.array with 2 columns and N rows (N being the number of candidates)

required

Returns:

Name Type Description
closest_point np.array

The furthest point

max_index int

The index of the furthest point in the candidates array

Source code in src/micromorph/bacteria/utilities.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def find_furthest_point(point: np.array, candidates: np.array) -> tuple[np.array, int]:
    """
    Find the furthest point from a given point, from a list of candidates.


    Parameters
    ----------
    point : np.array
        A np.array with 2 columns and 1 row (x, y)
    candidates : np.array
        An np.array with 2 columns and N rows (N being the number of candidates)

    Returns
    -------
    closest_point : np.array
        The furthest point
    max_index : int
        The index of the furthest point in the candidates array
    """
    distances_xy = candidates - point

    distances = np.sum(np.square(distances_xy), 1)

    max_index = np.where(distances == np.max(distances))

    max_index = max_index[0][0]  # Ensures we take the first one if there's two points with same distance

    closest_point = candidates[max_index, :]

    return closest_point, max_index

fix_coordinates_order(coordinates: np.array) -> np.array #

This function fixes the coordinate order in a list of points (usually extracted from a binary image). In shorts, it picks a point and then finds the closest point to it, and so on.

Parameters:

Name Type Description Default
coordinates np.array

An np.array with 2 columns and N rows (N being the number of points)

required

Returns:

Name Type Description
ordered_coordinates np.array

An np.array with 2 columns and N rows (N being the number of points)

Source code in src/micromorph/bacteria/utilities.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def fix_coordinates_order(coordinates: np.array) -> np.array:
    """
    This function fixes the coordinate order in a list of points (usually extracted from a binary image).
    In shorts, it picks a point and then finds the closest point to it, and so on.

    Parameters
    ----------
    coordinates : np.array
        An np.array with 2 columns and N rows (N being the number of points)

    Returns
    -------
    ordered_coordinates : np.array
        An np.array with 2 columns and N rows (N being the number of points)
    """
    ordered_coordinates = np.zeros(coordinates.shape)

    # Set first coordinate of the boundary
    ordered_coordinates[0, :] = coordinates[0, :]
    coordinates = np.delete(coordinates, 0, 0)

    for i, point in enumerate(ordered_coordinates, start=1):
        previous_point = ordered_coordinates[i - 1, :]
        if coordinates.shape[0] > 1:
            closest_point, index = find_closest_point(previous_point, coordinates)

            ordered_coordinates[i, :] = closest_point

            coordinates = np.delete(coordinates, index, 0)
        else:
            candidate = np.array([[coordinates[0][0], coordinates[0][1]]])

            distance_to_candidate = np.sqrt(np.sum((candidate - previous_point) ** 2))

            if distance_to_candidate < 3:
                ordered_coordinates[i, 0] = coordinates[0][0]
                ordered_coordinates[i, 1] = coordinates[0][1]
            else:
                ordered_coordinates = np.delete(ordered_coordinates, i, 0)
            break

    return ordered_coordinates

get_boundary_coords(binary_image: np.array) -> np.array #

Runs the find boundary function from skimage, and then extracts the coordinates of the boundary.

Parameters:

Name Type Description Default
binary_image np.array

A binary image

required

Returns:

Name Type Description
boundary_xy np.array

An np.array with 2 columns and N rows (N being the number of boundary pixels). The points will not be ordered.

Source code in src/micromorph/bacteria/utilities.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def get_boundary_coords(binary_image: np.array) -> np.array:
    """
    Runs the find boundary function from skimage, and then extracts the coordinates of the boundary.

    Parameters
    ----------
    binary_image : np.array
        A binary image

    Returns
    -------
    boundary_xy : np.array
        An np.array with 2 columns and N rows (N being the number of boundary pixels). The points will not be ordered.
    """

    # Get boundaries from binary image
    # boundary_img = find_boundaries(binary_image, connectivity=4, mode='inner', background=0)
    boundary_xy = find_contours(binary_image)[0]

    # # ...and extract coordinates
    # boundary_xy = get_coords(boundary_img)
    return boundary_xy

get_coords(binary_image: np.array) -> np.array #

A function to get the coordinates of the non-zero pixels in a binary image.

Parameters:

Name Type Description Default
binary_image np.array

A binary image

required

Returns:

Name Type Description
xy np.array

An np.array with 2 columns and N rows (N being the number of non-zero pixels)

Source code in src/micromorph/bacteria/utilities.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def get_coords(binary_image: np.array) -> np.array:
    """
    A function to get the coordinates of the non-zero pixels in a binary image.

    Parameters
    ----------
    binary_image : np.array
        A binary image

    Returns
    -------
    xy : np.array
        An np.array with 2 columns and N rows (N being the number of non-zero pixels)
    """

    xy = np.fliplr(np.asarray(np.where(binary_image)).T)
    return xy

get_width_profile_lines(medial_axis: np.array, n_points: int = 3, line_magnitude: int or float = 10) #

Function to get the lines that will be used to calculate the width of the bacterium through fitting.

Parameters:

Name Type Description Default
medial_axis np.array

An np.array with 2 columns and N rows (N being the number of points in the medial axis).

required
n_points int

The number of points to sample along the medial axis.

3
line_magnitude int or float

The length of the lines that will be perpendicular to the medial axis.

10

Returns:

Type Description
x_high, y_high, x_low, y_low : np.arrays

np.arrays with 1 column and N rows (N being the number of points in the medial axis).

Source code in src/micromorph/bacteria/utilities.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
def get_width_profile_lines(medial_axis: np.array, n_points: int = 3, line_magnitude: int or float = 10):
    """
    Function to get the lines that will be used to calculate the width of the bacterium through fitting.

    Parameters
    ----------
    medial_axis : np.array
        An np.array with 2 columns and N rows (N being the number of points in the medial axis).
    n_points : int
        The number of points to sample along the medial axis.
    line_magnitude : int or float
        The length of the lines that will be perpendicular to the medial axis.

    Returns
    -------
    x_high, y_high, x_low, y_low : np.arrays
        np.arrays with 1 column and N rows (N being the number of points in the medial axis).
    """

    # Find slope of medial axis
    delta_vectors = np.diff(medial_axis, axis=0)
    parallel_angles = np.arctan2(delta_vectors[:, 1], delta_vectors[:, 0])
    perpendicular_angles = parallel_angles + np.deg2rad(90)

    # Remove first point from the medial axis, to make it same length as other vectors
    medial_axis = np.delete(medial_axis, 0, axis=0)

    # Get arclength curves
    distances = calculate_distance_along_line(medial_axis)

    # Get spline fit of distances vs x
    x_coords = medial_axis[:, 0]
    y_coords = medial_axis[:, 1]


    if len(distances) > 3:
        x_spline = splrep(distances, x_coords)
        y_spline = splrep(distances, y_coords)
        angles_spline = splrep(distances, perpendicular_angles)

        sampling_distances = np.linspace(0, distances[-1], n_points, endpoint=False)

        x_points = []
        y_points = []
        angles = []

        for val in sampling_distances:
            x_points.append(splev(val, x_spline))
            y_points.append(splev(val, y_spline))
            angles.append(splev(val, angles_spline))
    else:
        x_points = x_coords
        y_points = y_coords
        angles = perpendicular_angles

    x_ref = x_points
    y_ref = y_points
    perpendicular_angles = angles

    # Create lines 1 distance_line away from medial axis point, in each direction
    dx_perp = np.cos(perpendicular_angles) * line_magnitude / 2
    dy_perp = np.sin(perpendicular_angles) * line_magnitude / 2

    x_high = x_ref + dx_perp
    y_high = y_ref + dy_perp

    x_low = x_ref - dx_perp
    y_low = y_ref - dy_perp

    return x_high, y_high, x_low, y_low

prune_short_branches(skeleton: np.array) -> np.array #

Function to find the longest branch in a skeleton.

Parameters:

Name Type Description Default
skeleton np.array

The binary image, skeletonised

required

Returns:

Name Type Description
pruned_skeleton np.array

A binary image with only the longest branch of the skeleton

Source code in src/micromorph/bacteria/utilities.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def prune_short_branches(skeleton: np.array) -> np.array:
    """
    Function to find the longest branch in a skeleton.

    Parameters
    ----------
    skeleton : np.array
        The binary image, skeletonised

    Returns
    -------
    pruned_skeleton : np.array
        A binary image with only the longest branch of the skeleton
    """
    if not skeleton.dtype == bool:
        skeleton = skeleton.astype(bool)

    branch_points = find_branchpoints(skeleton)
    separated_branches = np.copy(skeleton) ^ branch_points

    labelled_branches = label(separated_branches)

    unique_labels = np.unique(labelled_branches)

    # remove zero, as it's the background
    unique_labels = unique_labels[1:]

    # find the highest occurence
    max_occurence = 0
    max_label = 0
    for n in unique_labels:
        occurence = np.sum(labelled_branches == n)
        if occurence > max_occurence:
            max_occurence = occurence
            max_label = n

    pruned_skeleton = labelled_branches == max_label

    return pruned_skeleton

trace_axis(medial_axis_image: np.array) -> np.array #

Trace the medial axis, returning an ordered array of points from one end to the other. The first endpoint is defined as the most leftmost of the endpoints.

Parameters:

Name Type Description Default
medial_axis_image np.array

The binary image with the medial axis

required

Returns:

Name Type Description
medial_axis np.array

An np.array with 2 columns and N rows (N being the number of points in the medial axis)

Source code in src/micromorph/bacteria/utilities.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def trace_axis(medial_axis_image: np.array) -> np.array:
    """
    Trace the medial axis, returning an ordered array of points from one end to the other. The first endpoint is defined as the most leftmost of the endpoints.

    Parameters
    ----------
    medial_axis_image : np.array
        The binary image with the medial axis

    Returns
    -------
    medial_axis : np.array
        An np.array with 2 columns and N rows (N being the number of points in the medial axis)
    """
    # Want to get medial axis in ordered point list.

    # Get coordinates (unordered) of the medial axis
    medial_axis_non_ordered = get_coords(medial_axis_image)

    # Grab endpoints of the medial axis line, and get their coordinates
    endpoints_image = find_endpoints(medial_axis_image)
    endpoints = get_coords(endpoints_image)

    # Pick one endpoint to start ordering from
    # This is arbitrary, so let's pick point that is the leftmost
    chosen_endpoint_index = np.where(endpoints[:, 0] == np.min(endpoints[:, 0]))
    chosen_endpoint_index = chosen_endpoint_index[0][0]

    # Create a new array where the ordered points will be stored
    medial_axis = np.zeros(medial_axis_non_ordered.shape)

    # Assign first point, and remove it from medial_axis_non_ordered
    medial_axis[0, :] = endpoints[chosen_endpoint_index, :]

    index_to_delete = np.where((medial_axis_non_ordered[:, 0] == endpoints[chosen_endpoint_index, 0]) &
                               (medial_axis_non_ordered[:, 1] == endpoints[chosen_endpoint_index, 1]))

    medial_axis_non_ordered = np.delete(medial_axis_non_ordered, index_to_delete, 0)

    for i, point in enumerate(medial_axis, start=1):
        previous_point = medial_axis[i - 1, :]

        if medial_axis_non_ordered.shape[0] > 1:
            closest_point, index = find_closest_point(previous_point, medial_axis_non_ordered)

            medial_axis[i, :] = closest_point

            # Delete point from pool
            medial_axis_non_ordered = np.delete(medial_axis_non_ordered, index, 0)
        else:
            medial_axis[i, 0] = medial_axis_non_ordered[0][0]
            medial_axis[i, 1] = medial_axis_non_ordered[0][1]
            break

    return medial_axis