File size: 5,881 Bytes
6db9857
 
 
 
 
 
b3bcf00
 
6db9857
 
 
 
 
f6412c7
 
 
 
 
 
 
 
 
 
 
6db9857
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3bcf00
 
 
6db9857
 
b3bcf00
 
 
 
 
 
 
6db9857
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6412c7
6db9857
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Image preprocessing identical to the Python pipeline (preprocess.py), so the browser feeds
// the model the same numbers that training, evaluation and the API use:
//   resize the shorter side to 256 with Pillow's bilinear filter (reproduced exactly:
//   two passes, 22-bit fixed-point coefficients, the same rounding), centre-crop 224x224
//   (Python's round-half-to-even), scale to [0, 1], normalise with the ImageNet mean/std.
// Input and intermediate images are RGB Uint8Arrays, row-major, 3 bytes per pixel.
// Verified against the Python code on all 758 validation photos (web/check_preprocess.mjs)
// and, for tall and unusual images, through the page (web/check_page.cjs).

export const RESIZE = 256;
export const CROP = 224;
export const MEAN = [0.485, 0.456, 0.406];
export const STD = [0.229, 0.224, 0.225];
// Same limits as preprocess.py: bigger or more elongated images are refused.
export const MAX_PIXELS = 25_000_000;
export const MAX_ASPECT = 10;

export function checkSize(w, h) {
  if (w < 1 || h < 1) throw new Error("The image is empty.");
  if (w * h > MAX_PIXELS) throw new Error(`That image is too large: ${w}x${h} pixels.`);
  if (Math.max(w, h) / Math.min(w, h) > MAX_ASPECT) {
    throw new Error(`That image is too elongated: ${w}x${h}.`);
  }
}

const PRECISION_BITS = 32 - 8 - 2; // Pillow's Resample.c

function bilinear(x) {
  x = Math.abs(x);
  return x < 1 ? 1 - x : 0;
}

// Pillow precompute_coeffs + normalize_coeffs_8bpc for one axis.
function coefficients(inSize, outSize) {
  const scale = inSize / outSize;
  const filterscale = Math.max(scale, 1);
  const support = 1 * filterscale; // bilinear support is 1
  const ksize = Math.ceil(support) * 2 + 1;
  const bounds = new Int32Array(outSize * 2);
  const kk = new Int32Array(outSize * ksize);
  const k = new Float64Array(ksize);
  for (let xx = 0; xx < outSize; xx++) {
    const center = (xx + 0.5) * scale;
    const ss = 1 / filterscale;
    let xmin = Math.trunc(center - support + 0.5);
    if (xmin < 0) xmin = 0;
    let xmax = Math.trunc(center + support + 0.5);
    if (xmax > inSize) xmax = inSize;
    xmax -= xmin;
    let ww = 0;
    for (let x = 0; x < xmax; x++) {
      const w = bilinear((x + xmin - center + 0.5) * ss);
      k[x] = w;
      ww += w;
    }
    for (let x = 0; x < ksize; x++) {
      const v = x < xmax && ww !== 0 ? k[x] / ww : 0;
      kk[xx * ksize + x] = Math.trunc(v < 0 ? -0.5 + v * (1 << PRECISION_BITS) : 0.5 + v * (1 << PRECISION_BITS));
    }
    bounds[xx * 2] = xmin;
    bounds[xx * 2 + 1] = xmax;
  }
  return { bounds, kk, ksize };
}

function clip8(ss) {
  const v = ss >> PRECISION_BITS;
  return v < 0 ? 0 : v > 255 ? 255 : v;
}

function resizeHorizontal(src, w, h, outW) {
  const { bounds, kk, ksize } = coefficients(w, outW);
  const out = new Uint8Array(outW * h * 3);
  const half = 1 << (PRECISION_BITS - 1);
  for (let y = 0; y < h; y++) {
    for (let xx = 0; xx < outW; xx++) {
      const xmin = bounds[xx * 2], xmax = bounds[xx * 2 + 1];
      let s0 = half, s1 = half, s2 = half;
      for (let x = 0; x < xmax; x++) {
        const c = kk[xx * ksize + x], i = (y * w + x + xmin) * 3;
        s0 += src[i] * c; s1 += src[i + 1] * c; s2 += src[i + 2] * c;
      }
      const o = (y * outW + xx) * 3;
      out[o] = clip8(s0); out[o + 1] = clip8(s1); out[o + 2] = clip8(s2);
    }
  }
  return out;
}

function resizeVertical(src, w, h, outH) {
  const { bounds, kk, ksize } = coefficients(h, outH);
  const out = new Uint8Array(w * outH * 3);
  const half = 1 << (PRECISION_BITS - 1);
  for (let yy = 0; yy < outH; yy++) {
    const ymin = bounds[yy * 2], ymax = bounds[yy * 2 + 1];
    for (let x = 0; x < w; x++) {
      let s0 = half, s1 = half, s2 = half;
      for (let y = 0; y < ymax; y++) {
        const c = kk[yy * ksize + y], i = ((y + ymin) * w + x) * 3;
        s0 += src[i] * c; s1 += src[i + 1] * c; s2 += src[i + 2] * c;
      }
      const o = (yy * w + x) * 3;
      out[o] = clip8(s0); out[o + 1] = clip8(s1); out[o + 2] = clip8(s2);
    }
  }
  return out;
}

// Pillow 13 (Resample.c) resizes horizontally first, then vertically, unless the height
// shrinks by more than twice as much as the width (tall photos): then vertically first. A
// pass that changes nothing is skipped. The order changes the rounding between passes.
export function resize(src, w, h, outW, outH) {
  let data = src;
  if (h - outH > 0 && h - outH > (w - outW) * 2) {
    if (outH !== h) data = resizeVertical(data, w, h, outH);
    if (outW !== w) data = resizeHorizontal(data, w, outH, outW);
  } else {
    if (outW !== w) data = resizeHorizontal(data, w, h, outW);
    if (outH !== h) data = resizeVertical(data, outW, h, outH);
  }
  return data;
}

export function shorterSideSize(w, h, size = RESIZE) {
  // torchvision: the long side is truncated, int(size * long / short)
  return w <= h ? [size, Math.trunc((size * h) / w)] : [Math.trunc((size * w) / h), size];
}

// Python's round(): halves go to the nearest even integer.
export function roundHalfEven(x) {
  const r = Math.round(x);
  return Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r;
}

export function preprocess(rgb, w, h) {
  checkSize(w, h);
  const [rw, rh] = shorterSideSize(w, h);
  const resized = resize(rgb, w, h, rw, rh);
  const top = roundHalfEven((rh - CROP) / 2);
  const left = roundHalfEven((rw - CROP) / 2);
  const out = new Float32Array(3 * CROP * CROP);
  const plane = CROP * CROP;
  for (let y = 0; y < CROP; y++) {
    for (let x = 0; x < CROP; x++) {
      const i = ((y + top) * rw + x + left) * 3;
      for (let c = 0; c < 3; c++) {
        // float32 arithmetic in the same order as NumPy: (v / 255 - mean) / std
        const v = Math.fround(Math.fround(resized[i + c] / 255) - Math.fround(MEAN[c]));
        out[c * plane + y * CROP + x] = Math.fround(v / Math.fround(STD[c]));
      }
    }
  }
  return out;
}