diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..70126e2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Normalize all text files to LF, including in Windows working trees. +# Prettier (endOfLine: lf, its default) and the CI format-check assume LF; +# without this, Windows checkouts get CRLF and local format-checks disagree +# with CI. +* text=auto eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 742c368..9292adf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,8 +2,8 @@ The full guidance is in **[`AGENTS.md`](../AGENTS.md)** (source of truth) and the roadmap in [`docs/jsfeat-parity-and-refactor-audit.md`](../docs/jsfeat-parity-and-refactor-audit.md). Critical points, inlined because Copilot injects this file directly: -- **TypeScript port of [jsfeat](https://github.com/inspirit/jsfeat)** for WebARKit. npm: `@webarkit/jsfeat-next`. Node v20.18.0. -- Build: `npm run build-ts` → `dist/jsfeatNext.js` (UMD) + `types/`. Watch: `npm run dev-ts`. Format: `npm run format` (Prettier). **No test suite** — verify via `examples/*.html`. +- **TypeScript port of [jsfeat](https://github.com/inspirit/jsfeat)** for WebARKit. npm: `@webarkit/jsfeat-next`. Node v24.18.0 (npm 11). +- Build: `npm run build-ts` → `dist/jsfeatNext.js` (UMD) + `types/`. Watch: `npm run dev-ts`. Format: `npm run format` (Prettier). **Tests:** `npm test` (Vitest, parity vs original jsfeat); also verify via `examples/*.html`. - **Architecture:** most algorithms live **inline in `src/jsfeatNext.ts` (~3,900 lines)**, attached as `jsfeatNext.X = class X extends jsfeatNext`. Edit *there* for `imgproc, fast_corners, math, linalg, orb, yape06, motion_estimator, optical_flow_lk, pyramid_t`. - **⚠️ Trap:** several `src//.ts` files are **type-only stubs** that `throw new Error("Method not implemented.")` (e.g. `src/imgproc/imgproc.ts`). Do not treat them as the implementation and never instantiate them. - **API notes:** consumers use `jsfeatNext.jsfeatNext` (double namespace); algorithm modules require `new` (instance methods), unlike jsfeat's static namespace; each `new` allocates its own cache. `haar` and `bbf` are not ported. diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9a51bd9..d3f6134 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,8 +30,7 @@ jobs: ${{ runner.os }}-node- - name: Install - run: | - npm install + run: npm ci - name: Format check run: npm run format-check diff --git a/.nvmrc b/.nvmrc index 2162b3c..72753e1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.18.0 \ No newline at end of file +v24.18.0 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 75c6ac4..3e0f95c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,12 +9,12 @@ ## Environment & commands -- **Node:** v20.18.0 (see `.nvmrc`). **Package manager:** npm. +- **Node:** v24.18.0 (see `.nvmrc`; npm 11). **Package manager:** npm. - Install: `npm install` - Build (prod): `npm run build-ts` → runs `tsc` (emits `.d.ts` to `types/`) then webpack → `dist/jsfeatNext.js` - Watch/dev: `npm run dev-ts` - Format: `npm run format` (write) · `npm run format-check` (verify) — Prettier, config in `.prettierrc.json` -- **Tests:** none yet. The only verification is manual: open `examples/*.html` in a browser after building. Do **not** claim behavior is verified without a real check. +- **Test:** `npm test` (Vitest) runs characterization tests asserting parity against the original `jsfeat` (see `tests/`). Also verify visually via `examples/*.html` after building. Do **not** claim behavior is verified without a real check. ## Architecture — read this before editing diff --git a/examples/js/compatibility.js b/examples/js/compatibility.js index fa94107..190d102 100644 --- a/examples/js/compatibility.js +++ b/examples/js/compatibility.js @@ -1,71 +1,71 @@ -/** -* this code is from all around the web :) -* if u want to put some credits u are welcome! -*/ -var compatibility = (function() { - var lastTime = 0, - isLittleEndian = true, - - URL = window.URL || window.webkitURL, - - requestAnimationFrame = function(callback, element) { - var requestAnimationFrame = - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = window.setTimeout(function() { - callback(currTime + timeToCall); - }, timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - - return requestAnimationFrame.call(window, callback, element); - }, - - cancelAnimationFrame = function(id) { - var cancelAnimationFrame = window.cancelAnimationFrame || - function(id) { - clearTimeout(id); - }; - return cancelAnimationFrame.call(window, id); - }, - - getUserMedia = function(options, success, error) { - var getUserMedia = - window.navigator.getUserMedia || - window.navigator.mozGetUserMedia || - window.navigator.webkitGetUserMedia || - window.navigator.msGetUserMedia || - function(options, success, error) { - error(); - }; - - return getUserMedia.call(window.navigator, options, success, error); - }, - - detectEndian = function() { - var buf = new ArrayBuffer(8); - var data = new Uint32Array(buf); - data[0] = 0xff000000; - isLittleEndian = true; - if (buf[0] === 0xff) { - isLittleEndian = false; - } - return isLittleEndian; - }; - - return { - URL: URL, - requestAnimationFrame: requestAnimationFrame, - cancelAnimationFrame: cancelAnimationFrame, - getUserMedia: getUserMedia, - detectEndian: detectEndian, - isLittleEndian: isLittleEndian - }; +/** +* this code is from all around the web :) +* if u want to put some credits u are welcome! +*/ +var compatibility = (function() { + var lastTime = 0, + isLittleEndian = true, + + URL = window.URL || window.webkitURL, + + requestAnimationFrame = function(callback, element) { + var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + return requestAnimationFrame.call(window, callback, element); + }, + + cancelAnimationFrame = function(id) { + var cancelAnimationFrame = window.cancelAnimationFrame || + function(id) { + clearTimeout(id); + }; + return cancelAnimationFrame.call(window, id); + }, + + getUserMedia = function(options, success, error) { + var getUserMedia = + window.navigator.getUserMedia || + window.navigator.mozGetUserMedia || + window.navigator.webkitGetUserMedia || + window.navigator.msGetUserMedia || + function(options, success, error) { + error(); + }; + + return getUserMedia.call(window.navigator, options, success, error); + }, + + detectEndian = function() { + var buf = new ArrayBuffer(8); + var data = new Uint32Array(buf); + data[0] = 0xff000000; + isLittleEndian = true; + if (buf[0] === 0xff) { + isLittleEndian = false; + } + return isLittleEndian; + }; + + return { + URL: URL, + requestAnimationFrame: requestAnimationFrame, + cancelAnimationFrame: cancelAnimationFrame, + getUserMedia: getUserMedia, + detectEndian: detectEndian, + isLittleEndian: isLittleEndian + }; })(); \ No newline at end of file diff --git a/examples/js/profiler.js b/examples/js/profiler.js index 91fc51e..4b64ad1 100644 --- a/examples/js/profiler.js +++ b/examples/js/profiler.js @@ -1,140 +1,140 @@ -var stopwatch = (function() { - "use strict"; - // - function stopwatch() { - this.start_time = 0; - this.stop_time = 0; - this.run_time = 0; - this.running = false; - } - - stopwatch.prototype.start = function() { - this.start_time = new Date().getTime(); - this.running = true; - } - - stopwatch.prototype.stop = function() { - this.stop_time = new Date().getTime(); - this.run_time = (this.stop_time - this.start_time); - this.running = false; - } - - stopwatch.prototype.get_runtime = function() { - return this.run_time; - } - - stopwatch.prototype.reset = function() { - this.run_time = 0; - } - - return stopwatch; -})(); - -var ring_buffer = (function() { - "use strict"; - - function ring_buffer(size) { - this.arr = new Int32Array(size); - this.begin = 0; - this.end = -1; - this.num_el = 0; - this.arr_size = size; - } - - ring_buffer.prototype.push_back = function(elem) { - if (this.num_el= 1) { - this.frame_timer.stop(); - ringbuff.push_back(this.frame_timer.get_runtime()); - var size = ringbuff.size(); - var sum = 0; - for(i = 0; i < size; ++i) { - sum += ringbuff.get(i); - } - this.fps = size / sum * 1000; - this.frame_timer.start(); - } - } - - profiler.prototype.find_task = function(subj) { - var n = this.timers.length | 0; - var i = 0; - for(i = 0; i < n; ++i) { - var pair = this.timers[i]; - if(pair[0] === subj) { - return pair; - } - } - return null; - } - - profiler.prototype.start = function(subj) { - var task = this.find_task(subj); - task[1].start(); - } - - profiler.prototype.stop = function(subj) { - var task = this.find_task(subj); - task[1].stop(); - } - - profiler.prototype.log = function() { - var n = this.timers.length | 0; - var i = 0; - var str = "FPS: " + this.fps.toFixed(2) + ""; - for(i = 0; i < n; ++i) { - var pair = this.timers[i]; - str += "
" + pair[0] + ": " + pair[1].get_runtime() + "ms"; - } - return str; - } - - return profiler; +var stopwatch = (function() { + "use strict"; + // + function stopwatch() { + this.start_time = 0; + this.stop_time = 0; + this.run_time = 0; + this.running = false; + } + + stopwatch.prototype.start = function() { + this.start_time = new Date().getTime(); + this.running = true; + } + + stopwatch.prototype.stop = function() { + this.stop_time = new Date().getTime(); + this.run_time = (this.stop_time - this.start_time); + this.running = false; + } + + stopwatch.prototype.get_runtime = function() { + return this.run_time; + } + + stopwatch.prototype.reset = function() { + this.run_time = 0; + } + + return stopwatch; +})(); + +var ring_buffer = (function() { + "use strict"; + + function ring_buffer(size) { + this.arr = new Int32Array(size); + this.begin = 0; + this.end = -1; + this.num_el = 0; + this.arr_size = size; + } + + ring_buffer.prototype.push_back = function(elem) { + if (this.num_el= 1) { + this.frame_timer.stop(); + ringbuff.push_back(this.frame_timer.get_runtime()); + var size = ringbuff.size(); + var sum = 0; + for(i = 0; i < size; ++i) { + sum += ringbuff.get(i); + } + this.fps = size / sum * 1000; + this.frame_timer.start(); + } + } + + profiler.prototype.find_task = function(subj) { + var n = this.timers.length | 0; + var i = 0; + for(i = 0; i < n; ++i) { + var pair = this.timers[i]; + if(pair[0] === subj) { + return pair; + } + } + return null; + } + + profiler.prototype.start = function(subj) { + var task = this.find_task(subj); + task[1].start(); + } + + profiler.prototype.stop = function(subj) { + var task = this.find_task(subj); + task[1].stop(); + } + + profiler.prototype.log = function() { + var n = this.timers.length | 0; + var i = 0; + var str = "FPS: " + this.fps.toFixed(2) + ""; + for(i = 0; i < n; ++i) { + var pair = this.timers[i]; + str += "
" + pair[0] + ": " + pair[1].get_runtime() + "ms"; + } + return str; + } + + return profiler; })(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 83dd3de..f74f643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "LGPL-3.0-or-later", "dependencies": { "@babel/runtime": "^7.26.9", - "prettier": "^3.5.1", + "prettier": "3.5.1", "ts-loader": "^9.5.2" }, "devDependencies": { @@ -20,6 +20,7 @@ "babel-loader": "^9.2.1", "rimraf": "^6.0.1", "typescript": "^5.7.3", + "vitest": "^4.1.9", "webpack": "5.98.0", "webpack-cli": "6.0.1" } @@ -1586,6 +1587,40 @@ "node": ">=14.17.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1657,9 +1692,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1671,6 +1707,335 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1710,6 +2075,119 @@ "undici-types": "~6.20.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -1985,6 +2463,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -2129,6 +2617,16 @@ } ] }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -2238,6 +2736,16 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2336,6 +2844,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2354,6 +2872,16 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2527,6 +3055,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -2758,16 +3301,277 @@ "json5": "lib/cli.js" }, "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/loader-runner": { @@ -2797,6 +3601,16 @@ "dev": true, "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2866,6 +3680,25 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -2876,6 +3709,20 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -2970,6 +3817,13 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2999,6 +3853,35 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prettier": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", @@ -3181,6 +4064,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3300,6 +4217,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3322,6 +4246,16 @@ "node": ">= 8" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -3339,6 +4273,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3527,6 +4475,81 @@ } } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3610,6 +4633,14 @@ "node": ">=8" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -3709,6 +4740,207 @@ "punycode": "^2.1.0" } }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -3855,6 +5087,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index a7bc5da..78c8965 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "build-ts": "rimraf ./build && tsc && webpack --mode production", "dev-ts": "rimraf ./build && tsc && webpack --mode development --progress --watch", "format-check": "prettier --check .", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest" }, "repository": { "type": "git", @@ -36,12 +38,13 @@ "babel-loader": "^9.2.1", "rimraf": "^6.0.1", "typescript": "^5.7.3", + "vitest": "^4.1.9", "webpack": "5.98.0", "webpack-cli": "6.0.1" }, "dependencies": { "@babel/runtime": "^7.26.9", - "prettier": "^3.5.1", + "prettier": "3.5.1", "ts-loader": "^9.5.2" } -} \ No newline at end of file +} diff --git a/tests/jsfeat.d.ts b/tests/jsfeat.d.ts new file mode 100644 index 0000000..f5993a2 --- /dev/null +++ b/tests/jsfeat.d.ts @@ -0,0 +1,4 @@ +// The original jsfeat (vendored under tests/vendor/) is used only as a +// golden-output *oracle* in the characterization tests. It ships no type +// declarations, so we declare it loosely. +declare module "*oracle.cjs"; diff --git a/tests/parity/detectors.test.ts b/tests/parity/detectors.test.ts new file mode 100644 index 0000000..565f1ef --- /dev/null +++ b/tests/parity/detectors.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import { yape } from "../../src/yape/yape"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45) + * for the feature detectors/descriptors/tracker: + * fast_corners, yape06, yape, orb.describe, optical_flow_lk.track. + * + * API parity note (Axis 2): all of these are STATIC namespaces in the + * original jsfeat but INSTANCE classes in jsfeatNext (except yape, which is + * instantiated in both — original: new jsfeat.yape()). + */ + +const W = 96; +const H = 72; +const U8C1 = jsfeatNext.U8_t | jsfeatNext.C1_t; +const OU8C1 = jsfeat.U8_t | jsfeat.C1_t; + +/** deterministic PRNG (mulberry32) for reproducible shape placement */ +function rng(seed: number): () => number { + let a = seed >>> 0; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** + * Deterministic corner-rich image: bright squares of varying size/intensity + * scattered on a dark gradient (isolated square corners are ideal FAST/YAPE + * material). shiftX/shiftY translate the shapes (for optical-flow frames). + */ +function grayPair(shiftX = 0, shiftY = 0) { + const next = new jsfeatNext.matrix_t(W, H, U8C1); + const orig = new jsfeat.matrix_t(W, H, OU8C1); + const px = new Uint8Array(W * H); + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + px[y * W + x] = 20 + ((x + y) & 7); // dark textured base + } + } + const rand = rng(99); + for (let s = 0; s < 25; s++) { + const cx = 8 + Math.floor(rand() * (W - 24)) + shiftX; + const cy = 8 + Math.floor(rand() * (H - 24)) + shiftY; + const size = 3 + Math.floor(rand() * 6); + const val = 120 + Math.floor(rand() * 130); + for (let y = cy; y < Math.min(H, cy + size); y++) { + for (let x = cx; x < Math.min(W, cx + size); x++) { + if (x >= 0 && y >= 0) px[y * W + x] = val; + } + } + } + next.data.set(px); + orig.data.set(px); + return { next, orig }; +} + +function makeCorners(n: number) { + const nextC = Array.from({ length: n }, () => new jsfeatNext.keypoint_t(0, 0, 0, 0, -1)); + const origC = Array.from({ length: n }, () => new jsfeat.keypoint_t(0, 0, 0, 0, -1)); + return { nextC, origC }; +} + +function expectSameCorners( + nextC: { x: number; y: number; score: number }[], + origC: { x: number; y: number; score: number }[], + count: number +) { + for (let i = 0; i < count; i++) { + expect(nextC[i].x).toBe(origC[i].x); + expect(nextC[i].y).toBe(origC[i].y); + expect(nextC[i].score).toBeCloseTo(origC[i].score, 5); + } +} + +describe("parity: fast_corners vs original jsfeat.fast_corners", () => { + it("set_threshold + detect find identical corners", () => { + const { next, orig } = grayPair(); + const { nextC, origC } = makeCorners(W * H); + + const fc = new jsfeatNext.fast_corners(); + fc.set_threshold(20); + jsfeat.fast_corners.set_threshold(20); + + const nN = fc.detect(next, nextC, 3); + const nO = jsfeat.fast_corners.detect(orig, origC, 3); + + expect(nN).toBe(nO); + expect(nN).toBeGreaterThan(0); + expectSameCorners(nextC, origC, nN); + }); +}); + +describe("parity: yape06 vs original jsfeat.yape06", () => { + it("detect finds identical keypoints (default thresholds)", () => { + const { next, orig } = grayPair(); + const { nextC, origC } = makeCorners(W * H); + + const y06 = new jsfeatNext.yape06(); + // original is a static namespace with mutable thresholds; align them + jsfeat.yape06.laplacian_threshold = y06.laplacian_threshold; + jsfeat.yape06.min_eigen_value_threshold = y06.min_eigen_value_threshold; + + const nN = y06.detect(next, nextC, 5); + const nO = jsfeat.yape06.detect(orig, origC, 5); + + expect(nN).toBe(nO); + expect(nN).toBeGreaterThan(0); + expectSameCorners(nextC, origC, nN); + }); +}); + +describe("parity: yape vs original jsfeat.yape", () => { + it("init + detect find identical keypoints", () => { + const { next, orig } = grayPair(); + const { nextC, origC } = makeCorners(W * H); + + // API divergence (Axis 2): original jsfeat.yape is a STATIC namespace + // (jsfeat.yape.init(...)), jsfeatNext's yape is a class to instantiate. + const yN = new yape(); + yN.init(W, H, 5, 1); + jsfeat.yape.init(W, H, 5, 1); + + const nN = yN.detect(next, nextC, 4); + const nO = jsfeat.yape.detect(orig, origC, 4); + + expect(nN).toBe(nO); + expect(nN).toBeGreaterThan(0); + expectSameCorners(nextC, origC, nN); + }); +}); + +describe("parity: orb.describe vs original jsfeat.orb", () => { + it("produces identical 32-byte descriptors for identical corners", () => { + const { next, orig } = grayPair(); + + // corners from the (already parity-verified) FAST detector + const { nextC, origC } = makeCorners(W * H); + const fc = new jsfeatNext.fast_corners(); + fc.set_threshold(20); + jsfeat.fast_corners.set_threshold(20); + const count = fc.detect(next, nextC, 20); // generous border for 32px patches + const countO = jsfeat.fast_corners.detect(orig, origC, 20); + expect(count).toBe(countO); + expect(count).toBeGreaterThan(0); + // deterministic angles (orb.describe reads corner.angle) + for (let i = 0; i < count; i++) { + const ang = ((i * 37) % 360) * (Math.PI / 180); + nextC[i].angle = ang; + origC[i].angle = ang; + } + + const descN = new jsfeatNext.matrix_t(32, count, U8C1); + const descO = new jsfeat.matrix_t(32, count, OU8C1); + const orb = new jsfeatNext.orb(); + orb.describe(next, nextC, count, descN); + jsfeat.orb.describe(orig, origC, count, descO); + + for (let i = 0; i < count * 32; i++) { + expect(descN.data[i]).toBe(descO.data[i]); + } + }); +}); + +describe("parity: optical_flow_lk vs original jsfeat.optical_flow_lk", () => { + it("tracks identical points across an identical shift", () => { + // 2 levels + 9px window: with more levels the top-level image + // (W>>2 = 24px) can't fit a tracking window and every point fails + const levels = 2; + const { next: prevN, orig: prevO } = grayPair(); + const { next: currN, orig: currO } = grayPair(3, 2); // shifted frame + + const prevPyrN = new jsfeatNext.pyramid_t(levels); + prevPyrN.allocate(W, H, U8C1); + prevPyrN.build(prevN, false); + const currPyrN = new jsfeatNext.pyramid_t(levels); + currPyrN.allocate(W, H, U8C1); + currPyrN.build(currN, false); + + const prevPyrO = new jsfeat.pyramid_t(levels); + prevPyrO.allocate(W, H, OU8C1); + prevPyrO.build(prevO, false); + const currPyrO = new jsfeat.pyramid_t(levels); + currPyrO.allocate(W, H, OU8C1); + currPyrO.build(currO, false); + + // track the FAST corners of the prev frame (grid points on the flat + // background fail the min-eigenvalue check on both sides — aperture + // problem — so corners are the meaningful trackable set) + const { nextC } = makeCorners(W * H); + const fc = new jsfeatNext.fast_corners(); + fc.set_threshold(20); + const nCorners = fc.detect(prevN, nextC, 16); + const pts: number[] = []; + for (let i = 0; i < nCorners; i++) { + pts.push(nextC[i].x, nextC[i].y); + } + const count = pts.length >> 1; + expect(count).toBeGreaterThan(0); + const prevXYN = Float32Array.from(pts); + const currXYN = new Float32Array(count * 2); + const statusN = new Uint8Array(count); + const prevXYO = Float32Array.from(pts); + const currXYO = new Float32Array(count * 2); + const statusO = new Uint8Array(count); + + const lk = new jsfeatNext.optical_flow_lk(); + lk.track(prevPyrN, currPyrN, prevXYN, currXYN, count, 9, 30, statusN, 0.01, 0.0001); + jsfeat.optical_flow_lk.track(prevPyrO, currPyrO, prevXYO, currXYO, count, 9, 30, statusO, 0.01, 0.0001); + + for (let i = 0; i < count; i++) { + expect(statusN[i]).toBe(statusO[i]); + if (statusN[i]) { + expect(currXYN[i * 2]).toBeCloseTo(currXYO[i * 2], 4); + expect(currXYN[i * 2 + 1]).toBeCloseTo(currXYO[i * 2 + 1], 4); + } + } + // sanity: at least some points tracked + expect(Array.from(statusN).some((s) => s === 1)).toBe(true); + }); +}); diff --git a/tests/parity/imgproc.test.ts b/tests/parity/imgproc.test.ts new file mode 100644 index 0000000..430ca54 --- /dev/null +++ b/tests/parity/imgproc.test.ts @@ -0,0 +1,236 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45) + * for imgproc. Synthetic, deterministic images; every function compared with + * the original jsfeat.imgproc on identical inputs. + */ + +const W = 64; +const H = 48; +const U8C1 = jsfeatNext.U8_t | jsfeatNext.C1_t; +const OU8C1 = jsfeat.U8_t | jsfeat.C1_t; + +const ip = new jsfeatNext.imgproc(); + +/** deterministic grayscale test pattern with edges, gradients and texture */ +function grayValue(x: number, y: number): number { + const g = (x * 3 + y * 5 + ((x * y) % 7) * 11) % 256; + // add a bright box for hard edges (canny/hough material) + return x > 20 && x < 40 && y > 15 && y < 35 ? 230 : g; +} + +function grayPair(w = W, h = H) { + const next = new jsfeatNext.matrix_t(w, h, U8C1); + const orig = new jsfeat.matrix_t(w, h, OU8C1); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const v = grayValue(x, y); + next.data[y * w + x] = v; + orig.data[y * w + x] = v; + } + } + return { next, orig }; +} + +function rgbaBuffer(w = W, h = H): Uint8Array { + const buf = new Uint8Array(w * h * 4); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const i = (y * w + x) * 4; + buf[i] = (x * 5) % 256; + buf[i + 1] = (y * 7) % 256; + buf[i + 2] = (x + y * 3) % 256; + buf[i + 3] = 255; + } + } + return buf; +} + +function expectSame(actual: ArrayLike, expected: ArrayLike, len: number, digits?: number) { + for (let i = 0; i < len; i++) { + if (digits === undefined) { + expect(actual[i]).toBe(expected[i]); + } else { + expect(actual[i]).toBeCloseTo(expected[i], digits); + } + } +} + +describe("parity: imgproc vs original jsfeat.imgproc", () => { + it("grayscale (RGBA default code)", () => { + const src = rgbaBuffer(); + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.grayscale(src, W, H, dstN); + jsfeat.imgproc.grayscale(src, W, H, dstO); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("grayscale (COLOR_BGRA2GRAY)", () => { + const src = rgbaBuffer(); + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.grayscale(src, W, H, dstN, jsfeatNext.COLOR_BGRA2GRAY); + jsfeat.imgproc.grayscale(src, W, H, dstO, jsfeat.COLOR_BGRA2GRAY); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("resample (u8 fast path)", () => { + const { next, orig } = grayPair(); + const dstN = new jsfeatNext.matrix_t(32, 24, U8C1); + const dstO = new jsfeat.matrix_t(32, 24, OU8C1); + ip.resample(next, dstN, 32, 24); + jsfeat.imgproc.resample(orig, dstO, 32, 24); + expectSame(dstN.data, dstO.data, 32 * 24); + }); + + it("box_blur_gray (default scale + NOSCALE)", () => { + const { next, orig } = grayPair(); + for (const opt of [0, jsfeatNext.BOX_BLUR_NOSCALE]) { + const dstN = new jsfeatNext.matrix_t(W, H, opt ? jsfeatNext.S32_t | jsfeatNext.C1_t : U8C1); + const dstO = new jsfeat.matrix_t(W, H, opt ? jsfeat.S32_t | jsfeat.C1_t : OU8C1); + ip.box_blur_gray(next, dstN, 2, opt); + jsfeat.imgproc.box_blur_gray(orig, dstO, 2, opt); + expectSame(dstN.data, dstO.data, W * H); + } + }); + + it("gaussian_blur", () => { + const { next, orig } = grayPair(); + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.gaussian_blur(next, dstN, 5, 1.5); + jsfeat.imgproc.gaussian_blur(orig, dstO, 5, 1.5); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("pyrdown", () => { + const { next, orig } = grayPair(); + const dstN = new jsfeatNext.matrix_t(W >> 1, H >> 1, U8C1); + const dstO = new jsfeat.matrix_t(W >> 1, H >> 1, OU8C1); + ip.pyrdown(next, dstN); + jsfeat.imgproc.pyrdown(orig, dstO); + expectSame(dstN.data, dstO.data, (W >> 1) * (H >> 1)); + }); + + it("scharr_derivatives", () => { + const { next, orig } = grayPair(); + const dstN = new jsfeatNext.matrix_t(W, H, jsfeatNext.S32C2_t); + const dstO = new jsfeat.matrix_t(W, H, jsfeat.S32C2_t); + ip.scharr_derivatives(next, dstN); + jsfeat.imgproc.scharr_derivatives(orig, dstO); + expectSame(dstN.data, dstO.data, W * H * 2); + }); + + it("sobel_derivatives", () => { + const { next, orig } = grayPair(); + const dstN = new jsfeatNext.matrix_t(W, H, jsfeatNext.S32C2_t); + const dstO = new jsfeat.matrix_t(W, H, jsfeat.S32C2_t); + ip.sobel_derivatives(next, dstN); + jsfeat.imgproc.sobel_derivatives(orig, dstO); + expectSame(dstN.data, dstO.data, W * H * 2); + }); + + it("compute_integral_image (sum + sqsum + tilted)", () => { + const { next, orig } = grayPair(); + const len = (W + 1) * (H + 1); + const sumN = new Int32Array(len); + const sqN = new Int32Array(len); + const tiltN = new Int32Array(len); + const sumO = new Int32Array(len); + const sqO = new Int32Array(len); + const tiltO = new Int32Array(len); + ip.compute_integral_image(next, sumN as never, sqN as never, tiltN as never); + jsfeat.imgproc.compute_integral_image(orig, sumO, sqO, tiltO); + expectSame(sumN, sumO, len); + expectSame(sqN, sqO, len); + expectSame(tiltN, tiltO, len); + }); + + it("equalize_histogram", () => { + const { next, orig } = grayPair(); + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.equalize_histogram(next, dstN); + jsfeat.imgproc.equalize_histogram(orig, dstO); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("canny", () => { + const { next, orig } = grayPair(); + // blur first, as the samples do + const blurN = new jsfeatNext.matrix_t(W, H, U8C1); + const blurO = new jsfeat.matrix_t(W, H, OU8C1); + ip.gaussian_blur(next, blurN, 5, 1.2); + jsfeat.imgproc.gaussian_blur(orig, blurO, 5, 1.2); + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.canny(blurN, dstN, 20, 60); + jsfeat.imgproc.canny(blurO, dstO, 20, 60); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("warp_perspective", () => { + const { next, orig } = grayPair(); + const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; + const tN = new jsfeatNext.matrix_t(3, 3, F32C1); + const tO = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + const tr = new jsfeatNext.transform(); + tr.perspective_4point_transform(tN, 0, 0, 4, 2, 63, 0, 60, 1, 63, 47, 62, 46, 0, 47, 2, 44); + for (let i = 0; i < 9; i++) tO.data[i] = tN.data[i]; + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.warp_perspective(next, dstN, tN, 0); + jsfeat.imgproc.warp_perspective(orig, dstO, tO, 0); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("warp_affine", () => { + const { next, orig } = grayPair(); + const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; + const tN = new jsfeatNext.matrix_t(3, 3, F32C1); + const tO = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + // rotation + slight scale + translation + const ang = 0.3; + const cs = Math.cos(ang) * 0.98; + const sn = Math.sin(ang) * 0.98; + const vals = [cs, -sn, 5.0, sn, cs, -3.0]; + for (let i = 0; i < 6; i++) { + tN.data[i] = vals[i]; + tO.data[i] = vals[i]; + } + const dstN = new jsfeatNext.matrix_t(W, H, U8C1); + const dstO = new jsfeat.matrix_t(W, H, OU8C1); + ip.warp_affine(next, dstN, tN, 0); + jsfeat.imgproc.warp_affine(orig, dstO, tO, 0); + expectSame(dstN.data, dstO.data, W * H); + }); + + it("hough_transform — original jsfeat is BROKEN here, jsfeatNext fixes it (documented divergence)", () => { + const { next, orig } = grayPair(); + // edge map first (already parity-verified above) + const edgesN = new jsfeatNext.matrix_t(W, H, U8C1); + const edgesO = new jsfeat.matrix_t(W, H, OU8C1); + ip.canny(next, edgesN, 20, 60); + jsfeat.imgproc.canny(orig, edgesO, 20, 60); + + // Parity finding (#45): the original jsfeat hough_transform uses + // `min_theta`/`max_theta` without declaring them; under the bundle's + // "use strict" IIFE that is a ReferenceError, so the distributed + // jsfeat function is unusable. jsfeatNext declares them (fix). + expect(() => jsfeat.imgproc.hough_transform(edgesO, 1, Math.PI / 180, 25)).toThrow(ReferenceError); + + // jsfeatNext's fixed version works and returns sane [rho, theta] lines + const linesN = ip.hough_transform(edgesN, 1, Math.PI / 180, 25); + expect(Array.isArray(linesN)).toBe(true); + expect(linesN.length).toBeGreaterThan(0); + for (const line of linesN as unknown as [number, number][]) { + expect(Number.isFinite(line[0])).toBe(true); + expect(line[1]).toBeGreaterThanOrEqual(0); + expect(line[1]).toBeLessThanOrEqual(Math.PI); + } + }); +}); diff --git a/tests/parity/linalg.test.ts b/tests/parity/linalg.test.ts new file mode 100644 index 0000000..4f94aec --- /dev/null +++ b/tests/parity/linalg.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45) + * for linalg: lu_solve, cholesky_solve, svd_decompose, svd_solve, svd_invert, + * eigenVV — each vs the original jsfeat.linalg on identical inputs. + */ + +function rng(seed: number): () => number { + let a = seed >>> 0; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; +const OF32C1 = jsfeat.F32_t | jsfeat.C1_t; +const la = new jsfeatNext.linalg(); + +function fillPair( + next: { data: Float32Array }, + orig: { data: Float32Array }, + len: number, + rand: () => number, + scale = 10 +) { + for (let i = 0; i < len; i++) { + const v = rand() * scale - scale / 2; + next.data[i] = v; + orig.data[i] = v; + } +} + +/** builds a well-conditioned symmetric positive-definite pair (A = M'M + n*I) */ +function spdPair(n: number, seed: number) { + const rand = rng(seed); + const raw = Array.from({ length: n * n }, () => rand() * 4 - 2); + const next = new jsfeatNext.matrix_t(n, n, F32C1); + const orig = new jsfeat.matrix_t(n, n, OF32C1); + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + let s = 0; + for (let k = 0; k < n; k++) s += raw[k * n + i] * raw[k * n + j]; + if (i === j) s += n; + next.data[i * n + j] = s; + orig.data[i * n + j] = s; + } + } + return { next, orig }; +} + +function expectDataClose(actual: { data: Float32Array }, expected: { data: Float32Array }, len: number, digits = 4) { + for (let i = 0; i < len; i++) { + expect(actual.data[i]).toBeCloseTo(expected.data[i], digits); + } +} + +describe("parity: linalg vs original jsfeat.linalg", () => { + it("lu_solve", () => { + const { next: A, orig: Ao } = spdPair(4, 21); + const B = new jsfeatNext.matrix_t(1, 4, F32C1); + const Bo = new jsfeat.matrix_t(1, 4, OF32C1); + fillPair(B, Bo, 4, rng(22)); + const r = la.lu_solve(A, B); + const ro = jsfeat.linalg.lu_solve(Ao, Bo); + expect(r).toBe(ro); + expectDataClose(B, Bo, 4); + }); + + it("cholesky_solve", () => { + const { next: A, orig: Ao } = spdPair(4, 23); + const B = new jsfeatNext.matrix_t(1, 4, F32C1); + const Bo = new jsfeat.matrix_t(1, 4, OF32C1); + fillPair(B, Bo, 4, rng(24)); + const r = la.cholesky_solve(A, B); + const ro = jsfeat.linalg.cholesky_solve(Ao, Bo); + expect(r).toBe(ro); + expectDataClose(B, Bo, 4); + }); + + it("svd_decompose (SVD_U_T | SVD_V_T)", () => { + const rows = 5, + cols = 4; + const A = new jsfeatNext.matrix_t(cols, rows, F32C1); + const Ao = new jsfeat.matrix_t(cols, rows, OF32C1); + fillPair(A, Ao, rows * cols, rng(25)); + + const W = new jsfeatNext.matrix_t(1, cols, F32C1); + const U = new jsfeatNext.matrix_t(rows, rows, F32C1); + const V = new jsfeatNext.matrix_t(cols, cols, F32C1); + const Wo = new jsfeat.matrix_t(1, cols, OF32C1); + const Uo = new jsfeat.matrix_t(rows, rows, OF32C1); + const Vo = new jsfeat.matrix_t(cols, cols, OF32C1); + + const opts = jsfeatNext.SVD_U_T | jsfeatNext.SVD_V_T; + la.svd_decompose(A, W, U, V, opts); + jsfeat.linalg.svd_decompose(Ao, Wo, Uo, Vo, jsfeat.SVD_U_T | jsfeat.SVD_V_T); + + expectDataClose(W, Wo, cols); + expectDataClose(U, Uo, rows * rows); + expectDataClose(V, Vo, cols * cols); + }); + + it("svd_solve", () => { + const n = 4; + const { next: A, orig: Ao } = spdPair(n, 26); + const B = new jsfeatNext.matrix_t(1, n, F32C1); + const Bo = new jsfeat.matrix_t(1, n, OF32C1); + fillPair(B, Bo, n, rng(27)); + const X = new jsfeatNext.matrix_t(1, n, F32C1); + const Xo = new jsfeat.matrix_t(1, n, OF32C1); + la.svd_solve(A, X, B); + jsfeat.linalg.svd_solve(Ao, Xo, Bo); + expectDataClose(X, Xo, n); + }); + + it("svd_invert", () => { + const n = 4; + const { next: A, orig: Ao } = spdPair(n, 28); + const Ai = new jsfeatNext.matrix_t(n, n, F32C1); + const Aio = new jsfeat.matrix_t(n, n, OF32C1); + la.svd_invert(Ai, A); + jsfeat.linalg.svd_invert(Aio, Ao); + expectDataClose(Ai, Aio, n * n); + }); + + it("eigenVV (values and vectors of a symmetric matrix)", () => { + const n = 5; + const { next: A, orig: Ao } = spdPair(n, 29); + const vects = new jsfeatNext.matrix_t(n, n, F32C1); + const vals = new jsfeatNext.matrix_t(1, n, F32C1); + const vectso = new jsfeat.matrix_t(n, n, OF32C1); + const valso = new jsfeat.matrix_t(1, n, OF32C1); + la.eigenVV(A, vects, vals); + jsfeat.linalg.eigenVV(Ao, vectso, valso); + expectDataClose(vals, valso, n); + expectDataClose(vects, vectso, n * n); + }); +}); diff --git a/tests/parity/math.get_gaussian_kernel.test.ts b/tests/parity/math.get_gaussian_kernel.test.ts new file mode 100644 index 0000000..edbad46 --- /dev/null +++ b/tests/parity/math.get_gaussian_kernel.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization test (Phase 0, issue #39). + * + * Asserts that jsfeatNext reproduces the *original* jsfeat output bit-for-bit + * (within float tolerance) for a pure, deterministic function. `jsfeat` (npm) + * is the golden oracle; if this passes, the behavior is pinned and any future + * de-duplication refactor (#47) can be proven behavior-preserving. + */ +describe("parity: math.get_gaussian_kernel vs original jsfeat", () => { + const cases = [ + { size: 3, sigma: 0 }, // sigma derived from size (OpenCV formula) + { size: 5, sigma: 1.2 }, + { size: 7, sigma: 2.5 }, + { size: 9, sigma: 0 }, + ]; + + for (const { size, sigma } of cases) { + it(`size=${size} sigma=${sigma}`, () => { + const expected = new Float32Array(size); + jsfeat.math.get_gaussian_kernel(size, sigma, expected, jsfeat.F32_t | jsfeat.C1_t); + + const actual = new Float32Array(size); + const m = new jsfeatNext.math(); + m.get_gaussian_kernel(size, sigma, actual, jsfeatNext.F32_t | jsfeatNext.C1_t); + + for (let i = 0; i < size; i++) { + expect(actual[i]).toBeCloseTo(expected[i], 6); + } + }); + } +}); diff --git a/tests/parity/math.test.ts b/tests/parity/math.test.ts new file mode 100644 index 0000000..179fdda --- /dev/null +++ b/tests/parity/math.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect, vi } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45) + * for the remaining jsfeatNext.math functions: qsort, median, and the + * deprecated matrix_t-based perspective_4point_transform (kept for parity + * with the distributed jsfeat bundle, where it lives under jsfeat.math). + */ + +function rng(seed: number): () => number { + let a = seed >>> 0; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const m = new jsfeatNext.math(); +// identical "less than" comparator for both implementations +const lt = (a: number, b: number) => (a < b ? 1 : 0); + +describe("parity: math.qsort / math.median vs original jsfeat.math", () => { + it("qsort sorts identically", () => { + const rand = rng(42); + const a: number[] = Array.from({ length: 101 }, () => Math.round(rand() * 1000) - 500); + const b = a.slice(); + m.qsort(a, 0, a.length - 1, lt); + jsfeat.math.qsort(b, 0, b.length - 1, lt); + expect(a).toEqual(b); + }); + + it("qsort on a sub-range only", () => { + const rand = rng(7); + const a: number[] = Array.from({ length: 50 }, () => Math.round(rand() * 100)); + const b = a.slice(); + m.qsort(a, 10, 39, lt); + jsfeat.math.qsort(b, 10, 39, lt); + expect(a).toEqual(b); + }); + + it("median returns the same value", () => { + for (const seed of [1, 2, 3]) { + const rand = rng(seed); + const a: number[] = Array.from({ length: 31 }, () => Math.round(rand() * 255)); + const b = a.slice(); + const mv = m.median(a, 0, a.length - 1); + const ov = jsfeat.math.median(b, 0, b.length - 1); + expect(mv).toBe(ov); + } + }); +}); + +describe("parity: deprecated math.perspective_4point_transform vs jsfeat.math (bundle)", () => { + it("matches the bundle's matrix_t-based version", () => { + // silence the intentional deprecation warning during the test + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; + const next = new jsfeatNext.matrix_t(3, 3, F32C1); + const orig = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + const s = [10, 12, 300, 8, 315, 230, 5, 240]; + const d = [24.5, 18.2, 300.1, 12.7, 289.4, 230.6, 31.8, 221.9]; + m.perspective_4point_transform( + next, + s[0], + s[1], + d[0], + d[1], + s[2], + s[3], + d[2], + d[3], + s[4], + s[5], + d[4], + d[5], + s[6], + s[7], + d[6], + d[7] + ); + jsfeat.math.perspective_4point_transform( + orig, + s[0], + s[1], + d[0], + d[1], + s[2], + s[3], + d[2], + d[3], + s[4], + s[5], + d[4], + d[5], + s[6], + s[7], + d[6], + d[7] + ); + for (let i = 0; i < 9; i++) { + expect(next.data[i]).toBeCloseTo(orig.data[i], 5); + } + expect(warn).toHaveBeenCalled(); // parity note: jsfeatNext adds a deprecation warning + warn.mockRestore(); + }); +}); diff --git a/tests/parity/matmath.test.ts b/tests/parity/matmath.test.ts new file mode 100644 index 0000000..785e20c --- /dev/null +++ b/tests/parity/matmath.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import matmath from "../../src/matmath/matmath"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45). + * + * Every function in src/matmath/matmath.ts is exercised against the same + * function in the original jsfeat (`jsfeat.matmath`, a static namespace) on + * identical inputs. Deterministic pseudo-random data keeps runs reproducible. + */ + +// small deterministic PRNG (mulberry32) so failures are reproducible +function rng(seed: number): () => number { + let a = seed >>> 0; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; + +function makePair(cols: number, rows: number, seed: number) { + // one matrix for jsfeatNext, one identical copy for the jsfeat oracle + const next = new jsfeatNext.matrix_t(cols, rows, F32C1); + const orig = new jsfeat.matrix_t(cols, rows, jsfeat.F32_t | jsfeat.C1_t); + const rand = rng(seed); + for (let i = 0; i < cols * rows; i++) { + const v = rand() * 20 - 10; + next.data[i] = v; + orig.data[i] = v; + } + return { next, orig }; +} + +function expectDataClose(actual: { data: Float32Array }, expected: { data: Float32Array }, len: number) { + for (let i = 0; i < len; i++) { + expect(actual.data[i]).toBeCloseTo(expected.data[i], 5); + } +} + +const mm = new matmath(); + +describe("parity: matmath vs original jsfeat.matmath", () => { + it("identity", () => { + const { next, orig } = makePair(4, 4, 1); + mm.identity(next, 2.5); + jsfeat.matmath.identity(orig, 2.5); + expectDataClose(next, orig, 16); + }); + + it("transpose", () => { + const { next: a, orig: ao } = makePair(5, 3, 2); + const at = new jsfeatNext.matrix_t(3, 5, F32C1); + const ato = new jsfeat.matrix_t(3, 5, jsfeat.F32_t | jsfeat.C1_t); + mm.transpose(at, a); + jsfeat.matmath.transpose(ato, ao); + expectDataClose(at, ato, 15); + }); + + it("multiply (C = A * B)", () => { + const { next: a, orig: ao } = makePair(4, 3, 3); + const { next: b, orig: bo } = makePair(2, 4, 4); + const c = new jsfeatNext.matrix_t(2, 3, F32C1); + const co = new jsfeat.matrix_t(2, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.multiply(c, a, b); + jsfeat.matmath.multiply(co, ao, bo); + expectDataClose(c, co, 6); + }); + + it("multiply_ABt (C = A * B')", () => { + const { next: a, orig: ao } = makePair(4, 3, 5); + const { next: b, orig: bo } = makePair(4, 2, 6); + const c = new jsfeatNext.matrix_t(2, 3, F32C1); + const co = new jsfeat.matrix_t(2, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.multiply_ABt(c, a, b); + jsfeat.matmath.multiply_ABt(co, ao, bo); + expectDataClose(c, co, 6); + }); + + it("multiply_AtB (C = A' * B)", () => { + const { next: a, orig: ao } = makePair(3, 4, 7); + const { next: b, orig: bo } = makePair(2, 4, 8); + const c = new jsfeatNext.matrix_t(2, 3, F32C1); + const co = new jsfeat.matrix_t(2, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.multiply_AtB(c, a, b); + jsfeat.matmath.multiply_AtB(co, ao, bo); + expectDataClose(c, co, 6); + }); + + it("multiply_AAt (C = A * A')", () => { + const { next: a, orig: ao } = makePair(5, 3, 9); + const c = new jsfeatNext.matrix_t(3, 3, F32C1); + const co = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.multiply_AAt(c, a); + jsfeat.matmath.multiply_AAt(co, ao); + expectDataClose(c, co, 9); + }); + + it("multiply_AtA (C = A' * A)", () => { + const { next: a, orig: ao } = makePair(3, 5, 10); + const c = new jsfeatNext.matrix_t(3, 3, F32C1); + const co = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.multiply_AtA(c, a); + jsfeat.matmath.multiply_AtA(co, ao); + expectDataClose(c, co, 9); + }); + + it("identity_3x3", () => { + const { next, orig } = makePair(3, 3, 11); + mm.identity_3x3(next, 3.0); + jsfeat.matmath.identity_3x3(orig, 3.0); + expectDataClose(next, orig, 9); + }); + + it("invert_3x3", () => { + const { next: a, orig: ao } = makePair(3, 3, 12); + // nudge the diagonal so the matrix is well-conditioned (same on both) + for (let d = 0; d < 3; d++) { + a.data[d * 4] += 5; + ao.data[d * 4] += 5; + } + const inv = new jsfeatNext.matrix_t(3, 3, F32C1); + const invo = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.invert_3x3(a, inv); + jsfeat.matmath.invert_3x3(ao, invo); + expectDataClose(inv, invo, 9); + }); + + it("multiply_3x3", () => { + const { next: a, orig: ao } = makePair(3, 3, 13); + const { next: b, orig: bo } = makePair(3, 3, 14); + const c = new jsfeatNext.matrix_t(3, 3, F32C1); + const co = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + mm.multiply_3x3(c, a, b); + jsfeat.matmath.multiply_3x3(co, ao, bo); + expectDataClose(c, co, 9); + }); + + it("mat3x3_determinant", () => { + const { next, orig } = makePair(3, 3, 15); + expect(mm.mat3x3_determinant(next)).toBeCloseTo(jsfeat.matmath.mat3x3_determinant(orig), 4); + }); + + it("determinant_3x3 (scalar args)", () => { + const rand = rng(16); + const v = Array.from({ length: 9 }, () => rand() * 10 - 5); + const actual = mm.determinant_3x3(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8]); + const expected = jsfeat.matmath.determinant_3x3(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8]); + expect(actual).toBeCloseTo(expected, 6); + }); +}); diff --git a/tests/parity/motion_estimator.test.ts b/tests/parity/motion_estimator.test.ts new file mode 100644 index 0000000..6c0126a --- /dev/null +++ b/tests/parity/motion_estimator.test.ts @@ -0,0 +1,183 @@ +import { describe, it, expect, afterEach, vi } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45) + * for motion_estimator.ransac / lmeds with homography2d and affine2d kernels. + * + * RANSAC/LMEDS draw random subsets via Math.random. To make both sides + * deterministic AND identical, Math.random is stubbed with the same seeded + * PRNG sequence for the jsfeatNext run and again (re-seeded) for the oracle. + * + * API note (Axis 2): kernels live under jsfeat.motion_model.* in the + * original; jsfeatNext exposes them as jsfeatNext.homography2d/affine2d. + */ + +function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; +const U8C1 = jsfeatNext.U8_t | jsfeatNext.C1_t; + +/** ground-truth homography used to synthesize correspondences */ +const GT = [1.05, 0.02, 8.0, -0.03, 0.98, -5.0, 0.0002, -0.0001, 1.0]; + +function makeCorrespondences(n: number, outliers: number) { + const rand = mulberry32(1234); + const from: { x: number; y: number }[] = []; + const to: { x: number; y: number }[] = []; + for (let i = 0; i < n; i++) { + const x = 10 + rand() * 300; + const y = 10 + rand() * 220; + const wsc = 1.0 / (GT[6] * x + GT[7] * y + GT[8]); + let X = (GT[0] * x + GT[1] * y + GT[2]) * wsc; + let Y = (GT[3] * x + GT[4] * y + GT[5]) * wsc; + if (i < outliers) { + X += 40 + rand() * 60; // gross outlier + Y -= 40 + rand() * 60; + } + from.push({ x, y }); + to.push({ x: X, y: Y }); + } + return { from, to }; +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function seededRandom(seed: number) { + const r = mulberry32(seed); + return vi.spyOn(Math, "random").mockImplementation(r); +} + +describe("parity: motion_estimator vs original jsfeat.motion_estimator", () => { + const N = 40; + const OUT = 6; + + it("ransac with homography2d kernel: same model and same inlier mask", () => { + const { from, to } = makeCorrespondences(N, OUT); + + const params = new jsfeatNext.ransac_params_t(4, 3.0, 0.5, 0.99); + const me = new jsfeatNext.motion_estimator(); + const kernel = new jsfeatNext.homography2d(); + const model = new jsfeatNext.matrix_t(3, 3, F32C1); + const mask = new jsfeatNext.matrix_t(N, 1, U8C1); + + seededRandom(2024); + const okN = me.ransac(params, kernel, from, to, N, model, mask, 1000); + vi.restoreAllMocks(); + + const paramsO = new jsfeat.ransac_params_t(4, 3.0, 0.5, 0.99); + const kernelO = new jsfeat.motion_model.homography2d(); + const modelO = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + const maskO = new jsfeat.matrix_t(N, 1, jsfeat.U8_t | jsfeat.C1_t); + + seededRandom(2024); // identical random sequence for the oracle + const okO = jsfeat.motion_estimator.ransac(paramsO, kernelO, from, to, N, modelO, maskO, 1000); + vi.restoreAllMocks(); + + expect(okN).toBe(okO); + expect(okN).toBe(true); + for (let i = 0; i < N; i++) { + expect(mask.data[i]).toBe(maskO.data[i]); + } + for (let i = 0; i < 9; i++) { + expect(model.data[i]).toBeCloseTo(modelO.data[i], 5); + } + // sanity: the outliers must be rejected + for (let i = 0; i < OUT; i++) { + expect(mask.data[i]).toBe(0); + } + }); + + it("lmeds with homography2d kernel: same model and same inlier mask", () => { + const { from, to } = makeCorrespondences(N, OUT); + + const params = new jsfeatNext.ransac_params_t(4, 0, 0.45, 0.99); + const me = new jsfeatNext.motion_estimator(); + const kernel = new jsfeatNext.homography2d(); + const model = new jsfeatNext.matrix_t(3, 3, F32C1); + const mask = new jsfeatNext.matrix_t(N, 1, U8C1); + + seededRandom(777); + const okN = me.lmeds(params, kernel, from, to, N, model, mask, 1000); + vi.restoreAllMocks(); + + const paramsO = new jsfeat.ransac_params_t(4, 0, 0.45, 0.99); + const kernelO = new jsfeat.motion_model.homography2d(); + const modelO = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + const maskO = new jsfeat.matrix_t(N, 1, jsfeat.U8_t | jsfeat.C1_t); + + seededRandom(777); + const okO = jsfeat.motion_estimator.lmeds(paramsO, kernelO, from, to, N, modelO, maskO, 1000); + vi.restoreAllMocks(); + + expect(okN).toBe(okO); + for (let i = 0; i < N; i++) { + expect(mask.data[i]).toBe(maskO.data[i]); + } + for (let i = 0; i < 9; i++) { + expect(model.data[i]).toBeCloseTo(modelO.data[i], 5); + } + }); + + it("ransac with affine2d kernel: same model and same inlier mask", () => { + // affine ground truth (no perspective terms) + const rand = mulberry32(555); + const from: { x: number; y: number }[] = []; + const to: { x: number; y: number }[] = []; + for (let i = 0; i < N; i++) { + const x = 10 + rand() * 300; + const y = 10 + rand() * 220; + let X = 0.9 * x - 0.1 * y + 12; + let Y = 0.15 * x + 1.05 * y - 8; + if (i < OUT) { + X += 50 + rand() * 50; + Y += 50 + rand() * 50; + } + from.push({ x, y }); + to.push({ x: X, y: Y }); + } + + // BUG FOUND BY THIS SUITE (documented divergence, see the tracking + // issue): jsfeatNext's motion_model base class lost the default + // check_subset() (original jsfeat returns true there; only + // homography2d overrides it), so RANSAC with an affine2d kernel + // throws TypeError in jsfeatNext while it works in original jsfeat. + const params = new jsfeatNext.ransac_params_t(3, 3.0, 0.5, 0.99); + const me = new jsfeatNext.motion_estimator(); + const kernel = new jsfeatNext.affine2d(); + const model = new jsfeatNext.matrix_t(3, 3, F32C1); + const mask = new jsfeatNext.matrix_t(N, 1, U8C1); + + seededRandom(31337); + expect(() => me.ransac(params, kernel, from, to, N, model, mask, 1000)).toThrow(TypeError); + vi.restoreAllMocks(); + + // ...whereas the original jsfeat succeeds on the same data: + const paramsO = new jsfeat.ransac_params_t(3, 3.0, 0.5, 0.99); + const kernelO = new jsfeat.motion_model.affine2d(); + const modelO = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + const maskO = new jsfeat.matrix_t(N, 1, jsfeat.U8_t | jsfeat.C1_t); + + seededRandom(31337); + const okO = jsfeat.motion_estimator.ransac(paramsO, kernelO, from, to, N, modelO, maskO, 1000); + vi.restoreAllMocks(); + + expect(okO).toBe(true); + // the oracle also rejects the synthetic outliers + for (let i = 0; i < OUT; i++) { + expect(maskO.data[i]).toBe(0); + } + }); +}); diff --git a/tests/parity/structs.test.ts b/tests/parity/structs.test.ts new file mode 100644 index 0000000..22533f8 --- /dev/null +++ b/tests/parity/structs.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45) + * for the core structs and remaining paths: matrix_t (construction, resize, + * copy_to), the data-type helpers, and imgproc.resample's f32 path. + * + * API note (Axis 2): get_data_type/get_channel/get_data_type_size are + * top-level functions in original jsfeat but INSTANCE methods on the + * jsfeatNext base class. + */ + +const U8C1 = jsfeatNext.U8_t | jsfeatNext.C1_t; +const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; + +describe("parity: matrix_t vs original jsfeat.matrix_t", () => { + it("construction exposes the same shape and buffer size", () => { + const nx = new jsfeatNext.matrix_t(320, 240, jsfeatNext.U8C3_t); + const ox = new jsfeat.matrix_t(320, 240, jsfeat.U8C3_t); + expect(nx.cols).toBe(ox.cols); + expect(nx.rows).toBe(ox.rows); + expect(nx.channel).toBe(ox.channel); + expect(nx.type).toBe(ox.type); + expect(nx.data.length).toBe(ox.data.length); + expect(nx.buffer.size).toBe(ox.buffer.size); + }); + + it("resize matches (grow reallocates, shrink keeps shape fields)", () => { + const nx = new jsfeatNext.matrix_t(16, 16, U8C1); + const ox = new jsfeat.matrix_t(16, 16, jsfeat.U8_t | jsfeat.C1_t); + nx.resize(64, 32, 1); + ox.resize(64, 32, 1); + expect(nx.cols).toBe(ox.cols); + expect(nx.rows).toBe(ox.rows); + expect(nx.data.length).toBe(ox.data.length); + nx.resize(8, 8, 1); + ox.resize(8, 8, 1); + expect(nx.cols).toBe(ox.cols); + expect(nx.rows).toBe(ox.rows); + expect(nx.data.length).toBe(ox.data.length); // shrink keeps the buffer + }); + + it("copy_to copies identically", () => { + const nx = new jsfeatNext.matrix_t(8, 6, U8C1); + const ox = new jsfeat.matrix_t(8, 6, jsfeat.U8_t | jsfeat.C1_t); + for (let i = 0; i < 48; i++) { + nx.data[i] = (i * 7) & 255; + ox.data[i] = (i * 7) & 255; + } + const nd = new jsfeatNext.matrix_t(8, 6, U8C1); + const od = new jsfeat.matrix_t(8, 6, jsfeat.U8_t | jsfeat.C1_t); + nx.copy_to(nd); + ox.copy_to(od); + for (let i = 0; i < 48; i++) { + expect(nd.data[i]).toBe(od.data[i]); + } + }); +}); + +describe("parity: data-type helpers vs original jsfeat top-level functions", () => { + it("get_data_type / get_channel / get_data_type_size agree for all popular formats", () => { + const base = new jsfeatNext(); + const formats = [ + jsfeatNext.U8C1_t, + jsfeatNext.U8C3_t, + jsfeatNext.U8C4_t, + jsfeatNext.F32C1_t, + jsfeatNext.F32C2_t, + jsfeatNext.S32C1_t, + jsfeatNext.S32C2_t, + ]; + for (const t of formats) { + expect(base.get_data_type(t)).toBe(jsfeat.get_data_type(t)); + expect(base.get_channel(t)).toBe(jsfeat.get_channel(t)); + expect(base.get_data_type_size(t)).toBe(jsfeat.get_data_type_size(t)); + } + }); +}); + +describe("parity: imgproc.resample f32 path", () => { + it("resamples float matrices identically", () => { + const W = 40, + H = 30; + const ip = new jsfeatNext.imgproc(); + const nx = new jsfeatNext.matrix_t(W, H, F32C1); + const ox = new jsfeat.matrix_t(W, H, jsfeat.F32_t | jsfeat.C1_t); + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + const v = Math.sin(x * 0.35) * 40 + Math.cos(y * 0.2) * 25 + 100; + nx.data[y * W + x] = v; + ox.data[y * W + x] = v; + } + } + const nd = new jsfeatNext.matrix_t(20, 15, F32C1); + const od = new jsfeat.matrix_t(20, 15, jsfeat.F32_t | jsfeat.C1_t); + ip.resample(nx, nd, 20, 15); + jsfeat.imgproc.resample(ox, od, 20, 15); + for (let i = 0; i < 20 * 15; i++) { + expect(nd.data[i]).toBeCloseTo(od.data[i], 4); + } + }); +}); diff --git a/tests/parity/transform.test.ts b/tests/parity/transform.test.ts new file mode 100644 index 0000000..ecfe345 --- /dev/null +++ b/tests/parity/transform.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect } from "vitest"; +import jsfeatNext from "../../src/jsfeatNext"; +import { transform } from "../../src/transform/transform"; +import jsfeat from "../vendor/oracle.cjs"; + +/** + * Characterization tests (Phase 0, issue #39; parity audit rows for issue #45). + * src/transform/transform.ts vs the original jsfeat. + * + * Parity-audit notes captured by these tests (see the audit doc, Axis 1/2): + * - `transform` was NEVER included in jsfeat's distributed build or npm + * package; only `math.perspective_4point_transform` (matrix_t-based) made + * it into the bundle. The vendored oracle evaluates src/jsfeat_transform.js + * to restore it. + * - SIGNATURE DIVERGENCE: original jsfeat.transform.* functions take RAW + * ARRAYS (mat[0]...), while jsfeatNext's transform methods take matrix_t + * and read .data. Same math, different calling convention. + */ + +const F32C1 = jsfeatNext.F32_t | jsfeatNext.C1_t; +const tr = new transform(); + +function mat3x3() { + return new jsfeatNext.matrix_t(3, 3, F32C1); +} + +function expectClose(actual: ArrayLike, expected: ArrayLike, len: number) { + for (let i = 0; i < len; i++) { + expect(actual[i]).toBeCloseTo(expected[i], 5); + } +} + +describe("parity: transform vs original jsfeat", () => { + // a realistic quad -> quad mapping (like a tracked marker in WebARKit) + const s = [0, 0, 320, 0, 320, 240, 0, 240]; + const d = [24.5, 18.2, 300.1, 12.7, 289.4, 230.6, 31.8, 221.9]; + + function forwardNext(model: ReturnType) { + tr.perspective_4point_transform( + model, + s[0], + s[1], + d[0], + d[1], + s[2], + s[3], + d[2], + d[3], + s[4], + s[5], + d[4], + d[5], + s[6], + s[7], + d[6], + d[7] + ); + } + + it("perspective_4point_transform (vs jsfeat.transform, raw-array oracle)", () => { + const next = mat3x3(); + forwardNext(next); + const orig = new Float32Array(9); + jsfeat.transform.perspective_4point_transform( + orig, + s[0], + s[1], + d[0], + d[1], + s[2], + s[3], + d[2], + d[3], + s[4], + s[5], + d[4], + d[5], + s[6], + s[7], + d[6], + d[7] + ); + expectClose(next.data, orig, 9); + }); + + it("perspective_4point_transform (vs jsfeat.math, matrix_t oracle from the distributed bundle)", () => { + const next = mat3x3(); + forwardNext(next); + const orig = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t); + jsfeat.math.perspective_4point_transform( + orig, + s[0], + s[1], + d[0], + d[1], + s[2], + s[3], + d[2], + d[3], + s[4], + s[5], + d[4], + d[5], + s[6], + s[7], + d[6], + d[7] + ); + expectClose(next.data, orig.data, 9); + }); + + it("invert_affine_transform", () => { + const affine = [1.2, -0.3, 14.0, 0.25, 0.9, -7.5]; + const srcN = mat3x3(); + for (let i = 0; i < 6; i++) srcN.data[i] = affine[i]; + const dstN = mat3x3(); + tr.invert_affine_transform(srcN, dstN); + + const dstO = new Float32Array(9); + jsfeat.transform.invert_affine_transform(Float32Array.from(srcN.data), dstO); + + expectClose(dstN.data, dstO, 6); + }); + + it("invert_perspective_transform", () => { + const hN = mat3x3(); + forwardNext(hN); // forward homography, already parity-checked above + const invN = mat3x3(); + tr.invert_perspective_transform(hN, invN); + + const invO = new Float32Array(9); + jsfeat.transform.invert_perspective_transform(Float32Array.from(hN.data), invO); + + expectClose(invN.data, invO, 9); + }); +}); diff --git a/tests/vendor/jsfeat-master.js b/tests/vendor/jsfeat-master.js new file mode 100644 index 0000000..d263a20 --- /dev/null +++ b/tests/vendor/jsfeat-master.js @@ -0,0 +1,5639 @@ +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +// namespace ? +var jsfeat = jsfeat || { REVISION: 'ALPHA' }; +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +(function(global) { + "use strict"; + // + + // CONSTANTS + var EPSILON = 0.0000001192092896; + var FLT_MIN = 1E-37; + + // implementation from CCV project + // currently working only with u8,s32,f32 + var U8_t = 0x0100, + S32_t = 0x0200, + F32_t = 0x0400, + S64_t = 0x0800, + F64_t = 0x1000; + + var C1_t = 0x01, + C2_t = 0x02, + C3_t = 0x03, + C4_t = 0x04; + + var _data_type_size = new Int32Array([ -1, 1, 4, -1, 4, -1, -1, -1, 8, -1, -1, -1, -1, -1, -1, -1, 8 ]); + + var get_data_type = (function () { + return function(type) { + return (type & 0xFF00); + } + })(); + + var get_channel = (function () { + return function(type) { + return (type & 0xFF); + } + })(); + + var get_data_type_size = (function () { + return function(type) { + return _data_type_size[(type & 0xFF00) >> 8]; + } + })(); + + // color conversion + var COLOR_RGBA2GRAY = 0; + var COLOR_RGB2GRAY = 1; + var COLOR_BGRA2GRAY = 2; + var COLOR_BGR2GRAY = 3; + + // box blur option + var BOX_BLUR_NOSCALE = 0x01; + // svd options + var SVD_U_T = 0x01; + var SVD_V_T = 0x02; + + var data_t = (function () { + function data_t(size_in_bytes, buffer) { + // we need align size to multiple of 8 + this.size = ((size_in_bytes + 7) | 0) & -8; + if (typeof buffer === "undefined") { + this.buffer = new ArrayBuffer(this.size); + } else { + this.buffer = buffer; + this.size = buffer.length; + } + this.u8 = new Uint8Array(this.buffer); + this.i32 = new Int32Array(this.buffer); + this.f32 = new Float32Array(this.buffer); + this.f64 = new Float64Array(this.buffer); + } + return data_t; + })(); + + var matrix_t = (function () { + // columns, rows, data_type + function matrix_t(c, r, data_type, data_buffer) { + this.type = get_data_type(data_type)|0; + this.channel = get_channel(data_type)|0; + this.cols = c|0; + this.rows = r|0; + if (typeof data_buffer === "undefined") { + this.allocate(); + } else { + this.buffer = data_buffer; + // data user asked for + this.data = this.type&U8_t ? this.buffer.u8 : (this.type&S32_t ? this.buffer.i32 : (this.type&F32_t ? this.buffer.f32 : this.buffer.f64)); + } + } + matrix_t.prototype.allocate = function() { + // clear references + delete this.data; + delete this.buffer; + // + this.buffer = new data_t((this.cols * get_data_type_size(this.type) * this.channel) * this.rows); + this.data = this.type&U8_t ? this.buffer.u8 : (this.type&S32_t ? this.buffer.i32 : (this.type&F32_t ? this.buffer.f32 : this.buffer.f64)); + } + matrix_t.prototype.copy_to = function(other) { + var od = other.data, td = this.data; + var i = 0, n = (this.cols*this.rows*this.channel)|0; + for(; i < n-4; i+=4) { + od[i] = td[i]; + od[i+1] = td[i+1]; + od[i+2] = td[i+2]; + od[i+3] = td[i+3]; + } + for(; i < n; ++i) { + od[i] = td[i]; + } + } + matrix_t.prototype.resize = function(c, r, ch) { + if (typeof ch === "undefined") { ch = this.channel; } + // relocate buffer only if new size doesnt fit + var new_size = (c * get_data_type_size(this.type) * ch) * r; + if(new_size > this.buffer.size) { + this.cols = c; + this.rows = r; + this.channel = ch; + this.allocate(); + } else { + this.cols = c; + this.rows = r; + this.channel = ch; + } + } + + return matrix_t; + })(); + + var pyramid_t = (function () { + + function pyramid_t(levels) { + this.levels = levels|0; + this.data = new Array(levels); + this.pyrdown = jsfeat.imgproc.pyrdown; + } + + pyramid_t.prototype.allocate = function(start_w, start_h, data_type) { + var i = this.levels; + while(--i >= 0) { + this.data[i] = new matrix_t(start_w >> i, start_h >> i, data_type); + } + } + + pyramid_t.prototype.build = function(input, skip_first_level) { + if (typeof skip_first_level === "undefined") { skip_first_level = true; } + // just copy data to first level + var i = 2, a = input, b = this.data[0]; + if(!skip_first_level) { + var j=input.cols*input.rows; + while(--j >= 0) { + b.data[j] = input.data[j]; + } + } + b = this.data[1]; + this.pyrdown(a, b); + for(; i < this.levels; ++i) { + a = b; + b = this.data[i]; + this.pyrdown(a, b); + } + } + + return pyramid_t; + })(); + + var keypoint_t = (function () { + function keypoint_t(x,y,score,level,angle) { + if (typeof x === "undefined") { x=0; } + if (typeof y === "undefined") { y=0; } + if (typeof score === "undefined") { score=0; } + if (typeof level === "undefined") { level=0; } + if (typeof angle === "undefined") { angle=-1.0; } + + this.x = x; + this.y = y; + this.score = score; + this.level = level; + this.angle = angle; + } + return keypoint_t; + })(); + + + // data types + global.U8_t = U8_t; + global.S32_t = S32_t; + global.F32_t = F32_t; + global.S64_t = S64_t; + global.F64_t = F64_t; + // data channels + global.C1_t = C1_t; + global.C2_t = C2_t; + global.C3_t = C3_t; + global.C4_t = C4_t; + + // popular formats + global.U8C1_t = U8_t | C1_t; + global.U8C3_t = U8_t | C3_t; + global.U8C4_t = U8_t | C4_t; + + global.F32C1_t = F32_t | C1_t; + global.F32C2_t = F32_t | C2_t; + global.S32C1_t = S32_t | C1_t; + global.S32C2_t = S32_t | C2_t; + + // constants + global.EPSILON = EPSILON; + global.FLT_MIN = FLT_MIN; + + // color convert + global.COLOR_RGBA2GRAY = COLOR_RGBA2GRAY; + global.COLOR_RGB2GRAY = COLOR_RGB2GRAY; + global.COLOR_BGRA2GRAY = COLOR_BGRA2GRAY; + global.COLOR_BGR2GRAY = COLOR_BGR2GRAY; + + // options + global.BOX_BLUR_NOSCALE = BOX_BLUR_NOSCALE; + global.SVD_U_T = SVD_U_T; + global.SVD_V_T = SVD_V_T; + + global.get_data_type = get_data_type; + global.get_channel = get_channel; + global.get_data_type_size = get_data_type_size; + + global.data_t = data_t; + global.matrix_t = matrix_t; + global.pyramid_t = pyramid_t; + global.keypoint_t = keypoint_t; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +(function(global) { + "use strict"; + // + + var cache = (function() { + + // very primitive array cache, still need testing if it helps + // of course V8 has its own powerful cache sys but i'm not sure + // it caches several multichannel 640x480 buffer creations each frame + + var _pool_node_t = (function () { + function _pool_node_t(size_in_bytes) { + this.next = null; + this.data = new jsfeat.data_t(size_in_bytes); + this.size = this.data.size; + this.buffer = this.data.buffer; + this.u8 = this.data.u8; + this.i32 = this.data.i32; + this.f32 = this.data.f32; + this.f64 = this.data.f64; + } + _pool_node_t.prototype.resize = function(size_in_bytes) { + delete this.data; + this.data = new jsfeat.data_t(size_in_bytes); + this.size = this.data.size; + this.buffer = this.data.buffer; + this.u8 = this.data.u8; + this.i32 = this.data.i32; + this.f32 = this.data.f32; + this.f64 = this.data.f64; + } + return _pool_node_t; + })(); + + var _pool_head, _pool_tail; + var _pool_size = 0; + + return { + + allocate: function(capacity, data_size) { + _pool_head = _pool_tail = new _pool_node_t(data_size); + for (var i = 0; i < capacity; ++i) { + var node = new _pool_node_t(data_size); + _pool_tail = _pool_tail.next = node; + + _pool_size++; + } + }, + + get_buffer: function(size_in_bytes) { + // assume we have enough free nodes + var node = _pool_head; + _pool_head = _pool_head.next; + _pool_size--; + + if(size_in_bytes > node.size) { + node.resize(size_in_bytes); + } + + return node; + }, + + put_buffer: function(node) { + _pool_tail = _pool_tail.next = node; + _pool_size++; + } + }; + })(); + + global.cache = cache; + // for now we dont need more than 30 buffers + // if having cache sys really helps we can add auto extending sys + cache.allocate(30, 640*4); + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +(function(global) { + "use strict"; + // + + var math = (function() { + + var qsort_stack = new Int32Array(48*2); + + return { + get_gaussian_kernel: function(size, sigma, kernel, data_type) { + var i=0,x=0.0,t=0.0,sigma_x=0.0,scale_2x=0.0; + var sum = 0.0; + var kern_node = jsfeat.cache.get_buffer(size<<2); + var _kernel = kern_node.f32;//new Float32Array(size); + + if((size&1) == 1 && size <= 7 && sigma <= 0) { + switch(size>>1) { + case 0: + _kernel[0] = 1.0; + sum = 1.0; + break; + case 1: + _kernel[0] = 0.25, _kernel[1] = 0.5, _kernel[2] = 0.25; + sum = 0.25+0.5+0.25; + break; + case 2: + _kernel[0] = 0.0625, _kernel[1] = 0.25, _kernel[2] = 0.375, + _kernel[3] = 0.25, _kernel[4] = 0.0625; + sum = 0.0625+0.25+0.375+0.25+0.0625; + break; + case 3: + _kernel[0] = 0.03125, _kernel[1] = 0.109375, _kernel[2] = 0.21875, + _kernel[3] = 0.28125, _kernel[4] = 0.21875, _kernel[5] = 0.109375, _kernel[6] = 0.03125; + sum = 0.03125+0.109375+0.21875+0.28125+0.21875+0.109375+0.03125; + break; + } + } else { + sigma_x = sigma > 0 ? sigma : ((size-1)*0.5 - 1.0)*0.3 + 0.8; + scale_2x = -0.5/(sigma_x*sigma_x); + + for( ; i < size; ++i ) + { + x = i - (size-1)*0.5; + t = Math.exp(scale_2x*x*x); + + _kernel[i] = t; + sum += t; + } + } + + if(data_type & jsfeat.U8_t) { + // int based kernel + sum = 256.0/sum; + for (i = 0; i < size; ++i) { + kernel[i] = (_kernel[i] * sum + 0.5)|0; + } + } else { + // classic kernel + sum = 1.0/sum; + for (i = 0; i < size; ++i) { + kernel[i] = _kernel[i] * sum; + } + } + + jsfeat.cache.put_buffer(kern_node); + }, + + // model is 3x3 matrix_t + perspective_4point_transform: function(model, src_x0, src_y0, dst_x0, dst_y0, + src_x1, src_y1, dst_x1, dst_y1, + src_x2, src_y2, dst_x2, dst_y2, + src_x3, src_y3, dst_x3, dst_y3) { + var t1 = src_x0; + var t2 = src_x2; + var t4 = src_y1; + var t5 = t1 * t2 * t4; + var t6 = src_y3; + var t7 = t1 * t6; + var t8 = t2 * t7; + var t9 = src_y2; + var t10 = t1 * t9; + var t11 = src_x1; + var t14 = src_y0; + var t15 = src_x3; + var t16 = t14 * t15; + var t18 = t16 * t11; + var t20 = t15 * t11 * t9; + var t21 = t15 * t4; + var t24 = t15 * t9; + var t25 = t2 * t4; + var t26 = t6 * t2; + var t27 = t6 * t11; + var t28 = t9 * t11; + var t30 = 1.0 / (t21-t24 - t25 + t26 - t27 + t28); + var t32 = t1 * t15; + var t35 = t14 * t11; + var t41 = t4 * t1; + var t42 = t6 * t41; + var t43 = t14 * t2; + var t46 = t16 * t9; + var t48 = t14 * t9 * t11; + var t51 = t4 * t6 * t2; + var t55 = t6 * t14; + var Hr0 = -(t8-t5 + t10 * t11 - t11 * t7 - t16 * t2 + t18 - t20 + t21 * t2) * t30; + var Hr1 = (t5 - t8 - t32 * t4 + t32 * t9 + t18 - t2 * t35 + t27 * t2 - t20) * t30; + var Hr2 = t1; + var Hr3 = (-t9 * t7 + t42 + t43 * t4 - t16 * t4 + t46 - t48 + t27 * t9 - t51) * t30; + var Hr4 = (-t42 + t41 * t9 - t55 * t2 + t46 - t48 + t55 * t11 + t51 - t21 * t9) * t30; + var Hr5 = t14; + var Hr6 = (-t10 + t41 + t43 - t35 + t24 - t21 - t26 + t27) * t30; + var Hr7 = (-t7 + t10 + t16 - t43 + t27 - t28 - t21 + t25) * t30; + + t1 = dst_x0; + t2 = dst_x2; + t4 = dst_y1; + t5 = t1 * t2 * t4; + t6 = dst_y3; + t7 = t1 * t6; + t8 = t2 * t7; + t9 = dst_y2; + t10 = t1 * t9; + t11 = dst_x1; + t14 = dst_y0; + t15 = dst_x3; + t16 = t14 * t15; + t18 = t16 * t11; + t20 = t15 * t11 * t9; + t21 = t15 * t4; + t24 = t15 * t9; + t25 = t2 * t4; + t26 = t6 * t2; + t27 = t6 * t11; + t28 = t9 * t11; + t30 = 1.0 / (t21-t24 - t25 + t26 - t27 + t28); + t32 = t1 * t15; + t35 = t14 * t11; + t41 = t4 * t1; + t42 = t6 * t41; + t43 = t14 * t2; + t46 = t16 * t9; + t48 = t14 * t9 * t11; + t51 = t4 * t6 * t2; + t55 = t6 * t14; + var Hl0 = -(t8-t5 + t10 * t11 - t11 * t7 - t16 * t2 + t18 - t20 + t21 * t2) * t30; + var Hl1 = (t5 - t8 - t32 * t4 + t32 * t9 + t18 - t2 * t35 + t27 * t2 - t20) * t30; + var Hl2 = t1; + var Hl3 = (-t9 * t7 + t42 + t43 * t4 - t16 * t4 + t46 - t48 + t27 * t9 - t51) * t30; + var Hl4 = (-t42 + t41 * t9 - t55 * t2 + t46 - t48 + t55 * t11 + t51 - t21 * t9) * t30; + var Hl5 = t14; + var Hl6 = (-t10 + t41 + t43 - t35 + t24 - t21 - t26 + t27) * t30; + var Hl7 = (-t7 + t10 + t16 - t43 + t27 - t28 - t21 + t25) * t30; + + // the following code computes R = Hl * inverse Hr + t2 = Hr4-Hr7*Hr5; + t4 = Hr0*Hr4; + t5 = Hr0*Hr5; + t7 = Hr3*Hr1; + t8 = Hr2*Hr3; + t10 = Hr1*Hr6; + var t12 = Hr2*Hr6; + t15 = 1.0 / (t4-t5*Hr7-t7+t8*Hr7+t10*Hr5-t12*Hr4); + t18 = -Hr3+Hr5*Hr6; + var t23 = -Hr3*Hr7+Hr4*Hr6; + t28 = -Hr1+Hr2*Hr7; + var t31 = Hr0-t12; + t35 = Hr0*Hr7-t10; + t41 = -Hr1*Hr5+Hr2*Hr4; + var t44 = t5-t8; + var t47 = t4-t7; + t48 = t2*t15; + var t49 = t28*t15; + var t50 = t41*t15; + var mat = model.data; + mat[0] = Hl0*t48+Hl1*(t18*t15)-Hl2*(t23*t15); + mat[1] = Hl0*t49+Hl1*(t31*t15)-Hl2*(t35*t15); + mat[2] = -Hl0*t50-Hl1*(t44*t15)+Hl2*(t47*t15); + mat[3] = Hl3*t48+Hl4*(t18*t15)-Hl5*(t23*t15); + mat[4] = Hl3*t49+Hl4*(t31*t15)-Hl5*(t35*t15); + mat[5] = -Hl3*t50-Hl4*(t44*t15)+Hl5*(t47*t15); + mat[6] = Hl6*t48+Hl7*(t18*t15)-t23*t15; + mat[7] = Hl6*t49+Hl7*(t31*t15)-t35*t15; + mat[8] = -Hl6*t50-Hl7*(t44*t15)+t47*t15; + }, + + // The current implementation was derived from *BSD system qsort(): + // Copyright (c) 1992, 1993 + // The Regents of the University of California. All rights reserved. + qsort: function(array, low, high, cmp) { + var isort_thresh = 7; + var t,ta,tb,tc; + var sp = 0,left=0,right=0,i=0,n=0,m=0,ptr=0,ptr2=0,d=0; + var left0=0,left1=0,right0=0,right1=0,pivot=0,a=0,b=0,c=0,swap_cnt=0; + + var stack = qsort_stack; + + if( (high-low+1) <= 1 ) return; + + stack[0] = low; + stack[1] = high; + + while( sp >= 0 ) { + + left = stack[sp<<1]; + right = stack[(sp<<1)+1]; + sp--; + + for(;;) { + n = (right - left) + 1; + + if( n <= isort_thresh ) { + //insert_sort: + for( ptr = left + 1; ptr <= right; ptr++ ) { + for( ptr2 = ptr; ptr2 > left && cmp(array[ptr2],array[ptr2-1]); ptr2--) { + t = array[ptr2]; + array[ptr2] = array[ptr2-1]; + array[ptr2-1] = t; + } + } + break; + } else { + swap_cnt = 0; + + left0 = left; + right0 = right; + pivot = left + (n>>1); + + if( n > 40 ) { + d = n >> 3; + a = left, b = left + d, c = left + (d<<1); + ta = array[a],tb = array[b],tc = array[c]; + left = cmp(ta, tb) ? (cmp(tb, tc) ? b : (cmp(ta, tc) ? c : a)) + : (cmp(tc, tb) ? b : (cmp(ta, tc) ? a : c)); + + a = pivot - d, b = pivot, c = pivot + d; + ta = array[a],tb = array[b],tc = array[c]; + pivot = cmp(ta, tb) ? (cmp(tb, tc) ? b : (cmp(ta, tc) ? c : a)) + : (cmp(tc, tb) ? b : (cmp(ta, tc) ? a : c)); + + a = right - (d<<1), b = right - d, c = right; + ta = array[a],tb = array[b],tc = array[c]; + right = cmp(ta, tb) ? (cmp(tb, tc) ? b : (cmp(ta, tc) ? c : a)) + : (cmp(tc, tb) ? b : (cmp(ta, tc) ? a : c)); + } + + a = left, b = pivot, c = right; + ta = array[a],tb = array[b],tc = array[c]; + pivot = cmp(ta, tb) ? (cmp(tb, tc) ? b : (cmp(ta, tc) ? c : a)) + : (cmp(tc, tb) ? b : (cmp(ta, tc) ? a : c)); + if( pivot != left0 ) { + t = array[pivot]; + array[pivot] = array[left0]; + array[left0] = t; + pivot = left0; + } + left = left1 = left0 + 1; + right = right1 = right0; + + ta = array[pivot]; + for(;;) { + while( left <= right && !cmp(ta, array[left]) ) { + if( !cmp(array[left], ta) ) { + if( left > left1 ) { + t = array[left1]; + array[left1] = array[left]; + array[left] = t; + } + swap_cnt = 1; + left1++; + } + left++; + } + + while( left <= right && !cmp(array[right], ta) ) { + if( !cmp(ta, array[right]) ) { + if( right < right1 ) { + t = array[right1]; + array[right1] = array[right]; + array[right] = t; + } + swap_cnt = 1; + right1--; + } + right--; + } + + if( left > right ) break; + + t = array[left]; + array[left] = array[right]; + array[right] = t; + swap_cnt = 1; + left++; + right--; + } + + if( swap_cnt == 0 ) { + left = left0, right = right0; + //goto insert_sort; + for( ptr = left + 1; ptr <= right; ptr++ ) { + for( ptr2 = ptr; ptr2 > left && cmp(array[ptr2],array[ptr2-1]); ptr2--) { + t = array[ptr2]; + array[ptr2] = array[ptr2-1]; + array[ptr2-1] = t; + } + } + break; + } + + n = Math.min( (left1 - left0), (left - left1) ); + m = (left-n)|0; + for( i = 0; i < n; ++i,++m ) { + t = array[left0+i]; + array[left0+i] = array[m]; + array[m] = t; + } + + n = Math.min( (right0 - right1), (right1 - right) ); + m = (right0-n+1)|0; + for( i = 0; i < n; ++i,++m ) { + t = array[left+i]; + array[left+i] = array[m]; + array[m] = t; + } + n = (left - left1); + m = (right1 - right); + if( n > 1 ) { + if( m > 1 ) { + if( n > m ) { + ++sp; + stack[sp<<1] = left0; + stack[(sp<<1)+1] = left0 + n - 1; + left = right0 - m + 1, right = right0; + } else { + ++sp; + stack[sp<<1] = right0 - m + 1; + stack[(sp<<1)+1] = right0; + left = left0, right = left0 + n - 1; + } + } else { + left = left0, right = left0 + n - 1; + } + } + else if( m > 1 ) + left = right0 - m + 1, right = right0; + else + break; + } + } + } + }, + + median: function(array, low, high) { + var w; + var middle=0,ll=0,hh=0,median=(low+high)>>1; + for (;;) { + if (high <= low) return array[median]; + if (high == (low + 1)) { + if (array[low] > array[high]) { + w = array[low]; + array[low] = array[high]; + array[high] = w; + } + return array[median]; + } + middle = ((low + high) >> 1); + if (array[middle] > array[high]) { + w = array[middle]; + array[middle] = array[high]; + array[high] = w; + } + if (array[low] > array[high]) { + w = array[low]; + array[low] = array[high]; + array[high] = w; + } + if (array[middle] > array[low]) { + w = array[middle]; + array[middle] = array[low]; + array[low] = w; + } + ll = (low + 1); + w = array[middle]; + array[middle] = array[ll]; + array[ll] = w; + hh = high; + for (;;) { + do ++ll; while (array[low] > array[ll]); + do --hh; while (array[hh] > array[low]); + if (hh < ll) break; + w = array[ll]; + array[ll] = array[hh]; + array[hh] = w; + } + w = array[low]; + array[low] = array[hh]; + array[hh] = w; + if (hh <= median) + low = ll; + else if (hh >= median) + high = (hh - 1); + } + return 0; + } + }; + + })(); + + global.math = math; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + */ + +(function(global) { + "use strict"; + // + + var matmath = (function() { + + return { + identity: function(M, value) { + if (typeof value === "undefined") { value=1; } + var src=M.data; + var rows=M.rows, cols=M.cols, cols_1=(cols+1)|0; + var len = rows * cols; + var k = len; + while(--len >= 0) src[len] = 0.0; + len = k; + k = 0; + while(k < len) { + src[k] = value; + k = k + cols_1; + } + }, + + transpose: function(At, A) { + var i=0,j=0,nrows=A.rows,ncols=A.cols; + var Ai=0,Ati=0,pAt=0; + var ad=A.data,atd=At.data; + + for (; i < nrows; Ati += 1, Ai += ncols, i++) { + pAt = Ati; + for (j = 0; j < ncols; pAt += nrows, j++) atd[pAt] = ad[Ai+j]; + } + }, + + // C = A * B + multiply: function(C, A, B) { + var i=0,j=0,k=0; + var Ap=0,pA=0,pB=0,p_B=0,Cp=0; + var ncols=A.cols,nrows=A.rows,mcols=B.cols; + var ad=A.data,bd=B.data,cd=C.data; + var sum=0.0; + + for (; i < nrows; Ap += ncols, i++) { + for (p_B = 0, j = 0; j < mcols; Cp++, p_B++, j++) { + pB = p_B; + pA = Ap; + sum = 0.0; + for (k = 0; k < ncols; pA++, pB += mcols, k++) { + sum += ad[pA] * bd[pB]; + } + cd[Cp] = sum; + } + } + }, + + // C = A * B' + multiply_ABt: function(C, A, B) { + var i=0,j=0,k=0; + var Ap=0,pA=0,pB=0,Cp=0; + var ncols=A.cols,nrows=A.rows,mrows=B.rows; + var ad=A.data,bd=B.data,cd=C.data; + var sum=0.0; + + for (; i < nrows; Ap += ncols, i++) { + for (pB = 0, j = 0; j < mrows; Cp++, j++) { + pA = Ap; + sum = 0.0; + for (k = 0; k < ncols; pA++, pB++, k++) { + sum += ad[pA] * bd[pB]; + } + cd[Cp] = sum; + } + } + }, + + // C = A' * B + multiply_AtB: function(C, A, B) { + var i=0,j=0,k=0; + var Ap=0,pA=0,pB=0,p_B=0,Cp=0; + var ncols=A.cols,nrows=A.rows,mcols=B.cols; + var ad=A.data,bd=B.data,cd=C.data; + var sum=0.0; + + for (; i < ncols; Ap++, i++) { + for (p_B = 0, j = 0; j < mcols; Cp++, p_B++, j++) { + pB = p_B; + pA = Ap; + sum = 0.0; + for (k = 0; k < nrows; pA += ncols, pB += mcols, k++) { + sum += ad[pA] * bd[pB]; + } + cd[Cp] = sum; + } + } + }, + + // C = A * A' + multiply_AAt: function(C, A) { + var i=0,j=0,k=0; + var pCdiag=0,p_A=0,pA=0,pB=0,pC=0,pCt=0; + var ncols=A.cols,nrows=A.rows; + var ad=A.data,cd=C.data; + var sum=0.0; + + for (; i < nrows; pCdiag += nrows + 1, p_A = pA, i++) { + pC = pCdiag; + pCt = pCdiag; + pB = p_A; + for (j = i; j < nrows; pC++, pCt += nrows, j++) { + pA = p_A; + sum = 0.0; + for (k = 0; k < ncols; k++) { + sum += ad[pA++] * ad[pB++]; + } + cd[pC] = sum + cd[pCt] = sum; + } + } + }, + + // C = A' * A + multiply_AtA: function(C, A) { + var i=0,j=0,k=0; + var p_A=0,pA=0,pB=0,p_C=0,pC=0,p_CC=0; + var ncols=A.cols,nrows=A.rows; + var ad=A.data,cd=C.data; + var sum=0.0; + + for (; i < ncols; p_C += ncols, i++) { + p_A = i; + p_CC = p_C + i; + pC = p_CC; + for (j = i; j < ncols; pC++, p_CC += ncols, j++) { + pA = p_A; + pB = j; + sum = 0.0; + for (k = 0; k < nrows; pA += ncols, pB += ncols, k++) { + sum += ad[pA] * ad[pB]; + } + cd[pC] = sum + cd[p_CC] = sum; + } + } + }, + + // various small matrix operations + identity_3x3: function(M, value) { + if (typeof value === "undefined") { value=1; } + var dt=M.data; + dt[0] = dt[4] = dt[8] = value; + dt[1] = dt[2] = dt[3] = 0; + dt[5] = dt[6] = dt[7] = 0; + }, + + invert_3x3: function(from, to) { + var A = from.data, invA = to.data; + var t1 = A[4]; + var t2 = A[8]; + var t4 = A[5]; + var t5 = A[7]; + var t8 = A[0]; + + var t9 = t8*t1; + var t11 = t8*t4; + var t13 = A[3]; + var t14 = A[1]; + var t15 = t13*t14; + var t17 = A[2]; + var t18 = t13*t17; + var t20 = A[6]; + var t21 = t20*t14; + var t23 = t20*t17; + var t26 = 1.0/(t9*t2-t11*t5-t15*t2+t18*t5+t21*t4-t23*t1); + invA[0] = (t1*t2-t4*t5)*t26; + invA[1] = -(t14*t2-t17*t5)*t26; + invA[2] = -(-t14*t4+t17*t1)*t26; + invA[3] = -(t13*t2-t4*t20)*t26; + invA[4] = (t8*t2-t23)*t26; + invA[5] = -(t11-t18)*t26; + invA[6] = -(-t13*t5+t1*t20)*t26; + invA[7] = -(t8*t5-t21)*t26; + invA[8] = (t9-t15)*t26; + }, + // C = A * B + multiply_3x3: function(C, A, B) { + var Cd=C.data, Ad=A.data, Bd=B.data; + var m1_0 = Ad[0], m1_1 = Ad[1], m1_2 = Ad[2]; + var m1_3 = Ad[3], m1_4 = Ad[4], m1_5 = Ad[5]; + var m1_6 = Ad[6], m1_7 = Ad[7], m1_8 = Ad[8]; + + var m2_0 = Bd[0], m2_1 = Bd[1], m2_2 = Bd[2]; + var m2_3 = Bd[3], m2_4 = Bd[4], m2_5 = Bd[5]; + var m2_6 = Bd[6], m2_7 = Bd[7], m2_8 = Bd[8]; + + Cd[0] = m1_0 * m2_0 + m1_1 * m2_3 + m1_2 * m2_6; + Cd[1] = m1_0 * m2_1 + m1_1 * m2_4 + m1_2 * m2_7; + Cd[2] = m1_0 * m2_2 + m1_1 * m2_5 + m1_2 * m2_8; + Cd[3] = m1_3 * m2_0 + m1_4 * m2_3 + m1_5 * m2_6; + Cd[4] = m1_3 * m2_1 + m1_4 * m2_4 + m1_5 * m2_7; + Cd[5] = m1_3 * m2_2 + m1_4 * m2_5 + m1_5 * m2_8; + Cd[6] = m1_6 * m2_0 + m1_7 * m2_3 + m1_8 * m2_6; + Cd[7] = m1_6 * m2_1 + m1_7 * m2_4 + m1_8 * m2_7; + Cd[8] = m1_6 * m2_2 + m1_7 * m2_5 + m1_8 * m2_8; + }, + + mat3x3_determinant: function(M) { + var md=M.data; + return md[0] * md[4] * md[8] - + md[0] * md[5] * md[7] - + md[3] * md[1] * md[8] + + md[3] * md[2] * md[7] + + md[6] * md[1] * md[5] - + md[6] * md[2] * md[4]; + }, + + determinant_3x3: function(M11, M12, M13, + M21, M22, M23, + M31, M32, M33) { + return M11 * M22 * M33 - M11 * M23 * M32 - + M21 * M12 * M33 + M21 * M13 * M32 + + M31 * M12 * M23 - M31 * M13 * M22; + } + }; + + })(); + + global.matmath = matmath; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + */ + +(function(global) { + "use strict"; + // + + var linalg = (function() { + + var swap = function(A, i0, i1, t) { + t = A[i0]; + A[i0] = A[i1]; + A[i1] = t; + } + + var hypot = function(a, b) { + a = Math.abs(a); + b = Math.abs(b); + if( a > b ) { + b /= a; + return a*Math.sqrt(1.0 + b*b); + } + if( b > 0 ) { + a /= b; + return b*Math.sqrt(1.0 + a*a); + } + return 0.0; + } + + var JacobiImpl = function(A, astep, W, V, vstep, n) { + var eps = jsfeat.EPSILON; + var i=0,j=0,k=0,m=0,l=0,idx=0,_in=0,_in2=0; + var iters=0,max_iter=n*n*30; + var mv=0.0,val=0.0,p=0.0,y=0.0,t=0.0,s=0.0,c=0.0,a0=0.0,b0=0.0; + + var indR_buff = jsfeat.cache.get_buffer(n<<2); + var indC_buff = jsfeat.cache.get_buffer(n<<2); + var indR = indR_buff.i32; + var indC = indC_buff.i32; + + if(V) { + for(; i < n; i++) { + k = i*vstep; + for(j = 0; j < n; j++) { + V[k + j] = 0.0; + } + V[k + i] = 1.0; + } + } + + for(k = 0; k < n; k++) { + W[k] = A[(astep + 1)*k]; + if(k < n - 1) { + for(m = k+1, mv = Math.abs(A[astep*k + m]), i = k+2; i < n; i++) { + val = Math.abs(A[astep*k+i]); + if(mv < val) + mv = val, m = i; + } + indR[k] = m; + } + if(k > 0) { + for(m = 0, mv = Math.abs(A[k]), i = 1; i < k; i++) { + val = Math.abs(A[astep*i+k]); + if(mv < val) + mv = val, m = i; + } + indC[k] = m; + } + } + + if(n > 1) for( ; iters < max_iter; iters++) { + // find index (k,l) of pivot p + for(k = 0, mv = Math.abs(A[indR[0]]), i = 1; i < n-1; i++) { + val = Math.abs(A[astep*i + indR[i]]); + if( mv < val ) + mv = val, k = i; + } + l = indR[k]; + for(i = 1; i < n; i++) { + val = Math.abs(A[astep*indC[i] + i]); + if( mv < val ) + mv = val, k = indC[i], l = i; + } + + p = A[astep*k + l]; + + if(Math.abs(p) <= eps) break; + + y = (W[l] - W[k])*0.5; + t = Math.abs(y) + hypot(p, y); + s = hypot(p, t); + c = t/s; + s = p/s; t = (p/t)*p; + if(y < 0) + s = -s, t = -t; + A[astep*k + l] = 0; + + W[k] -= t; + W[l] += t; + + // rotate rows and columns k and l + for (i = 0; i < k; i++) { + _in = (astep * i + k); + _in2 = (astep * i + l); + a0 = A[_in]; + b0 = A[_in2]; + A[_in] = a0 * c - b0 * s; + A[_in2] = a0 * s + b0 * c; + } + for (i = (k + 1); i < l; i++) { + _in = (astep * k + i); + _in2 = (astep * i + l); + a0 = A[_in]; + b0 = A[_in2]; + A[_in] = a0 * c - b0 * s; + A[_in2] = a0 * s + b0 * c; + } + i = l + 1; + _in = (astep * k + i); + _in2 = (astep * l + i); + for (; i < n; i++, _in++, _in2++) { + a0 = A[_in]; + b0 = A[_in2]; + A[_in] = a0 * c - b0 * s; + A[_in2] = a0 * s + b0 * c; + } + + // rotate eigenvectors + if (V) { + _in = vstep * k; + _in2 = vstep * l; + for (i = 0; i < n; i++, _in++, _in2++) { + a0 = V[_in]; + b0 = V[_in2]; + V[_in] = a0 * c - b0 * s; + V[_in2] = a0 * s + b0 * c; + } + } + + for(j = 0; j < 2; j++) { + idx = j == 0 ? k : l; + if(idx < n - 1) { + for(m = idx+1, mv = Math.abs(A[astep*idx + m]), i = idx+2; i < n; i++) { + val = Math.abs(A[astep*idx+i]); + if( mv < val ) + mv = val, m = i; + } + indR[idx] = m; + } + if(idx > 0) { + for(m = 0, mv = Math.abs(A[idx]), i = 1; i < idx; i++) { + val = Math.abs(A[astep*i+idx]); + if( mv < val ) + mv = val, m = i; + } + indC[idx] = m; + } + } + } + + // sort eigenvalues & eigenvectors + for(k = 0; k < n-1; k++) { + m = k; + for(i = k+1; i < n; i++) { + if(W[m] < W[i]) + m = i; + } + if(k != m) { + swap(W, m, k, mv); + if(V) { + for(i = 0; i < n; i++) { + swap(V, vstep*m + i, vstep*k + i, mv); + } + } + } + } + + + jsfeat.cache.put_buffer(indR_buff); + jsfeat.cache.put_buffer(indC_buff); + } + + var JacobiSVDImpl = function(At, astep, _W, Vt, vstep, m, n, n1) { + var eps = jsfeat.EPSILON * 2.0; + var minval = jsfeat.FLT_MIN; + var i=0,j=0,k=0,iter=0,max_iter=Math.max(m, 30); + var Ai=0,Aj=0,Vi=0,Vj=0,changed=0; + var c=0.0, s=0.0, t=0.0; + var t0=0.0,t1=0.0,sd=0.0,beta=0.0,gamma=0.0,delta=0.0,a=0.0,p=0.0,b=0.0; + var seed = 0x1234; + var val=0.0,val0=0.0,asum=0.0; + + var W_buff = jsfeat.cache.get_buffer(n<<3); + var W = W_buff.f64; + + for(; i < n; i++) { + for(k = 0, sd = 0; k < m; k++) { + t = At[i*astep + k]; + sd += t*t; + } + W[i] = sd; + + if(Vt) { + for(k = 0; k < n; k++) { + Vt[i*vstep + k] = 0; + } + Vt[i*vstep + i] = 1; + } + } + + for(; iter < max_iter; iter++) { + changed = 0; + + for(i = 0; i < n-1; i++) { + for(j = i+1; j < n; j++) { + Ai = (i*astep)|0, Aj = (j*astep)|0; + a = W[i], p = 0, b = W[j]; + + k = 2; + p += At[Ai]*At[Aj]; + p += At[Ai+1]*At[Aj+1]; + + for(; k < m; k++) + p += At[Ai+k]*At[Aj+k]; + + if(Math.abs(p) <= eps*Math.sqrt(a*b)) continue; + + p *= 2.0; + beta = a - b, gamma = hypot(p, beta); + if( beta < 0 ) { + delta = (gamma - beta)*0.5; + s = Math.sqrt(delta/gamma); + c = (p/(gamma*s*2.0)); + } else { + c = Math.sqrt((gamma + beta)/(gamma*2.0)); + s = (p/(gamma*c*2.0)); + } + + a=0.0, b=0.0; + + k = 2; // unroll + t0 = c*At[Ai] + s*At[Aj]; + t1 = -s*At[Ai] + c*At[Aj]; + At[Ai] = t0; At[Aj] = t1; + a += t0*t0; b += t1*t1; + + t0 = c*At[Ai+1] + s*At[Aj+1]; + t1 = -s*At[Ai+1] + c*At[Aj+1]; + At[Ai+1] = t0; At[Aj+1] = t1; + a += t0*t0; b += t1*t1; + + for( ; k < m; k++ ) + { + t0 = c*At[Ai+k] + s*At[Aj+k]; + t1 = -s*At[Ai+k] + c*At[Aj+k]; + At[Ai+k] = t0; At[Aj+k] = t1; + + a += t0*t0; b += t1*t1; + } + + W[i] = a; W[j] = b; + + changed = 1; + + if(Vt) { + Vi = (i*vstep)|0, Vj = (j*vstep)|0; + + k = 2; + t0 = c*Vt[Vi] + s*Vt[Vj]; + t1 = -s*Vt[Vi] + c*Vt[Vj]; + Vt[Vi] = t0; Vt[Vj] = t1; + + t0 = c*Vt[Vi+1] + s*Vt[Vj+1]; + t1 = -s*Vt[Vi+1] + c*Vt[Vj+1]; + Vt[Vi+1] = t0; Vt[Vj+1] = t1; + + for(; k < n; k++) { + t0 = c*Vt[Vi+k] + s*Vt[Vj+k]; + t1 = -s*Vt[Vi+k] + c*Vt[Vj+k]; + Vt[Vi+k] = t0; Vt[Vj+k] = t1; + } + } + } + } + if(changed == 0) break; + } + + for(i = 0; i < n; i++) { + for(k = 0, sd = 0; k < m; k++) { + t = At[i*astep + k]; + sd += t*t; + } + W[i] = Math.sqrt(sd); + } + + for(i = 0; i < n-1; i++) { + j = i; + for(k = i+1; k < n; k++) { + if(W[j] < W[k]) + j = k; + } + if(i != j) { + swap(W, i, j, sd); + if(Vt) { + for(k = 0; k < m; k++) { + swap(At, i*astep + k, j*astep + k, t); + } + + for(k = 0; k < n; k++) { + swap(Vt, i*vstep + k, j*vstep + k, t); + } + } + } + } + + for(i = 0; i < n; i++) { + _W[i] = W[i]; + } + + if(!Vt) { + jsfeat.cache.put_buffer(W_buff); + return; + } + + for(i = 0; i < n1; i++) { + + sd = i < n ? W[i] : 0; + + while(sd <= minval) { + // if we got a zero singular value, then in order to get the corresponding left singular vector + // we generate a random vector, project it to the previously computed left singular vectors, + // subtract the projection and normalize the difference. + val0 = (1.0/m); + for(k = 0; k < m; k++) { + seed = (seed * 214013 + 2531011); + val = (((seed >> 16) & 0x7fff) & 256) != 0 ? val0 : -val0; + At[i*astep + k] = val; + } + for(iter = 0; iter < 2; iter++) { + for(j = 0; j < i; j++) { + sd = 0; + for(k = 0; k < m; k++) { + sd += At[i*astep + k]*At[j*astep + k]; + } + asum = 0.0; + for(k = 0; k < m; k++) { + t = (At[i*astep + k] - sd*At[j*astep + k]); + At[i*astep + k] = t; + asum += Math.abs(t); + } + asum = asum ? 1.0/asum : 0; + for(k = 0; k < m; k++) { + At[i*astep + k] *= asum; + } + } + } + sd = 0; + for(k = 0; k < m; k++) { + t = At[i*astep + k]; + sd += t*t; + } + sd = Math.sqrt(sd); + } + + s = (1.0/sd); + for(k = 0; k < m; k++) { + At[i*astep + k] *= s; + } + } + + jsfeat.cache.put_buffer(W_buff); + } + + return { + + lu_solve: function(A, B) { + var i=0,j=0,k=0,p=1,astep=A.cols; + var ad=A.data, bd=B.data; + var t,alpha,d,s; + + for(i = 0; i < astep; i++) { + k = i; + for(j = i+1; j < astep; j++) { + if(Math.abs(ad[j*astep + i]) > Math.abs(ad[k*astep+i])) { + k = j; + } + } + + if(Math.abs(ad[k*astep+i]) < jsfeat.EPSILON) { + return 0; // FAILED + } + + if(k != i) { + for(j = i; j < astep; j++ ) { + swap(ad, i*astep+j, k*astep+j, t); + } + + swap(bd, i, k, t); + p = -p; + } + + d = -1.0/ad[i*astep+i]; + + for(j = i+1; j < astep; j++) { + alpha = ad[j*astep+i]*d; + + for(k = i+1; k < astep; k++) { + ad[j*astep+k] += alpha*ad[i*astep+k]; + } + + bd[j] += alpha*bd[i]; + } + + ad[i*astep+i] = -d; + } + + for(i = astep-1; i >= 0; i--) { + s = bd[i]; + for(k = i+1; k < astep; k++) { + s -= ad[i*astep+k]*bd[k]; + } + bd[i] = s*ad[i*astep+i]; + } + + return 1; // OK + }, + + cholesky_solve: function(A, B) { + var col=0,row=0,col2=0,cs=0,rs=0,i=0,j=0; + var size = A.cols; + var ad=A.data, bd=B.data; + var val,inv_diag; + + for (col = 0; col < size; col++) { + inv_diag = 1.0; + cs = (col * size); + rs = cs; + for (row = col; row < size; row++) + { + // correct for the parts of cholesky already computed + val = ad[(rs+col)]; + for (col2 = 0; col2 < col; col2++) { + val -= ad[(col2*size+col)] * ad[(rs+col2)]; + } + if (row == col) { + // this is the diagonal element so don't divide + ad[(rs+col)] = val; + if(val == 0) { + return 0; + } + inv_diag = 1.0 / val; + } else { + // cache the value without division in the upper half + ad[(cs+row)] = val; + // divide my the diagonal element for all others + ad[(rs+col)] = val * inv_diag; + } + rs = (rs + size); + } + } + + // first backsub through L + cs = 0; + for (i = 0; i < size; i++) { + val = bd[i]; + for (j = 0; j < i; j++) { + val -= ad[(cs+j)] * bd[j]; + } + bd[i] = val; + cs = (cs + size); + } + // backsub through diagonal + cs = 0; + for (i = 0; i < size; i++) { + bd[i] /= ad[(cs + i)]; + cs = (cs + size); + } + // backsub through L Transpose + i = (size-1); + for (; i >= 0; i--) { + val = bd[i]; + j = (i + 1); + cs = (j * size); + for (; j < size; j++) { + val -= ad[(cs + i)] * bd[j]; + cs = (cs + size); + } + bd[i] = val; + } + + return 1; + }, + + svd_decompose: function(A, W, U, V, options) { + if (typeof options === "undefined") { options = 0; }; + var at=0,i=0,j=0,_m=A.rows,_n=A.cols,m=_m,n=_n; + var dt = A.type | jsfeat.C1_t; // we only work with single channel + + if(m < n) { + at = 1; + i = m; + m = n; + n = i; + } + + var a_buff = jsfeat.cache.get_buffer((m*m)<<3); + var w_buff = jsfeat.cache.get_buffer(n<<3); + var v_buff = jsfeat.cache.get_buffer((n*n)<<3); + + var a_mt = new jsfeat.matrix_t(m, m, dt, a_buff.data); + var w_mt = new jsfeat.matrix_t(1, n, dt, w_buff.data); + var v_mt = new jsfeat.matrix_t(n, n, dt, v_buff.data); + + if(at == 0) { + // transpose + jsfeat.matmath.transpose(a_mt, A); + } else { + for(i = 0; i < _n*_m; i++) { + a_mt.data[i] = A.data[i]; + } + for(; i < n*m; i++) { + a_mt.data[i] = 0; + } + } + + JacobiSVDImpl(a_mt.data, m, w_mt.data, v_mt.data, n, m, n, m); + + if(W) { + for(i=0; i < n; i++) { + W.data[i] = w_mt.data[i]; + } + for(; i < _n; i++) { + W.data[i] = 0; + } + } + + if (at == 0) { + if(U && (options & jsfeat.SVD_U_T)) { + i = m*m; + while(--i >= 0) { + U.data[i] = a_mt.data[i]; + } + } else if(U) { + jsfeat.matmath.transpose(U, a_mt); + } + + if(V && (options & jsfeat.SVD_V_T)) { + i = n*n; + while(--i >= 0) { + V.data[i] = v_mt.data[i]; + } + } else if(V) { + jsfeat.matmath.transpose(V, v_mt); + } + } else { + if(U && (options & jsfeat.SVD_U_T)) { + i = n*n; + while(--i >= 0) { + U.data[i] = v_mt.data[i]; + } + } else if(U) { + jsfeat.matmath.transpose(U, v_mt); + } + + if(V && (options & jsfeat.SVD_V_T)) { + i = m*m; + while(--i >= 0) { + V.data[i] = a_mt.data[i]; + } + } else if(V) { + jsfeat.matmath.transpose(V, a_mt); + } + } + + jsfeat.cache.put_buffer(a_buff); + jsfeat.cache.put_buffer(w_buff); + jsfeat.cache.put_buffer(v_buff); + + }, + + svd_solve: function(A, X, B) { + var i=0,j=0,k=0; + var pu=0,pv=0; + var nrows=A.rows,ncols=A.cols; + var sum=0.0,xsum=0.0,tol=0.0; + var dt = A.type | jsfeat.C1_t; + + var u_buff = jsfeat.cache.get_buffer((nrows*nrows)<<3); + var w_buff = jsfeat.cache.get_buffer(ncols<<3); + var v_buff = jsfeat.cache.get_buffer((ncols*ncols)<<3); + + var u_mt = new jsfeat.matrix_t(nrows, nrows, dt, u_buff.data); + var w_mt = new jsfeat.matrix_t(1, ncols, dt, w_buff.data); + var v_mt = new jsfeat.matrix_t(ncols, ncols, dt, v_buff.data); + + var bd = B.data, ud = u_mt.data, wd = w_mt.data, vd = v_mt.data; + + this.svd_decompose(A, w_mt, u_mt, v_mt, 0); + + tol = jsfeat.EPSILON * wd[0] * ncols; + + for (; i < ncols; i++, pv += ncols) { + xsum = 0.0; + for(j = 0; j < ncols; j++) { + if(wd[j] > tol) { + for(k = 0, sum = 0.0, pu = 0; k < nrows; k++, pu += ncols) { + sum += ud[pu + j] * bd[k]; + } + xsum += sum * vd[pv + j] / wd[j]; + } + } + X.data[i] = xsum; + } + + jsfeat.cache.put_buffer(u_buff); + jsfeat.cache.put_buffer(w_buff); + jsfeat.cache.put_buffer(v_buff); + }, + + svd_invert: function(Ai, A) { + var i=0,j=0,k=0; + var pu=0,pv=0,pa=0; + var nrows=A.rows,ncols=A.cols; + var sum=0.0,tol=0.0; + var dt = A.type | jsfeat.C1_t; + + var u_buff = jsfeat.cache.get_buffer((nrows*nrows)<<3); + var w_buff = jsfeat.cache.get_buffer(ncols<<3); + var v_buff = jsfeat.cache.get_buffer((ncols*ncols)<<3); + + var u_mt = new jsfeat.matrix_t(nrows, nrows, dt, u_buff.data); + var w_mt = new jsfeat.matrix_t(1, ncols, dt, w_buff.data); + var v_mt = new jsfeat.matrix_t(ncols, ncols, dt, v_buff.data); + + var id = Ai.data, ud = u_mt.data, wd = w_mt.data, vd = v_mt.data; + + this.svd_decompose(A, w_mt, u_mt, v_mt, 0); + + tol = jsfeat.EPSILON * wd[0] * ncols; + + for (; i < ncols; i++, pv += ncols) { + for (j = 0, pu = 0; j < nrows; j++, pa++) { + for (k = 0, sum = 0.0; k < ncols; k++, pu++) { + if (wd[k] > tol) sum += vd[pv + k] * ud[pu] / wd[k]; + } + id[pa] = sum; + } + } + + jsfeat.cache.put_buffer(u_buff); + jsfeat.cache.put_buffer(w_buff); + jsfeat.cache.put_buffer(v_buff); + }, + + eigenVV: function(A, vects, vals) { + var n=A.cols,i=n*n; + var dt = A.type | jsfeat.C1_t; + + var a_buff = jsfeat.cache.get_buffer((n*n)<<3); + var w_buff = jsfeat.cache.get_buffer(n<<3); + var a_mt = new jsfeat.matrix_t(n, n, dt, a_buff.data); + var w_mt = new jsfeat.matrix_t(1, n, dt, w_buff.data); + + while(--i >= 0) { + a_mt.data[i] = A.data[i]; + } + + JacobiImpl(a_mt.data, n, w_mt.data, vects ? vects.data : null, n, n); + + if(vals) { + while(--n >= 0) { + vals.data[n] = w_mt.data[n]; + } + } + + jsfeat.cache.put_buffer(a_buff); + jsfeat.cache.put_buffer(w_buff); + } + + }; + + })(); + + global.linalg = linalg; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + */ + +(function(global) { + "use strict"; + // + + var motion_model = (function() { + + var sqr = function(x) { + return x*x; + } + + // does isotropic normalization + var iso_normalize_points = function(from, to, T0, T1, count) { + var i=0; + var cx0=0.0, cy0=0.0, d0=0.0, s0=0.0; + var cx1=0.0, cy1=0.0, d1=0.0, s1=0.0; + var dx=0.0,dy=0.0; + + for (; i < count; ++i) { + cx0 += from[i].x; + cy0 += from[i].y; + cx1 += to[i].x; + cy1 += to[i].y; + } + + cx0 /= count; cy0 /= count; + cx1 /= count; cy1 /= count; + + for (i = 0; i < count; ++i) { + dx = from[i].x - cx0; + dy = from[i].y - cy0; + d0 += Math.sqrt(dx*dx + dy*dy); + dx = to[i].x - cx1; + dy = to[i].y - cy1; + d1 += Math.sqrt(dx*dx + dy*dy); + } + + d0 /= count; d1 /= count; + + s0 = Math.SQRT2 / d0; s1 = Math.SQRT2 / d1; + + T0[0] = T0[4] = s0; + T0[2] = -cx0*s0; + T0[5] = -cy0*s0; + T0[1] = T0[3] = T0[6] = T0[7] = 0.0; + T0[8] = 1.0; + + T1[0] = T1[4] = s1; + T1[2] = -cx1*s1; + T1[5] = -cy1*s1; + T1[1] = T1[3] = T1[6] = T1[7] = 0.0; + T1[8] = 1.0; + } + + var have_collinear_points = function(points, count) { + var j=0,k=0,i=(count-1)|0; + var dx1=0.0,dy1=0.0,dx2=0.0,dy2=0.0; + + // check that the i-th selected point does not belong + // to a line connecting some previously selected points + for(; j < i; ++j) { + dx1 = points[j].x - points[i].x; + dy1 = points[j].y - points[i].y; + for(k = 0; k < j; ++k) { + dx2 = points[k].x - points[i].x; + dy2 = points[k].y - points[i].y; + if( Math.abs(dx2*dy1 - dy2*dx1) <= jsfeat.EPSILON*(Math.abs(dx1) + Math.abs(dy1) + Math.abs(dx2) + Math.abs(dy2))) + return true; + } + } + return false; + } + + var T0 = new jsfeat.matrix_t(3, 3, jsfeat.F32_t|jsfeat.C1_t); + var T1 = new jsfeat.matrix_t(3, 3, jsfeat.F32_t|jsfeat.C1_t); + var AtA = new jsfeat.matrix_t(6, 6, jsfeat.F32_t|jsfeat.C1_t); + var AtB = new jsfeat.matrix_t(6, 1, jsfeat.F32_t|jsfeat.C1_t); + + var affine2d = (function () { + + function affine2d() { + // empty constructor + } + + affine2d.prototype.run = function(from, to, model, count) { + var i=0,j=0; + var dt=model.type|jsfeat.C1_t; + var md=model.data, t0d=T0.data, t1d=T1.data; + var pt0,pt1,px=0.0,py=0.0; + + iso_normalize_points(from, to, t0d, t1d, count); + + var a_buff = jsfeat.cache.get_buffer((2*count*6)<<3); + var b_buff = jsfeat.cache.get_buffer((2*count)<<3); + + var a_mt = new jsfeat.matrix_t(6, 2*count, dt, a_buff.data); + var b_mt = new jsfeat.matrix_t(1, 2*count, dt, b_buff.data); + var ad=a_mt.data, bd=b_mt.data; + + for (; i < count; ++i) { + pt0 = from[i]; + pt1 = to[i]; + + px = t0d[0]*pt0.x + t0d[1]*pt0.y + t0d[2]; + py = t0d[3]*pt0.x + t0d[4]*pt0.y + t0d[5]; + + j = i*2*6; + ad[j]=px, ad[j+1]=py, ad[j+2]=1.0, ad[j+3]=0.0, ad[j+4]=0.0, ad[j+5]=0.0; + + j += 6; + ad[j]=0.0, ad[j+1]=0.0, ad[j+2]=0.0, ad[j+3]=px, ad[j+4]=py, ad[j+5]=1.0; + + bd[i<<1] = t1d[0]*pt1.x + t1d[1]*pt1.y + t1d[2]; + bd[(i<<1)+1] = t1d[3]*pt1.x + t1d[4]*pt1.y + t1d[5]; + } + + jsfeat.matmath.multiply_AtA(AtA, a_mt); + jsfeat.matmath.multiply_AtB(AtB, a_mt, b_mt); + + jsfeat.linalg.lu_solve(AtA, AtB); + + md[0] = AtB.data[0], md[1]=AtB.data[1], md[2]=AtB.data[2]; + md[3] = AtB.data[3], md[4]=AtB.data[4], md[5]=AtB.data[5]; + md[6] = 0.0, md[7] = 0.0, md[8] = 1.0; // fill last row + + // denormalize + jsfeat.matmath.invert_3x3(T1, T1); + jsfeat.matmath.multiply_3x3(model, T1, model); + jsfeat.matmath.multiply_3x3(model, model, T0); + + // free buffer + jsfeat.cache.put_buffer(a_buff); + jsfeat.cache.put_buffer(b_buff); + + return 1; + } + + affine2d.prototype.error = function(from, to, model, err, count) { + var i=0; + var pt0,pt1; + var m=model.data; + + for (; i < count; ++i) { + pt0 = from[i]; + pt1 = to[i]; + + err[i] = sqr(pt1.x - m[0]*pt0.x - m[1]*pt0.y - m[2]) + + sqr(pt1.y - m[3]*pt0.x - m[4]*pt0.y - m[5]); + } + } + + affine2d.prototype.check_subset = function(from, to, count) { + return true; // all good + } + + return affine2d; + })(); + + var mLtL = new jsfeat.matrix_t(9, 9, jsfeat.F32_t|jsfeat.C1_t); + var Evec = new jsfeat.matrix_t(9, 9, jsfeat.F32_t|jsfeat.C1_t); + + var homography2d = (function () { + + function homography2d() { + // empty constructor + //this.T0 = new jsfeat.matrix_t(3, 3, jsfeat.F32_t|jsfeat.C1_t); + //this.T1 = new jsfeat.matrix_t(3, 3, jsfeat.F32_t|jsfeat.C1_t); + //this.mLtL = new jsfeat.matrix_t(9, 9, jsfeat.F32_t|jsfeat.C1_t); + //this.Evec = new jsfeat.matrix_t(9, 9, jsfeat.F32_t|jsfeat.C1_t); + } + + homography2d.prototype.run = function(from, to, model, count) { + var i=0,j=0; + var md=model.data, t0d=T0.data, t1d=T1.data; + var LtL=mLtL.data, evd=Evec.data; + var x=0.0,y=0.0,X=0.0,Y=0.0; + + // norm + var smx=0.0, smy=0.0, cmx=0.0, cmy=0.0, sMx=0.0, sMy=0.0, cMx=0.0, cMy=0.0; + + for(; i < count; ++i) { + cmx += to[i].x; + cmy += to[i].y; + cMx += from[i].x; + cMy += from[i].y; + } + + cmx /= count; cmy /= count; + cMx /= count; cMy /= count; + + for(i = 0; i < count; ++i) + { + smx += Math.abs(to[i].x - cmx); + smy += Math.abs(to[i].y - cmy); + sMx += Math.abs(from[i].x - cMx); + sMy += Math.abs(from[i].y - cMy); + } + + if( Math.abs(smx) < jsfeat.EPSILON + || Math.abs(smy) < jsfeat.EPSILON + || Math.abs(sMx) < jsfeat.EPSILON + || Math.abs(sMy) < jsfeat.EPSILON ) return 0; + + smx = count/smx; smy = count/smy; + sMx = count/sMx; sMy = count/sMy; + + t0d[0] = sMx; t0d[1] = 0; t0d[2] = -cMx*sMx; + t0d[3] = 0; t0d[4] = sMy; t0d[5] = -cMy*sMy; + t0d[6] = 0; t0d[7] = 0; t0d[8] = 1; + + t1d[0] = 1.0/smx; t1d[1] = 0; t1d[2] = cmx; + t1d[3] = 0; t1d[4] = 1.0/smy; t1d[5] = cmy; + t1d[6] = 0; t1d[7] = 0; t1d[8] = 1; + // + + // construct system + i = 81; + while(--i >= 0) { + LtL[i] = 0.0; + } + for(i = 0; i < count; ++i) { + x = (to[i].x - cmx) * smx; + y = (to[i].y - cmy) * smy; + X = (from[i].x - cMx) * sMx; + Y = (from[i].y - cMy) * sMy; + + LtL[0] += X*X; + LtL[1] += X*Y; + LtL[2] += X; + + LtL[6] += X*-x*X; + LtL[7] += X*-x*Y; + LtL[8] += X*-x; + LtL[10] += Y*Y; + LtL[11] += Y; + + LtL[15] += Y*-x*X; + LtL[16] += Y*-x*Y; + LtL[17] += Y*-x; + LtL[20] += 1.0; + + LtL[24] += -x*X; + LtL[25] += -x*Y; + LtL[26] += -x; + LtL[30] += X*X; + LtL[31] += X*Y; + LtL[32] += X; + LtL[33] += X*-y*X; + LtL[34] += X*-y*Y; + LtL[35] += X*-y; + LtL[40] += Y*Y; + LtL[41] += Y; + LtL[42] += Y*-y*X; + LtL[43] += Y*-y*Y; + LtL[44] += Y*-y; + LtL[50] += 1.0; + LtL[51] += -y*X; + LtL[52] += -y*Y; + LtL[53] += -y; + LtL[60] += -x*X*-x*X + -y*X*-y*X; + LtL[61] += -x*X*-x*Y + -y*X*-y*Y; + LtL[62] += -x*X*-x + -y*X*-y; + LtL[70] += -x*Y*-x*Y + -y*Y*-y*Y; + LtL[71] += -x*Y*-x + -y*Y*-y; + LtL[80] += -x*-x + -y*-y; + } + // + + // symmetry + for(i = 0; i < 9; ++i) { + for(j = 0; j < i; ++j) + LtL[i*9+j] = LtL[j*9+i]; + } + + jsfeat.linalg.eigenVV(mLtL, Evec); + + md[0]=evd[72], md[1]=evd[73], md[2]=evd[74]; + md[3]=evd[75], md[4]=evd[76], md[5]=evd[77]; + md[6]=evd[78], md[7]=evd[79], md[8]=evd[80]; + + // denormalize + jsfeat.matmath.multiply_3x3(model, T1, model); + jsfeat.matmath.multiply_3x3(model, model, T0); + + // set bottom right to 1.0 + x = 1.0/md[8]; + md[0] *= x; md[1] *= x; md[2] *= x; + md[3] *= x; md[4] *= x; md[5] *= x; + md[6] *= x; md[7] *= x; md[8] = 1.0; + + return 1; + } + + homography2d.prototype.error = function(from, to, model, err, count) { + var i=0; + var pt0,pt1,ww=0.0,dx=0.0,dy=0.0; + var m=model.data; + + for (; i < count; ++i) { + pt0 = from[i]; + pt1 = to[i]; + + ww = 1.0/(m[6]*pt0.x + m[7]*pt0.y + 1.0); + dx = (m[0]*pt0.x + m[1]*pt0.y + m[2])*ww - pt1.x; + dy = (m[3]*pt0.x + m[4]*pt0.y + m[5])*ww - pt1.y; + err[i] = (dx*dx + dy*dy); + } + } + + homography2d.prototype.check_subset = function(from, to, count) { + // seems to reject good subsets actually + //if( have_collinear_points(from, count) || have_collinear_points(to, count) ) { + //return false; + //} + if( count == 4 ) { + var negative = 0; + + var fp0=from[0],fp1=from[1],fp2=from[2],fp3=from[3]; + var tp0=to[0],tp1=to[1],tp2=to[2],tp3=to[3]; + + // set1 + var A11=fp0.x, A12=fp0.y, A13=1.0; + var A21=fp1.x, A22=fp1.y, A23=1.0; + var A31=fp2.x, A32=fp2.y, A33=1.0; + + var B11=tp0.x, B12=tp0.y, B13=1.0; + var B21=tp1.x, B22=tp1.y, B23=1.0; + var B31=tp2.x, B32=tp2.y, B33=1.0; + + var detA = jsfeat.matmath.determinant_3x3(A11,A12,A13, A21,A22,A23, A31,A32,A33); + var detB = jsfeat.matmath.determinant_3x3(B11,B12,B13, B21,B22,B23, B31,B32,B33); + + if(detA*detB < 0) negative++; + + // set2 + A11=fp1.x, A12=fp1.y; + A21=fp2.x, A22=fp2.y; + A31=fp3.x, A32=fp3.y; + + B11=tp1.x, B12=tp1.y; + B21=tp2.x, B22=tp2.y; + B31=tp3.x, B32=tp3.y; + + detA = jsfeat.matmath.determinant_3x3(A11,A12,A13, A21,A22,A23, A31,A32,A33); + detB = jsfeat.matmath.determinant_3x3(B11,B12,B13, B21,B22,B23, B31,B32,B33); + + if(detA*detB < 0) negative++; + + // set3 + A11=fp0.x, A12=fp0.y; + A21=fp2.x, A22=fp2.y; + A31=fp3.x, A32=fp3.y; + + B11=tp0.x, B12=tp0.y; + B21=tp2.x, B22=tp2.y; + B31=tp3.x, B32=tp3.y; + + detA = jsfeat.matmath.determinant_3x3(A11,A12,A13, A21,A22,A23, A31,A32,A33); + detB = jsfeat.matmath.determinant_3x3(B11,B12,B13, B21,B22,B23, B31,B32,B33); + + if(detA*detB < 0) negative++; + + // set4 + A11=fp0.x, A12=fp0.y; + A21=fp1.x, A22=fp1.y; + A31=fp3.x, A32=fp3.y; + + B11=tp0.x, B12=tp0.y; + B21=tp1.x, B22=tp1.y; + B31=tp3.x, B32=tp3.y; + + detA = jsfeat.matmath.determinant_3x3(A11,A12,A13, A21,A22,A23, A31,A32,A33); + detB = jsfeat.matmath.determinant_3x3(B11,B12,B13, B21,B22,B23, B31,B32,B33); + + if(detA*detB < 0) negative++; + + if(negative != 0 && negative != 4) { + return false; + } + } + return true; // all good + } + + return homography2d; + })(); + + return { + + affine2d:affine2d, + homography2d:homography2d + + }; + + })(); + + var ransac_params_t = (function () { + function ransac_params_t(size, thresh, eps, prob) { + if (typeof size === "undefined") { size=0; } + if (typeof thresh === "undefined") { thresh=0.5; } + if (typeof eps === "undefined") { eps=0.5; } + if (typeof prob === "undefined") { prob=0.99; } + + this.size = size; + this.thresh = thresh; + this.eps = eps; + this.prob = prob; + }; + ransac_params_t.prototype.update_iters = function(_eps, max_iters) { + var num = Math.log(1 - this.prob); + var denom = Math.log(1 - Math.pow(1 - _eps, this.size)); + return (denom >= 0 || -num >= max_iters*(-denom) ? max_iters : Math.round(num/denom))|0; + }; + return ransac_params_t; + })(); + + var motion_estimator = (function() { + + var get_subset = function(kernel, from, to, need_cnt, max_cnt, from_sub, to_sub) { + var max_try = 1000; + var indices = []; + var i=0, j=0, ssiter=0, idx_i=0, ok=false; + for(; ssiter < max_try; ++ssiter) { + i = 0; + for (; i < need_cnt && ssiter < max_try;) { + ok = false; + idx_i = 0; + while (!ok) { + ok = true; + idx_i = indices[i] = Math.floor(Math.random() * max_cnt)|0; + for (j = 0; j < i; ++j) { + if (idx_i == indices[j]) + { ok = false; break; } + } + } + from_sub[i] = from[idx_i]; + to_sub[i] = to[idx_i]; + if( !kernel.check_subset( from_sub, to_sub, i+1 ) ) { + ssiter++; + continue; + } + ++i; + } + break; + } + + return (i == need_cnt && ssiter < max_try); + } + + var find_inliers = function(kernel, model, from, to, count, thresh, err, mask) { + var numinliers = 0, i=0, f=0; + var t = thresh*thresh; + + kernel.error(from, to, model, err, count); + + for(; i < count; ++i) { + f = err[i] <= t; + mask[i] = f; + numinliers += f; + } + return numinliers; + } + + return { + + ransac: function(params, kernel, from, to, count, model, mask, max_iters) { + if (typeof max_iters === "undefined") { max_iters=1000; } + + if(count < params.size) return false; + + var model_points = params.size; + var niters = max_iters, iter=0; + var result = false; + + var subset0 = []; + var subset1 = []; + var found = false; + + var mc=model.cols,mr=model.rows; + var dt = model.type | jsfeat.C1_t; + + var m_buff = jsfeat.cache.get_buffer((mc*mr)<<3); + var ms_buff = jsfeat.cache.get_buffer(count); + var err_buff = jsfeat.cache.get_buffer(count<<2); + var M = new jsfeat.matrix_t(mc, mr, dt, m_buff.data); + var curr_mask = new jsfeat.matrix_t(count, 1, jsfeat.U8C1_t, ms_buff.data); + + var inliers_max = -1, numinliers=0; + var nmodels = 0; + + var err = err_buff.f32; + + // special case + if(count == model_points) { + if(kernel.run(from, to, M, count) <= 0) { + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + return false; + } + + M.copy_to(model); + if(mask) { + while(--count >= 0) { + mask.data[count] = 1; + } + } + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + return true; + } + + for (; iter < niters; ++iter) { + // generate subset + found = get_subset(kernel, from, to, model_points, count, subset0, subset1); + if(!found) { + if(iter == 0) { + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + return false; + } + break; + } + + nmodels = kernel.run( subset0, subset1, M, model_points ); + if(nmodels <= 0) + continue; + + // TODO handle multimodel output + + numinliers = find_inliers(kernel, M, from, to, count, params.thresh, err, curr_mask.data); + + if( numinliers > Math.max(inliers_max, model_points-1) ) { + M.copy_to(model); + inliers_max = numinliers; + if(mask) curr_mask.copy_to(mask); + niters = params.update_iters((count - numinliers)/count, niters); + result = true; + } + } + + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + + return result; + }, + + lmeds: function(params, kernel, from, to, count, model, mask, max_iters) { + if (typeof max_iters === "undefined") { max_iters=1000; } + + if(count < params.size) return false; + + var model_points = params.size; + var niters = max_iters, iter=0; + var result = false; + + var subset0 = []; + var subset1 = []; + var found = false; + + var mc=model.cols,mr=model.rows; + var dt = model.type | jsfeat.C1_t; + + var m_buff = jsfeat.cache.get_buffer((mc*mr)<<3); + var ms_buff = jsfeat.cache.get_buffer(count); + var err_buff = jsfeat.cache.get_buffer(count<<2); + var M = new jsfeat.matrix_t(mc, mr, dt, m_buff.data); + var curr_mask = new jsfeat.matrix_t(count, 1, jsfeat.U8_t|jsfeat.C1_t, ms_buff.data); + + var numinliers=0; + var nmodels = 0; + + var err = err_buff.f32; + var min_median = 1000000000.0, sigma=0.0, median=0.0; + + params.eps = 0.45; + niters = params.update_iters(params.eps, niters); + + // special case + if(count == model_points) { + if(kernel.run(from, to, M, count) <= 0) { + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + return false; + } + + M.copy_to(model); + if(mask) { + while(--count >= 0) { + mask.data[count] = 1; + } + } + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + return true; + } + + for (; iter < niters; ++iter) { + // generate subset + found = get_subset(kernel, from, to, model_points, count, subset0, subset1); + if(!found) { + if(iter == 0) { + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + return false; + } + break; + } + + nmodels = kernel.run( subset0, subset1, M, model_points ); + if(nmodels <= 0) + continue; + + // TODO handle multimodel output + + kernel.error(from, to, M, err, count); + median = jsfeat.math.median(err, 0, count-1); + + if(median < min_median) { + min_median = median; + M.copy_to(model); + result = true; + } + } + + if(result) { + sigma = 2.5*1.4826*(1 + 5.0/(count - model_points))*Math.sqrt(min_median); + sigma = Math.max(sigma, 0.001); + + numinliers = find_inliers(kernel, model, from, to, count, sigma, err, curr_mask.data); + if(mask) curr_mask.copy_to(mask); + + result = numinliers >= model_points; + } + + jsfeat.cache.put_buffer(m_buff); + jsfeat.cache.put_buffer(ms_buff); + jsfeat.cache.put_buffer(err_buff); + + return result; + } + + }; + + })(); + + global.ransac_params_t = ransac_params_t; + global.motion_model = motion_model; + global.motion_estimator = motion_estimator; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +(function(global) { + "use strict"; + // + + var imgproc = (function() { + + var _resample_u8 = function(src, dst, nw, nh) { + var xofs_count=0; + var ch=src.channel,w=src.cols,h=src.rows; + var src_d=src.data,dst_d=dst.data; + var scale_x = w / nw, scale_y = h / nh; + var inv_scale_256 = (scale_x * scale_y * 0x10000)|0; + var dx=0,dy=0,sx=0,sy=0,sx1=0,sx2=0,i=0,k=0,fsx1=0.0,fsx2=0.0; + var a=0,b=0,dxn=0,alpha=0,beta=0,beta1=0; + + var buf_node = jsfeat.cache.get_buffer((nw*ch)<<2); + var sum_node = jsfeat.cache.get_buffer((nw*ch)<<2); + var xofs_node = jsfeat.cache.get_buffer((w*2*3)<<2); + + var buf = buf_node.i32; + var sum = sum_node.i32; + var xofs = xofs_node.i32; + + for (; dx < nw; dx++) { + fsx1 = dx * scale_x, fsx2 = fsx1 + scale_x; + sx1 = (fsx1 + 1.0 - 1e-6)|0, sx2 = fsx2|0; + sx1 = Math.min(sx1, w - 1); + sx2 = Math.min(sx2, w - 1); + + if(sx1 > fsx1) { + xofs[k++] = (dx * ch)|0; + xofs[k++] = ((sx1 - 1)*ch)|0; + xofs[k++] = ((sx1 - fsx1) * 0x100)|0; + xofs_count++; + } + for(sx = sx1; sx < sx2; sx++){ + xofs_count++; + xofs[k++] = (dx * ch)|0; + xofs[k++] = (sx * ch)|0; + xofs[k++] = 256; + } + if(fsx2 - sx2 > 1e-3) { + xofs_count++; + xofs[k++] = (dx * ch)|0; + xofs[k++] = (sx2 * ch)|0; + xofs[k++] = ((fsx2 - sx2) * 256)|0; + } + } + + for (dx = 0; dx < nw * ch; dx++) { + buf[dx] = sum[dx] = 0; + } + dy = 0; + for (sy = 0; sy < h; sy++) { + a = w * sy; + for (k = 0; k < xofs_count; k++) { + dxn = xofs[k*3]; + sx1 = xofs[k*3+1]; + alpha = xofs[k*3+2]; + for (i = 0; i < ch; i++) { + buf[dxn + i] += src_d[a+sx1+i] * alpha; + } + } + if ((dy + 1) * scale_y <= sy + 1 || sy == h - 1) { + beta = (Math.max(sy + 1 - (dy + 1) * scale_y, 0.0) * 256)|0; + beta1 = 256 - beta; + b = nw * dy; + if (beta <= 0) { + for (dx = 0; dx < nw * ch; dx++) { + dst_d[b+dx] = Math.min(Math.max((sum[dx] + buf[dx] * 256) / inv_scale_256, 0), 255); + sum[dx] = buf[dx] = 0; + } + } else { + for (dx = 0; dx < nw * ch; dx++) { + dst_d[b+dx] = Math.min(Math.max((sum[dx] + buf[dx] * beta1) / inv_scale_256, 0), 255); + sum[dx] = buf[dx] * beta; + buf[dx] = 0; + } + } + dy++; + } else { + for(dx = 0; dx < nw * ch; dx++) { + sum[dx] += buf[dx] * 256; + buf[dx] = 0; + } + } + } + + jsfeat.cache.put_buffer(sum_node); + jsfeat.cache.put_buffer(buf_node); + jsfeat.cache.put_buffer(xofs_node); + } + + var _resample = function(src, dst, nw, nh) { + var xofs_count=0; + var ch=src.channel,w=src.cols,h=src.rows; + var src_d=src.data,dst_d=dst.data; + var scale_x = w / nw, scale_y = h / nh; + var scale = 1.0 / (scale_x * scale_y); + var dx=0,dy=0,sx=0,sy=0,sx1=0,sx2=0,i=0,k=0,fsx1=0.0,fsx2=0.0; + var a=0,b=0,dxn=0,alpha=0.0,beta=0.0,beta1=0.0; + + var buf_node = jsfeat.cache.get_buffer((nw*ch)<<2); + var sum_node = jsfeat.cache.get_buffer((nw*ch)<<2); + var xofs_node = jsfeat.cache.get_buffer((w*2*3)<<2); + + var buf = buf_node.f32; + var sum = sum_node.f32; + var xofs = xofs_node.f32; + + for (; dx < nw; dx++) { + fsx1 = dx * scale_x, fsx2 = fsx1 + scale_x; + sx1 = (fsx1 + 1.0 - 1e-6)|0, sx2 = fsx2|0; + sx1 = Math.min(sx1, w - 1); + sx2 = Math.min(sx2, w - 1); + + if(sx1 > fsx1) { + xofs_count++; + xofs[k++] = ((sx1 - 1)*ch)|0; + xofs[k++] = (dx * ch)|0; + xofs[k++] = (sx1 - fsx1) * scale; + } + for(sx = sx1; sx < sx2; sx++){ + xofs_count++; + xofs[k++] = (sx * ch)|0; + xofs[k++] = (dx * ch)|0; + xofs[k++] = scale; + } + if(fsx2 - sx2 > 1e-3) { + xofs_count++; + xofs[k++] = (sx2 * ch)|0; + xofs[k++] = (dx * ch)|0; + xofs[k++] = (fsx2 - sx2) * scale; + } + } + + for (dx = 0; dx < nw * ch; dx++) { + buf[dx] = sum[dx] = 0; + } + dy = 0; + for (sy = 0; sy < h; sy++) { + a = w * sy; + for (k = 0; k < xofs_count; k++) { + sx1 = xofs[k*3]|0; + dxn = xofs[k*3+1]|0; + alpha = xofs[k*3+2]; + for (i = 0; i < ch; i++) { + buf[dxn + i] += src_d[a+sx1+i] * alpha; + } + } + if ((dy + 1) * scale_y <= sy + 1 || sy == h - 1) { + beta = Math.max(sy + 1 - (dy + 1) * scale_y, 0.0); + beta1 = 1.0 - beta; + b = nw * dy; + if (Math.abs(beta) < 1e-3) { + for (dx = 0; dx < nw * ch; dx++) { + dst_d[b+dx] = sum[dx] + buf[dx]; + sum[dx] = buf[dx] = 0; + } + } else { + for (dx = 0; dx < nw * ch; dx++) { + dst_d[b+dx] = sum[dx] + buf[dx] * beta1; + sum[dx] = buf[dx] * beta; + buf[dx] = 0; + } + } + dy++; + } else { + for(dx = 0; dx < nw * ch; dx++) { + sum[dx] += buf[dx]; + buf[dx] = 0; + } + } + } + jsfeat.cache.put_buffer(sum_node); + jsfeat.cache.put_buffer(buf_node); + jsfeat.cache.put_buffer(xofs_node); + } + + var _convol_u8 = function(buf, src_d, dst_d, w, h, filter, kernel_size, half_kernel) { + var i=0,j=0,k=0,sp=0,dp=0,sum=0,sum1=0,sum2=0,sum3=0,f0=filter[0],fk=0; + var w2=w<<1,w3=w*3,w4=w<<2; + // hor pass + for (; i < h; ++i) { + sum = src_d[sp]; + for (j = 0; j < half_kernel; ++j) { + buf[j] = sum; + } + for (j = 0; j <= w-2; j+=2) { + buf[j + half_kernel] = src_d[sp+j]; + buf[j + half_kernel+1] = src_d[sp+j+1]; + } + for (; j < w; ++j) { + buf[j + half_kernel] = src_d[sp+j]; + } + sum = src_d[sp+w-1]; + for (j = w; j < half_kernel + w; ++j) { + buf[j + half_kernel] = sum; + } + for (j = 0; j <= w-4; j+=4) { + sum = buf[j] * f0, + sum1 = buf[j+1] * f0, + sum2 = buf[j+2] * f0, + sum3 = buf[j+3] * f0; + for (k = 1; k < kernel_size; ++k) { + fk = filter[k]; + sum += buf[k + j] * fk; + sum1 += buf[k + j+1] * fk; + sum2 += buf[k + j+2] * fk; + sum3 += buf[k + j+3] * fk; + } + dst_d[dp+j] = Math.min(sum >> 8, 255); + dst_d[dp+j+1] = Math.min(sum1 >> 8, 255); + dst_d[dp+j+2] = Math.min(sum2 >> 8, 255); + dst_d[dp+j+3] = Math.min(sum3 >> 8, 255); + } + for (; j < w; ++j) { + sum = buf[j] * f0; + for (k = 1; k < kernel_size; ++k) { + sum += buf[k + j] * filter[k]; + } + dst_d[dp+j] = Math.min(sum >> 8, 255); + } + sp += w; + dp += w; + } + + // vert pass + for (i = 0; i < w; ++i) { + sum = dst_d[i]; + for (j = 0; j < half_kernel; ++j) { + buf[j] = sum; + } + k = i; + for (j = 0; j <= h-2; j+=2, k+=w2) { + buf[j+half_kernel] = dst_d[k]; + buf[j+half_kernel+1] = dst_d[k+w]; + } + for (; j < h; ++j, k+=w) { + buf[j+half_kernel] = dst_d[k]; + } + sum = dst_d[(h-1)*w + i]; + for (j = h; j < half_kernel + h; ++j) { + buf[j + half_kernel] = sum; + } + dp = i; + for (j = 0; j <= h-4; j+=4, dp+=w4) { + sum = buf[j] * f0, + sum1 = buf[j+1] * f0, + sum2 = buf[j+2] * f0, + sum3 = buf[j+3] * f0; + for (k = 1; k < kernel_size; ++k) { + fk = filter[k]; + sum += buf[k + j] * fk; + sum1 += buf[k + j+1] * fk; + sum2 += buf[k + j+2] * fk; + sum3 += buf[k + j+3] * fk; + } + dst_d[dp] = Math.min(sum >> 8, 255); + dst_d[dp+w] = Math.min(sum1 >> 8, 255); + dst_d[dp+w2] = Math.min(sum2 >> 8, 255); + dst_d[dp+w3] = Math.min(sum3 >> 8, 255); + } + for (; j < h; ++j, dp+=w) { + sum = buf[j] * f0; + for (k = 1; k < kernel_size; ++k) { + sum += buf[k + j] * filter[k]; + } + dst_d[dp] = Math.min(sum >> 8, 255); + } + } + } + + var _convol = function(buf, src_d, dst_d, w, h, filter, kernel_size, half_kernel) { + var i=0,j=0,k=0,sp=0,dp=0,sum=0.0,sum1=0.0,sum2=0.0,sum3=0.0,f0=filter[0],fk=0.0; + var w2=w<<1,w3=w*3,w4=w<<2; + // hor pass + for (; i < h; ++i) { + sum = src_d[sp]; + for (j = 0; j < half_kernel; ++j) { + buf[j] = sum; + } + for (j = 0; j <= w-2; j+=2) { + buf[j + half_kernel] = src_d[sp+j]; + buf[j + half_kernel+1] = src_d[sp+j+1]; + } + for (; j < w; ++j) { + buf[j + half_kernel] = src_d[sp+j]; + } + sum = src_d[sp+w-1]; + for (j = w; j < half_kernel + w; ++j) { + buf[j + half_kernel] = sum; + } + for (j = 0; j <= w-4; j+=4) { + sum = buf[j] * f0, + sum1 = buf[j+1] * f0, + sum2 = buf[j+2] * f0, + sum3 = buf[j+3] * f0; + for (k = 1; k < kernel_size; ++k) { + fk = filter[k]; + sum += buf[k + j] * fk; + sum1 += buf[k + j+1] * fk; + sum2 += buf[k + j+2] * fk; + sum3 += buf[k + j+3] * fk; + } + dst_d[dp+j] = sum; + dst_d[dp+j+1] = sum1; + dst_d[dp+j+2] = sum2; + dst_d[dp+j+3] = sum3; + } + for (; j < w; ++j) { + sum = buf[j] * f0; + for (k = 1; k < kernel_size; ++k) { + sum += buf[k + j] * filter[k]; + } + dst_d[dp+j] = sum; + } + sp += w; + dp += w; + } + + // vert pass + for (i = 0; i < w; ++i) { + sum = dst_d[i]; + for (j = 0; j < half_kernel; ++j) { + buf[j] = sum; + } + k = i; + for (j = 0; j <= h-2; j+=2, k+=w2) { + buf[j+half_kernel] = dst_d[k]; + buf[j+half_kernel+1] = dst_d[k+w]; + } + for (; j < h; ++j, k+=w) { + buf[j+half_kernel] = dst_d[k]; + } + sum = dst_d[(h-1)*w + i]; + for (j = h; j < half_kernel + h; ++j) { + buf[j + half_kernel] = sum; + } + dp = i; + for (j = 0; j <= h-4; j+=4, dp+=w4) { + sum = buf[j] * f0, + sum1 = buf[j+1] * f0, + sum2 = buf[j+2] * f0, + sum3 = buf[j+3] * f0; + for (k = 1; k < kernel_size; ++k) { + fk = filter[k]; + sum += buf[k + j] * fk; + sum1 += buf[k + j+1] * fk; + sum2 += buf[k + j+2] * fk; + sum3 += buf[k + j+3] * fk; + } + dst_d[dp] = sum; + dst_d[dp+w] = sum1; + dst_d[dp+w2] = sum2; + dst_d[dp+w3] = sum3; + } + for (; j < h; ++j, dp+=w) { + sum = buf[j] * f0; + for (k = 1; k < kernel_size; ++k) { + sum += buf[k + j] * filter[k]; + } + dst_d[dp] = sum; + } + } + } + + return { + // TODO: add support for RGB/BGR order + // for raw arrays + grayscale: function(src, w, h, dst, code) { + // this is default image data representation in browser + if (typeof code === "undefined") { code = jsfeat.COLOR_RGBA2GRAY; } + var x=0, y=0, i=0, j=0, ir=0,jr=0; + var coeff_r = 4899, coeff_g = 9617, coeff_b = 1868, cn = 4; + + if(code == jsfeat.COLOR_BGRA2GRAY || code == jsfeat.COLOR_BGR2GRAY) { + coeff_r = 1868; + coeff_b = 4899; + } + if(code == jsfeat.COLOR_RGB2GRAY || code == jsfeat.COLOR_BGR2GRAY) { + cn = 3; + } + var cn2 = cn<<1, cn3 = (cn*3)|0; + + dst.resize(w, h, 1); + var dst_u8 = dst.data; + + for(y = 0; y < h; ++y, j+=w, i+=w*cn) { + for(x = 0, ir = i, jr = j; x <= w-4; x+=4, ir+=cn<<2, jr+=4) { + dst_u8[jr] = (src[ir] * coeff_r + src[ir+1] * coeff_g + src[ir+2] * coeff_b + 8192) >> 14; + dst_u8[jr + 1] = (src[ir+cn] * coeff_r + src[ir+cn+1] * coeff_g + src[ir+cn+2] * coeff_b + 8192) >> 14; + dst_u8[jr + 2] = (src[ir+cn2] * coeff_r + src[ir+cn2+1] * coeff_g + src[ir+cn2+2] * coeff_b + 8192) >> 14; + dst_u8[jr + 3] = (src[ir+cn3] * coeff_r + src[ir+cn3+1] * coeff_g + src[ir+cn3+2] * coeff_b + 8192) >> 14; + } + for (; x < w; ++x, ++jr, ir+=cn) { + dst_u8[jr] = (src[ir] * coeff_r + src[ir+1] * coeff_g + src[ir+2] * coeff_b + 8192) >> 14; + } + } + }, + // derived from CCV library + resample: function(src, dst, nw, nh) { + var h=src.rows,w=src.cols; + if (h > nh && w > nw) { + dst.resize(nw, nh, src.channel); + // using the fast alternative (fix point scale, 0x100 to avoid overflow) + if (src.type&jsfeat.U8_t && dst.type&jsfeat.U8_t && h * w / (nh * nw) < 0x100) { + _resample_u8(src, dst, nw, nh); + } else { + _resample(src, dst, nw, nh); + } + } + }, + + box_blur_gray: function(src, dst, radius, options) { + if (typeof options === "undefined") { options = 0; } + var w=src.cols, h=src.rows, h2=h<<1, w2=w<<1; + var i=0,x=0,y=0,end=0; + var windowSize = ((radius << 1) + 1)|0; + var radiusPlusOne = (radius + 1)|0, radiusPlus2 = (radiusPlusOne+1)|0; + var scale = options&jsfeat.BOX_BLUR_NOSCALE ? 1 : (1.0 / (windowSize*windowSize)); + + var tmp_buff = jsfeat.cache.get_buffer((w*h)<<2); + + var sum=0, dstIndex=0, srcIndex = 0, nextPixelIndex=0, previousPixelIndex=0; + var data_i32 = tmp_buff.i32; // to prevent overflow + var data_u8 = src.data; + var hold=0; + + dst.resize(w, h, src.channel); + + // first pass + // no need to scale + //data_u8 = src.data; + //data_i32 = tmp; + for (y = 0; y < h; ++y) { + dstIndex = y; + sum = radiusPlusOne * data_u8[srcIndex]; + + for(i = (srcIndex+1)|0, end=(srcIndex+radius)|0; i <= end; ++i) { + sum += data_u8[i]; + } + + nextPixelIndex = (srcIndex + radiusPlusOne)|0; + previousPixelIndex = srcIndex; + hold = data_u8[previousPixelIndex]; + for(x = 0; x < radius; ++x, dstIndex += h) { + data_i32[dstIndex] = sum; + sum += data_u8[nextPixelIndex]- hold; + nextPixelIndex ++; + } + for(; x < w-radiusPlus2; x+=2, dstIndex += h2) { + data_i32[dstIndex] = sum; + sum += data_u8[nextPixelIndex]- data_u8[previousPixelIndex]; + + data_i32[dstIndex+h] = sum; + sum += data_u8[nextPixelIndex+1]- data_u8[previousPixelIndex+1]; + + nextPixelIndex +=2; + previousPixelIndex +=2; + } + for(; x < w-radiusPlusOne; ++x, dstIndex += h) { + data_i32[dstIndex] = sum; + sum += data_u8[nextPixelIndex]- data_u8[previousPixelIndex]; + + nextPixelIndex ++; + previousPixelIndex ++; + } + + hold = data_u8[nextPixelIndex-1]; + for(; x < w; ++x, dstIndex += h) { + data_i32[dstIndex] = sum; + + sum += hold- data_u8[previousPixelIndex]; + previousPixelIndex ++; + } + + srcIndex += w; + } + // + // second pass + srcIndex = 0; + //data_i32 = tmp; // this is a transpose + data_u8 = dst.data; + + // dont scale result + if(scale == 1) { + for (y = 0; y < w; ++y) { + dstIndex = y; + sum = radiusPlusOne * data_i32[srcIndex]; + + for(i = (srcIndex+1)|0, end=(srcIndex+radius)|0; i <= end; ++i) { + sum += data_i32[i]; + } + + nextPixelIndex = srcIndex + radiusPlusOne; + previousPixelIndex = srcIndex; + hold = data_i32[previousPixelIndex]; + + for(x = 0; x < radius; ++x, dstIndex += w) { + data_u8[dstIndex] = sum; + sum += data_i32[nextPixelIndex]- hold; + nextPixelIndex ++; + } + for(; x < h-radiusPlus2; x+=2, dstIndex += w2) { + data_u8[dstIndex] = sum; + sum += data_i32[nextPixelIndex]- data_i32[previousPixelIndex]; + + data_u8[dstIndex+w] = sum; + sum += data_i32[nextPixelIndex+1]- data_i32[previousPixelIndex+1]; + + nextPixelIndex +=2; + previousPixelIndex +=2; + } + for(; x < h-radiusPlusOne; ++x, dstIndex += w) { + data_u8[dstIndex] = sum; + + sum += data_i32[nextPixelIndex]- data_i32[previousPixelIndex]; + nextPixelIndex ++; + previousPixelIndex ++; + } + hold = data_i32[nextPixelIndex-1]; + for(; x < h; ++x, dstIndex += w) { + data_u8[dstIndex] = sum; + + sum += hold- data_i32[previousPixelIndex]; + previousPixelIndex ++; + } + + srcIndex += h; + } + } else { + for (y = 0; y < w; ++y) { + dstIndex = y; + sum = radiusPlusOne * data_i32[srcIndex]; + + for(i = (srcIndex+1)|0, end=(srcIndex+radius)|0; i <= end; ++i) { + sum += data_i32[i]; + } + + nextPixelIndex = srcIndex + radiusPlusOne; + previousPixelIndex = srcIndex; + hold = data_i32[previousPixelIndex]; + + for(x = 0; x < radius; ++x, dstIndex += w) { + data_u8[dstIndex] = sum*scale; + sum += data_i32[nextPixelIndex]- hold; + nextPixelIndex ++; + } + for(; x < h-radiusPlus2; x+=2, dstIndex += w2) { + data_u8[dstIndex] = sum*scale; + sum += data_i32[nextPixelIndex]- data_i32[previousPixelIndex]; + + data_u8[dstIndex+w] = sum*scale; + sum += data_i32[nextPixelIndex+1]- data_i32[previousPixelIndex+1]; + + nextPixelIndex +=2; + previousPixelIndex +=2; + } + for(; x < h-radiusPlusOne; ++x, dstIndex += w) { + data_u8[dstIndex] = sum*scale; + + sum += data_i32[nextPixelIndex]- data_i32[previousPixelIndex]; + nextPixelIndex ++; + previousPixelIndex ++; + } + hold = data_i32[nextPixelIndex-1]; + for(; x < h; ++x, dstIndex += w) { + data_u8[dstIndex] = sum*scale; + + sum += hold- data_i32[previousPixelIndex]; + previousPixelIndex ++; + } + + srcIndex += h; + } + } + + jsfeat.cache.put_buffer(tmp_buff); + }, + + gaussian_blur: function(src, dst, kernel_size, sigma) { + if (typeof sigma === "undefined") { sigma = 0.0; } + if (typeof kernel_size === "undefined") { kernel_size = 0; } + kernel_size = kernel_size == 0 ? (Math.max(1, (4.0 * sigma + 1.0 - 1e-8)) * 2 + 1)|0 : kernel_size; + var half_kernel = kernel_size >> 1; + var w = src.cols, h = src.rows; + var data_type = src.type, is_u8 = data_type&jsfeat.U8_t; + + dst.resize(w, h, src.channel); + + var src_d = src.data, dst_d = dst.data; + var buf,filter,buf_sz=(kernel_size + Math.max(h, w))|0; + + var buf_node = jsfeat.cache.get_buffer(buf_sz<<2); + var filt_node = jsfeat.cache.get_buffer(kernel_size<<2); + + if(is_u8) { + buf = buf_node.i32; + filter = filt_node.i32; + } else if(data_type&jsfeat.S32_t) { + buf = buf_node.i32; + filter = filt_node.f32; + } else { + buf = buf_node.f32; + filter = filt_node.f32; + } + + jsfeat.math.get_gaussian_kernel(kernel_size, sigma, filter, data_type); + + if(is_u8) { + _convol_u8(buf, src_d, dst_d, w, h, filter, kernel_size, half_kernel); + } else { + _convol(buf, src_d, dst_d, w, h, filter, kernel_size, half_kernel); + } + + jsfeat.cache.put_buffer(buf_node); + jsfeat.cache.put_buffer(filt_node); + }, + hough_transform: function( img, rho_res, theta_res, threshold ) { + var image = img.data; + + var width = img.cols; + var height = img.rows; + var step = width; + + min_theta = 0.0; + max_theta = Math.PI; + + numangle = Math.round((max_theta - min_theta) / theta_res); + numrho = Math.round(((width + height) * 2 + 1) / rho_res); + irho = 1.0 / rho_res; + + var accum = new Int32Array((numangle+2) * (numrho+2)); //typed arrays are initialized to 0 + var tabSin = new Float32Array(numangle); + var tabCos = new Float32Array(numangle); + + var n=0; + var ang = min_theta; + for(; n < numangle; n++ ) { + tabSin[n] = Math.sin(ang) * irho; + tabCos[n] = Math.cos(ang) * irho; + ang += theta_res + } + + // stage 1. fill accumulator + for( var i = 0; i < height; i++ ) { + for( var j = 0; j < width; j++ ) { + if( image[i * step + j] != 0 ) { + //console.log(r, (n+1) * (numrho+2) + r+1, tabCos[n], tabSin[n]); + for(var n = 0; n < numangle; n++ ) { + var r = Math.round( j * tabCos[n] + i * tabSin[n] ); + r += (numrho - 1) / 2; + accum[(n+1) * (numrho+2) + r+1] += 1; + } + } + } + } + + // stage 2. find local maximums + //TODO: Consider making a vector class that uses typed arrays + _sort_buf = new Array(); + for(var r = 0; r < numrho; r++ ) { + for(var n = 0; n < numangle; n++ ) { + var base = (n+1) * (numrho+2) + r+1; + if( accum[base] > threshold && + accum[base] > accum[base - 1] && accum[base] >= accum[base + 1] && + accum[base] > accum[base - numrho - 2] && accum[base] >= accum[base + numrho + 2] ) { + _sort_buf.push(base); + } + } + } + + // stage 3. sort the detected lines by accumulator value + _sort_buf.sort(function(l1, l2) { + return accum[l1] > accum[l2] || (accum[l1] == accum[l2] && l1 < l2); + }); + + // stage 4. store the first min(total,linesMax) lines to the output buffer + linesMax = Math.min(numangle*numrho, _sort_buf.length); + scale = 1.0 / (numrho+2); + lines = new Array(); + for( var i = 0; i < linesMax; i++ ) { + var idx = _sort_buf[i]; + var n = Math.floor(idx*scale) - 1; + var r = idx - (n+1)*(numrho+2) - 1; + var lrho = (r - (numrho - 1)*0.5) * rho_res; + var langle = n * theta_res; + lines.push([lrho, langle]); + } + return lines; + }, + // assume we always need it for u8 image + pyrdown: function(src, dst, sx, sy) { + // this is needed for bbf + if (typeof sx === "undefined") { sx = 0; } + if (typeof sy === "undefined") { sy = 0; } + + var w = src.cols, h = src.rows; + var w2 = w >> 1, h2 = h >> 1; + var _w2 = w2 - (sx << 1), _h2 = h2 - (sy << 1); + var x=0,y=0,sptr=sx+sy*w,sline=0,dptr=0,dline=0; + + dst.resize(w2, h2, src.channel); + + var src_d = src.data, dst_d = dst.data; + + for(y = 0; y < _h2; ++y) { + sline = sptr; + dline = dptr; + for(x = 0; x <= _w2-2; x+=2, dline+=2, sline += 4) { + dst_d[dline] = (src_d[sline] + src_d[sline+1] + + src_d[sline+w] + src_d[sline+w+1] + 2) >> 2; + dst_d[dline+1] = (src_d[sline+2] + src_d[sline+3] + + src_d[sline+w+2] + src_d[sline+w+3] + 2) >> 2; + } + for(; x < _w2; ++x, ++dline, sline += 2) { + dst_d[dline] = (src_d[sline] + src_d[sline+1] + + src_d[sline+w] + src_d[sline+w+1] + 2) >> 2; + } + sptr += w << 1; + dptr += w2; + } + }, + + // dst: [gx,gy,...] + scharr_derivatives: function(src, dst) { + var w = src.cols, h = src.rows; + var dstep = w<<1,x=0,y=0,x1=0,a,b,c,d,e,f; + var srow0=0,srow1=0,srow2=0,drow=0; + var trow0,trow1; + + dst.resize(w, h, 2); // 2 channel output gx, gy + + var img = src.data, gxgy=dst.data; + + var buf0_node = jsfeat.cache.get_buffer((w+2)<<2); + var buf1_node = jsfeat.cache.get_buffer((w+2)<<2); + + if(src.type&jsfeat.U8_t || src.type&jsfeat.S32_t) { + trow0 = buf0_node.i32; + trow1 = buf1_node.i32; + } else { + trow0 = buf0_node.f32; + trow1 = buf1_node.f32; + } + + for(; y < h; ++y, srow1+=w) { + srow0 = ((y > 0 ? y-1 : 1)*w)|0; + srow2 = ((y < h-1 ? y+1 : h-2)*w)|0; + drow = (y*dstep)|0; + // do vertical convolution + for(x = 0, x1 = 1; x <= w-2; x+=2, x1+=2) { + a = img[srow0+x], b = img[srow2+x]; + trow0[x1] = ( (a + b)*3 + (img[srow1+x])*10 ); + trow1[x1] = ( b - a ); + // + a = img[srow0+x+1], b = img[srow2+x+1]; + trow0[x1+1] = ( (a + b)*3 + (img[srow1+x+1])*10 ); + trow1[x1+1] = ( b - a ); + } + for(; x < w; ++x, ++x1) { + a = img[srow0+x], b = img[srow2+x]; + trow0[x1] = ( (a + b)*3 + (img[srow1+x])*10 ); + trow1[x1] = ( b - a ); + } + // make border + x = (w + 1)|0; + trow0[0] = trow0[1]; trow0[x] = trow0[w]; + trow1[0] = trow1[1]; trow1[x] = trow1[w]; + // do horizontal convolution, interleave the results and store them + for(x = 0; x <= w-4; x+=4) { + a = trow1[x+2], b = trow1[x+1], c = trow1[x+3], d = trow1[x+4], + e = trow0[x+2], f = trow0[x+3]; + gxgy[drow++] = ( e - trow0[x] ); + gxgy[drow++] = ( (a + trow1[x])*3 + b*10 ); + gxgy[drow++] = ( f - trow0[x+1] ); + gxgy[drow++] = ( (c + b)*3 + a*10 ); + + gxgy[drow++] = ( (trow0[x+4] - e) ); + gxgy[drow++] = ( ((d + a)*3 + c*10) ); + gxgy[drow++] = ( (trow0[x+5] - f) ); + gxgy[drow++] = ( ((trow1[x+5] + c)*3 + d*10) ); + } + for(; x < w; ++x) { + gxgy[drow++] = ( (trow0[x+2] - trow0[x]) ); + gxgy[drow++] = ( ((trow1[x+2] + trow1[x])*3 + trow1[x+1]*10) ); + } + } + jsfeat.cache.put_buffer(buf0_node); + jsfeat.cache.put_buffer(buf1_node); + }, + + // compute gradient using Sobel kernel [1 2 1] * [-1 0 1]^T + // dst: [gx,gy,...] + sobel_derivatives: function(src, dst) { + var w = src.cols, h = src.rows; + var dstep = w<<1,x=0,y=0,x1=0,a,b,c,d,e,f; + var srow0=0,srow1=0,srow2=0,drow=0; + var trow0,trow1; + + dst.resize(w, h, 2); // 2 channel output gx, gy + + var img = src.data, gxgy=dst.data; + + var buf0_node = jsfeat.cache.get_buffer((w+2)<<2); + var buf1_node = jsfeat.cache.get_buffer((w+2)<<2); + + if(src.type&jsfeat.U8_t || src.type&jsfeat.S32_t) { + trow0 = buf0_node.i32; + trow1 = buf1_node.i32; + } else { + trow0 = buf0_node.f32; + trow1 = buf1_node.f32; + } + + for(; y < h; ++y, srow1+=w) { + srow0 = ((y > 0 ? y-1 : 1)*w)|0; + srow2 = ((y < h-1 ? y+1 : h-2)*w)|0; + drow = (y*dstep)|0; + // do vertical convolution + for(x = 0, x1 = 1; x <= w-2; x+=2, x1+=2) { + a = img[srow0+x], b = img[srow2+x]; + trow0[x1] = ( (a + b) + (img[srow1+x]*2) ); + trow1[x1] = ( b - a ); + // + a = img[srow0+x+1], b = img[srow2+x+1]; + trow0[x1+1] = ( (a + b) + (img[srow1+x+1]*2) ); + trow1[x1+1] = ( b - a ); + } + for(; x < w; ++x, ++x1) { + a = img[srow0+x], b = img[srow2+x]; + trow0[x1] = ( (a + b) + (img[srow1+x]*2) ); + trow1[x1] = ( b - a ); + } + // make border + x = (w + 1)|0; + trow0[0] = trow0[1]; trow0[x] = trow0[w]; + trow1[0] = trow1[1]; trow1[x] = trow1[w]; + // do horizontal convolution, interleave the results and store them + for(x = 0; x <= w-4; x+=4) { + a = trow1[x+2], b = trow1[x+1], c = trow1[x+3], d = trow1[x+4], + e = trow0[x+2], f = trow0[x+3]; + gxgy[drow++] = ( e - trow0[x] ); + gxgy[drow++] = ( a + trow1[x] + b*2 ); + gxgy[drow++] = ( f - trow0[x+1] ); + gxgy[drow++] = ( c + b + a*2 ); + + gxgy[drow++] = ( trow0[x+4] - e ); + gxgy[drow++] = ( d + a + c*2 ); + gxgy[drow++] = ( trow0[x+5] - f ); + gxgy[drow++] = ( trow1[x+5] + c + d*2 ); + } + for(; x < w; ++x) { + gxgy[drow++] = ( trow0[x+2] - trow0[x] ); + gxgy[drow++] = ( trow1[x+2] + trow1[x] + trow1[x+1]*2 ); + } + } + jsfeat.cache.put_buffer(buf0_node); + jsfeat.cache.put_buffer(buf1_node); + }, + + // please note: + // dst_(type) size should be cols = src.cols+1, rows = src.rows+1 + compute_integral_image: function(src, dst_sum, dst_sqsum, dst_tilted) { + var w0=src.cols|0,h0=src.rows|0,src_d=src.data; + var w1=(w0+1)|0; + var s=0,s2=0,p=0,pup=0,i=0,j=0,v=0,k=0; + + if(dst_sum && dst_sqsum) { + // fill first row with zeros + for(; i < w1; ++i) { + dst_sum[i] = 0, dst_sqsum[i] = 0; + } + p = (w1+1)|0, pup = 1; + for(i = 0, k = 0; i < h0; ++i, ++p, ++pup) { + s = s2 = 0; + for(j = 0; j <= w0-2; j+=2, k+=2, p+=2, pup+=2) { + v = src_d[k]; + s += v, s2 += v*v; + dst_sum[p] = dst_sum[pup] + s; + dst_sqsum[p] = dst_sqsum[pup] + s2; + + v = src_d[k+1]; + s += v, s2 += v*v; + dst_sum[p+1] = dst_sum[pup+1] + s; + dst_sqsum[p+1] = dst_sqsum[pup+1] + s2; + } + for(; j < w0; ++j, ++k, ++p, ++pup) { + v = src_d[k]; + s += v, s2 += v*v; + dst_sum[p] = dst_sum[pup] + s; + dst_sqsum[p] = dst_sqsum[pup] + s2; + } + } + } else if(dst_sum) { + // fill first row with zeros + for(; i < w1; ++i) { + dst_sum[i] = 0; + } + p = (w1+1)|0, pup = 1; + for(i = 0, k = 0; i < h0; ++i, ++p, ++pup) { + s = 0; + for(j = 0; j <= w0-2; j+=2, k+=2, p+=2, pup+=2) { + s += src_d[k]; + dst_sum[p] = dst_sum[pup] + s; + s += src_d[k+1]; + dst_sum[p+1] = dst_sum[pup+1] + s; + } + for(; j < w0; ++j, ++k, ++p, ++pup) { + s += src_d[k]; + dst_sum[p] = dst_sum[pup] + s; + } + } + } else if(dst_sqsum) { + // fill first row with zeros + for(; i < w1; ++i) { + dst_sqsum[i] = 0; + } + p = (w1+1)|0, pup = 1; + for(i = 0, k = 0; i < h0; ++i, ++p, ++pup) { + s2 = 0; + for(j = 0; j <= w0-2; j+=2, k+=2, p+=2, pup+=2) { + v = src_d[k]; + s2 += v*v; + dst_sqsum[p] = dst_sqsum[pup] + s2; + v = src_d[k+1]; + s2 += v*v; + dst_sqsum[p+1] = dst_sqsum[pup+1] + s2; + } + for(; j < w0; ++j, ++k, ++p, ++pup) { + v = src_d[k]; + s2 += v*v; + dst_sqsum[p] = dst_sqsum[pup] + s2; + } + } + } + + if(dst_tilted) { + // fill first row with zeros + for(i = 0; i < w1; ++i) { + dst_tilted[i] = 0; + } + // diagonal + p = (w1+1)|0, pup = 0; + for(i = 0, k = 0; i < h0; ++i, ++p, ++pup) { + for(j = 0; j <= w0-2; j+=2, k+=2, p+=2, pup+=2) { + dst_tilted[p] = src_d[k] + dst_tilted[pup]; + dst_tilted[p+1] = src_d[k+1] + dst_tilted[pup+1]; + } + for(; j < w0; ++j, ++k, ++p, ++pup) { + dst_tilted[p] = src_d[k] + dst_tilted[pup]; + } + } + // diagonal + p = (w1+w0)|0, pup = w0; + for(i = 0; i < h0; ++i, p+=w1, pup+=w1) { + dst_tilted[p] += dst_tilted[pup]; + } + + for(j = w0-1; j > 0; --j) { + p = j+h0*w1, pup=p-w1; + for(i = h0; i > 0; --i, p-=w1, pup-=w1) { + dst_tilted[p] += dst_tilted[pup] + dst_tilted[pup+1]; + } + } + } + }, + equalize_histogram: function(src, dst) { + var w=src.cols,h=src.rows,src_d=src.data; + + dst.resize(w, h, src.channel); + + var dst_d=dst.data,size=w*h; + var i=0,prev=0,hist0,norm; + + var hist0_node = jsfeat.cache.get_buffer(256<<2); + hist0 = hist0_node.i32; + for(; i < 256; ++i) hist0[i] = 0; + for (i = 0; i < size; ++i) { + ++hist0[src_d[i]]; + } + + prev = hist0[0]; + for (i = 1; i < 256; ++i) { + prev = hist0[i] += prev; + } + + norm = 255 / size; + for (i = 0; i < size; ++i) { + dst_d[i] = (hist0[src_d[i]] * norm + 0.5)|0; + } + jsfeat.cache.put_buffer(hist0_node); + }, + + canny: function(src, dst, low_thresh, high_thresh) { + var w=src.cols,h=src.rows,src_d=src.data; + + dst.resize(w, h, src.channel); + + var dst_d=dst.data; + var i=0,j=0,grad=0,w2=w<<1,_grad=0,suppress=0,f=0,x=0,y=0,s=0; + var tg22x=0,tg67x=0; + + // cache buffers + var dxdy_node = jsfeat.cache.get_buffer((h * w2)<<2); + var buf_node = jsfeat.cache.get_buffer((3 * (w + 2))<<2); + var map_node = jsfeat.cache.get_buffer(((h+2) * (w + 2))<<2); + var stack_node = jsfeat.cache.get_buffer((h * w)<<2); + + + var buf = buf_node.i32; + var map = map_node.i32; + var stack = stack_node.i32; + var dxdy = dxdy_node.i32; + var dxdy_m = new jsfeat.matrix_t(w, h, jsfeat.S32C2_t, dxdy_node.data); + var row0=1,row1=(w+2+1)|0,row2=(2*(w+2)+1)|0,map_w=(w+2)|0,map_i=(map_w+1)|0,stack_i=0; + + this.sobel_derivatives(src, dxdy_m); + + if(low_thresh > high_thresh) { + i = low_thresh; + low_thresh = high_thresh; + high_thresh = i; + } + + i = (3 * (w + 2))|0; + while(--i>=0) { + buf[i] = 0; + } + + i = ((h+2) * (w + 2))|0; + while(--i>=0) { + map[i] = 0; + } + + for (; j < w; ++j, grad+=2) { + //buf[row1+j] = Math.abs(dxdy[grad]) + Math.abs(dxdy[grad+1]); + x = dxdy[grad], y = dxdy[grad+1]; + //buf[row1+j] = x*x + y*y; + buf[row1+j] = ((x ^ (x >> 31)) - (x >> 31)) + ((y ^ (y >> 31)) - (y >> 31)); + } + + for(i=1; i <= h; ++i, grad+=w2) { + if(i == h) { + j = row2+w; + while(--j>=row2) { + buf[j] = 0; + } + } else { + for (j = 0; j < w; j++) { + //buf[row2+j] = Math.abs(dxdy[grad+(j<<1)]) + Math.abs(dxdy[grad+(j<<1)+1]); + x = dxdy[grad+(j<<1)], y = dxdy[grad+(j<<1)+1]; + //buf[row2+j] = x*x + y*y; + buf[row2+j] = ((x ^ (x >> 31)) - (x >> 31)) + ((y ^ (y >> 31)) - (y >> 31)); + } + } + _grad = (grad - w2)|0; + map[map_i-1] = 0; + suppress = 0; + for(j = 0; j < w; ++j, _grad+=2) { + f = buf[row1+j]; + if (f > low_thresh) { + x = dxdy[_grad]; + y = dxdy[_grad+1]; + s = x ^ y; + // seems ot be faster than Math.abs + x = ((x ^ (x >> 31)) - (x >> 31))|0; + y = ((y ^ (y >> 31)) - (y >> 31))|0; + //x * tan(22.5) x * tan(67.5) == 2 * x + x * tan(22.5) + tg22x = x * 13573; + tg67x = tg22x + ((x + x) << 15); + y <<= 15; + if (y < tg22x) { + if (f > buf[row1+j-1] && f >= buf[row1+j+1]) { + if (f > high_thresh && !suppress && map[map_i+j-map_w] != 2) { + map[map_i+j] = 2; + suppress = 1; + stack[stack_i++] = map_i + j; + } else { + map[map_i+j] = 1; + } + continue; + } + } else if (y > tg67x) { + if (f > buf[row0+j] && f >= buf[row2+j]) { + if (f > high_thresh && !suppress && map[map_i+j-map_w] != 2) { + map[map_i+j] = 2; + suppress = 1; + stack[stack_i++] = map_i + j; + } else { + map[map_i+j] = 1; + } + continue; + } + } else { + s = s < 0 ? -1 : 1; + if (f > buf[row0+j-s] && f > buf[row2+j+s]) { + if (f > high_thresh && !suppress && map[map_i+j-map_w] != 2) { + map[map_i+j] = 2; + suppress = 1; + stack[stack_i++] = map_i + j; + } else { + map[map_i+j] = 1; + } + continue; + } + } + } + map[map_i+j] = 0; + suppress = 0; + } + map[map_i+w] = 0; + map_i += map_w; + j = row0; + row0 = row1; + row1 = row2; + row2 = j; + } + + j = map_i - map_w - 1; + for(i = 0; i < map_w; ++i, ++j) { + map[j] = 0; + } + // path following + while(stack_i > 0) { + map_i = stack[--stack_i]; + map_i -= map_w+1; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i += 1; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i += 1; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i += map_w; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i -= 2; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i += map_w; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i += 1; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + map_i += 1; + if(map[map_i] == 1) map[map_i] = 2, stack[stack_i++] = map_i; + } + + map_i = map_w + 1; + row0 = 0; + for(i = 0; i < h; ++i, map_i+=map_w) { + for(j = 0; j < w; ++j) { + dst_d[row0++] = (map[map_i+j] == 2) * 0xff; + } + } + + // free buffers + jsfeat.cache.put_buffer(dxdy_node); + jsfeat.cache.put_buffer(buf_node); + jsfeat.cache.put_buffer(map_node); + jsfeat.cache.put_buffer(stack_node); + }, + // transform is 3x3 matrix_t + warp_perspective: function(src, dst, transform, fill_value) { + if (typeof fill_value === "undefined") { fill_value = 0; } + var src_width=src.cols|0, src_height=src.rows|0, dst_width=dst.cols|0, dst_height=dst.rows|0; + var src_d=src.data, dst_d=dst.data; + var x=0,y=0,off=0,ixs=0,iys=0,xs=0.0,ys=0.0,xs0=0.0,ys0=0.0,ws=0.0,sc=0.0,a=0.0,b=0.0,p0=0.0,p1=0.0; + var td=transform.data; + var m00=td[0],m01=td[1],m02=td[2], + m10=td[3],m11=td[4],m12=td[5], + m20=td[6],m21=td[7],m22=td[8]; + + for(var dptr = 0; y < dst_height; ++y) { + xs0 = m01 * y + m02, + ys0 = m11 * y + m12, + ws = m21 * y + m22; + for(x = 0; x < dst_width; ++x, ++dptr, xs0+=m00, ys0+=m10, ws+=m20) { + sc = 1.0 / ws; + xs = xs0 * sc, ys = ys0 * sc; + ixs = xs | 0, iys = ys | 0; + + if(xs > 0 && ys > 0 && ixs < (src_width - 1) && iys < (src_height - 1)) { + a = Math.max(xs - ixs, 0.0); + b = Math.max(ys - iys, 0.0); + off = (src_width*iys + ixs)|0; + + p0 = src_d[off] + a * (src_d[off+1] - src_d[off]); + p1 = src_d[off+src_width] + a * (src_d[off+src_width+1] - src_d[off+src_width]); + + dst_d[dptr] = p0 + b * (p1 - p0); + } + else dst_d[dptr] = fill_value; + } + } + }, + // transform is 3x3 or 2x3 matrix_t only first 6 values referenced + warp_affine: function(src, dst, transform, fill_value) { + if (typeof fill_value === "undefined") { fill_value = 0; } + var src_width=src.cols, src_height=src.rows, dst_width=dst.cols, dst_height=dst.rows; + var src_d=src.data, dst_d=dst.data; + var x=0,y=0,off=0,ixs=0,iys=0,xs=0.0,ys=0.0,a=0.0,b=0.0,p0=0.0,p1=0.0; + var td=transform.data; + var m00=td[0],m01=td[1],m02=td[2], + m10=td[3],m11=td[4],m12=td[5]; + + for(var dptr = 0; y < dst_height; ++y) { + xs = m01 * y + m02; + ys = m11 * y + m12; + for(x = 0; x < dst_width; ++x, ++dptr, xs+=m00, ys+=m10) { + ixs = xs | 0; iys = ys | 0; + + if(ixs >= 0 && iys >= 0 && ixs < (src_width - 1) && iys < (src_height - 1)) { + a = xs - ixs; + b = ys - iys; + off = src_width*iys + ixs; + + p0 = src_d[off] + a * (src_d[off+1] - src_d[off]); + p1 = src_d[off+src_width] + a * (src_d[off+src_width+1] - src_d[off+src_width]); + + dst_d[dptr] = p0 + b * (p1 - p0); + } + else dst_d[dptr] = fill_value; + } + } + }, + + // Basic RGB Skin detection filter + // from http://popscan.blogspot.fr/2012/08/skin-detection-in-digital-images.html + skindetector: function(src,dst) { + var r,g,b,j; + var i = src.width*src.height; + while(i--){ + j = i*4; + r = src.data[j]; + g = src.data[j+1]; + b = src.data[j+2]; + if((r>95)&&(g>40)&&(b>20) + &&(r>g)&&(r>b) + &&(r-Math.min(g,b)>15) + &&(Math.abs(r-g)>15)){ + dst[i] = 255; + } else { + dst[i] = 0; + } + } + } + }; + })(); + + global.imgproc = imgproc; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * This is FAST corner detector, contributed to OpenCV by the author, Edward Rosten. + */ + +/* +The references are: + * Machine learning for high-speed corner detection, + E. Rosten and T. Drummond, ECCV 2006 + * Faster and better: A machine learning approach to corner detection + E. Rosten, R. Porter and T. Drummond, PAMI, 2009 +*/ + +(function(global) { + "use strict"; + // + var fast_corners = (function() { + + var offsets16 = new Int32Array([0, 3, 1, 3, 2, 2, 3, 1, 3, 0, 3, -1, 2, -2, 1, -3, 0, -3, -1, -3, -2, -2, -3, -1, -3, 0, -3, 1, -2, 2, -1, 3]); + + var threshold_tab = new Uint8Array(512); + var pixel_off = new Int32Array(25); + var score_diff = new Int32Array(25); + + // private functions + var _cmp_offsets = function(pixel, step, pattern_size) { + var k = 0; + var offsets = offsets16; + for( ; k < pattern_size; ++k ) { + pixel[k] = offsets[k<<1] + offsets[(k<<1)+1] * step; + } + for( ; k < 25; ++k ) { + pixel[k] = pixel[k - pattern_size]; + } + }, + + _cmp_score_16 = function(src, off, pixel, d, threshold) { + var N = 25, k = 0, v = src[off]; + var a0 = threshold,a=0,b0=0,b=0; + + for( ; k < N; ++k ) { + d[k] = v - src[off+pixel[k]]; + } + + for( k = 0; k < 16; k += 2 ) { + a = Math.min(d[k+1], d[k+2]); + a = Math.min(a, d[k+3]); + + if( a <= a0 ) continue; + + a = Math.min(a, d[k+4]); + a = Math.min(a, d[k+5]); + a = Math.min(a, d[k+6]); + a = Math.min(a, d[k+7]); + a = Math.min(a, d[k+8]); + a0 = Math.max(a0, Math.min(a, d[k])); + a0 = Math.max(a0, Math.min(a, d[k+9])); + } + + b0 = -a0; + for( k = 0; k < 16; k += 2 ) { + b = Math.max(d[k+1], d[k+2]); + b = Math.max(b, d[k+3]); + b = Math.max(b, d[k+4]); + b = Math.max(b, d[k+5]); + + if( b >= b0 ) continue; + b = Math.max(b, d[k+6]); + b = Math.max(b, d[k+7]); + b = Math.max(b, d[k+8]); + b0 = Math.min(b0, Math.max(b, d[k])); + b0 = Math.min(b0, Math.max(b, d[k+9])); + } + + return -b0-1; + }; + + var _threshold = 20; + + return { + set_threshold: function(threshold) { + _threshold = Math.min(Math.max(threshold, 0), 255); + for (var i = -255; i <= 255; ++i) { + threshold_tab[(i + 255)] = (i < -_threshold ? 1 : (i > _threshold ? 2 : 0)); + } + return _threshold; + }, + + detect: function(src, corners, border) { + if (typeof border === "undefined") { border = 3; } + + var K = 8, N = 25; + var img = src.data, w = src.cols, h = src.rows; + var i=0, j=0, k=0, vt=0, x=0, m3=0; + var buf_node = jsfeat.cache.get_buffer(3 * w); + var cpbuf_node = jsfeat.cache.get_buffer(((w+1)*3)<<2); + var buf = buf_node.u8; + var cpbuf = cpbuf_node.i32; + var pixel = pixel_off; + var sd = score_diff; + var sy = Math.max(3, border); + var ey = Math.min((h-2), (h-border)); + var sx = Math.max(3, border); + var ex = Math.min((w - 3), (w - border)); + var _count = 0, corners_cnt = 0, pt; + var score_func = _cmp_score_16; + var thresh_tab = threshold_tab; + var threshold = _threshold; + + var v=0,tab=0,d=0,ncorners=0,cornerpos=0,curr=0,ptr=0,prev=0,pprev=0; + var jp1=0,jm1=0,score=0; + + _cmp_offsets(pixel, w, 16); + + // local vars are faster? + var pixel0 = pixel[0]; + var pixel1 = pixel[1]; + var pixel2 = pixel[2]; + var pixel3 = pixel[3]; + var pixel4 = pixel[4]; + var pixel5 = pixel[5]; + var pixel6 = pixel[6]; + var pixel7 = pixel[7]; + var pixel8 = pixel[8]; + var pixel9 = pixel[9]; + var pixel10 = pixel[10]; + var pixel11 = pixel[11]; + var pixel12 = pixel[12]; + var pixel13 = pixel[13]; + var pixel14 = pixel[14]; + var pixel15 = pixel[15]; + + for(i = 0; i < w*3; ++i) { + buf[i] = 0; + } + + for(i = sy; i < ey; ++i) { + ptr = ((i * w) + sx)|0; + m3 = (i - 3)%3; + curr = (m3*w)|0; + cornerpos = (m3*(w+1))|0; + for (j = 0; j < w; ++j) buf[curr+j] = 0; + ncorners = 0; + + if( i < (ey - 1) ) { + j = sx; + + for( ; j < ex; ++j, ++ptr ) { + v = img[ptr]; + tab = ( - v + 255 ); + d = ( thresh_tab[tab+img[ptr+pixel0]] | thresh_tab[tab+img[ptr+pixel8]] ); + + if( d == 0 ) { + continue; + } + + d &= ( thresh_tab[tab+img[ptr+pixel2]] | thresh_tab[tab+img[ptr+pixel10]] ); + d &= ( thresh_tab[tab+img[ptr+pixel4]] | thresh_tab[tab+img[ptr+pixel12]] ); + d &= ( thresh_tab[tab+img[ptr+pixel6]] | thresh_tab[tab+img[ptr+pixel14]] ); + + if( d == 0 ) { + continue; + } + + d &= ( thresh_tab[tab+img[ptr+pixel1]] | thresh_tab[tab+img[ptr+pixel9]] ); + d &= ( thresh_tab[tab+img[ptr+pixel3]] | thresh_tab[tab+img[ptr+pixel11]] ); + d &= ( thresh_tab[tab+img[ptr+pixel5]] | thresh_tab[tab+img[ptr+pixel13]] ); + d &= ( thresh_tab[tab+img[ptr+pixel7]] | thresh_tab[tab+img[ptr+pixel15]] ); + + if( d & 1 ) { + vt = (v - threshold); + _count = 0; + + for( k = 0; k < N; ++k ) { + x = img[(ptr+pixel[k])]; + if(x < vt) { + ++_count; + if( _count > K ) { + ++ncorners; + cpbuf[cornerpos+ncorners] = j; + buf[curr+j] = score_func(img, ptr, pixel, sd, threshold); + break; + } + } + else { + _count = 0; + } + } + } + + if( d & 2 ) { + vt = (v + threshold); + _count = 0; + + for( k = 0; k < N; ++k ) { + x = img[(ptr+pixel[k])]; + if(x > vt) { + ++_count; + if( _count > K ) { + ++ncorners; + cpbuf[cornerpos+ncorners] = j; + buf[curr+j] = score_func(img, ptr, pixel, sd, threshold); + break; + } + } + else { + _count = 0; + } + } + } + } + } + + cpbuf[cornerpos+w] = ncorners; + + if ( i == sy ) { + continue; + } + + m3 = (i - 4 + 3)%3; + prev = (m3*w)|0; + cornerpos = (m3*(w+1))|0; + m3 = (i - 5 + 3)%3; + pprev = (m3*w)|0; + + ncorners = cpbuf[cornerpos+w]; + + for( k = 0; k < ncorners; ++k ) { + j = cpbuf[cornerpos+k]; + jp1 = (j+1)|0; + jm1 = (j-1)|0; + score = buf[prev+j]; + if( (score > buf[prev+jp1] && score > buf[prev+jm1] && + score > buf[pprev+jm1] && score > buf[pprev+j] && score > buf[pprev+jp1] && + score > buf[curr+jm1] && score > buf[curr+j] && score > buf[curr+jp1]) ) { + // save corner + pt = corners[corners_cnt]; + pt.x = j, pt.y = (i-1), pt.score = score; + corners_cnt++; + } + } + } // y loop + jsfeat.cache.put_buffer(buf_node); + jsfeat.cache.put_buffer(cpbuf_node); + return corners_cnt; + } + }; + })(); + + global.fast_corners = fast_corners; + fast_corners.set_threshold(20); // set default + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * Copyright 2007 Computer Vision Lab, + * Ecole Polytechnique Federale de Lausanne (EPFL), Switzerland. + * @author Vincent Lepetit (http://cvlab.epfl.ch/~lepetit) + */ + +(function(global) { + "use strict"; + // + + var yape06 = (function() { + + var compute_laplacian = function(src, dst, w, h, Dxx, Dyy, sx,sy, ex,ey) { + var y=0,x=0,yrow=(sy*w+sx)|0,row=yrow; + + for(y = sy; y < ey; ++y, yrow+=w, row = yrow) { + for(x = sx; x < ex; ++x, ++row) { + dst[row] = -4 * src[row] + src[row+Dxx] + src[row-Dxx] + src[row+Dyy] + src[row-Dyy]; + } + } + } + + var hessian_min_eigen_value = function(src, off, tr, Dxx, Dyy, Dxy, Dyx) { + var Ixx = -2 * src[off] + src[off + Dxx] + src[off - Dxx]; + var Iyy = -2 * src[off] + src[off + Dyy] + src[off - Dyy]; + var Ixy = src[off + Dxy] + src[off - Dxy] - src[off + Dyx] - src[off - Dyx]; + var sqrt_delta = ( Math.sqrt(((Ixx - Iyy) * (Ixx - Iyy) + 4 * Ixy * Ixy) ) )|0; + + return Math.min(Math.abs(tr - sqrt_delta), Math.abs(-(tr + sqrt_delta))); + } + + return { + + laplacian_threshold: 30, + min_eigen_value_threshold: 25, + + detect: function(src, points, border) { + if (typeof border === "undefined") { border = 5; } + var x=0,y=0; + var w=src.cols, h=src.rows, srd_d=src.data; + var Dxx = 5, Dyy = (5 * w)|0; + var Dxy = (3 + 3 * w)|0, Dyx = (3 - 3 * w)|0; + var lap_buf = jsfeat.cache.get_buffer((w*h)<<2); + var laplacian = lap_buf.i32; + var lv=0, row=0,rowx=0,min_eigen_value=0,pt; + var number_of_points = 0; + var lap_thresh = this.laplacian_threshold; + var eigen_thresh = this.min_eigen_value_threshold; + + var sx = Math.max(5, border)|0; + var sy = Math.max(3, border)|0; + var ex = Math.min(w-5, w-border)|0; + var ey = Math.min(h-3, h-border)|0; + + x = w*h; + while(--x>=0) {laplacian[x]=0;} + compute_laplacian(srd_d, laplacian, w, h, Dxx, Dyy, sx,sy, ex,ey); + + row = (sy*w+sx)|0; + for(y = sy; y < ey; ++y, row += w) { + for(x = sx, rowx=row; x < ex; ++x, ++rowx) { + + lv = laplacian[rowx]; + if ((lv < -lap_thresh && + lv < laplacian[rowx - 1] && lv < laplacian[rowx + 1] && + lv < laplacian[rowx - w] && lv < laplacian[rowx + w] && + lv < laplacian[rowx - w - 1] && lv < laplacian[rowx + w - 1] && + lv < laplacian[rowx - w + 1] && lv < laplacian[rowx + w + 1]) + || + (lv > lap_thresh && + lv > laplacian[rowx - 1] && lv > laplacian[rowx + 1] && + lv > laplacian[rowx - w] && lv > laplacian[rowx + w] && + lv > laplacian[rowx - w - 1] && lv > laplacian[rowx + w - 1] && + lv > laplacian[rowx - w + 1] && lv > laplacian[rowx + w + 1]) + ) { + + min_eigen_value = hessian_min_eigen_value(srd_d, rowx, lv, Dxx, Dyy, Dxy, Dyx); + if (min_eigen_value > eigen_thresh) { + pt = points[number_of_points]; + pt.x = x, pt.y = y, pt.score = min_eigen_value; + ++number_of_points; + ++x, ++rowx; // skip next pixel since this is maxima in 3x3 + } + } + } + } + + jsfeat.cache.put_buffer(lap_buf); + + return number_of_points; + } + + }; + })(); + + global.yape06 = yape06; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * Copyright 2007 Computer Vision Lab, + * Ecole Polytechnique Federale de Lausanne (EPFL), Switzerland. + */ + +(function(global) { + "use strict"; + // + + var yape = (function() { + + var precompute_directions = function(step, dirs, R) { + var i = 0; + var x, y; + + x = R; + for(y = 0; y < x; y++, i++) + { + x = (Math.sqrt((R * R - y * y)) + 0.5)|0; + dirs[i] = (x + step * y); + } + for(x-- ; x < y && x >= 0; x--, i++) + { + y = (Math.sqrt((R * R - x * x)) + 0.5)|0; + dirs[i] = (x + step * y); + } + for( ; -x < y; x--, i++) + { + y = (Math.sqrt((R * R - x * x)) + 0.5)|0; + dirs[i] = (x + step * y); + } + for(y-- ; y >= 0; y--, i++) + { + x = (-Math.sqrt((R * R - y * y)) - 0.5)|0; + dirs[i] = (x + step * y); + } + for(; y > x; y--, i++) + { + x = (-Math.sqrt((R * R - y * y)) - 0.5)|0; + dirs[i] = (x + step * y); + } + for(x++ ; x <= 0; x++, i++) + { + y = (-Math.sqrt((R * R - x * x)) - 0.5)|0; + dirs[i] = (x + step * y); + } + for( ; x < -y; x++, i++) + { + y = (-Math.sqrt((R * R - x * x)) - 0.5)|0; + dirs[i] = (x + step * y); + } + for(y++ ; y < 0; y++, i++) + { + x = (Math.sqrt((R * R - y * y)) + 0.5)|0; + dirs[i] = (x + step * y); + } + + dirs[i] = dirs[0]; + dirs[i + 1] = dirs[1]; + return i; + } + + var third_check = function (Sb, off, step) { + var n = 0; + if(Sb[off+1] != 0) n++; + if(Sb[off-1] != 0) n++; + if(Sb[off+step] != 0) n++; + if(Sb[off+step+1] != 0) n++; + if(Sb[off+step-1] != 0) n++; + if(Sb[off-step] != 0) n++; + if(Sb[off-step+1] != 0) n++; + if(Sb[off-step-1] != 0) n++; + + return n; + } + + var is_local_maxima = function(p, off, v, step, neighborhood) { + var x, y; + + if (v > 0) { + off -= step*neighborhood; + for (y= -neighborhood; y<=neighborhood; ++y) { + for (x= -neighborhood; x<=neighborhood; ++x) { + if (p[off+x] > v) return false; + } + off += step; + } + } else { + off -= step*neighborhood; + for (y= -neighborhood; y<=neighborhood; ++y) { + for (x= -neighborhood; x<=neighborhood; ++x) { + if (p[off+x] < v) return false; + } + off += step; + } + } + return true; + } + + var perform_one_point = function(I, x, Scores, Im, Ip, dirs, opposite, dirs_nb) { + var score = 0; + var a = 0, b = (opposite - 1)|0; + var A=0, B0=0, B1=0, B2=0; + var state=0; + + // WE KNOW THAT NOT(A ~ I0 & B1 ~ I0): + A = I[x+dirs[a]]; + if ((A <= Ip)) { + if ((A >= Im)) { // A ~ I0 + B0 = I[x+dirs[b]]; + if ((B0 <= Ip)) { + if ((B0 >= Im)) { Scores[x] = 0; return; } + else { + b++; B1 = I[x+dirs[b]]; + if ((B1 > Ip)) { + b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) state = 3; + else if ((B2 < Im)) state = 6; + else { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + } + else/* if ((B1 < Im))*/ { + b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) state = 7; + else if ((B2 < Im)) state = 2; + else { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + } + //else { Scores[x] = 0; return; } // A ~ I0, B1 ~ I0 + } + } + else { // B0 < I0 + b++; B1 = I[x+dirs[b]]; + if ((B1 > Ip)) { + b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) state = 3; + else if ((B2 < Im)) state = 6; + else { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + } + else if ((B1 < Im)) { + b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) state = 7; + else if ((B2 < Im)) state = 2; + else { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + } + else { Scores[x] = 0; return; } // A ~ I0, B1 ~ I0 + } + } + else { // A > I0 + B0 = I[x+dirs[b]]; + if ((B0 > Ip)) { Scores[x] = 0; return; } + b++; B1 = I[x+dirs[b]]; + if ((B1 > Ip)) { Scores[x] = 0; return; } + b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { Scores[x] = 0; return; } + state = 1; + } + } + else // A < I0 + { + B0 = I[x+dirs[b]]; + if ((B0 < Im)) { Scores[x] = 0; return; } + b++; B1 = I[x+dirs[b]]; + if ((B1 < Im)) { Scores[x] = 0; return; } + b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { Scores[x] = 0; return; } + state = 0; + } + + for(a = 1; a <= opposite; a++) + { + A = I[x+dirs[a]]; + + switch(state) + { + case 0: + if ((A > Ip)) { + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { Scores[x] = 0; return; } + { score -= A + B1; state = 0; break; }; + } + if ((A < Im)) { + if ((B1 > Ip)) { Scores[x] = 0; return; } + if ((B2 > Ip)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { Scores[x] = 0; return; } + { score -= A + B1; state = 8; break; }; + } + // A ~ I0 + if ((B1 <= Ip)) { Scores[x] = 0; return; } + if ((B2 <= Ip)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { score -= A + B1; state = 3; break; }; + if ((B2 < Im)) { score -= A + B1; state = 6; break; }; + { Scores[x] = 0; return; } + + case 1: + if ((A < Im)) { + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { Scores[x] = 0; return; } + { score -= A + B1; state = 1; break; }; + } + if ((A > Ip)) { + if ((B1 < Im)) { Scores[x] = 0; return; } + if ((B2 < Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { Scores[x] = 0; return; } + { score -= A + B1; state = 9; break; }; + } + // A ~ I0 + if ((B1 >= Im)) { Scores[x] = 0; return; } + if ((B2 >= Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { score -= A + B1; state = 2; break; }; + if ((B2 > Ip)) { score -= A + B1; state = 7; break; }; + { Scores[x] = 0; return; } + + case 2: + if ((A > Ip)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((A < Im)) + { + if ((B2 > Ip)) { Scores[x] = 0; return; } + { score -= A + B1; state = 4; break; }; + } + // A ~ I0 + if ((B2 > Ip)) { score -= A + B1; state = 7; break; }; + if ((B2 < Im)) { score -= A + B1; state = 2; break; }; + { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + + case 3: + if ((A < Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((A > Ip)) { + if ((B2 < Im)) { Scores[x] = 0; return; } + { score -= A + B1; state = 5; break; }; + } + // A ~ I0 + if ((B2 > Ip)) { score -= A + B1; state = 3; break; }; + if ((B2 < Im)) { score -= A + B1; state = 6; break; }; + { Scores[x] = 0; return; } + + case 4: + if ((A > Ip)) { Scores[x] = 0; return; } + if ((A < Im)) { + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { Scores[x] = 0; return; } + { score -= A + B1; state = 1; break; }; + } + if ((B2 >= Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { score -= A + B1; state = 2; break; }; + if ((B2 > Ip)) { score -= A + B1; state = 7; break; }; + { Scores[x] = 0; return; } + + case 5: + if ((A < Im)) { Scores[x] = 0; return; } + if ((A > Ip)) { + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { Scores[x] = 0; return; } + { score -= A + B1; state = 0; break; }; + } + // A ~ I0 + if ((B2 <= Ip)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { score -= A + B1; state = 3; break; }; + if ((B2 < Im)) { score -= A + B1; state = 6; break; }; + { Scores[x] = 0; return; } + + case 7: + if ((A > Ip)) { Scores[x] = 0; return; } + if ((A < Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + // A ~ I0 + if ((B2 > Ip)) { score -= A + B1; state = 3; break; }; + if ((B2 < Im)) { score -= A + B1; state = 6; break; }; + { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + + case 6: + if ((A > Ip)) { Scores[x] = 0; return; } + if ((A < Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + // A ~ I0 + if ((B2 < Im)) { score -= A + B1; state = 2; break; }; + if ((B2 > Ip)) { score -= A + B1; state = 7; break; }; + { Scores[x] = 0; return; } // A ~ I0, B2 ~ I0 + + case 8: + if ((A > Ip)) { + if ((B2 < Im)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { Scores[x] = 0; return; } + { score -= A + B1; state = 9; break; }; + } + if ((A < Im)) { + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { Scores[x] = 0; return; } + { score -= A + B1; state = 1; break; }; + } + { Scores[x] = 0; return; } + + case 9: + if ((A < Im)) { + if ((B2 > Ip)) { Scores[x] = 0; return; } + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 > Ip)) { Scores[x] = 0; return; } + { score -= A + B1; state = 8; break; }; + } + if ((A > Ip)) { + B1 = B2; b++; B2 = I[x+dirs[b]]; + if ((B2 < Im)) { Scores[x] = 0; return; } + { score -= A + B1; state = 0; break; }; + } + { Scores[x] = 0; return; } + + default: + //"PB default"; + break; + } // switch(state) + } // for(a...) + + Scores[x] = (score + dirs_nb * I[x]); + } + + var lev_table_t = (function () { + function lev_table_t(w, h, r) { + this.dirs = new Int32Array(1024); + this.dirs_count = precompute_directions(w, this.dirs, r)|0; + this.scores = new Int32Array(w*h); + this.radius = r|0; + } + return lev_table_t; + })(); + + return { + + level_tables: [], + tau: 7, + + init: function(width, height, radius, pyramid_levels) { + if (typeof pyramid_levels === "undefined") { pyramid_levels = 1; } + var i; + radius = Math.min(radius, 7); + radius = Math.max(radius, 3); + for(i = 0; i < pyramid_levels; ++i) { + this.level_tables[i] = new lev_table_t(width>>i, height>>i, radius); + } + }, + + detect: function(src, points, border) { + if (typeof border === "undefined") { border = 4; } + var t = this.level_tables[0]; + var R = t.radius|0, Rm1 = (R-1)|0; + var dirs = t.dirs; + var dirs_count = t.dirs_count|0; + var opposite = dirs_count >> 1; + var img = src.data, w=src.cols|0, h=src.rows|0,hw=w>>1; + var scores = t.scores; + var x=0,y=0,row=0,rowx=0,ip=0,im=0,abs_score=0, score=0; + var tau = this.tau|0; + var number_of_points = 0, pt; + + var sx = Math.max(R+1, border)|0; + var sy = Math.max(R+1, border)|0; + var ex = Math.min(w-R-2, w-border)|0; + var ey = Math.min(h-R-2, h-border)|0; + + row = (sy*w+sx)|0; + for(y = sy; y < ey; ++y, row+=w) { + for(x = sx, rowx = row; x < ex; ++x, ++rowx) { + ip = img[rowx] + tau, im = img[rowx] - tau; + + if (im= 3 && is_local_maxima(scores, rowx, score, hw, R)) { + pt = points[number_of_points]; + pt.x = x, pt.y = y, pt.score = abs_score; + ++number_of_points; + + x += Rm1, rowx += Rm1; + } + } + } + } + + return number_of_points; + } + }; + + })(); + + global.yape = yape; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * Original implementation derived from OpenCV, + * @authors Ethan Rublee, Vincent Rabaud, Gary Bradski + */ + +(function(global) { + "use strict"; + // + + var orb = (function() { + + var bit_pattern_31_ = new Int32Array([ + 8,-3, 9,5/*mean (0), correlation (0)*/, + 4,2, 7,-12/*mean (1.12461e-05), correlation (0.0437584)*/, + -11,9, -8,2/*mean (3.37382e-05), correlation (0.0617409)*/, + 7,-12, 12,-13/*mean (5.62303e-05), correlation (0.0636977)*/, + 2,-13, 2,12/*mean (0.000134953), correlation (0.085099)*/, + 1,-7, 1,6/*mean (0.000528565), correlation (0.0857175)*/, + -2,-10, -2,-4/*mean (0.0188821), correlation (0.0985774)*/, + -13,-13, -11,-8/*mean (0.0363135), correlation (0.0899616)*/, + -13,-3, -12,-9/*mean (0.121806), correlation (0.099849)*/, + 10,4, 11,9/*mean (0.122065), correlation (0.093285)*/, + -13,-8, -8,-9/*mean (0.162787), correlation (0.0942748)*/, + -11,7, -9,12/*mean (0.21561), correlation (0.0974438)*/, + 7,7, 12,6/*mean (0.160583), correlation (0.130064)*/, + -4,-5, -3,0/*mean (0.228171), correlation (0.132998)*/, + -13,2, -12,-3/*mean (0.00997526), correlation (0.145926)*/, + -9,0, -7,5/*mean (0.198234), correlation (0.143636)*/, + 12,-6, 12,-1/*mean (0.0676226), correlation (0.16689)*/, + -3,6, -2,12/*mean (0.166847), correlation (0.171682)*/, + -6,-13, -4,-8/*mean (0.101215), correlation (0.179716)*/, + 11,-13, 12,-8/*mean (0.200641), correlation (0.192279)*/, + 4,7, 5,1/*mean (0.205106), correlation (0.186848)*/, + 5,-3, 10,-3/*mean (0.234908), correlation (0.192319)*/, + 3,-7, 6,12/*mean (0.0709964), correlation (0.210872)*/, + -8,-7, -6,-2/*mean (0.0939834), correlation (0.212589)*/, + -2,11, -1,-10/*mean (0.127778), correlation (0.20866)*/, + -13,12, -8,10/*mean (0.14783), correlation (0.206356)*/, + -7,3, -5,-3/*mean (0.182141), correlation (0.198942)*/, + -4,2, -3,7/*mean (0.188237), correlation (0.21384)*/, + -10,-12, -6,11/*mean (0.14865), correlation (0.23571)*/, + 5,-12, 6,-7/*mean (0.222312), correlation (0.23324)*/, + 5,-6, 7,-1/*mean (0.229082), correlation (0.23389)*/, + 1,0, 4,-5/*mean (0.241577), correlation (0.215286)*/, + 9,11, 11,-13/*mean (0.00338507), correlation (0.251373)*/, + 4,7, 4,12/*mean (0.131005), correlation (0.257622)*/, + 2,-1, 4,4/*mean (0.152755), correlation (0.255205)*/, + -4,-12, -2,7/*mean (0.182771), correlation (0.244867)*/, + -8,-5, -7,-10/*mean (0.186898), correlation (0.23901)*/, + 4,11, 9,12/*mean (0.226226), correlation (0.258255)*/, + 0,-8, 1,-13/*mean (0.0897886), correlation (0.274827)*/, + -13,-2, -8,2/*mean (0.148774), correlation (0.28065)*/, + -3,-2, -2,3/*mean (0.153048), correlation (0.283063)*/, + -6,9, -4,-9/*mean (0.169523), correlation (0.278248)*/, + 8,12, 10,7/*mean (0.225337), correlation (0.282851)*/, + 0,9, 1,3/*mean (0.226687), correlation (0.278734)*/, + 7,-5, 11,-10/*mean (0.00693882), correlation (0.305161)*/, + -13,-6, -11,0/*mean (0.0227283), correlation (0.300181)*/, + 10,7, 12,1/*mean (0.125517), correlation (0.31089)*/, + -6,-3, -6,12/*mean (0.131748), correlation (0.312779)*/, + 10,-9, 12,-4/*mean (0.144827), correlation (0.292797)*/, + -13,8, -8,-12/*mean (0.149202), correlation (0.308918)*/, + -13,0, -8,-4/*mean (0.160909), correlation (0.310013)*/, + 3,3, 7,8/*mean (0.177755), correlation (0.309394)*/, + 5,7, 10,-7/*mean (0.212337), correlation (0.310315)*/, + -1,7, 1,-12/*mean (0.214429), correlation (0.311933)*/, + 3,-10, 5,6/*mean (0.235807), correlation (0.313104)*/, + 2,-4, 3,-10/*mean (0.00494827), correlation (0.344948)*/, + -13,0, -13,5/*mean (0.0549145), correlation (0.344675)*/, + -13,-7, -12,12/*mean (0.103385), correlation (0.342715)*/, + -13,3, -11,8/*mean (0.134222), correlation (0.322922)*/, + -7,12, -4,7/*mean (0.153284), correlation (0.337061)*/, + 6,-10, 12,8/*mean (0.154881), correlation (0.329257)*/, + -9,-1, -7,-6/*mean (0.200967), correlation (0.33312)*/, + -2,-5, 0,12/*mean (0.201518), correlation (0.340635)*/, + -12,5, -7,5/*mean (0.207805), correlation (0.335631)*/, + 3,-10, 8,-13/*mean (0.224438), correlation (0.34504)*/, + -7,-7, -4,5/*mean (0.239361), correlation (0.338053)*/, + -3,-2, -1,-7/*mean (0.240744), correlation (0.344322)*/, + 2,9, 5,-11/*mean (0.242949), correlation (0.34145)*/, + -11,-13, -5,-13/*mean (0.244028), correlation (0.336861)*/, + -1,6, 0,-1/*mean (0.247571), correlation (0.343684)*/, + 5,-3, 5,2/*mean (0.000697256), correlation (0.357265)*/, + -4,-13, -4,12/*mean (0.00213675), correlation (0.373827)*/, + -9,-6, -9,6/*mean (0.0126856), correlation (0.373938)*/, + -12,-10, -8,-4/*mean (0.0152497), correlation (0.364237)*/, + 10,2, 12,-3/*mean (0.0299933), correlation (0.345292)*/, + 7,12, 12,12/*mean (0.0307242), correlation (0.366299)*/, + -7,-13, -6,5/*mean (0.0534975), correlation (0.368357)*/, + -4,9, -3,4/*mean (0.099865), correlation (0.372276)*/, + 7,-1, 12,2/*mean (0.117083), correlation (0.364529)*/, + -7,6, -5,1/*mean (0.126125), correlation (0.369606)*/, + -13,11, -12,5/*mean (0.130364), correlation (0.358502)*/, + -3,7, -2,-6/*mean (0.131691), correlation (0.375531)*/, + 7,-8, 12,-7/*mean (0.160166), correlation (0.379508)*/, + -13,-7, -11,-12/*mean (0.167848), correlation (0.353343)*/, + 1,-3, 12,12/*mean (0.183378), correlation (0.371916)*/, + 2,-6, 3,0/*mean (0.228711), correlation (0.371761)*/, + -4,3, -2,-13/*mean (0.247211), correlation (0.364063)*/, + -1,-13, 1,9/*mean (0.249325), correlation (0.378139)*/, + 7,1, 8,-6/*mean (0.000652272), correlation (0.411682)*/, + 1,-1, 3,12/*mean (0.00248538), correlation (0.392988)*/, + 9,1, 12,6/*mean (0.0206815), correlation (0.386106)*/, + -1,-9, -1,3/*mean (0.0364485), correlation (0.410752)*/, + -13,-13, -10,5/*mean (0.0376068), correlation (0.398374)*/, + 7,7, 10,12/*mean (0.0424202), correlation (0.405663)*/, + 12,-5, 12,9/*mean (0.0942645), correlation (0.410422)*/, + 6,3, 7,11/*mean (0.1074), correlation (0.413224)*/, + 5,-13, 6,10/*mean (0.109256), correlation (0.408646)*/, + 2,-12, 2,3/*mean (0.131691), correlation (0.416076)*/, + 3,8, 4,-6/*mean (0.165081), correlation (0.417569)*/, + 2,6, 12,-13/*mean (0.171874), correlation (0.408471)*/, + 9,-12, 10,3/*mean (0.175146), correlation (0.41296)*/, + -8,4, -7,9/*mean (0.183682), correlation (0.402956)*/, + -11,12, -4,-6/*mean (0.184672), correlation (0.416125)*/, + 1,12, 2,-8/*mean (0.191487), correlation (0.386696)*/, + 6,-9, 7,-4/*mean (0.192668), correlation (0.394771)*/, + 2,3, 3,-2/*mean (0.200157), correlation (0.408303)*/, + 6,3, 11,0/*mean (0.204588), correlation (0.411762)*/, + 3,-3, 8,-8/*mean (0.205904), correlation (0.416294)*/, + 7,8, 9,3/*mean (0.213237), correlation (0.409306)*/, + -11,-5, -6,-4/*mean (0.243444), correlation (0.395069)*/, + -10,11, -5,10/*mean (0.247672), correlation (0.413392)*/, + -5,-8, -3,12/*mean (0.24774), correlation (0.411416)*/, + -10,5, -9,0/*mean (0.00213675), correlation (0.454003)*/, + 8,-1, 12,-6/*mean (0.0293635), correlation (0.455368)*/, + 4,-6, 6,-11/*mean (0.0404971), correlation (0.457393)*/, + -10,12, -8,7/*mean (0.0481107), correlation (0.448364)*/, + 4,-2, 6,7/*mean (0.050641), correlation (0.455019)*/, + -2,0, -2,12/*mean (0.0525978), correlation (0.44338)*/, + -5,-8, -5,2/*mean (0.0629667), correlation (0.457096)*/, + 7,-6, 10,12/*mean (0.0653846), correlation (0.445623)*/, + -9,-13, -8,-8/*mean (0.0858749), correlation (0.449789)*/, + -5,-13, -5,-2/*mean (0.122402), correlation (0.450201)*/, + 8,-8, 9,-13/*mean (0.125416), correlation (0.453224)*/, + -9,-11, -9,0/*mean (0.130128), correlation (0.458724)*/, + 1,-8, 1,-2/*mean (0.132467), correlation (0.440133)*/, + 7,-4, 9,1/*mean (0.132692), correlation (0.454)*/, + -2,1, -1,-4/*mean (0.135695), correlation (0.455739)*/, + 11,-6, 12,-11/*mean (0.142904), correlation (0.446114)*/, + -12,-9, -6,4/*mean (0.146165), correlation (0.451473)*/, + 3,7, 7,12/*mean (0.147627), correlation (0.456643)*/, + 5,5, 10,8/*mean (0.152901), correlation (0.455036)*/, + 0,-4, 2,8/*mean (0.167083), correlation (0.459315)*/, + -9,12, -5,-13/*mean (0.173234), correlation (0.454706)*/, + 0,7, 2,12/*mean (0.18312), correlation (0.433855)*/, + -1,2, 1,7/*mean (0.185504), correlation (0.443838)*/, + 5,11, 7,-9/*mean (0.185706), correlation (0.451123)*/, + 3,5, 6,-8/*mean (0.188968), correlation (0.455808)*/, + -13,-4, -8,9/*mean (0.191667), correlation (0.459128)*/, + -5,9, -3,-3/*mean (0.193196), correlation (0.458364)*/, + -4,-7, -3,-12/*mean (0.196536), correlation (0.455782)*/, + 6,5, 8,0/*mean (0.1972), correlation (0.450481)*/, + -7,6, -6,12/*mean (0.199438), correlation (0.458156)*/, + -13,6, -5,-2/*mean (0.211224), correlation (0.449548)*/, + 1,-10, 3,10/*mean (0.211718), correlation (0.440606)*/, + 4,1, 8,-4/*mean (0.213034), correlation (0.443177)*/, + -2,-2, 2,-13/*mean (0.234334), correlation (0.455304)*/, + 2,-12, 12,12/*mean (0.235684), correlation (0.443436)*/, + -2,-13, 0,-6/*mean (0.237674), correlation (0.452525)*/, + 4,1, 9,3/*mean (0.23962), correlation (0.444824)*/, + -6,-10, -3,-5/*mean (0.248459), correlation (0.439621)*/, + -3,-13, -1,1/*mean (0.249505), correlation (0.456666)*/, + 7,5, 12,-11/*mean (0.00119208), correlation (0.495466)*/, + 4,-2, 5,-7/*mean (0.00372245), correlation (0.484214)*/, + -13,9, -9,-5/*mean (0.00741116), correlation (0.499854)*/, + 7,1, 8,6/*mean (0.0208952), correlation (0.499773)*/, + 7,-8, 7,6/*mean (0.0220085), correlation (0.501609)*/, + -7,-4, -7,1/*mean (0.0233806), correlation (0.496568)*/, + -8,11, -7,-8/*mean (0.0236505), correlation (0.489719)*/, + -13,6, -12,-8/*mean (0.0268781), correlation (0.503487)*/, + 2,4, 3,9/*mean (0.0323324), correlation (0.501938)*/, + 10,-5, 12,3/*mean (0.0399235), correlation (0.494029)*/, + -6,-5, -6,7/*mean (0.0420153), correlation (0.486579)*/, + 8,-3, 9,-8/*mean (0.0548021), correlation (0.484237)*/, + 2,-12, 2,8/*mean (0.0616622), correlation (0.496642)*/, + -11,-2, -10,3/*mean (0.0627755), correlation (0.498563)*/, + -12,-13, -7,-9/*mean (0.0829622), correlation (0.495491)*/, + -11,0, -10,-5/*mean (0.0843342), correlation (0.487146)*/, + 5,-3, 11,8/*mean (0.0929937), correlation (0.502315)*/, + -2,-13, -1,12/*mean (0.113327), correlation (0.48941)*/, + -1,-8, 0,9/*mean (0.132119), correlation (0.467268)*/, + -13,-11, -12,-5/*mean (0.136269), correlation (0.498771)*/, + -10,-2, -10,11/*mean (0.142173), correlation (0.498714)*/, + -3,9, -2,-13/*mean (0.144141), correlation (0.491973)*/, + 2,-3, 3,2/*mean (0.14892), correlation (0.500782)*/, + -9,-13, -4,0/*mean (0.150371), correlation (0.498211)*/, + -4,6, -3,-10/*mean (0.152159), correlation (0.495547)*/, + -4,12, -2,-7/*mean (0.156152), correlation (0.496925)*/, + -6,-11, -4,9/*mean (0.15749), correlation (0.499222)*/, + 6,-3, 6,11/*mean (0.159211), correlation (0.503821)*/, + -13,11, -5,5/*mean (0.162427), correlation (0.501907)*/, + 11,11, 12,6/*mean (0.16652), correlation (0.497632)*/, + 7,-5, 12,-2/*mean (0.169141), correlation (0.484474)*/, + -1,12, 0,7/*mean (0.169456), correlation (0.495339)*/, + -4,-8, -3,-2/*mean (0.171457), correlation (0.487251)*/, + -7,1, -6,7/*mean (0.175), correlation (0.500024)*/, + -13,-12, -8,-13/*mean (0.175866), correlation (0.497523)*/, + -7,-2, -6,-8/*mean (0.178273), correlation (0.501854)*/, + -8,5, -6,-9/*mean (0.181107), correlation (0.494888)*/, + -5,-1, -4,5/*mean (0.190227), correlation (0.482557)*/, + -13,7, -8,10/*mean (0.196739), correlation (0.496503)*/, + 1,5, 5,-13/*mean (0.19973), correlation (0.499759)*/, + 1,0, 10,-13/*mean (0.204465), correlation (0.49873)*/, + 9,12, 10,-1/*mean (0.209334), correlation (0.49063)*/, + 5,-8, 10,-9/*mean (0.211134), correlation (0.503011)*/, + -1,11, 1,-13/*mean (0.212), correlation (0.499414)*/, + -9,-3, -6,2/*mean (0.212168), correlation (0.480739)*/, + -1,-10, 1,12/*mean (0.212731), correlation (0.502523)*/, + -13,1, -8,-10/*mean (0.21327), correlation (0.489786)*/, + 8,-11, 10,-6/*mean (0.214159), correlation (0.488246)*/, + 2,-13, 3,-6/*mean (0.216993), correlation (0.50287)*/, + 7,-13, 12,-9/*mean (0.223639), correlation (0.470502)*/, + -10,-10, -5,-7/*mean (0.224089), correlation (0.500852)*/, + -10,-8, -8,-13/*mean (0.228666), correlation (0.502629)*/, + 4,-6, 8,5/*mean (0.22906), correlation (0.498305)*/, + 3,12, 8,-13/*mean (0.233378), correlation (0.503825)*/, + -4,2, -3,-3/*mean (0.234323), correlation (0.476692)*/, + 5,-13, 10,-12/*mean (0.236392), correlation (0.475462)*/, + 4,-13, 5,-1/*mean (0.236842), correlation (0.504132)*/, + -9,9, -4,3/*mean (0.236977), correlation (0.497739)*/, + 0,3, 3,-9/*mean (0.24314), correlation (0.499398)*/, + -12,1, -6,1/*mean (0.243297), correlation (0.489447)*/, + 3,2, 4,-8/*mean (0.00155196), correlation (0.553496)*/, + -10,-10, -10,9/*mean (0.00239541), correlation (0.54297)*/, + 8,-13, 12,12/*mean (0.0034413), correlation (0.544361)*/, + -8,-12, -6,-5/*mean (0.003565), correlation (0.551225)*/, + 2,2, 3,7/*mean (0.00835583), correlation (0.55285)*/, + 10,6, 11,-8/*mean (0.00885065), correlation (0.540913)*/, + 6,8, 8,-12/*mean (0.0101552), correlation (0.551085)*/, + -7,10, -6,5/*mean (0.0102227), correlation (0.533635)*/, + -3,-9, -3,9/*mean (0.0110211), correlation (0.543121)*/, + -1,-13, -1,5/*mean (0.0113473), correlation (0.550173)*/, + -3,-7, -3,4/*mean (0.0140913), correlation (0.554774)*/, + -8,-2, -8,3/*mean (0.017049), correlation (0.55461)*/, + 4,2, 12,12/*mean (0.01778), correlation (0.546921)*/, + 2,-5, 3,11/*mean (0.0224022), correlation (0.549667)*/, + 6,-9, 11,-13/*mean (0.029161), correlation (0.546295)*/, + 3,-1, 7,12/*mean (0.0303081), correlation (0.548599)*/, + 11,-1, 12,4/*mean (0.0355151), correlation (0.523943)*/, + -3,0, -3,6/*mean (0.0417904), correlation (0.543395)*/, + 4,-11, 4,12/*mean (0.0487292), correlation (0.542818)*/, + 2,-4, 2,1/*mean (0.0575124), correlation (0.554888)*/, + -10,-6, -8,1/*mean (0.0594242), correlation (0.544026)*/, + -13,7, -11,1/*mean (0.0597391), correlation (0.550524)*/, + -13,12, -11,-13/*mean (0.0608974), correlation (0.55383)*/, + 6,0, 11,-13/*mean (0.065126), correlation (0.552006)*/, + 0,-1, 1,4/*mean (0.074224), correlation (0.546372)*/, + -13,3, -9,-2/*mean (0.0808592), correlation (0.554875)*/, + -9,8, -6,-3/*mean (0.0883378), correlation (0.551178)*/, + -13,-6, -8,-2/*mean (0.0901035), correlation (0.548446)*/, + 5,-9, 8,10/*mean (0.0949843), correlation (0.554694)*/, + 2,7, 3,-9/*mean (0.0994152), correlation (0.550979)*/, + -1,-6, -1,-1/*mean (0.10045), correlation (0.552714)*/, + 9,5, 11,-2/*mean (0.100686), correlation (0.552594)*/, + 11,-3, 12,-8/*mean (0.101091), correlation (0.532394)*/, + 3,0, 3,5/*mean (0.101147), correlation (0.525576)*/, + -1,4, 0,10/*mean (0.105263), correlation (0.531498)*/, + 3,-6, 4,5/*mean (0.110785), correlation (0.540491)*/, + -13,0, -10,5/*mean (0.112798), correlation (0.536582)*/, + 5,8, 12,11/*mean (0.114181), correlation (0.555793)*/, + 8,9, 9,-6/*mean (0.117431), correlation (0.553763)*/, + 7,-4, 8,-12/*mean (0.118522), correlation (0.553452)*/, + -10,4, -10,9/*mean (0.12094), correlation (0.554785)*/, + 7,3, 12,4/*mean (0.122582), correlation (0.555825)*/, + 9,-7, 10,-2/*mean (0.124978), correlation (0.549846)*/, + 7,0, 12,-2/*mean (0.127002), correlation (0.537452)*/, + -1,-6, 0,-11/*mean (0.127148), correlation (0.547401)*/ + ]); + + var H = new jsfeat.matrix_t(3, 3, jsfeat.F32_t|jsfeat.C1_t); + var patch_img = new jsfeat.matrix_t(32, 32, jsfeat.U8_t|jsfeat.C1_t); + + var rectify_patch = function(src, dst, angle, px, py, psize) { + var cosine = Math.cos(angle); + var sine = Math.sin(angle); + + H.data[0] = cosine, H.data[1] = -sine, H.data[2] = (-cosine + sine ) * psize*0.5 + px, + H.data[3] = sine, H.data[4] = cosine, H.data[5] = (-sine - cosine) * psize*0.5 + py; + + jsfeat.imgproc.warp_affine(src, dst, H, 128); + } + + return { + + describe: function(src, corners, count, descriptors) { + var DESCR_SIZE = 32; // bytes; + var i=0,b=0,px=0.0,py=0.0,angle=0.0; + var t0=0, t1=0, val=0; + var img = src.data, w = src.cols, h = src.rows; + var patch_d = patch_img.data; + var patch_off = 16*32 + 16; // center of patch + var patt=0; + + if(!(descriptors.type&jsfeat.U8_t)) { + // relocate to U8 type + descriptors.type = jsfeat.U8_t; + descriptors.cols = DESCR_SIZE; + descriptors.rows = count; + descriptors.channel = 1; + descriptors.allocate(); + } else { + descriptors.resize(DESCR_SIZE, count, 1); + } + + var descr_d = descriptors.data; + var descr_off = 0; + + for(i = 0; i < count; ++i) { + px = corners[i].x; + py = corners[i].y; + angle = corners[i].angle; + + rectify_patch(src, patch_img, angle, px, py, 32); + + // describe the patch + patt = 0; + for (b = 0; b < DESCR_SIZE; ++b) { + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val = (t0 < t1)|0; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 1; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 2; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 3; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 4; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 5; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 6; + + t0 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + t1 = patch_d[patch_off + bit_pattern_31_[patt+1] * 32 + bit_pattern_31_[patt]]; patt += 2 + val |= (t0 < t1) << 7; + + descr_d[descr_off+b] = val; + } + descr_off += DESCR_SIZE; + } + } + }; + })(); + + global.orb = orb; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * this code is a rewrite from OpenCV's Lucas-Kanade optical flow implementation + */ + +(function(global) { + "use strict"; + // + var optical_flow_lk = (function() { + + // short link to shar deriv + var scharr_deriv = jsfeat.imgproc.scharr_derivatives; + + return { + track: function(prev_pyr, curr_pyr, prev_xy, curr_xy, count, win_size, max_iter, status, eps, min_eigen_threshold) { + if (typeof max_iter === "undefined") { max_iter = 30; } + if (typeof status === "undefined") { status = new Uint8Array(count); } + if (typeof eps === "undefined") { eps = 0.01; } + if (typeof min_eigen_threshold === "undefined") { min_eigen_threshold = 0.0001; } + + var half_win = (win_size-1)*0.5; + var win_area = (win_size*win_size)|0; + var win_area2 = win_area << 1; + var prev_imgs = prev_pyr.data, next_imgs = curr_pyr.data; + var img_prev=prev_imgs[0].data,img_next=next_imgs[0].data; + var w0 = prev_imgs[0].cols, h0 = prev_imgs[0].rows,lw=0,lh=0; + + var iwin_node = jsfeat.cache.get_buffer(win_area<<2); + var deriv_iwin_node = jsfeat.cache.get_buffer(win_area2<<2); + var deriv_lev_node = jsfeat.cache.get_buffer((h0*(w0<<1))<<2); + + var deriv_m = new jsfeat.matrix_t(w0, h0, jsfeat.S32C2_t, deriv_lev_node.data); + + var iwin_buf = iwin_node.i32; + var deriv_iwin = deriv_iwin_node.i32; + var deriv_lev = deriv_lev_node.i32; + + var dstep=0,src=0,dsrc=0,iptr=0,diptr=0,jptr=0; + var lev_sc=0.0,prev_x=0.0,prev_y=0.0,next_x=0.0,next_y=0.0; + var prev_delta_x=0.0,prev_delta_y=0.0,delta_x=0.0,delta_y=0.0; + var iprev_x=0,iprev_y=0,inext_x=0,inext_y=0; + var i=0,j=0,x=0,y=0,level=0,ptid=0,iter=0; + var brd_tl=0,brd_r=0,brd_b=0; + var a=0.0,b=0.0,b1=0.0,b2=0.0; + + // fixed point math + var W_BITS14 = 14; + var W_BITS4 = 14; + var W_BITS1m5 = W_BITS4 - 5; + var W_BITS1m51 = (1 << ((W_BITS1m5) - 1)); + var W_BITS14_ = (1 << W_BITS14); + var W_BITS41 = (1 << ((W_BITS4) - 1)); + var FLT_SCALE = 1.0/(1 << 20); + var iw00=0,iw01=0,iw10=0,iw11=0,ival=0,ixval=0,iyval=0; + var A11=0.0,A12=0.0,A22=0.0,D=0.0,min_eig=0.0; + + var FLT_EPSILON = 0.00000011920929; + eps *= eps; + + // reset status + for(; i < count; ++i) { + status[i] = 1; + } + + var max_level = (prev_pyr.levels - 1)|0; + level = max_level; + + for(; level >= 0; --level) { + lev_sc = (1.0/(1 << level)); + lw = w0 >> level; + lh = h0 >> level; + dstep = lw << 1; + img_prev = prev_imgs[level].data; + img_next = next_imgs[level].data; + + brd_r = (lw - win_size)|0; + brd_b = (lh - win_size)|0; + + // calculate level derivatives + scharr_deriv(prev_imgs[level], deriv_m); + + // iterate through points + for(ptid = 0; ptid < count; ++ptid) { + i = ptid << 1; + j = i + 1; + prev_x = prev_xy[i]*lev_sc; + prev_y = prev_xy[j]*lev_sc; + + if( level == max_level ) { + next_x = prev_x; + next_y = prev_y; + } else { + next_x = curr_xy[i]*2.0; + next_y = curr_xy[j]*2.0; + } + curr_xy[i] = next_x; + curr_xy[j] = next_y; + + prev_x -= half_win; + prev_y -= half_win; + iprev_x = prev_x|0; + iprev_y = prev_y|0; + + // border check + x = (iprev_x <= brd_tl)|(iprev_x >= brd_r)|(iprev_y <= brd_tl)|(iprev_y >= brd_b); + if( x != 0 ) { + if( level == 0 ) { + status[ptid] = 0; + } + continue; + } + + a = prev_x - iprev_x; + b = prev_y - iprev_y; + iw00 = (((1.0 - a)*(1.0 - b)*W_BITS14_) + 0.5)|0; + iw01 = ((a*(1.0 - b)*W_BITS14_) + 0.5)|0; + iw10 = (((1.0 - a)*b*W_BITS14_) + 0.5)|0; + iw11 = (W_BITS14_ - iw00 - iw01 - iw10); + + A11 = 0.0, A12 = 0.0, A22 = 0.0; + + // extract the patch from the first image, compute covariation matrix of derivatives + for( y = 0; y < win_size; ++y ) { + src = ( (y + iprev_y)*lw + iprev_x )|0; + dsrc = src << 1; + + iptr = (y*win_size)|0; + diptr = iptr << 1; + for(x = 0 ; x < win_size; ++x, ++src, ++iptr, dsrc += 2) { + ival = ( (img_prev[src])*iw00 + (img_prev[src+1])*iw01 + + (img_prev[src+lw])*iw10 + (img_prev[src+lw+1])*iw11 ); + ival = (((ival) + W_BITS1m51) >> (W_BITS1m5)); + + ixval = ( deriv_lev[dsrc]*iw00 + deriv_lev[dsrc+2]*iw01 + + deriv_lev[dsrc+dstep]*iw10 + deriv_lev[dsrc+dstep+2]*iw11 ); + ixval = (((ixval) + W_BITS41) >> (W_BITS4)); + + iyval = ( deriv_lev[dsrc+1]*iw00 + deriv_lev[dsrc+3]*iw01 + deriv_lev[dsrc+dstep+1]*iw10 + + deriv_lev[dsrc+dstep+3]*iw11 ); + iyval = (((iyval) + W_BITS41) >> (W_BITS4)); + + iwin_buf[iptr] = ival; + deriv_iwin[diptr++] = ixval; + deriv_iwin[diptr++] = iyval; + + A11 += ixval*ixval; + A12 += ixval*iyval; + A22 += iyval*iyval; + } + } + + A11 *= FLT_SCALE; A12 *= FLT_SCALE; A22 *= FLT_SCALE; + + D = A11*A22 - A12*A12; + min_eig = (A22 + A11 - Math.sqrt((A11-A22)*(A11-A22) + 4.0*A12*A12)) / win_area2; + + if( min_eig < min_eigen_threshold || D < FLT_EPSILON ) + { + if( level == 0 ) { + status[ptid] = 0; + } + continue; + } + + D = 1.0/D; + + next_x -= half_win; + next_y -= half_win; + prev_delta_x = 0.0; + prev_delta_y = 0.0; + + for( iter = 0; iter < max_iter; ++iter ) { + inext_x = next_x|0; + inext_y = next_y|0; + + x = (inext_x <= brd_tl)|(inext_x >= brd_r)|(inext_y <= brd_tl)|(inext_y >= brd_b); + if( x != 0 ) { + if( level == 0 ) { + status[ptid] = 0; + } + break; + } + + a = next_x - inext_x; + b = next_y - inext_y; + iw00 = (((1.0 - a)*(1.0 - b)*W_BITS14_) + 0.5)|0; + iw01 = ((a*(1.0 - b)*W_BITS14_) + 0.5)|0; + iw10 = (((1.0 - a)*b*W_BITS14_) + 0.5)|0; + iw11 = (W_BITS14_ - iw00 - iw01 - iw10); + b1 = 0.0, b2 = 0.0; + + for( y = 0; y < win_size; ++y ) { + jptr = ( (y + inext_y)*lw + inext_x )|0; + + iptr = (y*win_size)|0; + diptr = iptr << 1; + for( x = 0 ; x < win_size; ++x, ++jptr, ++iptr ) { + ival = ( (img_next[jptr])*iw00 + (img_next[jptr+1])*iw01 + + (img_next[jptr+lw])*iw10 + (img_next[jptr+lw+1])*iw11 ); + ival = (((ival) + W_BITS1m51) >> (W_BITS1m5)); + ival = (ival - iwin_buf[iptr]); + + b1 += ival * deriv_iwin[diptr++]; + b2 += ival * deriv_iwin[diptr++]; + } + } + + b1 *= FLT_SCALE; + b2 *= FLT_SCALE; + + delta_x = ((A12*b2 - A22*b1) * D); + delta_y = ((A12*b1 - A11*b2) * D); + + next_x += delta_x; + next_y += delta_y; + curr_xy[i] = next_x + half_win; + curr_xy[j] = next_y + half_win; + + if( delta_x*delta_x + delta_y*delta_y <= eps ) { + break; + } + + if( iter > 0 && Math.abs(delta_x + prev_delta_x) < 0.01 && + Math.abs(delta_y + prev_delta_y) < 0.01 ) { + curr_xy[i] -= delta_x*0.5; + curr_xy[j] -= delta_y*0.5; + break; + } + + prev_delta_x = delta_x; + prev_delta_y = delta_y; + } + } // points loop + } // levels loop + + jsfeat.cache.put_buffer(iwin_node); + jsfeat.cache.put_buffer(deriv_iwin_node); + jsfeat.cache.put_buffer(deriv_lev_node); + } + }; + })(); + + global.optical_flow_lk = optical_flow_lk; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * this code is a rewrite from https://github.com/mtschirs/js-objectdetect implementation + * @author Martin Tschirsich / http://www.tu-darmstadt.de/~m_t + */ + +(function(global) { + "use strict"; + // + var haar = (function() { + + var _group_func = function(r1, r2) { + var distance = (r1.width * 0.25 + 0.5)|0; + + return r2.x <= r1.x + distance && + r2.x >= r1.x - distance && + r2.y <= r1.y + distance && + r2.y >= r1.y - distance && + r2.width <= (r1.width * 1.5 + 0.5)|0 && + (r2.width * 1.5 + 0.5)|0 >= r1.width; + } + + return { + + edges_density: 0.07, + + detect_single_scale: function(int_sum, int_sqsum, int_tilted, int_canny_sum, width, height, scale, classifier) { + var win_w = (classifier.size[0] * scale)|0, + win_h = (classifier.size[1] * scale)|0, + step_x = (0.5 * scale + 1.5)|0, + step_y = step_x; + var i,j,k,x,y,ex=(width-win_w)|0,ey=(height-win_h)|0; + var w1=(width+1)|0,edge_dens,mean,variance,std; + var inv_area = 1.0 / (win_w * win_h); + var stages,stage,trees,tree,sn,tn,fn,found=true,stage_thresh,stage_sum,tree_sum,feature,features; + var fi_a,fi_b,fi_c,fi_d,fw,fh; + + var ii_a=0,ii_b=win_w,ii_c=win_h*w1,ii_d=ii_c+win_w; + var edges_thresh = ((win_w*win_h) * 0xff * this.edges_density)|0; + // if too much gradient we also can skip + //var edges_thresh_high = ((win_w*win_h) * 0xff * 0.3)|0; + + var rects = []; + for(y = 0; y < ey; y += step_y) { + ii_a = y * w1; + for(x = 0; x < ex; x += step_x, ii_a += step_x) { + + mean = int_sum[ii_a] + - int_sum[ii_a+ii_b] + - int_sum[ii_a+ii_c] + + int_sum[ii_a+ii_d]; + + // canny prune + if(int_canny_sum) { + edge_dens = (int_canny_sum[ii_a] + - int_canny_sum[ii_a+ii_b] + - int_canny_sum[ii_a+ii_c] + + int_canny_sum[ii_a+ii_d]); + if(edge_dens < edges_thresh || mean < 20) { + x += step_x, ii_a += step_x; + continue; + } + } + + mean *= inv_area; + variance = (int_sqsum[ii_a] + - int_sqsum[ii_a+ii_b] + - int_sqsum[ii_a+ii_c] + + int_sqsum[ii_a+ii_d]) * inv_area - mean * mean; + + std = variance > 0. ? Math.sqrt(variance) : 1; + + stages = classifier.complexClassifiers; + sn = stages.length; + found = true; + for(i = 0; i < sn; ++i) { + stage = stages[i]; + stage_thresh = stage.threshold; + trees = stage.simpleClassifiers; + tn = trees.length; + stage_sum = 0; + for(j = 0; j < tn; ++j) { + tree = trees[j]; + tree_sum = 0; + features = tree.features; + fn = features.length; + if(tree.tilted === 1) { + for(k=0; k < fn; ++k) { + feature = features[k]; + fi_a = ~~(x + feature[0] * scale) + ~~(y + feature[1] * scale) * w1; + fw = ~~(feature[2] * scale); + fh = ~~(feature[3] * scale); + fi_b = fw * w1; + fi_c = fh * w1; + + tree_sum += (int_tilted[fi_a] + - int_tilted[fi_a + fw + fi_b] + - int_tilted[fi_a - fh + fi_c] + + int_tilted[fi_a + fw - fh + fi_b + fi_c]) * feature[4]; + } + } else { + for(k=0; k < fn; ++k) { + feature = features[k]; + fi_a = ~~(x + feature[0] * scale) + ~~(y + feature[1] * scale) * w1; + fw = ~~(feature[2] * scale); + fh = ~~(feature[3] * scale); + fi_c = fh * w1; + + tree_sum += (int_sum[fi_a] + - int_sum[fi_a+fw] + - int_sum[fi_a+fi_c] + + int_sum[fi_a+fi_c+fw]) * feature[4]; + } + } + stage_sum += (tree_sum * inv_area < tree.threshold * std) ? tree.left_val : tree.right_val; + } + if (stage_sum < stage_thresh) { + found = false; + break; + } + } + + if(found) { + rects.push({"x" : x, + "y" : y, + "width" : win_w, + "height" : win_h, + "neighbor" : 1, + "confidence" : stage_sum}); + x += step_x, ii_a += step_x; + } + } + } + return rects; + }, + + detect_multi_scale: function(int_sum, int_sqsum, int_tilted, int_canny_sum, width, height, classifier, scale_factor, scale_min) { + if (typeof scale_factor === "undefined") { scale_factor = 1.2; } + if (typeof scale_min === "undefined") { scale_min = 1.0; } + var win_w = classifier.size[0]; + var win_h = classifier.size[1]; + var rects = []; + while (scale_min * win_w < width && scale_min * win_h < height) { + rects = rects.concat(this.detect_single_scale(int_sum, int_sqsum, int_tilted, int_canny_sum, width, height, scale_min, classifier)); + scale_min *= scale_factor; + } + return rects; + }, + + // OpenCV method to group detected rectangles + group_rectangles: function(rects, min_neighbors) { + if (typeof min_neighbors === "undefined") { min_neighbors = 1; } + var i, j, n = rects.length; + var node = []; + for (i = 0; i < n; ++i) { + node[i] = {"parent" : -1, + "element" : rects[i], + "rank" : 0}; + } + for (i = 0; i < n; ++i) { + if (!node[i].element) + continue; + var root = i; + while (node[root].parent != -1) + root = node[root].parent; + for (j = 0; j < n; ++j) { + if( i != j && node[j].element && _group_func(node[i].element, node[j].element)) { + var root2 = j; + + while (node[root2].parent != -1) + root2 = node[root2].parent; + + if(root2 != root) { + if(node[root].rank > node[root2].rank) + node[root2].parent = root; + else { + node[root].parent = root2; + if (node[root].rank == node[root2].rank) + node[root2].rank++; + root = root2; + } + + /* compress path from node2 to the root: */ + var temp, node2 = j; + while (node[node2].parent != -1) { + temp = node2; + node2 = node[node2].parent; + node[temp].parent = root; + } + + /* compress path from node to the root: */ + node2 = i; + while (node[node2].parent != -1) { + temp = node2; + node2 = node[node2].parent; + node[temp].parent = root; + } + } + } + } + } + var idx_seq = []; + var class_idx = 0; + for(i = 0; i < n; i++) { + j = -1; + var node1 = i; + if(node[node1].element) { + while (node[node1].parent != -1) + node1 = node[node1].parent; + if(node[node1].rank >= 0) + node[node1].rank = ~class_idx++; + j = ~node[node1].rank; + } + idx_seq[i] = j; + } + + var comps = []; + for (i = 0; i < class_idx+1; ++i) { + comps[i] = {"neighbors" : 0, + "x" : 0, + "y" : 0, + "width" : 0, + "height" : 0, + "confidence" : 0}; + } + + // count number of neighbors + for(i = 0; i < n; ++i) { + var r1 = rects[i]; + var idx = idx_seq[i]; + + if (comps[idx].neighbors == 0) + comps[idx].confidence = r1.confidence; + + ++comps[idx].neighbors; + + comps[idx].x += r1.x; + comps[idx].y += r1.y; + comps[idx].width += r1.width; + comps[idx].height += r1.height; + comps[idx].confidence = Math.max(comps[idx].confidence, r1.confidence); + } + + var seq2 = []; + // calculate average bounding box + for(i = 0; i < class_idx; ++i) { + n = comps[i].neighbors; + if (n >= min_neighbors) + seq2.push({"x" : (comps[i].x * 2 + n) / (2 * n), + "y" : (comps[i].y * 2 + n) / (2 * n), + "width" : (comps[i].width * 2 + n) / (2 * n), + "height" : (comps[i].height * 2 + n) / (2 * n), + "neighbors" : comps[i].neighbors, + "confidence" : comps[i].confidence}); + } + + var result_seq = []; + n = seq2.length; + // filter out small face rectangles inside large face rectangles + for(i = 0; i < n; ++i) { + var r1 = seq2[i]; + var flag = true; + for(j = 0; j < n; ++j) { + var r2 = seq2[j]; + var distance = (r2.width * 0.25 + 0.5)|0; + + if(i != j && + r1.x >= r2.x - distance && + r1.y >= r2.y - distance && + r1.x + r1.width <= r2.x + r2.width + distance && + r1.y + r1.height <= r2.y + r2.height + distance && + (r2.neighbors > Math.max(3, r1.neighbors) || r1.neighbors < 3)) { + flag = false; + break; + } + } + + if(flag) + result_seq.push(r1); + } + return result_seq; + } + }; + + })(); + + global.haar = haar; + +})(jsfeat); +/** + * BBF: Brightness Binary Feature + * + * @author Eugene Zatepyakin / http://inspirit.ru/ + * + * this code is a rewrite from https://github.com/liuliu/ccv implementation + * @author Liu Liu / http://liuliu.me/ + * + * The original paper refers to: YEF∗ Real-Time Object Detection, Yotam Abramson and Bruno Steux + */ + +(function(global) { + "use strict"; + // + var bbf = (function() { + + var _group_func = function(r1, r2) { + var distance = (r1.width * 0.25 + 0.5)|0; + + return r2.x <= r1.x + distance && + r2.x >= r1.x - distance && + r2.y <= r1.y + distance && + r2.y >= r1.y - distance && + r2.width <= (r1.width * 1.5 + 0.5)|0 && + (r2.width * 1.5 + 0.5)|0 >= r1.width; + } + + var img_pyr = new jsfeat.pyramid_t(1); + + return { + + interval: 4, + scale: 1.1486, + next: 5, + scale_to: 1, + + // make features local copy + // to avoid array allocation with each scale + // this is strange but array works faster than Int32 version??? + prepare_cascade: function(cascade) { + var sn = cascade.stage_classifier.length; + for (var j = 0; j < sn; j++) { + var orig_feature = cascade.stage_classifier[j].feature; + var f_cnt = cascade.stage_classifier[j].count; + var feature = cascade.stage_classifier[j]._feature = new Array(f_cnt); + for (var k = 0; k < f_cnt; k++) { + feature[k] = {"size" : orig_feature[k].size, + "px" : new Array(orig_feature[k].size), + "pz" : new Array(orig_feature[k].size), + "nx" : new Array(orig_feature[k].size), + "nz" : new Array(orig_feature[k].size)}; + } + } + }, + + build_pyramid: function(src, min_width, min_height, interval) { + if (typeof interval === "undefined") { interval = 4; } + + var sw=src.cols,sh=src.rows; + var i=0,nw=0,nh=0; + var new_pyr=false; + var src0=src,src1=src; + var data_type = jsfeat.U8_t | jsfeat.C1_t; + + this.interval = interval; + this.scale = Math.pow(2, 1 / (this.interval + 1)); + this.next = (this.interval + 1)|0; + this.scale_to = (Math.log(Math.min(sw / min_width, sh / min_height)) / Math.log(this.scale))|0; + + var pyr_l = ((this.scale_to + this.next * 2) * 4) | 0; + if(img_pyr.levels != pyr_l) { + img_pyr.levels = pyr_l; + img_pyr.data = new Array(pyr_l); + new_pyr = true; + img_pyr.data[0] = src; // first is src + } + + for (i = 1; i <= this.interval; ++i) { + nw = (sw / Math.pow(this.scale, i))|0; + nh = (sh / Math.pow(this.scale, i))|0; + src0 = img_pyr.data[i<<2]; + if(new_pyr || nw != src0.cols || nh != src0.rows) { + img_pyr.data[i<<2] = new jsfeat.matrix_t(nw, nh, data_type); + src0 = img_pyr.data[i<<2]; + } + jsfeat.imgproc.resample(src, src0, nw, nh); + } + for (i = this.next; i < this.scale_to + this.next * 2; ++i) { + src1 = img_pyr.data[(i << 2) - (this.next << 2)]; + src0 = img_pyr.data[i<<2]; + nw = src1.cols >> 1; + nh = src1.rows >> 1; + if(new_pyr || nw != src0.cols || nh != src0.rows) { + img_pyr.data[i<<2] = new jsfeat.matrix_t(nw, nh, data_type); + src0 = img_pyr.data[i<<2]; + } + jsfeat.imgproc.pyrdown(src1, src0); + } + for (i = this.next * 2; i < this.scale_to + this.next * 2; ++i) { + src1 = img_pyr.data[(i << 2) - (this.next << 2)]; + nw = src1.cols >> 1; + nh = src1.rows >> 1; + src0 = img_pyr.data[(i<<2)+1]; + if(new_pyr || nw != src0.cols || nh != src0.rows) { + img_pyr.data[(i<<2)+1] = new jsfeat.matrix_t(nw, nh, data_type); + src0 = img_pyr.data[(i<<2)+1]; + } + jsfeat.imgproc.pyrdown(src1, src0, 1, 0); + // + src0 = img_pyr.data[(i<<2)+2]; + if(new_pyr || nw != src0.cols || nh != src0.rows) { + img_pyr.data[(i<<2)+2] = new jsfeat.matrix_t(nw, nh, data_type); + src0 = img_pyr.data[(i<<2)+2]; + } + jsfeat.imgproc.pyrdown(src1, src0, 0, 1); + // + src0 = img_pyr.data[(i<<2)+3]; + if(new_pyr || nw != src0.cols || nh != src0.rows) { + img_pyr.data[(i<<2)+3] = new jsfeat.matrix_t(nw, nh, data_type); + src0 = img_pyr.data[(i<<2)+3]; + } + jsfeat.imgproc.pyrdown(src1, src0, 1, 1); + } + return img_pyr; + }, + + detect: function(pyramid, cascade) { + var interval = this.interval; + var scale = this.scale; + var next = this.next; + var scale_upto = this.scale_to; + var i=0,j=0,k=0,n=0,x=0,y=0,q=0,sn=0,f_cnt=0,q_cnt=0,p=0,pmin=0,nmax=0,f=0,i4=0,qw=0,qh=0; + var sum=0.0, alpha, feature, orig_feature, feature_k, feature_o, flag = true, shortcut=true; + var scale_x = 1.0, scale_y = 1.0; + var dx = [0, 1, 0, 1]; + var dy = [0, 0, 1, 1]; + var seq = []; + var pyr=pyramid.data, bpp = 1, bpp2 = 2, bpp4 = 4; + + var u8 = [], u8o = [0,0,0]; + var step = [0,0,0]; + var paddings = [0,0,0]; + + for (i = 0; i < scale_upto; i++) { + i4 = (i<<2); + qw = pyr[i4 + (next << 3)].cols - (cascade.width >> 2); + qh = pyr[i4 + (next << 3)].rows - (cascade.height >> 2); + step[0] = pyr[i4].cols * bpp; + step[1] = pyr[i4 + (next << 2)].cols * bpp; + step[2] = pyr[i4 + (next << 3)].cols * bpp; + paddings[0] = (pyr[i4].cols * bpp4) - (qw * bpp4); + paddings[1] = (pyr[i4 + (next << 2)].cols * bpp2) - (qw * bpp2); + paddings[2] = (pyr[i4 + (next << 3)].cols * bpp) - (qw * bpp); + sn = cascade.stage_classifier.length; + for (j = 0; j < sn; j++) { + orig_feature = cascade.stage_classifier[j].feature; + feature = cascade.stage_classifier[j]._feature; + f_cnt = cascade.stage_classifier[j].count; + for (k = 0; k < f_cnt; k++) { + feature_k = feature[k]; + feature_o = orig_feature[k]; + q_cnt = feature_o.size|0; + for (q = 0; q < q_cnt; q++) { + feature_k.px[q] = (feature_o.px[q] * bpp) + feature_o.py[q] * step[feature_o.pz[q]]; + feature_k.pz[q] = feature_o.pz[q]; + feature_k.nx[q] = (feature_o.nx[q] * bpp) + feature_o.ny[q] * step[feature_o.nz[q]]; + feature_k.nz[q] = feature_o.nz[q]; + } + } + } + u8[0] = pyr[i4].data; u8[1] = pyr[i4 + (next<<2)].data; + for (q = 0; q < 4; q++) { + u8[2] = pyr[i4 + (next<<3) + q].data; + u8o[0] = (dx[q]*bpp2) + dy[q] * (pyr[i4].cols*bpp2); + u8o[1] = (dx[q]*bpp) + dy[q] * (pyr[i4 + (next<<2)].cols*bpp); + u8o[2] = 0; + for (y = 0; y < qh; y++) { + for (x = 0; x < qw; x++) { + sum = 0; + flag = true; + sn = cascade.stage_classifier.length; + for (j = 0; j < sn; j++) { + sum = 0; + alpha = cascade.stage_classifier[j].alpha; + feature = cascade.stage_classifier[j]._feature; + f_cnt = cascade.stage_classifier[j].count; + for (k = 0; k < f_cnt; k++) { + feature_k = feature[k]; + pmin = u8[feature_k.pz[0]][u8o[feature_k.pz[0]] + feature_k.px[0]]; + nmax = u8[feature_k.nz[0]][u8o[feature_k.nz[0]] + feature_k.nx[0]]; + if (pmin <= nmax) { + sum += alpha[k << 1]; + } else { + shortcut = true; + q_cnt = feature_k.size; + for (f = 1; f < q_cnt; f++) { + if (feature_k.pz[f] >= 0) { + p = u8[feature_k.pz[f]][u8o[feature_k.pz[f]] + feature_k.px[f]]; + if (p < pmin) { + if (p <= nmax) { + shortcut = false; + break; + } + pmin = p; + } + } + if (feature_k.nz[f] >= 0) { + n = u8[feature_k.nz[f]][u8o[feature_k.nz[f]] + feature_k.nx[f]]; + if (n > nmax) { + if (pmin <= n) { + shortcut = false; + break; + } + nmax = n; + } + } + } + sum += (shortcut) ? alpha[(k << 1) + 1] : alpha[k << 1]; + } + } + if (sum < cascade.stage_classifier[j].threshold) { + flag = false; + break; + } + } + if (flag) { + seq.push({"x" : (x * 4 + dx[q] * 2) * scale_x, + "y" : (y * 4 + dy[q] * 2) * scale_y, + "width" : cascade.width * scale_x, + "height" : cascade.height * scale_y, + "neighbor" : 1, + "confidence" : sum}); + ++x; + u8o[0] += bpp4; + u8o[1] += bpp2; + u8o[2] += bpp; + } + u8o[0] += bpp4; + u8o[1] += bpp2; + u8o[2] += bpp; + } + u8o[0] += paddings[0]; + u8o[1] += paddings[1]; + u8o[2] += paddings[2]; + } + } + scale_x *= scale; + scale_y *= scale; + } + + return seq; + }, + + // OpenCV method to group detected rectangles + group_rectangles: function(rects, min_neighbors) { + if (typeof min_neighbors === "undefined") { min_neighbors = 1; } + var i, j, n = rects.length; + var node = []; + for (i = 0; i < n; ++i) { + node[i] = {"parent" : -1, + "element" : rects[i], + "rank" : 0}; + } + for (i = 0; i < n; ++i) { + if (!node[i].element) + continue; + var root = i; + while (node[root].parent != -1) + root = node[root].parent; + for (j = 0; j < n; ++j) { + if( i != j && node[j].element && _group_func(node[i].element, node[j].element)) { + var root2 = j; + + while (node[root2].parent != -1) + root2 = node[root2].parent; + + if(root2 != root) { + if(node[root].rank > node[root2].rank) + node[root2].parent = root; + else { + node[root].parent = root2; + if (node[root].rank == node[root2].rank) + node[root2].rank++; + root = root2; + } + + /* compress path from node2 to the root: */ + var temp, node2 = j; + while (node[node2].parent != -1) { + temp = node2; + node2 = node[node2].parent; + node[temp].parent = root; + } + + /* compress path from node to the root: */ + node2 = i; + while (node[node2].parent != -1) { + temp = node2; + node2 = node[node2].parent; + node[temp].parent = root; + } + } + } + } + } + var idx_seq = []; + var class_idx = 0; + for(i = 0; i < n; i++) { + j = -1; + var node1 = i; + if(node[node1].element) { + while (node[node1].parent != -1) + node1 = node[node1].parent; + if(node[node1].rank >= 0) + node[node1].rank = ~class_idx++; + j = ~node[node1].rank; + } + idx_seq[i] = j; + } + + var comps = []; + for (i = 0; i < class_idx+1; ++i) { + comps[i] = {"neighbors" : 0, + "x" : 0, + "y" : 0, + "width" : 0, + "height" : 0, + "confidence" : 0}; + } + + // count number of neighbors + for(i = 0; i < n; ++i) { + var r1 = rects[i]; + var idx = idx_seq[i]; + + if (comps[idx].neighbors == 0) + comps[idx].confidence = r1.confidence; + + ++comps[idx].neighbors; + + comps[idx].x += r1.x; + comps[idx].y += r1.y; + comps[idx].width += r1.width; + comps[idx].height += r1.height; + comps[idx].confidence = Math.max(comps[idx].confidence, r1.confidence); + } + + var seq2 = []; + // calculate average bounding box + for(i = 0; i < class_idx; ++i) { + n = comps[i].neighbors; + if (n >= min_neighbors) + seq2.push({"x" : (comps[i].x * 2 + n) / (2 * n), + "y" : (comps[i].y * 2 + n) / (2 * n), + "width" : (comps[i].width * 2 + n) / (2 * n), + "height" : (comps[i].height * 2 + n) / (2 * n), + "neighbors" : comps[i].neighbors, + "confidence" : comps[i].confidence}); + } + + var result_seq = []; + n = seq2.length; + // filter out small face rectangles inside large face rectangles + for(i = 0; i < n; ++i) { + var r1 = seq2[i]; + var flag = true; + for(j = 0; j < n; ++j) { + var r2 = seq2[j]; + var distance = (r2.width * 0.25 + 0.5)|0; + + if(i != j && + r1.x >= r2.x - distance && + r1.y >= r2.y - distance && + r1.x + r1.width <= r2.x + r2.width + distance && + r1.y + r1.height <= r2.y + r2.height + distance && + (r2.neighbors > Math.max(3, r1.neighbors) || r1.neighbors < 3)) { + flag = false; + break; + } + } + + if(flag) + result_seq.push(r1); + } + return result_seq; + } + + }; + + })(); + + global.bbf = bbf; + +})(jsfeat); +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +(function(lib) { + "use strict"; + + if (typeof module === "undefined" || typeof module.exports === "undefined") { + // in a browser, define its namespaces in global + window.jsfeat = lib; + } else { + // in commonjs, or when AMD wrapping has been applied, define its namespaces as exports + module.exports = lib; + } +})(jsfeat); diff --git a/tests/vendor/jsfeat_transform.js b/tests/vendor/jsfeat_transform.js new file mode 100644 index 0000000..9684426 --- /dev/null +++ b/tests/vendor/jsfeat_transform.js @@ -0,0 +1,172 @@ +/** + * @author Eugene Zatepyakin / http://inspirit.ru/ + */ + +(function(global) { + "use strict"; + // + + var transform = (function() { + // + return { + affine_3point_transform: function(mat, src_x0, src_y0, dst_x0, dst_y0, + src_x1, src_y1, dst_x1, dst_y1, + src_x2, src_y2, dst_x2, dst_y2) { + // we need linear algebra module first + }, + + perspective_4point_transform: function(mat, src_x0, src_y0, dst_x0, dst_y0, + src_x1, src_y1, dst_x1, dst_y1, + src_x2, src_y2, dst_x2, dst_y2, + src_x3, src_y3, dst_x3, dst_y3) { + var t1 = src_x0; + var t2 = src_x2; + var t4 = src_y1; + var t5 = t1 * t2 * t4; + var t6 = src_y3; + var t7 = t1 * t6; + var t8 = t2 * t7; + var t9 = src_y2; + var t10 = t1 * t9; + var t11 = src_x1; + var t14 = src_y0; + var t15 = src_x3; + var t16 = t14 * t15; + var t18 = t16 * t11; + var t20 = t15 * t11 * t9; + var t21 = t15 * t4; + var t24 = t15 * t9; + var t25 = t2 * t4; + var t26 = t6 * t2; + var t27 = t6 * t11; + var t28 = t9 * t11; + var t30 = 1.0 / (t21-t24 - t25 + t26 - t27 + t28); + var t32 = t1 * t15; + var t35 = t14 * t11; + var t41 = t4 * t1; + var t42 = t6 * t41; + var t43 = t14 * t2; + var t46 = t16 * t9; + var t48 = t14 * t9 * t11; + var t51 = t4 * t6 * t2; + var t55 = t6 * t14; + var Hr0 = -(t8-t5 + t10 * t11 - t11 * t7 - t16 * t2 + t18 - t20 + t21 * t2) * t30; + var Hr1 = (t5 - t8 - t32 * t4 + t32 * t9 + t18 - t2 * t35 + t27 * t2 - t20) * t30; + var Hr2 = t1; + var Hr3 = (-t9 * t7 + t42 + t43 * t4 - t16 * t4 + t46 - t48 + t27 * t9 - t51) * t30; + var Hr4 = (-t42 + t41 * t9 - t55 * t2 + t46 - t48 + t55 * t11 + t51 - t21 * t9) * t30; + var Hr5 = t14; + var Hr6 = (-t10 + t41 + t43 - t35 + t24 - t21 - t26 + t27) * t30; + var Hr7 = (-t7 + t10 + t16 - t43 + t27 - t28 - t21 + t25) * t30; + + t1 = dst_x0; + t2 = dst_x2; + t4 = dst_y1; + t5 = t1 * t2 * t4; + t6 = dst_y3; + t7 = t1 * t6; + t8 = t2 * t7; + t9 = dst_y2; + t10 = t1 * t9; + t11 = dst_x1; + t14 = dst_y0; + t15 = dst_x3; + t16 = t14 * t15; + t18 = t16 * t11; + t20 = t15 * t11 * t9; + t21 = t15 * t4; + t24 = t15 * t9; + t25 = t2 * t4; + t26 = t6 * t2; + t27 = t6 * t11; + t28 = t9 * t11; + t30 = 1.0 / (t21-t24 - t25 + t26 - t27 + t28); + t32 = t1 * t15; + t35 = t14 * t11; + t41 = t4 * t1; + t42 = t6 * t41; + t43 = t14 * t2; + t46 = t16 * t9; + t48 = t14 * t9 * t11; + t51 = t4 * t6 * t2; + t55 = t6 * t14; + var Hl0 = -(t8-t5 + t10 * t11 - t11 * t7 - t16 * t2 + t18 - t20 + t21 * t2) * t30; + var Hl1 = (t5 - t8 - t32 * t4 + t32 * t9 + t18 - t2 * t35 + t27 * t2 - t20) * t30; + var Hl2 = t1; + var Hl3 = (-t9 * t7 + t42 + t43 * t4 - t16 * t4 + t46 - t48 + t27 * t9 - t51) * t30; + var Hl4 = (-t42 + t41 * t9 - t55 * t2 + t46 - t48 + t55 * t11 + t51 - t21 * t9) * t30; + var Hl5 = t14; + var Hl6 = (-t10 + t41 + t43 - t35 + t24 - t21 - t26 + t27) * t30; + var Hl7 = (-t7 + t10 + t16 - t43 + t27 - t28 - t21 + t25) * t30; + + // the following code computes R = Hl * inverse Hr + t2 = Hr4-Hr7*Hr5; + t4 = Hr0*Hr4; + t5 = Hr0*Hr5; + t7 = Hr3*Hr1; + t8 = Hr2*Hr3; + t10 = Hr1*Hr6; + var t12 = Hr2*Hr6; + t15 = 1.0 / (t4-t5*Hr7-t7+t8*Hr7+t10*Hr5-t12*Hr4); + t18 = -Hr3+Hr5*Hr6; + var t23 = -Hr3*Hr7+Hr4*Hr6; + t28 = -Hr1+Hr2*Hr7; + var t31 = Hr0-t12; + t35 = Hr0*Hr7-t10; + t41 = -Hr1*Hr5+Hr2*Hr4; + var t44 = t5-t8; + var t47 = t4-t7; + t48 = t2*t15; + var t49 = t28*t15; + var t50 = t41*t15; + mat[0] = Hl0*t48+Hl1*(t18*t15)-Hl2*(t23*t15); + mat[1] = Hl0*t49+Hl1*(t31*t15)-Hl2*(t35*t15); + mat[2] = -Hl0*t50-Hl1*(t44*t15)+Hl2*(t47*t15); + mat[3] = Hl3*t48+Hl4*(t18*t15)-Hl5*(t23*t15); + mat[4] = Hl3*t49+Hl4*(t31*t15)-Hl5*(t35*t15); + mat[5] = -Hl3*t50-Hl4*(t44*t15)+Hl5*(t47*t15); + mat[6] = Hl6*t48+Hl7*(t18*t15)-t23*t15; + mat[7] = Hl6*t49+Hl7*(t31*t15)-t35*t15; + mat[8] = -Hl6*t50-Hl7*(t44*t15)+t47*t15; + }, + + invert_affine_transform: function(src, dst) { + var m11 = src[0], m12 = src[1], m13 = src[2]; + var m21 = src[3], m22 = src[4], m23 = src[5]; + + var det = 1.0 / (m11 * m22 - m12 * m21); + + dst[0] = det * m22; + dst[1] = det * -m12; + dst[2] = det * (m12*m23 - m13*m22); + + dst[3] = det * -m21; + dst[4] = det * m11; + dst[5] = det * (m13*m21 - m11*m23); + }, + + invert_perspective_transform: function(src, dst) { + var m11 = src[0], m12 = src[1], m13 = src[2]; + var m21 = src[3], m22 = src[4], m23 = src[5]; + var m31 = src[6], m32 = src[7], m33 = src[8]; + + var det = 1.0 / (m11 * (m22*m33 - m23*m32) - m12 * (m21*m33 - m23*m31) + m13 * (m21*m32 - m22*m31)); + + dst[0] = det * (m22*m33 - m23*m32); + dst[1] = det * (m13*m32 - m12*m33); + dst[2] = det * (m12*m23 - m13*m22); + + dst[3] = det * (m23*m31 - m21*m33); + dst[4] = det * (m11*m33 - m13*m31); + dst[5] = det * (m13*m21 - m11*m23); + + dst[6] = det * (m21*m32 - m22*m31); + dst[7] = det * (m12*m31 - m11*m32); + dst[8] = det * (m11*m22 - m12*m21); + } + }; + })(); + + global.transform = transform; + +})(jsfeat); \ No newline at end of file diff --git a/tests/vendor/oracle.cjs b/tests/vendor/oracle.cjs new file mode 100644 index 0000000..0d1552d --- /dev/null +++ b/tests/vendor/oracle.cjs @@ -0,0 +1,26 @@ +/** + * Golden oracle for the parity/characterization tests: the ORIGINAL jsfeat. + * + * Vendored from https://github.com/inspirit/jsfeat + * commit 4c7b336bbeeb26e6cd4cdf3c7d414abe273846f3 (master): + * - jsfeat-master.js = build/jsfeat.js (the distributed bundle) + * - jsfeat_transform.js = src/jsfeat_transform.js (never included in the + * distributed build/npm package — a parity-audit finding in itself, see + * docs/jsfeat-parity-and-refactor-audit.md and issue #45) + * + * NOTE the original calling conventions differ between the two: + * - jsfeat.math.perspective_4point_transform(model, ...) takes a matrix_t + * - jsfeat.transform.* takes RAW ARRAYS + * jsfeatNext uses matrix_t everywhere (signature divergence, documented). + */ +const fs = require("fs"); +const path = require("path"); + +const jsfeat = require("./jsfeat-master.js"); + +// jsfeat_transform.js is an IIFE `(function(global){ ... })(jsfeat)` that +// expects a `jsfeat` identifier in scope; evaluate it with ours. +const transformSrc = fs.readFileSync(path.join(__dirname, "jsfeat_transform.js"), "utf8"); +new Function("jsfeat", transformSrc)(jsfeat); + +module.exports = jsfeat; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..5f7d586 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // pure CV math runs headless; no DOM needed + environment: "node", + include: ["tests/**/*.test.ts"], + }, +});