integrated finger process pipeline on remapped 2d image

This commit is contained in:
ykcheungab
2026-06-14 17:48:30 +08:00
parent 667bdf1961
commit 30bebcd439
36 changed files with 3593 additions and 2043 deletions
+213
View File
@@ -0,0 +1,213 @@
import numpy
from scipy import sparse
from scipy.sparse import linalg as sparse_linalg
from .config import MaskedPoissonConfig
def normalize_mask(mask, shape):
"""Normalize optional mask to bool array matching grid shape."""
if mask is None:
return None
mask_arr = numpy.asarray(mask, dtype=bool)
if mask_arr.shape != shape:
raise ValueError("Mask dimensions should match the Poisson grid")
return mask_arr
def compute_boundary_mask(active):
"""Return active pixels with at least one inactive 4-neighbor."""
up = numpy.zeros_like(active)
down = numpy.zeros_like(active)
left = numpy.zeros_like(active)
right = numpy.zeros_like(active)
up[1:, :] = active[:-1, :]
down[:-1, :] = active[1:, :]
left[:, 1:] = active[:, :-1]
right[:, :-1] = active[:, 1:]
fully_surrounded = up & down & left & right
return active & ~fully_surrounded
def sample_boundary_coords(boundary_coords, config: MaskedPoissonConfig):
"""Subsample boundary pixel coordinates for capacitance correction."""
if boundary_coords.size == 0:
return boundary_coords
sampled = boundary_coords
if config.boundary_stride > 1:
sampled = sampled[:: config.boundary_stride]
if config.max_boundary > 0 and len(sampled) > config.max_boundary:
picks = numpy.linspace(0, len(sampled) - 1, num=config.max_boundary, dtype=numpy.int32)
sampled = sampled[picks]
return numpy.ascontiguousarray(sampled, dtype=numpy.int32)
class MaskedDomain:
"""Reduced Laplacian system on active mask pixels (2D only)."""
__slots__ = (
"shape",
"mask",
"index_map",
"factor",
"lap",
"num_active",
"pinned",
"active_coords",
)
def __init__(
self,
shape,
mask,
index_map,
factor,
lap,
num_active,
pinned,
active_coords,
):
self.shape = shape
self.mask = mask
self.index_map = index_map
self.factor = factor
self.lap = lap
self.num_active = num_active
self.pinned = pinned
self.active_coords = active_coords
@classmethod
def build(cls, mask, spacings):
"""Build masked Laplacian domain from bool mask and grid spacings (dy, dx).
mask: (H, W) bool; True = active domain pixels
spacings: (dy, dx) grid spacing tuple
returns: MaskedDomain with factorized sparse Laplacian
"""
active = numpy.asarray(mask, dtype=bool)
h, w = active.shape
active_coords = numpy.argwhere(active)
num_active = int(active.sum())
if num_active == 0:
return cls(
shape=active.shape,
mask=active.copy(),
index_map=numpy.full(active.shape, -1, dtype=numpy.int32),
factor=None,
lap=None,
num_active=0,
pinned=0,
active_coords=active_coords,
)
index_map = numpy.full(active.shape, -1, dtype=numpy.int32)
index_map[active] = numpy.arange(num_active, dtype=numpy.int32)
dy, dx = float(spacings[0]), float(spacings[1])
coeff_y = 1.0 / (dy * dy)
coeff_x = 1.0 / (dx * dx)
rows = []
cols = []
data = []
pinned = 0
for row, col in active_coords:
idx = int(index_map[row, col])
if idx == pinned:
rows.append(idx)
cols.append(idx)
data.append(1.0)
continue
diag = 0.0
for drow, dcol, coeff in ((-1, 0, coeff_y), (1, 0, coeff_y), (0, -1, coeff_x), (0, 1, coeff_x)):
nrow = row + drow
ncol = col + dcol
if nrow < 0 or nrow >= h or ncol < 0 or ncol >= w or not active[nrow, ncol]:
continue
rows.append(idx)
cols.append(int(index_map[nrow, ncol]))
data.append(-coeff)
diag += coeff
if diag == 0.0:
rows.append(idx)
cols.append(idx)
data.append(1.0)
else:
rows.append(idx)
cols.append(idx)
data.append(diag)
lap = sparse.csc_matrix((data, (rows, cols)), shape=(num_active, num_active), dtype=numpy.float64)
factor = sparse_linalg.factorized(lap)
return cls(
shape=active.shape,
mask=active.copy(),
index_map=index_map,
factor=factor,
lap=lap,
num_active=num_active,
pinned=pinned,
active_coords=active_coords,
)
def matches(self, mask):
"""True if this domain was built for the same mask array."""
mask_arr = numpy.asarray(mask, dtype=bool)
return self.shape == mask_arr.shape and numpy.array_equal(self.mask, mask_arr)
def prepare_rhs(self, f):
"""Extract gauge-fixed RHS on active pixels.
f: (H, W) scalar source field
returns: (N,) RHS on active pixels; empty if no active pixels
"""
rhs = numpy.asarray(f, dtype=numpy.float64)[self.mask]
if rhs.size == 0:
return rhs
rhs -= rhs.mean()
rhs[self.pinned] = 0.0
return rhs
def scatter(self, sol_active, template):
"""Map reduced solution back to full grid.
sol_active: (N,) potential on active pixels
template: (H, W) array used for output shape/dtype
returns: (H, W) potential; zero outside mask
"""
out = numpy.zeros_like(template, dtype=numpy.float32)
if sol_active.size:
out[self.mask] = numpy.asarray(sol_active, dtype=numpy.float32)
return out
def residual(self, field, rhs_active):
"""Compute masked Laplacian residual L u - rhs on active pixels.
field: (H, W) full-grid field
rhs_active: (N,) RHS on active pixels (or None for homogeneous)
returns: (N,) residual vector
"""
vec = numpy.asarray(field, dtype=numpy.float64)[self.mask]
residual = self.lap.dot(vec)
if rhs_active is not None:
residual = residual - rhs_active
return residual
def to_cache_dict(self, capfft=None):
"""Legacy dict view for tests and facade compatibility."""
return {
"shape": self.shape,
"mask": self.mask,
"index_map": self.index_map,
"factor": self.factor,
"lap": self.lap,
"num_active": self.num_active,
"pinned": self.pinned,
"active_coords": self.active_coords,
"capfft": capfft,
}