nx_imgxform.nx source
↩ module page · 303 lines · 11274 B
1// nx_imgxform.nx -- DETERMINISTIC IMAGE-TRANSFORMATION BATTERY: the adversary set a reverse-image
2// engine must survive. TinEye's product claim is that it finds "modified versions" of an image --
3// resized, cropped, edited, colour-adjusted, watermarked. You cannot honestly say how close we are to
4// that without APPLYING those modifications and MEASURING what survives. This organ is that adversary:
5// 16 named transformation classes, pure integer, zero float, bit-reproducible (fixed LCG seed), each a
6// pure function grayscale(w,h) -> grayscale(w2,h2).
7//
8// It deliberately holds NO ranking or hashing logic -- it only DEFORMS. nx_imgbench does the measuring,
9// nx_phash/nx_visdesc do the describing. Single responsibility: this file's one job is "make a modified
10// copy of an image, the way the real world modifies images".
11//
12// The classes are chosen from what actually happens to an image on the open web (and therefore what
13// TinEye/Google/Yandex are judged on): CDN rescaling, thumbnail generation, re-encoding artifacts,
14// editorial cropping, letterboxing into a fixed aspect, mirroring, EXIF-rotation, brightness/contrast
15// "enhancement", additive sensor/compression noise, and watermark/caption bars.
16// license_tier: ORIGINAL
17import "syscalls.nx"
18
19const XF_IDENT: i64 = 0
20const XF_SCALE50: i64 = 1
21const XF_SCALE200: i64 = 2
22const XF_BRIGHT_UP: i64 = 3
23const XF_BRIGHT_DN: i64 = 4
24const XF_CONTRAST: i64 = 5
25const XF_CROP10: i64 = 6
26const XF_CROP20: i64 = 7
27const XF_CROP30: i64 = 8
28const XF_PAD10: i64 = 9
29const XF_FLIPH: i64 = 10
30const XF_ROT90: i64 = 11
31const XF_ROT180: i64 = 12
32const XF_NOISE: i64 = 13
33const XF_WATERMARK: i64 = 14
34const XF_BLOCKQ: i64 = 15
35const XF_CROPSCALE: i64 = 16 // crop the centre THEN rescale it back to full size = occlusion + SCALE
36const XF_COUNT: i64 = 17
37
38const XF_NOISE_AMP: i64 = 12 // +/- grey levels of additive noise (heavy-recompression scale)
39const XF_BRIGHT_D: i64 = 40 // brightness offset applied by XF_BRIGHT_UP / XF_BRIGHT_DN
40const XF_WM_ROWPCT: i64 = 85 // watermark/caption bar starts at this percent of image height
41const XF_WM_LEVEL: i64 = 235 // watermark bar grey level (near-white caption bar)
42const XF_QSTEP: i64 = 16 // XF_BLOCKQ quantisation step (coarse-JPEG proxy)
43
44func xf_clamp(v: i64) -> i64 { if v < 0 { return 0 } if v > 255 { return 255 } return v }
45
46// MMIX LCG (Knuth) -- deterministic 64-bit stream so every transformation is bit-reproducible.
47func xf_rng(s: *i64) -> i64 { let x: i64 = s[0] * 0x5851F42D4C957F2D + 0x14057B7EF767814F; s[0] = x; return x }
48
49// human-readable class name (used by the ruler's report rows)
50func xf_name(kind: i64) -> *u8 {
51 if kind == XF_IDENT { return "identity " as *u8 }
52 if kind == XF_SCALE50 { return "rescale-50pct " as *u8 }
53 if kind == XF_SCALE200 { return "rescale-200pct" as *u8 }
54 if kind == XF_BRIGHT_UP { return "brightness+40 " as *u8 }
55 if kind == XF_BRIGHT_DN { return "brightness-40 " as *u8 }
56 if kind == XF_CONTRAST { return "contrast+25pct" as *u8 }
57 if kind == XF_CROP10 { return "crop-10pct " as *u8 }
58 if kind == XF_CROP20 { return "crop-20pct " as *u8 }
59 if kind == XF_CROP30 { return "crop-30pct " as *u8 }
60 if kind == XF_PAD10 { return "letterbox-10 " as *u8 }
61 if kind == XF_FLIPH { return "mirror-horiz " as *u8 }
62 if kind == XF_ROT90 { return "rotate-90 " as *u8 }
63 if kind == XF_ROT180 { return "rotate-180 " as *u8 }
64 if kind == XF_NOISE { return "noise-pm12 " as *u8 }
65 if kind == XF_WATERMARK { return "watermark-bar " as *u8 }
66 if kind == XF_BLOCKQ { return "recompress-8x8" as *u8 }
67 if kind == XF_CROPSCALE { return "crop+rescale " as *u8 }
68 return "unknown " as *u8
69}
70
71// nearest-neighbour resample (the CDN/thumbnailer operation) into a caller-sized buffer
72func xf_resample(gray: *u8, w: i64, h: i64, out: *u8, dw: i64, dh: i64) -> i64 {
73 var oy: i64 = 0
74 while oy < dh {
75 let sy: i64 = (oy * h) / dh
76 var ox: i64 = 0
77 while ox < dw {
78 let sx: i64 = (ox * w) / dw
79 out[oy * dw + ox] = gray[sy * w + sx]
80 ox = ox + 1
81 }
82 oy = oy + 1
83 }
84 return 0
85}
86
87// centre crop keeping keep_pct percent of EACH dimension (so crop-30 discards ~51 percent of the pixels)
88func xf_crop(gray: *u8, w: i64, h: i64, keep_pct: i64, outwh: *i64) -> *u8 {
89 var nw: i64 = w * keep_pct / 100
90 var nh: i64 = h * keep_pct / 100
91 if nw < 1 { nw = 1 }
92 if nh < 1 { nh = 1 }
93 let x0: i64 = (w - nw) / 2
94 let y0: i64 = (h - nh) / 2
95 let out: *u8 = sys_mmap(nw * nh)
96 var y: i64 = 0
97 while y < nh {
98 var x: i64 = 0
99 while x < nw { out[y * nw + x] = gray[(y0 + y) * w + (x0 + x)]; x = x + 1 }
100 y = y + 1
101 }
102 outwh[0] = nw
103 outwh[1] = nh
104 return out
105}
106
107// letterbox: surround the image with a black border of pct percent per side (fixed-aspect embedding)
108func xf_pad(gray: *u8, w: i64, h: i64, pct: i64, outwh: *i64) -> *u8 {
109 let bx: i64 = w * pct / 100
110 let by: i64 = h * pct / 100
111 let nw: i64 = w + 2 * bx
112 let nh: i64 = h + 2 * by
113 let out: *u8 = sys_mmap(nw * nh)
114 var i: i64 = 0
115 let tot: i64 = nw * nh
116 while i < tot { out[i] = 0 as u8; i = i + 1 }
117 var y: i64 = 0
118 while y < h {
119 var x: i64 = 0
120 while x < w { out[(by + y) * nw + (bx + x)] = gray[y * w + x]; x = x + 1 }
121 y = y + 1
122 }
123 outwh[0] = nw
124 outwh[1] = nh
125 return out
126}
127
128// coarse block quantisation: pull each pixel halfway to its 8x8 block mean, then snap to XF_QSTEP.
129// A float-free stand-in for aggressive JPEG re-encoding (the dominant real-web degradation).
130func xf_blockq(gray: *u8, w: i64, h: i64, out: *u8) -> i64 {
131 var by: i64 = 0
132 while by < h {
133 var bx: i64 = 0
134 while bx < w {
135 var sum: i64 = 0
136 var cnt: i64 = 0
137 var y: i64 = by
138 while y < by + 8 {
139 if y < h {
140 var x: i64 = bx
141 while x < bx + 8 {
142 if x < w { sum = sum + (gray[y * w + x] as i64); cnt = cnt + 1 }
143 x = x + 1
144 }
145 }
146 y = y + 1
147 }
148 var m: i64 = 0
149 if cnt > 0 { m = sum / cnt }
150 y = by
151 while y < by + 8 {
152 if y < h {
153 var x: i64 = bx
154 while x < bx + 8 {
155 if x < w {
156 let p: i64 = gray[y * w + x] as i64
157 var v: i64 = m + (p - m) / 2
158 v = (v / XF_QSTEP) * XF_QSTEP
159 out[y * w + x] = xf_clamp(v) as u8
160 }
161 x = x + 1
162 }
163 }
164 y = y + 1
165 }
166 bx = bx + 8
167 }
168 by = by + 8
169 }
170 return 0
171}
172
173// THE BATTERY. Returns a FRESH buffer holding the transformed image; outwh[0]=width, outwh[1]=height.
174// Never returns 0 for a valid kind, so callers do not need a null path for in-range classes.
175func xf_apply(kind: i64, gray: *u8, w: i64, h: i64, outwh: *i64) -> *u8 {
176 let n: i64 = w * h
177
178 if kind == XF_SCALE50 {
179 var dw: i64 = w / 2
180 var dh: i64 = h / 2
181 if dw < 1 { dw = 1 }
182 if dh < 1 { dh = 1 }
183 let o: *u8 = sys_mmap(dw * dh)
184 xf_resample(gray, w, h, o, dw, dh)
185 outwh[0] = dw; outwh[1] = dh
186 return o
187 }
188 if kind == XF_SCALE200 {
189 let dw: i64 = w * 2
190 let dh: i64 = h * 2
191 let o: *u8 = sys_mmap(dw * dh)
192 xf_resample(gray, w, h, o, dw, dh)
193 outwh[0] = dw; outwh[1] = dh
194 return o
195 }
196 if kind == XF_CROP10 { return xf_crop(gray, w, h, 90, outwh) }
197 if kind == XF_CROP20 { return xf_crop(gray, w, h, 80, outwh) }
198 if kind == XF_CROP30 { return xf_crop(gray, w, h, 70, outwh) }
199 if kind == XF_PAD10 { return xf_pad(gray, w, h, 10, outwh) }
200 if kind == XF_CROPSCALE {
201 // the real-web cropped-thumbnail: keep the centre 70%, then RESAMPLE it back to the original
202 // w x h. The surviving content is the same pixels shown at ~1.43x scale + a translated origin --
203 // a similarity (scale+translation), which a translation-only matcher structurally cannot verify.
204 let cwh: *i64 = sys_mmap(16) as *i64
205 let c: *u8 = xf_crop(gray, w, h, 70, cwh)
206 let o2: *u8 = sys_mmap(w * h)
207 xf_resample(c, cwh[0], cwh[1], o2, w, h)
208 outwh[0] = w; outwh[1] = h
209 return o2
210 }
211
212 if kind == XF_ROT90 {
213 let nw: i64 = h
214 let nh: i64 = w
215 let o: *u8 = sys_mmap(nw * nh)
216 var y: i64 = 0
217 while y < nh {
218 var x: i64 = 0
219 while x < nw { o[y * nw + x] = gray[(h - 1 - x) * w + y]; x = x + 1 }
220 y = y + 1
221 }
222 outwh[0] = nw; outwh[1] = nh
223 return o
224 }
225
226 // every remaining class is same-size
227 let o: *u8 = sys_mmap(n)
228 outwh[0] = w
229 outwh[1] = h
230
231 if kind == XF_IDENT {
232 var i: i64 = 0
233 while i < n { o[i] = gray[i]; i = i + 1 }
234 return o
235 }
236 if kind == XF_BRIGHT_UP {
237 var i: i64 = 0
238 while i < n { o[i] = xf_clamp((gray[i] as i64) + XF_BRIGHT_D) as u8; i = i + 1 }
239 return o
240 }
241 if kind == XF_BRIGHT_DN {
242 var i: i64 = 0
243 while i < n { o[i] = xf_clamp((gray[i] as i64) - XF_BRIGHT_D) as u8; i = i + 1 }
244 return o
245 }
246 if kind == XF_CONTRAST {
247 var i: i64 = 0
248 while i < n { o[i] = xf_clamp(((gray[i] as i64) - 128) * 5 / 4 + 128) as u8; i = i + 1 }
249 return o
250 }
251 if kind == XF_FLIPH {
252 var y: i64 = 0
253 while y < h {
254 var x: i64 = 0
255 while x < w { o[y * w + x] = gray[y * w + (w - 1 - x)]; x = x + 1 }
256 y = y + 1
257 }
258 return o
259 }
260 if kind == XF_ROT180 {
261 var y: i64 = 0
262 while y < h {
263 var x: i64 = 0
264 while x < w { o[y * w + x] = gray[(h - 1 - y) * w + (w - 1 - x)]; x = x + 1 }
265 y = y + 1
266 }
267 return o
268 }
269 if kind == XF_NOISE {
270 let s: *i64 = sys_mmap(8) as *i64
271 s[0] = 0x2545F4914F6CDD1D
272 var i: i64 = 0
273 while i < n {
274 var r: i64 = xf_rng(s) >> 17
275 if r < 0 { r = 0 - r }
276 let d: i64 = (r % (2 * XF_NOISE_AMP + 1)) - XF_NOISE_AMP
277 o[i] = xf_clamp((gray[i] as i64) + d) as u8
278 i = i + 1
279 }
280 return o
281 }
282 if kind == XF_WATERMARK {
283 var i: i64 = 0
284 while i < n { o[i] = gray[i]; i = i + 1 }
285 let y0: i64 = h * XF_WM_ROWPCT / 100
286 var y: i64 = y0
287 while y < h {
288 var x: i64 = 0
289 while x < w { o[y * w + x] = XF_WM_LEVEL as u8; x = x + 1 }
290 y = y + 1
291 }
292 return o
293 }
294 if kind == XF_BLOCKQ {
295 xf_blockq(gray, w, h, o)
296 return o
297 }
298
299 // unknown class -> identity copy (fail-safe: never a null deref, never silent garbage)
300 var i: i64 = 0
301 while i < n { o[i] = gray[i]; i = i + 1 }
302 return o
303}