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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
use crate::circular_feature_descriptor::CircularFeatureDescriptor;
use crate::photo::Photo;
/// A grid that computes and stores `CircularFeatureDescriptor` values for each
/// (x, y) location in an image. Each descriptor captures the "center of mass"
/// of the R/G/B channels in a circular neighborhood around that point.
pub struct CircularFeatureGrid {
/// A vector of circular feature descriptors, one per position in the grid.
feature_descriptors: Vec<CircularFeatureDescriptor>,
}
impl CircularFeatureGrid {
/// Creates a new `CircularFeatureGrid` without manually specifying rotation handling.
pub fn new(photo: &Photo, width: usize, height: usize, circle_radius: usize) -> Self {
// By default, we allow rotation-based computations (the `true` parameter).
Self::new_with_rotation(photo, width, height, circle_radius, true)
}
/// Creates a new `CircularFeatureGrid`, optionally using rotation-based computations.
///
/// - `photo` holds the pixel data.
/// - `width`, `height` define the grid size in pixels.
/// - `circle_radius` sets how large each neighborhood is.
/// - `rotation` indicates whether advanced rotation alignment is used.
///
/// Creates a new `CircularFeatureGrid`, optionally using rotation-based computations.
///
/// - `photo` holds the pixel data.
/// - `width`, `height` define the grid size in pixels.
/// - `circle_radius` sets how large each neighborhood is.
/// - `_rotation` is accepted for backwards compatibility; both settings always
/// computed the same descriptor.
pub fn new_with_rotation(
photo: &Photo,
width: usize,
height: usize,
circle_radius: usize,
_rotation: bool,
) -> Self {
let radius = circle_radius as isize;
// Half-width of the disc for each row, computed once here instead of
// being recomputed (with a `sqrt` and a `round`) for every descriptor.
let row_half_width: Vec<isize> = (-radius..=radius)
.map(|dy| (((radius * radius) as f64 - (dy * dy) as f64).sqrt().round()) as isize)
.collect();
let mut feature_descriptors = vec![CircularFeatureDescriptor::default(); width * height];
Self::populate_feature_descriptors(
&photo.img_data,
width,
height,
radius,
&row_half_width,
&mut feature_descriptors,
);
CircularFeatureGrid {
feature_descriptors,
}
}
/// Returns a reference to the vector of `CircularFeatureDescriptor` objects.
pub fn get_infos(&self) -> &Vec<CircularFeatureDescriptor> {
&self.feature_descriptors
}
/// Fills in the `feature_descriptors` for every position in the grid.
///
/// Along the interior of a row the disc is *slid* rather than re-walked: consecutive
/// centres overlap in all but one column per disc row, so [`AbsSums`] can update the
/// sums by subtracting the pixel leaving each of the `2r+1` rows and adding the one
/// entering it. That is 2*(2r+1) pixel loads per descriptor instead of the whole
/// disc — 42 instead of 317 at radius 10 — and, being integer arithmetic, gives
/// bit-identical sums.
///
/// Only the border ring, where the disc wraps toroidally and the overlap argument
/// does not hold, still walks the full disc.
fn populate_feature_descriptors(
data: &[u8],
width: usize,
height: usize,
radius: isize,
row_half_width: &[isize],
out: &mut [CircularFeatureDescriptor],
) {
let (w, h) = (width as isize, height as isize);
let interior_x = radius < w - radius;
for y in 0..h {
let interior_row = y >= radius && y < h - radius && interior_x;
if !interior_row {
for x in 0..w {
let sums = Self::disc_sums_wrapping(data, w, h, x, y, radius, row_half_width);
out[(x + y * w) as usize] = Self::finish_descriptor(x, y, sums);
}
continue;
}
for x in 0..radius {
let sums = Self::disc_sums_wrapping(data, w, h, x, y, radius, row_half_width);
out[(x + y * w) as usize] = Self::finish_descriptor(x, y, sums);
}
// Seed the running sums at the first interior centre of this row.
let mut acc = AbsSums::default();
for (i, &half) in row_half_width.iter().enumerate() {
let py = y + i as isize - radius;
for px in (radius - half)..=(radius + half) {
acc.add(px, py, data, w);
}
}
for x in radius..(w - radius) {
if x > radius {
// Slide one column right: one pixel out and one in per disc row.
for (i, &half) in row_half_width.iter().enumerate() {
let py = y + i as isize - radius;
acc.sub(x - 1 - half, py, data, w);
acc.add(x + half, py, data, w);
}
}
out[(x + y * w) as usize] = Self::finish_descriptor(x, y, acc.to_disc_sums(x, y));
}
for x in (w - radius)..w {
let sums = Self::disc_sums_wrapping(data, w, h, x, y, radius, row_half_width);
out[(x + y * w) as usize] = Self::finish_descriptor(x, y, sums);
}
}
}
/// Accumulates the disc sums for a centre near an edge, wrapping toroidally.
fn disc_sums_wrapping(
data: &[u8],
w: isize,
h: isize,
center_x: isize,
center_y: isize,
radius: isize,
row_half_width: &[isize],
) -> DiscSums {
let mut s = DiscSums::default();
for dy in -radius..=radius {
let half = row_half_width[(dy + radius) as usize];
let wrapped_y = (center_y + dy + h) % h;
let row = wrapped_y * w;
for dx in -half..=half {
let wrapped_x = (center_x + dx + w) % w;
let p = ((wrapped_x + row) * 4) as usize;
let (r, g, b) = (data[p] as isize, data[p + 1] as isize, data[p + 2] as isize);
s.add(dx, dy, r, g, b);
}
}
s
}
/// Turns accumulated disc sums into a `CircularFeatureDescriptor`, computing the
/// per-channel "centre of mass" and aligning each channel to the combined angle.
fn finish_descriptor(
center_x: isize,
center_y: isize,
s: DiscSums,
) -> CircularFeatureDescriptor {
let mut descriptor = CircularFeatureDescriptor::default();
let (sum_red, sum_green, sum_blue) = (s.sum_red, s.sum_green, s.sum_blue);
let inv = |n: isize| if n == 0 { 0.0 } else { 1.0 / n as f32 };
let (ir, ig, ib) = (inv(sum_red), inv(sum_green), inv(sum_blue));
let red_cm_x = s.wx_red as f32 * ir;
let red_cm_y = s.wy_red as f32 * ir;
let green_cm_x = s.wx_green as f32 * ig;
let green_cm_y = s.wy_green as f32 * ig;
let blue_cm_x = s.wx_blue as f32 * ib;
let blue_cm_y = s.wy_blue as f32 * ib;
let sum_all = sum_red + sum_green + sum_blue;
let ia = inv(sum_all);
let total_cm_x = (s.wx_red + s.wx_green + s.wx_blue) as f32 * ia;
let total_cm_y = (s.wy_red + s.wy_green + s.wy_blue) as f32 * ia;
let total_radius = (total_cm_x * total_cm_x + total_cm_y * total_cm_y).sqrt();
descriptor.total_angle = if sum_all == 0 {
0.0
} else {
total_cm_y.atan2(total_cm_x)
};
// Rotating a channel's centre of mass by -total_angle needs no trigonometry:
// cos(total_angle) and sin(total_angle) are total_cm_x/total_radius and
// total_cm_y/total_radius by construction, so the rotation is one dot and one
// cross product scaled by 1/total_radius. That replaces three `atan2`, three
// `sin`, three `cos` and three `sqrt` per pixel with a single `sqrt`.
let (c, sn) = if total_radius == 0.0 {
(1.0, 0.0)
} else {
(total_cm_x / total_radius, total_cm_y / total_radius)
};
let rot = |x: f32, y: f32| (x * c + y * sn, y * c - x * sn);
let (arx, ary) = rot(red_cm_x, red_cm_y);
let (agx, agy) = rot(green_cm_x, green_cm_y);
let (abx, aby) = rot(blue_cm_x, blue_cm_y);
descriptor.center_x = center_x as u16;
descriptor.center_y = center_y as u16;
let q = |v: f32| f32::round(v * 100.0) as i16;
descriptor.feature_vector = [q(arx), q(ary), q(agx), q(agy), q(abx), q(aby)];
descriptor
}
}
/// Running colour and position-weighted sums over one circular neighbourhood.
#[derive(Default, Clone, Copy)]
struct DiscSums {
sum_red: isize,
sum_green: isize,
sum_blue: isize,
wx_red: isize,
wy_red: isize,
wx_green: isize,
wy_green: isize,
wx_blue: isize,
wy_blue: isize,
}
impl DiscSums {
#[inline(always)]
fn add(&mut self, dx: isize, dy: isize, r: isize, g: isize, b: isize) {
self.sum_red += r;
self.sum_green += g;
self.sum_blue += b;
self.wx_red += dx * r;
self.wy_red += dy * r;
self.wx_green += dx * g;
self.wy_green += dy * g;
self.wx_blue += dx * b;
self.wy_blue += dy * b;
}
}
/// The same sums as [`DiscSums`], but with the position moments kept in absolute image
/// coordinates so that they survive a shift of the disc.
///
/// `DiscSums` weights each pixel by its offset *from the centre*, which changes for every
/// retained pixel when the disc moves, so those moments cannot be updated incrementally.
/// The absolute moments `A = sum(px*v)` and `B = sum(py*v)` can be, and the relative ones
/// come back exactly as `Wx = A - x*S` and `Wy = B - y*S`.
#[derive(Default, Clone, Copy)]
struct AbsSums {
s: [i64; 3],
ax: [i64; 3],
ay: [i64; 3],
}
impl AbsSums {
#[inline(always)]
fn rgb(data: &[u8], px: isize, py: isize, w: isize) -> (i64, i64, i64) {
let p = ((py * w + px) * 4) as usize;
// SAFETY: only reached for centres at least `radius` from every edge, and
// `half <= radius`, so `px` is in `0..w` and `py` in `0..h`.
unsafe {
(
*data.get_unchecked(p) as i64,
*data.get_unchecked(p + 1) as i64,
*data.get_unchecked(p + 2) as i64,
)
}
}
#[inline(always)]
fn add(&mut self, px: isize, py: isize, data: &[u8], w: isize) {
let (r, g, b) = Self::rgb(data, px, py, w);
let (pxi, pyi) = (px as i64, py as i64);
for (c, v) in [r, g, b].into_iter().enumerate() {
self.s[c] += v;
self.ax[c] += pxi * v;
self.ay[c] += pyi * v;
}
}
#[inline(always)]
fn sub(&mut self, px: isize, py: isize, data: &[u8], w: isize) {
let (r, g, b) = Self::rgb(data, px, py, w);
let (pxi, pyi) = (px as i64, py as i64);
for (c, v) in [r, g, b].into_iter().enumerate() {
self.s[c] -= v;
self.ax[c] -= pxi * v;
self.ay[c] -= pyi * v;
}
}
#[inline(always)]
fn to_disc_sums(self, x: isize, y: isize) -> DiscSums {
let (xi, yi) = (x as i64, y as i64);
let rel = |c: usize| {
(
(self.ax[c] - xi * self.s[c]) as isize,
(self.ay[c] - yi * self.s[c]) as isize,
)
};
let (wx_red, wy_red) = rel(0);
let (wx_green, wy_green) = rel(1);
let (wx_blue, wy_blue) = rel(2);
DiscSums {
sum_red: self.s[0] as isize,
sum_green: self.s[1] as isize,
sum_blue: self.s[2] as isize,
wx_red,
wy_red,
wx_green,
wy_green,
wx_blue,
wy_blue,
}
}
}