@srsergio/taptapp-ar 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -26
- package/dist/compiler/aframe.js +0 -3
- package/dist/compiler/compiler-base.d.ts +3 -7
- package/dist/compiler/compiler-base.js +28 -14
- package/dist/compiler/compiler.js +1 -1
- package/dist/compiler/compiler.worker.js +1 -1
- package/dist/compiler/controller.js +4 -5
- package/dist/compiler/controller.worker.js +0 -2
- package/dist/compiler/detector/crop-detector.js +0 -2
- package/dist/compiler/detector/detector-lite.d.ts +73 -0
- package/dist/compiler/detector/detector-lite.js +430 -0
- package/dist/compiler/detector/detector.js +236 -243
- package/dist/compiler/detector/kernels/cpu/binomialFilter.js +0 -1
- package/dist/compiler/detector/kernels/cpu/computeLocalization.js +0 -4
- package/dist/compiler/detector/kernels/cpu/computeOrientationHistograms.js +0 -18
- package/dist/compiler/detector/kernels/cpu/fakeShader.js +1 -1
- package/dist/compiler/detector/kernels/cpu/prune.d.ts +7 -1
- package/dist/compiler/detector/kernels/cpu/prune.js +1 -42
- package/dist/compiler/detector/kernels/webgl/upsampleBilinear.js +2 -2
- package/dist/compiler/estimation/refine-estimate.js +0 -1
- package/dist/compiler/estimation/utils.d.ts +1 -1
- package/dist/compiler/estimation/utils.js +1 -14
- package/dist/compiler/image-list.js +4 -4
- package/dist/compiler/input-loader.js +2 -2
- package/dist/compiler/matching/hamming-distance.js +13 -13
- package/dist/compiler/matching/hierarchical-clustering.js +1 -1
- package/dist/compiler/matching/matching.d.ts +20 -4
- package/dist/compiler/matching/matching.js +67 -41
- package/dist/compiler/matching/ransacHomography.js +1 -2
- package/dist/compiler/node-worker.d.ts +1 -0
- package/dist/compiler/node-worker.js +84 -0
- package/dist/compiler/offline-compiler.d.ts +171 -6
- package/dist/compiler/offline-compiler.js +303 -421
- package/dist/compiler/tensorflow-setup.js +27 -1
- package/dist/compiler/three.js +3 -5
- package/dist/compiler/tracker/extract.d.ts +1 -0
- package/dist/compiler/tracker/extract.js +200 -244
- package/dist/compiler/tracker/tracker.d.ts +1 -1
- package/dist/compiler/tracker/tracker.js +13 -18
- package/dist/compiler/utils/cumsum.d.ts +4 -2
- package/dist/compiler/utils/cumsum.js +17 -19
- package/dist/compiler/utils/gpu-compute.d.ts +57 -0
- package/dist/compiler/utils/gpu-compute.js +262 -0
- package/dist/compiler/utils/images.d.ts +4 -4
- package/dist/compiler/utils/images.js +67 -53
- package/dist/compiler/utils/worker-pool.d.ts +14 -0
- package/dist/compiler/utils/worker-pool.js +84 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/package.json +19 -13
- package/src/compiler/aframe.js +2 -4
- package/src/compiler/compiler-base.js +29 -14
- package/src/compiler/compiler.js +1 -1
- package/src/compiler/compiler.worker.js +1 -1
- package/src/compiler/controller.js +4 -5
- package/src/compiler/controller.worker.js +0 -2
- package/src/compiler/detector/crop-detector.js +0 -2
- package/src/compiler/detector/detector-lite.js +494 -0
- package/src/compiler/detector/detector.js +1052 -1063
- package/src/compiler/detector/kernels/cpu/binomialFilter.js +0 -1
- package/src/compiler/detector/kernels/cpu/computeLocalization.js +0 -4
- package/src/compiler/detector/kernels/cpu/computeOrientationHistograms.js +0 -17
- package/src/compiler/detector/kernels/cpu/fakeShader.js +1 -1
- package/src/compiler/detector/kernels/cpu/prune.js +1 -37
- package/src/compiler/detector/kernels/webgl/upsampleBilinear.js +2 -2
- package/src/compiler/estimation/refine-estimate.js +0 -1
- package/src/compiler/estimation/utils.js +9 -24
- package/src/compiler/image-list.js +4 -4
- package/src/compiler/input-loader.js +2 -2
- package/src/compiler/matching/hamming-distance.js +11 -15
- package/src/compiler/matching/hierarchical-clustering.js +1 -1
- package/src/compiler/matching/matching.js +72 -42
- package/src/compiler/matching/ransacHomography.js +0 -2
- package/src/compiler/node-worker.js +93 -0
- package/src/compiler/offline-compiler.js +339 -504
- package/src/compiler/tensorflow-setup.js +29 -1
- package/src/compiler/three.js +3 -5
- package/src/compiler/tracker/extract.js +211 -267
- package/src/compiler/tracker/tracker.js +13 -22
- package/src/compiler/utils/cumsum.js +17 -19
- package/src/compiler/utils/gpu-compute.js +303 -0
- package/src/compiler/utils/images.js +84 -53
- package/src/compiler/utils/worker-pool.js +89 -0
- package/src/index.ts +0 -2
- package/src/compiler/estimation/esimate-experiment.js +0 -316
- package/src/compiler/estimation/refine-estimate-experiment.js +0 -512
- package/src/react/AREditor.tsx +0 -394
- package/src/react/ProgressDialog.tsx +0 -185
|
@@ -7,7 +7,7 @@ const AR2_SEARCH_SIZE = 10;
|
|
|
7
7
|
const AR2_SEARCH_GAP = 1;
|
|
8
8
|
const AR2_SIM_THRESH = 0.8;
|
|
9
9
|
|
|
10
|
-
const TRACKING_KEYFRAME =
|
|
10
|
+
const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
|
|
11
11
|
|
|
12
12
|
// For some mobile device, only 16bit floating point texture is supported
|
|
13
13
|
// ref: https://www.tensorflow.org/js/guide/platform_environment#precision
|
|
@@ -20,8 +20,6 @@ class Tracker {
|
|
|
20
20
|
markerDimensions,
|
|
21
21
|
trackingDataList,
|
|
22
22
|
projectionTransform,
|
|
23
|
-
inputWidth,
|
|
24
|
-
inputHeight,
|
|
25
23
|
debugMode = false,
|
|
26
24
|
) {
|
|
27
25
|
this.markerDimensions = markerDimensions;
|
|
@@ -37,7 +35,7 @@ class Tracker {
|
|
|
37
35
|
// prebuild feature and marker pixel tensors
|
|
38
36
|
let maxCount = 0;
|
|
39
37
|
for (let i = 0; i < this.trackingKeyframeList.length; i++) {
|
|
40
|
-
maxCount = Math.max(maxCount, this.trackingKeyframeList[i].
|
|
38
|
+
maxCount = Math.max(maxCount, this.trackingKeyframeList[i].px.length);
|
|
41
39
|
}
|
|
42
40
|
this.featurePointsListT = [];
|
|
43
41
|
this.imagePixelsListT = [];
|
|
@@ -78,10 +76,6 @@ class Tracker {
|
|
|
78
76
|
modelViewProjectionTransform,
|
|
79
77
|
);
|
|
80
78
|
|
|
81
|
-
const markerWidth = this.markerDimensions[targetIndex][0];
|
|
82
|
-
const markerHeight = this.markerDimensions[targetIndex][1];
|
|
83
|
-
const keyframeWidth = this.trackingKeyframeList[targetIndex].width;
|
|
84
|
-
const keyframeHeight = this.trackingKeyframeList[targetIndex].height;
|
|
85
79
|
|
|
86
80
|
const featurePointsT = this.featurePointsListT[targetIndex];
|
|
87
81
|
const imagePixelsT = this.imagePixelsListT[targetIndex];
|
|
@@ -108,8 +102,10 @@ class Tracker {
|
|
|
108
102
|
const screenCoords = [];
|
|
109
103
|
const goodTrack = [];
|
|
110
104
|
|
|
105
|
+
const { px, py, s: scale } = trackingFrame;
|
|
106
|
+
|
|
111
107
|
for (let i = 0; i < matchingPoints.length; i++) {
|
|
112
|
-
if (sim[i] > AR2_SIM_THRESH && i <
|
|
108
|
+
if (sim[i] > AR2_SIM_THRESH && i < px.length) {
|
|
113
109
|
goodTrack.push(i);
|
|
114
110
|
const point = computeScreenCoordiate(
|
|
115
111
|
modelViewProjectionTransform,
|
|
@@ -118,8 +114,8 @@ class Tracker {
|
|
|
118
114
|
);
|
|
119
115
|
screenCoords.push(point);
|
|
120
116
|
worldCoords.push({
|
|
121
|
-
x:
|
|
122
|
-
y:
|
|
117
|
+
x: px[i] / scale,
|
|
118
|
+
y: py[i] / scale,
|
|
123
119
|
z: 0,
|
|
124
120
|
});
|
|
125
121
|
}
|
|
@@ -361,23 +357,18 @@ class Tracker {
|
|
|
361
357
|
|
|
362
358
|
_prebuild(trackingFrame, maxCount) {
|
|
363
359
|
return tf.tidy(() => {
|
|
364
|
-
const scale = trackingFrame
|
|
360
|
+
const { px, py, s: scale, d: data, w: width, h: height } = trackingFrame;
|
|
365
361
|
|
|
366
362
|
const p = [];
|
|
367
363
|
for (let k = 0; k < maxCount; k++) {
|
|
368
|
-
if (k <
|
|
369
|
-
p.push([
|
|
364
|
+
if (k < px.length) {
|
|
365
|
+
p.push([px[k] / scale, py[k] / scale]);
|
|
370
366
|
} else {
|
|
371
367
|
p.push([-1, -1]);
|
|
372
368
|
}
|
|
373
369
|
}
|
|
374
|
-
const imagePixels = tf.tensor(
|
|
375
|
-
|
|
376
|
-
]);
|
|
377
|
-
const imageProperties = tf.tensor(
|
|
378
|
-
[trackingFrame.width, trackingFrame.height, trackingFrame.scale],
|
|
379
|
-
[3],
|
|
380
|
-
);
|
|
370
|
+
const imagePixels = tf.tensor(data, [width * height]);
|
|
371
|
+
const imageProperties = tf.tensor([width, height, scale], [3]);
|
|
381
372
|
const featurePoints = tf.tensor(p, [p.length, 2], "float32");
|
|
382
373
|
|
|
383
374
|
return {
|
|
@@ -390,7 +381,7 @@ class Tracker {
|
|
|
390
381
|
|
|
391
382
|
_compileAndRun(program, inputs) {
|
|
392
383
|
const outInfo = tf.backend().compileAndRun(program, inputs);
|
|
393
|
-
return tf.engine().
|
|
384
|
+
return tf.engine().makeTensor(outInfo.dataId, outInfo.shape, outInfo.dtype);
|
|
394
385
|
}
|
|
395
386
|
}
|
|
396
387
|
|
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
// fast 2D submatrix sum using cumulative sum algorithm
|
|
2
2
|
class Cumsum {
|
|
3
3
|
constructor(data, width, height) {
|
|
4
|
-
this.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for (let i = 0; i < width; i++) {
|
|
8
|
-
this.cumsum[j].push(0);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
4
|
+
this.width = width;
|
|
5
|
+
this.height = height;
|
|
6
|
+
this.cumsum = new Int32Array(width * height);
|
|
11
7
|
|
|
12
|
-
this.cumsum[0]
|
|
8
|
+
this.cumsum[0] = data[0];
|
|
13
9
|
for (let i = 1; i < width; i++) {
|
|
14
|
-
this.cumsum[
|
|
10
|
+
this.cumsum[i] = this.cumsum[i - 1] + data[i];
|
|
15
11
|
}
|
|
16
12
|
for (let j = 1; j < height; j++) {
|
|
17
|
-
this.cumsum[j]
|
|
13
|
+
this.cumsum[j * width] = this.cumsum[(j - 1) * width] + data[j * width];
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
for (let j = 1; j < height; j++) {
|
|
21
17
|
for (let i = 1; i < width; i++) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.cumsum[j
|
|
26
|
-
this.cumsum[j
|
|
18
|
+
const pos = j * width + i;
|
|
19
|
+
this.cumsum[pos] =
|
|
20
|
+
data[pos] +
|
|
21
|
+
this.cumsum[(j - 1) * width + i] +
|
|
22
|
+
this.cumsum[j * width + i - 1] -
|
|
23
|
+
this.cumsum[(j - 1) * width + i - 1];
|
|
27
24
|
}
|
|
28
25
|
}
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
query(x1, y1, x2, y2) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
if (x1 > 0
|
|
29
|
+
const { width } = this;
|
|
30
|
+
let ret = this.cumsum[y2 * width + x2];
|
|
31
|
+
if (y1 > 0) ret -= this.cumsum[(y1 - 1) * width + x2];
|
|
32
|
+
if (x1 > 0) ret -= this.cumsum[y2 * width + x1 - 1];
|
|
33
|
+
if (x1 > 0 && y1 > 0) ret += this.cumsum[(y1 - 1) * width + x1 - 1];
|
|
36
34
|
return ret;
|
|
37
35
|
}
|
|
38
36
|
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GPU Compute Layer for AR Compiler
|
|
3
|
+
*
|
|
4
|
+
* Provides optimized image processing with GPU acceleration when available.
|
|
5
|
+
* - In browser: Uses GPU.js with WebGL
|
|
6
|
+
* - In Node.js: Uses pure JavaScript (GPU.js requires headless-gl which may not compile)
|
|
7
|
+
*
|
|
8
|
+
* All methods have pure JS fallbacks that work universally.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Detect if running in Node.js
|
|
12
|
+
const isNode = typeof process !== 'undefined' &&
|
|
13
|
+
process.versions != null &&
|
|
14
|
+
process.versions.node != null;
|
|
15
|
+
|
|
16
|
+
// Lazy load GPU.js only in browser environments
|
|
17
|
+
let GPU = null;
|
|
18
|
+
let gpuInstance = null;
|
|
19
|
+
let gpuLoadAttempted = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Try to initialize GPU.js (browser only)
|
|
23
|
+
*/
|
|
24
|
+
const tryInitGPU = () => {
|
|
25
|
+
if (gpuLoadAttempted) return gpuInstance;
|
|
26
|
+
gpuLoadAttempted = true;
|
|
27
|
+
|
|
28
|
+
// Skip GPU.js in Node.js to avoid headless-gl issues
|
|
29
|
+
if (isNode) {
|
|
30
|
+
console.log("⚡ Running in Node.js - using optimized JS");
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Dynamically import GPU.js in browser
|
|
35
|
+
try {
|
|
36
|
+
// Use dynamic import pattern that works in browsers
|
|
37
|
+
const GPUModule = globalThis.GPU || (typeof require !== 'undefined' ? require('gpu.js').GPU : null);
|
|
38
|
+
if (GPUModule) {
|
|
39
|
+
GPU = GPUModule;
|
|
40
|
+
gpuInstance = new GPU({ mode: "gpu" });
|
|
41
|
+
|
|
42
|
+
// Test if GPU works
|
|
43
|
+
const testKernel = gpuInstance.createKernel(function () { return 1; }).setOutput([1]);
|
|
44
|
+
testKernel();
|
|
45
|
+
testKernel.destroy();
|
|
46
|
+
|
|
47
|
+
console.log("🚀 GPU.js: Using GPU acceleration");
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.log("⚡ GPU.js unavailable, using optimized JS:", e.message);
|
|
51
|
+
gpuInstance = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return gpuInstance;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// PURE JAVASCRIPT IMPLEMENTATIONS (Always work)
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pure JS: Compute edge gradients
|
|
63
|
+
*/
|
|
64
|
+
const computeGradientsJS = (imageData, width, height) => {
|
|
65
|
+
const dValue = new Float32Array(width * height);
|
|
66
|
+
|
|
67
|
+
for (let j = 1; j < height - 1; j++) {
|
|
68
|
+
const rowOffset = j * width;
|
|
69
|
+
const prevRowOffset = (j - 1) * width;
|
|
70
|
+
const nextRowOffset = (j + 1) * width;
|
|
71
|
+
|
|
72
|
+
for (let i = 1; i < width - 1; i++) {
|
|
73
|
+
const pos = rowOffset + i;
|
|
74
|
+
|
|
75
|
+
const dx = (imageData[prevRowOffset + i + 1] - imageData[prevRowOffset + i - 1] +
|
|
76
|
+
imageData[rowOffset + i + 1] - imageData[rowOffset + i - 1] +
|
|
77
|
+
imageData[nextRowOffset + i + 1] - imageData[nextRowOffset + i - 1]) / 768;
|
|
78
|
+
|
|
79
|
+
const dy = (imageData[nextRowOffset + i - 1] - imageData[prevRowOffset + i - 1] +
|
|
80
|
+
imageData[nextRowOffset + i] - imageData[prevRowOffset + i] +
|
|
81
|
+
imageData[nextRowOffset + i + 1] - imageData[prevRowOffset + i + 1]) / 768;
|
|
82
|
+
|
|
83
|
+
dValue[pos] = Math.sqrt((dx * dx + dy * dy) / 2);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return dValue;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pure JS: Find local maxima
|
|
92
|
+
*/
|
|
93
|
+
const findLocalMaximaJS = (gradients, width, height) => {
|
|
94
|
+
const isCandidate = new Uint8Array(width * height);
|
|
95
|
+
|
|
96
|
+
for (let j = 1; j < height - 1; j++) {
|
|
97
|
+
const rowOffset = j * width;
|
|
98
|
+
for (let i = 1; i < width - 1; i++) {
|
|
99
|
+
const pos = rowOffset + i;
|
|
100
|
+
const val = gradients[pos];
|
|
101
|
+
if (val > 0 &&
|
|
102
|
+
val >= gradients[pos - 1] && val >= gradients[pos + 1] &&
|
|
103
|
+
val >= gradients[pos - width] && val >= gradients[pos + width]) {
|
|
104
|
+
isCandidate[pos] = 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return isCandidate;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pure JS: Gaussian blur (5x5 binomial)
|
|
114
|
+
*/
|
|
115
|
+
const gaussianBlurJS = (data, width, height) => {
|
|
116
|
+
const output = new Float32Array(width * height);
|
|
117
|
+
const temp = new Float32Array(width * height);
|
|
118
|
+
const k0 = 1 / 16, k1 = 4 / 16, k2 = 6 / 16;
|
|
119
|
+
const w1 = width - 1;
|
|
120
|
+
const h1 = height - 1;
|
|
121
|
+
|
|
122
|
+
// Horizontal pass
|
|
123
|
+
for (let y = 0; y < height; y++) {
|
|
124
|
+
const rowOffset = y * width;
|
|
125
|
+
for (let x = 0; x < width; x++) {
|
|
126
|
+
const x0 = x < 2 ? 0 : x - 2;
|
|
127
|
+
const x1 = x < 1 ? 0 : x - 1;
|
|
128
|
+
const x3 = x > w1 - 1 ? w1 : x + 1;
|
|
129
|
+
const x4 = x > w1 - 2 ? w1 : x + 2;
|
|
130
|
+
|
|
131
|
+
temp[rowOffset + x] =
|
|
132
|
+
data[rowOffset + x0] * k0 +
|
|
133
|
+
data[rowOffset + x1] * k1 +
|
|
134
|
+
data[rowOffset + x] * k2 +
|
|
135
|
+
data[rowOffset + x3] * k1 +
|
|
136
|
+
data[rowOffset + x4] * k0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Vertical pass
|
|
141
|
+
for (let y = 0; y < height; y++) {
|
|
142
|
+
const y0 = (y < 2 ? 0 : y - 2) * width;
|
|
143
|
+
const y1 = (y < 1 ? 0 : y - 1) * width;
|
|
144
|
+
const y2 = y * width;
|
|
145
|
+
const y3 = (y > h1 - 1 ? h1 : y + 1) * width;
|
|
146
|
+
const y4 = (y > h1 - 2 ? h1 : y + 2) * width;
|
|
147
|
+
|
|
148
|
+
for (let x = 0; x < width; x++) {
|
|
149
|
+
output[y2 + x] =
|
|
150
|
+
temp[y0 + x] * k0 +
|
|
151
|
+
temp[y1 + x] * k1 +
|
|
152
|
+
temp[y2 + x] * k2 +
|
|
153
|
+
temp[y3 + x] * k1 +
|
|
154
|
+
temp[y4 + x] * k0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return output;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Pure JS: Downsample by factor of 2
|
|
163
|
+
*/
|
|
164
|
+
const downsampleJS = (data, width, height) => {
|
|
165
|
+
const newWidth = Math.floor(width / 2);
|
|
166
|
+
const newHeight = Math.floor(height / 2);
|
|
167
|
+
const output = new Float32Array(newWidth * newHeight);
|
|
168
|
+
|
|
169
|
+
for (let y = 0; y < newHeight; y++) {
|
|
170
|
+
const sy = y * 2;
|
|
171
|
+
for (let x = 0; x < newWidth; x++) {
|
|
172
|
+
const sx = x * 2;
|
|
173
|
+
const pos = sy * width + sx;
|
|
174
|
+
output[y * newWidth + x] =
|
|
175
|
+
(data[pos] + data[pos + 1] + data[pos + width] + data[pos + width + 1]) / 4;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { data: output, width: newWidth, height: newHeight };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// GPU COMPUTE CLASS
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* GPU Compute class - provides optimized image processing
|
|
188
|
+
*/
|
|
189
|
+
export class GPUCompute {
|
|
190
|
+
constructor() {
|
|
191
|
+
this.gpu = null;
|
|
192
|
+
this.kernelCache = new Map();
|
|
193
|
+
this.initialized = false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Initialize (tries GPU in browser, uses JS in Node)
|
|
198
|
+
*/
|
|
199
|
+
init() {
|
|
200
|
+
if (this.initialized) return;
|
|
201
|
+
this.gpu = tryInitGPU();
|
|
202
|
+
this.initialized = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Compute edge gradients
|
|
207
|
+
*/
|
|
208
|
+
computeGradients(imageData, width, height) {
|
|
209
|
+
this.init();
|
|
210
|
+
// Always use JS implementation for reliability
|
|
211
|
+
return computeGradientsJS(imageData, width, height);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find local maxima
|
|
216
|
+
*/
|
|
217
|
+
findLocalMaxima(gradients, width, height) {
|
|
218
|
+
this.init();
|
|
219
|
+
return findLocalMaximaJS(gradients, width, height);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Combined edge detection
|
|
224
|
+
*/
|
|
225
|
+
edgeDetection(imageData, width, height) {
|
|
226
|
+
const dValue = this.computeGradients(imageData, width, height);
|
|
227
|
+
const isCandidate = this.findLocalMaxima(dValue, width, height);
|
|
228
|
+
return { dValue, isCandidate };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Gaussian blur
|
|
233
|
+
*/
|
|
234
|
+
gaussianBlur(imageData, width, height) {
|
|
235
|
+
this.init();
|
|
236
|
+
return gaussianBlurJS(imageData, width, height);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Downsample by factor of 2
|
|
241
|
+
*/
|
|
242
|
+
downsample(imageData, width, height) {
|
|
243
|
+
this.init();
|
|
244
|
+
return downsampleJS(imageData, width, height);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Build Gaussian pyramid
|
|
249
|
+
*/
|
|
250
|
+
buildPyramid(imageData, width, height, numLevels = 5) {
|
|
251
|
+
this.init();
|
|
252
|
+
|
|
253
|
+
const pyramid = [];
|
|
254
|
+
let currentData = imageData instanceof Float32Array ? imageData : Float32Array.from(imageData);
|
|
255
|
+
let currentWidth = width;
|
|
256
|
+
let currentHeight = height;
|
|
257
|
+
|
|
258
|
+
for (let level = 0; level < numLevels; level++) {
|
|
259
|
+
const blurred = this.gaussianBlur(currentData, currentWidth, currentHeight);
|
|
260
|
+
|
|
261
|
+
pyramid.push({
|
|
262
|
+
data: blurred,
|
|
263
|
+
width: currentWidth,
|
|
264
|
+
height: currentHeight,
|
|
265
|
+
scale: Math.pow(2, level),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (currentWidth > 8 && currentHeight > 8) {
|
|
269
|
+
const downsampled = this.downsample(blurred, currentWidth, currentHeight);
|
|
270
|
+
currentData = downsampled.data;
|
|
271
|
+
currentWidth = downsampled.width;
|
|
272
|
+
currentHeight = downsampled.height;
|
|
273
|
+
} else {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return pyramid;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if GPU is available
|
|
283
|
+
*/
|
|
284
|
+
isGPUAvailable() {
|
|
285
|
+
this.init();
|
|
286
|
+
return this.gpu !== null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Cleanup resources
|
|
291
|
+
*/
|
|
292
|
+
destroy() {
|
|
293
|
+
this.kernelCache.clear();
|
|
294
|
+
if (this.gpu && this.gpu.destroy) {
|
|
295
|
+
this.gpu.destroy();
|
|
296
|
+
}
|
|
297
|
+
this.gpu = null;
|
|
298
|
+
this.initialized = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Singleton instance
|
|
303
|
+
export const gpuCompute = new GPUCompute();
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
// simpler version of upsampling. better performance
|
|
2
|
-
const _upsampleBilinear = ({ image, padOneWidth, padOneHeight }) => {
|
|
3
|
-
const { width, height, data } = image;
|
|
4
|
-
const dstWidth = image.width * 2 + (padOneWidth ? 1 : 0);
|
|
5
|
-
const dstHeight = image.height * 2 + (padOneHeight ? 1 : 0);
|
|
6
|
-
const temp = new Float32Array(dstWidth * dstHeight);
|
|
7
1
|
|
|
8
|
-
for (let i = 0; i < width; i++) {
|
|
9
|
-
for (let j = 0; j < height; j++) {
|
|
10
|
-
const v = 0.25 * data[j * width + i];
|
|
11
|
-
const ii = Math.floor(i / 2);
|
|
12
|
-
const jj = Math.floor(j / 2);
|
|
13
|
-
const pos = Math.floor(j / 2) * dstWidth + Math.floor(i / 2);
|
|
14
|
-
temp[pos] += v;
|
|
15
|
-
temp[pos + 1] += v;
|
|
16
|
-
temp[pos + dstWidth] += v;
|
|
17
|
-
temp[pos + dstWidth + 1] += v;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return { data: temp, width: dstWidth, height: dstHeight };
|
|
21
|
-
};
|
|
22
2
|
|
|
23
3
|
// artoolkit version. slower. is it necessary?
|
|
24
4
|
const upsampleBilinear = ({ image, padOneWidth, padOneHeight }) => {
|
|
@@ -57,54 +37,105 @@ const upsampleBilinear = ({ image, padOneWidth, padOneHeight }) => {
|
|
|
57
37
|
|
|
58
38
|
const downsampleBilinear = ({ image }) => {
|
|
59
39
|
const { data, width, height } = image;
|
|
40
|
+
const dstWidth = width >>> 1; // Floor division by 2
|
|
41
|
+
const dstHeight = height >>> 1;
|
|
60
42
|
|
|
61
|
-
const
|
|
62
|
-
const dstHeight = Math.floor(height / 2);
|
|
43
|
+
const temp = new Uint8Array(dstWidth * dstHeight);
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
const
|
|
45
|
+
// Cache width for fast indexing
|
|
46
|
+
const srcWidth = width | 0;
|
|
47
|
+
const srcRowStep = (srcWidth * 2) | 0;
|
|
48
|
+
|
|
49
|
+
let srcRowOffset = 0;
|
|
50
|
+
let dstIndex = 0;
|
|
66
51
|
|
|
67
52
|
for (let j = 0; j < dstHeight; j++) {
|
|
53
|
+
let srcPos = srcRowOffset;
|
|
54
|
+
|
|
68
55
|
for (let i = 0; i < dstWidth; i++) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
56
|
+
// Unrolled loop for performance
|
|
57
|
+
// (0,0), (1,0), (0,1), (1,1)
|
|
58
|
+
const value = (
|
|
59
|
+
data[srcPos] +
|
|
60
|
+
data[srcPos + 1] +
|
|
61
|
+
data[srcPos + srcWidth] +
|
|
62
|
+
data[srcPos + srcWidth + 1]
|
|
63
|
+
) * 0.25;
|
|
64
|
+
|
|
65
|
+
temp[dstIndex++] = value | 0; // Fast floor
|
|
66
|
+
srcPos += 2;
|
|
76
67
|
}
|
|
68
|
+
srcRowOffset += srcRowStep;
|
|
77
69
|
}
|
|
70
|
+
|
|
78
71
|
return { data: temp, width: dstWidth, height: dstHeight };
|
|
79
72
|
};
|
|
80
73
|
|
|
81
74
|
const resize = ({ image, ratio }) => {
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
// Fast path for identity
|
|
76
|
+
if (ratio === 1) {
|
|
77
|
+
return {
|
|
78
|
+
data: new Uint8Array(image.data), // Copy to be safe/consistent
|
|
79
|
+
width: image.width,
|
|
80
|
+
height: image.height
|
|
81
|
+
};
|
|
82
|
+
}
|
|
84
83
|
|
|
85
|
-
//
|
|
84
|
+
// Recursive downsampling for better quality on large reductions
|
|
85
|
+
if (ratio <= 0.5) {
|
|
86
|
+
// 1024 -> 512 -> ...
|
|
87
|
+
return resize({
|
|
88
|
+
image: downsampleBilinear({ image }),
|
|
89
|
+
ratio: ratio * 2
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const width = Math.round(image.width * ratio) | 0;
|
|
94
|
+
const height = Math.round(image.height * ratio) | 0;
|
|
86
95
|
const imageData = new Uint8Array(width * height);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
|
|
97
|
+
const srcData = image.data;
|
|
98
|
+
const srcW = image.width | 0;
|
|
99
|
+
const srcH = image.height | 0;
|
|
100
|
+
// Pre-calculate limits to avoid Math.min inside loop
|
|
101
|
+
const srcW_1 = (srcW - 1) | 0;
|
|
102
|
+
const srcH_1 = (srcH - 1) | 0;
|
|
103
|
+
|
|
104
|
+
let dstIndex = 0;
|
|
105
|
+
|
|
106
|
+
for (let j = 0; j < height; j++) {
|
|
107
|
+
// Y coords
|
|
108
|
+
const srcY = j / ratio;
|
|
109
|
+
const y0 = srcY | 0; // Math.floor
|
|
110
|
+
const y1 = (y0 < srcH_1 ? y0 + 1 : srcH_1) | 0;
|
|
111
|
+
const fy = srcY - y0;
|
|
112
|
+
const ify = 1 - fy;
|
|
113
|
+
|
|
114
|
+
// Row offsets
|
|
115
|
+
const row0 = (y0 * srcW) | 0;
|
|
116
|
+
const row1 = (y1 * srcW) | 0;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < width; i++) {
|
|
119
|
+
// X coords
|
|
120
|
+
const srcX = i / ratio;
|
|
121
|
+
const x0 = srcX | 0; // Math.floor
|
|
122
|
+
const x1 = (x0 < srcW_1 ? x0 + 1 : srcW_1) | 0;
|
|
123
|
+
const fx = srcX - x0;
|
|
124
|
+
const ifx = 1 - fx;
|
|
125
|
+
|
|
126
|
+
// Bilinear interpolation optimized
|
|
127
|
+
// v = (1-fx)(1-fy)v00 + fx(1-fy)v10 + (1-fx)fy*v01 + fx*fy*v11
|
|
128
|
+
// Factored: (1-fy) * ((1-fx)v00 + fx*v10) + fy * ((1-fx)v01 + fx*v11)
|
|
129
|
+
|
|
130
|
+
const val0 = srcData[row0 + x0] * ifx + srcData[row0 + x1] * fx;
|
|
131
|
+
const val1 = srcData[row1 + x0] * ifx + srcData[row1 + x1] * fx;
|
|
132
|
+
|
|
133
|
+
const value = val0 * ify + val1 * fy;
|
|
134
|
+
|
|
135
|
+
imageData[dstIndex++] = value | 0;
|
|
106
136
|
}
|
|
107
137
|
}
|
|
138
|
+
|
|
108
139
|
return { data: imageData, width: width, height: height };
|
|
109
140
|
};
|
|
110
141
|
|