nx_visdesc.nx source
↩ module page · 144 lines · 6331 B
1// nx_visdesc.nx -- GRAYSCALE VISUAL-SIMILARITY DESCRIPTOR (edge-orientation layout, integer).
2// The dHash/aHash engine (nx_phash) finds NEAR-COPIES (the same image, recompressed/rebrightened/
3// resized). It cannot find images that merely LOOK ALIKE -- two DIFFERENT photos with the same
4// composition, texture, or structure. This descriptor does: a 4x4 spatial grid of magnitude-weighted
5// edge-orientation histograms (MPEG-7 Edge-Histogram-Descriptor / GIST spatial-envelope lineage) plus
6// a brightness-normalised tone layout, packed into an 80-dim integer vector ranked by L1 distance.
7// "Google-lite" visual similarity -- structural/textural, NOT semantic object recognition (the trained
8// -CNN rung is separate). Integer + deterministic + zero-dep = the sovereign moat over float feature
9// extractors.
10//
11// genealogy_id: manjunath_2001_mpeg7_ehd + oliva_torralba_2001_gist (spatial envelope)
12// composes: nx_image_sobel_x / nx_image_sobel_y / nx_image_s64_get -- the existing Sobel substrate
13// (ZERO new gradient math). license_tier: ORIGINAL
14import "nx_image.nx"
15
16const VD_GRID: i64 = 4 // 4x4 spatial cells
17const VD_CELL: i64 = 5 // per cell: 4 orientation bins + 1 relative-tone value
18const VD_DIM: i64 = 80 // VD_GRID*VD_GRID*VD_CELL
19const VD_OSCALE: i64 = 256 // each cell's orientation histogram normalised to this sum (contrast-invariant)
20const VD_MAGTHRESH: i64 = 24 // gradient-magnitude noise floor (|gx|+|gy|); below = flat, ignored
21const VD_TONEBIAS: i64 = 128 // tone stored as (cell_mean - global_mean) + bias, clamped 0..255 (brightness-invariant)
22
23func nx_visdesc_dim() -> i64 { return VD_DIM }
24
25// wrap a row-major u8 grayscale buffer in a 1-channel Image so the Sobel substrate can run on it.
26func vd_wrap_gray(gray: *u8, w: i64, h: i64) -> *Image {
27 let img: *Image = nx_image_alloc(w, h, 1)
28 var y: i64 = 0
29 while y < h {
30 var x: i64 = 0
31 while x < w { nx_image_set(img, x, y, 0, gray[y*w+x] as i64); x = x + 1 }
32 y = y + 1
33 }
34 return img
35}
36
37// classify a gradient (gx,gy) into one of 4 orientation bins; returns 0..3, or -1 below the floor.
38func vd_orient_bin(gx: i64, gy: i64) -> i64 {
39 var ax: i64 = gx
40 if ax < 0 { ax = 0 - ax }
41 var ay: i64 = gy
42 if ay < 0 { ay = 0 - ay }
43 if ax + ay < VD_MAGTHRESH { return 0 - 1 }
44 if ax >= 2*ay { return 0 } // gradient ~horizontal -> vertical edge
45 if ay >= 2*ax { return 1 } // gradient ~vertical -> horizontal edge
46 var pos: i64 = 0
47 if gx > 0 { if gy > 0 { pos = 1 } }
48 if gx < 0 { if gy < 0 { pos = 1 } }
49 if pos == 1 { return 2 } // diagonal "/"
50 return 3 // diagonal "\"
51}
52
53// extract the 80-dim descriptor of a w*h grayscale buffer into out[0..80); returns VD_DIM.
54func nx_visdesc_extract(gray: *u8, w: i64, h: i64, out: *i64) -> i64 {
55 let img: *Image = vd_wrap_gray(gray, w, h)
56 let gx: *ImageS64 = nx_image_sobel_x(img)
57 let gy: *ImageS64 = nx_image_sobel_y(img)
58 var gsum: i64 = 0
59 var i: i64 = 0
60 let npx: i64 = w*h
61 while i < npx { gsum = gsum + (gray[i] as i64); i = i + 1 }
62 var gmean: i64 = 0
63 if npx > 0 { gmean = gsum / npx }
64
65 var cy: i64 = 0
66 while cy < VD_GRID {
67 let y0: i64 = cy*h/VD_GRID
68 let y1: i64 = (cy+1)*h/VD_GRID
69 var cx: i64 = 0
70 while cx < VD_GRID {
71 let x0: i64 = cx*w/VD_GRID
72 let x1: i64 = (cx+1)*w/VD_GRID
73 var o0: i64 = 0
74 var o1: i64 = 0
75 var o2: i64 = 0
76 var o3: i64 = 0
77 var tsum: i64 = 0
78 var tcnt: i64 = 0
79 var y: i64 = y0
80 while y < y1 {
81 var x: i64 = x0
82 while x < x1 {
83 // Orientation only on INTERIOR pixels: the shared nx_image_get zero-pads out of
84 // bounds, so border-ring Sobel gradients do NOT shift with uniform brightness --
85 // accumulating them would break brightness invariance. The interior gradient of
86 // (image + c) equals that of image (Sobel kernels sum to 0), so interior-only is
87 // exactly brightness-invariant. Tone uses every pixel (it is invariant regardless).
88 var interior: i64 = 0
89 if x>0 { if x<w-1 { if y>0 { if y<h-1 { interior = 1 } } } }
90 if interior == 1 {
91 let ix: i64 = nx_image_s64_get(gx, x, y)
92 let iy: i64 = nx_image_s64_get(gy, x, y)
93 var mag: i64 = ix
94 if mag < 0 { mag = 0 - mag }
95 var amy: i64 = iy
96 if amy < 0 { amy = 0 - amy }
97 mag = mag + amy
98 let b: i64 = vd_orient_bin(ix, iy)
99 if b == 0 { o0 = o0 + mag }
100 if b == 1 { o1 = o1 + mag }
101 if b == 2 { o2 = o2 + mag }
102 if b == 3 { o3 = o3 + mag }
103 }
104 tsum = tsum + (gray[y*w+x] as i64)
105 tcnt = tcnt + 1
106 x = x + 1
107 }
108 y = y + 1
109 }
110 let base: i64 = (cy*VD_GRID + cx)*VD_CELL
111 let total: i64 = o0 + o1 + o2 + o3
112 if total > 0 {
113 out[base] = o0*VD_OSCALE/total
114 out[base+1] = o1*VD_OSCALE/total
115 out[base+2] = o2*VD_OSCALE/total
116 out[base+3] = o3*VD_OSCALE/total
117 } else {
118 out[base] = 0; out[base+1] = 0; out[base+2] = 0; out[base+3] = 0
119 }
120 var cmean: i64 = 0
121 if tcnt > 0 { cmean = tsum / tcnt }
122 var tone: i64 = cmean - gmean + VD_TONEBIAS
123 if tone < 0 { tone = 0 }
124 if tone > 255 { tone = 255 }
125 out[base+4] = tone
126 cx = cx + 1
127 }
128 cy = cy + 1
129 }
130 return VD_DIM
131}
132
133// L1 (Manhattan) distance between two descriptors; smaller = more visually similar.
134func nx_visdesc_l1(a: *i64, b: *i64) -> i64 {
135 var d: i64 = 0
136 var i: i64 = 0
137 while i < VD_DIM {
138 var t: i64 = a[i] - b[i]
139 if t < 0 { t = 0 - t }
140 d = d + t
141 i = i + 1
142 }
143 return d
144}