diff --git a/.babelrc b/.babelrc index 2b7bafa..1bd836b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,8 @@ { + "plugins": [ + ["transform-react-remove-prop-types", { + "removeImport": true + }] + ], "presets": ["@babel/preset-env", "@babel/preset-react"] -} +} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index e2020f8..27fac27 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { jest: true, browser: true }, - plugins: ["jest", "promise", "prettier", "react", "react-hooks"], + plugins: ["jest", "markdown", "promise", "prettier", "react", "react-hooks"], rules: { "import/no-extraneous-dependencies": "off", "no-alert": "error", @@ -33,5 +33,14 @@ module.exports = { "react/jsx-filename-extension": "off", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", - } + }, + overrides: [{ + "files": ["**/*.md"], + "rules": { + "react/jsx-no-undef": "off", + "import/no-unresolved": "off", + "no-unused-expressions": "off", + "react/react-in-jsx-scope": "off" + } + }] } diff --git a/.github/workflows/github-pages.yaml b/.github/workflows/github-pages.yaml new file mode 100644 index 0000000..d77b119 --- /dev/null +++ b/.github/workflows/github-pages.yaml @@ -0,0 +1,23 @@ +name: github pages + +on: + release: + types: [created] + +jobs: + build-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 12.x + - run: npm ci + - run: npm run docs:build + - run: echo "react-posenet.yoyota.dev" > ./dist-docs/CNAME + - name: deploy + uses: peaceiris/actions-gh-pages@v2 + env: + PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }} + PUBLISH_BRANCH: gh-pages + PUBLISH_DIR: ./dist-docs diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index af79b6e..89bc16d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -7,7 +7,6 @@ jobs: publish-npm: runs-on: ubuntu-latest steps: - - run: echo $NPM_TOKEN - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: diff --git a/.gitignore b/.gitignore index b173461..4c3ce05 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ yarn-debug.log* yarn-error.log* # dist -dist \ No newline at end of file +dist +dist-docs \ No newline at end of file diff --git a/README.md b/README.md index 3233842..4d8fb47 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ # React PoseNet React PoseNet is a handy wrapper component for [tfjs-models/posenet](https://github.com/tensorflow/tfjs-models/tree/master/posenet) + +## Documentation + +https://react-posenet.yoyota.dev + +## Example + +[pull up counter](https://github.com/yoyota/react-posenet-pull-up) + + +## Installation + +```bash +npm install --save react-posenet +``` + +### Usage + +```jsx +import PoseNet from "react-posenet" + +export default function App() { + return +} +``` + +## Core Feautres + +### [onEstimate](https://react-posenet.yoyota.dev/#/Props%20examples?id=section-onestimate) + +gets called after estimation. [poses](https://github.com/tensorflow/tfjs-models/tree/master/posenet#keypoints) is a passed parameter + +### [input](https://react-posenet.yoyota.dev/#/Props%20examples?id=section-input) +the input image to feed through the network. see +[tfjs-posenet document](https://github.com/tensorflow/tfjs-models/tree/master/posenet#params-in-estimatesinglepose) +If input is not specified react-posenet try to [getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) \ No newline at end of file diff --git a/docs/Documentation.md b/docs/Documentation.md new file mode 100644 index 0000000..4507fd7 --- /dev/null +++ b/docs/Documentation.md @@ -0,0 +1,9 @@ +- [Github repository](https://github.com/yoyota/react-posenet) + +React PoseNet is a handy wrapper component for [tfjs-models/posenet](https://github.com/tensorflow/tfjs-models/tree/master/posenet) + +## Installation + +```bash +npm install --save react-posenet +``` diff --git a/docs/input.md b/docs/input.md new file mode 100644 index 0000000..6a2893a --- /dev/null +++ b/docs/input.md @@ -0,0 +1,13 @@ +```jsx +import { useMemo } from "react" +import PoseNet from "react-posenet" + +const input = useMemo(() => { + const image = new Image() + image.crossOrigin = "" + image.src = "https://i.imgur.com/oV2ZNuD.jpg" + return image +}, []) + +; +``` diff --git a/docs/onEstimate.md b/docs/onEstimate.md new file mode 100644 index 0000000..0d7981a --- /dev/null +++ b/docs/onEstimate.md @@ -0,0 +1,19 @@ +If your device dose not have camera, you will see error message like +Requested device not found + +```jsx +import { useState } from "react" +import PoseNet from "react-posenet" + +const [posesString, setPosesString] = useState([]) + +;<> + { + setPosesString(JSON.stringify(poses)) + }} + /> +

{posesString}

+ +``` diff --git a/docs/test.md b/docs/test.md new file mode 100644 index 0000000..d89db1d --- /dev/null +++ b/docs/test.md @@ -0,0 +1 @@ +# Test3 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 54d1d73..3300fee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-posenet", - "version": "0.0.0-alpha.1", + "version": "0.0.0-alpha.9", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2140,6 +2140,12 @@ "@types/babel__traverse": "^7.0.6" } }, + "babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "dev": true + }, "babel-preset-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", @@ -4165,6 +4171,89 @@ } } }, + "eslint-plugin-markdown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-1.0.1.tgz", + "integrity": "sha512-nAUURNHJGPooBMZMP23FmTbh3LTdgoSqeFBv9FA3fYrJ+vDUJxrp6nKiQF4iDNAmnWQnmnrDvV61BmIF4X9QAQ==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "remark-parse": "^5.0.0", + "unified": "^6.1.2" + }, + "dependencies": { + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "remark-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz", + "integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==", + "dev": true, + "requires": { + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^1.1.0", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^1.0.0", + "vfile-location": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "unified": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", + "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "dev": true, + "requires": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^2.0.0", + "x-is-string": "^0.1.0" + } + }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", + "dev": true + }, + "vfile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", + "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "dev": true, + "requires": { + "is-buffer": "^1.1.4", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + } + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "dev": true, + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + } + } + }, "eslint-plugin-prettier": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", @@ -9674,8 +9763,7 @@ "regenerator-runtime": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" }, "regenerator-transform": { "version": "0.14.1", @@ -12492,6 +12580,12 @@ "async-limiter": "~1.0.0" } }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", + "dev": true + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", diff --git a/package.json b/package.json index 6e83d7d..024d4e5 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "main": "dist/index.js", "scripts": { "build": "rollup --config", - "lint": "eslint --fix-dry-run src", + "lint": "eslint --ext md --ext js --fix-dry-run src docs", "start": "styleguidist server", - "styleguide:build": "styleguidist build" + "docs:build": "styleguidist build" }, "repository": { "type": "git", @@ -22,23 +22,27 @@ "@tensorflow-models/posenet": "^2.2.1", "@tensorflow/tfjs": "^1.3.2", "@tensorflow/tfjs-core": "^1.4.0", - "await-to-js": "^2.1.1" + "await-to-js": "^2.1.1", + "regenerator-runtime": "^0.13.3" }, "peerDependencies": { "react": "^16.12.0", "react-dom": "^16.12.0" }, "devDependencies": { + "prop-types": "^15.7.2", "@babel/core": "^7.7.5", "@babel/preset-env": "^7.7.5", "@babel/preset-react": "^7.7.4", "babel-loader": "^8.0.6", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "eslint": "^6.7.2", "eslint-config-airbnb": "^17.1.1", "eslint-config-prettier": "^6.7.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-jest": "^22.21.0", "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-markdown": "^1.0.1", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-react": "^7.17.0", @@ -55,4 +59,4 @@ "rollup-plugin-terser": "^5.1.3", "webpack": "^4.41.2" } -} +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 15a3454..c600f3b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,11 +19,11 @@ export default [ }, external: ["react", "react-dom"], plugins: [ + resolve(), babel({ exclude: "node_modules/**" }), external(), - resolve(), terser() ] } diff --git a/src/components/PoseNet.js b/src/components/PoseNet.js index c618359..75bf055 100644 --- a/src/components/PoseNet.js +++ b/src/components/PoseNet.js @@ -1,4 +1,5 @@ import React, { useRef, useState, useEffect } from "react" +import PropTypes from "prop-types" import to from "await-to-js" import Loading from "./Loading" import useLoadPoseNet from "../hooks/useLoadPoseNet" @@ -10,18 +11,18 @@ import { } from "../util" export default function PoseNet({ - id = "", - className = "", - facingMode = "user", - frameRate = 20, - input = undefined, - onEstimate = () => {}, - inferenceConfig = {}, - modelConfig = {}, - minPoseConfidence = 0.1, - minPartConfidence = 0.5, - width = 600, - height = 500 + id, + className, + facingMode, + frameRate, + input, + onEstimate, + inferenceConfig, + modelConfig, + minPoseConfidence, + minPartConfidence, + width, + height }) { const videoRef = useRef() const canvasRef = useRef() @@ -127,3 +128,72 @@ export default function PoseNet({ ) } + +PoseNet.propTypes = { + /** canvas id */ + id: PropTypes.string, + /** canvas className */ + className: PropTypes.string, + /** @see https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode */ + facingMode: PropTypes.string, + /** First of all frameRate is parameter of [getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) + * see [MediaTrackConstraints.frameRate](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/frameRate) + *
+ * second frameRate affects how often estimation occurs. react-posenet internally
+ * [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval)(() => { estimatePose() } , (1000 / framerate)) + * to estimate image continuously */ + frameRate: PropTypes.number, + /** + * the input image to feed through the network.
+ * If input is not specified react-posenet try to [getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
+ * @see [tfjs-posenet document](https://github.com/tensorflow/tfjs-models/tree/master/posenet#params-in-estimatesinglepose) + */ + input: PropTypes.element, + /** + * gets called after estimation. [poses](https://github.com/tensorflow/tfjs-models/tree/master/posenet#keypoints) is a passed parameter + */ + onEstimate: PropTypes.func, + /** + * If you want swtich between single / multi pose estimation.
+ * use decodingMethod [please check this code](https://github.com/tensorflow/tfjs-models/blob/master/posenet/demos/camera.js#L392)
+ * {decodingMethod: "single-person"} / {decodingMethod: "multi-person"} + * @see [tfjs-posenet documentation](https://github.com/tensorflow/tfjs-models/tree/master/posenet#params-in-estimatemultipleposes) + */ + inferenceConfig: PropTypes.shape({ + decodingMethod: PropTypes.string, + flipHorizontal: PropTypes.bool, + maxDetections: PropTypes.number, + scoreThreshold: PropTypes.number, + nmsRadius: PropTypes.number + }), + /** @see [tfjs-posenet documentation](https://github.com/tensorflow/tfjs-models/tree/master/posenet#config-params-in-posenetload) */ + modelConfig: PropTypes.shape({ + architecture: PropTypes.string, + outputStride: PropTypes.number, + inputResolution: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), + quantBytes: PropTypes.number + }), + /** minimum confidence constraint for pose */ + minPoseConfidence: PropTypes.number, + /** minimum confidence constraint for each [part](https://github.com/tensorflow/tfjs-models/tree/master/posenet#keypoints) */ + minPartConfidence: PropTypes.number, + /** canvas width */ + width: PropTypes.number, + /** canvas height */ + height: PropTypes.number +} + +PoseNet.defaultProps = { + id: "", + className: "", + facingMode: "user", + frameRate: 20, + input: undefined, + onEstimate: () => {}, + inferenceConfig: {}, + modelConfig: {}, + minPoseConfidence: 0.1, + minPartConfidence: 0.5, + width: 600, + height: 500 +} diff --git a/src/components/PoseNet.md b/src/components/PoseNet.md index ce07dfd..4c00280 100644 --- a/src/components/PoseNet.md +++ b/src/components/PoseNet.md @@ -1,40 +1,6 @@ -#### PoseNet +If your device dose not have camera, you will see error message like +Requested device not found ```jsx -import { useState } from "react"; -import PoseNet from "react-posenet"; - -const [posesString, setPosesString] = useState([]); - -<> - { - setPosesString(JSON.stringify(poses)); - }} - /> -

{posesString}

-; + ``` - - diff --git a/src/index.js b/src/index.js index b4b67c2..fde304c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +import "regenerator-runtime/runtime" import PoseNet from "./components/PoseNet" export default PoseNet diff --git a/styleguide.config.js b/styleguide.config.js index 85ed3f6..5f24e1c 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -1,7 +1,31 @@ const path = require("path") module.exports = { - components: ["src/components/PoseNet.js"], + title: "PoseNet React", + pagePerSection: true, + sections: [ + { + name: "Documentation", + content: "docs/Documentation.md", + components: () => ["./src/components/PoseNet.js"] + }, + { + name: "Props examples", + sections: [ + { + name: "input", + content: "docs/input.md", + exampleMode: "expand" + }, + { + name: "onEstimate", + content: "docs/onEstimate.md", + exampleMode: "expand" + } + ], + sectionDepth: 0 + } + ], webpackConfig: { module: { rules: [ @@ -13,7 +37,6 @@ module.exports = { ] } }, - title: "PoseNet React", styleguideDir: "dist-docs", moduleAliases: { "react-posenet": path.resolve(__dirname, "src")