Tune phase-field evidence and spacing
This commit is contained in:
parent
272bc4ce1b
commit
d9fa411daf
3 changed files with 80 additions and 16 deletions
|
|
@ -242,6 +242,19 @@ Important metric correction:
|
|||
- exact-match ratio
|
||||
- on the pinned badge comparison under `artifacts/phase-field-metric-check/`, that new metric finally agrees with human judgment: `phase-field` scores `0.4244` versus continuous `0.4291` where lower is better
|
||||
|
||||
First focused blemish pass:
|
||||
|
||||
- the tracked sword-tip focus fixture now lives in `tests/fixtures/real/ai-badge-tip-focus.json`
|
||||
- the next phase-field pass tried exactly the three obvious fixes:
|
||||
- stronger local evidence
|
||||
- less timid edge-aware smoothness gating
|
||||
- a better spacing story with both preferred spacing and an upper spacing clamp
|
||||
- the resulting pinned badge run is under `artifacts/phase-field-v2-badge-126/`
|
||||
- this did move the specific blemish a little in the right direction, but only a little:
|
||||
- on the tip focus crop, `source_structure` improved from `0.4187` to `0.4179`
|
||||
- on the whole image, `source_structure` regressed slightly from `0.4244` to `0.4248`
|
||||
- honest read: the pass shaved the targeted contour swelling a bit, but not enough to call the issue solved, and it did so by spending a little of the whole-image win
|
||||
|
||||
## High-value regression case
|
||||
|
||||
### AI badge emblem with baked checkerboard background
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ from dataclasses import asdict, dataclass
|
|||
@dataclass(slots=True)
|
||||
class SolverHyperParams:
|
||||
phase_field_patch_extent: float = 0.18
|
||||
phase_field_data_coherence_weight: float = 1.00
|
||||
phase_field_data_edge_weight: float = 0.35
|
||||
phase_field_smoothness_weight: float = 0.28
|
||||
phase_field_edge_gate_strength: float = 6.0
|
||||
phase_field_data_coherence_weight: float = 1.10
|
||||
phase_field_data_edge_weight: float = 0.55
|
||||
phase_field_data_center_edge_weight: float = 0.90
|
||||
phase_field_spacing_weight: float = 0.24
|
||||
phase_field_smoothness_weight: float = 0.26
|
||||
phase_field_edge_gate_strength: float = 11.0
|
||||
phase_field_collapse_weight: float = 1.20
|
||||
phase_field_min_spacing_ratio: float = 0.18
|
||||
phase_field_min_spacing_ratio: float = 0.10
|
||||
phase_field_max_spacing_ratio: float = 1.28
|
||||
phase_field_magnitude_weight: float = 0.08
|
||||
phase_field_learning_rate: float = 0.10
|
||||
phase_field_max_displacement_ratio: float = 0.48
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ class _PhaseFieldPrep:
|
|||
height: int
|
||||
|
||||
|
||||
def _charbonnier(torch, value, epsilon: float = 1e-4):
|
||||
return torch.sqrt(value.square() + epsilon)
|
||||
|
||||
|
||||
def _require_torch():
|
||||
try:
|
||||
import torch
|
||||
|
|
@ -119,6 +123,11 @@ def _sample_patch_scalar(F, scalar_t, coords_px, offsets_t, *, width: int, heigh
|
|||
return sampled.reshape(batch, out_h, out_w, samples)
|
||||
|
||||
|
||||
def _spacing_loss(torch, step, target_step: float):
|
||||
normalized = step / max(target_step, 1e-4)
|
||||
return _charbonnier(torch, normalized - 1.0).mean()
|
||||
|
||||
|
||||
def _displacement_diagnostics(uv_field: np.ndarray, selected_x, selected_y, *, width: int, height: int) -> dict[str, np.ndarray | float]:
|
||||
uv_x = (uv_field[..., 0] + 1.0) * 0.5 * max(1.0, float(width - 1))
|
||||
uv_y = (uv_field[..., 1] + 1.0) * 0.5 * max(1.0, float(height - 1))
|
||||
|
|
@ -166,19 +175,41 @@ def _displacement_diagnostics(uv_field: np.ndarray, selected_x, selected_y, *, w
|
|||
}
|
||||
|
||||
|
||||
def _project_displacements_in_place(torch, disp_t, base_x_t, base_y_t, *, min_dx: float, min_dy: float, max_dx: float, max_dy: float, width: int, height: int) -> None:
|
||||
def _project_displacements_in_place(
|
||||
torch,
|
||||
disp_t,
|
||||
base_x_t,
|
||||
base_y_t,
|
||||
*,
|
||||
min_dx: float,
|
||||
min_dy: float,
|
||||
max_step_x: float,
|
||||
max_step_y: float,
|
||||
max_dx: float,
|
||||
max_dy: float,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> None:
|
||||
with torch.no_grad():
|
||||
pos_x = (base_x_t + disp_t[..., 0]).clamp(0.0, max(0.0, float(width - 1)))
|
||||
pos_y = (base_y_t + disp_t[..., 1]).clamp(0.0, max(0.0, float(height - 1)))
|
||||
|
||||
for index in range(1, pos_x.shape[2]):
|
||||
pos_x[:, :, index] = torch.maximum(pos_x[:, :, index], pos_x[:, :, index - 1] + min_dx)
|
||||
lower = pos_x[:, :, index - 1] + min_dx
|
||||
upper = pos_x[:, :, index - 1] + max_step_x
|
||||
pos_x[:, :, index] = torch.minimum(torch.maximum(pos_x[:, :, index], lower), upper)
|
||||
for index in range(pos_x.shape[2] - 2, -1, -1):
|
||||
pos_x[:, :, index] = torch.minimum(pos_x[:, :, index], pos_x[:, :, index + 1] - min_dx)
|
||||
lower = pos_x[:, :, index + 1] - max_step_x
|
||||
upper = pos_x[:, :, index + 1] - min_dx
|
||||
pos_x[:, :, index] = torch.maximum(torch.minimum(pos_x[:, :, index], upper), lower)
|
||||
for index in range(1, pos_y.shape[1]):
|
||||
pos_y[:, index, :] = torch.maximum(pos_y[:, index, :], pos_y[:, index - 1, :] + min_dy)
|
||||
lower = pos_y[:, index - 1, :] + min_dy
|
||||
upper = pos_y[:, index - 1, :] + max_step_y
|
||||
pos_y[:, index, :] = torch.minimum(torch.maximum(pos_y[:, index, :], lower), upper)
|
||||
for index in range(pos_y.shape[1] - 2, -1, -1):
|
||||
pos_y[:, index, :] = torch.minimum(pos_y[:, index, :], pos_y[:, index + 1, :] - min_dy)
|
||||
lower = pos_y[:, index + 1, :] - max_step_y
|
||||
upper = pos_y[:, index + 1, :] - min_dy
|
||||
pos_y[:, index, :] = torch.maximum(torch.minimum(pos_y[:, index, :], upper), lower)
|
||||
|
||||
pos_x = pos_x.clamp(0.0, max(0.0, float(width - 1)))
|
||||
pos_y = pos_y.clamp(0.0, max(0.0, float(height - 1)))
|
||||
|
|
@ -275,8 +306,16 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
|
||||
center_rgba = patch_rgba[..., 0, :]
|
||||
neighbor_rgba = patch_rgba[..., 1:, :]
|
||||
local_coherence = (neighbor_rgba - center_rgba[..., None, :]).abs().mean()
|
||||
local_edge = patch_edge.mean()
|
||||
neighbor_weights = patch_rgba.new_tensor([1.0, 1.0, 1.0, 1.0, 0.75, 0.75, 0.75, 0.75]).view(1, 1, 1, -1)
|
||||
color_delta = (neighbor_rgba - center_rgba[..., None, :]).abs().mean(dim=-1)
|
||||
local_coherence = (color_delta * neighbor_weights).sum() / neighbor_weights.sum().clamp_min(1e-6) / max(
|
||||
1.0, float(color_delta.shape[0] * color_delta.shape[1] * color_delta.shape[2])
|
||||
)
|
||||
center_edge = patch_edge[..., 0].mean()
|
||||
ring_edge = ((patch_edge[..., 1:] * neighbor_weights).sum() / neighbor_weights.sum().clamp_min(1e-6)) / max(
|
||||
1.0, float(patch_edge.shape[0] * patch_edge.shape[1] * patch_edge.shape[2])
|
||||
)
|
||||
local_edge = center_edge * solver_params.phase_field_data_center_edge_weight + ring_edge
|
||||
|
||||
disp_norm = disp_t.clone()
|
||||
disp_norm[..., 0] = disp_norm[..., 0] / max(prep.cell_x, 1e-4)
|
||||
|
|
@ -287,24 +326,27 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
smoothness = sampled_rgba.new_tensor(0.0)
|
||||
if pos_mid_x is not None:
|
||||
edge_x = _sample_scalar(F, prep.edge_t, pos_mid_x, width=prep.width, height=prep.height)
|
||||
weight_x = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_x)
|
||||
weight_x = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_x.clamp_min(0.0))
|
||||
delta_x = disp_norm[:, :, 1:, :] - disp_norm[:, :, :-1, :]
|
||||
smoothness = smoothness + (weight_x * torch.sqrt(delta_x.square().sum(dim=-1) + 1e-6)).mean()
|
||||
smoothness = smoothness + (weight_x * _charbonnier(torch, delta_x, epsilon=1e-4).mean(dim=-1)).mean()
|
||||
if pos_mid_y is not None:
|
||||
edge_y = _sample_scalar(F, prep.edge_t, pos_mid_y, width=prep.width, height=prep.height)
|
||||
weight_y = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_y)
|
||||
weight_y = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_y.clamp_min(0.0))
|
||||
delta_y = disp_norm[:, 1:, :, :] - disp_norm[:, :-1, :, :]
|
||||
smoothness = smoothness + (weight_y * torch.sqrt(delta_y.square().sum(dim=-1) + 1e-6)).mean()
|
||||
smoothness = smoothness + (weight_y * _charbonnier(torch, delta_y, epsilon=1e-4).mean(dim=-1)).mean()
|
||||
|
||||
collapse = sampled_rgba.new_tensor(0.0)
|
||||
spacing = sampled_rgba.new_tensor(0.0)
|
||||
min_dx = solver_params.phase_field_min_spacing_ratio * prep.cell_x
|
||||
min_dy = solver_params.phase_field_min_spacing_ratio * prep.cell_y
|
||||
if pos_px.shape[2] > 1:
|
||||
step_x = pos_px[:, :, 1:, 0] - pos_px[:, :, :-1, 0]
|
||||
collapse = collapse + torch.relu(min_dx - step_x).square().mean()
|
||||
spacing = spacing + _spacing_loss(torch, step_x, prep.cell_x)
|
||||
if pos_px.shape[1] > 1:
|
||||
step_y = pos_px[:, 1:, :, 1] - pos_px[:, :-1, :, 1]
|
||||
collapse = collapse + torch.relu(min_dy - step_y).square().mean()
|
||||
spacing = spacing + _spacing_loss(torch, step_y, prep.cell_y)
|
||||
|
||||
magnitude = (
|
||||
(disp_t[..., 0] / max(prep.cell_x, 1e-4)).square()
|
||||
|
|
@ -315,6 +357,7 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
local_coherence * solver_params.phase_field_data_coherence_weight
|
||||
+ local_edge * solver_params.phase_field_data_edge_weight
|
||||
+ smoothness * solver_params.phase_field_smoothness_weight
|
||||
+ spacing * solver_params.phase_field_spacing_weight
|
||||
+ collapse * solver_params.phase_field_collapse_weight
|
||||
+ magnitude * solver_params.phase_field_magnitude_weight
|
||||
)
|
||||
|
|
@ -322,6 +365,7 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
"local_coherence": float(local_coherence.detach().cpu().item()),
|
||||
"local_edge": float(local_edge.detach().cpu().item()),
|
||||
"smoothness": float(smoothness.detach().cpu().item()),
|
||||
"spacing": float(spacing.detach().cpu().item()),
|
||||
"collapse": float(collapse.detach().cpu().item()),
|
||||
"magnitude": float(magnitude.detach().cpu().item()),
|
||||
}
|
||||
|
|
@ -369,6 +413,8 @@ def optimize_phase_field(
|
|||
final_terms: dict[str, float] = {}
|
||||
min_dx = solver_params.phase_field_min_spacing_ratio * prep.cell_x
|
||||
min_dy = solver_params.phase_field_min_spacing_ratio * prep.cell_y
|
||||
max_step_x = solver_params.phase_field_max_spacing_ratio * prep.cell_x
|
||||
max_step_y = solver_params.phase_field_max_spacing_ratio * prep.cell_y
|
||||
max_dx = solver_params.phase_field_max_displacement_ratio * prep.cell_x
|
||||
max_dy = solver_params.phase_field_max_displacement_ratio * prep.cell_y
|
||||
|
||||
|
|
@ -384,6 +430,8 @@ def optimize_phase_field(
|
|||
prep.base_y_t,
|
||||
min_dx=min_dx,
|
||||
min_dy=min_dy,
|
||||
max_step_x=max_step_x,
|
||||
max_step_y=max_step_y,
|
||||
max_dx=max_dx,
|
||||
max_dy=max_dy,
|
||||
width=prep.width,
|
||||
|
|
|
|||
Loading…
Reference in a new issue