"""Train the DnCNN denoiser on synthetic clean/noisy patches (self-contained, CPU). Generates synthetic clean microscopy images, adds Poisson+Gaussian noise, extracts patches, and trains the net to predict the noise (residual learning). Saves weights. Usage: python -m scripts.train_denoiser """ from __future__ import annotations import sys from pathlib import Path import numpy as np from core import model from core.synth import add_noise, clean_image def _patches(n_images: int, patch: int, per_img: int, rng): X, Y = [], [] for i in range(n_images): clean = clean_image(seed=i) noisy = add_noise(clean, sigma=rng.uniform(0.08, 0.16), seed=1000 + i) H, W = clean.shape for _ in range(per_img): y0, x0 = rng.integers(0, H - patch), rng.integers(0, W - patch) c = clean[y0:y0 + patch, x0:x0 + patch] n = noisy[y0:y0 + patch, x0:x0 + patch] X.append(n) Y.append(n - c) # target = noise residual return np.stack(X)[:, None], np.stack(Y)[:, None] def main() -> int: out_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("dncnn.pt") n_images = int(sys.argv[2]) if len(sys.argv) > 2 else 24 epochs = int(sys.argv[3]) if len(sys.argv) > 3 else 15 import torch torch.manual_seed(0) rng = np.random.default_rng(0) X, Y = _patches(n_images, patch=64, per_img=40, rng=rng) print(f"patches: {len(X)}") Xt = torch.from_numpy(X.astype(np.float32)) Yt = torch.from_numpy(Y.astype(np.float32)) net = model.build_net() opt = torch.optim.Adam(net.parameters(), lr=1e-3) lossf = torch.nn.MSELoss() bs = 32 for ep in range(epochs): net.train() perm = torch.randperm(len(Xt)) tot = 0.0 for j in range(0, len(perm), bs): b = perm[j:j + bs] opt.zero_grad() loss = lossf(net(Xt[b]), Yt[b]) loss.backward(); opt.step() tot += float(loss) * len(b) print(f"epoch {ep+1:2d} mse={tot/len(Xt):.5f}") out_path.parent.mkdir(parents=True, exist_ok=True) torch.save(net.state_dict(), out_path) print(f"saved {out_path}") return 0 if __name__ == "__main__": sys.exit(main())