Skip to content

Synthetic data generation

Generation for synthetic datasets.

qim3d.generate.ParameterVisualizer

Interactive Jupyter widget for exploring synthetic data generation parameters.

Provides a Graphical User Interface (GUI) to tune the parameters of qim3d.generate.volume in real-time. Users can adjust sliders for noise scale, threshold, and gamma while immediately seeing the resulting 3D structure.

Parameters:

Name Type Description Default
base_shape tuple

Determines the shape of the generate volume. This will not be updated 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.

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.

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.

0.0
nsmax float

Determines maximum value for the noise scale slider.

0.1
dsmin float

Determines minimum value for the decay rate slider.

0.1
dsmax float

Determines maximum value for the decay rate slider.

20.0
gsmin float

Determines minimum value for the gamma slider.

0.1
gsmax float

Determines maximum value for the gamma slider.

2.0
tsmin float

Determines minimum value for the threshold slider.

0.0
tsmax float

Determines maximum value for the threshold slider.

1.0
grid_visible bool

Determines if the grid should be visible upon plot generation.

False

Raises:

Type Description
ValueError

If either base_shape, noise slider, decay slider, gamma slider, or threshold slider 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
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 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
class ParameterVisualizer:
    """
    Interactive Jupyter widget for exploring synthetic data generation parameters.

    Provides a Graphical User Interface (GUI) to tune the parameters of `qim3d.generate.volume` 
    in real-time. Users can adjust sliders for noise scale, threshold, and gamma while immediately 
    seeing the resulting 3D structure.

    Args:
        base_shape (tuple, optional): Determines the shape of the generate volume. This will not be updated 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.
        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.
        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. 
        nsmax (float, optional): Determines maximum value for the noise scale slider.
        dsmin (float, optional): Determines minimum value for the decay rate slider. 
        dsmax (float, optional): Determines maximum value for the decay rate slider. 
        gsmin (float, optional): Determines minimum value for the gamma slider. 
        gsmax (float, optional): Determines maximum value for the gamma slider. 
        tsmin (float, optional): Determines minimum value for the threshold slider. 
        tsmax (float, optional): Determines maximum value for the threshold slider. 
        grid_visible (bool, optional): Determines if the grid should be visible upon plot generation.

    Raises:
        ValueError: If either `base_shape`, `noise slider`, `decay slider`, `gamma slider`, or `threshold slider` 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):
        """
        Extracts the generated volume from the widget's current state.

        Allows you to retrieve the numpy array resulting from your interactive adjustments 
        so you can use it in your pipeline (e.g., saving it or using it for training).

        Returns:
            vol (numpy.ndarray): The 3D volume currently visualized in the widget.

        Example:
            ```python
            # After adjusting sliders in the widget:
            my_custom_blob = viz.get_volume()
            qim3d.io.save("custom_blob.tif", my_custom_blob)
            ```
        """
        return self.plt_volume.volume

qim3d.generate.ParameterVisualizer.get_volume

get_volume()

Extracts the generated volume from the widget's current state.

Allows you to retrieve the numpy array resulting from your interactive adjustments so you can use it in your pipeline (e.g., saving it or using it for training).

Returns:

Name Type Description
vol ndarray

The 3D volume currently visualized in the widget.

Example
# After adjusting sliders in the widget:
my_custom_blob = viz.get_volume()
qim3d.io.save("custom_blob.tif", my_custom_blob)
Source code in qim3d/generate/_generators.py
def get_volume(self):
    """
    Extracts the generated volume from the widget's current state.

    Allows you to retrieve the numpy array resulting from your interactive adjustments 
    so you can use it in your pipeline (e.g., saving it or using it for training).

    Returns:
        vol (numpy.ndarray): The 3D volume currently visualized in the widget.

    Example:
        ```python
        # After adjusting sliders in the widget:
        my_custom_blob = viz.get_volume()
        qim3d.io.save("custom_blob.tif", my_custom_blob)
        ```
    """
    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,
)

Generates a synthetic 3D volume using structured Perlin noise.

Creates valid 3D morphological structures that resemble biological or material samples (e.g., cells, tissues, pores). By default, it generates a "blob-like" object, but it can also create specific geometric shapes like cylinders or tubes.

This function is ideal for:

  • Benchmarking: Creating standard inputs for testing algorithms.
  • Augmentation: Generating synthetic samples to train deep learning models.
  • Simulation: Modeling physical structures with controlled noise properties.

Supported Shapes:

  • Blob: (Default) amorphous, organic-looking structure.
  • Cylinder: A solid cylindrical rod.
  • Tube: A hollow cylinder.

Parameters:

Name Type Description Default
base_shape tuple

The resolution of the internal noise grid. Higher values create finer details but require more computation.

(128, 128, 128)
final_shape tuple

The final output resolution. If None, matches base_shape. Use this to upsample the generated volume.

None
noise_scale float

Controls the "zoom" of the noise texture. Smaller values = smooth, large features. Larger values = rough, high-frequency details.

0.02
noise_type str

The noise algorithm: perlin (standard) or simplex (faster, different artifacts).

'perlin'
decay_rate float

Controls how quickly the object fades into the background at the edges. Higher values create sharper, distinct boundaries.

10
gamma float

Adjusts contrast. <1 flattens values (fuzzier), >1 pushes values to extremes (binary-like).

1
threshold float

The cut-off value (0.0 to 1.0) defining the object's surface. Lower values make the object larger/fatter; higher values make it smaller/thinner.

0.5
max_value float

The maximum intensity value in the output array (e.g., 255 for 8-bit images).

255
shape str

Forces the volume into a geometric shape: None (Blob), cylinder, or tube.

None
tube_hole_ratio float

Only for tube. Defines the relative thickness of the wall. 0.1 is a thick wall, 0.9 is a thin shell.

0.5
axis int

The orientation axis (0, 1, or 2) for cylinders and tubes.

0
order int

Interpolation order when resizing to final_shape (0=Nearest, 1=Linear, etc.).

1
dtype str

Output data type (e.g., uint8, float32).

'uint8'
hollow int

If > 0, hollows out the blob by eroding the center, creating a shell of thickness hollow.

0
seed int

Random seed for reproducibility.

0

Returns:

Name Type Description
vol ndarray

The generated 3D volume.

Raises:

Type Description
ValueError

If shape or noise_type is invalid.

TypeError

If either base_shape or 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:
    """
    Generates a synthetic 3D volume using structured Perlin noise.

    Creates valid 3D morphological structures that resemble biological or material samples 
    (e.g., cells, tissues, pores). By default, it generates a "blob-like" object, but it can 
    also create specific geometric shapes like **cylinders** or **tubes**.

    This function is ideal for:

    * **Benchmarking:** Creating standard inputs for testing algorithms.
    * **Augmentation:** Generating synthetic samples to train deep learning models.
    * **Simulation:** Modeling physical structures with controlled noise properties.

    **Supported Shapes:**

    * **Blob:** (Default) amorphous, organic-looking structure.
    * **Cylinder:** A solid cylindrical rod.
    * **Tube:** A hollow cylinder.

    Args:
        base_shape (tuple, optional): 
            The resolution of the internal noise grid. Higher values create finer details 
            but require more computation.
        final_shape (tuple, optional): 
            The final output resolution. If `None`, matches `base_shape`. 
            Use this to upsample the generated volume.
        noise_scale (float, optional): 
            Controls the "zoom" of the noise texture. Smaller values = smooth, large features. 
            Larger values = rough, high-frequency details.
        noise_type (str, optional): 
            The noise algorithm: `perlin` (standard) or `simplex` (faster, different artifacts).
        decay_rate (float, optional): 
            Controls how quickly the object fades into the background at the edges. 
            Higher values create sharper, distinct boundaries.
        gamma (float, optional): 
            Adjusts contrast. `<1` flattens values (fuzzier), `>1` pushes values to extremes (binary-like).
        threshold (float, optional): 
            The cut-off value (0.0 to 1.0) defining the object's surface. 
            Lower values make the object larger/fatter; higher values make it smaller/thinner.
        max_value (float, optional): 
            The maximum intensity value in the output array (e.g., 255 for 8-bit images).
        shape (str, optional): 
            Forces the volume into a geometric shape: `None` (Blob), `cylinder`, or `tube`.
        tube_hole_ratio (float, optional): 
            Only for `tube`. Defines the relative thickness of the wall. 
            `0.1` is a thick wall, `0.9` is a thin shell.
        axis (int, optional): 
            The orientation axis (0, 1, or 2) for cylinders and tubes.
        order (int, optional): 
            Interpolation order when resizing to `final_shape` (0=Nearest, 1=Linear, etc.).
        dtype (str, optional): 
            Output data type (e.g., `uint8`, `float32`).
        hollow (int, optional): 
            If > 0, hollows out the blob by eroding the center, creating a shell of thickness `hollow`.
        seed (int, optional): 
            Random seed for reproducibility.

    Returns:
        vol (numpy.ndarray): 
            The generated 3D volume.

    Raises:
        ValueError: If `shape` or `noise_type` is invalid.
        TypeError: If either `base_shape` or `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(
    n_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,
)

Generates a synthetic dataset of multiple non-overlapping volumes with ground truth labels.

This function creates a "collection" volume populated with multiple objects. It uses a collision detection algorithm to ensure objects do not overlap. Crucially, it returns a label mask where each object is identified by a unique integer ID, making this tool ideal for generating training data for Instance Segmentation or Object Detection models.

Objects can be generated synthetically (blobs, cylinders, tubes) with randomized properties, or you can inject your own pre-existing 3D arrays using the data parameter.

Parameters:

Name Type Description Default
n_volumes int

Target number of volumes/objects to place in the collection.

15
collection_shape tuple

Dimensions of the final container volume (Z, Y, X).

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

Pre-defined 3D volume(s) to place into the collection. If provided, the function picks randomly from this list instead of generating new synthetic blobs. Useful for creating scenes with specific real-world objects.

None
positions list[tuple]

List of specific (z, y, x) center coordinates for placement. If None, positions are chosen randomly. If provided, n_volumes must match the length of this list.

None
shape_range tuple[tuple]

Defines the size variance of generated objects. Format: ((min_z, min_y, min_x), (max_z, max_y, max_x)).

((40, 40, 40), (60, 60, 60))
shape_magnification_range tuple[float]

Range for random uniform scaling factors applied to the object shape.

(1.0, 1.0)
noise_type str

Algorithm for synthetic texture generation: 'perlin', 'simplex', or 'mixed' (randomly selects per object).

'perlin'
noise_range tuple[float]

Range for the noise scale parameter (roughness).

(0.02, 0.03)
rotation_degree_range tuple[int]

Range of rotation angles (in degrees) to apply to each object.

(0, 360)
rotation_axes list[tuple]

List of axis pairs to rotate around (e.g., [(0, 1)] for XY rotation).

None
gamma_range tuple[float]

Range for gamma correction (contrast).

(0.9, 1)
value_range tuple[int]

Range for the maximum intensity value of the objects.

(128, 255)
threshold_range tuple[float]

Range for the threshold used to define the object surface/size.

(0.5, 0.55)
decay_rate_range tuple[float]

Range for the edge decay rate (fading at boundaries).

(5, 10)
shape str

Force a specific geometric shape: 'cylinder', 'tube', or None (organic blob).

None
tube_hole_ratio float

Ratio of the inner hole if shape='tube'.

0.5
axis int

Orientation axis (0, 1, 2) if shape is defined.

0
verbose bool

If True, enables detailed logging of placement attempts.

False
same_seed bool

If True, reuses the same random seed for every object (they will look identical).

False
hollow bool

If True, applies a hollowing operation to synthetic objects.

False
seed int

Global random seed for reproducibility.

0
dtype str

Data type of the output arrays.

'uint8'
return_positions bool

If True, the function returns a third element containing the list of successfully placed coordinates.

False

Returns:

Name Type Description
volume_collection ndarray

The 3D volume containing all placed objects.

labels ndarray

A 3D integer mask of the same shape, where 0 is background and 1..N are the object IDs.

positions list[tuple]

(Only if return_positions=True) A list of (z, y, x) coordinates for the centers of the placed objects.

Raises:

Type Description
TypeError

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

ValueError

If data contains non-3D arrays or volumes larger than collection_shape.

ValueError

If ranges or shapes are incorrectly defined.

Example
import qim3d

# Generate synthetic collection of volumes
n_volumes = 15
volume_collection, labels = qim3d.generate.volume_collection(n_volumes=n_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(n_labels=n_volumes)
qim3d.viz.slicer(labels, colormap=cmap, max_value=n_volumes)
synthetic_collection

Collection of fiber-like structures
import qim3d

# Generate synthetic collection of cylindrical structures
volume_collection, labels = qim3d.generate.volume_collection(
    n_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(
    n_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(n_volumes = 30,
                                                             data = [volume_1, volume_2])
# Visualize
qim3d.viz.volumetric(volume_collection)
Source code in qim3d/generate/_aggregators.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
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
def volume_collection(
    n_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]:
    """
    Generates a synthetic dataset of multiple non-overlapping volumes with ground truth labels.

    This function creates a "collection" volume populated with multiple objects. It uses a collision 
    detection algorithm to ensure objects do not overlap. Crucially, it returns a **label mask** where each object is identified by a unique integer ID, making this tool ideal for generating 
    training data for **Instance Segmentation** or **Object Detection** models.

    Objects can be generated synthetically (blobs, cylinders, tubes) with randomized properties, 
    or you can inject your own pre-existing 3D arrays using the `data` parameter.

    Args:
        n_volumes (int, optional): 
            Target number of volumes/objects to place in the collection.
        collection_shape (tuple, optional): 
            Dimensions of the final container volume (Z, Y, X).
        data (numpy.ndarray or list[numpy.ndarray], optional): 
            Pre-defined 3D volume(s) to place into the collection. If provided, the function picks 
            randomly from this list instead of generating new synthetic blobs. Useful for creating 
            scenes with specific real-world objects.
        positions (list[tuple], optional): 
            List of specific (z, y, x) center coordinates for placement. If `None`, positions are 
            chosen randomly. If provided, `n_volumes` must match the length of this list.
        shape_range (tuple[tuple], optional): 
            Defines the size variance of generated objects. Format: `((min_z, min_y, min_x), (max_z, max_y, max_x))`.
        shape_magnification_range (tuple[float], optional): 
            Range for random uniform scaling factors applied to the object shape.
        noise_type (str, optional): 
            Algorithm for synthetic texture generation: `'perlin'`, `'simplex'`, or `'mixed'` (randomly selects per object).
        noise_range (tuple[float], optional): 
            Range for the noise scale parameter (roughness).
        rotation_degree_range (tuple[int], optional): 
            Range of rotation angles (in degrees) to apply to each object.
        rotation_axes (list[tuple], optional): 
            List of axis pairs to rotate around (e.g., `[(0, 1)]` for XY rotation).
        gamma_range (tuple[float], optional): 
            Range for gamma correction (contrast).
        value_range (tuple[int], optional): 
            Range for the maximum intensity value of the objects.
        threshold_range (tuple[float], optional): 
            Range for the threshold used to define the object surface/size.
        decay_rate_range (tuple[float], optional): 
            Range for the edge decay rate (fading at boundaries).
        shape (str, optional): 
            Force a specific geometric shape: `'cylinder'`, `'tube'`, or `None` (organic blob).
        tube_hole_ratio (float, optional): 
            Ratio of the inner hole if `shape='tube'`.
        axis (int, optional): 
            Orientation axis (0, 1, 2) if `shape` is defined.
        verbose (bool, optional): 
            If `True`, enables detailed logging of placement attempts.
        same_seed (bool, optional): 
            If `True`, reuses the same random seed for every object (they will look identical).
        hollow (bool, optional): 
            If `True`, applies a hollowing operation to synthetic objects.
        seed (int, optional): 
            Global random seed for reproducibility.
        dtype (str, optional): 
            Data type of the output arrays.
        return_positions (bool, optional): 
            If `True`, the function returns a third element containing the list of successfully placed coordinates.

    Returns:
        volume_collection (numpy.ndarray): 
            The 3D volume containing all placed objects.
        labels (numpy.ndarray): 
            A 3D integer mask of the same shape, where 0 is background and 1..N are the object IDs.
        positions (list[tuple]): 
            (Only if `return_positions=True`) A list of (z, y, x) coordinates for the centers of the placed objects.

    Raises:
        TypeError: If `data` is not a numpy array or list of arrays.
        ValueError: If `data` contains non-3D arrays or volumes larger than `collection_shape`.
        ValueError: If ranges or shapes are incorrectly defined.

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        n_volumes = 15
        volume_collection, labels = qim3d.generate.volume_collection(n_volumes=n_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(n_labels=n_volumes)
        qim3d.viz.slicer(labels, colormap=cmap, max_value=n_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(
            n_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(
            n_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(n_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) != n_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(n_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 n_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}/{n_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,
)

Generates a 3D noise field or adds synthetic background noise to an existing volume.

Unlike volume (which creates structures), this function generates unstructured uniform noise. It is useful for simulating:

  • Sensor Noise: Electronic noise or grain common in CT/microscopy scans.
  • Imaging Artifacts: Low-contrast background variations.
  • Data Augmentation: Making training data more robust by adding random interference.

Parameters:

Name Type Description Default
background_shape tuple

The shape of the noise volume to generate (Z, Y, X).

required
baseline_value float

The constant base intensity level of the background.

0
min_noise_value float

The lower bound of the random noise distribution.

0
max_noise_value float

The upper bound of the random noise distribution.

20
generate_method str

How to combine the baseline with the noise: 'add', 'subtract', 'multiply', or 'divide'.

'add'
apply_method str

If apply_to is provided, this defines how the noise is merged with the input volume.

None
seed int

Random seed for reproducibility.

0
dtype str

Output data type.

'uint8'
apply_to ndarray

An existing 3D volume. If provided, the noise is applied directly to this array using apply_method.

None

Returns:

Name Type Description
background ndarray

The noise volume (if apply_to is None) or the modified input volume.

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:
    """
    Generates a 3D noise field or adds synthetic background noise to an existing volume.

    Unlike `volume` (which creates structures), this function generates **unstructured uniform noise**. 
    It is useful for simulating:

    * **Sensor Noise:** Electronic noise or grain common in CT/microscopy scans.
    * **Imaging Artifacts:** Low-contrast background variations.
    * **Data Augmentation:** Making training data more robust by adding random interference.

    Args:
        background_shape (tuple): 
            The shape of the noise volume to generate (Z, Y, X).
        baseline_value (float, optional): 
            The constant base intensity level of the background.
        min_noise_value (float, optional): 
            The lower bound of the random noise distribution.
        max_noise_value (float, optional): 
            The upper bound of the random noise distribution.
        generate_method (str, optional): 
            How to combine the baseline with the noise: `'add'`, `'subtract'`, `'multiply'`, or `'divide'`.
        apply_method (str, optional): 
            If `apply_to` is provided, this defines how the noise is merged with the input volume.
        seed (int, optional): 
            Random seed for reproducibility.
        dtype (str, optional): 
            Output data type.
        apply_to (numpy.ndarray, optional): 
            An existing 3D volume. If provided, the noise is applied directly to this array 
            using `apply_method`.

    Returns:
        background (numpy.ndarray): 
            The noise volume (if `apply_to` is None) or the modified input volume.

    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