Skip to content

Synthetic Image Cases

voids.generators.porous_image provides package-level helpers for constructing controlled 2-D and 3-D binary porous images. These are intended for testing, algorithm development, and reproducible demonstrations before moving to scanner-derived data.

The central convention is:

  • True means void,
  • False means solid or matrix,
  • all shapes follow NumPy axis order.

Macro/Micro Pore Images

The macro/micro generator builds a two-porosity synthetic image from two PoreSpy blobs fields:

  1. generate a resolved macropore image,
  2. generate a finer-scale micropore score field,
  3. keep micropores only inside the solid matrix of the macropore image,
  4. combine the two masks.
from voids.generators import generate_macro_micro_blobs_matrix

case = generate_macro_micro_blobs_matrix(
    shape=(160, 160, 160),
    macro_porosity=0.25,
    matrix_microporosity=0.12,
    macro_blobiness=1.2,
    micropore_blobiness=8.0,
    axis_index=0,
    seed_start=2026,
    max_tries=50,
)

void = case.void
macro = case.macro_void
micropores = case.micropore_void

The invariant is:

\[ \Omega_{\mathrm{void}} = \Omega_{\mathrm{macro}} \cup \Omega_{\mathrm{micro}}, \qquad \Omega_{\mathrm{micro}} \subset \Omega_{\mathrm{matrix}}. \]

The reported total porosity is:

\[ \phi_{\mathrm{total}} = \phi_{\mathrm{macro}} + (1-\phi_{\mathrm{macro}}) \phi_{\mathrm{micro|matrix}}, \]

where \(\phi_{\mathrm{micro|matrix}}\) is the micropore fraction measured only in the matrix region of the macro image.

The micropore_blobiness parameter controls the feature scale of the small pores. In PoreSpy blobs, larger blobiness values produce smaller features, so a typical macro/micro setup uses:

  • lower macro_blobiness for broader connected macropores,
  • higher micropore_blobiness for smaller pores in the matrix.

Synthetic interpretation

The micropore mask is a controlled synthetic model, not a calibrated unresolved-porosity law. It is useful for algorithm sensitivity studies, but the values should not be interpreted as a measured carbonate microporosity field without calibration.


Vug Insertions

Existing helpers can add idealized circular, elliptical, spherical, or ellipsoidal vugs to a binary matrix:

from voids.generators import insert_spherical_vug

with_vug, vug_mask = insert_spherical_vug(
    case.void,
    radius_vox=18,
    center=(80, 80, 80),
)

The operation is a boolean union, so existing pores remain void.

For sweeps that specify vug support as a fraction of the complete voxel volume, use a centered superellipsoid:

from voids.generators import insert_centered_superellipsoidal_vug

with_vug, vug_mask = insert_centered_superellipsoidal_vug(
    case.void,
    volume_fraction=0.40,
    exponent=2.5,
    margin_vox=1,
)

The mask contains round(volume_fraction * case.void.size) voxels. An exponent of 2 is spherical; larger exponents progressively approach a rounded cube. This matters for high fractions: an enclosed sphere can occupy no more than \(\pi/6\approx0.524\) of a cube. The generator rejects a requested fraction that cannot fit inside the configured exponent and boundary margin instead of silently clipping it. When the requested count intersects a tied outer score shell, deterministic tie selection preserves the exact voxel count but can break perfect reflection symmetry only on that final shell.

The executed 52_mwe_synthetic_3d_matrix_centered_vug_volume_fraction notebook demonstrates a \(300^3\), 10 µm sweep through vug fraction 0.60 and includes a live central-slice atlas.


Export And Import

Synthetic cases are ordinary voids image volumes once generated. Export and import them through the dedicated Image Volume And Surface Mesh I/O section, which documents VolumeData, supported voxel formats, STL/OBJ surface exports, sidecar metadata, and explicit voxel-size handling.

A typical synthetic-case export wraps the generated void mask with physical resolution metadata before writing files:

from voids.io import VolumeData, save_volume_bundle

case_data = VolumeData(
    with_vug,
    voxel_size=(40.0e-6, 40.0e-6, 40.0e-6),
    units={"length": "m"},
    metadata=case.metadata,
)

written = save_volume_bundle(
    case_data,
    "outputs/synthetic_case",
    stem="macro_micro_vug",
    formats=("raw", "npy", "h5", "nc", "tiff", "stl", "obj"),
)

Read the exported data with load_volume_data when physical resolution matters:

from voids.io import load_volume_data

volume_data = load_volume_data("outputs/synthetic_case/macro_micro_vug.tiff")