init
This commit is contained in:
commit
ce2116733e
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||||
210
bun.lock
Normal file
210
bun.lock
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "ball-tracking-and-predict",
|
||||||
|
"dependencies": {
|
||||||
|
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||||
|
"vue": "^3.5.18",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.1.2",
|
||||||
|
"vue-tsc": "^3.0.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.29", "", {}, "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="],
|
||||||
|
|
||||||
|
"@techstark/opencv-js": ["@techstark/opencv-js@4.10.0-release.1", "", {}, "sha512-S4XELidRiQeA0q1s9VQLo540wCxUo24r1O4C+LqZ6llX+sPCXvZCPv3Ice8dEIr0uavyZ8YZeKXSBdDgMXSXjw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.29" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw=="],
|
||||||
|
|
||||||
|
"@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="],
|
||||||
|
|
||||||
|
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
|
||||||
|
|
||||||
|
"@volar/typescript": ["@volar/typescript@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag=="],
|
||||||
|
|
||||||
|
"@vue/compiler-core": ["@vue/compiler-core@3.5.21", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/shared": "3.5.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw=="],
|
||||||
|
|
||||||
|
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.21", "", { "dependencies": { "@vue/compiler-core": "3.5.21", "@vue/shared": "3.5.21" } }, "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ=="],
|
||||||
|
|
||||||
|
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.21", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/compiler-core": "3.5.21", "@vue/compiler-dom": "3.5.21", "@vue/compiler-ssr": "3.5.21", "@vue/shared": "3.5.21", "estree-walker": "^2.0.2", "magic-string": "^0.30.18", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ=="],
|
||||||
|
|
||||||
|
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/shared": "3.5.21" } }, "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w=="],
|
||||||
|
|
||||||
|
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
|
||||||
|
|
||||||
|
"@vue/language-core": ["@vue/language-core@3.0.6", "", { "dependencies": { "@volar/language-core": "2.4.23", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^2.0.5", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A=="],
|
||||||
|
|
||||||
|
"@vue/reactivity": ["@vue/reactivity@3.5.21", "", { "dependencies": { "@vue/shared": "3.5.21" } }, "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA=="],
|
||||||
|
|
||||||
|
"@vue/runtime-core": ["@vue/runtime-core@3.5.21", "", { "dependencies": { "@vue/reactivity": "3.5.21", "@vue/shared": "3.5.21" } }, "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA=="],
|
||||||
|
|
||||||
|
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.21", "", { "dependencies": { "@vue/reactivity": "3.5.21", "@vue/runtime-core": "3.5.21", "@vue/shared": "3.5.21", "csstype": "^3.1.3" } }, "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w=="],
|
||||||
|
|
||||||
|
"@vue/server-renderer": ["@vue/server-renderer@3.5.21", "", { "dependencies": { "@vue/compiler-ssr": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "vue": "3.5.21" } }, "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA=="],
|
||||||
|
|
||||||
|
"@vue/shared": ["@vue/shared@3.5.21", "", {}, "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw=="],
|
||||||
|
|
||||||
|
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
|
||||||
|
|
||||||
|
"alien-signals": ["alien-signals@2.0.7", "", {}, "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
|
||||||
|
|
||||||
|
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
|
||||||
|
|
||||||
|
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
|
||||||
|
|
||||||
|
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||||
|
|
||||||
|
"vue": ["vue@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", "@vue/runtime-dom": "3.5.21", "@vue/server-renderer": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA=="],
|
||||||
|
|
||||||
|
"vue-tsc": ["vue-tsc@3.0.6", "", { "dependencies": { "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "ball-tracking-and-predict",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||||
|
"vue": "^3.5.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.1.2",
|
||||||
|
"vue-tsc": "^3.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
319
src/App.vue
Normal file
319
src/App.vue
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, useTemplateRef, watch, computed, type CSSProperties } from 'vue';
|
||||||
|
import { extractFrameFromVideo } from './vfex';
|
||||||
|
import { YellowBallTracker } from './core';
|
||||||
|
import cv from '@techstark/opencv-js';
|
||||||
|
|
||||||
|
const frames = ref<VideoFrame[]>([]);
|
||||||
|
|
||||||
|
const rawCanvas = useTemplateRef('raw-canvas');
|
||||||
|
const processedCanvas = useTemplateRef('processed-canvas');
|
||||||
|
|
||||||
|
let rawCtx: CanvasRenderingContext2D;
|
||||||
|
let processedCtx: CanvasRenderingContext2D;
|
||||||
|
onMounted(() => {
|
||||||
|
rawCtx = rawCanvas.value!.getContext('2d', { willReadFrequently: true })!;
|
||||||
|
processedCtx = processedCanvas.value!.getContext('2d')!;
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxFrames = 128;
|
||||||
|
const seqLen = 120;
|
||||||
|
|
||||||
|
const decodeProgress = ref(0);
|
||||||
|
const trackProgress = ref(0);
|
||||||
|
|
||||||
|
const centerPosArr = ref<({ x: number; y: number } | null)[]>([]);
|
||||||
|
const predictedPosArr = ref<[number, number][][]>([]);
|
||||||
|
async function handleFileChange(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const file = input.files[0];
|
||||||
|
console.log('Selected file:', file);
|
||||||
|
// You can add further processing of the file here
|
||||||
|
frames.value = await extractFrameFromVideo(URL.createObjectURL(file), maxFrames, (p) => decodeProgress.value = p.decode)
|
||||||
|
|
||||||
|
const { codedHeight: h, codedWidth: w } = frames.value[0];
|
||||||
|
|
||||||
|
console.log("解码完成", frames.value.length, h, w);
|
||||||
|
|
||||||
|
|
||||||
|
if (!rawCanvas.value || !processedCanvas.value) return;
|
||||||
|
rawCanvas.value.width = w;
|
||||||
|
rawCanvas.value.height = h;
|
||||||
|
processedCanvas.value.width = w;
|
||||||
|
processedCanvas.value.height = h;
|
||||||
|
|
||||||
|
const tracker = new YellowBallTracker({ lowerYellow: new cv.Scalar(20, 100, 100, 0), upperYellow: new cv.Scalar(40, 255, 255, 255), blurSize: 7, morphKernel: 5, minContourArea: 20, useRoi: true, roiScaleX: 0.25, roiScaleY: 0.35 });
|
||||||
|
tracker.initMats(w, h);
|
||||||
|
|
||||||
|
let lastPos = null
|
||||||
|
|
||||||
|
for (let index = 0; index < frames.value.length; index++) {
|
||||||
|
selectedIndex.value = index;
|
||||||
|
await reqNextFrame();
|
||||||
|
|
||||||
|
const center = tracker.detectYellowBall(rawCtx.getImageData(0, 0, w, h), lastPos);
|
||||||
|
centerPosArr.value[index] = center;
|
||||||
|
lastPos = center;
|
||||||
|
|
||||||
|
console.log(center);
|
||||||
|
|
||||||
|
|
||||||
|
trackProgress.value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstAvailableIndex = centerPosArr.value.findIndex(c => c !== null);
|
||||||
|
if (firstAvailableIndex === -1) {
|
||||||
|
console.warn("未检测到小球");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstAvailableIndex + seqLen >= centerPosArr.value.length) {
|
||||||
|
console.warn("视频帧数不足,无法预测");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (let index = firstAvailableIndex; index < centerPosArr.value.length - seqLen; index++) {
|
||||||
|
const seq = centerPosArr.value.slice(index, index + seqLen).map(p => p ? [p.x, p.y] : [0, 0]);
|
||||||
|
const body = {
|
||||||
|
sequences: [seq],
|
||||||
|
steps: 30,
|
||||||
|
return_angles: true,
|
||||||
|
unwrap_from_last: true
|
||||||
|
}
|
||||||
|
const resp = await fetch("http://127.0.0.1:8000/predict", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
console.log(json);
|
||||||
|
|
||||||
|
predictedPosArr.value[index + seqLen] = json.pred_xy[0];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reqNextFrame = () => new Promise<void>((resolve) => {
|
||||||
|
requestAnimationFrame(() => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
// ========================
|
||||||
|
// 预测轨迹动画渲染辅助
|
||||||
|
// ========================
|
||||||
|
let animReqId: number | null = null;
|
||||||
|
let animStartTs = 0;
|
||||||
|
|
||||||
|
function cancelPredictionAnim() {
|
||||||
|
if (animReqId !== null) {
|
||||||
|
cancelAnimationFrame(animReqId);
|
||||||
|
animReqId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBaseFrame(index: number) {
|
||||||
|
const frame = frames.value[index];
|
||||||
|
if (!frame) return;
|
||||||
|
// 背景帧
|
||||||
|
rawCtx.drawImage(frame, 0, 0);
|
||||||
|
processedCtx.drawImage(frame, 0, 0);
|
||||||
|
// 检测到的小球标记
|
||||||
|
if (centerPosArr.value[index]) {
|
||||||
|
const { x, y } = centerPosArr.value[index]!;
|
||||||
|
processedCtx.beginPath();
|
||||||
|
processedCtx.strokeStyle = 'red';
|
||||||
|
processedCtx.lineWidth = 4;
|
||||||
|
processedCtx.arc(x, y, 20, 0, Math.PI * 2);
|
||||||
|
processedCtx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPredictionPartial(preds: Array<[number, number]>, count: number) {
|
||||||
|
if (!preds.length) return;
|
||||||
|
const [sx, sy] = preds[0];
|
||||||
|
const [ex, ey] = preds[preds.length - 1];
|
||||||
|
const gradient = processedCtx.createLinearGradient(sx, sy, ex, ey);
|
||||||
|
gradient.addColorStop(0, 'rgba(0, 255, 0, 0.95)');
|
||||||
|
gradient.addColorStop(1, 'rgba(0, 255, 0, 0.3)');
|
||||||
|
|
||||||
|
if (count <= 0) {
|
||||||
|
// 只有起点时,画一个小点提升可见性
|
||||||
|
processedCtx.beginPath();
|
||||||
|
processedCtx.fillStyle = 'rgba(0, 255, 0, 0.95)';
|
||||||
|
processedCtx.arc(sx, sy, 3, 0, Math.PI * 2);
|
||||||
|
processedCtx.fill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = Math.min(count, preds.length - 1);
|
||||||
|
processedCtx.beginPath();
|
||||||
|
processedCtx.lineWidth = 12;
|
||||||
|
processedCtx.lineCap = 'round';
|
||||||
|
processedCtx.lineJoin = 'round';
|
||||||
|
processedCtx.moveTo(sx, sy);
|
||||||
|
for (let i = 1; i <= last; i++) {
|
||||||
|
const [x, y] = preds[i];
|
||||||
|
processedCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
processedCtx.strokeStyle = gradient;
|
||||||
|
processedCtx.stroke();
|
||||||
|
// 在终点画一个蓝色小球
|
||||||
|
processedCtx.beginPath();
|
||||||
|
processedCtx.strokeStyle = 'blue';
|
||||||
|
processedCtx.lineWidth = 4;
|
||||||
|
processedCtx.arc(preds[last][0], preds[last][1], 20, 0, Math.PI * 2);
|
||||||
|
processedCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function animatePrediction(index: number) {
|
||||||
|
const preds = predictedPosArr.value[index];
|
||||||
|
if (!preds || preds.length === 0) return;
|
||||||
|
|
||||||
|
cancelPredictionAnim();
|
||||||
|
animStartTs = performance.now();
|
||||||
|
|
||||||
|
const totalSegments = Math.max(1, preds.length - 1);
|
||||||
|
const duration = 800; // ms
|
||||||
|
const perSeg = duration / totalSegments;
|
||||||
|
|
||||||
|
const step = (now: number) => {
|
||||||
|
const elapsed = now - animStartTs;
|
||||||
|
// 目标画到的点下标(包含起点),至少为 1(起点),至多为 preds.length - 1
|
||||||
|
const targetIdx = Math.min(preds.length - 1, Math.floor(elapsed / perSeg) + 1);
|
||||||
|
|
||||||
|
// 每帧重绘背景与部分轨迹
|
||||||
|
renderBaseFrame(index);
|
||||||
|
drawPredictionPartial(preds, targetIdx);
|
||||||
|
|
||||||
|
if (targetIdx < preds.length - 1) {
|
||||||
|
animReqId = requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
// 最后一帧,收尾并清空动画句柄
|
||||||
|
animReqId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animReqId = requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedIndex, async (newIndex) => {
|
||||||
|
if (!rawCanvas.value || !processedCanvas.value) return;
|
||||||
|
if (!rawCtx || !processedCtx) return;
|
||||||
|
cancelPredictionAnim();
|
||||||
|
renderBaseFrame(newIndex);
|
||||||
|
// 如果当前帧已有预测,则启动动画;否则等待预测到达时再动画
|
||||||
|
if (predictedPosArr.value[newIndex]) {
|
||||||
|
animatePrediction(newIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// 时间轴进度条(当前帧、用于预测的范围、预测范围)
|
||||||
|
// ========================
|
||||||
|
const framesLen = computed(() => frames.value.length);
|
||||||
|
const currentIndex = computed(() => selectedIndex.value);
|
||||||
|
|
||||||
|
function clampIndex(i: number) {
|
||||||
|
const max = Math.max(0, framesLen.value - 1);
|
||||||
|
return Math.min(Math.max(i, 0), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPercent(i: number) {
|
||||||
|
if (framesLen.value <= 1) return 0;
|
||||||
|
const total = framesLen.value - 1;
|
||||||
|
const clamped = clampIndex(i);
|
||||||
|
return (clamped / total) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seqStartIdx = computed(() => Math.max(0, currentIndex.value - seqLen));
|
||||||
|
const seqEndIdx = computed(() => currentIndex.value - 1);
|
||||||
|
|
||||||
|
const predLen = computed(() => {
|
||||||
|
const preds = predictedPosArr.value[currentIndex.value];
|
||||||
|
return preds ? preds.length : 0;
|
||||||
|
});
|
||||||
|
const predStartIdx = computed(() => currentIndex.value);
|
||||||
|
const predEndIdx = computed(() => currentIndex.value + Math.max(0, predLen.value - 1));
|
||||||
|
|
||||||
|
const seqRangeStyle = computed<CSSProperties>(() => {
|
||||||
|
if (framesLen.value === 0) return { display: 'none' };
|
||||||
|
const start = seqStartIdx.value;
|
||||||
|
const end = seqEndIdx.value;
|
||||||
|
if (end < start) return { display: 'none' };
|
||||||
|
const left = toPercent(start);
|
||||||
|
const width = Math.max(0, toPercent(end) - toPercent(start));
|
||||||
|
return {
|
||||||
|
left: `${left}%`,
|
||||||
|
width: `${width}%`,
|
||||||
|
background: 'rgba(0, 153, 255, 0.35)'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const predRangeStyle = computed<CSSProperties>(() => {
|
||||||
|
if (framesLen.value === 0) return { display: 'none' };
|
||||||
|
if (predLen.value === 0) return { display: 'none' };
|
||||||
|
const start = predStartIdx.value;
|
||||||
|
const end = Math.min(framesLen.value - 1, predEndIdx.value);
|
||||||
|
if (end < start) return { display: 'none' };
|
||||||
|
const left = toPercent(start);
|
||||||
|
const width = Math.max(0, toPercent(end) - toPercent(start));
|
||||||
|
return {
|
||||||
|
left: `${left}%`,
|
||||||
|
width: `${width}%`,
|
||||||
|
background: 'linear-gradient(90deg, rgba(0,255,0,0.95), rgba(0,255,0,0.3))'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentCursorStyle = computed<CSSProperties>(() => {
|
||||||
|
if (framesLen.value === 0) return { left: '0%' };
|
||||||
|
return { left: `${toPercent(currentIndex.value)}%` };
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input type="file" name="video" id="video" @change="handleFileChange"
|
||||||
|
accept="video/*" />
|
||||||
|
解码:<progress :value="decodeProgress" :max="1"></progress>
|
||||||
|
追踪:<progress :value="trackProgress" :max="frames.length - 1"></progress>
|
||||||
|
<input type="range" min="0" :max="frames.length - 1" style="width: 100%;"
|
||||||
|
v-model="selectedIndex" />
|
||||||
|
<!-- 自定义时间轴:淡蓝色=用于预测的输入序列,绿色渐变=预测范围,红线=当前帧位置 -->
|
||||||
|
<div class="timeline" aria-label="prediction timeline">
|
||||||
|
<div class="range seq" :style="seqRangeStyle"></div>
|
||||||
|
<div class="range pred" :style="predRangeStyle"></div>
|
||||||
|
<div class="cursor" :style="currentCursorStyle"></div>
|
||||||
|
</div>
|
||||||
|
<canvas ref="raw-canvas" style="display: none;"/>
|
||||||
|
<canvas ref="processed-canvas" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
height: 12px;
|
||||||
|
margin: 8px 0 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eeeeee;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.timeline .range {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.timeline .cursor {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
width: 2px;
|
||||||
|
background: #ff3b30;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
41
src/components/HelloWorld.vue
Normal file
41
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{ msg: string }>()
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
220
src/core.ts
Normal file
220
src/core.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import cv from '@techstark/opencv-js'
|
||||||
|
|
||||||
|
export interface TrackerParams {
|
||||||
|
lowerYellow: any;
|
||||||
|
upperYellow: any;
|
||||||
|
blurSize: number;
|
||||||
|
morphKernel: number;
|
||||||
|
minContourArea: number;
|
||||||
|
useRoi: boolean;
|
||||||
|
roiScaleX: number;
|
||||||
|
roiScaleY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class YellowBallTracker {
|
||||||
|
public srcMat: cv.Mat | null = null
|
||||||
|
private hsvMat: cv.Mat | null = null
|
||||||
|
private blurredMat: cv.Mat | null = null
|
||||||
|
private maskMat: cv.Mat | null = null
|
||||||
|
private hierarchy: cv.Mat | null = null
|
||||||
|
private contours: cv.MatVector | null = null
|
||||||
|
private morphKernelMat: cv.Mat | null = null
|
||||||
|
private lowerBoundMat: cv.Mat | null = null
|
||||||
|
private upperBoundMat: cv.Mat | null = null
|
||||||
|
private params: TrackerParams
|
||||||
|
|
||||||
|
constructor(params: TrackerParams) {
|
||||||
|
this.params = params
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateKernel() {
|
||||||
|
if (this.morphKernelMat) this.morphKernelMat.delete()
|
||||||
|
this.morphKernelMat = cv.Mat.ones(this.params.morphKernel, this.params.morphKernel, cv.CV_8U)
|
||||||
|
}
|
||||||
|
|
||||||
|
public initMats(w: number, h: number) {
|
||||||
|
this.cleanupMats()
|
||||||
|
|
||||||
|
this.srcMat = new cv.Mat(h, w, cv.CV_8UC4)
|
||||||
|
this.hsvMat = new cv.Mat()
|
||||||
|
this.blurredMat = new cv.Mat()
|
||||||
|
this.maskMat = new cv.Mat()
|
||||||
|
this.hierarchy = new cv.Mat()
|
||||||
|
this.contours = new cv.MatVector()
|
||||||
|
this.updateKernel()
|
||||||
|
// 创建阈值 Mat
|
||||||
|
this.lowerBoundMat = new cv.Mat(h, w, cv.CV_8UC3, this.params.lowerYellow)
|
||||||
|
this.upperBoundMat = new cv.Mat(h, w, cv.CV_8UC3, this.params.upperYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateROI(lastPos: Position): { x: number, y: number, width: number, height: number } {
|
||||||
|
const width = this.srcMat!.cols * this.params.roiScaleX
|
||||||
|
const height = this.srcMat!.rows * this.params.roiScaleY
|
||||||
|
const x = Math.max(0, Math.min(this.srcMat!.cols - width, lastPos.x - width / 2))
|
||||||
|
const y = Math.max(0, Math.min(this.srcMat!.rows - height, lastPos.y - height / 2))
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.round(x),
|
||||||
|
y: Math.round(y),
|
||||||
|
width: Math.round(width),
|
||||||
|
height: Math.round(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawROIDebugInfo(roi: { x: number, y: number, width: number, height: number }) {
|
||||||
|
cv.rectangle(this.srcMat!,
|
||||||
|
new cv.Point(roi.x, roi.y),
|
||||||
|
new cv.Point(roi.x + roi.width, roi.y + roi.height),
|
||||||
|
new cv.Scalar(255, 0, 0, 255), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private processImage(inputMat: cv.Mat, lowerBound: cv.Mat, upperBound: cv.Mat) {
|
||||||
|
// 颜色空间转换和模糊处理
|
||||||
|
cv.cvtColor(inputMat, this.hsvMat!, cv.COLOR_RGBA2RGB)
|
||||||
|
cv.cvtColor(this.hsvMat!, this.hsvMat!, cv.COLOR_RGB2HSV)
|
||||||
|
cv.blur(this.hsvMat!, this.blurredMat!, new cv.Size(this.params.blurSize, this.params.blurSize))
|
||||||
|
|
||||||
|
// 颜色范围过滤
|
||||||
|
cv.inRange(this.blurredMat!, lowerBound, upperBound, this.maskMat!)
|
||||||
|
|
||||||
|
// 形态学操作
|
||||||
|
cv.morphologyEx(this.maskMat!, this.maskMat!, cv.MORPH_OPEN, this.morphKernelMat!)
|
||||||
|
cv.morphologyEx(this.maskMat!, this.maskMat!, cv.MORPH_CLOSE, this.morphKernelMat!)
|
||||||
|
cv.dilate(this.maskMat!, this.maskMat!, this.morphKernelMat!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private findBestContour(offsetX: number = 0, offsetY: number = 0): Position | null {
|
||||||
|
// 查找轮廓
|
||||||
|
this.contours!.delete()
|
||||||
|
this.contours = new cv.MatVector()
|
||||||
|
cv.findContours(this.maskMat!, this.contours, this.hierarchy!, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
// 找到最大面积的轮廓
|
||||||
|
let maxArea = 0
|
||||||
|
let best: Position | null = null
|
||||||
|
|
||||||
|
for (let i = 0; i < this.contours.size(); i++) {
|
||||||
|
const cnt = this.contours.get(i)
|
||||||
|
const area = cv.contourArea(cnt)
|
||||||
|
|
||||||
|
if (area > this.params.minContourArea && area > maxArea) {
|
||||||
|
maxArea = area
|
||||||
|
const circle = cv.minEnclosingCircle(cnt)
|
||||||
|
best = {
|
||||||
|
x: Math.round(circle.center.x + offsetX),
|
||||||
|
y: Math.round(circle.center.y + offsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cnt.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawDetectionResult(position: Position) {
|
||||||
|
cv.circle(this.srcMat!, new cv.Point(position.x, position.y), 10, new cv.Scalar(0, 255, 0, 255), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
public detectYellowBall(imgData: ImageData, lastPos?: Position | null): Position | null {
|
||||||
|
if (!this.srcMat || !this.hsvMat || !this.blurredMat || !this.maskMat || !this.hierarchy ||
|
||||||
|
!this.contours || !this.morphKernelMat || !this.lowerBoundMat || !this.upperBoundMat) {
|
||||||
|
console.error('Mats not initialized')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set image data
|
||||||
|
this.srcMat.data.set(imgData.data)
|
||||||
|
|
||||||
|
let best: Position | null = null
|
||||||
|
let srcRoiMat = new cv.Mat()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试ROI检测
|
||||||
|
if (this.params.useRoi && lastPos) {
|
||||||
|
const roi = this.calculateROI(lastPos)
|
||||||
|
this.drawROIDebugInfo(roi)
|
||||||
|
|
||||||
|
// 提取ROI区域
|
||||||
|
const roiRect = new cv.Rect(roi.x, roi.y, roi.width, roi.height)
|
||||||
|
srcRoiMat = this.srcMat.roi(roiRect)
|
||||||
|
|
||||||
|
// 为ROI区域创建合适大小的阈值Mat
|
||||||
|
const roiLowerBound = new cv.Mat(roi.height, roi.width, cv.CV_8UC3, this.params.lowerYellow)
|
||||||
|
const roiUpperBound = new cv.Mat(roi.height, roi.width, cv.CV_8UC3, this.params.upperYellow)
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.processImage(srcRoiMat, roiLowerBound, roiUpperBound)
|
||||||
|
best = this.findBestContour(roi.x, roi.y)
|
||||||
|
} finally {
|
||||||
|
roiLowerBound.delete()
|
||||||
|
roiUpperBound.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果ROI中没找到或者没启用ROI,就在全图搜索
|
||||||
|
if (!best) {
|
||||||
|
if (this.params.useRoi && lastPos) {
|
||||||
|
console.log("ROI失败,全域搜索")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processImage(this.srcMat, this.lowerBoundMat, this.upperBoundMat)
|
||||||
|
best = this.findBestContour()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在原图上绘制找到的点
|
||||||
|
if (best) {
|
||||||
|
this.drawDetectionResult(best)
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 清理资源
|
||||||
|
srcRoiMat.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOutputCanvas(targetCanvas: HTMLCanvasElement) {
|
||||||
|
if (!this.srcMat) return null
|
||||||
|
cv.imshow(targetCanvas, this.srcMat)
|
||||||
|
return targetCanvas
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateParams(params: Partial<TrackerParams>) {
|
||||||
|
this.params = { ...this.params, ...params }
|
||||||
|
|
||||||
|
// 如果更新了morphKernel,需要重新创建kernel
|
||||||
|
if ('morphKernel' in params) {
|
||||||
|
this.updateKernel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanupMats() {
|
||||||
|
// 清理资源
|
||||||
|
if (this.srcMat) this.srcMat.delete()
|
||||||
|
if (this.hsvMat) this.hsvMat.delete()
|
||||||
|
if (this.blurredMat) this.blurredMat.delete()
|
||||||
|
if (this.maskMat) this.maskMat.delete()
|
||||||
|
if (this.hierarchy) this.hierarchy.delete()
|
||||||
|
if (this.contours) this.contours.delete()
|
||||||
|
if (this.morphKernelMat) this.morphKernelMat.delete()
|
||||||
|
if (this.lowerBoundMat) this.lowerBoundMat.delete()
|
||||||
|
if (this.upperBoundMat) this.upperBoundMat.delete()
|
||||||
|
|
||||||
|
// 重置引用
|
||||||
|
this.srcMat = null
|
||||||
|
this.hsvMat = null
|
||||||
|
this.blurredMat = null
|
||||||
|
this.maskMat = null
|
||||||
|
this.hierarchy = null
|
||||||
|
this.contours = null
|
||||||
|
this.morphKernelMat = null
|
||||||
|
this.lowerBoundMat = null
|
||||||
|
this.upperBoundMat = null
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
0
src/style.css
Normal file
0
src/style.css
Normal file
109
src/vfex/index.ts
Normal file
109
src/vfex/index.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
export async function downloadRes(url: string, onProgress: (progress: number) => void): Promise<ArrayBuffer> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open('GET', url, true)
|
||||||
|
xhr.responseType = 'arraybuffer'
|
||||||
|
|
||||||
|
xhr.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
const percentComplete = event.loaded / event.total
|
||||||
|
onProgress(percentComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
reject(new Error(`Failed to load video: ${xhr.statusText}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xhr.response) {
|
||||||
|
reject(new Error('Failed to load video data'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(xhr.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(new Error('Network error occurred'))
|
||||||
|
xhr.send()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Worker 并处理帧提取
|
||||||
|
async function extractFramesWithWorker(
|
||||||
|
blobPath: string,
|
||||||
|
expectFrames: number,
|
||||||
|
onProgress: (progress: number, totalSize: number) => void
|
||||||
|
): Promise<VideoFrame[]> {
|
||||||
|
let totalSize = 0
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const frames: VideoFrame[] = []
|
||||||
|
const worker = new Worker(new URL('./worker.js', import.meta.url),
|
||||||
|
{ type: 'classic' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleWorkerMessage = (event: MessageEvent) => {
|
||||||
|
const data = event.data
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (key === 'append_frame') {
|
||||||
|
const frame = data[key] as VideoFrame
|
||||||
|
frames.push(frame)
|
||||||
|
totalSize += frame.allocationSize()
|
||||||
|
onProgress(frames.length / expectFrames, totalSize )
|
||||||
|
|
||||||
|
if (frames.length >= expectFrames) {
|
||||||
|
worker.removeEventListener('message', handleWorkerMessage)
|
||||||
|
worker.terminate()
|
||||||
|
resolve(frames.slice(0, expectFrames))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// console.log(`[vFrameExtractor]:`, key, data[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.addEventListener('message', handleWorkerMessage)
|
||||||
|
worker.addEventListener('error', (error) => {
|
||||||
|
worker.terminate()
|
||||||
|
reject(new Error(`Worker error: ${error.message}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
worker.postMessage({ dataUri: blobPath })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
export async function extractFrameFromVideo(
|
||||||
|
videoPath: string,
|
||||||
|
expectFrames: number,
|
||||||
|
onProgress?: (progress: { download: number, decode: number}) => void
|
||||||
|
): Promise<VideoFrame[]> {
|
||||||
|
try {
|
||||||
|
// 下载视频
|
||||||
|
const arrayBuffer = await downloadRes(videoPath, (downloadProgress) => {
|
||||||
|
onProgress?.({ download: downloadProgress, decode: 0})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建 Blob 和 URL
|
||||||
|
const blob = new Blob([arrayBuffer], { type: 'video/mp4' })
|
||||||
|
const blobPath = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 提取帧
|
||||||
|
const frames = await extractFramesWithWorker(blobPath, expectFrames, (decodeProgress) => {
|
||||||
|
onProgress?.({ download: 1, decode: decodeProgress })
|
||||||
|
})
|
||||||
|
|
||||||
|
return frames
|
||||||
|
} finally {
|
||||||
|
// 清理 URL
|
||||||
|
URL.revokeObjectURL(blobPath)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error instanceof Error ? error : new Error('Unknown error occurred')
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/vfex/worker.js
Normal file
174
src/vfex/worker.js
Normal file
File diff suppressed because one or more lines are too long
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
15
tsconfig.app.json
Normal file
15
tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user