├── jest.setup.ts ├── .eslintignore ├── .gitignore ├── src ├── index.ts ├── types │ ├── index.ts │ ├── FaceDetectionReturnType.ts │ └── FaceDetectionOptions.ts └── hooks │ └── useFaceDetection.ts ├── index.d.ts ├── .vscode └── settings.json ├── .prettierrc.js ├── tsconfig.json ├── scripts ├── add_react.js └── remove_react.js ├── jest.config.js ├── rollup.config.js ├── CHANGELOG.md ├── .eslintrc.js ├── package.json └── README.md /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | util 4 | scripts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | storybook-static/ -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks/useFaceDetection'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: any; 3 | export = value; 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnPaste": true 4 | } 5 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FaceDetectionOptions'; 2 | export * from './FaceDetectionReturnType'; 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; -------------------------------------------------------------------------------- /src/types/FaceDetectionReturnType.ts: -------------------------------------------------------------------------------- 1 | import { NormalizedRect } from '@mediapipe/face_detection'; 2 | import { LegacyRef, RefObject } from 'react'; 3 | import Webcam from 'react-webcam'; 4 | 5 | export type BoundingBox = NormalizedRect; 6 | 7 | export interface IFaceDetectionReturnType { 8 | boundingBox: BoundingBox[]; 9 | isLoading: boolean; 10 | detected: boolean; 11 | facesDetected: number; 12 | webcamRef: LegacyRef; 13 | imgRef: RefObject; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "declaration": true, 5 | "declarationDir": "build", 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": ["es6", "dom", "es2016", "es2017"], 9 | "sourceMap": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true 14 | }, 15 | "include": ["src/**/*", "index.d.ts"], 16 | "exclude": ["node_modules", "build"] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/add_react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this script when you want to revert back from 3 | * the effect of 'remove_react' script. 4 | * 5 | * @USAGE `node add_react` 6 | */ 7 | /* eslint-disable @typescript-eslint/no-var-requires */ 8 | const fs = require('fs'); 9 | const { execSync } = require('child_process'); 10 | 11 | if (fs.existsSync('node_modules/_react')) { 12 | execSync('cd node_modules && mv _react react'); 13 | } 14 | 15 | if (fs.existsSync('node_modules/_react-dom')) { 16 | execSync('cd node_modules && mv _react-dom react-dom'); 17 | } 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["src"], 3 | setupFilesAfterEnv: ["./jest.setup.ts"], 4 | moduleFileExtensions: ["ts", "tsx", "js"], 5 | testPathIgnorePatterns: ["node_modules/"], 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | testMatch: ["**/*.test.(ts|tsx)"], 10 | moduleNameMapper: { 11 | // Mocks out all these file formats when tests are run. 12 | "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": 13 | "identity-obj-proxy", 14 | "\\.(css|less|scss|sass)$": "identity-obj-proxy" 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/remove_react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this script when you are using 3 | * linked React dependency from the other and 4 | * you want to make sure only one React instance is used. 5 | * 6 | * @USAGE `node remove_react` 7 | */ 8 | /* eslint-disable @typescript-eslint/no-var-requires */ 9 | const fs = require('fs'); 10 | const { execSync } = require('child_process'); 11 | 12 | if (fs.existsSync('node_modules/react')) { 13 | execSync('cd node_modules && mv react _react'); 14 | console.log('*** Temporary REMOVE local "react" dependencies'); 15 | } 16 | 17 | if (fs.existsSync('node_modules/react-dom')) { 18 | execSync('cd node_modules && mv react-dom _react-dom'); 19 | console.log('*** Temporary REMOVE local "react-dom" dependencies'); 20 | } 21 | -------------------------------------------------------------------------------- /src/types/FaceDetectionOptions.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from '@mediapipe/camera_utils'; 2 | import { FaceDetection, Options, Results } from '@mediapipe/face_detection'; 3 | 4 | export type FaceDetectionResults = Results; 5 | 6 | export type FaceDetectionOptions = Options; 7 | 8 | export type CameraOptions = { 9 | mediaSrc: HTMLVideoElement; 10 | onFrame: () => Promise; 11 | width: number; 12 | height: number; 13 | }; 14 | 15 | export type ICamera = (cameraOptions: CameraOptions) => Camera; 16 | export interface IFaceDetectionOptions { 17 | mirrored?: boolean; 18 | handleOnResults?: (results: FaceDetectionResults) => void; 19 | faceDetectionOptions?: FaceDetectionOptions; 20 | faceDetection: FaceDetection; 21 | camera?: ICamera; 22 | } 23 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import packageJson from './package.json'; 7 | 8 | export default { 9 | input: 'src/index.ts', 10 | output: [ 11 | { 12 | file: packageJson.main, 13 | format: 'cjs', 14 | sourcemap: true, 15 | }, 16 | ], 17 | plugins: [ 18 | peerDepsExternal(), 19 | commonjs(), 20 | resolve(), 21 | typescript({ 22 | useTsconfigDeclarationDir: true, 23 | tsconfigOverride: { 24 | esModuleInterop: true, 25 | }, 26 | }), 27 | terser(), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.0.2](https://github.com/lauirvin/react-use-face-detection/compare/v1.0.1...v1.0.2) (2024-10-30) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Added cleanup function to useEffect for video resource management ([cbf9be7](https://github.com/lauirvin/react-use-face-detection/commit/cbf9be7104c124d6fcae8d15987bc928f69009ee)) 11 | 12 | ### [1.0.1](https://github.com/lauirvin/react-use-face-detection/compare/v0.0.10...v1.0.1) (2022-04-12) 13 | 14 | # Changelog 15 | 16 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | browser: true, 5 | es6: true, 6 | }, 7 | extends: [ 8 | 'airbnb', 9 | 'plugin:react/recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier/@typescript-eslint', 12 | 'plugin:prettier/recommended', 13 | ], 14 | globals: { 15 | Atomics: 'readonly', 16 | SharedArrayBuffer: 'readonly', 17 | }, 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | ecmaFeatures: { 21 | jsx: true, 22 | }, 23 | ecmaVersion: 202, 24 | sourceType: 'module', 25 | }, 26 | settings: { 27 | react: { 28 | version: 'detect', 29 | }, 30 | }, 31 | plugins: ['react', '@typescript-eslint', 'react-hooks'], 32 | rules: { 33 | 'react/sort-comp': 0, 34 | 'react/jsx-filename-extension': [ 35 | 1, 36 | { 37 | extensions: ['.jsx', '.tsx'], 38 | }, 39 | ], 40 | 'react/no-unescaped-entities': 0, 41 | 'react/require-default-props': 0, 42 | 'react-hooks/exhaustive-deps': 0, 43 | 'no-use-before-define': 0, 44 | 'no-underscore-dangle': 0, 45 | 'no-tabs': 0, 46 | 'no-plusplus': 0, 47 | 'import/extensions': 0, 48 | 'import/no-unresolved': 0, 49 | indent: 'off', 50 | 'class-methods-use-this': 0, 51 | '@typescript-eslint/indent': ['error', 2], 52 | '@typescript-eslint/no-use-before-define': 0, 53 | '@typescript-eslint/interface-name-prefix': 0, 54 | '@typescript-eslint/explicit-function-return-type': 0, 55 | '@typescript-eslint/explicit-member-accessibility': 0, 56 | '@typescript-eslint/camelcase': 0, 57 | 'jsx-a11y/media-has-caption': 0, 58 | 'react/jsx-props-no-spreading': 0, 59 | 'no-nested-ternary': 0, 60 | 'react/no-danger': 0, 61 | 'react/prop-types': 0, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-face-detection", 3 | "version": "1.0.2", 4 | "main": "build/index.js", 5 | "module": "build/index.esm.js", 6 | "files": [ 7 | "build" 8 | ], 9 | "types": "build/index.d.ts", 10 | "description": "Face detection React hook powered by @mediapipe/face_detection, @mediapipe/camera_utils, react-webcam", 11 | "scripts": { 12 | "dev": "rollup -c -w", 13 | "build": "rollup -c", 14 | "test": "jest", 15 | "lint": "eslint '*/**/*.{js,ts,tsx}' --fix", 16 | "test:watch": "jest --watch", 17 | "prepublishOnly": "yarn build", 18 | "add:react": "node scripts/add_react", 19 | "rm:react": "node scripts/remove_react", 20 | "link:react": "yarn link \"react\" \"react-dom\" && yarn rm:react", 21 | "release": "standard-version" 22 | }, 23 | "keywords": [ 24 | "React", 25 | "Library", 26 | "Rollup", 27 | "Typescript", 28 | "Face Detection", 29 | "Face", 30 | "Hooks", 31 | "@mediapipe/face_detection", 32 | "Machine Learning", 33 | "MediaPipe" 34 | ], 35 | "author": "Irvin Ives Lau", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/lauirvin/react-use-face-detection/issues" 39 | }, 40 | "homepage": "https://github.com/lauirvin/react-use-face-detection", 41 | "peerDependencies": { 42 | "react": ">=16.8.0", 43 | "react-dom": ">=16.8.0" 44 | }, 45 | "dependencies": { 46 | "@mediapipe/camera_utils": "^0.3.1640029074", 47 | "@mediapipe/face_detection": "^0.4.1646425229", 48 | "@testing-library/react": "^10.0.2", 49 | "react-webcam": "^7.0.1", 50 | "standard-version": "^9.3.2" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.10.3", 54 | "@rollup/plugin-commonjs": "^11.1.0", 55 | "@rollup/plugin-image": "^2.0.5", 56 | "@rollup/plugin-node-resolve": "^7.1.3", 57 | "@testing-library/jest-dom": "^5.5.0", 58 | "@types/jest": "^24.0.24", 59 | "@types/react": "^16.9.12", 60 | "@types/react-dom": "^16.9.8", 61 | "@typescript-eslint/eslint-plugin": "^4.8.1", 62 | "@typescript-eslint/parser": "^4.8.1", 63 | "babel-loader": "^8.1.0", 64 | "babel-preset-react-app": "^9.1.2", 65 | "eslint": "^7.14.0", 66 | "eslint-config-airbnb": "^18.2.1", 67 | "eslint-config-airbnb-typescript": "^12.0.0", 68 | "eslint-config-prettier": "^6.15.0", 69 | "eslint-plugin-import": "^2.22.1", 70 | "eslint-plugin-jsx-a11y": "^6.4.1", 71 | "eslint-plugin-prettier": "^3.1.4", 72 | "eslint-plugin-react": "^7.21.5", 73 | "eslint-plugin-react-hooks": "^4.2.0", 74 | "husky": "^4.3.0", 75 | "identity-obj-proxy": "^3.0.0", 76 | "jest": "^24.9.0", 77 | "lint-staged": "^10.5.2", 78 | "prettier": "2.2.0", 79 | "react": "^16.14.0", 80 | "react-dom": "^16.14.0", 81 | "rollup": "^1.27.4", 82 | "rollup-plugin-copy": "^3.3.0", 83 | "rollup-plugin-peer-deps-external": "^2.2.0", 84 | "rollup-plugin-terser": "^7.0.2", 85 | "rollup-plugin-typescript2": "^0.27.0", 86 | "ts-jest": "^26.4.4", 87 | "typescript": "^3.7.2" 88 | }, 89 | "husky": { 90 | "hooks": { 91 | "pre-commit": "yarn lint && yarn build && yarn prettier --write '*/**/*.{js,ts,tsx}'", 92 | "pre-push": "" 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/hooks/useFaceDetection.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react'; 2 | import Webcam from 'react-webcam'; 3 | import { Results, NormalizedRect } from '@mediapipe/face_detection'; 4 | import { IFaceDetectionOptions, IFaceDetectionReturnType } from '../types'; 5 | 6 | export const useFaceDetection = (props?: IFaceDetectionOptions): IFaceDetectionReturnType => { 7 | const { 8 | mirrored, 9 | handleOnResults, 10 | faceDetectionOptions: options, 11 | faceDetection: faceDetectionInitializer, 12 | camera: cameraInitializer, 13 | } = props || ({} as IFaceDetectionOptions); 14 | 15 | /** Bounding Box for element to use, e.g. can create a bounding box with these values using a div */ 16 | const [boundingBox, setBoundingBox] = useState([]); 17 | const [isLoading, setIsLoading] = useState(true); 18 | 19 | /** Refs */ 20 | const webcamRef = useRef(null); 21 | const imgRef = useRef(null); 22 | const camera = useRef(cameraInitializer).current; 23 | const faceDetection = useRef(faceDetectionInitializer).current; 24 | const faceDetectionOptions = useRef(options); 25 | 26 | const onResults = useCallback( 27 | (results: Results) => { 28 | /** Callback to return detection results */ 29 | if (handleOnResults) handleOnResults(results); 30 | 31 | const { detections } = results; 32 | 33 | /** Set bounding box data */ 34 | const boundingBoxes = detections.map((detection) => { 35 | const xCenter = detection.boundingBox.xCenter - detection.boundingBox.width / 2; 36 | return { 37 | ...detection.boundingBox, 38 | yCenter: detection.boundingBox.yCenter - detection.boundingBox.height / 2, 39 | xCenter: mirrored ? 1 - xCenter : xCenter, 40 | }; 41 | }); 42 | 43 | setBoundingBox(boundingBoxes); 44 | }, 45 | [handleOnResults, mirrored], 46 | ); 47 | 48 | const handleFaceDetection = useCallback( 49 | async (mediaSrc: HTMLVideoElement | HTMLImageElement) => { 50 | /** Configure faceDetection usage/options */ 51 | faceDetection.setOptions({ ...faceDetectionOptions.current }); 52 | faceDetection.onResults(onResults); 53 | 54 | /** Handle webcam detection */ 55 | if (mediaSrc instanceof HTMLVideoElement && camera) { 56 | const cameraConfig = { 57 | mediaSrc, 58 | width: mediaSrc.videoWidth, 59 | height: mediaSrc.videoHeight, 60 | onFrame: async () => { 61 | await faceDetection.send({ image: mediaSrc }); 62 | if (isLoading) setIsLoading(false); 63 | }, 64 | }; 65 | 66 | camera(cameraConfig).start(); 67 | } 68 | 69 | /** Handle image face detection */ 70 | if (mediaSrc instanceof HTMLImageElement) { 71 | await faceDetection.send({ image: mediaSrc }); 72 | if (isLoading) setIsLoading(false); 73 | } 74 | }, 75 | [camera, faceDetection, isLoading, onResults], 76 | ); 77 | 78 | useEffect(() => { 79 | if (webcamRef.current && webcamRef.current.video) { 80 | handleFaceDetection(webcamRef.current.video); 81 | } 82 | 83 | if (imgRef.current) { 84 | handleFaceDetection(imgRef.current); 85 | } 86 | 87 | const videoElement = webcamRef?.current?.video; 88 | 89 | return () => { 90 | if (!videoElement) return; 91 | 92 | if (videoElement && videoElement.srcObject) { 93 | const stream = videoElement.srcObject as MediaStream; 94 | if (stream) { 95 | stream.getTracks().forEach((track) => track.stop()); 96 | } 97 | } 98 | 99 | if (!camera) return; 100 | 101 | camera({ 102 | mediaSrc: videoElement, 103 | onFrame: () => Promise.resolve(), 104 | width: videoElement.videoWidth, 105 | height: videoElement.videoHeight, 106 | }).stop(); 107 | }; 108 | }, [handleFaceDetection, isLoading, onResults, camera]); 109 | 110 | return { 111 | boundingBox, 112 | isLoading, 113 | detected: boundingBox.length > 0, 114 | facesDetected: boundingBox.length, 115 | webcamRef, 116 | imgRef, 117 | }; 118 | }; 119 | 120 | export default useFaceDetection; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | logo 4 | 5 |

6 |

7 | useFaceDetection Hook 8 |

9 | 10 | #

[✨ Live Demo ✨](https://serene-meerkat-95a23d.netlify.app/)

11 | 12 | ## 📚 Introduction 13 | 14 | Simple React Hook to detect faces from an HTMLImageElement or [react-webcam](https://www.npmjs.com/package/react-webcam) video source. Powered by [MediaPipe](https://google.github.io/mediapipe/) 15 | 16 | ## 💡 Usage 17 | 18 | - [Image Face Detection Code Example](https://github.com/lauirvin/react-use-face-detection-demo/blob/master/src/components/ImageDemo.tsx) 19 | 20 | - [React Webcam Face Detection Code Example](https://github.com/lauirvin/react-use-face-detection-demo/blob/master/src/components/WebcamDemo.tsx) 21 | 22 | - [Project Example](https://github.com/lauirvin/react-use-face-detection-demo) 23 | 24 | ## 🧰 Installation 25 | 26 | To install, you can use [npm](https://npmjs.org/) or [yarn](https://yarnpkg.com): 27 | 28 | **npm** 29 | 30 | ```sh 31 | npm install react-use-face-detection 32 | ``` 33 | 34 | **yarn** 35 | 36 | ```sh 37 | yarn add react-use-face-detection 38 | ``` 39 | 40 | ## ⚙️ Options 41 | 42 | | Name | Type | Optional | Description | 43 | | -------------------- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 44 | | mirrored | boolean | true | This enables a mirrored detection of the faces in the provided media source - e.g. if you flip the media source horizontally, this would enable the correct output of your flipped media source. | 45 | | handleOnResults | (results: FaceDetectionResults) => void | true | Callback function that returns the FaceDetectionResults | 46 | | faceDetectionOptions | FaceDetectionOptions | true | Options for configuring the face detection model - see https://google.github.io/mediapipe/solutions/face_detection.html#javascript-solution-api | 47 | | faceDetection | FaceDetection | false | Initialize the face detection model from @mediapipe/face_detection | 48 | | camera | (cameraOptions: CameraOptions) => Camera | true | Initialize the camera utility model from @mediapipe/camera_utils | 49 | 50 | ## 🎁 Returned Values 51 | 52 | | Name | Type | Description | 53 | | ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------- | 54 | | boundingBox | BoundingBox[] | Returns details and coordinates of the bounding box around the detected face(s) | 55 | | isLoading | boolean | Returns a boolean that detects whether if the model has been loaded | 56 | | detected | boolean | Returns a boolean that detects whether if a face has been detected | 57 | | facesDetected | number | Returns a number of faces that have been detected | 58 | | webcamRef | LegacyRef | Returns a ref object for the [react-webcam](https://www.npmjs.com/package/react-webcam) `` node | 59 | | imgRef | RefObject | Returns a ref object for the `` element | 60 | 61 | ## 👷 Built With 62 | 63 | - [ReactJS](https://reactjs.org/) - Frontend Javascript web framework 64 | - [TypeScript](https://www.typescriptlang.org/) - Open-source language which builds on JavaScript 65 | - [MediaPipe](https://google.github.io/mediapipe/) - Machine Learning framework 66 | - [React Webcam](https://www.npmjs.com/package/react-webcam) - Webcam Library 67 | 68 | ## 📚 Author 69 | 70 | - **Irvin Ives Lau** - [lauirvin](https://github.com/lauirvin) 71 | - https://www.irviniveslau.com 72 | --------------------------------------------------------------------------------