diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 3e641b36..2694ee46 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -22,6 +22,7 @@ definitions: - python -m venv venv - source venv/bin/activate - pip install .[dev] + - pip uninstall pylinac -y - pip freeze artifacts: - venv/** @@ -45,6 +46,7 @@ definitions: - "*.rst" - step: &cbct-tests name: Run CBCT Tests + size: 2x script: - source venv/bin/activate - pytest tests_basic/test_cbct.py -n 2 --cov=pylinac.cbct --cov-report term --junitxml=./test-reports/pytest_results.xml diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index d934fba6..a1bbb377 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -33,12 +33,23 @@ Picket Fence * A new method is available ``plot_leaf_error``. This method will create a figure of the leaf error boxplot. This is similar to the leaf error subplot that shows up at the right/bottom of the analyzed image, but can be called independently. +Winston-Lutz +^^^^^^^^^^^^ + +* For multi-target multi-field analysis, the analysis has been sped up considerably. The speedup depends on the + image size and the number of BBs, but overall the speed up is ~2x. +* Calls to WL image's ``plot()`` method now accepts keyword arguments that are passed to the underlying image plot method. + E.g. ``wl_image.plot(vmin=1)``. + Core ^^^^ * Pylinac is meant to be compatible with all Python versions still in security lifecycles, which is currently 3.8. Some syntax was introduced that was not compatible with Python 3.8. This has been fixed. Note that Python 3.8 will be EOL in October 2024. The next pylinac release after that will drop support for Python 3.8. +* When computing image metrics, a failed metric analysis would still add the metric to the running list of metrics under + certain conditions such as running image metrics in a try clause. + This could result in errors when trying to plot the metrics. Now, if a metric computation fails, the metric is not added to the list. v 3.22.0 -------- diff --git a/pylinac/core/image.py b/pylinac/core/image.py index bce938e8..329009f1 100644 --- a/pylinac/core/image.py +++ b/pylinac/core/image.py @@ -892,8 +892,8 @@ def compute(self, metrics: list[MetricBase] | MetricBase) -> Any | dict[str, Any metrics = [metrics] for metric in metrics: metric.inject_image(self) - self.metrics.append(metric) value = metric.context_calculate() + self.metrics.append(metric) metric_data[metric.name] = value # TODO: use |= when 3.9 is min supported version self.metric_values.update(metric_data) diff --git a/pylinac/ct.py b/pylinac/ct.py index 38894d79..868dca3e 100644 --- a/pylinac/ct.py +++ b/pylinac/ct.py @@ -1889,8 +1889,14 @@ def find_phantom_axis(self) -> (Callable, Callable): ) common_idxs = np.intersect1d(x_idxs, y_idxs) # fit to 1D polynomials; inspiration: https://stackoverflow.com/a/45351484 - fit_zx = np.poly1d(np.polyfit(zs[common_idxs], center_xs[common_idxs], deg=1)) - fit_zy = np.poly1d(np.polyfit(zs[common_idxs], center_ys[common_idxs], deg=1)) + # rcond should be explicitly passed. Started randomly failing in the pipe. v1.14.0 numpy release notes + # say it should be explicitly passed. Value is arbitrary but small and tests pass. + fit_zx = np.poly1d( + np.polyfit(zs[common_idxs], center_xs[common_idxs], deg=1, rcond=0.00001) + ) + fit_zy = np.poly1d( + np.polyfit(zs[common_idxs], center_ys[common_idxs], deg=1, rcond=0.00001) + ) return fit_zx, fit_zy @property diff --git a/pylinac/winston_lutz.py b/pylinac/winston_lutz.py index ab810389..82e7eb44 100644 --- a/pylinac/winston_lutz.py +++ b/pylinac/winston_lutz.py @@ -68,11 +68,7 @@ is_solid, is_symmetric, ) -from .metrics.image import ( - GlobalSizedDiskLocator, - GlobalSizedFieldLocator, - SizedDiskLocator, -) +from .metrics.image import GlobalSizedFieldLocator, SizedDiskLocator BB_ERROR_MESSAGE = ( "Unable to locate the BB. Make sure the field edges do not obscure the BB, that there are no artifacts in the images, that the 'bb_size' parameter is close to reality, " @@ -667,6 +663,7 @@ def plot( clear_fig: bool = False, zoom: bool = True, legend: bool = True, + **kwargs, ) -> plt.Axes: """Plot an individual WL image. @@ -683,7 +680,9 @@ def plot( legend : bool Whether to show the legend. """ - ax = super().plot(ax=ax, show=False, clear_fig=clear_fig, show_metrics=True) + ax = super().plot( + ax=ax, show=False, clear_fig=clear_fig, show_metrics=True, **kwargs + ) # show EPID center ax.axvline(x=self.epid.x, color="b") epid_handle = ax.axhline(y=self.epid.y, color="b") @@ -2090,17 +2089,34 @@ def find_bb_centroids( self, bb_diameter_mm: float, low_density: bool ) -> list[Point]: """Find the specific BB based on the arrangement rather than a single one. This is in local pixel coordinates""" - # get initial starting conditions - bb_tolerance_mm = self._calculate_bb_tolerance(bb_diameter_mm) - centers = self.compute( - metrics=GlobalSizedDiskLocator( - radius_mm=bb_diameter_mm / 2, - radius_tolerance_mm=bb_tolerance_mm, - invert=not low_density, - detection_conditions=self.detection_conditions, - max_number=len(self.bb_arrangement), + centers = [] + for bb in self.bb_arrangement: + bb_diameter_mm = bb.bb_size_mm + bb_tolerance_mm = self._calculate_bb_tolerance(bb_diameter_mm) + left, sup = bb_projection_with_rotation( + offset_left=bb.offset_left_mm, + offset_up=bb.offset_up_mm, + offset_in=bb.offset_in_mm, + gantry=self.gantry_angle, + couch=self.couch_angle, + sad=self.sad, ) - ) + try: + new_centers = self.compute( + metrics=SizedDiskLocator.from_center_physical( + expected_position_mm=Point( + x=left, y=-sup + ), # we do -sup because sup is in WL coordinate space but the disk locator is in image coordinates. + search_window_mm=(40 + bb_diameter_mm, 40 + bb_diameter_mm), + radius_mm=bb_diameter_mm / 2, + radius_tolerance_mm=bb_tolerance_mm / 2, + invert=not low_density, + detection_conditions=self.detection_conditions, + ) + ) + centers.extend(new_centers) + except ValueError: + pass return centers