| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const TPL = 24; |
| const R = 16; |
| const STEP = 1; |
| const GRID = 2 * R / STEP + 1; |
| const NSHIFT = GRID * GRID; |
| const MAXB = 64; |
|
|
| const WGSL = ` |
| struct Params { n_boxes : u32, fw : u32, fh : u32, p3 : u32 } |
| @group(0) @binding(0) var<uniform> params : Params; |
| @group(0) @binding(1) var<storage, read> frame : array<f32>; // [fh, fw] gray |
| @group(0) @binding(2) var<storage, read> tpl : array<f32>; // [MAXB, 24*24] zero-mean |
| // boxinfo: [MAXB] vec4<f32> = (x0, y0, sx, sy) — угол окна поиска в px кадра |
| // и шаг сэмплирования шаблона (bw/24, bh/24) |
| @group(0) @binding(3) var<storage, read> boxinfo : array<vec4<f32>>; |
| @group(0) @binding(4) var<storage, read_write> out : array<vec4<f32>>; // (dx, dy, score, 0) |
| |
| const TPL : u32 = ${TPL}u; |
| const GRID : i32 = ${GRID}; |
| const STEP : i32 = ${STEP}; |
| const R : i32 = ${R}; |
| const NSHIFT : u32 = ${NSHIFT}u; |
| |
| var<workgroup> best : array<f32, 256>; |
| var<workgroup> bestI : array<u32, 256>; |
| |
| fn sample(x : f32, y : f32) -> f32 { |
| // билинейно, с клампом к краям кадра |
| let fx = clamp(x, 0.0, f32(params.fw - 1u)); |
| let fy = clamp(y, 0.0, f32(params.fh - 1u)); |
| let x0 = u32(fx); let y0 = u32(fy); |
| let x1 = min(x0 + 1u, params.fw - 1u); |
| let y1 = min(y0 + 1u, params.fh - 1u); |
| let ax = fx - f32(x0); let ay = fy - f32(y0); |
| let v00 = frame[y0 * params.fw + x0]; |
| let v01 = frame[y0 * params.fw + x1]; |
| let v10 = frame[y1 * params.fw + x0]; |
| let v11 = frame[y1 * params.fw + x1]; |
| return mix(mix(v00, v01, ax), mix(v10, v11, ax), ay); |
| } |
| |
| @compute @workgroup_size(256) |
| fn main(@builtin(workgroup_id) wg_id : vec3<u32>, |
| @builtin(local_invocation_index) lid : u32) { |
| let b = wg_id.x; |
| let info = boxinfo[b]; |
| var myBest = -1.0e30; |
| var myIdx = 0u; |
| for (var s = lid; s < NSHIFT; s = s + 256u) { |
| let dy = f32(i32(s / u32(GRID)) * STEP - R); |
| let dx = f32(i32(s % u32(GRID)) * STEP - R); |
| var score = 0.0; |
| for (var i = 0u; i < TPL; i = i + 1u) { |
| let y = info.y + dy + (f32(i) + 0.5) * info.w; |
| for (var j = 0u; j < TPL; j = j + 1u) { |
| let x = info.x + dx + (f32(j) + 0.5) * info.z; |
| score = score + tpl[b * (TPL * TPL) + i * TPL + j] * sample(x, y); |
| } |
| } |
| if (score > myBest) { myBest = score; myIdx = s; } |
| } |
| best[lid] = myBest; bestI[lid] = myIdx; |
| workgroupBarrier(); |
| var stride = 128u; |
| loop { |
| if (stride == 0u) { break; } |
| if (lid < stride) { |
| if (best[lid + stride] > best[lid]) { |
| best[lid] = best[lid + stride]; bestI[lid] = bestI[lid + stride]; |
| } |
| } |
| workgroupBarrier(); |
| stride = stride >> 1u; |
| } |
| if (lid == 0u) { |
| let s = bestI[0]; |
| let dy = f32(i32(s / u32(GRID)) * STEP - R); |
| let dx = f32(i32(s % u32(GRID)) * STEP - R); |
| out[b] = vec4<f32>(dx, dy, best[0], 0.0); |
| } |
| } |
| `; |
|
|
| export class Tracker { |
| constructor(device) { |
| this.device = device; |
| this.boxes = []; |
| this.fw = 0; this.fh = 0; |
| } |
|
|
| async init() { |
| const d = this.device; |
| this.pipe = await d.createComputePipelineAsync({ |
| layout: 'auto', |
| compute: { module: d.createShaderModule({ code: WGSL }), entryPoint: 'main' }, |
| }); |
| this.u = d.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); |
| this.tplBuf = d.createBuffer({ size: MAXB * TPL * TPL * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); |
| this.infoBuf = d.createBuffer({ size: MAXB * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); |
| this.outBuf = d.createBuffer({ size: MAXB * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC }); |
| this.frameBuf = null; |
| return this; |
| } |
|
|
| _ensureFrame(fw, fh) { |
| if (this.frameBuf && this.fw === fw && this.fh === fh) return; |
| this.frameBuf?.destroy(); |
| this.fw = fw; this.fh = fh; |
| this.frameBuf = this.device.createBuffer({ |
| size: fw * fh * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); |
| this.bind = this.device.createBindGroup({ |
| layout: this.pipe.getBindGroupLayout(0), |
| entries: [this.u, this.frameBuf, this.tplBuf, this.infoBuf, this.outBuf] |
| .map((buffer, i) => ({ binding: i, resource: { buffer } })), |
| }); |
| } |
|
|
| uploadFrame(gray, fw, fh) { |
| this._ensureFrame(fw, fh); |
| this.device.queue.writeBuffer(this.frameBuf, 0, gray); |
| this.device.queue.writeBuffer(this.u, 0, new Uint32Array([this.boxes.length, fw, fh, 0])); |
| } |
|
|
| |
| |
| |
| setKeyframe(dets, gray, fw, fh, stateBoxes = null) { |
| const src = dets.slice(0, MAXB); |
| this.boxes = src.map((d, i) => ({ det: d, |
| box: { ...((stateBoxes && stateBoxes[i]) || d.box) } })); |
| this.uploadFrame(gray, fw, fh); |
| const tpl = new Float32Array(MAXB * TPL * TPL); |
| for (let b = 0; b < src.length; b++) { |
| const bx = src[b].box; |
| const x0 = bx.x1 * fw, y0 = bx.y1 * fh; |
| const sx = (bx.x2 - bx.x1) * fw / TPL, sy = (bx.y2 - bx.y1) * fh / TPL; |
| const t = new Float32Array(TPL * TPL); |
| let mean = 0; |
| for (let i = 0; i < TPL; i++) |
| for (let j = 0; j < TPL; j++) { |
| const y = Math.min(fh - 1, Math.max(0, Math.round(y0 + (i + 0.5) * sy))); |
| const x = Math.min(fw - 1, Math.max(0, Math.round(x0 + (j + 0.5) * sx))); |
| const v = gray[y * fw + x]; |
| t[i * TPL + j] = v; mean += v; |
| } |
| mean /= TPL * TPL; |
| for (let k = 0; k < TPL * TPL; k++) tpl[b * TPL * TPL + k] = t[k] - mean; |
| } |
| this.device.queue.writeBuffer(this.tplBuf, 0, tpl); |
| } |
|
|
| |
| async track(gray, fw, fh) { |
| if (!this.boxes.length) return []; |
| this.uploadFrame(gray, fw, fh); |
| const info = new Float32Array(MAXB * 4); |
| for (let b = 0; b < this.boxes.length; b++) { |
| const bx = this.boxes[b].box; |
| info[b * 4] = bx.x1 * fw; |
| info[b * 4 + 1] = bx.y1 * fh; |
| info[b * 4 + 2] = (bx.x2 - bx.x1) * fw / TPL; |
| info[b * 4 + 3] = (bx.y2 - bx.y1) * fh / TPL; |
| } |
| this.device.queue.writeBuffer(this.infoBuf, 0, info); |
| const enc = this.device.createCommandEncoder(); |
| const pass = enc.beginComputePass(); |
| pass.setPipeline(this.pipe); |
| pass.setBindGroup(0, this.bind); |
| pass.dispatchWorkgroups(this.boxes.length); |
| pass.end(); |
| const st = this.device.createBuffer({ |
| size: this.boxes.length * 16, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }); |
| enc.copyBufferToBuffer(this.outBuf, 0, st, 0, this.boxes.length * 16); |
| this.device.queue.submit([enc.finish()]); |
| await st.mapAsync(GPUMapMode.READ); |
| const res = new Float32Array(st.getMappedRange().slice(0)); |
| st.unmap(); st.destroy(); |
| const dxs = [], dys = []; |
| const out = this.boxes.map((t, b) => { |
| const dx = res[b * 4] / fw, dy = res[b * 4 + 1] / fh; |
| dxs.push(res[b * 4]); dys.push(res[b * 4 + 1]); |
| t.box.x1 += dx; t.box.x2 += dx; |
| t.box.y1 += dy; t.box.y2 += dy; |
| return { ...t.det, box: { ...t.box } }; |
| }); |
| const med = (a) => a.slice().sort((x, y) => x - y)[a.length >> 1]; |
| this.lastShift = { dx: med(dxs) / fw, dy: med(dys) / fh }; |
| return out; |
| } |
| } |
|
|
| export function toGray(imageData, fw, fh) { |
| const g = new Float32Array(fw * fh); |
| const d = imageData.data; |
| for (let i = 0; i < fw * fh; i++) { |
| g[i] = (0.299 * d[i * 4] + 0.587 * d[i * 4 + 1] + 0.114 * d[i * 4 + 2]) / 255; |
| } |
| return g; |
| } |
|
|