From 96f227f695e4208f7a5b6a30f4f611052a26978e Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sun, 5 Jul 2026 23:41:49 +0200 Subject: [PATCH 01/14] test: add Vitest characterization harness with first jsfeat parity test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add vitest + original jsfeat (golden oracle) as devDependencies - Add vitest.config.ts (node env) and test / test:watch scripts - First parity test: math.get_gaussian_kernel matches original jsfeat within float tolerance across 4 cases (imports the real impl from src/, so it stays green through the planned de-dup refactor) Part of Phase 0 (#39) — the test gate that makes the monolith de-duplication provably behavior-preserving. Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 1274 ++++++++++++++++- package.json | 8 +- tests/jsfeat.d.ts | 3 + tests/parity/math.get_gaussian_kernel.test.ts | 35 + vitest.config.ts | 9 + 5 files changed, 1318 insertions(+), 11 deletions(-) create mode 100644 tests/jsfeat.d.ts create mode 100644 tests/parity/math.get_gaussian_kernel.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 83dd3de..c11edc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,10 @@ "@babel/plugin-transform-runtime": "^7.26.9", "@babel/preset-env": "^7.26.9", "babel-loader": "^9.2.1", + "jsfeat": "^0.0.8", "rimraf": "^6.0.1", "typescript": "^5.7.3", + "vitest": "^4.1.9", "webpack": "5.98.0", "webpack-cli": "6.0.1" } @@ -1586,6 +1588,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 +1693,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 +1708,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 +2076,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 +2464,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 +2618,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 +2737,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 +2845,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 +2873,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 +3056,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", @@ -2744,6 +3288,12 @@ "node": ">=6" } }, + "node_modules/jsfeat": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/jsfeat/-/jsfeat-0.0.8.tgz", + "integrity": "sha512-wG2Rzv9ywe2KGnX+N8VJtFO0srwyZaRmV8cBjsE52PCNdNJk8nVGtiK9HQ5wZZ5fUxk3wVPqV3zr8KxQjulR3w==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -2758,16 +3308,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 +3608,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 +3687,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 +3716,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 +3824,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 +3860,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 +4071,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 +4224,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 +4253,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 +4280,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 +4482,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 +4640,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 +4747,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 +5094,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..963b32a 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", @@ -34,8 +36,10 @@ "@babel/plugin-transform-runtime": "^7.26.9", "@babel/preset-env": "^7.26.9", "babel-loader": "^9.2.1", + "jsfeat": "^0.0.8", "rimraf": "^6.0.1", "typescript": "^5.7.3", + "vitest": "^4.1.9", "webpack": "5.98.0", "webpack-cli": "6.0.1" }, @@ -44,4 +48,4 @@ "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..bbd1a9f --- /dev/null +++ b/tests/jsfeat.d.ts @@ -0,0 +1,3 @@ +// The original `jsfeat` package is used only as a golden-output *oracle* in the +// characterization tests. It ships no type declarations, so we declare it loosely. +declare module "jsfeat"; 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..25ff12c --- /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 "jsfeat"; + +/** + * 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/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"], + }, +}); From 280883897333b54d1927ff8c5dc0b0f1b0236f6d Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 13:35:56 +0200 Subject: [PATCH 02/14] ci: install without lockfile so Linux native binaries resolve vitest 4 pulls in rolldown, which ships per-platform native binaries. The committed package-lock.json is generated on Windows, so on the Linux CI runner npm skipped the linux-x64 binding and the Test step crashed with 'Cannot find module @rolldown/binding-linux-x64-gnu'. Use 'npm install --no-package-lock' in CI so dependencies resolve for the runner's platform. Stopgap until the Vite migration (#42) reworks tooling. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/CI.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9a51bd9..044892d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,8 +30,11 @@ jobs: ${{ runner.os }}-node- - name: Install + # Resolve from package.json rather than the committed lockfile: it is generated + # on Windows, so npm otherwise skips the Linux-only native binaries (rolldown, + # pulled in by vitest 4) on the CI runner. Revisit during the Vite migration (#42). run: | - npm install + npm install --no-package-lock - name: Format check run: npm run format-check From 1b44bda364f1b4fce88ab4d224ab42b730ce6124 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 13:46:51 +0200 Subject: [PATCH 03/14] build(deps): pin prettier to 3.5.1 for stable lock-free CI formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI installs with --no-package-lock so vitest 4's rolldown native binary resolves on Linux, but that also unpins prettier — a newer prettier reformatted 4 existing src files and failed format-check. Pin prettier to exactly 3.5.1 so the lock-free install keeps the formatter that matches the committed code. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 963b32a..9a4376e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@babel/runtime": "^7.26.9", - "prettier": "^3.5.1", + "prettier": "3.5.1", "ts-loader": "^9.5.2" } } From f8bc0faa4127d3e980f8fb7c4400496f48d74172 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 14:03:22 +0200 Subject: [PATCH 04/14] ci: install via npm ci with a cross-platform-corrected lockfile Replace the fragile 'npm install --no-package-lock' (which unpinned every dependency and cascaded into prettier, then std-env ESM, breakage) with 'npm ci'. Regenerate package-lock.json in a node:20 Linux container so it correctly resolves the platform-specific native binaries that vitest 4 pulls in via rolldown. Verified end-to-end: 'npm ci && vitest run' in node:20 -> 4 tests pass. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/CI.yml | 6 +----- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 044892d..d3f6134 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,11 +30,7 @@ jobs: ${{ runner.os }}-node- - name: Install - # Resolve from package.json rather than the committed lockfile: it is generated - # on Windows, so npm otherwise skips the Linux-only native binaries (rolldown, - # pulled in by vitest 4) on the CI runner. Revisit during the Vite migration (#42). - run: | - npm install --no-package-lock + run: npm ci - name: Format check run: npm run format-check diff --git a/package-lock.json b/package-lock.json index c11edc1..d618613 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": { From 51a702bec02d70e5cdf2d1c0797f2e5ef0a83f50 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 16:08:35 +0200 Subject: [PATCH 05/14] ci: upgrade Node to 24 (npm 11) to fix optional native-binary install npm 10.8.2 (bundled with Node 20) intermittently fails to install vitest 4's platform-specific rolldown native binary on the Linux CI runner (npm bug #4828), crashing 'npm ci' in the Test step. Node 24 ships npm 11, which resolves optional dependencies correctly. Verified in node:24 containers: 'npm ci' + vitest (4 passed) and 'npm run build-ts' both succeed. Co-Authored-By: Claude Opus 4.8 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From af789377d2f9be77e450bc40815086afd1979605 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 16:16:28 +0200 Subject: [PATCH 06/14] docs(agents): update Node to v24 and note the Vitest test suite This branch bumps .nvmrc to Node 24 (npm 11) and adds Vitest characterization tests, so update the canonical AGENTS.md and the inlined Copilot instructions accordingly (Node version + 'npm test'). Co-Authored-By: Claude Opus 4.8 --- .github/copilot-instructions.md | 4 ++-- AGENTS.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/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 From ea8a797598e7a1265d2be878a0e1919182d28122 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 22:46:06 +0200 Subject: [PATCH 07/14] test: add matmath and transform parity suites; vendor jsfeat master as oracle Expands the Phase 0 characterization harness (#39) and starts filling the function-level parity audit (#45): - tests/parity/matmath.test.ts: all 12 matmath functions vs the oracle - tests/parity/transform.test.ts: all 3 transform functions vs the oracle - Replace the npm jsfeat dev-dependency with a vendored oracle (tests/vendor/, from inspirit/jsfeat@4c7b336): npm jsfeat@0.0.8 and even the distributed build/jsfeat.js NEVER included the transform module, so oracle.cjs evaluates src/jsfeat_transform.js against the bundle. Parity findings captured (for the audit doc / #45): - jsfeat's transform module is absent from every distributed jsfeat build; only math.perspective_4point_transform made it into the bundle. - Signature divergence: jsfeat.transform.* take raw arrays, jsfeatNext's transform methods take matrix_t. npm test: 20 passed. Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 7 - package.json | 1 - tests/jsfeat.d.ts | 7 +- tests/parity/math.get_gaussian_kernel.test.ts | 2 +- tests/parity/matmath.test.ts | 157 + tests/parity/transform.test.ts | 100 + tests/vendor/jsfeat-master.js | 5639 +++++++++++++++++ tests/vendor/jsfeat_transform.js | 172 + tests/vendor/oracle.cjs | 26 + 9 files changed, 6099 insertions(+), 12 deletions(-) create mode 100644 tests/parity/matmath.test.ts create mode 100644 tests/parity/transform.test.ts create mode 100644 tests/vendor/jsfeat-master.js create mode 100644 tests/vendor/jsfeat_transform.js create mode 100644 tests/vendor/oracle.cjs diff --git a/package-lock.json b/package-lock.json index d618613..f74f643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@babel/plugin-transform-runtime": "^7.26.9", "@babel/preset-env": "^7.26.9", "babel-loader": "^9.2.1", - "jsfeat": "^0.0.8", "rimraf": "^6.0.1", "typescript": "^5.7.3", "vitest": "^4.1.9", @@ -3288,12 +3287,6 @@ "node": ">=6" } }, - "node_modules/jsfeat": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/jsfeat/-/jsfeat-0.0.8.tgz", - "integrity": "sha512-wG2Rzv9ywe2KGnX+N8VJtFO0srwyZaRmV8cBjsE52PCNdNJk8nVGtiK9HQ5wZZ5fUxk3wVPqV3zr8KxQjulR3w==", - "dev": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/package.json b/package.json index 9a4376e..78c8965 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@babel/plugin-transform-runtime": "^7.26.9", "@babel/preset-env": "^7.26.9", "babel-loader": "^9.2.1", - "jsfeat": "^0.0.8", "rimraf": "^6.0.1", "typescript": "^5.7.3", "vitest": "^4.1.9", diff --git a/tests/jsfeat.d.ts b/tests/jsfeat.d.ts index bbd1a9f..f5993a2 100644 --- a/tests/jsfeat.d.ts +++ b/tests/jsfeat.d.ts @@ -1,3 +1,4 @@ -// The original `jsfeat` package is used only as a golden-output *oracle* in the -// characterization tests. It ships no type declarations, so we declare it loosely. -declare module "jsfeat"; +// 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/math.get_gaussian_kernel.test.ts b/tests/parity/math.get_gaussian_kernel.test.ts index 25ff12c..edbad46 100644 --- a/tests/parity/math.get_gaussian_kernel.test.ts +++ b/tests/parity/math.get_gaussian_kernel.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import jsfeatNext from "../../src/jsfeatNext"; -import jsfeat from "jsfeat"; +import jsfeat from "../vendor/oracle.cjs"; /** * Characterization test (Phase 0, issue #39). 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/transform.test.ts b/tests/parity/transform.test.ts new file mode 100644 index 0000000..be5cd7e --- /dev/null +++ b/tests/parity/transform.test.ts @@ -0,0 +1,100 @@ +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; From d97d083cda9bbc5ebcf0ed47c87183289b8d70d5 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 22:50:20 +0200 Subject: [PATCH 08/14] style: format transform parity test with prettier Co-Authored-By: Claude Opus 4.8 --- tests/parity/transform.test.ts | 60 +++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/tests/parity/transform.test.ts b/tests/parity/transform.test.ts index be5cd7e..ecfe345 100644 --- a/tests/parity/transform.test.ts +++ b/tests/parity/transform.test.ts @@ -38,10 +38,22 @@ describe("parity: transform vs original jsfeat", () => { 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] + 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] ); } @@ -51,10 +63,22 @@ describe("parity: transform vs original jsfeat", () => { 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] + 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); }); @@ -65,10 +89,22 @@ describe("parity: transform vs original jsfeat", () => { 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] + 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); }); From 6ad7a9269e12a9049a00a23b611569e7e8b47034 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 6 Jul 2026 23:48:40 +0200 Subject: [PATCH 09/14] chore: add .gitattributes to force LF line endings Windows checkouts were getting CRLF working trees, making local prettier --check disagree with the Linux CI format-check (prettier 3 defaults endOfLine: lf). Normalize everything to LF; renormalizes two example JS files that were committed with CRLF. Co-Authored-By: Claude Opus 4.8 --- .gitattributes | 5 + examples/js/compatibility.js | 140 +++++++++--------- examples/js/profiler.js | 278 +++++++++++++++++------------------ 3 files changed, 214 insertions(+), 209 deletions(-) create mode 100644 .gitattributes 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/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 From 6ae7d896d7272d1ef73ea4da5cff7a1bc985c97b Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 7 Jul 2026 00:03:28 +0200 Subject: [PATCH 10/14] test: add math and linalg parity suites Continues #39 / #45: - math: qsort (full + sub-range), median, and the deprecated matrix_t-based perspective_4point_transform (parity with the jsfeat bundle's math version; jsfeatNext adds a deprecation console.warn - documented divergence) - linalg: lu_solve, cholesky_solve, svd_decompose (U_T|V_T), svd_solve, svd_invert, eigenVV - all match the oracle on identical inputs npm test: 30 passed. Co-Authored-By: Claude Opus 4.8 --- tests/parity/linalg.test.ts | 145 ++++++++++++++++++++++++++++++++++++ tests/parity/math.test.ts | 112 ++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 tests/parity/linalg.test.ts create mode 100644 tests/parity/math.test.ts 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.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(); + }); +}); From c420bd8d7a4803f6ed354838e24b867e763add9d Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 7 Jul 2026 00:08:51 +0200 Subject: [PATCH 11/14] test: add imgproc parity suite; document original jsfeat hough bug Continues #39 / #45. 14 imgproc functions vs the oracle on identical synthetic images: grayscale (RGBA + BGRA codes), resample (u8 fast path), box_blur_gray (scaled + NOSCALE), gaussian_blur, pyrdown, scharr/sobel derivatives, compute_integral_image (sum+sqsum+tilted), equalize_histogram, canny, warp_perspective, warp_affine - all bit-for-bit matches. Parity finding: the ORIGINAL jsfeat hough_transform references min_theta / max_theta without declaring them and throws ReferenceError under the bundle's strict-mode IIFE - the distributed function is unusable. jsfeatNext declares them (a fix, documented divergence); the test pins both behaviors. npm test: 44 passed. Co-Authored-By: Claude Opus 4.8 --- tests/parity/imgproc.test.ts | 236 +++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 tests/parity/imgproc.test.ts 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); + } + }); +}); From 15ce9ad5303b49f4b5b5a21f5cfd9ecf7e3bf8a3 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 7 Jul 2026 11:37:54 +0200 Subject: [PATCH 12/14] test: add detector/descriptor/tracker parity suites Continues #39 / #45 on a deterministic corner-rich synthetic image: - fast_corners: identical corners (x, y, score) at threshold 20 - yape06: identical keypoints at default thresholds - yape: identical keypoints (API divergence documented: original is a static namespace, jsfeatNext requires instantiation) - orb.describe: identical 32-byte descriptors for identical corners+angles - optical_flow_lk.track: identical status + tracked positions across a (3,2) shift, tracking FAST corners through 2-level pyramids npm test: 49 passed. Co-Authored-By: Claude Opus 4.8 --- tests/parity/detectors.test.ts | 227 +++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 tests/parity/detectors.test.ts 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); + }); +}); From 46a8fb83adf391eb41b4bfd7187970336ccedb14 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 7 Jul 2026 11:41:42 +0200 Subject: [PATCH 13/14] test: add motion_estimator parity suite; pin affine2d ransac bug (#51) Continues #39 / #45. RANSAC and LMEDS made deterministic by stubbing Math.random with an identical seeded sequence for both sides: - ransac + homography2d: same success flag, same 3x3 model (5 decimals), same inlier mask; synthetic outliers rejected on both sides - lmeds + homography2d: same model + mask - ransac + affine2d: BUG FOUND - jsfeatNext throws TypeError because the motion_model base class lost the default check_subset() that original jsfeat provides; the oracle succeeds on identical data. Pinned as a documented divergence; fix tracked in #51. npm test: 52 passed. Co-Authored-By: Claude Opus 4.8 --- tests/parity/motion_estimator.test.ts | 183 ++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tests/parity/motion_estimator.test.ts 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); + } + }); +}); From d7ba8bc1d8687b000e4c8627df15e44318d96f9f Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 7 Jul 2026 11:56:57 +0200 Subject: [PATCH 14/14] test: add structs, data-type helper and resample-f32 parity suites Completes the planned Phase 0 coverage (#39 / #45): - matrix_t: construction shape/buffer, resize grow+shrink, copy_to - get_data_type / get_channel / get_data_type_size across all popular formats (API divergence documented: top-level functions in jsfeat, instance methods in jsfeatNext) - imgproc.resample f32 path npm test: 57 passed. Co-Authored-By: Claude Opus 4.8 --- tests/parity/structs.test.ts | 104 +++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/parity/structs.test.ts 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); + } + }); +});