|
| 1 | +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. |
| 2 | + |
| 3 | +""" |
| 4 | +ROI Coordinate Utilities |
| 5 | +========================= |
| 6 | +
|
| 7 | +This module provides utility functions for rounding ROI coordinates to appropriate |
| 8 | +precision based on the sampling characteristics of signals and images. |
| 9 | +
|
| 10 | +These functions are used when converting interactive PlotPy shapes to ROI objects |
| 11 | +to ensure coordinates are displayed with reasonable precision. |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +import numpy as np |
| 17 | +from sigima.objects import ImageObj, ROI1DParam, ROI2DParam, SignalObj |
| 18 | + |
| 19 | + |
| 20 | +def round_signal_coords( |
| 21 | + obj: SignalObj, coords: list[float], precision_factor: float = 0.1 |
| 22 | +) -> list[float]: |
| 23 | + """Round signal coordinates to appropriate precision based on sampling period. |
| 24 | +
|
| 25 | + Rounds to a fraction of the median sampling period to avoid excessive decimal |
| 26 | + places while maintaining reasonable precision. |
| 27 | +
|
| 28 | + Args: |
| 29 | + obj: signal object |
| 30 | + coords: coordinates to round |
| 31 | + precision_factor: fraction of sampling period to use as rounding precision. |
| 32 | + Default is 0.1 (1/10th of sampling period). |
| 33 | +
|
| 34 | + Returns: |
| 35 | + Rounded coordinates |
| 36 | + """ |
| 37 | + if len(obj.x) < 2: |
| 38 | + # Cannot compute sampling period, return coords as-is |
| 39 | + return coords |
| 40 | + # Compute median sampling period |
| 41 | + sampling_period = float(np.median(np.diff(obj.x))) |
| 42 | + if sampling_period == 0: |
| 43 | + # Avoid division by zero for constant x arrays |
| 44 | + return coords |
| 45 | + # Round to specified fraction of sampling period |
| 46 | + precision = sampling_period * precision_factor |
| 47 | + # Determine number of decimal places |
| 48 | + if precision > 0: |
| 49 | + decimals = max(0, int(-np.floor(np.log10(precision)))) |
| 50 | + return [round(c, decimals) for c in coords] |
| 51 | + return coords |
| 52 | + |
| 53 | + |
| 54 | +def round_image_coords( |
| 55 | + obj: ImageObj, coords: list[float], precision_factor: float = 0.1 |
| 56 | +) -> list[float]: |
| 57 | + """Round image coordinates to appropriate precision based on pixel spacing. |
| 58 | +
|
| 59 | + Rounds to a fraction of the pixel spacing to avoid excessive decimal places |
| 60 | + while maintaining reasonable precision. Uses separate precision for X and Y. |
| 61 | +
|
| 62 | + Args: |
| 63 | + obj: image object |
| 64 | + coords: flat list of coordinates [x0, y0, x1, y1, ...] to round |
| 65 | + precision_factor: fraction of pixel spacing to use as rounding precision. |
| 66 | + Default is 0.1 (1/10th of pixel spacing). |
| 67 | +
|
| 68 | + Returns: |
| 69 | + Rounded coordinates |
| 70 | +
|
| 71 | + Raises: |
| 72 | + ValueError: if coords does not contain an even number of elements |
| 73 | + """ |
| 74 | + if len(coords) % 2 != 0: |
| 75 | + raise ValueError("coords must contain an even number of elements (x, y pairs).") |
| 76 | + if len(coords) == 0: |
| 77 | + return coords |
| 78 | + |
| 79 | + rounded = list(coords) |
| 80 | + if obj.is_uniform_coords: |
| 81 | + # Use dx, dy for uniform coordinates |
| 82 | + precision_x = abs(obj.dx) * precision_factor |
| 83 | + precision_y = abs(obj.dy) * precision_factor |
| 84 | + else: |
| 85 | + # Compute average spacing for non-uniform coordinates |
| 86 | + if len(obj.xcoords) > 1: |
| 87 | + avg_dx = float(np.mean(np.abs(np.diff(obj.xcoords)))) |
| 88 | + precision_x = avg_dx * precision_factor |
| 89 | + else: |
| 90 | + precision_x = 0 |
| 91 | + if len(obj.ycoords) > 1: |
| 92 | + avg_dy = float(np.mean(np.abs(np.diff(obj.ycoords)))) |
| 93 | + precision_y = avg_dy * precision_factor |
| 94 | + else: |
| 95 | + precision_y = 0 |
| 96 | + |
| 97 | + # Round X coordinates (even indices) |
| 98 | + if precision_x > 0: |
| 99 | + decimals_x = max(0, int(-np.floor(np.log10(precision_x)))) |
| 100 | + for i in range(0, len(rounded), 2): |
| 101 | + rounded[i] = round(rounded[i], decimals_x) |
| 102 | + |
| 103 | + # Round Y coordinates (odd indices) |
| 104 | + if precision_y > 0: |
| 105 | + decimals_y = max(0, int(-np.floor(np.log10(precision_y)))) |
| 106 | + for i in range(1, len(rounded), 2): |
| 107 | + rounded[i] = round(rounded[i], decimals_y) |
| 108 | + |
| 109 | + return rounded |
| 110 | + |
| 111 | + |
| 112 | +def round_signal_roi_param( |
| 113 | + obj: SignalObj, param: ROI1DParam, precision_factor: float = 0.1 |
| 114 | +) -> None: |
| 115 | + """Round signal ROI parameter coordinates in-place. |
| 116 | +
|
| 117 | + Args: |
| 118 | + obj: signal object |
| 119 | + param: ROI parameter to round (modified in-place) |
| 120 | + precision_factor: fraction of sampling period to use as rounding precision |
| 121 | + """ |
| 122 | + coords = round_signal_coords(obj, [param.xmin, param.xmax], precision_factor) |
| 123 | + param.xmin, param.xmax = coords |
| 124 | + |
| 125 | + |
| 126 | +def round_image_roi_param( |
| 127 | + obj: ImageObj, param: ROI2DParam, precision_factor: float = 0.1 |
| 128 | +) -> None: |
| 129 | + """Round image ROI parameter coordinates in-place. |
| 130 | +
|
| 131 | + Args: |
| 132 | + obj: image object |
| 133 | + param: ROI parameter to round (modified in-place) |
| 134 | + precision_factor: fraction of pixel spacing to use as rounding precision |
| 135 | + """ |
| 136 | + if param.geometry == "rectangle": |
| 137 | + # Round x0, y0, dx, dy |
| 138 | + x0, y0, x1, y1 = param.x0, param.y0, param.x0 + param.dx, param.y0 + param.dy |
| 139 | + coords = round_image_coords(obj, [x0, y0, x1, y1], precision_factor) |
| 140 | + param.x0, param.y0 = coords[0], coords[1] |
| 141 | + # Round dx and dy to avoid floating-point errors in subtraction |
| 142 | + dx_dy_rounded = round_image_coords( |
| 143 | + obj, [coords[2] - coords[0], coords[3] - coords[1]], precision_factor |
| 144 | + ) |
| 145 | + param.dx = dx_dy_rounded[0] |
| 146 | + param.dy = dx_dy_rounded[1] |
| 147 | + elif param.geometry == "circle": |
| 148 | + # Round xc, yc, r |
| 149 | + coords = round_image_coords(obj, [param.xc, param.yc], precision_factor) |
| 150 | + param.xc, param.yc = coords |
| 151 | + # Round radius using X precision |
| 152 | + r_rounded = round_image_coords(obj, [param.r, 0], precision_factor)[0] |
| 153 | + param.r = r_rounded |
| 154 | + elif param.geometry == "polygon": |
| 155 | + # Round polygon points |
| 156 | + rounded = round_image_coords(obj, param.points.tolist(), precision_factor) |
| 157 | + param.points = np.array(rounded) |
0 commit comments