Porosity Maps¶
This page documents the continuum porosity-map workflow implemented in
voids.image.porosity.
It is separate from pore-network porosity: here the output is a regular grid of
cell-average porosity values intended for continuum, FEM, finite-volume, or
external solver workflows.
The minimal demonstration is
notebooks/33_mwe_synthetic_porosity_maps, which builds porosity maps from a
synthetic PoreSpy blobs binary image and a derived toy grayscale image.
What block_shape Means¶
block_shape defines how many fine image voxels are averaged into one porosity-map
cell.

The 2-D schematic shows the same operation used in 3-D: each highlighted fine voxel block becomes one coarse cell whose value is the average porosity over that block.
For a 3-D image with shape
and coarse-cell block sizes \((b_0, b_1, b_2)\), represented in code by
block_shape=(b_0, b_1, b_2),
the output porosity map has shape
When strict=True, which is the default, every image dimension must be exactly
divisible by the corresponding block dimension.
For example:
gives a porosity map with shape:
Each coarse porosity cell is the average over one \(10 \times 10 \times 10\) voxel block. If the fine voxel size is \(40\,\mu\mathrm{m}\), then the porosity-map cell size is:
In code:
porosity = porosity_map_from_binary(
image,
block_shape=(10, 10, 10),
voxel_size=40.0e-6,
)
porosity.shape # (30, 30, 30)
porosity.cell_size # (4.0e-4, 4.0e-4, 4.0e-4) in meters
Axis convention
block_shape follows the NumPy array axis order of the input image.
voids does not silently reinterpret array axes as geological or scanner axes.
If a dataset uses a different physical axis order, transpose or document that
convention before exporting fields to another solver.
Binary-Image Porosity¶
For a segmented binary image, the input is interpreted as a void mask. By default:
Trueor1means void,Falseor0means solid.
Let \(V_{ijk}\) be the fine-grid void indicator:
For porosity-map cell \((I,J,K)\), with block shape \((b_0,b_1,b_2)\), the local porosity is:
So the binary porosity map is exactly a local void-volume fraction on the regular image grid.
If image_is_void=False, the input binary image is interpreted as a solid mask
and inverted before applying the same formula.
Conservation Check¶
If the image shape is exactly divisible by block_shape, the mean of the local
porosity map equals the global void fraction:
where \(N_c\) is the number of coarse cells and \(N_v\) is the number of fine voxels.
This is one of the most important synthetic verification checks.
Grayscale-Image Porosity¶
For a grayscale image, voids first maps each fine voxel intensity to a porosity
value using a two-point linear calibration.
The calibration inputs are:
| Parameter | Meaning |
|---|---|
solid_gray |
grayscale value assigned to background_porosity |
pore_gray |
grayscale value assigned to porosity \(1\) |
background_porosity |
porosity floor at the solid endpoint |
Let \(G_{ijk}\) be the grayscale value of voxel \((i,j,k)\), \(G_s\) be
solid_gray, \(G_p\) be pore_gray, and \(\phi_b\) be background_porosity.
The voxel-scale porosity is:
By default, values are clipped to:
Then the coarse grayscale porosity map is a block average of those voxel porosities:

The top row corresponds to the binary void-fraction formula. The bottom row corresponds to the grayscale calibration formula followed by the same block average.
Micro-CT Grayscale Calibration In The Literature¶
Ferreira et al. (2020) used resampled micro-CT images of carbonate plugs to construct porosity fields for a two-scale continuum reactive-flow solver. The image-to-porosity calculation assigns a pore grayscale threshold to 100% porosity, a solid grayscale threshold to a background porosity \(BP\), and linearly maps each micro-CT voxel between those endpoints. In fractional notation, the same calculation is:
This is the same two-endpoint linear calibration implemented by
calibrated_porosity_from_grayscale, with background_porosity playing the
role of \(BP\). The additional block_shape averaging in voids is a separate
coarsening step: use block_shape=(1, 1, 1) when each image voxel is intended
to become one continuum cell, or use larger blocks when the target solver uses a
coarser regular grid.
Dark-Pore And Bright-Pore Images¶
The formula does not require pores to be darker than solid.
It only requires solid_gray and pore_gray to be distinct.
For a dark-pore image:
For a bright-pore image:
Both cases are handled by the same denominator \(G_p - G_s\).
Important Assumption¶
The grayscale path assumes that a linear grayscale interpolation is a defensible proxy for local porosity between the two calibration endpoints. That may be incorrect when the image has severe beam hardening, ring artifacts, mixed mineral phases, uncorrected scanner drift, or a nonlinear intensity-density relationship. For real micro-CT data, calibration must be treated as part of the physical model, not as a plotting choice.
Physical Volumes¶
For a porosity map with cell size
the bulk volume of one porosity cell is:
The total represented bulk volume is:
The implied pore volume is:
The mean porosity is therefore:
This relation is used by PorosityMap.bulk_volume,
PorosityMap.void_volume, and PorosityMap.mean_porosity.
2-D maps
The same implementation accepts 2-D arrays, where the product of cell_size
is an area. If the target solver expects a 3-D volume, supply a 3-D image or
define an explicit thickness outside this 2-D helper.
Kozeny-Carman Permeability Field¶
voids can also generate an associated permeability field from a porosity map
using the Kozeny-Carman closure.
For cell porosity \(\phi\), characteristic length \(d\), and Kozeny constant
\(C\), the implemented permeability form is:
The equivalent inverse-permeability, or Darcy resistance, form is:

The permeability map and inverse-permeability map live on the same regular grid as the porosity map. The figure uses a logarithmic color scale for \(k\) and \(k^{-1}\), because permeability closures can vary by orders of magnitude.
The default \(C=180\) is the common packed-sphere Kozeny-Carman value associated with the classical Kozeny and Carman packed-bed relation. The length \(d\) is a model parameter, not inferred automatically. In a sub-resolution porosity interpretation, \(d\) is often tied to the voxel or control-volume length scale when no better grain-scale or micropore-scale calibration is available, but that choice is empirical and should be tested.
The endpoint limits are:
These are mathematical limits. Many external solvers require finite values, so
voids exposes explicit caps and endpoint parameters:
from voids.image.porosity import permeability_map_from_porosity
permeability = permeability_map_from_porosity(
porosity,
characteristic_length=4.0e-4,
kozeny_constant=180.0,
max_permeability=1.0e-8,
)
The resulting object is a PermeabilityMap on the same grid as the input
porosity map:
permeability.values # k field
permeability.inverse_values # k^{-1} field for Darcy-Brinkman resistance terms
permeability.cell_size # inherited from the porosity map
Closure calibration
The Kozeny-Carman field is a closure model, not a direct measurement. The same porosity map can produce very different permeability fields if \(d\), \(C\), endpoint handling, or caps are changed. For real data, these parameters should be calibrated or at least sensitivity-tested against laboratory permeability, direct numerical simulation, or another reference.
Exported HDF5 Files¶
save_porosity_map_hdf5 writes:
/porosity: the cell-average porosity array,- root attribute
schema_version, - root attribute
metadata, encoded as JSON.
save_permeability_map_hdf5 writes:
/permeability: the cell-wise permeability array,- root attribute
schema_version, - root attribute
metadata, encoded as JSON.
The metadata includes:
- porosity-map shape,
- cell size,
- origin,
- units,
- source/calibration metadata such as
block_shape,solid_gray,pore_gray, andbackground_porosity.
This export is intentionally solver-neutral. It is a stable field file, not a full FEM mesh. Solver-specific exporters should define cell ordering, mesh topology, boundary patches, and field naming explicitly.
Structured Mesh Export¶
voids.mesh can convert a regular PorosityMap, optionally paired with a
matching PermeabilityMap, into a structured mesh. The default cells are
quadrilaterals for 2-D maps and hexahedra for 3-D maps. If a downstream solver
expects simplex cells, the same map grid can also be subdivided into triangles
in 2-D or tetrahedra in 3-D.
The cell data order is explicit:
This means the mesh is a representation of the coarse map grid, not a
segmentation-boundary mesh of the original image.
For a 2-D slice, each coarse porosity cell becomes one quadrilateral by default,
or two triangles when element_type="triangle". For a 3-D map, each coarse
porosity cell becomes one hexahedron by default, or six tetrahedra when
element_type="tetra" or element_type="tetrahedron". In the simplex exports,
the child cells inherit the same porosity, permeability, and parent
cell_index values as the original coarse map cell.
The 2-D scheme shows the default cell-for-cell quadrilateral export. The 3-D default is analogous: each coarse map volume cell becomes one hexahedral mesh cell instead of one quadrilateral. The simplex options subdivide those default cells, but porosity and permeability remain cell-wise fields inherited from the parent map cell.
from voids.mesh import write_structured_map_meshes
paths = write_structured_map_meshes(
porosity,
"outputs/case_a",
stem="case_a_porosity_permeability",
permeability_map=permeability,
formats=("gmsh", "vtk", "vtu", "netgen"),
)
triangle_paths = write_structured_map_meshes(
porosity,
"outputs/case_a",
stem="case_a_porosity_permeability_triangles",
permeability_map=permeability,
formats=("gmsh", "vtk", "vtu"),
element_type="triangle",
)
The Gmsh .msh, VTK .vtk, and VTU .vtu exports are intended to carry the
floating porosity and permeability arrays as cell data.
The Netgen .vol writer available through meshio can write the structured
geometry, but it should not be treated as the authoritative carrier of
floating porosity/permeability fields. Keep the HDF5 map files, or a
cell-data-preserving format such as VTU, as the source of truth for the
coefficients.
voids uses meshio for mesh-file I/O.
The meshio project documents support for Gmsh, VTK, VTU, Netgen, and many other
formats, but format support does not imply that every downstream code preserves
the same field-data names, physical tags, or boundary-region conventions.
Check the exported mesh in the target solver before interpreting a simulation.
Gmsh export versus Gmsh meshing
The .msh export is a structured map mesh written in Gmsh format. It does
not run Gmsh to remesh the bone/marrow interface. The triangular and
tetrahedral options are structured subdivisions of the porosity-map cells,
not boundary-conforming image-to-geometry meshes. If a later workflow needs
a boundary-conforming triangular or tetrahedral mesh, the image-to-geometry
step and boundary labels should be specified as a separate model.
Synthetic Verification Plan¶
Synthetic cases are useful because the expected porosity is known before running the code. They should be used before interpreting real scanner-derived fields.
1. All-Void And All-Solid Images¶
For an all-void binary image:
For an all-solid binary image:
These cases verify phase polarity and the averaging formula.
2. Known Block Fractions¶
Construct a small image where each block has a manually known number of void voxels. For example, a \(2 \times 2\) block with three void voxels must give:
This verifies that block_shape is applied in the expected axis order.
3. Global Conservation¶
For a binary image whose shape is divisible by block_shape, verify:
This check is robust and should hold for random synthetic images, PoreSpy
blobs, checkerboards, and hand-built masks.
4. Grayscale Endpoint Calibration¶
Use a tiny grayscale image containing only the calibration endpoints:
The midpoint should give:
This verifies the linear calibration independently of block averaging.
5. Binary-To-Grayscale Consistency¶
Generate a grayscale image directly from a binary image using exactly the two calibration endpoint intensities and no blur or noise:
If background_porosity=0, then:
If background_porosity > 0, then:
This is the cleanest synthetic test connecting the binary and grayscale workflows.
6. HDF5 Round Trip¶
After export and import, verify:
and check that cell_size, origin, units, and calibration metadata are
unchanged.
This verifies the interchange format, not the physical calibration.
7. Kozeny-Carman Closure Checks¶
For the permeability closure, verify the exact interior formula:
and the reciprocal relation:
Also verify endpoint behavior:
If finite caps are supplied, verify that the cap is the only reason the output differs from the mathematical closure.
8. PoreSpy Blob Sanity Check¶
For a PoreSpy blobs realization with target porosity \(\phi_t\), check:
then check conservation:
This verifies that the workflow preserves the realized synthetic image porosity. It does not prove that the morphology is realistic for a particular rock.
What This Does Not Validate¶
These calculations validate the mechanics of local porosity mapping. They do not, by themselves, validate:
- scanner grayscale calibration,
- beam-hardening correction,
- mineral-density interpretation,
- unresolved microporosity,
- permeability closure,
- connectivity or flow behavior,
- or agreement with laboratory porosity.
For real datasets, a defensible validation sequence should include:
- record voxel size, image crop, support mask, and preprocessing,
- document
solid_gray,pore_gray, andbackground_porosity, - compare mean porosity against laboratory porosity or a trusted image-derived reference,
- inspect slices of the grayscale image, binary segmentation, and porosity map,
- test sensitivity to the calibration endpoints and block size,
- only then export the field to an external continuum solver.
Relation To Published Micro-Continuum Models¶
The voids porosity-map representation is compatible with the micro-continuum
porosity fields described by Soulaine and Tchelepi (2016) and Soulaine et al.
(2016), but it does not implement their Darcy-Brinkman or
Darcy-Brinkman-Stokes solver.
In Soulaine and Tchelepi (2016) and Soulaine et al. (2016), the central image-derived field is a local void fraction:
and, specifically in the sub-resolution porosity formulation of Soulaine et al. (2016),
Cells with \(\epsilon_f=1\) represent fully resolved free-flow regions, cells with \(\epsilon_f=0\) represent solid or no-flow regions, and intermediate values represent porous control volumes whose sub-cell pore structure is not resolved. The flow model then uses a single-domain Darcy-Brinkman-type equation, with a permeability or inverse-permeability field coupled to porosity.
The voids porosity map stores the same type of mathematical object:
a cell-average void fraction on a regular grid.
Therefore, a PorosityMap.values array can be interpreted as a candidate
\(\epsilon_f\) field for a micro-continuum solver if the control-volume size,
axis order, and physical units are aligned with that solver.
The important difference is what voids currently does with the field.
| Aspect | voids porosity map |
Published micro-continuum model |
|---|---|---|
| Main field | Cell-average porosity \(\phi\) | Void fraction \(\epsilon_f\) or microporosity \(\epsilon_{\mathrm{micro}}\) |
| Binary image route | Block-average of a resolved void indicator | Compatible with a filtered control-volume porosity |
| Grayscale route | Linear grayscale-to-porosity calibration, then block average | Compatible only if calibration matches the image-processing assumptions |
| Flow equations | Not solved by this feature | Darcy-Brinkman or Darcy-Brinkman-Stokes single-domain solve |
| Permeability closure | Optional Kozeny-Carman \(k(\phi)\) and \(k^{-1}(\phi)\) field generation | Required, commonly specified as a porosity-dependent \(k=k(\epsilon)\) relation |
| Free-flow handling | Stored only as \(\phi=1\) cells | Solver switches toward Stokes/Navier-Stokes behavior |
| Porous/no-flow handling | Stored only as \(\phi=0\) or \(0<\phi<1\) cells | Solver gives high resistance or no flow depending on closure |
Same Interpretation When block_shape=(1, 1, 1)¶
If the input image voxel is the intended micro-continuum control volume, then:
produces one porosity-map cell per image voxel. For a binary image, each value is either 0 or 1. For a calibrated grayscale image, each value can be intermediate and may be used as a sub-resolution porosity estimate.
This is closest to the voxel-wise porosity-field interpretation used in the sub-resolution porosity formulation of Soulaine et al. (2016).
Filtered Interpretation When block_shape > 1¶
When block_shape is larger than one, voids is not merely copying voxel labels.
It is applying a spatial filter:
This is compatible with the general micro-continuum idea of averaging below a chosen cutoff length. However, the physical interpretation changes:
- intermediate \(\phi\) values may represent resolved pores and grains mixed by coarse graining,
- not necessarily true sub-resolution microporosity inside each original voxel,
- so the permeability closure should be chosen for the chosen filter scale, not blindly copied from a voxel-scale model.
Remaining Solver-Input Requirements¶
To use these compatible porosity and permeability fields as solver inputs for the Darcy-Brinkman and Darcy-Brinkman-Stokes micro-continuum formulations described by Soulaine and Tchelepi (2016) and Soulaine et al. (2016), the next pieces are:
- optional lower/upper clamps for \(\phi=0\) and \(\phi=1\) depending on the external solver formulation,
- explicit cell ordering and axis convention for the target solver,
- solver-specific export, for example OpenFOAM scalar fields for porosity and permeability,
- validation against known synthetic cases before real micro-CT data.
So the safest statement is:
Summary
voids now computes a porosity field that is mathematically compatible with
the \(\epsilon_f\) field used in published micro-continuum models, and
it can derive an associated Kozeny-Carman permeability or inverse-permeability
field.
It does not implement the associated Darcy-Brinkman-Stokes equations or a
solver-specific field export.
References¶
- Ferreira, L. P., Surmas, R., Tonietto, S. N., Silva, M. A. P., PeƧanha, R. P. (2020). Modeling reactive flow on carbonates with realistic porosity and permeability fields. Advances in Water Resources, 139, 103564. https://doi.org/10.1016/j.advwatres.2020.103564
- Kozeny, J. (1927). Uber kapillare Leitung des Wassers im Boden. Sitzungsberichte der Akademie der Wissenschaften in Wien, Mathematisch-Naturwissenschaftliche Klasse, 136(2a), 271-306. https://www.zobodat.at/publikation_articles.php?id=185297
- Carman, P. C. (1937). Fluid flow through granular beds. Transactions of the Institution of Chemical Engineers, 15, 150-166. Reprinted in Chemical Engineering Research and Design, 75, S32-S48. https://doi.org/10.1016/S0263-8762(97)80003-2
- Soulaine, C., Tchelepi, H. A. (2016). Micro-continuum Approach for Pore-Scale Simulation of Subsurface Processes. Transport in Porous Media. https://doi.org/10.1007/s11242-016-0701-3
- Soulaine, C., Gjetvaj, F., Garing, C., Roman, S., Russian, A., Gouze, P., Tchelepi, H. A. (2016). The Impact of Sub-Resolution Porosity of X-ray Microtomography Images on the Permeability. Transport in Porous Media, 113(1). https://doi.org/10.1007/s11242-016-0690-2