File size: 6,791 Bytes
3f22414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
 * Tiling utilities — dividing images into overlapping tiles and
 * reassembling them with seam-free stitching.
 */

/**
 * Compute the interior region a tile should contribute, trimming overlap
 * margins at seams. Returns a canvas-space rect { x, y, w, h }.
 *
 * Each side touching another tile is trimmed by half the overlap;
 * edges against the canvas boundary keep their full extent.
 */
export function overlapCrop(destX, destY, tileW, tileH, canvasW, canvasH, overlap) {
  const cropL = destX > 0 ? (overlap / 2) | 0 : 0;
  const cropT = destY > 0 ? (overlap / 2) | 0 : 0;
  const cropR = (destX + tileW) < canvasW ? (overlap / 2) | 0 : 0;
  const cropB = (destY + tileH) < canvasH ? (overlap / 2) | 0 : 0;
  return {
    x: destX + cropL,
    y: destY + cropT,
    w: tileW - cropL - cropR,
    h: tileH - cropT - cropB,
  };
}

/**
 * Build the grid of overlapping source tiles that cover the image.
 * Returns an array of { x, y, w, h } in source-pixel coordinates.
 */
export function buildTileGrid(srcW, srcH, tileSize, overlap) {
  const noTiling = tileSize <= 0;
  const size = noTiling ? Math.max(srcW, srcH) : tileSize;
  const step = noTiling ? size : size - overlap;
  const tiles = [];
  for (let ty = 0; ty < srcH; ty += step) {
    for (let tx = 0; tx < srcW; tx += step) {
      tiles.push({
        x: tx, y: ty,
        w: Math.min(size, srcW - tx),
        h: Math.min(size, srcH - ty),
      });
    }
  }
  return tiles;
}

/** Write an ImageData to the canvas, trimming overlap margins at tile seams. */
export function pasteTileCropped(ctx, imgData, dx, dy, canvasW, canvasH, overlap) {
  const crop = overlapCrop(dx, dy, imgData.width, imgData.height, canvasW, canvasH, overlap);
  if (crop.w <= 0 || crop.h <= 0) return;
  ctx.putImageData(imgData, dx, dy, crop.x - dx, crop.y - dy, crop.w, crop.h);
}

// ─── Gaussian tile blending ────────────────────────────────────────────
// For diffusion-style refiners (e.g. TinySR) the tile-edge artifacts are
// strong enough that the half-overlap hard crop above shows visible seams.
// `makeGaussianWeights2D` produces a per-pixel weight kernel matching
// TinySR's pipeline.js: variance=0.01 scaled by tile^2 so the shape is
// scale-invariant. Engines that opt in maintain a float32 accumulator and
// a contribution buffer, then divide once at the end.

/**
 * 2D Gaussian weight kernel for tile blending. Returns a Float32Array of
 * length tileH*tileW with pixel-space weights (single channel — apply the
 * same weight to all 3 colour channels at accumulation time).
 *
 * Matches the formula in tinysr/tools/web/pipeline.js:makeGaussianWeights
 * so the visual behaviour ports over directly.
 */
export function makeGaussianWeights2D(tileH, tileW) {
  const variance = 0.01;
  const midX = (tileW - 1) / 2;
  const midY = tileH / 2;  // intentional asymmetry — matches the upstream
  const denomX = tileW * tileW * 2 * variance;
  const denomY = tileH * tileH * 2 * variance;
  const norm = 1 / Math.sqrt(2 * Math.PI * variance);
  const xs = new Float32Array(tileW);
  const ys = new Float32Array(tileH);
  for (let i = 0; i < tileW; i++) xs[i] = norm * Math.exp(-((i - midX) ** 2) / denomX);
  for (let i = 0; i < tileH; i++) ys[i] = norm * Math.exp(-((i - midY) ** 2) / denomY);
  const out = new Float32Array(tileH * tileW);
  for (let y = 0; y < tileH; y++) {
    const yw = ys[y];
    const rowOff = y * tileW;
    for (let x = 0; x < tileW; x++) out[rowOff + x] = yw * xs[x];
  }
  return out;
}

/**
 * Accumulate one model-output tile into the canvas-sized float32 buffers
 * `accumRGB` (3*outW*outH, RGB-planar) and `accumW` (outW*outH), weighted
 * by `weights`. `srcRGB` is in [0, modelValueRange]; `layout` is 'chw' for
 * RGB-planar input or 'hwc' for RGB-interleaved. Crops to the top-left
 * (tileW × tileH) region if the model output was padded.
 */
export function accumulateGaussianTile(
  accumRGB, accumW, outW, outH,
  srcRGB, srcStrideW, srcStrideH,
  tileW, tileH, destX, destY,
  weights, valueScale, layout,
) {
  const outPlane = outW * outH;
  const isCHW = layout === 'chw';
  const chanStride = isCHW ? srcStrideW * srcStrideH : 1;
  const colStride = isCHW ? 1 : 3;
  const rowStride = isCHW ? srcStrideW : srcStrideW * 3;
  for (let y = 0; y < tileH; y++) {
    const dy = destY + y;
    if (dy < 0 || dy >= outH) continue;
    const wRow = y * tileW;
    const sRow = y * rowStride;
    const dRow = dy * outW;
    for (let x = 0; x < tileW; x++) {
      const dx = destX + x;
      if (dx < 0 || dx >= outW) continue;
      const w = weights[wRow + x];
      const srcIdx = sRow + x * colStride;
      const dstIdx = dRow + dx;
      accumRGB[dstIdx]                += srcRGB[srcIdx]                  * valueScale * w;
      accumRGB[outPlane + dstIdx]     += srcRGB[srcIdx + chanStride]     * valueScale * w;
      accumRGB[2 * outPlane + dstIdx] += srcRGB[srcIdx + 2 * chanStride] * valueScale * w;
      accumW[dstIdx] += w;
    }
  }
}

/**
 * Divide accumulated RGB by weights inside a rectangular region of the
 * output canvas, clamp to [0,255], and write via putImageData. Called
 * after each tile accumulates so the user sees progressive preview; the
 * last tile to touch any given pixel ends up writing the final value
 * (which is the same value a single full-canvas finalize would produce).
 *
 * Clips the region to the canvas bounds, so callers can pass tile-sized
 * rects without worrying about edge tiles.
 */
export function finalizeGaussianRegion(ctx, outX, outY, regionW, regionH, outW, outH, accumRGB, accumW) {
  const x0 = Math.max(0, outX | 0);
  const y0 = Math.max(0, outY | 0);
  const x1 = Math.min(outW, (outX + regionW) | 0);
  const y1 = Math.min(outH, (outY + regionH) | 0);
  const w = x1 - x0;
  const h = y1 - y0;
  if (w <= 0 || h <= 0) return;
  const imgData = ctx.createImageData(w, h);
  const px = imgData.data;
  const plane = outW * outH;
  for (let y = 0; y < h; y++) {
    const srcRow = (y0 + y) * outW;
    const dstRow = y * w;
    for (let x = 0; x < w; x++) {
      // accumW is zero only if no tile covered this pixel — shouldn't
      // happen for pixels reached by this call, but guard against NaN.
      const srcIdx = srcRow + x0 + x;
      const wAcc = accumW[srcIdx] || 1;
      const r = accumRGB[srcIdx] / wAcc;
      const g = accumRGB[plane + srcIdx] / wAcc;
      const b = accumRGB[2 * plane + srcIdx] / wAcc;
      const o = (dstRow + x) * 4;
      px[o]     = r < 0 ? 0 : r > 255 ? 255 : (r + 0.5) | 0;
      px[o + 1] = g < 0 ? 0 : g > 255 ? 255 : (g + 0.5) | 0;
      px[o + 2] = b < 0 ? 0 : b > 255 ? 255 : (b + 0.5) | 0;
      px[o + 3] = 255;
    }
  }
  ctx.putImageData(imgData, x0, y0);
}