integrated finger process pipeline on remapped 2d image
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user