code wiki / (root) / nx_visdesc.nx

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}