Image Segmentation And Network Extraction¶
This page documents the current image-to-network machinery in voids. It is
intended to make the scientific assumptions visible: how a grayscale image
becomes a binary void/solid image, how a segmented image becomes a pore network,
which geometric quantities are assigned, and which backend choices are available.
voids does not try to hide image processing, extraction, and transport behind a
single opaque button. The recommended workflow is explicit:
- preprocess and segment the image into phases
- verify void connectivity and sample geometry
- extract a full pore network with a chosen backend
- normalize geometry, labels, and provenance
- prune to an axis-spanning subnetwork
- solve flow or run other graph/geometry diagnostics

The figure is schematic: real 3-D networks are more complex, but the reduction steps are the same. The important point is that each stage changes the scientific object being represented.
Phase Convention¶
All image-extraction paths expect a 2-D or 3-D phase image. For the current single-phase workflows:
| Value | Meaning |
|---|---|
0 or False |
solid / inactive phase |
nonzero or True |
void / active phase |
Mathematically, the binary void indicator is
where \(\Omega_v\) is the void phase and \(\Omega_s\) is the solid matrix.
This convention is intentionally simple. If the original data are grayscale CT, SEM, or another intensity image, the threshold and cleanup choices should be recorded in provenance metadata because they influence porosity, connectivity, and permeability.
Segmentation Helpers In voids¶
The segmentation module provides basic, reproducible preprocessing helpers for common research workflows. These are not meant to replace a full image-analysis study for difficult scans, but they are useful for scripted benchmarks and controlled datasets.
Cylindrical-Specimen Crop¶
For a raw 3-D grayscale volume \(I(k,y,x)\), crop_nonzero_cylindrical_volume
builds a slice-wise specimen support mask from a background discriminator:
It then computes the common support over all slices,
and returns the largest axis-aligned rectangle fully contained in \(C\). This is useful for cylindrical plugs because it avoids including the air outside the sample when later computing porosity or permeability.
Relevant functions:
largest_true_rectangle(mask2d)crop_nonzero_cylindrical_volume(raw, background_value=...)preprocess_grayscale_cylindrical_volume(raw, ...)
Threshold Binarization¶
binarize_grayscale_volume converts a cropped grayscale volume into an integer
binary phase image. If no threshold is supplied, the threshold is computed with
one of the supported scikit-image methods.
For dark voids,
For bright voids,
Supported automatic threshold methods are:
| Method | Typical use |
|---|---|
otsu |
robust first choice for bimodal histograms |
li |
entropy/minimum-cross-entropy thresholding |
yen |
often useful for high-contrast foregrounds |
isodata |
iterative intermeans thresholding |
triangle |
skewed histogram thresholding |
Relevant functions:
binarize_grayscale_volume(cropped, threshold=None, method="otsu", void_phase="dark")binarize_2d_with_voids(gray2d, threshold=None, method="otsu", void_phase="dark")preprocess_grayscale_cylindrical_volume(...)
Connectivity Screening¶
Before extracting a network or solving flow, check whether the void phase spans
the intended flow axis. voids uses face-connected components through
scipy.ndimage.label.
For a flow axis \(\alpha\), a connected void component \(C_m\) spans if
Relevant functions:
has_spanning_cluster(void_mask, axis_index)has_spanning_cluster_2d(void_mask, axis_index)
Segmentation is part of the model
A thresholded image is not a neutral input. Threshold method, phase polarity,
denoising, cropping, and connectivity cleanup can change the extracted
topology. Record those choices in Provenance.user_notes.
Minimal Segmentation Example¶
import numpy as np
from voids.image.segmentation import (
has_spanning_cluster,
preprocess_grayscale_cylindrical_volume,
)
raw = np.load("scan.npy")
seg = preprocess_grayscale_cylindrical_volume(
raw,
background_value=0.0,
threshold_method="otsu",
void_phase="dark",
)
phases = seg.binary
spans_x = has_spanning_cluster(phases.astype(bool), axis_index=0)
print("threshold =", seg.threshold)
print("crop bounds =", seg.crop.crop_bounds_yx)
print("void fraction =", phases.mean())
print("spans x =", spans_x)
For already segmented data, skip this step and pass the binary/integer image
directly to construct_spanning_network or extract_spanning_pore_network.
Sample Geometry¶
Network extraction also needs a voxel size. For an image with shape
\((n_x,n_y,n_z)\) and voxel edge length \(\Delta x\), voids assigns
and directional cross-sections
In code, this is handled by infer_sample_axes and stored in
SampleGeometry. The selected flow axis defaults to the longest image axis when
flow_axis is omitted.
Scale once
PoreSpy-style outputs are typically in voxel units before import. The
voids image workflow scales those outputs by voxel_size. Do not manually
scale the same network twice.
Network Extraction Entry Points¶
The main high-level function is construct_spanning_network. It returns a
NetworkConstructionResult with both the full network and the axis-spanning
subnetwork.
from voids.image import construct_spanning_network
result = construct_spanning_network(
backend="native_maximal_ball",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={
"distance_map_backend": "scipy",
"flow_boundary_mode": "external_reservoir",
},
provenance_notes={
"segmentation": "Otsu threshold, dark voids, cylindrical common crop",
},
)
net_full = result.net_full
net_spanning = result.net
Use extract_spanning_pore_network when you only need image extraction. Use
construct_spanning_network when you want a single interface that can also
construct imported reference networks in the same canonical schema.
Backend Overview¶
| Public backend | Normalized backend | Dependency | Main purpose |
|---|---|---|---|
porespy, snow2, porespy_snow2 |
porespy_snow2 |
PoreSpy | Standard PoreSpy snow2 extraction |
porespy_imperial, imperial_snow2, snow2_imperial |
porespy_snow2_imperial |
PoreSpy | snow2 with benchmark-tuned defaults for a more conservative reduction |
prego |
prego |
voids plus PoreSpy region geometry |
PREGO-style seed-based pore-region growing |
native_maximal_ball, maximal_ball, maxball |
native_maximal_ball |
NumPy/SciPy, optional edt |
Native dependency-light maximal-ball extraction |
PoreSpy snow2¶
The PoreSpy backend calls porespy.networks.snow2, normalizes the result into a
PoreSpy/OpenPNM-style dictionary, scales geometric fields to physical units, and
imports the dictionary into the canonical voids.Network schema.
PoreSpy extraction options are forwarded as extraction_kwargs:
result = construct_spanning_network(
backend="porespy",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={"sigma": 0.4, "r_max": 5, "boundary_width": 3},
)
PoreSpy Imperial Approximation¶
backend="porespy_imperial" still uses PoreSpy snow2, but starts with
defaults selected during the external-reference benchmark investigation:
| Option | Default |
|---|---|
sigma |
1.0 |
r_max |
4 |
boundary_width |
1 |
User-supplied extraction_kwargs override these values. This backend is a
practical approximation mode, not an exact replica of any external extractor.
PREGO¶
backend="prego" implements a PREGO-style seeded region-growing segmentation
based on Khan and Gostick's 2024 pore-region growing paper. It uses local
distance-transform maxima to choose seed points, orders those seeds by
descending distance-transform radius, grows pore regions with a configurable
FIFO strategy, and fills the remaining foreground voxels with a face-connected
queue. The resulting region image is passed to PoreSpy's regions_to_network,
then imported into the canonical voids.Network schema.
The paper leaves some floating-point tie cases and exact queue-level behavior
underspecified, so this backend is a transparent native implementation rather
than a bitwise reproduction of the authors' code. Treat permeability,
coordination-number, and throat-area differences relative to snow2 as
scientific outputs to validate, not just implementation noise.
result = construct_spanning_network(
backend="prego",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={
"settings": {
"r_max": 4,
"sigma": 0.4,
"peak_footprint": "sphere",
"growth_mode": "level_queue",
}
},
)
The default peak_footprint="sphere" uses PoreSpy's SNOW-style spherical peak
search. Set peak_footprint="cube" for a faster cubic local-maximum filter
when runtime is more important than seed-search fidelity.
The default growth_mode="level_queue" uses delayed seed activation and
level-by-level FIFO growth. Set growth_mode="fast" to use the faster
approximation that stamps non-overlapping seed spheres in descending radius order
before the final FIFO fill. The level-queue mode is the more direct algorithmic
path, but it is not claimed to be a bitwise reproduction of any external
implementation.
The internal PREGO region-label and FIFO queue arrays use the smallest signed
integer type that is safe for the image dimensions and number of seeds; for the
256^3 blob benchmark this is int16, but larger or more heavily seeded images
automatically widen before integer overflow is possible.
PoreSpy's regions_to_network output commonly contains pore/throat diameters,
throat.direct_length, and throat.total_length, but not explicit
throat.conduit_lengths.* arrays. During import, voids preserves explicit
conduit lengths when present and otherwise derives a sphere-cylinder
pore1-core-pore2 split from the available diameters and direct length. The
derivation metadata is stored in net.extra["conduit_lengths"], so PREGO
networks can use the hagen_poiseuille conduit model instead of silently
collapsing to a one-throat model whenever that geometry is available.
Native Maximal-Ball¶
The native maximal-ball backend is implemented inside voids. It is intended
for transparent, dependency-light extraction and staged verification against the
classical maximal-ball literature.

The current native implementation includes:
- Euclidean void-distance field
- boundary clipping heuristic
- maximal-ball candidate selection and overlap suppression
- parent-child ball hierarchy
- root-region voxel assignment and boundary cleanup
- region adjacency and interface face counting
- pore/throat geometry assembly
- optional external-reservoir helper pores on the flow axis
- shape-factor repair and pore shape-factor reconstruction
It is not yet exact external-reference parity. The remaining benchmark mismatch is concentrated in throat cross-section, shape-factor, and throat-surface ball details. See the External Reference CNM Benchmark for the current evidence.
Native Maximal-Ball Equations¶
Distance And Radius Fields¶
For each void voxel center \(\mathbf{x}\), the Euclidean distance transform is
The default maximal-ball radius convention subtracts half a voxel:
This is controlled by MaximalBallSettings.radius_field_mode:
| Mode | Meaning |
|---|---|
half_voxel |
use \(d - 0.5\), default |
edt |
use the plain Euclidean distance map |
The distance transform backend is selected with distance_map_backend:
| Backend | Meaning |
|---|---|
auto |
prefer optional edt for 3-D if installed, otherwise SciPy |
scipy |
use scipy.ndimage.distance_transform_edt |
edt |
require the optional edt package |
Candidate Selection¶
Candidate balls are selected from the radius field, then sorted by decreasing
radius. The user-facing selector is
MaximalBallSettings.candidate_selection_mode:
| Mode | Meaning |
|---|---|
threshold_all |
retain all voxels above the resolved radius threshold before suppression |
local_maxima |
start from local radius maxima before suppression |
The resolved minimal radius defaults to an Imperial-style rule based on the average positive distance-map value:
unless minimal_pore_radius_voxels is explicitly supplied.
Hierarchy And Region Assignment¶
Retained balls are organized into a hierarchy using distance and radius criteria. At a high level, a ball \(b_i=(\mathbf{x}_i,r_i)\) can be assigned to a larger parent \(b_j=(\mathbf{x}_j,r_j)\) when the centers are close enough relative to the radii and the hierarchy factors. Root balls become pore seeds.
Voxel regions are then grown from those roots. The output is a label image \(\ell(\mathbf{x})\), where each assigned void voxel belongs to a pore region:
Region adjacency is obtained by scanning neighboring voxel faces. If two neighboring voxels belong to different pore labels, their shared face contributes to a throat candidate.
Throat Area And Shape Factor¶
The default throat cross-sectional area is based on interface face count:
With throat_area_mode="vector_magnitude", voids instead uses the magnitude
of the oriented face-balance vector:
The hydraulic shape factor is the usual dimensionless quantity
When a throat has area and an inscribed radius surrogate, the Imperial-style export repair uses
The radius used in that expression is controlled by
throat_shape_factor_radius_mode:
| Mode | Meaning |
|---|---|
inscribed |
use the exported throat inscribed radius, default |
surface_ball |
use the interface-supporting surface-ball radius for the shape-factor calculation |
Conduit Lengths¶
For each throat, voids computes a pore1-core-pore2 conduit. The current
maximal-ball default anchors the conduit at the ordered pair's second
interface-supporting ball:
The pore-to-anchor distances are
The segment lengths are regularized to avoid zero-resistance artifacts:
with an additional minimum total-length safeguard. The anchor convention is
controlled by throat_anchor_mode:
| Mode | Meaning |
|---|---|
second_side |
use the ordered pair's second interface-supporting ball, default |
largest_support |
use the side with the larger supporting radius |
Boundary Treatment¶
Boundary labels drive pressure boundary conditions and spanning-subnetwork selection. The canonical Cartesian labels are:
| Axis | Lower label | Upper label |
|---|---|---|
x |
inlet_xmin |
outlet_xmax |
y |
inlet_ymin |
outlet_ymax |
z |
inlet_zmin |
outlet_zmax |
For PoreSpy-style networks, ensure_cartesian_boundary_labels mirrors common
aliases and geometric boundary labels to this convention.
For native maximal-ball, PoreSpy snow2, and PREGO extraction,
flow_boundary_mode controls how the flow axis is represented:
| Mode | Meaning |
|---|---|
direct |
label physical pores that touch the boundary |
external_reservoir |
add zero-volume helper pores outside/at the boundary and connect them with boundary throats |
external_reservoir is often preferable for permeability benchmarks because it
avoids imposing pressure directly at internal pore centers. For PoreSpy-style
backends this is a post-import network augmentation: voids adds zero-volume
helper pores at the selected Cartesian boundary and connects them to the
boundary-touching physical pores. This mode requires geometric conductance
models; it intentionally refuses networks that already contain a precomputed
throat.hydraulic_conductance, since a new boundary throat cannot be assigned a
consistent precomputed value without re-solving the reference model.
Pyramids-And-Cuboids Transport Geometry¶
PoreSpy-style extraction backends also accept
transport_geometry="pyramids_and_cuboids". This does not alter the
segmentation or the region-growing labels. It attaches OpenPNM-style hydraulic
size factors to the imported Network using truncated-pyramid pore segments and
cuboid throat segments.
The generated size factors are stored in net.throat["hydraulic_size_factors"]
so they serialize with the rest of the throat geometry. The
conductance_model="auto" single-phase solve will use them ahead of local
fallbacks such as hagen_poiseuille or valvatne_blunt. The option depends on
the imported pore1-core-pore2 conduit lengths and on pore/throat size
surrogates; it is therefore still a reduced hydraulic model, not a direct
voxel-scale Stokes solve.
A PREGO extraction with external-reservoir boundaries and pyramids-and-cuboids transport geometry looks like:
result = extract_spanning_pore_network(
phases,
voxel_size=2.25e-6,
backend="prego",
flow_axis="x",
extraction_kwargs={
"flow_boundary_mode": "external_reservoir",
"transport_geometry": "pyramids_and_cuboids",
"settings": {"r_max": 4, "sigma": 0.4},
"regions_to_network_kwargs": {"accuracy": "standard"},
},
)
Spanning Subnetwork¶
After full-network import, voids prunes to the components that span the chosen
axis. Given connected-component labels \(c_i\), the retained components satisfy
The result object stores both:
| Field | Meaning |
|---|---|
net_full |
full extracted/imported network |
net |
induced axis-spanning subnetwork |
pore_indices |
retained pore indices in net_full |
throat_mask |
retained throat mask in net_full |
This split is important for porosity interpretation: absolute porosity may use the full network, while effective transport should use the spanning network.
Main Configuration Reference¶
extract_spanning_pore_network¶
| Option | Default | Meaning |
|---|---|---|
backend |
porespy |
image extraction backend |
flow_axis |
longest axis | axis used for boundary labels and spanning pruning |
length_unit |
m |
stored length unit metadata |
pressure_unit |
Pa |
stored pressure unit metadata |
geometry_repairs |
imperial_export |
importer geometry repair mode for PoreSpy-style outputs |
repair_seed |
0 |
seed for stochastic repair fallback |
strict |
True |
require required topology fields during import |
extraction_kwargs |
None |
backend-specific controls |
PREGO extraction_kwargs¶
| Key | Default | Meaning |
|---|---|---|
settings |
None |
PregoSettings instance or mapping |
prego_settings |
None |
alias for settings |
distance_map |
None |
optional precomputed void-space distance transform |
peaks |
None |
optional precomputed seed markers |
regions_to_network_kwargs |
None |
options forwarded to PoreSpy regions_to_network |
PregoSettings¶
| Field | Default | Meaning |
|---|---|---|
r_max |
4 |
local-maximum filter radius for seed detection |
sigma |
0.4 |
Gaussian smoothing applied before seed detection |
peak_footprint |
sphere |
local-maximum filter shape for seed detection; use cube for a faster local approximation |
growth_mode |
level_queue |
delayed seed activation with level-by-level FIFO growth; use fast for stamped spheres plus FIFO fill |
distance_map_backend |
auto |
distance-transform implementation |
edt_parallel_threads |
None |
optional worker count for the edt backend |
cleanup_unassigned |
True |
leave unfilled foreground voxels as background labels |
Native Maximal-Ball extraction_kwargs¶
| Key | Default | Meaning |
|---|---|---|
distance_map_backend |
auto |
distance-transform implementation |
apply_boundary_clipping |
True |
apply Imperial-style distance clipping near boundaries |
flow_boundary_mode |
direct |
direct labels or external helper reservoirs |
boundary_axis |
flow_axis |
axis used by external-reservoir helpers |
boundary_length_epsilon |
1.0e-300 |
tiny reservoir-side conduit length |
boundary_radius_scale |
1.1 |
helper-pore radius multiplier |
throat_area_mode |
face_count |
throat area from face count or oriented vector magnitude |
throat_shape_factor_radius_mode |
inscribed |
radius used for throat shape-factor repair |
throat_anchor_mode |
second_side |
anchor convention for conduit lengths |
settings |
None |
MaximalBallSettings instance or mapping |
maximal_ball_settings |
None |
alias for settings |
MaximalBallSettings¶
| Field | Default | Meaning |
|---|---|---|
minimal_pore_radius_voxels |
resolved | smallest candidate pore radius |
clip_radius_fraction_streamwise |
0.05 |
boundary clipping along streamwise axis |
clip_radius_fraction_transverse |
0.98 |
boundary clipping along transverse axes |
medial_surface_mid_radius_fraction |
0.7 |
medial-surface support criterion |
medial_surface_noise_voxels |
resolved | noise scale for medial-surface logic |
hierarchy_length_factor |
0.6 |
parent-child distance factor |
hierarchy_radius_factor |
1.1 |
parent-child radius factor |
radius_smoothing_iterations |
3 |
local radius-field smoothing passes |
retention_radius_factor |
0.15 |
overlap-suppression radius factor |
retention_radius_offset_voxels |
resolved | overlap-suppression radius offset |
radius_field_mode |
half_voxel |
radius convention |
candidate_selection_mode |
threshold_all |
candidate selector |
Recommended Recipes¶
Standard PoreSpy Extraction¶
Use this when you want the established PoreSpy snow2 path.
result = construct_spanning_network(
backend="porespy",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={"sigma": 0.4, "r_max": 5},
)
Native Maximal-Ball Benchmark Mode¶
Use this when you want the current native maximal-ball path with boundary helper pores suitable for permeability benchmarks.
result = construct_spanning_network(
backend="native_maximal_ball",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={
"distance_map_backend": "scipy",
"flow_boundary_mode": "external_reservoir",
},
)
Controlled Geometry Experiment¶
Use this to test throat-area and shape-factor conventions explicitly.
result = construct_spanning_network(
backend="native_maximal_ball",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={
"flow_boundary_mode": "external_reservoir",
"throat_area_mode": "vector_magnitude",
"throat_shape_factor_radius_mode": "surface_ball",
"throat_anchor_mode": "second_side",
},
)
Do not treat a lower permeability error from one case as proof of a better general model. These options change coupled geometric assumptions and should be validated across cases.
Provenance Checklist¶
For publishable or benchmark-quality image workflows, record at least:
| Item | Example |
|---|---|
| raw image source | dataset id, scan id, crop id |
| voxel size | 2.0e-6 m |
| threshold rule | otsu, explicit threshold, or external segmentation method |
| phase polarity | dark voids or bright voids |
| cleanup | opening/closing, connected-component filtering, manual edits |
| extraction backend | porespy, prego, native_maximal_ball, etc. |
| backend options | sigma, r_max, PREGO or maximal-ball settings |
| boundary treatment | direct labels or external reservoir helpers |
| spanning axis | x, y, or z |
| geometry repairs | imperial_export or none |
Use provenance_notes for this metadata:
result = construct_spanning_network(
backend="native_maximal_ball",
phases=phases,
voxel_size=2.0e-6,
flow_axis="x",
extraction_kwargs={"flow_boundary_mode": "external_reservoir"},
provenance_notes={
"segmentation_threshold": "otsu",
"void_phase": "dark",
"cleanup": "none",
"operator": "scripted benchmark",
},
)
Known Limitations¶
- Basic threshold segmentation is available, but advanced grayscale/ML/manual segmentation remains an upstream scientific preprocessing task.
- The native maximal-ball backend is not yet exact external-reference parity.
- PREGO's default seed search and growth path now favor the level-queue
algorithm over the older fast approximation, but the backend still relies on PoreSpy's
regions_to_networkgeometry reduction andvoidspost-import conduit/boundary transport geometry downstream. - Pore and throat geometry are model reductions, not direct measurements of all voxel-scale surface detail.
- Boundary labels are inferred from Cartesian assumptions unless supplied by the backend or imported network.
- Very thin topological connections can pass a spanning check while being physically questionable for continuum-scale permeability.
For the current single-phase verification status, see:
- External Reference CNM Benchmark
- OpenPNM Extracted-Network Cross-Check
- XLB Direct-Image Permeability Benchmark
- PREGO Synthetic Blob Backend Comparison
References¶
The page above mixes two different sources of methodology:
- basic grayscale preprocessing and thresholding utilities implemented through standard scientific-Python image-processing routines,
- and maximal-ball plus extracted-network ideas from the broader pore-network modeling literature.
The most relevant references for the current voids image-to-network workflow are:
- Khan, Z. A., and J. T. Gostick (2024). Enhancing pore network extraction performance via seed-based pore region growing segmentation. Advances in Water Resources, 183, 104591. https://doi.org/10.1016/j.advwatres.2023.104591. This is the PREGO reference behind the native seed-based region-growing backend.
- Dong, H., and M. J. Blunt (2009). Pore-network extraction from micro-computerized-tomography images. Physical Review E, 80, 036307. This is the key maximal-ball extraction reference behind the native maximal-ball backend.
- Raeini, A. Q., B. Bijeljic, and M. J. Blunt (2017). Generalized network modeling: Network extraction as a coarse-scale discretization of the void space of porous media. Physical Review E, 96, 013312. This is the main extraction-as-discretization reference and is the closest conceptual reference for the reduction viewpoint adopted in this page.
- Bultreys, T., Q. Lin, Y. Gao, A. Q. Raeini, A. AlRatrout, B. Bijeljic, and M. J. Blunt (2018). Validation of model predictions of pore-scale fluid distributions during two-phase flow. Physical Review E, 97, 053104. This is relevant because it documents validation of this broader extraction and pore-scale modeling lineage.
- Al-Kharusi, A. S., and M. J. Blunt (2008). Multiphase flow predictions from carbonate pore space images using extracted network models. Water Resources Research, 44, W06S01. This is a useful image-to-network workflow reference showing the broader extraction pipeline from image to transport prediction.
- Valvatne, P. H., and M. J. Blunt (2004). Predictive pore-scale modeling of two-phase flow in mixed wet media. Water Resources Research, 40(7). This is primarily a flow-model reference rather than a segmentation reference, but it is still relevant here because the extracted geometry is ultimately consumed by the same conduit-style pore-network modeling tradition.
- Valvatne, P. H. (2004). Predictive pore-scale modelling of multiphase flow. PhD thesis. This thesis provides additional background on the pore-network geometry and flow-model context used by conduit-based network models.
- Blunt, M. J., M. D. Jackson, M. Piri, and P. H. Valvatne (2002). Detailed physics, predictive capabilities and macroscopic consequences for pore-network models of multiphase flow. Advances in Water Resources, 25, 1069-1089. This is broader background for why extracted pore-throat networks are used as reduced models of the void space.
- Blunt, M. J., et al. (2013). Pore-scale imaging and modelling. Advances in Water Resources, 51, 197-216. This is a broader review placing segmented imaging, network extraction, and direct-image simulation in the same pore-scale modeling landscape.