|
10 | 10 | import pandas as pd |
11 | 11 |
|
12 | 12 | from elf.io import open_file |
| 13 | +from scipy.ndimage import distance_transform_edt, binary_dilation, binary_closing |
13 | 14 | from scipy.sparse import csr_matrix |
14 | 15 | from scipy.spatial import distance |
15 | 16 | from scipy.spatial import cKDTree, ConvexHull |
@@ -615,3 +616,177 @@ def postprocess_ihc_seg( |
615 | 616 | table.loc[:, "component_labels"] = comp_labels |
616 | 617 |
|
617 | 618 | return table |
| 619 | + |
| 620 | + |
| 621 | +def dilate_and_trim( |
| 622 | + arr_orig: np.ndarray, |
| 623 | + edt: np.ndarray, |
| 624 | + iterations: int = 15, |
| 625 | + offset: float = 0.4, |
| 626 | +) -> np.ndarray: |
| 627 | + """Dilate and trim original binary array according to a |
| 628 | + Euclidean Distance Trasform computed for a separate target array. |
| 629 | +
|
| 630 | + Args: |
| 631 | + arr_orig: Original 3D binary array |
| 632 | + edt: 3D array containing Euclidean Distance transform for guiding dilation |
| 633 | + iterations: Number of iterations for dilations |
| 634 | + offset: Offset for regulating dilation. value should be in range(0, 0.45) |
| 635 | +
|
| 636 | + Returns: |
| 637 | + Dilated binary array |
| 638 | + """ |
| 639 | + border_coords = [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)] |
| 640 | + for _ in range(iterations): |
| 641 | + arr_dilated = binary_dilation(arr_orig) |
| 642 | + for x in range(arr_dilated.shape[0]): |
| 643 | + for y in range(arr_dilated.shape[1]): |
| 644 | + for z in range(arr_dilated.shape[2]): |
| 645 | + if arr_dilated[x, y, z] != 0: |
| 646 | + if arr_orig[x, y, z] == 0: |
| 647 | + min_dist = float('inf') |
| 648 | + for dx, dy, dz in border_coords: |
| 649 | + nx, ny, nz = x+dx, y+dy, z+dz |
| 650 | + if arr_orig[nx, ny, nz] == 1: |
| 651 | + min_dist = min([min_dist, edt[nx, ny, nz]]) |
| 652 | + if edt[x, y, z] >= min_dist - offset: |
| 653 | + arr_dilated[x, y, z] = 0 |
| 654 | + arr_orig = arr_dilated |
| 655 | + return arr_dilated |
| 656 | + |
| 657 | + |
| 658 | +def filter_cochlea_volume_single( |
| 659 | + table: pd.DataFrame, |
| 660 | + components: Optional[List[int]] = [1], |
| 661 | + scale_factor: int = 48, |
| 662 | + resolution: float = 0.38, |
| 663 | + dilation_iterations: int = 12, |
| 664 | + padding: int = 1200, |
| 665 | +) -> np.ndarray: |
| 666 | + """Filter cochlea volume based on a segmentation table. |
| 667 | + Centroids contained in the segmentation table are used to create a down-scaled binary array. |
| 668 | + The array can be dilated. |
| 669 | +
|
| 670 | + Args: |
| 671 | + table: Segmentation table. |
| 672 | + components: Component labels for filtering segmentation table. |
| 673 | + scale_factor: Down-sampling factor for filtering. |
| 674 | + resolution: Resolution of pixel in µm. |
| 675 | + dilation_iterations: Iterations for dilating binary segmentation mask. A negative value omits binary closing. |
| 676 | + padding: Padding in pixel to apply to guessed dimensions based on centroid coordinates. |
| 677 | +
|
| 678 | + Returns: |
| 679 | + Binary 3D array of filtered cochlea. |
| 680 | + """ |
| 681 | + # filter components |
| 682 | + if components is not None: |
| 683 | + table = table[table["component_labels"].isin(components)] |
| 684 | + |
| 685 | + # identify approximate input dimensions for down-scaling |
| 686 | + centroids = list(zip(table["anchor_x"] / resolution, |
| 687 | + table["anchor_y"] / resolution, |
| 688 | + table["anchor_z"] / resolution)) |
| 689 | + |
| 690 | + # padding the array allows for dilation without worrying about array borders |
| 691 | + max_x = table["anchor_x"].max() / resolution + padding |
| 692 | + max_y = table["anchor_y"].max() / resolution + padding |
| 693 | + max_z = table["anchor_z"].max() / resolution + padding |
| 694 | + ref_dimensions = (max_x, max_y, max_z) |
| 695 | + |
| 696 | + # down-scale arrays |
| 697 | + array_downscaled = downscaled_centroids(centroids, ref_dimensions=ref_dimensions, |
| 698 | + scale_factor=scale_factor, downsample_mode="capped") |
| 699 | + |
| 700 | + if dilation_iterations > 0: |
| 701 | + array_dilated = binary_dilation(array_downscaled, np.ones((3, 3, 3)), iterations=dilation_iterations) |
| 702 | + return binary_closing(array_dilated, np.ones((3, 3, 3)), iterations=1) |
| 703 | + |
| 704 | + elif dilation_iterations == 0: |
| 705 | + return binary_closing(array_downscaled, np.ones((3, 3, 3)), iterations=1) |
| 706 | + |
| 707 | + else: |
| 708 | + return array_downscaled |
| 709 | + |
| 710 | + |
| 711 | +def filter_cochlea_volume( |
| 712 | + sgn_table: pd.DataFrame, |
| 713 | + ihc_table: pd.DataFrame, |
| 714 | + sgn_components: Optional[List[int]] = [1], |
| 715 | + ihc_components: Optional[List[int]] = [1], |
| 716 | + scale_factor: int = 48, |
| 717 | + resolution: float = 0.38, |
| 718 | + dilation_iterations: int = 12, |
| 719 | + padding: int = 1200, |
| 720 | + dilation_method = "individual", |
| 721 | +) -> np.ndarray: |
| 722 | + """Filter cochlea volume with SGN and IHC segmentation. |
| 723 | + Centroids contained in the segmentation tables are used to create down-scaled binary arrays. |
| 724 | + The arrays are then dilated using guided dilation to fill the section inbetween SGNs and IHCs. |
| 725 | +
|
| 726 | + Args: |
| 727 | + sgn_table: SGN segmentation table. |
| 728 | + ihc_table: IHC segmentation table. |
| 729 | + sgn_components: Component labels for filtering SGN segmentation table. |
| 730 | + ihc_components: Component labels for filtering IHC segmentation table. |
| 731 | + scale_factor: Down-sampling factor for filtering. |
| 732 | + resolution: Resolution of pixel in µm. |
| 733 | + dilation_iterations: Iterations for dilating binary segmentation mask. |
| 734 | + padding: Padding in pixel to apply to guessed dimensions based on centroid coordinates. |
| 735 | + dilation_method: Dilation style for SGN and IHC segmentation, either 'individual', 'combined' or no dilation. |
| 736 | +
|
| 737 | + Returns: |
| 738 | + Binary 3D array of filtered cochlea. |
| 739 | + """ |
| 740 | + # filter components |
| 741 | + if sgn_components is not None: |
| 742 | + sgn_table = sgn_table[sgn_table["component_labels"].isin(sgn_components)] |
| 743 | + if ihc_components is not None: |
| 744 | + ihc_table = ihc_table[ihc_table["component_labels"].isin(ihc_components)] |
| 745 | + |
| 746 | + # identify approximate input dimensions for down-scaling |
| 747 | + centroids_sgn = list(zip(sgn_table["anchor_x"] / resolution, |
| 748 | + sgn_table["anchor_y"] / resolution, |
| 749 | + sgn_table["anchor_z"] / resolution)) |
| 750 | + centroids_ihc = list(zip(ihc_table["anchor_x"] / resolution, |
| 751 | + ihc_table["anchor_y"] / resolution, |
| 752 | + ihc_table["anchor_z"] / resolution)) |
| 753 | + |
| 754 | + # padding the array allows for dilation without worrying about array borders |
| 755 | + max_x = max([sgn_table["anchor_x"].max(), ihc_table["anchor_x"].max()]) / resolution + padding |
| 756 | + max_y = max([sgn_table["anchor_y"].max(), ihc_table["anchor_y"].max()]) / resolution + padding |
| 757 | + max_z = max([sgn_table["anchor_z"].max(), ihc_table["anchor_z"].max()]) / resolution + padding |
| 758 | + ref_dimensions = (max_x, max_y, max_z) |
| 759 | + |
| 760 | + # down-scale arrays |
| 761 | + array_downscaled_sgn = downscaled_centroids(centroids_sgn, ref_dimensions=ref_dimensions, |
| 762 | + scale_factor=scale_factor, downsample_mode="capped") |
| 763 | + |
| 764 | + array_downscaled_ihc = downscaled_centroids(centroids_ihc, ref_dimensions=ref_dimensions, |
| 765 | + scale_factor=scale_factor, downsample_mode="capped") |
| 766 | + |
| 767 | + # dilate down-scaled SGN array in direction of IHC segmentation |
| 768 | + distance_from_sgn = distance_transform_edt(~array_downscaled_sgn.astype(bool)) |
| 769 | + iterations = 20 |
| 770 | + arr_dilated = dilate_and_trim(array_downscaled_ihc.copy(), distance_from_sgn, iterations=iterations, offset=0.4) |
| 771 | + |
| 772 | + # dilate single structures first |
| 773 | + if dilation_method == "individual": |
| 774 | + ihc_dilated = binary_dilation(array_downscaled_ihc, np.ones((3, 3, 3)), iterations=dilation_iterations) |
| 775 | + sgn_dilated = binary_dilation(array_downscaled_sgn, np.ones((3, 3, 3)), iterations=dilation_iterations) |
| 776 | + combined_dilated = arr_dilated + ihc_dilated + sgn_dilated |
| 777 | + combined_dilated[combined_dilated > 0] = 1 |
| 778 | + combined_dilated = binary_dilation(combined_dilated, np.ones((3, 3, 3)), iterations=1) |
| 779 | + |
| 780 | + # dilate combined structure |
| 781 | + elif dilation_method == "combined": |
| 782 | + # combine SGN, IHC, and region between both to form output mask |
| 783 | + combined_structure = arr_dilated + array_downscaled_ihc + array_downscaled_sgn |
| 784 | + combined_structure[combined_structure > 0] = 1 |
| 785 | + combined_dilated = binary_dilation(combined_structure, np.ones((3, 3, 3)), iterations=dilation_iterations) |
| 786 | + |
| 787 | + # no dilation of combined structure |
| 788 | + else: |
| 789 | + combined_dilated = arr_dilated + ihc_dilated + sgn_dilated |
| 790 | + combined_dilated[combined_dilated > 0] = 1 |
| 791 | + |
| 792 | + return combined_dilated |
0 commit comments