Skip to content

Synthetic data generation

Generation for synthetic datasets.

qim3d.generate.ParameterVisualizer

Class for visualizing and experimenting with parameter changes and combinations on synthetic data.

Parameters:

Name Type Description Default
base_shape tuple

Determines the shape of the generate volume. This will not be update when exploring parameters and must be determined when generating the visualizer.

(128, 128, 128)
final_shape tuple

Desired shape of the final volume. If unspecified, will assume same shape as base_shape. Defaults to None.

None
seed int

Determines the seed for the volume generation. Enables the user to generate different volumes with the same parameters.

0
hollow int

Determines thickness of the hollowing operation. Volume is only hollowed if hollow>0. Defaults to 0.

0
initial_config dict

Dictionary that defines the starting parameters of the visualizer. Can be used if a specific setup is needed. The dictionary may contain the keywords: noise_type, noise_scale, decay_rate, gamma, threshold, shape and tube_hole_ratio.

None
nsmin float

Determines minimum value for the noise scale slider. Defaults to 0.0.

0.0
nsmax float

Determines maximum value for the noise scale slider. Defaults to 0.1.

0.1
dsmin float

Determines minimum value for the decay rate slider. Defaults to 0.1.

0.1
dsmax float

Determines maximum value for the decay rate slider. Defaults to 20.

20.0
gsmin float

Determines minimum value for the gamma slider. Defaults to 0.1.

0.1
gsmax float

Determines maximum value for the gamma slider. Defaults to 2.0.

2.0
tsmin float

Determines minimum value for the threshold slider. Defaults to 0.0.

0.0
tsmax float

Determines maximum value for the threshold slider. Defaults to 1.0.

1.0
grid_visible bool

Determines if the grid should be visible upon plot generation. Defaults to False.

False

Raises:

Type Description
ValueError

If base_shape is invalid.

ValueError

If noise slider values are invalid.

ValueError

If decay slider values are invalid.

ValueError

If gamma slider values are invalid.

ValueError

If threshold slider values are invalid.

Example

import qim3d

viz = qim3d.generate.ParameterVisualizer()
paramter_visualizer

Accessing the current volume

The most recently generated 3D volume can be retrieved at any time using the .get_volume() method:

vol = viz.get_volume()
This returns the synthetic volume as a NumPy ndarray corresponding to the current widget parameters.

Source code in qim3d/generate/_generators.py
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
class ParameterVisualizer:
    """
    Class for visualizing and experimenting with parameter changes and combinations on synthetic data.

    Args:
        base_shape (tuple, optional): Determines the shape of the generate volume. This will not be update when exploring parameters and must be determined when generating the visualizer.
        final_shape (tuple, optional): Desired shape of the final volume. If unspecified, will assume same shape as base_shape. Defaults to None.
        seed (int, optional): Determines the seed for the volume generation. Enables the user to generate different volumes with the same parameters.
        hollow (int, optional): Determines thickness of the hollowing operation. Volume is only hollowed if hollow>0. Defaults to 0.
        initial_config (dict, optional): Dictionary that defines the starting parameters of the visualizer. Can be used if a specific setup is needed. The dictionary may contain the keywords: `noise_type`, `noise_scale`, `decay_rate`, `gamma`, `threshold`, `shape` and `tube_hole_ratio`.
        nsmin (float, optional): Determines minimum value for the noise scale slider. Defaults to 0.0.
        nsmax (float, optional): Determines maximum value for the noise scale slider. Defaults to 0.1.
        dsmin (float, optional): Determines minimum value for the decay rate slider. Defaults to 0.1.
        dsmax (float, optional): Determines maximum value for the decay rate slider. Defaults to 20.
        gsmin (float, optional): Determines minimum value for the gamma slider. Defaults to 0.1.
        gsmax (float, optional): Determines maximum value for the gamma slider. Defaults to 2.0.
        tsmin (float, optional): Determines minimum value for the threshold slider. Defaults to 0.0.
        tsmax (float, optional): Determines maximum value for the threshold slider. Defaults to 1.0.
        grid_visible (bool, optional): Determines if the grid should be visible upon plot generation. Defaults to False.

    Raises:
        ValueError: If base_shape is invalid.
        ValueError: If noise slider values are invalid.
        ValueError: If decay slider values are invalid.
        ValueError: If gamma slider values are invalid.
        ValueError: If threshold slider values are invalid.

    Example:
        ```python
        import qim3d

        viz = qim3d.generate.ParameterVisualizer()
        ```
        ![paramter_visualizer](../../assets/screenshots/viz-synthetic_parameters.gif)

    Accessing the current volume:
            The most recently generated 3D volume can be retrieved at any time using the `.get_volume()` method:

            ```python
            vol = viz.get_volume()
            ```
            This returns the synthetic volume as a NumPy ndarray corresponding to the current widget parameters.

    """

    def __init__(
        self,
        base_shape: tuple = (128, 128, 128),
        final_shape: tuple = None,
        seed: int = 0,
        hollow: int = 0,
        initial_config: dict = None,
        nsmin: float = 0.0,
        nsmax: float = 0.1,
        dsmin: float = 0.1,
        dsmax: float = 20.0,
        gsmin: float = 0.1,
        gsmax: float = 2.0,
        tsmin: float = 0.0,
        tsmax: float = 1.0,
        grid_visible: bool = False,
    ):
        # Error checking:
        if not isinstance(base_shape, tuple) or len(base_shape) != 3:
            err = 'base_shape should be a tuple of three sizes.'
            raise ValueError(err)

        if final_shape is not None:
            if not isinstance(final_shape, tuple) or len(final_shape) != 3:
                err = 'final_shape should be a tuple of three sizes or None.'
                raise ValueError(err)

        if hollow < 0 or isinstance(hollow, float):
            err = 'Argument "hollow" should be 0 or a positive integer'
            raise ValueError(err)

        if nsmin > nsmax:
            err = f'Minimum slider value for noise must be less than or equal to the maximum. Given: min = {nsmin}, max = {nsmax}.'
            raise ValueError(err)

        if dsmin > dsmax:
            err = f'Minimum decay rate value must be less than or equal to the maximum. Given: min = {dsmin}, max = {dsmax}.'
            raise ValueError(err)

        if gsmin > gsmax:
            err = f'Minimum gamma value must be less than or equal to the maximum. Given: min = {gsmin}, max = {gsmax}.'
            raise ValueError(err)

        if tsmin > tsmax:
            err = f'Minimum threshold value must be less than or equal to the maximum. Given: min = {tsmin}, max = {tsmax}.'
            raise ValueError(err)

        self.base_shape = base_shape
        self.final_shape = final_shape
        self.hollow = hollow
        self.seed = int(seed)
        self.axis = 0  # Not customizable
        self.max_value = 255  # Not customizable

        # Min and max values for sliders
        self.nsmin = nsmin
        self.nsmax = nsmax
        self.dsmin = dsmin
        self.dsmax = dsmax
        self.gsmin = gsmin
        self.gsmax = gsmax
        self.tsmin = tsmin
        self.tsmax = tsmax

        self.grid_visible = grid_visible
        self.config = {
            'noise_scale': 0.02,
            'decay_rate': 10,
            'gamma': 1.0,
            'threshold': 0.5,
            'tube_hole_ratio': 0.5,
            'shape': None,
            'noise_type': 'perlin',
        }
        if initial_config:
            self.config.update(initial_config)

        self.state = {}
        self._build_widgets()
        self._setup_plot()
        self._display_ui()

    def _compute_volume(self) -> None:
        vol = volume(
            base_shape=self.base_shape,
            final_shape=self.final_shape,
            noise_type=self.config['noise_type'],
            noise_scale=self.config['noise_scale'],
            decay_rate=self.config['decay_rate'],
            gamma=self.config['gamma'],
            threshold=self.config['threshold'],
            shape=self.config['shape'],
            tube_hole_ratio=self.config['tube_hole_ratio'],
            seed=self.seed,
            hollow=self.hollow,
        )
        return scale_to_float16(vol)

    def _build_widgets(self) -> None:
        # Widgets
        self.noise_slider = widgets.FloatSlider(
            value=self.config['noise_scale'],
            min=self.nsmin,
            max=self.nsmax,
            step=0.001,
            description='Noise',
            readout_format='.3f',
            continuous_update=False,
        )
        self.decay_slider = widgets.FloatSlider(
            value=self.config['decay_rate'],
            min=self.dsmin,
            max=self.dsmax,
            step=0.1,
            description='Decay',
            continuous_update=False,
        )
        self.gamma_slider = widgets.FloatSlider(
            value=self.config['gamma'],
            min=self.gsmin,
            max=self.gsmax,
            step=0.1,
            description='Gamma',
            continuous_update=False,
        )
        self.threshold_slider = widgets.FloatSlider(
            value=self.config['threshold'],
            min=self.tsmin,
            max=self.tsmax,
            step=0.05,
            description='Threshold',
            continuous_update=False,
        )
        self.noise_type_dropdown = widgets.Dropdown(
            options=['perlin', 'simplex'], value='perlin', description='Noise Type'
        )
        self.shape_dropdown = widgets.Dropdown(
            options=[None, 'cylinder', 'tube'], value=None, description='Shape'
        )
        self.tube_hole_ratio_slider = widgets.FloatSlider(
            value=self.config['tube_hole_ratio'],
            min=0.0,
            max=1.0,
            step=0.05,
            description='Tube hole ratio',
            style={'description_width': 'initial'},
            continuous_update=False,
        )
        self.base_shape_x_text = widgets.IntText(
            value=self.base_shape[0],
        )
        self.base_shape_y_text = widgets.IntText(
            value=self.base_shape[1],
        )
        self.base_shape_z_text = widgets.IntText(
            value=self.base_shape[2],
        )
        self.final_same_as_base_checkbox = widgets.Checkbox(
            value=True, description='Same as base_shape'
        )
        self.final_shape_x_text = widgets.IntText(
            value=self.base_shape[0],
        )
        self.final_shape_y_text = widgets.IntText(
            value=self.base_shape[1],
        )
        self.final_shape_z_text = widgets.IntText(
            value=self.base_shape[2],
        )
        self.hollow_text = widgets.BoundedIntText(
            value=self.hollow,
            min=0,
            max=1000,  # chosen arbitrarily atm.
            step=1,
            description='Hollow',
        )
        self.colormap_dropdown = widgets.Dropdown(
            options=['magma', 'viridis', 'gray', 'plasma'],
            value='magma',
            description='Colormap',
        )
        self.grid_checkbox = widgets.Checkbox(
            value=self.grid_visible, description='Show grid'
        )

        # Observers
        self.noise_slider.observe(self._on_change, names='value')
        self.noise_type_dropdown.observe(self._on_change, names='value')
        self.decay_slider.observe(self._on_change, names='value')
        self.gamma_slider.observe(self._on_change, names='value')
        self.threshold_slider.observe(self._on_change, names='value')
        self.shape_dropdown.observe(self._on_change, names='value')
        self.tube_hole_ratio_slider.observe(self._on_change, names='value')
        self.base_shape_x_text.observe(self._on_change, names='value')
        self.base_shape_y_text.observe(self._on_change, names='value')
        self.base_shape_z_text.observe(self._on_change, names='value')
        self.final_shape_x_text.observe(self._on_change, names='value')
        self.final_shape_y_text.observe(self._on_change, names='value')
        self.final_shape_z_text.observe(self._on_change, names='value')
        self.hollow_text.observe(self._on_change, names='value')
        self.colormap_dropdown.observe(self._on_change, names='value')
        self.grid_checkbox.observe(self._on_change, names='value')
        self.final_same_as_base_checkbox.observe(
            self._on_checkbox_change, names='value'
        )
        self.final_same_as_base_checkbox.observe(self._on_change, names='value')
        # Initial state
        self._on_checkbox_change({'new': self.final_same_as_base_checkbox.value})

    def _on_checkbox_change(self, change) -> None:
        disabled = change['new']
        self.final_shape_x_text.disabled = disabled
        self.final_shape_y_text.disabled = disabled
        self.final_shape_z_text.disabled = disabled

    def _get_base_shape(self) -> tuple:
        # Check valid axes
        for axis in [
            self.base_shape_x_text,
            self.base_shape_y_text,
            self.base_shape_z_text,
        ]:
            if axis.value < 1:
                axis.value = 1
        return (
            self.base_shape_x_text.value,
            self.base_shape_y_text.value,
            self.base_shape_z_text.value,
        )

    def _get_final_shape(self) -> tuple:
        if self.final_same_as_base_checkbox.value:
            return None
        else:
            return (
                self.final_shape_x_text.value,
                self.final_shape_y_text.value,
                self.final_shape_z_text.value,
            )

    def _setup_plot(self) -> None:
        vol = self._compute_volume()

        cmap = plt.get_cmap(self.colormap_dropdown.value)
        attr_vals = np.linspace(0.0, 1.0, num=cmap.N)
        rgb_vals = cmap(np.arange(0, cmap.N))[:, :3]
        color_map = np.column_stack((attr_vals, rgb_vals)).tolist()

        pixel_count = np.prod(vol.shape)
        y1, x1 = 256, 16777216  # 256 samples at res 256*256*256=16.777.216
        y2, x2 = 32, 134217728  # 32 samples at res 512*512*512=134.217.728
        a = (y1 - y2) / (x1 - x2)
        b = y1 - a * x1
        samples = int(min(max(a * pixel_count + b, 64), 512))

        self.plot = k3d.plot(grid_visible=self.grid_visible)
        self.plt_volume = k3d.volume(
            vol,
            bounds=[0, vol.shape[0], 0, vol.shape[1], 0, vol.shape[2]],
            color_map=color_map,
            samples=samples,
            color_range=[np.min(vol), np.max(vol)],
            opacity_function=[],
            interpolation=True,
        )
        self.plot += self.plt_volume

    def _on_change(self, change: None = None) -> None:
        self.config['noise_type'] = self.noise_type_dropdown.value
        self.config['noise_scale'] = self.noise_slider.value
        self.config['decay_rate'] = self.decay_slider.value
        self.config['gamma'] = self.gamma_slider.value
        self.config['threshold'] = self.threshold_slider.value
        self.config['shape'] = self.shape_dropdown.value
        self.config['tube_hole_ratio'] = self.tube_hole_ratio_slider.value
        self.base_shape = self._get_base_shape()
        self.final_shape = self._get_final_shape()
        self.hollow = self.hollow_text.value

        # Update colormap
        cmap = plt.get_cmap(self.colormap_dropdown.value)
        attr_vals = np.linspace(0.0, 1.0, num=cmap.N)
        rgb_vals = cmap(np.arange(0, cmap.N))[:, :3]
        color_map = np.column_stack((attr_vals, rgb_vals)).tolist()
        self.plt_volume.color_map = color_map

        # Update grid
        self.plot.grid_visible = self.grid_checkbox.value

        # Recompute volume
        new_vol = self._compute_volume()

        # Recompute samples based on the new shape (same logic as in _setup_plot)
        pixel_count = int(np.prod(new_vol.shape))
        y1, x1 = 256, 16777216  # 256 samples at 256^3
        y2, x2 = 32, 134217728  # 32 samples  at 512^3
        a = (y1 - y2) / (x1 - x2)
        b = y1 - a * x1
        samples = int(min(max(a * pixel_count + b, 64), 512))

        # If the shape changed, rebuild the K3D volume actor (needed for K3D)
        if new_vol.shape != self.plt_volume.volume.shape:
            # Remove old actor
            self.plot -= self.plt_volume

            # Build fresh actor with updated bounds/color_range/samples
            self.plt_volume = k3d.volume(
                new_vol,
                bounds=[0, new_vol.shape[0], 0, new_vol.shape[1], 0, new_vol.shape[2]],
                color_map=color_map,
                samples=samples,
                color_range=[float(np.min(new_vol)), float(np.max(new_vol))],
                opacity_function=[],
                interpolation=True,
            )
            self.plot += self.plt_volume
        else:
            # Same shape: just update data AND color_range
            self.plt_volume.volume = new_vol
            self.plt_volume.color_range = [
                float(np.min(new_vol)),
                float(np.max(new_vol)),
            ]

    def _display_ui(self) -> None:
        small_box = widgets.Layout(width='65px')
        for box in [
            self.base_shape_x_text,
            self.base_shape_y_text,
            self.base_shape_z_text,
            self.final_shape_x_text,
            self.final_shape_y_text,
            self.final_shape_z_text,
        ]:
            box.layout = small_box

        self.base_shape_box = widgets.HBox(
            [
                widgets.Label('Base shape  '),
                widgets.Label('x'),
                self.base_shape_x_text,
                widgets.Label('y'),
                self.base_shape_y_text,
                widgets.Label('z'),
                self.base_shape_z_text,
            ]
        )
        self.final_shape_box = widgets.HBox(
            [
                widgets.Label('Final shape  '),
                widgets.Label('x'),
                self.final_shape_x_text,
                widgets.Label('y'),
                self.final_shape_y_text,
                widgets.Label('z'),
                self.final_shape_z_text,
            ]
        )

        parameters_controls = widgets.VBox(
            [
                self.base_shape_box,
                self.final_shape_box,
                self.final_same_as_base_checkbox,
                self.hollow_text,
                self.noise_type_dropdown,
                self.noise_slider,
                self.decay_slider,
                self.gamma_slider,
                self.threshold_slider,
                self.shape_dropdown,
                self.tube_hole_ratio_slider,
            ]
        )

        # Controls styling
        parameters_controls.layout = widgets.Layout(
            display='flex',
            flex_flow='column',
            flex='0 1',
            min_width='350px',  # Ensure it doesn't get too small
            height='auto',
            overflow_y='auto',
            border='1px solid lightgray',
            padding='10px',
            margin='0 1em 0 0',
        )

        visualization_controls = widgets.VBox(
            [self.colormap_dropdown, self.grid_checkbox]
        )

        visualization_controls.layout = widgets.Layout(
            display='flex',
            flex_flow='column',
            flex='0 1',
            min_width='350px',  # Ensure it doesn't get too small
            height='auto',
            overflow_y='auto',
            border='1px solid lightgray',
            padding='10px',
            margin='0 1em 0 0',
        )

        tabs = widgets.Tab(children=[parameters_controls, visualization_controls])
        tabs.set_title(0, 'Parameters')
        tabs.set_title(1, 'Visualization')

        plot_output = widgets.Output()
        plot_output.layout = widgets.Layout(
            flex='1 1 auto',
            height='auto',
            border='1px solid lightgray',
            overflow='auto',
            min_width='500px',
        )
        with plot_output:
            display(self.plot)

        ui = widgets.HBox(
            [tabs, plot_output],
            layout=widgets.Layout(
                width='100%', display='flex', flex_flow='row', align_items='stretch'
            ),
        )

        display(ui)

    def get_volume(self):
        """
        Retrieves the most recently generated volume from the visualizer.

        Returns:
            numpy.ndarray: The current synthetic 3D volume based on the widget parameters.

        Example:
        ```python
            viz = qim3d.generate.ParameterVisualizer()
            vol = viz.get_volume()
            '''

        """
        return self.plt_volume.volume

qim3d.generate.ParameterVisualizer.get_volume

get_volume()

Retrieves the most recently generated volume from the visualizer.

Returns:

Type Description

numpy.ndarray: The current synthetic 3D volume based on the widget parameters.

Example: ```python viz = qim3d.generate.ParameterVisualizer() vol = viz.get_volume() '''

Source code in qim3d/generate/_generators.py
def get_volume(self):
    """
    Retrieves the most recently generated volume from the visualizer.

    Returns:
        numpy.ndarray: The current synthetic 3D volume based on the widget parameters.

    Example:
    ```python
        viz = qim3d.generate.ParameterVisualizer()
        vol = viz.get_volume()
        '''

    """
    return self.plt_volume.volume

qim3d.generate.volume

volume(
    base_shape=(128, 128, 128),
    final_shape=None,
    noise_scale=0.02,
    noise_type='perlin',
    decay_rate=10,
    gamma=1,
    threshold=0.5,
    max_value=255,
    shape=None,
    tube_hole_ratio=0.5,
    axis=0,
    order=1,
    dtype='uint8',
    hollow=0,
    seed=0,
)

Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction.

Parameters:

Name Type Description Default
base_shape tuple of ints

Shape of the initial volume to generate. Defaults to (128, 128, 128).

(128, 128, 128)
final_shape tuple of ints

Desired shape of the final volume. If unspecified, will assume same shape as base_shape. Defaults to None.

None
noise_scale float

Scale factor for Perlin noise. Defaults to 0.05.

0.02
noise_type str

Type of noise to be used for volume generation. Should be simplex or perlin. Defaults to perlin.

'perlin'
decay_rate float

The decay rate of the fading of the noise. Can also be interpreted as the sharpness of the edge of the volume. Defaults to 5.0.

10
gamma float

Applies gamma correction, adjusting contrast in the volume. If gamma<0, the volume intensity is increased and if gamma>0 it's decreased. Defaults to 0.

1
threshold float

Threshold value for clipping low intensity values. Defaults to 0.5.

0.5
max_value int

Maximum value for the volume intensity. Defaults to 255.

255
shape str

Shape of the volume to generate, either cylinder, or tube. Defaults to None.

None
tube_hole_ratio float

Ratio for the inverted fade mask used to generate tubes. Will only have an effect if shape=tube. Defaults to 0.5.

0.5
axis int

Axis of the given shape. Will only be active if shape is defined. Defaults to 0.

0
order int

Order of the spline interpolation used in resizing. Defaults to 1.

1
dtype data - type

Desired data type of the output volume. Defaults to uint8.

'uint8'
hollow bool

Determines thickness of the hollowing operation. Volume is only hollowed if hollow>0. Defaults to 0.

0
seed int

Specifies a fixed offset for the generated noise. Only works for perlin noise. Defaults to 0.

0

Returns:

Name Type Description
volume ndarray

Generated 3D volume with specified parameters.

Raises:

Type Description
ValueError

If shape is invalid.

ValueError

If noise_type is invalid.

TypeError

If base_shape is not a tuple or does not have three elements.

TypeError

If final_shape is not a tuple or does not have three elements.

TypeError

If dtype is not a valid numpy number type.

ValueError

If hollow is not 0 or a positive integer.

Example

Example:

import qim3d

# Generate synthetic blob
vol = qim3d.generate.volume(noise_scale = 0.02)

# Visualize 3D volume
qim3d.viz.volumetric(vol)

# Visualize slices
qim3d.viz.slices_grid(vol, value_min = 0, value_max = 255, num_slices = 15)
synthetic_blob

Example
import qim3d

# Generate tubular synthetic blob
vol = qim3d.generate.volume(base_shape = (200, 100, 100),
                            final_shape = (400,100,100),
                            noise_scale = 0.03,
                            threshold = 0.85,
                            decay_rate=20,
                            gamma=0.15,
                            shape = "tube",
                            tube_hole_ratio = 0.4,
                            )

# Visualize synthetic volume
qim3d.viz.volumetric(vol)

# Visualize slices
qim3d.viz.slices_grid(vol, num_slices=15, slice_axis=1)
synthetic_blob_cylinder_slice

Example
import qim3d

# Generate tubular synthetic blob
vol = qim3d.generate.volume(base_shape = (200, 100, 100),
                        final_shape = (400, 100, 100),
                        noise_scale = 0.03,
                        gamma = 0.12,
                        threshold = 0.85,
                        shape = "tube",
                        )

# Visualize synthetic blob
qim3d.viz.volumetric(vol)

# Visualize
qim3d.viz.slices_grid(vol, num_slices=15)
synthetic_blob_tube_slice

Source code in qim3d/generate/_generators.py
def volume(
    base_shape: tuple = (128, 128, 128),
    final_shape: tuple = None,
    noise_scale: float = 0.02,
    noise_type: str = 'perlin',
    decay_rate: float = 10,
    gamma: float = 1,
    threshold: float = 0.5,
    max_value: float = 255,
    shape: str = None,
    tube_hole_ratio: float = 0.5,
    axis: int = 0,
    order: int = 1,
    dtype: str = 'uint8',
    hollow: int = 0,
    seed: int = 0,
) -> np.ndarray:
    """
    Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction.

    Args:
        base_shape (tuple of ints, optional): Shape of the initial volume to generate. Defaults to (128, 128, 128).
        final_shape (tuple of ints, optional): Desired shape of the final volume. If unspecified, will assume same shape as base_shape. Defaults to None.
        noise_scale (float, optional): Scale factor for Perlin noise. Defaults to 0.05.
        noise_type (str, optional): Type of noise to be used for volume generation. Should be `simplex` or `perlin`. Defaults to perlin.
        decay_rate (float, optional): The decay rate of the fading of the noise. Can also be interpreted as the sharpness of the edge of the volume. Defaults to 5.0.
        gamma (float, optional): Applies gamma correction, adjusting contrast in the volume. If gamma<0, the volume intensity is increased and if gamma>0 it's decreased. Defaults to 0.
        threshold (float, optional): Threshold value for clipping low intensity values. Defaults to 0.5.
        max_value (int, optional): Maximum value for the volume intensity. Defaults to 255.
        shape (str, optional): Shape of the volume to generate, either `cylinder`, or `tube`. Defaults to None.
        tube_hole_ratio (float, optional): Ratio for the inverted fade mask used to generate tubes. Will only have an effect if shape=`tube`. Defaults to 0.5.
        axis (int, optional): Axis of the given shape. Will only be active if shape is defined. Defaults to 0.
        order (int, optional): Order of the spline interpolation used in resizing. Defaults to 1.
        dtype (data-type, optional): Desired data type of the output volume. Defaults to `uint8`.
        hollow (bool, optional): Determines thickness of the hollowing operation. Volume is only hollowed if hollow>0. Defaults to 0.
        seed (int, optional): Specifies a fixed offset for the generated noise. Only works for perlin noise. Defaults to 0.

    Returns:
        volume (numpy.ndarray): Generated 3D volume with specified parameters.

    Raises:
        ValueError: If `shape` is invalid.
        ValueError: If `noise_type` is invalid.
        TypeError: If `base_shape` is not a tuple or does not have three elements.
        TypeError: If `final_shape` is not a tuple or does not have three elements.
        TypeError: If `dtype` is not a valid numpy number type.
        ValueError: If `hollow` is not 0 or a positive integer.

    Example:
        Example:
        ```python
        import qim3d

        # Generate synthetic blob
        vol = qim3d.generate.volume(noise_scale = 0.02)

        # Visualize 3D volume
        qim3d.viz.volumetric(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob_1.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(vol, value_min = 0, value_max = 255, num_slices = 15)
        ```
        ![synthetic_blob](../../assets/screenshots/synthetic_blob_slices.png)

    Example:
        ```python
        import qim3d

        # Generate tubular synthetic blob
        vol = qim3d.generate.volume(base_shape = (200, 100, 100),
                                    final_shape = (400,100,100),
                                    noise_scale = 0.03,
                                    threshold = 0.85,
                                    decay_rate=20,
                                    gamma=0.15,
                                    shape = "tube",
                                    tube_hole_ratio = 0.4,
                                    )

        # Visualize synthetic volume
        qim3d.viz.volumetric(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob_cylinder_1.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(vol, num_slices=15, slice_axis=1)
        ```
        ![synthetic_blob_cylinder_slice](../../assets/screenshots/synthetic_blob_cylinder_slice.png)

    Example:
        ```python
        import qim3d

        # Generate tubular synthetic blob
        vol = qim3d.generate.volume(base_shape = (200, 100, 100),
                                final_shape = (400, 100, 100),
                                noise_scale = 0.03,
                                gamma = 0.12,
                                threshold = 0.85,
                                shape = "tube",
                                )

        # Visualize synthetic blob
        qim3d.viz.volumetric(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob_tube_1.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize
        qim3d.viz.slices_grid(vol, num_slices=15)
        ```
        ![synthetic_blob_tube_slice](../../assets/screenshots/synthetic_blob_tube_slice.png)

    """
    # Control
    shape_types = ['cylinder', 'tube']
    if shape and shape not in shape_types:
        err = f'shape should be one of: {shape_types}'
        raise ValueError(err)
    noise_types = ['pnoise', 'perlin', 'p', 'snoise', 'simplex', 's']
    if noise_type not in noise_types:
        err = f'noise_type should be one of: {noise_types}'
        raise ValueError(err)

    if not isinstance(base_shape, tuple) or len(base_shape) != 3:
        message = 'base_shape must be a tuple with three dimensions (z, y, x)'
        raise TypeError(message)

    if final_shape and (not isinstance(final_shape, tuple) or len(final_shape) != 3):
        message = 'final_shape must be a tuple with three dimensions (z, y, x)'
        raise TypeError(message)

    try:
        d = np.dtype(dtype)
    except TypeError as e:
        err = f'Datatype {dtype} is not a valid dtype.'
        raise TypeError(err) from e

    if hollow < 0 or isinstance(hollow, float):
        err = 'Argument "hollow" should be 0 or a positive integer'
        raise ValueError(err)

    # Generate grid of coordinates
    z, y, x = np.indices(base_shape)

    # Generate noise
    if (
        np.round(noise_scale, 3) == 0
    ):  # Only detect three decimal position (0.001 is ok, but 0.0001 is 0)
        noise_scale = 0

    if noise_scale == 0:
        noise = np.ones(base_shape)
    else:
        if noise_type in noise_types[:3]:
            vectorized_noise = np.vectorize(pnoise3)
            noise = vectorized_noise(
                z.flatten() * noise_scale,
                y.flatten() * noise_scale,
                x.flatten() * noise_scale,
                base=seed,
            ).reshape(base_shape)
        elif noise_type in noise_types[3:]:
            vectorized_noise = np.vectorize(snoise3)
            noise = vectorized_noise(
                z.flatten() * noise_scale,
                y.flatten() * noise_scale,
                x.flatten() * noise_scale,
            ).reshape(base_shape)
        noise = (noise - np.min(noise)) / (np.max(noise) - np.min(noise))

    # Calculate the center of the array
    center = np.array([(s - 1) / 2 for s in base_shape])

    # Calculate the distance of each point from the center
    if not shape:
        distance = np.linalg.norm(
            [
                (z - center[0]) / center[0],
                (y - center[1]) / center[1],
                (x - center[2]) / center[2],
            ],
            axis=0,
        )
        max_distance = np.sqrt(3)
        # Set ratio
        miin = np.max(
            [
                distance[distance.shape[0] // 2, distance.shape[1] // 2, 0],
                distance[distance.shape[0] // 2, 0, distance.shape[2] // 2],
                distance[0, distance.shape[1] // 2, distance.shape[2] // 2],
            ]
        )
        ratio = miin / max_distance  # 0.577

    elif shape == 'cylinder' or shape == 'tube':
        distance_list = np.array(
            [
                (z - center[0]) / center[0],
                (y - center[1]) / center[1],
                (x - center[2]) / center[2],
            ]
        )
        # remove the axis along which the fading is not applied
        distance_list = np.delete(distance_list, axis, axis=0)
        distance = np.linalg.norm(distance_list, axis=0)
        max_distance = np.sqrt(2)
        # Set ratio
        miin = np.max(
            [
                distance[distance.shape[0] // 2, distance.shape[1] // 2, 0],
                distance[distance.shape[0] // 2, 0, distance.shape[2] // 2],
                distance[0, distance.shape[1] // 2, distance.shape[2] // 2],
            ]
        )
        ratio = miin / max_distance  # 0.707

    # Scale the distance such that the shortest distance (from center to any edge) is 1 (prevents clipping)
    scaled_distance = distance / (max_distance * ratio)

    # Apply decay rate
    faded_distance = np.power(scaled_distance, decay_rate)

    # Invert the distances to have 1 at the center and 0 at the edges
    fade_array = 1 - faded_distance
    fade_array[fade_array <= 0] = 0

    # Apply the fading to the volume
    vol_faded = noise * fade_array

    # Normalize the volume
    vol_normalized = vol_faded / np.max(vol_faded)

    # Apply gamma
    generated_vol = np.power(vol_normalized, gamma)

    # Scale to max_value
    generated_vol = generated_vol * max_value

    # Threshold
    generated_vol[generated_vol < threshold * max_value] = 0

    # Apply fade mask for creation of tube
    if shape == 'tube':
        generated_vol = qim3d.operations.fade_mask(
            generated_vol,
            geometry='cylindrical',
            axis=axis,
            ratio=tube_hole_ratio,
            decay_rate=5,
            invert=True,
        )

    # Scale up the volume of volume to size
    if final_shape:
        generated_vol = scipy.ndimage.zoom(
            generated_vol, np.array(final_shape) / np.array(base_shape), order=order
        )

    generated_vol = generated_vol.astype(dtype)

    if hollow > 0:
        generated_vol = qim3d.operations.make_hollow(generated_vol, hollow)

    return generated_vol

qim3d.generate.volume_collection

volume_collection(
    num_volumes=15,
    collection_shape=(200, 200, 200),
    data=None,
    positions=None,
    shape_range=((40, 40, 40), (60, 60, 60)),
    shape_magnification_range=(1.0, 1.0),
    noise_type='perlin',
    noise_range=(0.02, 0.03),
    rotation_degree_range=(0, 360),
    rotation_axes=None,
    gamma_range=(0.9, 1),
    value_range=(128, 255),
    threshold_range=(0.5, 0.55),
    decay_rate_range=(5, 10),
    shape=None,
    tube_hole_ratio=0.5,
    axis=0,
    verbose=False,
    same_seed=False,
    hollow=False,
    seed=0,
    dtype='uint8',
    return_positions=False,
)

Generate a 3D volume of multiple synthetic volumes using Perlin or Simplex noise.

Parameters:

Name Type Description Default
num_volumes int

Number of synthetic volumes to include in the collection. Defaults to 15.

15
collection_shape tuple of ints

Shape of the final collection volume to generate. Defaults to (200, 200, 200).

(200, 200, 200)
data ndarray or list[ndarray]

Predefined volume(s) to use for the collection. If provided, the function will use these volumes instead of generating new ones. Defaults to None.

None
positions list[tuple]

List of specific positions as (z, y, x) coordinates for the volumes. If not provided, they are placed randomly into the collection. Defaults to None.

None
shape_range tuple of tuple of ints

Determines the shape of the generated volumes with first element defining the minimum size and second element defining maximum. Defaults to ((40,40,40), (60,60,60)).

((40, 40, 40), (60, 60, 60))
shape_magnification_range tuple of floats

Range for scaling of volume shape in all dimensions. Defaults to (1.0, 1.0).

(1.0, 1.0)
noise_type str

Type of noise to be used for volume generation. Should be simplex, perlin or mixed. Defaults to perlin.

'perlin'
noise_range tuple of floats

Determines range for noise. First element is minimum and second is maximum. Defaults to (0.02, 0.03).

(0.02, 0.03)
rotation_degree_range tuple of ints

Determines range for rotation angle in degrees. First element is minimum and second is maximum. Defaults to (0, 360).

(0, 360)
rotation_axes list[tuple]

List of axis pairs that will be randomly chosen to rotate around. Defaults to [(0, 1), (0, 2), (1, 2)].

None
gamma_range tuple of floats

Determines minimum and maximum gamma correctness factor. Defaults to (0.9, 1.0)

(0.9, 1)
value_range tuple of ints

Determines minimum and maximum value for volume intensity. Defaults to (128, 255).

(128, 255)
threshold_range tuple of ints

Determines minimum and maximum value for thresholding. Defaults to (0.5, 0.55).

(0.5, 0.55)
decay_rate_range float

Determines minimum and maximum value for the decay_range. Defaults to (5,10).

(5, 10)
shape str or None

Shape of the volume to generate, either "cylinder", or "tube". Defaults to None.

None
tube_hole_ratio float

Ratio for the inverted fade mask used to generate tubes. Will only have an effect if shape=tube. Defaults to 0.5.

0.5
axis int

Determines the axis of the volume_shape if this is defined. Defaults to 0.

0
verbose bool

Flag to enable verbose logging. Defaults to False.

False
same_seed bool

Use the same seed for each generated volume. Note that in order to generate identical volumes, the min and max for the different parameters should be identical.

False
hollow bool

Create hollow objects using qim3d.operations.make_hollow(). Defaults to False.

False
seed int

Seed for reproducibility. Defaults to 0. Each generated volume will be generated with a randomly selected sub-seed generated from the original seed.

0
dtype str

dtype for resulting volume. Defaults to uint8.

'uint8'
return_positions bool

Flag to return position of randomly placed blobs.

False

Returns:

Name Type Description
volume_collection ndarray

3D volume of the generated collection of synthetic volumes with specified parameters.

labels ndarray

Array with labels for each voxel, same shape as volume_collection.

Raises:

Type Description
TypeError

If data is not a numpy array or list of numpy arrays.

ValueError

If data is provided but not a 3D numpy array.

ValueError

If noise_type is invalid.

TypeError

If collection_shape is not 3D.

ValueError

If volume parameters are invalid.

ValueError

If the shape_range is incorrectly defined.

ValueError

If the positions are incorrectly defined.

Example
import qim3d

# Generate synthetic collection of volumes
num_volumes = 15
volume_collection, labels = qim3d.generate.volume_collection(num_volumes=num_volumes)

# Visualize the collection
qim3d.viz.volumetric(volume_collection, grid_visible=True)

qim3d.viz.slicer(volume_collection)
synthetic_collection

# Visualize labels
cmap = qim3d.viz.colormaps.segmentation(num_labels=num_volumes)
qim3d.viz.slicer(labels, color_map=cmap, value_max=num_volumes)
synthetic_collection

Collection of fiber-like structures
import qim3d

# Generate synthetic collection of cylindrical structures
volume_collection, labels = qim3d.generate.volume_collection(
    num_volumes = 40,
    collection_shape = (300, 150, 150),
    shape_range = ((280, 10, 10), (290, 15, 15)),
    noise_range = (0.06,0.09),
    rotation_degree_range = (0,5),
    threshold_range = (0.1,0.3),
    gamma_range = (0.10, 0.20),
    shape = "cylinder"
    )

# Visualize the collection
qim3d.viz.volumetric(volume_collection)

# Visualize slices
qim3d.viz.slices_grid(volume_collection, num_slices=15)
synthetic_collection_cylinder

Create a collection of tubular (hollow) structures
import qim3d

# Generate synthetic collection of tubular (hollow) structures
volume_collection, labels = qim3d.generate.volume_collection(
    num_volumes = 10,
    collection_shape = (200, 200, 200),
    shape_range = ((185,35,35), (190,45,45)),
    noise_range = (0.02, 0.03),
    rotation_degree_range = (0,5),
    threshold_range = (0.6, 0.7),
    gamma_range = (0.1, 0.11),
    shape = "tube",
    tube_hole_ratio = 0.15,
    )

# Visualize the collection
qim3d.viz.volumetric(volume_collection)

# Visualize slices
qim3d.viz.slices_grid(volume_collection, num_slices=15, slice_axis=1)
synthetic_collection_tube

Using predefined volumes
import qim3d

# Generate two unique volumes to be used
volume_1 = qim3d.generate.volume(base_shape = (32,32,32), noise_scale = 0.0)
volume_2 = qim3d.generate.volume(base_shape = (32,32,32), noise_scale = 0.2)

# Generate collection from predefined volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 30,
                                                             data = [volume_1, volume_2])
# Visualize
qim3d.viz.volumetric(volume_collection)
Source code in qim3d/generate/_aggregators.py
def volume_collection(
    num_volumes: int = 15,
    collection_shape: tuple = (200, 200, 200),
    data: np.ndarray | list[np.ndarray] | None = None,
    positions: list[tuple] = None,
    shape_range: tuple[tuple] = ((40, 40, 40), (60, 60, 60)),
    shape_magnification_range: tuple[float] = (1.0, 1.0),
    noise_type: str = 'perlin',
    noise_range: tuple[float] = (0.02, 0.03),
    rotation_degree_range: tuple[int] = (0, 360),
    rotation_axes: list[tuple] = None,
    gamma_range: tuple[float] = (0.9, 1),
    value_range: tuple[int] = (128, 255),
    threshold_range: tuple[float] = (0.5, 0.55),
    decay_rate_range: tuple[float] = (5, 10),
    shape: str = None,
    tube_hole_ratio: float = 0.5,
    axis: int = 0,
    verbose: bool = False,
    same_seed: bool = False,
    hollow: bool = False,
    seed: int = 0,
    dtype: str = 'uint8',
    return_positions: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Generate a 3D volume of multiple synthetic volumes using Perlin or Simplex noise.

    Args:
        num_volumes (int, optional): Number of synthetic volumes to include in the collection. Defaults to 15.
        collection_shape (tuple of ints, optional): Shape of the final collection volume to generate. Defaults to (200, 200, 200).
        data (numpy.ndarray or list[numpy.ndarray], optional): Predefined volume(s) to use for the collection. If provided, the function will use these volumes instead of generating new ones. Defaults to None.
        positions (list[tuple], optional): List of specific positions as (z, y, x) coordinates for the volumes. If not provided, they are placed randomly into the collection. Defaults to None.
        shape_range (tuple of tuple of ints, optional): Determines the shape of the generated volumes with first element defining the minimum size and second element defining maximum. Defaults to ((40,40,40), (60,60,60)).
        shape_magnification_range (tuple of floats, optional): Range for scaling of volume shape in all dimensions. Defaults to (1.0, 1.0).
        noise_type (str, optional): Type of noise to be used for volume generation. Should be `simplex`, `perlin` or `mixed`. Defaults to perlin.
        noise_range (tuple of floats, optional): Determines range for noise. First element is minimum and second is maximum. Defaults to (0.02, 0.03).
        rotation_degree_range (tuple of ints, optional): Determines range for rotation angle in degrees. First element is minimum and second is maximum. Defaults to (0, 360).
        rotation_axes (list[tuple], optional): List of axis pairs that will be randomly chosen to rotate around. Defaults to [(0, 1), (0, 2), (1, 2)].
        gamma_range (tuple of floats, optional): Determines minimum and maximum gamma correctness factor. Defaults to (0.9, 1.0)
        value_range (tuple of ints, optional): Determines minimum and maximum value for volume intensity. Defaults to (128, 255).
        threshold_range (tuple of ints, optional): Determines minimum and maximum value for thresholding. Defaults to (0.5, 0.55).
        decay_rate_range (float, optional): Determines minimum and maximum value for the decay_range. Defaults to (5,10).
        shape (str or None, optional): Shape of the volume to generate, either "cylinder", or "tube". Defaults to None.
        tube_hole_ratio (float, optional): Ratio for the inverted fade mask used to generate tubes. Will only have an effect if shape=`tube`. Defaults to 0.5.
        axis (int, optional): Determines the axis of the volume_shape if this is defined. Defaults to 0.
        verbose (bool, optional): Flag to enable verbose logging. Defaults to False.
        same_seed (bool, optional): Use the same seed for each generated volume. Note that in order to generate identical volumes, the min and max for the different parameters should be identical.
        hollow (bool, optional): Create hollow objects using qim3d.operations.make_hollow(). Defaults to False.
        seed (int, optional): Seed for reproducibility. Defaults to 0. Each generated volume will be generated with a randomly selected sub-seed generated from the original seed.
        dtype (str, optional): dtype for resulting volume. Defaults to uint8.
        return_positions (bool, optional): Flag to return position of randomly placed blobs.

    Returns:
        volume_collection (numpy.ndarray): 3D volume of the generated collection of synthetic volumes with specified parameters.
        labels (numpy.ndarray): Array with labels for each voxel, same shape as volume_collection.

    Raises:
        TypeError: If `data` is not a numpy array or list of numpy arrays.
        ValueError: If `data` is provided but not a 3D numpy array.
        ValueError: If `noise_type` is invalid.
        TypeError: If `collection_shape` is not 3D.
        ValueError: If volume parameters are invalid.
        ValueError: If the `shape_range` is incorrectly defined.
        ValueError: If the `positions` are incorrectly defined.

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        num_volumes = 15
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes=num_volumes)

        # Visualize the collection
        qim3d.viz.volumetric(volume_collection, grid_visible=True)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_default_1.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        qim3d.viz.slicer(volume_collection)
        ```
        ![synthetic_collection](../../assets/screenshots/synthetic_collection_default.gif)

        ```python
        # Visualize labels
        cmap = qim3d.viz.colormaps.segmentation(num_labels=num_volumes)
        qim3d.viz.slicer(labels, color_map=cmap, value_max=num_volumes)
        ```
        ![synthetic_collection](../../assets/screenshots/synthetic_collection_default_labels.gif)

    Example: Collection of fiber-like structures
        ```python
        import qim3d

        # Generate synthetic collection of cylindrical structures
        volume_collection, labels = qim3d.generate.volume_collection(
            num_volumes = 40,
            collection_shape = (300, 150, 150),
            shape_range = ((280, 10, 10), (290, 15, 15)),
            noise_range = (0.06,0.09),
            rotation_degree_range = (0,5),
            threshold_range = (0.1,0.3),
            gamma_range = (0.10, 0.20),
            shape = "cylinder"
            )

        # Visualize the collection
        qim3d.viz.volumetric(volume_collection)

        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_cylinder_1.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(volume_collection, num_slices=15)
        ```
        ![synthetic_collection_cylinder](../../assets/screenshots/synthetic_collection_cylinder_slices.png)

    Example: Create a collection of tubular (hollow) structures
        ```python
        import qim3d

        # Generate synthetic collection of tubular (hollow) structures
        volume_collection, labels = qim3d.generate.volume_collection(
            num_volumes = 10,
            collection_shape = (200, 200, 200),
            shape_range = ((185,35,35), (190,45,45)),
            noise_range = (0.02, 0.03),
            rotation_degree_range = (0,5),
            threshold_range = (0.6, 0.7),
            gamma_range = (0.1, 0.11),
            shape = "tube",
            tube_hole_ratio = 0.15,
            )

        # Visualize the collection
        qim3d.viz.volumetric(volume_collection)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_tube_1.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(volume_collection, num_slices=15, slice_axis=1)
        ```
        ![synthetic_collection_tube](../../assets/screenshots/synthetic_collection_tube_slices.png)

    Example: Using predefined volumes
        ```python
        import qim3d

        # Generate two unique volumes to be used
        volume_1 = qim3d.generate.volume(base_shape = (32,32,32), noise_scale = 0.0)
        volume_2 = qim3d.generate.volume(base_shape = (32,32,32), noise_scale = 0.2)

        # Generate collection from predefined volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 30,
                                                                     data = [volume_1, volume_2])
        # Visualize
        qim3d.viz.volumetric(volume_collection)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_from_given_volumes.html" width="100%" height="500" frameborder="0"></iframe>

    """

    # Check valid input types
    if data is not None:
        if isinstance(data, np.ndarray):
            data_list = [data]
        elif isinstance(data, list):
            data_list = data
        else:
            msg = '`data` must be a numpy array or list of arrays'
            raise TypeError(msg)
        for idx, arr in enumerate(data_list):
            if not (isinstance(arr, np.ndarray) and arr.ndim == 3):
                msg = f'data[{idx}] must be a 3D numpy array'
                raise ValueError(msg)
    else:
        data_list = None

    if data_list is not None:
        valid = []
        for idx, vol in enumerate(data_list):
            # check each dimension
            if all(v <= c for v, c in zip(vol.shape, collection_shape)):
                valid.append(vol)
            else:
                msg = f'Skipping custom volume {idx} with shape {vol.shape} — larger than collection {collection_shape}'
                log.warning(msg)
        data_list = valid
        # if none remain, we can't build anything
        if not data_list:
            msg = f'No custom volumes fit within collection size {collection_shape}.'
            raise ValueError(msg)

    noise_types = ['pnoise', 'perlin', 'p', 'snoise', 'simplex', 's', 'mixed', 'm']
    if noise_type not in noise_types:
        err = f'noise_type should be one of: {noise_types}'
        raise ValueError(err)

    if not isinstance(collection_shape, tuple) or len(collection_shape) != 3:
        message = 'Shape of collection must be a tuple with three dimensions (z, y, x)'
        raise TypeError(message)

    if len(shape_range[0]) != len(shape_range[1]):
        message = 'Object shapes must be tuples of the same length'
        raise ValueError(message)
    if len(shape_range[0]) != 3 or len(shape_range[1]) != 3 or len(shape_range) != 2:
        message = 'shape_range should be defined as a tuple with two elements, each containing a tuple with three elements.'
        raise ValueError(message)

    if (positions is not None) and (len(positions) != num_volumes):
        message = 'Number of volumes must match number of positions, otherwise set positions = None'
        raise ValueError(message)

    if (positions is not None) and return_positions:
        log.debug('positions are given and thus not returned')
        return_positions = False

    if rotation_axes is None:
        rotation_axes = [(0, 1), (0, 2), (1, 2)]

    if verbose:
        original_log_level = log.getEffectiveLevel()
        log.setLevel('DEBUG')

    # Set seed for random number generator
    rng = np.random.default_rng(seed)

    # Set seed for random number generator for placement
    rng_pos = np.random.default_rng(seed)

    # Initialize the 3D array for the shape
    collection_array = np.zeros(
        (collection_shape[0], collection_shape[1], collection_shape[2]), dtype=dtype
    )
    labels = np.zeros_like(collection_array)

    # Initialize saved positions
    placed_positions = []

    if same_seed:
        seeds = rng.integers(0, 255, size=1).repeat(5000)
    else:
        seeds = rng.integers(low=0, high=255, size=5000)
    nt = rng.random(size=1000)

    # Fill the 3D array with synthetic blobs
    for i in tqdm(range(num_volumes), desc='Objects placed'):
        log.debug(f'\nObject #{i+1}')

        # Sample from blob parameter ranges
        if shape_range[0] == shape_range[1]:
            blob_shape = shape_range[0]
        else:
            blob_shape = tuple(
                rng.integers(low=shape_range[0][i], high=shape_range[1][i])
                for i in range(3)
            )

        magnification = rng.uniform(
            low=shape_magnification_range[0], high=shape_magnification_range[1]
        )

        final_shape = tuple(int(dim * magnification) for dim in blob_shape)
        log.debug(f'- Blob shape: {final_shape}')
        # Check if should keep final_shape separate from base_shape
        # Sample noise scale
        noise_scale = rng.uniform(low=noise_range[0], high=noise_range[1])
        log.debug(f'- Object noise scale: {noise_scale:.4f}')

        gamma = rng.uniform(low=gamma_range[0], high=gamma_range[1])
        log.debug(f'- Gamma correction: {gamma:.3f}')

        threshold = rng.uniform(low=threshold_range[0], high=threshold_range[1])
        log.debug(f'- Threshold: {threshold:.3f}')

        decay_rate = rng.uniform(low=decay_rate_range[0], high=decay_rate_range[1])
        log.debug(f'- Decay rate: {decay_rate:.3f}')

        if value_range[1] > value_range[0]:
            max_value = rng.integers(low=value_range[0], high=value_range[1])
        else:
            max_value = value_range[0]
        log.debug(f'- Max value: {max_value}')

        if noise_type == 'mixed' or noise_type == 'm':
            nti = 'perlin' if nt[i] >= 0.5 else 'simplex'
        else:
            nti = noise_type
        log.debug(f'- Noise type: {nti}')

        log.debug(f'- Seed: {seeds[i]}')

        # Pick volume from the list if provided, otherwise generate synthetic volume
        if data_list is not None:
            choice_i = rng.integers(len(data_list))
            blob = data_list[choice_i].copy()
        else:
            # Generate synthetic volume
            blob = qim3d.generate.volume(
                base_shape=final_shape,
                final_shape=final_shape,
                noise_scale=noise_scale,
                noise_type=nti,
                decay_rate=decay_rate,
                gamma=gamma,
                threshold=threshold,
                max_value=max_value,
                shape=shape,
                tube_hole_ratio=tube_hole_ratio,
                axis=axis,
                order=1,
                dtype=dtype,
                hollow=hollow,
                seed=seeds[i],
            )

        # Rotate volume
        if rotation_degree_range[1] > 0:
            angle = rng.uniform(
                low=rotation_degree_range[0], high=rotation_degree_range[1]
            )  # Sample rotation angle
            axes = rng.choice(rotation_axes)  # Sample the two axes to rotate around
            log.debug(f'- Rotation angle: {angle:.2f} at axes: {axes}')

            blob = scipy.ndimage.rotate(blob, angle, axes, order=1)

        # Place synthetic volume into the collection
        # If positions are specified, place volume at one of the specified positions
        collection_before = collection_array.copy()
        if positions:
            collection_array, placed, positions = specific_placement(
                collection_array, blob, positions.copy()
            )

        # Otherwise, place volume at a random available position
        else:
            collection_array, placed, pos = random_placement(
                collection_array, blob, rng_pos
            )
            if return_positions and placed:
                placed_positions.append(tuple(pos))

            log.debug(f'- Center placement (z,y,x): {pos}')
        # Break if volume could not be placed
        if not placed:
            break

        # Update labels
        new_labels = np.where(collection_array != collection_before, i + 1, 0).astype(
            labels.dtype
        )
        labels += new_labels

    if not placed:
        # Log error if not all num_volumes could be placed (this line of code has to be here, otherwise it will interfere with tqdm progress bar)
        log.error(
            f'Object #{i+1} could not be placed in the collection, no space found. Collection contains {i}/{num_volumes} volumes.'
        )
    if verbose:
        log.setLevel(original_log_level)

    if return_positions:
        return collection_array, labels, placed_positions
    else:
        return collection_array, labels

qim3d.generate.background

background(
    background_shape,
    baseline_value=0,
    min_noise_value=0,
    max_noise_value=20,
    generate_method='add',
    apply_method=None,
    seed=0,
    dtype='uint8',
    apply_to=None,
)

Generate a noise volume with random intensity values from a uniform distribution.

Parameters:

Name Type Description Default
background_shape tuple

The shape of the noise volume to generate.

required
baseline_value float

The baseline intensity of the noise volume. Default is 0.

0
min_noise_value float

The minimum intensity of the noise. Default is 0.

0
max_noise_value float

The maximum intensity of the noise. Default is 20.

20
generate_method str

The method used to combine baseline_value and noise. Choose from 'add' (baseline + noise), 'subtract' (baseline - noise), 'multiply' (baseline * noise), or 'divide' (baseline / (noise+ε)). Default is 'add'.

'add'
apply_method str

The method to apply the generated noise to apply_to, if provided. Choose from 'add' (apply_to + background), 'subtract' (apply_to - background), 'multiply' (apply_to * background), or 'divide' (apply_to / (background+ε)). Only applicable if apply_to is defined. Default is None.

None
seed int

The seed for the random number generator. Default is 0.

0
dtype data - type

Desired data type of the output volume. Default is 'uint8'.

'uint8'
apply_to ndarray

An input volume to which noise will be applied. Only applicable if apply_method is defined. Defaults to None.

None

Returns:

Name Type Description
background ndarray

The generated noise volume (if apply_to is None) or the input volume with added noise (if apply_to is not None).

Raises:

Type Description
ValueError

If apply_method is not one of 'add', 'subtract', 'multiply', or 'divide'.

ValueError

If apply_method is provided without apply_to input volume provided, or vice versa.

Example
import qim3d

# Generate noise volume
background = qim3d.generate.background(
    background_shape = (128, 128, 128),
    baseline_value = 20,
    min_noise_value = 100,
    max_noise_value = 200,
)

qim3d.viz.volumetric(background)
Example
import qim3d

# Generate synthetic collection of volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

# Apply noise to the synthetic collection
noisy_collection = qim3d.generate.background(
    background_shape = volume_collection.shape,
    min_noise_value = 0,
    max_noise_value = 20,
    generate_method = 'add',
    apply_method = 'add',
    apply_to = volume_collection
)

qim3d.viz.volumetric(noisy_collection)
Example
import qim3d

# Generate synthetic collection of volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

# Apply noise to the synthetic collection
noisy_collection = qim3d.generate.background(
    background_shape = volume_collection.shape,
    baseline_value = 0,
    min_noise_value = 0,
    max_noise_value = 30,
    generate_method = 'add',
    apply_method = 'divide',
    apply_to = volume_collection
)

qim3d.viz.volumetric(noisy_collection)

qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
synthetic_noisy_collection_slices

Example

import qim3d

# Generate synthetic collection of volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

# Apply noise to the synthetic collection
noisy_collection = qim3d.generate.background(
    background_shape = (200, 200, 200),
    baseline_value = 100,
    min_noise_value = 0.8,
    max_noise_value = 1.2,
    generate_method = "multiply",
    apply_method = "add",
    apply_to = volume_collection
)

qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
synthetic_noisy_collection_slices

Source code in qim3d/generate/_generators.py
def background(
    background_shape: tuple,
    baseline_value: float = 0,
    min_noise_value: float = 0,
    max_noise_value: float = 20,
    generate_method: str = 'add',
    apply_method: str = None,
    seed: int = 0,
    dtype: str = 'uint8',
    apply_to: np.ndarray = None,
) -> np.ndarray:
    """
    Generate a noise volume with random intensity values from a uniform distribution.

    Args:
        background_shape (tuple): The shape of the noise volume to generate.
        baseline_value (float, optional): The baseline intensity of the noise volume. Default is 0.
        min_noise_value (float, optional): The minimum intensity of the noise. Default is 0.
        max_noise_value (float, optional): The maximum intensity of the noise. Default is 20.
        generate_method (str, optional): The method used to combine `baseline_value` and noise. Choose from 'add' (`baseline + noise`), 'subtract' (`baseline - noise`), 'multiply' (`baseline * noise`), or 'divide' (`baseline / (noise+ε)`). Default is 'add'.
        apply_method (str, optional): The method to apply the generated noise to `apply_to`, if provided. Choose from 'add' (`apply_to + background`), 'subtract' (`apply_to - background`), 'multiply' (`apply_to * background`), or 'divide' (`apply_to / (background+ε)`). Only applicable if apply_to is defined. Default is None.
        seed (int, optional): The seed for the random number generator. Default is 0.
        dtype (data-type, optional): Desired data type of the output volume. Default is 'uint8'.
        apply_to (np.ndarray, optional): An input volume to which noise will be applied. Only applicable if apply_method is defined. Defaults to None.

    Returns:
        background (np.ndarray): The generated noise volume (if `apply_to` is None) or the input volume with added noise (if `apply_to` is not None).

    Raises:
        ValueError: If `apply_method` is not one of 'add', 'subtract', 'multiply', or 'divide'.
        ValueError: If `apply_method` is provided without `apply_to` input volume provided, or vice versa.

    Example:
        ```python
        import qim3d

        # Generate noise volume
        background = qim3d.generate.background(
            background_shape = (128, 128, 128),
            baseline_value = 20,
            min_noise_value = 100,
            max_noise_value = 200,
        )

        qim3d.viz.volumetric(background)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_noise_background.html" width="100%" height="500" frameborder="0"></iframe>

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

        # Apply noise to the synthetic collection
        noisy_collection = qim3d.generate.background(
            background_shape = volume_collection.shape,
            min_noise_value = 0,
            max_noise_value = 20,
            generate_method = 'add',
            apply_method = 'add',
            apply_to = volume_collection
        )

        qim3d.viz.volumetric(noisy_collection)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_noisy_collection_1.html" width="100%" height="500" frameborder="0"></iframe>

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

        # Apply noise to the synthetic collection
        noisy_collection = qim3d.generate.background(
            background_shape = volume_collection.shape,
            baseline_value = 0,
            min_noise_value = 0,
            max_noise_value = 30,
            generate_method = 'add',
            apply_method = 'divide',
            apply_to = volume_collection
        )

        qim3d.viz.volumetric(noisy_collection)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_noisy_collection_2.html" width="100%" height="500" frameborder="0"></iframe>
        ```python
        qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
        ```
        ![synthetic_noisy_collection_slices](../../assets/screenshots/synthetic_noisy_collection_slices_2.png)

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

        # Apply noise to the synthetic collection
        noisy_collection = qim3d.generate.background(
            background_shape = (200, 200, 200),
            baseline_value = 100,
            min_noise_value = 0.8,
            max_noise_value = 1.2,
            generate_method = "multiply",
            apply_method = "add",
            apply_to = volume_collection
        )

        qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
        ```
        ![synthetic_noisy_collection_slices](../../assets/screenshots/synthetic_noisy_collection_slices_3.png)

    """
    # Ensure dtype is a valid NumPy type
    dtype = np.dtype(dtype)

    # Define supported apply methods
    apply_operations = {
        'add': lambda a, b: a + b,
        'subtract': lambda a, b: a - b,
        'multiply': lambda a, b: a * b,
        'divide': lambda a, b: a / (b + 1e-8),  # Avoid division by zero
    }

    # Check if apply_method is provided without apply_to volume, or vice versa
    if (apply_to is None and apply_method is not None) or (
        apply_to is not None and apply_method is None
    ):
        msg = 'Supply both apply_method and apply_to when applying background to a volume.'
        # Validate apply_method
        raise ValueError(msg)

    # Validate apply_method
    if apply_method is not None and apply_method not in apply_operations:
        msg = f"Invalid apply_method '{apply_method}'. Choose from {list(apply_operations.keys())}."
        raise ValueError(msg)

    # Validate generate_method
    if generate_method not in apply_operations:
        msg = f"Invalid generate_method '{generate_method}'. Choose from {list(apply_operations.keys())}."
        raise ValueError(msg)

    # Check for shape mismatch
    if (apply_to is not None) and (apply_to.shape != background_shape):
        msg = f'Shape of input volume {apply_to.shape} does not match requested background_shape {background_shape}. Using input shape instead.'
        background_shape = apply_to.shape
        log.info(msg)

    # Generate the noise volume
    baseline = np.full(shape=background_shape, fill_value=baseline_value)

    # Start seeded generator
    rng = np.random.default_rng(seed=seed)
    noise = rng.uniform(
        low=float(min_noise_value), high=float(max_noise_value), size=background_shape
    )

    # Return error if multiplying or dividing with 0
    if baseline_value == 0.0 and (
        generate_method == 'multiply' or generate_method == 'divide'
    ):
        msg = f'Selection of baseline_value=0 and generate_method="{generate_method}" will not generate background noise. Either add baseline_value>0 or change generate_method.'
        raise ValueError(msg)

    # Apply method to initial background computation
    background_volume = apply_operations[generate_method](baseline, noise)

    # Warn user if the background noise is constant or none
    if np.min(background_volume) == np.max(background_volume):
        msg = 'Warning: The used settings have generated a background with a uniform value.'
        log.info(msg)

    # Apply method to the target volume if specified
    if apply_to is not None:
        background_volume = apply_operations[apply_method](apply_to, background_volume)

    # Clip value before dtype convertion
    clip_value = (
        np.iinfo(dtype).max if np.issubdtype(dtype, np.integer) else np.finfo(dtype).max
    )
    background_volume = np.clip(background_volume, 0, clip_value).astype(dtype)

    return background_volume