├── env.d.ts
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.yml
├── FUNDING.yml
└── workflows
│ └── static.yml
├── public
└── favicon.ico
├── readme_assets
├── install_guide.png
└── restart_ui_guide.png
├── .prettierrc.json
├── tsconfig.vitest.json
├── main.d.ts
├── preload.py
├── tsconfig.node.json
├── zip_dist.js
├── src
├── assets
│ ├── logo.svg
│ ├── main.css
│ └── base.css
├── components
│ ├── icons
│ │ ├── IconSupport.vue
│ │ ├── IconTooling.vue
│ │ ├── IconCommunity.vue
│ │ ├── IconDocumentation.vue
│ │ └── IconEcosystem.vue
│ ├── IconSwitch.vue
│ ├── LockSwitch.vue
│ ├── VisibleSwitch.vue
│ ├── GroupSwitch.vue
│ ├── __tests__
│ │ └── Openpose.spec.ts
│ ├── Header.vue
│ ├── FlipOutlined.vue
│ └── OpenposeObjectPanel.vue
├── stores
│ └── counter.ts
├── main.ts
├── Notification.ts
├── i18n.ts
├── Openpose.ts
└── App.vue
├── tsconfig.json
├── tsconfig.app.json
├── .eslintrc.cjs
├── vitest.config.ts
├── .gitignore
├── index.html
├── vite.config.ts
├── LICENSE
├── process_keypoints.py
├── package.json
├── README.zh.md
├── README.ja.md
├── scripts
└── openpose_editor.py
├── output_pose.json
├── pose_hand_fixed.json
└── README.md
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenlei/sd-webui-openpose-editor/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/readme_assets/install_guide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenlei/sd-webui-openpose-editor/HEAD/readme_assets/install_guide.png
--------------------------------------------------------------------------------
/readme_assets/restart_ui_guide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenlei/sd-webui-openpose-editor/HEAD/readme_assets/restart_ui_guide.png
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "printWidth": 100,
7 | "trailingComma": "none"
8 | }
--------------------------------------------------------------------------------
/tsconfig.vitest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "exclude": [],
4 | "compilerOptions": {
5 | "composite": true,
6 | "lib": [],
7 | "types": ["node", "jsdom"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/main.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | interface DataFromServer {
4 | image_url: string;
5 | pose: string;
6 | }
7 |
8 | declare global {
9 | interface Window {
10 | dataFromServer: DataFromServer;
11 | }
12 | }
--------------------------------------------------------------------------------
/preload.py:
--------------------------------------------------------------------------------
1 | def preload(parser):
2 | parser.add_argument(
3 | "--disable-openpose-editor-auto-update",
4 | action='store_true',
5 | help="Disable auto-update of openpose editor",
6 | default=None,
7 | )
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.node.json",
3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*", "package.json"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/zip_dist.js:
--------------------------------------------------------------------------------
1 | const zipdir = require('zip-dir');
2 |
3 | zipdir('./dist', { saveTo: './dist.zip' }, function (err, buffer) {
4 | if (err) {
5 | console.error('Error zipping "dist" directory:', err);
6 | } else {
7 | console.log('Successfully zipped "dist" directory.');
8 | }
9 | });
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "compilerOptions": {
4 | "lib": ["es2017"],
5 | },
6 | "references": [
7 | {
8 | "path": "./tsconfig.node.json"
9 | },
10 | {
11 | "path": "./tsconfig.app.json"
12 | },
13 | {
14 | "path": "./tsconfig.vitest.json"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/icons/IconSupport.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.web.json",
3 | "include": ["env.d.ts", "main.d.ts", "src/**/*", "src/**/*.vue"],
4 | "exclude": ["src/**/__tests__/*"],
5 | "compilerOptions": {
6 | "composite": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "@/*": ["./src/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/stores/counter.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue'
2 | import { defineStore } from 'pinia'
3 |
4 | export const useCounterStore = defineStore('counter', () => {
5 | const count = ref(0)
6 | const doubleCount = computed(() => count.value * 2)
7 | function increment() {
8 | count.value++
9 | }
10 |
11 | return { count, doubleCount, increment }
12 | })
13 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution')
3 |
4 | module.exports = {
5 | root: true,
6 | 'extends': [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/eslint-config-typescript',
10 | '@vue/eslint-config-prettier/skip-formatting'
11 | ],
12 | parserOptions: {
13 | ecmaVersion: 'latest'
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url'
2 | import { mergeConfig } from 'vite'
3 | import { configDefaults, defineConfig } from 'vitest/config'
4 | import viteConfig from './vite.config'
5 |
6 | export default mergeConfig(
7 | viteConfig,
8 | defineConfig({
9 | test: {
10 | environment: 'jsdom',
11 | exclude: [...configDefaults.exclude, 'e2e/*'],
12 | root: fileURLToPath(new URL('./', import.meta.url))
13 | }
14 | })
15 | )
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
30 | # Python
31 | __pycache__/
32 | *.pyc
33 |
34 | dist.zip
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Openpose Editor
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import Antd from 'ant-design-vue'
3 | import { createPinia } from 'pinia'
4 | import App from './App.vue'
5 | import NotificationPlugin from './Notification';
6 | import i18n from './i18n';
7 |
8 | const urlParams = new URLSearchParams(window.location.search);
9 | const theme = urlParams.get('theme');
10 | if (theme === 'dark') {
11 | import('ant-design-vue/dist/antd.dark.css');
12 | document.body.classList.add('dark-theme');
13 | } else {
14 | import('ant-design-vue/dist/antd.css');
15 | }
16 |
17 | import './assets/main.css'
18 |
19 | const app = createApp(App);
20 |
21 | app.use(createPinia())
22 | .use(NotificationPlugin)
23 | .use(Antd)
24 | .use(i18n)
25 | .mount('#app');
26 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: huchenlei
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/src/components/IconSwitch.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/Notification.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue';
2 | import { message, notification } from 'ant-design-vue';
3 |
4 | declare module '@vue/runtime-core' {
5 | interface ComponentCustomProperties {
6 | $message: typeof message;
7 | $notify: (params: string | { title: string; desc: string; [key: string]: any }) => void;
8 | }
9 | }
10 |
11 | export default {
12 | install: (app: App, options?: any) => {
13 | app.config.globalProperties.$message = message;
14 | app.config.globalProperties.$notify = (params) => {
15 | if (typeof params === 'string') {
16 | notification.error({
17 | message: params,
18 | });
19 | } else {
20 | notification.error({
21 | message: params.title,
22 | description: params.desc,
23 | ...params,
24 | });
25 | }
26 | };
27 | },
28 | };
--------------------------------------------------------------------------------
/src/components/LockSwitch.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/VisibleSwitch.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 | import { writeFileSync } from 'node:fs'
3 |
4 | import { defineConfig } from 'vite'
5 | import vue from '@vitejs/plugin-vue'
6 | import vueJsx from '@vitejs/plugin-vue-jsx'
7 | import packageJson from './package.json'
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | base: process.env.GITHUB_PAGES_PATH || (process.env.NODE_ENV === 'production'
12 | ? '/openpose_editor/'
13 | : '/'),
14 | plugins: [
15 | vue(),
16 | vueJsx(),
17 | {
18 | name: 'create-version-file',
19 | apply: 'build',
20 | writeBundle() {
21 | writeFileSync('dist/version.txt', `v${packageJson.version}`);
22 | },
23 | }
24 | ],
25 | resolve: {
26 | alias: {
27 | '@': fileURLToPath(new URL('./src', import.meta.url))
28 | }
29 | },
30 | })
31 |
--------------------------------------------------------------------------------
/src/components/GroupSwitch.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/icons/IconTooling.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/__tests__/Openpose.spec.ts:
--------------------------------------------------------------------------------
1 | import { OpenposeObject, OpenposeKeypoint2D, OpenposeConnection } from '../../Openpose';
2 | import { fabric } from 'fabric';
3 | import {describe, it, expect} from 'vitest'
4 |
5 | describe('OpenposeObject', () => {
6 | it.each([
7 | new OpenposeKeypoint2D(-1, 1, 1.0, 'rgb(0, 0, 0)', 'name'),
8 | new OpenposeKeypoint2D(1, 1, 0.0, 'rgb(0, 0, 0)', 'name'),
9 | new OpenposeKeypoint2D(1, -1, 1.0, 'rgb(0, 0, 0)', 'name'),
10 | ])('Should set invalid keypoints invisible', (invalid_keypoint: OpenposeKeypoint2D) => {
11 | const object = new OpenposeObject([invalid_keypoint], []);
12 | expect(object.keypoints[0].visible).toBeFalsy();
13 | });
14 |
15 | it.each([
16 | new OpenposeKeypoint2D(1, 1, 1.0, 'rgb(0, 0, 0)', 'name'),
17 | new OpenposeKeypoint2D(100, 1, 1.0, 'rgb(0, 0, 0)', 'name'),
18 | ])('Should set valid keypoints visible', (valid_keypoint: OpenposeKeypoint2D) => {
19 | const object = new OpenposeObject([valid_keypoint], []);
20 | expect(object.keypoints[0].visible).toBeTruthy();
21 | });
22 | });
--------------------------------------------------------------------------------
/src/components/icons/IconCommunity.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Chenlei Hu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/icons/IconDocumentation.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/process_keypoints.py:
--------------------------------------------------------------------------------
1 | """
2 | Used to process keypoints generated from ControlNet extension.
3 | """
4 | import json
5 | from typing import List, Tuple
6 |
7 | def process_keypoints(nums: List[float], width: int, height: int) -> List[List[float]]:
8 | if not nums:
9 | return []
10 |
11 | assert len(nums) % 3 == 0
12 |
13 | def find_min(nums: float):
14 | return min(num for num in nums if num > 0)
15 |
16 | base_x = find_min(nums[::3])
17 | base_y = find_min(nums[1::3])
18 |
19 | normalized = all(abs(num) <= 1.0 for num in nums)
20 | x_factor = width if normalized else 1.0
21 | y_factor = height if normalized else 1.0
22 |
23 | return [
24 | [(x-base_x) * x_factor, (y-base_y) * y_factor, c]
25 | for x, y, c in zip(nums[::3], nums[1::3], nums[2::3])
26 | ]
27 |
28 |
29 |
30 | if __name__ == '__main__':
31 | with open('pose_hand_fixed.json', 'r') as f:
32 | pose = json.load(f)
33 | person = pose["people"][0]
34 |
35 | with open('output_pose.json', 'w') as f:
36 | width = pose['canvas_width']
37 | height = pose['canvas_height']
38 |
39 | json.dump({
40 | 'left_hand': process_keypoints(person.get('hand_left_keypoints_2d'), width, height),
41 | 'right_hand': process_keypoints(person.get('hand_right_keypoints_2d'), width, height),
42 | 'face': process_keypoints(person.get('face_keypoints_2d'), width, height),
43 | }, f, indent=4)
44 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ['main']
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow one concurrent deployment
19 | concurrency:
20 | group: 'pages'
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | # Single deploy job since we're just deploying
25 | deploy:
26 | environment:
27 | name: github-pages
28 | url: ${{ steps.deployment.outputs.page_url }}
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v3
33 | - name: Set up Node
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version: 18
37 | cache: 'npm'
38 | - name: Install dependencies
39 | run: npm install
40 | - name: Install npm-run-all
41 | run: npm install -g npm-run-all
42 | - name: Build
43 | run: npm run build
44 | env:
45 | GITHUB_PAGES_PATH: '/sd-webui-openpose-editor/'
46 | - name: Setup Pages
47 | uses: actions/configure-pages@v3
48 | - name: Upload artifact
49 | uses: actions/upload-pages-artifact@v1
50 | with:
51 | # Upload dist repository
52 | path: './dist'
53 | - name: Deploy to GitHub Pages
54 | id: deployment
55 | uses: actions/deploy-pages@v1
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sd-webui-openpose-editor",
3 | "version": "0.3.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "run-p type-check build-only",
8 | "preview": "vite preview",
9 | "test:unit": "vitest",
10 | "build-only": "vite build",
11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
13 | "format": "prettier --write src/",
14 | "zip-dist": "node zip_dist.js"
15 | },
16 | "dependencies": {
17 | "@ant-design/icons-vue": "^6.1.0",
18 | "ant-design-vue": "^3.2.20",
19 | "crypto-js": "^4.1.1",
20 | "fabric": "^5.3.0",
21 | "lodash": "^4.17.21",
22 | "pinia": "^2.0.32",
23 | "vue": "^3.2.47",
24 | "vue-i18n": "^9.2.2"
25 | },
26 | "devDependencies": {
27 | "@rushstack/eslint-patch": "^1.2.0",
28 | "@types/crypto-js": "^4.1.1",
29 | "@types/fabric": "^5.3.2",
30 | "@types/jsdom": "^21.1.0",
31 | "@types/lodash": "^4.14.194",
32 | "@types/node": "^18.14.2",
33 | "@vitejs/plugin-vue": "^4.0.0",
34 | "@vitejs/plugin-vue-jsx": "^3.0.0",
35 | "@vue/eslint-config-prettier": "^7.1.0",
36 | "@vue/eslint-config-typescript": "^11.0.2",
37 | "@vue/test-utils": "^2.3.0",
38 | "@vue/tsconfig": "^0.1.3",
39 | "eslint": "^8.34.0",
40 | "eslint-plugin-vue": "^9.9.0",
41 | "jsdom": "^21.1.0",
42 | "npm-run-all": "^4.1.5",
43 | "prettier": "^2.8.4",
44 | "typescript": "~4.8.4",
45 | "vite": "^4.1.4",
46 | "vitest": "^0.29.1",
47 | "vue-tsc": "^1.2.0",
48 | "zip-dir": "^2.0.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @import './base.css';
2 |
3 | .visible-switch,
4 | .group-switch,
5 | .unjam-button,
6 | .lock-switch {
7 | padding: 3px;
8 | }
9 |
10 | .ant-collapse-header {
11 | align-items: center !important;
12 | justify-content: flex-start;
13 | }
14 |
15 | .ant-list-item {
16 | justify-content: flex-start;
17 | }
18 |
19 | /* Right align last item in the flex container with `flex-start` */
20 | .close-icon,
21 | .coords-group,
22 | .scale-ratio-input {
23 | margin-left: auto;
24 | }
25 |
26 | .ant-col {
27 | padding: 10px;
28 | }
29 |
30 | .image-thumbnail {
31 | width: 50px;
32 | height: 50px;
33 | object-fit: cover;
34 | margin-right: 8px;
35 | }
36 |
37 | .uploaded-file-item>div {
38 | display: flex;
39 | justify-content: flex-start;
40 | align-items: center;
41 | }
42 |
43 | .ant-space {
44 | margin-bottom: 5px;
45 | }
46 |
47 | li.keypoint-selected {
48 | background-color: #ffdddd;
49 | /* A light red for the background */
50 | color: #b20000;
51 | /* A darker red for the text */
52 | }
53 |
54 | .dark-theme li.keypoint-selected {
55 | color: #ffdddd;
56 | /* A light red for the text */
57 | background-color: #5e0303;
58 | /* A darker red for the background */
59 | }
60 |
61 | /* Make control-panel scrollable instead of whole page scrollable when
62 | control-panel's content overflows. */
63 | #control-panel {
64 | height: 100vh;
65 | overflow-y: auto;
66 | }
67 |
68 | /* Hide scroll bar for control-panel. */
69 | #control-panel::-webkit-scrollbar {
70 | display: none;
71 | }
72 |
73 | .ant-space {
74 | flex-wrap: wrap;
75 | }
76 |
77 | .ant-input-number {
78 | max-width: 100px;
79 | }
--------------------------------------------------------------------------------
/src/components/icons/IconEcosystem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 | SD-WEBUI-OPENPOSE-EDITOR
46 |
48 | {{ stars }}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/FlipOutlined.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/assets/base.css:
--------------------------------------------------------------------------------
1 | /* color palette from */
2 | :root {
3 | --vt-c-white: #ffffff;
4 | --vt-c-white-soft: #f8f8f8;
5 | --vt-c-white-mute: #f2f2f2;
6 |
7 | --vt-c-black: #181818;
8 | --vt-c-black-soft: #222222;
9 | --vt-c-black-mute: #282828;
10 |
11 | --vt-c-indigo: #2c3e50;
12 |
13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17 |
18 | --vt-c-text-light-1: var(--vt-c-indigo);
19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20 | --vt-c-text-dark-1: var(--vt-c-white);
21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22 | }
23 |
24 | /* semantic color variables for this project */
25 | :root {
26 | --color-background: var(--vt-c-white);
27 | --color-background-soft: var(--vt-c-white-soft);
28 | --color-background-mute: var(--vt-c-white-mute);
29 |
30 | --color-border: var(--vt-c-divider-light-2);
31 | --color-border-hover: var(--vt-c-divider-light-1);
32 |
33 | --color-heading: var(--vt-c-text-light-1);
34 | --color-text: var(--vt-c-text-light-1);
35 |
36 | --section-gap: 160px;
37 | }
38 |
39 | body.dark-theme {
40 | --color-background: var(--vt-c-black);
41 | --color-background-soft: var(--vt-c-black-soft);
42 | --color-background-mute: var(--vt-c-black-mute);
43 |
44 | --color-border: var(--vt-c-divider-dark-2);
45 | --color-border-hover: var(--vt-c-divider-dark-1);
46 |
47 | --color-heading: var(--vt-c-text-dark-1);
48 | --color-text: var(--vt-c-text-dark-1);
49 | }
50 |
51 | @media (prefers-color-scheme: dark) {
52 | :root {
53 | --color-background: var(--vt-c-black);
54 | --color-background-soft: var(--vt-c-black-soft);
55 | --color-background-mute: var(--vt-c-black-mute);
56 |
57 | --color-border: var(--vt-c-divider-dark-2);
58 | --color-border-hover: var(--vt-c-divider-dark-1);
59 |
60 | --color-heading: var(--vt-c-text-dark-1);
61 | --color-text: var(--vt-c-text-dark-2);
62 | }
63 | }
64 |
65 | *,
66 | *::before,
67 | *::after {
68 | box-sizing: border-box;
69 | margin: 0;
70 | position: relative;
71 | font-weight: normal;
72 | }
73 |
74 | body {
75 | min-height: 100vh;
76 | color: var(--color-text);
77 | background: var(--color-background);
78 | transition: color 0.5s, background-color 0.5s;
79 | line-height: 1.6;
80 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
81 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
82 | font-size: 15px;
83 | text-rendering: optimizeLegibility;
84 | -webkit-font-smoothing: antialiased;
85 | -moz-osx-font-smoothing: grayscale;
86 | }
87 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report
3 | title: "[Bug]: "
4 | labels: ["bug-report"]
5 |
6 | body:
7 | - type: checkboxes
8 | attributes:
9 | label: Is there an existing issue for this?
10 | description: Please search to see if an issue already exists for the bug you encountered, and that it hasn't been fixed in a recent build/commit.
11 | options:
12 | - label: I have searched the existing issues and checked the recent builds/commits of both this extension and the webui
13 | required: true
14 | - type: markdown
15 | attributes:
16 | value: |
17 | *Please fill this form with as much information as possible, don't forget to fill "What OS..." and "What browsers" and *provide screenshots if possible**
18 | - type: textarea
19 | id: what-did
20 | attributes:
21 | label: What happened?
22 | description: Tell us what happened in a very clear and simple way
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: steps
27 | attributes:
28 | label: Steps to reproduce the problem
29 | description: Please provide us with precise step by step information on how to reproduce the bug. Ideally with image/json uploaded to help debug the issue.
30 | value: |
31 | 1. Go to ....
32 | 2. Press ....
33 | 3. ...
34 | validations:
35 | required: true
36 | - type: textarea
37 | id: what-should
38 | attributes:
39 | label: What should have happened?
40 | description: Tell what you think the normal behavior should be
41 | validations:
42 | required: true
43 | - type: textarea
44 | id: commits
45 | attributes:
46 | label: Commit where the problem happens
47 | description: Which commit of the extension are you running on? Please include the commit of both the extension and the webui (Do not write *Latest version/repo/commit*, as this means nothing and will have changed by the time we read your issue. Rather, copy the **Commit** link at the bottom of the UI, or from the cmd/terminal if you can't launch it.)
48 | value: |
49 | webui:
50 | controlnet:
51 | openpose-editor:
52 | validations:
53 | required: true
54 | - type: dropdown
55 | id: browsers
56 | attributes:
57 | label: What browsers do you use to access the UI ?
58 | multiple: true
59 | options:
60 | - Mozilla Firefox
61 | - Google Chrome
62 | - Brave
63 | - Apple Safari
64 | - Microsoft Edge
65 | - type: textarea
66 | id: cmdargs
67 | attributes:
68 | label: Command Line Arguments
69 | description: Are you using any launching parameters/command line arguments (modified webui-user .bat/.sh) ? If yes, please write them below. Write "No" otherwise.
70 | render: Shell
71 | validations:
72 | required: true
73 | - type: textarea
74 | id: logs
75 | attributes:
76 | label: Console logs
77 | description: Please provide full cmd/terminal logs from the moment you started UI to the end of it, after your bug happened. If it's very long, provide a link to pastebin or similar service.
78 | render: Shell
79 | validations:
80 | required: true
81 | - type: textarea
82 | id: browserlogs
83 | attributes:
84 | label: Browser logs
85 | description: Please provide full browser logs from the moment you started UI to the end of it, after your bug happened. If it's very long, provide a link to pastebin or similar service.
86 | validations:
87 | required: true
88 | - type: textarea
89 | id: misc
90 | attributes:
91 | label: Additional information
92 | description: Please provide us with any relevant additional info or context.
93 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # Openpose Editor for ControlNet in Stable Diffusion WebUI
2 |
3 | 这个扩展专门为整合到 Stable Diffusion WebUI 的 ControlNet 扩展中而设计。
4 |
5 | 
6 |
7 | # 外部环境
8 | 需要预先安装[ControlNet](https://github.com/Mikubill/sd-webui-controlnet) `1.1.216`+.
9 |
10 | # 安装
11 | 从ControlNet扩展v1.1.411开始,用户不再需要在本地安装此扩展,因为ControlNet扩展现在使用远程端点https://huchenlei.github.io/sd-webui-openpose-editor/,如果未检测到本地编辑器安装。如果您的互联网连接差,或者连接到github.io域名有困难,仍然建议您在本地进行安装。
12 |
13 | ## 本地安装
14 | 
15 | 
16 |
17 | 在 UI 重启后,此扩展将尝试从 Github 下载编译好的 Vue 应用程序。请检查 `stable-diffusion-webui\extensions\sd-webui-openpose-editor\dist` 是否存在且包含内容。
18 |
19 | 中国大陆的一些用户报告了使用自动更新脚本下载 dist 时遇到的问题。在这种情况下,用户有两种手动获取 dist 的方法:
20 |
21 | ### 选项1:构建应用程序
22 | 确保你已经准备好了 nodeJS 环境并遵循 `Development` 部分的步骤。运行 `npm run build` 来编译应用程序。
23 |
24 | ### 选项2:下载编译好的应用程序
25 | 你可以从 [发布](https://github.com/huchenlei/sd-webui-openpose-editor/releases) 页面下载编译好的应用程序(`dist.zip`)。在仓库的根目录解压该包,确保解压后的目录命名为 `dist`。
26 |
27 | # 使用
28 | Openpose 编辑器核心是使用 Vue3 构建的。gradio 扩展脚本是一个轻量级的包装器,它将 Vue3 应用程序挂载在 `/openpose_editor_index` 上。
29 |
30 | 用户可以直接在 `localhost:7860/openpose_editor_index` 访问编辑器,如果需要,但主要的入口点是在 ControlNet 扩展中调用编辑器。在 ControlNet 扩展中,选择任何 openpose 预处理器,然后点击运行预处理器按钮。将会生成一个预处理器结果预览。点击生成图像右下角的 `Edit` 按钮将会在一个模态中打开 openpose 编辑器。编辑后,点击 `Send pose to ControlNet` 按钮会将姿势发送回 ControlNet。
31 |
32 | 以下演示展示了基本的工作流程:
33 |
34 | [](http://www.youtube.com/watch?v=WEHVpPNIh8M)
35 |
36 | # 特性
37 | 1. 支持在 controlnet 中使用的面部/手部。
38 | - 该扩展能识别 controlnet 预处理结果中的面部/手部对象。
39 | - 如果预处理结果中遗漏了它们,用户可以添加面部/手部。可以通过以下两种方式实现:
40 | - 添加默认的手(面部不支持,因为面部的关键点太多(70个关键点),这使得手动调整它们变得非常困难。)
41 | - 通过上传一个姿势 JSON 来添加对象。将会使用第一人称的相应对象。
42 | 2. 可视性切换
43 | - 如果 ControlNet 预处理器无法识别一个关键点,它将会有 `(-1, -1)` 作为坐标。这种无效的关键点在编辑器中将被设置为不可见。
44 | - 如果用户将一个关键点设置为不可见并将姿势发送回 controlnet,该关键点连接的肢体段将不会被渲染。实际上,这就是你在编辑器中移除一个肢体段的方式。
45 | 3. 组切换
46 | - 如果你不想意外地选择和修改一个画布对象(手/面部/身体)的关键点。你可以将它们分组。分组后的对象会表现得像一个单一的对象。你可以对组进行缩放、旋转、扭曲。
47 |
48 | # Development
49 | ## Recommended IDE Setup
50 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
51 |
52 | ## Type Support for `.vue` Imports in TS
53 |
54 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
55 |
56 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
57 |
58 | 1. Disable the built-in TypeScript Extension
59 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
60 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
61 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
62 |
63 | ## Customize configuration
64 |
65 | See [Vite Configuration Reference](https://vitejs.dev/config/).
66 |
67 | ## Project Setup
68 |
69 | ```sh
70 | npm install
71 | ```
72 |
73 | ### Compile and Hot-Reload for Development
74 |
75 | ```sh
76 | npm run dev
77 | ```
78 |
79 | ### Type-Check, Compile and Minify for Production
80 |
81 | ```sh
82 | npm run build
83 | ```
84 |
85 | ### Run Unit Tests with [Vitest](https://vitest.dev/)
86 |
87 | ```sh
88 | npm run test:unit
89 | ```
90 |
91 | ### Lint with [ESLint](https://eslint.org/)
92 |
93 | ```sh
94 | npm run lint
95 | ```
96 |
--------------------------------------------------------------------------------
/README.ja.md:
--------------------------------------------------------------------------------
1 | # Stable Diffusion WebUIのためのControlNet内Openposeエディタ
2 | この拡張機能は、特にStable Diffusion WebUIのControlNet拡張機能に統合するために開発されました。
3 |
4 | 
5 |
6 | # 前提条件
7 | [ControlNet](https://github.com/Mikubill/sd-webui-controlnet) `1.1.216`以上が必要です。
8 |
9 | # インストール
10 | ControlNet拡張v1.1.411から、ユーザーはこの拡張をローカルにインストールする必要はありません。ControlNet拡張は、ローカルのエディタのインストールが検出されない場合、https://huchenlei.github.io/sd-webui-openpose-editor/ というリモートエンドポイントを使用します。インターネット接続が不安定、またはgithub.ioドメインへの接続に問題がある場合は、ローカルインストールを推奨します。
11 |
12 | ## ローカルインストール
13 | 
14 | 
15 |
16 | UI再起動時、拡張機能はコンパイル済みのVueアプリをGithubからダウンロードしようとします。`stable-diffusion-webui\extensions\sd-webui-openpose-editor\dist`が存在し、中に内容があるかどうかを確認してください。
17 |
18 | 中国の一部のユーザーは、autoupdateスクリプトでdistのダウンロードに問題があると報告しています。そのような状況では、ユーザーは次の2つのオプションからdistを手動で取得することができます:
19 |
20 | ### オプション1:アプリケーションのビルド
21 | nodeJS環境が準備できていることを確認し、`Development`セクションに従ってください。アプリケーションをコンパイルするために`npm run build`を実行します。
22 |
23 | ### オプション2:コンパイル済みアプリケーションのダウンロード
24 | [リリース](https://github.com/huchenlei/sd-webui-openpose-editor/releases)ページからコンパイル済みアプリケーション(`dist.zip`)をダウンロードできます。リポジトリのルートでパッケージを解凍し、解凍したディレクトリが`dist`という名前であることを確認してください。
25 |
26 | # 使用法
27 | OpenposeエディタのコアはVue3で構築されています。Gradio拡張スクリプトは、`/openpose_editor_index`上にVue3アプリケーションをマウントする薄いラッパーです。
28 |
29 | 必要であれば、ユーザーは`localhost:7860/openpose_editor_index`でエディタに直接アクセスできますが、主なエントリーポイントはControlNet拡張機能内でエディタを呼び出すことです。ControlNet拡張機能で、任意のopenpose前処理器を選択し、前処理器実行ボタンを押します。前処理結果のプレビューが生成されます。生成された画像の右下隅の`編集`ボタンをクリックすると、モーダル内にopenposeエディタが表示されます。編集後、`ControlNetにポーズを送信`ボタンをクリックすると、ポーズがControlNetに送り返されます。
30 |
31 | 以下のデモは基本的なワークフローを示しています:
32 |
33 | [](http://www.youtube.com/watch?v=WEHVpPNIh8M)
34 |
35 | # 特長
36 | 1. controlnetで使用される顔/手のサポート。
37 | - 拡張機能はcontrolnetの前処理結果で顔/手のオブジェクトを認識します。
38 | - ユーザーは、前処理結果がそれらを欠落している場合に顔/手を追加することができます。これは以下のどちらかで行うことができます。
39 | - デフォルトの手を追加する(顔はキーポイントが多すぎる(70キーポイント)ため、手動でそれらを調整することは非常に困難なため、サポートされていません。)
40 | - ポーズJSONをアップロードしてオブジェクトを追加する。最初の人の対応するオブジェクトが使用されます。
41 | 1. 可視性の切り替え
42 | - キーポイントがControlNet前処理器に認識されない場合、その座標は`(-1, -1)`になります。このような無効なキーポイントはエディタで不可視に設定されます。
43 | - ユーザーがキーポイントを不可視に設定し、ポーズをcontrolnetに戻すと、キーポイントが接続する肢節はレンダリングされません。実質的に、これがエディタで肢節を削除する方法です。
44 | 1. グループ切り替え
45 | - キャンバスオブジェクト(手/顔/体)のキーポイントを誤って選択して変更することを避けたい場合、それらをグループ化できます。グループ化されたオブジェクトは一つのオブジェクトのように動作します。グループを拡大、回転、歪ませることができます。
46 |
47 |
48 | # Development
49 | ## Recommended IDE Setup
50 |
51 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
52 |
53 | ## Type Support for `.vue` Imports in TS
54 |
55 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
56 |
57 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
58 |
59 | 1. Disable the built-in TypeScript Extension
60 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
61 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
62 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
63 |
64 | ## Customize configuration
65 |
66 | See [Vite Configuration Reference](https://vitejs.dev/config/).
67 |
68 | ## Project Setup
69 |
70 | ```sh
71 | npm install
72 | ```
73 |
74 | ### Compile and Hot-Reload for Development
75 |
76 | ```sh
77 | npm run dev
78 | ```
79 |
80 | ### Type-Check, Compile and Minify for Production
81 |
82 | ```sh
83 | npm run build
84 | ```
85 |
86 | ### Run Unit Tests with [Vitest](https://vitest.dev/)
87 |
88 | ```sh
89 | npm run test:unit
90 | ```
91 |
92 | ### Lint with [ESLint](https://eslint.org/)
93 |
94 | ```sh
95 | npm run lint
96 | ```
97 |
--------------------------------------------------------------------------------
/scripts/openpose_editor.py:
--------------------------------------------------------------------------------
1 | import os
2 | import zipfile
3 | import gradio as gr
4 | import requests
5 | import json
6 | from fastapi import FastAPI, Request
7 | from fastapi.responses import HTMLResponse
8 | from fastapi.staticfiles import StaticFiles
9 | from fastapi.templating import Jinja2Templates
10 | from pydantic import BaseModel
11 | from typing import Optional
12 |
13 | import modules.script_callbacks as script_callbacks
14 | from modules import shared, scripts
15 |
16 |
17 | class Item(BaseModel):
18 | # image url.
19 | image_url: str
20 | # stringified pose JSON.
21 | pose: str
22 |
23 |
24 | EXTENSION_DIR = scripts.basedir()
25 | DIST_DIR = os.path.join(EXTENSION_DIR, 'dist')
26 |
27 |
28 | def get_latest_release(owner, repo) -> Optional[str]:
29 | url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
30 | response = requests.get(url)
31 | data = response.json()
32 | if response.status_code == 200:
33 | return data["tag_name"]
34 | else:
35 | return None
36 |
37 |
38 | def get_current_release() -> Optional[str]:
39 | if not os.path.exists(DIST_DIR):
40 | return None
41 |
42 | with open(os.path.join(DIST_DIR, "version.txt"), "r") as f:
43 | return f.read()
44 |
45 |
46 | def get_version_from_package_json():
47 | with open(os.path.join(EXTENSION_DIR, "package.json")) as f:
48 | data = json.load(f)
49 | return f"v{data.get('version', None)}"
50 |
51 |
52 | def download_latest_release(owner, repo):
53 | url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
54 | response = requests.get(url)
55 | data = response.json()
56 |
57 | if response.status_code == 200 and "assets" in data and len(data["assets"]) > 0:
58 | asset_url = data["assets"][0]["url"] # Get the URL of the first asset
59 | headers = {"Accept": "application/octet-stream"}
60 | response = requests.get(asset_url, headers=headers, allow_redirects=True)
61 |
62 | if response.status_code == 200:
63 | filename = "dist.zip"
64 | with open(filename, "wb") as file:
65 | file.write(response.content)
66 |
67 | # Unzip the file
68 | with zipfile.ZipFile(filename, "r") as zip_ref:
69 | zip_ref.extractall(DIST_DIR)
70 |
71 | # Remove the zip file
72 | os.remove(filename)
73 | else:
74 | print(f"Failed to download the file {url}.")
75 | else:
76 | print(f"Could not get the latest release or there are no assets {url}.")
77 |
78 |
79 | def need_update(current_version: Optional[str], package_version: str) -> bool:
80 | if current_version is None:
81 | return True
82 |
83 | def parse_version(version: str):
84 | return tuple(int(num) for num in version[1:].split('.'))
85 |
86 | return parse_version(current_version) < parse_version(package_version)
87 |
88 |
89 | def update_app():
90 | """Attempts to update the application to latest version"""
91 | owner = "huchenlei"
92 | repo = "sd-webui-openpose-editor"
93 |
94 | package_version = get_version_from_package_json()
95 | current_version = get_current_release()
96 |
97 | assert package_version is not None
98 | if need_update(current_version, package_version):
99 | download_latest_release(owner, repo)
100 |
101 |
102 | def mount_openpose_api(_: gr.Blocks, app: FastAPI):
103 | if not getattr(shared.cmd_opts, "disable_openpose_editor_auto_update", False):
104 | update_app()
105 |
106 | templates = Jinja2Templates(directory=DIST_DIR)
107 | app.mount(
108 | "/openpose_editor",
109 | StaticFiles(directory=DIST_DIR, html=True),
110 | name="openpose_editor",
111 | )
112 |
113 | @app.get("/openpose_editor_index", response_class=HTMLResponse)
114 | async def index_get(request: Request):
115 | return templates.TemplateResponse(
116 | "index.html", {"request": request, "data": {}}
117 | )
118 |
119 | @app.post("/openpose_editor_index", response_class=HTMLResponse)
120 | async def index_post(request: Request, item: Item):
121 | return templates.TemplateResponse(
122 | "index.html", {"request": request, "data": item.dict()}
123 | )
124 |
125 |
126 | script_callbacks.on_app_started(mount_openpose_api)
127 |
--------------------------------------------------------------------------------
/output_pose.json:
--------------------------------------------------------------------------------
1 | {
2 | "left_hand": [
3 | [
4 | 72.0,
5 | 138.6749968987715,
6 | 1
7 | ],
8 | [
9 | 50.00001525878906,
10 | 126.6749968987715,
11 | 1
12 | ],
13 | [
14 | 26.000015258789062,
15 | 109.6749968987715,
16 | 1
17 | ],
18 | [
19 | 15.0,
20 | 89.6749968987715,
21 | 1
22 | ],
23 | [
24 | 0.0,
25 | 74.6749968987715,
26 | 1
27 | ],
28 | [
29 | 46.88441843878036,
30 | 68.06475585206891,
31 | 1
32 | ],
33 | [
34 | 44.700867221123644,
35 | 41.31155552497435,
36 | 1
37 | ],
38 | [
39 | 42.99998474121094,
40 | 22.714115786649984,
41 | 1
42 | ],
43 | [
44 | 42.00298865302648,
45 | 7.0136598257858935,
46 | 1
47 | ],
48 | [
49 | 66.64955876004365,
50 | 63.25333859753661,
51 | 1
52 | ],
53 | [
54 | 65.00001525878906,
55 | 40.6749968987715,
56 | 1
57 | ],
58 | [
59 | 65.94870679770906,
60 | 22.18139755296059,
61 | 1
62 | ],
63 | [
64 | 65.38117571016846,
65 | 0.0,
66 | 1
67 | ],
68 | [
69 | 82.28699289317831,
70 | 68.16587713769567,
71 | 1
72 | ],
73 | [
74 | 84.50425981167291,
75 | 45.869876375420176,
76 | 1
77 | ],
78 | [
79 | 85.4529971269601,
80 | 27.623076702514766,
81 | 1
82 | ],
83 | [
84 | 85.02609712479875,
85 | 9.122938432072758,
86 | 1
87 | ],
88 | [
89 | 98.0260818660098,
90 | 76.01283565581218,
91 | 1
92 | ],
93 | [
94 | 103.05127794350199,
95 | 60.272436637095865,
96 | 1
97 | ],
98 | [
99 | 107.35044123995635,
100 | 46.921796571676936,
101 | 1
102 | ],
103 | [
104 | 110.70086722112376,
105 | 31.519236310001304,
106 | 1
107 | ]
108 | ],
109 | "right_hand": [
110 | [
111 | 37.00000762939453,
112 | 140.03029482565358,
113 | 1
114 | ],
115 | [
116 | 59.000003814697266,
117 | 132.03029482565358,
118 | 1
119 | ],
120 | [
121 | 83.00000381469727,
122 | 117.0302948256536,
123 | 1
124 | ],
125 | [
126 | 99.99999618530273,
127 | 99.0302948256536,
128 | 1
129 | ],
130 | [
131 | 117.99999618530273,
132 | 88.0302948256536,
133 | 1
134 | ],
135 | [
136 | 68.60503479194651,
137 | 69.66265371825791,
138 | 1
139 | ],
140 | [
141 | 72.0000114440918,
142 | 51.0302948256536,
143 | 1
144 | ],
145 | [
146 | 75.99999618530273,
147 | 34.0302948256536,
148 | 1
149 | ],
150 | [
151 | 80.00000381469727,
152 | 17.0302948256536,
153 | 1
154 | ],
155 | [
156 | 47.878141976595,
157 | 66.66265371825791,
158 | 1
159 | ],
160 | [
161 | 49.424375419378265,
162 | 45.4861751947855,
163 | 1
164 | ],
165 | [
166 | 51.0,
167 | 21.0302948256536,
168 | 1
169 | ],
170 | [
171 | 54.0,
172 | 0.0,
173 | 1
174 | ],
175 | [
176 | 29.575632210016238,
177 | 70.81461384130189,
178 | 1
179 | ],
180 | [
181 | 30.0,
182 | 44.574414456521666,
183 | 1
184 | ],
185 | [
186 | 30.848747024059264,
187 | 26.150394518043655,
188 | 1
189 | ],
190 | [
191 | 34.57563602471356,
192 | 7.270494210433782,
193 | 1
194 | ],
195 | [
196 | 11.000003814697266,
197 | 78.99843439499973,
198 | 1
199 | ],
200 | [
201 | 8.000003814697266,
202 | 61.0302948256536,
203 | 1
204 | ],
205 | [
206 | 4.000007629394531,
207 | 48.0302948256536,
208 | 1
209 | ],
210 | [
211 | 0.0,
212 | 34.0302948256536,
213 | 1
214 | ]
215 | ],
216 | "face": []
217 | }
--------------------------------------------------------------------------------
/pose_hand_fixed.json:
--------------------------------------------------------------------------------
1 | {
2 | "people": [
3 | {
4 | "pose_keypoints_2d": [
5 | 367,
6 | 172,
7 | 1,
8 | 367,
9 | 301,
10 | 1,
11 | 256,
12 | 301,
13 | 1,
14 | 104.85654973694608,
15 | 376.2731965157172,
16 | 1,
17 | 147.0056595578984,
18 | 238.99933417965596,
19 | 1,
20 | 475,
21 | 304,
22 | 1,
23 | 611.9877376245533,
24 | 383.99567216776376,
25 | 1,
26 | 588.9924539228022,
27 | 248.999667089828,
28 | 1,
29 | 0,
30 | 0,
31 | 0,
32 | 0,
33 | 0,
34 | 0,
35 | 0,
36 | 0,
37 | 0,
38 | 0,
39 | 0,
40 | 0,
41 | 0,
42 | 0,
43 | 0,
44 | 0,
45 | 0,
46 | 0,
47 | 344,
48 | 153,
49 | 1,
50 | 390,
51 | 153,
52 | 1,
53 | 314,
54 | 175,
55 | 1,
56 | 423,
57 | 176,
58 | 1
59 | ],
60 | "hand_right_keypoints_2d": [
61 | 152.01000953674315,
62 | 237,
63 | 1,
64 | 174.0100057220459,
65 | 229,
66 | 1,
67 | 198.0100057220459,
68 | 214,
69 | 1,
70 | 215.00999809265136,
71 | 196,
72 | 1,
73 | 233.00999809265136,
74 | 185,
75 | 1,
76 | 183.61503669929513,
77 | 166.63235889260432,
78 | 1,
79 | 187.01001335144042,
80 | 148,
81 | 1,
82 | 191.00999809265136,
83 | 131,
84 | 1,
85 | 195.0100057220459,
86 | 114,
87 | 1,
88 | 162.88814388394363,
89 | 163.63235889260432,
90 | 1,
91 | 164.4343773267269,
92 | 142.4558803691319,
93 | 1,
94 | 166.01000190734862,
95 | 118,
96 | 1,
97 | 169.01000190734862,
98 | 96.9697051743464,
99 | 1,
100 | 144.58563411736486,
101 | 167.7843190156483,
102 | 1,
103 | 145.01000190734862,
104 | 141.54411963086807,
105 | 1,
106 | 145.8587489314079,
107 | 123.12009969239006,
108 | 1,
109 | 149.58563793206218,
110 | 104.24019938478018,
111 | 1,
112 | 126.01000572204589,
113 | 175.96813956934614,
114 | 1,
115 | 123.01000572204589,
116 | 158,
117 | 1,
118 | 119.01000953674315,
119 | 145,
120 | 1,
121 | 115.01000190734862,
122 | 131,
123 | 1
124 | ],
125 | "hand_left_keypoints_2d": [
126 | 592.9895663894382,
127 | 244.00249844938574,
128 | 1,
129 | 570.9895816482273,
130 | 232.00249844938574,
131 | 1,
132 | 546.9895816482273,
133 | 215.00249844938574,
134 | 1,
135 | 535.9895663894382,
136 | 195.00249844938574,
137 | 1,
138 | 520.9895663894382,
139 | 180.00249844938574,
140 | 1,
141 | 567.8739848282186,
142 | 173.39225740268316,
143 | 1,
144 | 565.6904336105619,
145 | 146.6390570755886,
146 | 1,
147 | 563.9895511306491,
148 | 128.04161733726423,
149 | 1,
150 | 562.9925550424647,
151 | 112.34116137640014,
152 | 1,
153 | 587.6391251494819,
154 | 168.58084014815086,
155 | 1,
156 | 585.9895816482273,
157 | 146.00249844938574,
158 | 1,
159 | 586.9382731871473,
160 | 127.50889910357483,
161 | 1,
162 | 586.3707420996067,
163 | 105.32750155061424,
164 | 1,
165 | 603.2765592826165,
166 | 173.49337868830992,
167 | 1,
168 | 605.4938262011111,
169 | 151.19737792603442,
170 | 1,
171 | 606.4425635163983,
172 | 132.950578253129,
173 | 1,
174 | 606.015663514237,
175 | 114.450439982687,
176 | 1,
177 | 619.015648255448,
178 | 181.34033720642643,
179 | 1,
180 | 624.0408443329402,
181 | 165.5999381877101,
182 | 1,
183 | 628.3400076293946,
184 | 152.24929812229118,
185 | 1,
186 | 631.690433610562,
187 | 136.84673786061555,
188 | 1
189 | ]
190 | }
191 | ],
192 | "canvas_width": 768,
193 | "canvas_height": 512
194 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Openpose Editor for ControlNet in Stable Diffusion WebUI
2 | This extension is specifically build to be integrated into Stable Diffusion
3 | WebUI's ControlNet extension.
4 |
5 | 
6 |
7 | # Translations of README.md
8 | - [English](./README.md)
9 | - [中文](./README.zh.md)
10 | - [日本語](./README.ja.md)
11 |
12 | # Prerequisite
13 | [ControlNet](https://github.com/Mikubill/sd-webui-controlnet) `1.1.216`+
14 |
15 | # Installation
16 | From ControlNet extension v1.1.411, users no longer need to install this
17 | extension locally, as ControlNet extension now uses the remote endpoint at
18 | https://huchenlei.github.io/sd-webui-openpose-editor/ if no local editor
19 | installation is detected. Local installation is still recommended if you have
20 | poor internet connection, or have hard time connecting to github.io domain.
21 |
22 | ## Local Installation
23 | 
24 | 
25 |
26 | On UI restart, the extension will try to download the compiled Vue app from
27 | Github. Check whether `stable-diffusion-webui\extensions\sd-webui-openpose-editor\dist`
28 | exists and has content in it.
29 |
30 | Some users in China have reported having issue downloading dist with the autoupdate
31 | script. In such situtations, the user has 2 following options to get dist
32 | manually:
33 |
34 | ### Option1: Build the application
35 | Make sure you have nodeJS environment ready and follow `Development` section.
36 | Run `npm run build` to compile the application.
37 |
38 | ### Option2: Download the compiled application
39 | You can download the compiled application(`dist.zip`) from the
40 | [release](https://github.com/huchenlei/sd-webui-openpose-editor/releases) page.
41 | Unzip the package in the repository root and make sure hte unziped directory is
42 | named `dist`.
43 |
44 | # Usage
45 | The openpose editor core is build with Vue3. The gradio extension script is
46 | a thin wrapper that mounts the Vue3 Application on `/openpose_editor_index`.
47 |
48 | The user can directly access the editor at `localhost:7860/openpose_editor_index`
49 | or `https://huchenlei.github.io/sd-webui-openpose-editor/`
50 | if desired, but the main entry point is invoking the editor in the ControlNet
51 | extension. In ControlNet extension, select any openpose preprocessor, and hit
52 | the run preprocessor button. A preprocessor result preview will be genereated.
53 | Click `Edit` button at the bottom right corner of the generated image will bring
54 | up the openpose editor in a modal. After the edit, clicking the
55 | `Send pose to ControlNet` button will send back the pose to ControlNet.
56 |
57 | Following demo shows the basic workflow:
58 |
59 | [](http://www.youtube.com/watch?v=WEHVpPNIh8M)
60 |
61 | # Features
62 | 1. Support for face/hand used in controlnet.
63 | - The extension recognizes the face/hand objects in the controlnet preprocess
64 | results.
65 | - The user can add face/hand if the preprocessor result misses them. It can
66 | be done by either
67 | - Add Default hand (Face is not supported as face has too many keypoints (70 keypoints),
68 | which makes adjust them manually really hard.)
69 | - Add the object by uploading a pose JSON. The corresponding object of
70 | the first person will be used.
71 | 1. Visibility toggle
72 | - If a keypoint is not recognized by ControlNet preprocessor, it will have
73 | `(-1, -1)` as coordinates. Such invalid keypoints will be set as invisible
74 | in the editor.
75 | - If the user sets a keypoint as invisible and send the pose back to
76 | controlnet, the limb segments that the keypoint connects will not be rendered.
77 | Effectively this is how you remove a limb segment in the editor.
78 | 1. Group toggle
79 | - If you don't want to accidentally select and modify the keypoint of an
80 | canvas object (hand/face/body). You can group them. The grouped object will
81 | act like it is a single object. You can scale, rotate, skew the group.
82 |
83 | # Development
84 | ## Recommended IDE Setup
85 |
86 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
87 |
88 | ## Type Support for `.vue` Imports in TS
89 |
90 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
91 |
92 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
93 |
94 | 1. Disable the built-in TypeScript Extension
95 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
96 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
97 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
98 |
99 | ## Customize configuration
100 |
101 | See [Vite Configuration Reference](https://vitejs.dev/config/).
102 |
103 | ## Project Setup
104 |
105 | ```sh
106 | npm install
107 | ```
108 |
109 | ### Compile and Hot-Reload for Development
110 |
111 | ```sh
112 | npm run dev
113 | ```
114 |
115 | ### Type-Check, Compile and Minify for Production
116 |
117 | ```sh
118 | npm run build
119 | ```
120 |
121 | ### Run Unit Tests with [Vitest](https://vitest.dev/)
122 |
123 | ```sh
124 | npm run test:unit
125 | ```
126 |
127 | ### Lint with [ESLint](https://eslint.org/)
128 |
129 | ```sh
130 | npm run lint
131 | ```
132 |
--------------------------------------------------------------------------------
/src/components/OpenposeObjectPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ display_name }}
10 |
12 |
13 |
14 |
15 |
16 |
18 |
19 | {{ keypoint.name }}
20 |
21 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
175 |
176 |
181 |
--------------------------------------------------------------------------------
/src/i18n.ts:
--------------------------------------------------------------------------------
1 | import { createI18n } from 'vue-i18n'
2 |
3 | const messages = {
4 | en: {
5 | ui: {
6 | sendPose: 'Send pose to ControlNet',
7 | keybinding: 'Key Bindings',
8 | canvas: 'Canvas',
9 | resizeCanvas: 'Resize Canvas',
10 | resetZoom: 'Reset Zoom',
11 | backgroundImage: 'Background Image',
12 | uploadImage: 'Upload Image',
13 | poseControl: 'Pose Control',
14 | addPerson: 'Add Person',
15 | uploadJSON: 'Upload JSON',
16 | downloadJSON: 'Download JSON',
17 | downloadImage: 'Download Image',
18 | addLeftHand: 'Add left hand',
19 | addRightHand: 'Add right hand',
20 | addFace: 'Add face',
21 | panningKeybinding: '(SPACE / F) + Drag Mouse',
22 | panningDescription: 'Hold key to pan the canvas',
23 | zoomKeybinding: 'Mouse wheel',
24 | zoomDescription: 'Zoom in/out',
25 | hideKeybinding: 'Right click',
26 | hideDescription: 'Hide keypoint',
27 | flip: 'Flip object',
28 | }
29 | },
30 | zh: {
31 | ui: {
32 | sendPose: '发送姿势到ControlNet',
33 | keybinding: '键位绑定',
34 | canvas: '画布',
35 | resizeCanvas: '调整画布大小',
36 | resetZoom: '重置画布缩放',
37 | backgroundImage: '背景图片',
38 | uploadImage: '上传图片',
39 | poseControl: '姿势控制',
40 | addPerson: '添加人物',
41 | uploadJSON: '上传JSON',
42 | downloadJSON: '下载JSON',
43 | downloadImage: '下载图片',
44 | addLeftHand: '添加左手',
45 | addRightHand: '添加右手',
46 | addFace: '添加脸部',
47 | panningKeybinding: '(空格 或 F) + 拖动鼠标',
48 | panningDescription: '拖动画布',
49 | zoomKeybinding: '鼠标滚轮',
50 | zoomDescription: '调整画布缩放',
51 | hideKeybinding: '鼠标右键',
52 | hideDescription: '隐藏关键点',
53 | flip: '左右翻转',
54 | }
55 | },
56 | ja: {
57 | ui: {
58 | sendPose: 'ControlNetにポーズを送信',
59 | keybinding: 'キーバインディング',
60 | canvas: 'キャンバス',
61 | resizeCanvas: 'キャンバスのサイズを調整',
62 | resetZoom: 'ズームをリセット',
63 | backgroundImage: '背景画像',
64 | uploadImage: '画像をアップロード',
65 | poseControl: 'ポーズコントロール',
66 | addPerson: '人物を追加',
67 | uploadJSON: 'JSONをアップロード',
68 | downloadJSON: 'JSONをダウンロード',
69 | downloadImage: '画像をダウンロード',
70 | addLeftHand: '左手を追加',
71 | addRightHand: '右手を追加',
72 | addFace: '顔を追加',
73 | panningKeybinding: '(SPACE / F) + マウスのドラッグ',
74 | panningDescription: 'キーを押しながらキャンバスをパン',
75 | zoomKeybinding: 'マウスホイール',
76 | zoomDescription: 'ズームイン/アウト',
77 | hideKeybinding: '右クリック',
78 | hideDescription: 'キーポイントを隠す',
79 | }
80 | },
81 | ko: {
82 | ui: {
83 | sendPose: 'ControlNet에 자세 보내기',
84 | keybinding: '키 바인딩',
85 | canvas: '캔버스',
86 | resizeCanvas: '캔버스 크기 조정',
87 | resetZoom: '줌 리셋',
88 | backgroundImage: '배경 이미지',
89 | uploadImage: '이미지 업로드',
90 | poseControl: '자세 제어',
91 | addPerson: '사람 추가',
92 | uploadJSON: 'JSON 업로드',
93 | downloadJSON: 'JSON 다운로드',
94 | downloadImage: '이미지 다운로드',
95 | addLeftHand: '왼손 추가',
96 | addRightHand: '오른손 추가',
97 | addFace: '얼굴 추가',
98 | panningKeybinding: '(스페이스바 / F) + 마우스 드래그',
99 | panningDescription: '키를 누르고 캔버스 이동',
100 | zoomKeybinding: '마우스 휠',
101 | zoomDescription: '확대/축소',
102 | hideKeybinding: '오른쪽 클릭',
103 | hideDescription: '키포인트 숨기기',
104 | }
105 | },
106 | ru: {
107 | ui: {
108 | sendPose: 'Отправить позу в ControlNet',
109 | keybinding: 'Привязка клавиш',
110 | canvas: 'Холст',
111 | resizeCanvas: 'Изменить размер холста',
112 | resetZoom: 'Сбросить масштаб',
113 | backgroundImage: 'Фоновое изображение',
114 | uploadImage: 'Загрузить изображение',
115 | poseControl: 'Управление позой',
116 | addPerson: 'Добавить персонажа',
117 | uploadJSON: 'Загрузить JSON',
118 | downloadJSON: 'Скачать JSON',
119 | downloadImage: 'Скачать изображение',
120 | addLeftHand: 'Добавить левую руку',
121 | addRightHand: 'Добавить правую руку',
122 | addFace: 'Добавить лицо',
123 | panningKeybinding: '(ПРОБЕЛ / F) + Перетаскивание мыши',
124 | panningDescription: 'Зажмите клавишу, чтобы передвинуть холст',
125 | zoomKeybinding: 'Колесо мыши',
126 | zoomDescription: 'Увеличить/уменьшить',
127 | hideKeybinding: 'Правый клик',
128 | hideDescription: 'Скрыть ключевую точку',
129 | }
130 | },
131 | de: {
132 | ui: {
133 | sendPose: 'Pose an ControlNet senden',
134 | keybinding: 'Tastenbelegung',
135 | canvas: 'Leinwand',
136 | resizeCanvas: 'Leinwandgröße ändern',
137 | resetZoom: 'Zoom zurücksetzen',
138 | backgroundImage: 'Hintergrundbild',
139 | uploadImage: 'Bild hochladen',
140 | poseControl: 'Pose Kontrolle',
141 | addPerson: 'Person hinzufügen',
142 | uploadJSON: 'JSON hochladen',
143 | downloadJSON: 'JSON herunterladen',
144 | downloadImage: 'Bild herunterladen',
145 | addLeftHand: 'Linke Hand hinzufügen',
146 | addRightHand: 'Rechte Hand hinzufügen',
147 | addFace: 'Gesicht hinzufügen',
148 | panningKeybinding: '(LEERTASTE / F) + Maus ziehen',
149 | panningDescription: 'Halten Sie die Taste gedrückt, um die Leinwand zu verschieben',
150 | zoomKeybinding: 'Mausrad',
151 | zoomDescription: 'Vergrößern/Verkleinern',
152 | hideKeybinding: 'Rechtsklick',
153 | hideDescription: 'Schlüsselpunkt verbergen',
154 | }
155 | },
156 | es: {
157 | ui: {
158 | sendPose: 'Enviar pose a ControlNet',
159 | keybinding: 'Atajos de teclado',
160 | canvas: 'Lienzo',
161 | resizeCanvas: 'Cambiar tamaño de lienzo',
162 | resetZoom: 'Restablecer zoom',
163 | backgroundImage: 'Imagen de fondo',
164 | uploadImage: 'Subir imagen',
165 | poseControl: 'Control de pose',
166 | addPerson: 'Añadir persona',
167 | uploadJSON: 'Subir JSON',
168 | downloadJSON: 'Descargar JSON',
169 | downloadImage: 'Descargar imagen',
170 | addLeftHand: 'Añadir mano izquierda',
171 | addRightHand: 'Añadir mano derecha',
172 | addFace: 'Añadir rostro',
173 | panningKeybinding: '(ESPACIO / F) + Arrastrar ratón',
174 | panningDescription: 'Mantén presionada la tecla para mover el lienzo',
175 | zoomKeybinding: 'Rueda del ratón',
176 | zoomDescription: 'Acercar/Alejar',
177 | hideKeybinding: 'Clic derecho',
178 | hideDescription: 'Ocultar punto clave',
179 | }
180 | },
181 | fr: {
182 | ui: {
183 | sendPose: 'Envoyer la pose à ControlNet',
184 | keybinding: 'Raccourcis clavier',
185 | canvas: 'Toile',
186 | resizeCanvas: 'Redimensionner la toile',
187 | resetZoom: 'Réinitialiser le zoom',
188 | backgroundImage: 'Image de fond',
189 | uploadImage: 'Télécharger une image',
190 | poseControl: 'Contrôle de pose',
191 | addPerson: 'Ajouter une personne',
192 | uploadJSON: 'Télécharger JSON',
193 | downloadJSON: 'Télécharger JSON',
194 | downloadImage: 'Télécharger une image',
195 | addLeftHand: 'Ajouter la main gauche',
196 | addRightHand: 'Ajouter la main droite',
197 | addFace: 'Ajouter un visage',
198 | panningKeybinding: '(ESPACE / F) + Glisser la souris',
199 | panningDescription: 'Maintenez la touche pour déplacer la toile',
200 | zoomKeybinding: 'Molette de la souris',
201 | zoomDescription: 'Zoomer/Dézoomer',
202 | hideKeybinding: 'Clic droit',
203 | hideDescription: 'Masquer le point clé',
204 | }
205 | }
206 | };
207 |
208 | export default createI18n({
209 | locale: navigator.language.split('-')[0] || 'en',
210 | fallbackLocale: 'en',
211 | messages,
212 | });
213 |
--------------------------------------------------------------------------------
/src/Openpose.ts:
--------------------------------------------------------------------------------
1 | import { toRaw, markRaw } from 'vue';
2 | import { fabric } from 'fabric';
3 | import _ from 'lodash';
4 |
5 | const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
6 |
7 | class OpenposeKeypoint2D extends fabric.Circle {
8 | static idCounter: number = 0;
9 | id: number;
10 | confidence: number;
11 | name: string;
12 | connections: Array;
13 | selected: boolean;
14 | selected_in_group: boolean;
15 | constant_radius: number;
16 |
17 | constructor(
18 | x: number, y: number, confidence: number, color: string, name: string,
19 | opacity: number = 1.0, constant_radius: number = 2
20 | ) {
21 | super({
22 | radius: constant_radius,
23 | left: x,
24 | top: y,
25 | fill: color,
26 | stroke: color,
27 | strokeWidth: 1,
28 | hasControls: false, // Disallow user to scale the keypoint circle.
29 | hasBorders: false,
30 | opacity: opacity,
31 | });
32 |
33 | this.confidence = confidence;
34 | this.name = name;
35 | this.connections = [];
36 | this.id = OpenposeKeypoint2D.idCounter++;
37 | this.selected = false;
38 | this.selected_in_group = false;
39 | this.constant_radius = constant_radius;
40 |
41 | this.on('scaling', this._maintainConstantRadius.bind(this));
42 | this.on('skewing', this._maintainConstantRadius.bind(this));
43 | }
44 |
45 | addConnection(connection: OpenposeConnection): void {
46 | this.connections.push(connection);
47 | }
48 |
49 | updateConnections(transformMatrix: number[]) {
50 | if (transformMatrix.length !== 6)
51 | throw `Expect transformMatrix of length 6 but get ${transformMatrix}`;
52 |
53 | this.connections.forEach(c => c.update(this, transformMatrix));
54 | }
55 |
56 | _set(key: string, value: any): this {
57 | if (key === 'scaleX' || key === 'scaleY') {
58 | super._set('scaleX', 1);
59 | super._set('scaleY', 1);
60 | super._set('skewX', 0);
61 | super._set('skewY', 0);
62 | super._set('flipX', false);
63 | super._set('flipY', false);
64 | } else {
65 | super._set(key, value);
66 | }
67 | return this;
68 | }
69 |
70 | _maintainConstantRadius(): void {
71 | this.set('radius', this.constant_radius);
72 | this.setCoords();
73 | }
74 |
75 | get x(): number {
76 | return this.left!;
77 | }
78 |
79 | set x(x: number) {
80 | this.left = x;
81 | }
82 |
83 | get y(): number {
84 | return this.top!;
85 | }
86 |
87 | set y(y: number) {
88 | this.top = y;
89 | }
90 |
91 | get _visible(): boolean {
92 | return this.visible === undefined ? true : this.visible;
93 | }
94 |
95 | set _visible(visible: boolean) {
96 | this.visible = visible;
97 | this.connections.forEach(c => {
98 | c.updateVisibility();
99 | });
100 | }
101 |
102 | get abs_point(): fabric.Point {
103 | if (this.group) {
104 | const transformMatrix = this.group.calcTransformMatrix();
105 | return fabric.util.transformPoint(new fabric.Point(this.x, this.y), transformMatrix);
106 | } else {
107 | return new fabric.Point(this.x, this.y);
108 | }
109 | }
110 |
111 | get abs_x(): number {
112 | return this.abs_point.x;
113 | }
114 |
115 | get abs_y(): number {
116 | return this.abs_point.y;
117 | }
118 |
119 | distanceTo(other: OpenposeKeypoint2D): number {
120 | return Math.sqrt(
121 | Math.pow(this.x - other.x, 2) +
122 | Math.pow(this.y - other.y, 2)
123 | );
124 | }
125 |
126 | swap(other: OpenposeKeypoint2D): void {
127 | const otherX = other.x;
128 | const otherY = other.y;
129 |
130 | other.x = this.x;
131 | other.y = this.y;
132 |
133 | this.x = otherX;
134 | this.y = otherY;
135 |
136 | this.setCoords();
137 | other.setCoords();
138 | }
139 | };
140 |
141 | class OpenposeConnection extends fabric.Line {
142 | k1: OpenposeKeypoint2D;
143 | k2: OpenposeKeypoint2D;
144 |
145 | constructor(
146 | k1: OpenposeKeypoint2D, k2: OpenposeKeypoint2D, color: string,
147 | opacity: number = 1.0, strokeWidth: number = 2
148 | ) {
149 | super([k1.x, k1.y, k2.x, k2.y], {
150 | fill: color,
151 | stroke: color,
152 | strokeWidth,
153 | // Connections(Edges) themselves are not selectable, they will adjust when relevant keypoints move.
154 | selectable: false,
155 | // Connections should not appear in events.
156 | evented: false,
157 | opacity: opacity,
158 | });
159 | this.k1 = k1;
160 | this.k2 = k2;
161 | this.k1.addConnection(this);
162 | this.k2.addConnection(this);
163 | this.updateAll(IDENTITY_MATRIX);
164 | }
165 |
166 | /**
167 | * Update the connection because the coords of any of the keypoints has
168 | * changed.
169 | */
170 | update(p: OpenposeKeypoint2D, transformMatrix: number[]) {
171 | const rawGlobalPoint = fabric.util.transformPoint(
172 | p.getCenterPoint(),
173 | transformMatrix
174 | );
175 | const globalPoint = new fabric.Point(
176 | rawGlobalPoint.x - p.constant_radius / 4,
177 | rawGlobalPoint.y - p.constant_radius / 4
178 | );
179 | if (p === this.k1) {
180 | this.set({
181 | x1: globalPoint.x,
182 | y1: globalPoint.y,
183 | } as Partial);
184 | } else if (p === this.k2) {
185 | this.set({
186 | x2: globalPoint.x,
187 | y2: globalPoint.y,
188 | } as Partial);
189 | }
190 | }
191 |
192 | updateAll(transformMatrix: number[]) {
193 | this.update(this.k1, transformMatrix);
194 | this.update(this.k2, transformMatrix);
195 | }
196 |
197 | updateVisibility() {
198 | this.visible = this.k1._visible && this.k2._visible;
199 | }
200 |
201 | get length(): number {
202 | return this.k1.distanceTo(this.k2);
203 | }
204 | };
205 |
206 |
207 | class OpenposeObject {
208 | keypoints: OpenposeKeypoint2D[];
209 | connections: OpenposeConnection[];
210 | visible: boolean;
211 | group: fabric.Group | undefined;
212 | _locked: boolean;
213 | canvas: fabric.Canvas | undefined;
214 | openposeCanvas: fabric.Rect | undefined;
215 |
216 | // If the object is symmetrical, it should be flippable.
217 | flippable: boolean = false;
218 |
219 | constructor(keypoints: OpenposeKeypoint2D[], connections: OpenposeConnection[]) {
220 | this.keypoints = keypoints;
221 | this.connections = connections;
222 | this.visible = true;
223 | this.group = undefined;
224 | this._locked = false;
225 | this.canvas = undefined;
226 | this.openposeCanvas = undefined;
227 |
228 | // Negative x, y means invalid keypoint.
229 | this.keypoints.forEach(keypoint => {
230 | keypoint._visible = this.isKeypointValid(keypoint) && keypoint.confidence === 1.0;
231 | });
232 | }
233 |
234 | isKeypointValid(keypoint: OpenposeKeypoint2D): boolean {
235 | let offsetX = 0;
236 | let offsetY = 0;
237 | if (this.openposeCanvas !== undefined) {
238 | offsetX = this.openposeCanvas?.left!;
239 | offsetY = this.openposeCanvas?.top!;
240 | };
241 | return keypoint.abs_x - offsetX > 0 && keypoint.abs_y - offsetY > 0;
242 | }
243 |
244 | invalidKeypoints(): OpenposeKeypoint2D[] {
245 | return this.keypoints.filter(keypoint => !this.isKeypointValid(keypoint) && !keypoint._visible);
246 | }
247 |
248 | hasInvalidKeypoints(): boolean {
249 | return this.invalidKeypoints().length > 0;
250 | }
251 |
252 | addToCanvas(openposeCanvas: fabric.Rect) {
253 | this.canvas = openposeCanvas.canvas;
254 | this.openposeCanvas = openposeCanvas;
255 |
256 | this.keypoints.forEach(p => {
257 | p.x += openposeCanvas.left!;
258 | p.y += openposeCanvas.top!;
259 | this.canvas?.add(p);
260 | p.updateConnections(IDENTITY_MATRIX);
261 | });
262 |
263 | this.connections.forEach(c => {
264 | this.canvas?.add(c)
265 | });
266 | }
267 |
268 | removeFromCanvas() {
269 | this.keypoints.forEach(p => this.canvas?.remove(toRaw(p)));
270 | this.connections.forEach(c => this.canvas?.remove(toRaw(c)));
271 | if (this.grouped) {
272 | this.canvas?.remove(toRaw(this.group!));
273 | }
274 | this.canvas = undefined;
275 | }
276 |
277 | serialize(): number[] {
278 | const openposeCanvas = this.openposeCanvas;
279 |
280 | if (openposeCanvas === undefined)
281 | return [];
282 |
283 | return _.flatten(this.keypoints.map(p =>
284 | p._visible ? [
285 | p.abs_x - openposeCanvas.left!,
286 | p.abs_y - openposeCanvas.top!,
287 | 1.0
288 | ] : [0.0, 0.0, 0.0]
289 | ));
290 | }
291 |
292 | makeGroup() {
293 | if (this.group !== undefined)
294 | return;
295 | if (this.canvas === undefined)
296 | throw 'Cannot group object as the object is not on canvas yet. Call `addToCanvas` first.'
297 |
298 | const objects = [...this.keypoints, ...this.connections].map(o => toRaw(o));
299 |
300 | // Get all the objects as selection
301 | const sel = new fabric.ActiveSelection(objects, {
302 | canvas: this.canvas,
303 | lockScalingX: true,
304 | lockScalingY: true,
305 | opacity: _.mean(objects.map(o => o.opacity)),
306 | visible: this.visible,
307 | });
308 |
309 | // Make the objects active
310 | this.canvas.setActiveObject(sel);
311 |
312 | // Group the objects
313 | this.group = markRaw(sel.toGroup());
314 | }
315 |
316 | ungroup() {
317 | if (this.group === undefined)
318 | return;
319 | if (this.canvas === undefined)
320 | throw 'Cannot group object as the object is not on canvas yet. Call `addToCanvas` first.';
321 |
322 | this.group.toActiveSelection();
323 | this.group = undefined;
324 | this.canvas.discardActiveObject();
325 |
326 | // Need to refresh every connection, as their coords information are outdated once ungrouped
327 | this.connections.forEach(c => {
328 | // The scale/rotation/skew applied on the group will also apply on each connection.
329 | // Reset everything to 1 when ungrouping so that connection's behaviour
330 | // do not change.
331 | c.set({
332 | scaleX: 1.0,
333 | scaleY: 1.0,
334 | angle: 0,
335 | skewX: 0,
336 | skewY: 0,
337 | flipX: false,
338 | flipY: false,
339 | });
340 | c.updateAll(IDENTITY_MATRIX);
341 | });
342 | }
343 |
344 | set grouped(grouped: boolean) {
345 | if (this.grouped === grouped || this.locked) {
346 | return;
347 | }
348 |
349 | if (grouped) {
350 | this.makeGroup();
351 | } else {
352 | this.ungroup();
353 | }
354 | }
355 |
356 | get grouped(): boolean {
357 | return this.group !== undefined;
358 | }
359 |
360 | lockObject() {
361 | this.grouped = true;
362 | this.group!.set({
363 | selectable: false,
364 | evented: false,
365 | hasControls: false,
366 | hasBorders: false,
367 | });
368 | this._locked = true;
369 | }
370 |
371 | unlockObject() {
372 | this.grouped = true;
373 | this.group!.set({
374 | selectable: true,
375 | evented: true,
376 | hasControls: true,
377 | hasBorders: true,
378 | });
379 | this._locked = false;
380 | }
381 |
382 | set locked(locked: boolean) {
383 | if (this.locked === locked) return;
384 |
385 | if (locked) {
386 | this.lockObject();
387 | } else {
388 | this.unlockObject();
389 | }
390 | }
391 |
392 | get locked() {
393 | return this._locked;
394 | }
395 |
396 | flip() {
397 | if (!this.flippable) {
398 | throw "The object is not flippable.";
399 | }
400 |
401 | const nameMap = _.keyBy(this.keypoints, 'name');
402 |
403 | this.keypoints.forEach(keypoint => {
404 | const counterpart: OpenposeKeypoint2D | undefined =
405 | nameMap[keypoint.name.replace('left', 'right')];
406 |
407 | if (keypoint.name.startsWith('left') && counterpart !== undefined) {
408 | keypoint.swap(counterpart);
409 | keypoint.updateConnections(IDENTITY_MATRIX);
410 | counterpart.updateConnections(IDENTITY_MATRIX);
411 | }
412 | });
413 | }
414 | };
415 |
416 | function formatColor(color: [number, number, number]): string {
417 | return `rgb(${color.join(", ")})`;
418 | }
419 |
420 | class OpenposeBody extends OpenposeObject {
421 | static keypoints_connections: [number, number][] = [
422 | [0, 1], [1, 2], [2, 3], [3, 4],
423 | [1, 5], [5, 6], [6, 7], [1, 8],
424 | [8, 9], [9, 10], [1, 11], [11, 12],
425 | [12, 13], [0, 14], [14, 16], [0, 15],
426 | [15, 17],
427 | ];
428 |
429 | static colors: [number, number, number][] = [
430 | [255, 0, 0], [255, 85, 0], [255, 170, 0], [255, 255, 0],
431 | [170, 255, 0], [85, 255, 0], [0, 255, 0], [0, 255, 85],
432 | [0, 255, 170], [0, 255, 255], [0, 170, 255], [0, 85, 255],
433 | [0, 0, 255], [85, 0, 255], [170, 0, 255], [255, 0, 255],
434 | [255, 0, 170], [255, 0, 85]
435 | ];
436 |
437 | static keypoint_names = [
438 | "nose",
439 | "neck",
440 | "right_shoulder",
441 | "right_elbow",
442 | "right_wrist",
443 | "left_shoulder",
444 | "left_elbow",
445 | "left_wrist",
446 | "right_hip",
447 | "right_knee",
448 | "right_ankle",
449 | "left_hip",
450 | "left_knee",
451 | "left_ankle",
452 | "right_eye",
453 | "left_eye",
454 | "right_ear",
455 | "left_ear",
456 | ];
457 |
458 | /**
459 | * @param {Array>} rawKeypoints keypoints directly read from the openpose JSON format
460 | * [
461 | * [x1, y1, c1],
462 | * [x2, y2, c2],
463 | * ...
464 | * ]
465 | */
466 | constructor(rawKeypoints: [number, number, number][]) {
467 | const keypoints = _.zipWith(rawKeypoints, OpenposeBody.colors, OpenposeBody.keypoint_names,
468 | (p, color, keypoint_name) => new OpenposeKeypoint2D(
469 | p[0],
470 | p[1],
471 | p[2],
472 | formatColor(color),
473 | keypoint_name,
474 | /* opacity= */ 0.7,
475 | /* constant_radius= */ 4
476 | ));
477 |
478 | const connections = _.zipWith(OpenposeBody.keypoints_connections, OpenposeBody.colors.slice(0, 17),
479 | (connection, color) => {
480 | return new OpenposeConnection(
481 | keypoints[connection[0]],
482 | keypoints[connection[1]],
483 | formatColor(color),
484 | /* opacity= */ 0.7,
485 | /* strokeWidth= */ 4
486 | );
487 | });
488 |
489 | super(keypoints, connections);
490 | this.flippable = true;
491 | }
492 |
493 | static create(rawKeypoints: [number, number, number][]): OpenposeBody | undefined {
494 | if (rawKeypoints.length < OpenposeBody.keypoint_names.length) {
495 | console.warn(
496 | `Wrong number of keypoints for openpose body(Coco format).
497 | Expect ${OpenposeBody.keypoint_names.length} but got ${rawKeypoints.length}.`)
498 | return undefined;
499 | }
500 | rawKeypoints.slice(0, OpenposeBody.keypoint_names.length);
501 | return new OpenposeBody(rawKeypoints);
502 | }
503 |
504 | getKeypointByName(name: string): OpenposeKeypoint2D {
505 | const index = OpenposeBody.keypoint_names.findIndex(s => s === name);
506 | if (index === -1) {
507 | throw `'${name}' not found in keypoint names.`;
508 | }
509 | return this.keypoints[index];
510 | }
511 | };
512 |
513 | function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
514 | let r: number, g: number, b: number;
515 | let i = Math.floor(h * 6);
516 | let f = h * 6 - i;
517 | let p = v * (1 - s);
518 | let q = v * (1 - f * s);
519 | let t = v * (1 - (1 - f) * s);
520 |
521 | switch (i % 6) {
522 | case 0:
523 | r = v;
524 | g = t;
525 | b = p;
526 | break;
527 | case 1:
528 | r = q;
529 | g = v;
530 | b = p;
531 | break;
532 | case 2:
533 | r = p;
534 | g = v;
535 | b = t;
536 | break;
537 | case 3:
538 | r = p;
539 | g = q;
540 | b = v;
541 | break;
542 | case 4:
543 | r = t;
544 | g = p;
545 | b = v;
546 | break;
547 | case 5:
548 | r = v;
549 | g = p;
550 | b = q;
551 | break;
552 | }
553 |
554 | return [Math.round(r! * 255), Math.round(g! * 255), Math.round(b! * 255)];
555 | }
556 | class OpenposeHand extends OpenposeObject {
557 | static keypoint_connections: [number, number][] = [
558 | [0, 1], [1, 2], [2, 3], [3, 4],
559 | [0, 5], [5, 6], [6, 7], [7, 8],
560 | [0, 9], [9, 10], [10, 11], [11, 12],
561 | [0, 13], [13, 14], [14, 15], [15, 16],
562 | [0, 17], [17, 18], [18, 19], [19, 20],
563 | ];
564 |
565 | static keypoint_names: string[] = [
566 | 'wrist joint',
567 | ..._.range(4).map(i => `Thumb-${i}`),
568 | ..._.range(4).map(i => `Index Finger-${i}`),
569 | ..._.range(4).map(i => `Middle Finger-${i}`),
570 | ..._.range(4).map(i => `Ring Finger-${i}`),
571 | ..._.range(4).map(i => `Little Finger-${i}`),
572 | ];
573 |
574 | constructor(rawKeypoints: [number, number, number][]) {
575 | const keypoints = _.zipWith(rawKeypoints, OpenposeHand.keypoint_names,
576 | (rawKeypoint: [number, number, number], name: string) => new OpenposeKeypoint2D(
577 | rawKeypoint[0] > 0 ? rawKeypoint[0] : -1,
578 | rawKeypoint[1] > 0 ? rawKeypoint[1] : -1,
579 | rawKeypoint[2],
580 | formatColor([0, 0, 255]), // All hand keypoints are marked blue.
581 | name
582 | ));
583 |
584 | const connections = OpenposeHand.keypoint_connections.map((connection, i) => new OpenposeConnection(
585 | keypoints[connection[0]],
586 | keypoints[connection[1]],
587 | formatColor(hsvToRgb(i / OpenposeHand.keypoint_connections.length, 1.0, 1.0))
588 | ));
589 | super(keypoints, connections);
590 | }
591 |
592 | static create(rawKeypoints: [number, number, number][]): OpenposeHand | undefined {
593 | if (rawKeypoints.length < OpenposeHand.keypoint_names.length) {
594 | console.warn(
595 | `Wrong number of keypoints for openpose hand. Expect ${OpenposeHand.keypoint_names.length} but got ${rawKeypoints.length}.`)
596 | return undefined;
597 | }
598 | rawKeypoints.slice(0, OpenposeHand.keypoint_names.length);
599 | return new OpenposeHand(rawKeypoints);
600 | }
601 |
602 | /**
603 | * Size of a hand is calculated as the average connection distance
604 | * (all visible connections).
605 | */
606 | get size(): number {
607 | return _.mean(this.connections.filter(c => c.visible).map(c => c.length));
608 | }
609 | };
610 |
611 | class OpenposeFace extends OpenposeObject {
612 | static keypoint_names: string[] = [
613 | ..._.range(17).map(i => `FaceOutline-${i}`),
614 | ..._.range(5).map(i => `LeftEyebrow-${i}`),
615 | ..._.range(5).map(i => `RightEyebrow-${i}`),
616 | ..._.range(4).map(i => `NoseBridge-${i}`),
617 | ..._.range(5).map(i => `NoseBottom-${i}`),
618 | ..._.range(6).map(i => `LeftEyeOutline-${i}`),
619 | ..._.range(6).map(i => `RightEyeOutline-${i}`),
620 | ..._.range(12).map(i => `MouthOuterBound-${i}`),
621 | ..._.range(8).map(i => `MouthInnerBound-${i}`),
622 | 'LeftEyeball',
623 | 'RightEyeball',
624 | ];
625 |
626 | constructor(rawKeypoints: [number, number, number][]) {
627 | const keypoints = _.zipWith(rawKeypoints, OpenposeFace.keypoint_names,
628 | (rawKeypoint, name) => new OpenposeKeypoint2D(
629 | rawKeypoint[0] > 0 ? rawKeypoint[0] : -1,
630 | rawKeypoint[1] > 0 ? rawKeypoint[1] : -1,
631 | rawKeypoint[2],
632 | formatColor([255, 255, 255]),
633 | name
634 | ));
635 | super(keypoints, []);
636 | }
637 |
638 | static create(rawKeypoints: [number, number, number][]): OpenposeFace | undefined {
639 | if (rawKeypoints.length < OpenposeFace.keypoint_names.length) {
640 | console.warn(
641 | `Wrong number of keypoints for openpose face. Expect ${OpenposeFace.keypoint_names.length} but got ${rawKeypoints.length}.`)
642 | return undefined;
643 | }
644 | rawKeypoints.slice(0, OpenposeFace.keypoint_names.length);
645 | return new OpenposeFace(rawKeypoints);
646 | }
647 | }
648 |
649 | enum OpenposeBodyPart {
650 | LEFT_HAND = 'left_hand',
651 | RIGHT_HAND = 'right_hand',
652 | FACE = 'face',
653 | };
654 |
655 | class OpenposePerson {
656 | static id = 0;
657 |
658 | name: string;
659 | body: OpenposeBody | OpenposeAnimal;
660 | left_hand: OpenposeHand | undefined;
661 | right_hand: OpenposeHand | undefined;
662 | face: OpenposeFace | undefined;
663 | id: number;
664 | visible: boolean;
665 |
666 | constructor(name: string | null, body: OpenposeBody | OpenposeAnimal,
667 | left_hand: OpenposeHand | undefined = undefined,
668 | right_hand: OpenposeHand | undefined = undefined,
669 | face: OpenposeFace | undefined = undefined
670 | ) {
671 | this.body = body;
672 | this.left_hand = left_hand;
673 | this.right_hand = right_hand;
674 | this.face = face;
675 | this.id = OpenposePerson.id++;
676 | this.visible = true;
677 | this.name = name == null ? `Person ${this.id}` : name;
678 | }
679 |
680 | get isAnimal(): boolean {
681 | return this.body instanceof OpenposeAnimal;
682 | }
683 |
684 | addToCanvas(openposeCanvas: fabric.Rect) {
685 | [this.body, this.left_hand, this.right_hand, this.face].forEach(o => o?.addToCanvas(openposeCanvas));
686 | }
687 |
688 | removeFromCanvas() {
689 | [this.body, this.left_hand, this.right_hand, this.face].forEach(o => o?.removeFromCanvas());
690 | }
691 |
692 | allKeypoints(): OpenposeKeypoint2D[] {
693 | return _.flatten([this.body, this.left_hand, this.right_hand, this.face]
694 | .map(o => o === undefined ? [] : o.keypoints));
695 | }
696 |
697 | allKeypointsInvisible(): boolean {
698 | return _.every(this.allKeypoints(), keypoint => !keypoint._visible);
699 | }
700 |
701 | toJson(): IOpenposePersonJson | number[] {
702 | if (this.isAnimal) {
703 | return this.body.serialize();
704 | }
705 | return {
706 | pose_keypoints_2d: this.body.serialize(),
707 | hand_right_keypoints_2d: this.right_hand?.serialize(),
708 | hand_left_keypoints_2d: this.left_hand?.serialize(),
709 | face_keypoints_2d: this.face?.serialize(),
710 | } as IOpenposePersonJson;
711 | }
712 |
713 | private adjustHandSize(hand: OpenposeHand, wrist_keypoint: OpenposeKeypoint2D, elbow_keypoint: OpenposeKeypoint2D) {
714 | hand.grouped = true;
715 | // Scale the hand to fit body size
716 | const forearm_length = wrist_keypoint.distanceTo(elbow_keypoint);
717 | const hand_length = hand.size * 4; // There are 4 connections from wrist_joint to any fingertips.
718 | // Approximate hand size as 70% of forearm length.
719 | const scaleRatio = forearm_length * 0.7 / hand_length;
720 | hand.group!.scale(scaleRatio);
721 | }
722 |
723 | private adjustHandAngle(hand: OpenposeHand, wrist_keypoint: OpenposeKeypoint2D, elbow_keypoint: OpenposeKeypoint2D) {
724 | // Ungroup the hand
725 | hand.grouped = false;
726 |
727 | // Calculate the angle
728 | const initial_angle = fabric.util.degreesToRadians(90);
729 | const angle = Math.atan2(
730 | elbow_keypoint.abs_y - wrist_keypoint.abs_y,
731 | elbow_keypoint.abs_x - wrist_keypoint.abs_x
732 | );
733 |
734 | // Rotate each keypoint
735 | hand.keypoints.forEach(keypoint => {
736 | // Create a point for the current keypoint
737 | const point = new fabric.Point(keypoint.x, keypoint.y);
738 |
739 | // Create a point for the wrist (the rotation origin)
740 | const origin = new fabric.Point(wrist_keypoint.x, wrist_keypoint.y);
741 |
742 | // Rotate the point
743 | const rotatedPoint = fabric.util.rotatePoint(point, origin, angle - initial_angle);
744 |
745 | // Update the keypoint coordinates
746 | keypoint.x = rotatedPoint.x;
747 | keypoint.y = rotatedPoint.y;
748 | });
749 |
750 | // Update each connection
751 | hand.connections.forEach(connection => connection.updateAll(IDENTITY_MATRIX));
752 | }
753 |
754 | private adjustHandLocation(hand: OpenposeHand, wrist_keypoint: OpenposeKeypoint2D, elbow_keypoint: OpenposeKeypoint2D) {
755 | hand.grouped = true;
756 | // Move the group so that the wrist joint is at the wrist keypoint
757 | const wrist_joint = hand.keypoints[0]; // Assuming the wrist joint is the first keypoint
758 | const dx = wrist_keypoint.abs_x - wrist_joint.abs_x;
759 | const dy = wrist_keypoint.abs_y - wrist_joint.abs_y;
760 | hand.group!.left! += dx;
761 | hand.group!.top! += dy;
762 | }
763 |
764 | private adjustHand(hand: OpenposeHand, wrist_keypoint: OpenposeKeypoint2D, elbow_keypoint: OpenposeKeypoint2D) {
765 | this.adjustHandSize(hand, wrist_keypoint, elbow_keypoint);
766 | this.adjustHandAngle(hand, wrist_keypoint, elbow_keypoint);
767 | this.adjustHandLocation(hand, wrist_keypoint, elbow_keypoint);
768 | // Update group coordinates
769 | hand.group!.setCoords();
770 | }
771 |
772 | public attachLeftHand(hand: OpenposeHand) {
773 | if (!(this.body instanceof OpenposeBody)) {
774 | throw "Hand not supported for OpenposeAnimal.";
775 | }
776 | this.adjustHand(hand, this.body.getKeypointByName('left_wrist'), this.body.getKeypointByName('left_elbow'));
777 | this.left_hand = hand;
778 | }
779 |
780 | public attachRightHand(hand: OpenposeHand) {
781 | if (!(this.body instanceof OpenposeBody)) {
782 | throw "Hand not supported for OpenposeAnimal.";
783 | }
784 | this.adjustHand(hand, this.body.getKeypointByName('right_wrist'), this.body.getKeypointByName('right_elbow'));
785 | this.right_hand = hand;
786 | }
787 |
788 | public attachFace(face: OpenposeFace) {
789 | // TODO: adjust face location.
790 | this.face = face;
791 | }
792 | };
793 |
794 | class OpenposeAnimal extends OpenposeObject {
795 | // Note: the index here is from 1. So we need to shift -1 to get 0-indexed connections.
796 | static keypoint_connections: [number, number][] = [
797 | [1, 2],
798 | [2, 3],
799 | [1, 3],
800 | [3, 4],
801 | [4, 9],
802 | [9, 10],
803 | [10, 11],
804 | [4, 6],
805 | [6, 7],
806 | [7, 8],
807 | [4, 5],
808 | [5, 15],
809 | [15, 16],
810 | [16, 17],
811 | [5, 12],
812 | [12, 13],
813 | [13, 14],
814 | ];
815 |
816 | static colors: [number, number, number][] = [
817 | [255, 255, 255],
818 | [100, 255, 100],
819 | [150, 255, 255],
820 | [100, 50, 255],
821 | [50, 150, 200],
822 | [0, 255, 255],
823 | [0, 150, 0],
824 | [0, 0, 255],
825 | [0, 0, 150],
826 | [255, 50, 255],
827 | [255, 0, 255],
828 | [255, 0, 0],
829 | [150, 0, 0],
830 | [255, 255, 100],
831 | [0, 150, 0],
832 | [255, 255, 0],
833 | [150, 150, 150],
834 | ];
835 |
836 | static keypoint_names: string[] = Array.from(Array(17).keys()).map(i => `Keypoint-${i}`);
837 |
838 | constructor(rawKeypoints: [number, number, number][]) {
839 | console.log(OpenposeAnimal.keypoint_names);
840 | const keypoints = _.zipWith(rawKeypoints, OpenposeAnimal.colors, OpenposeAnimal.keypoint_names,
841 | (p, color, name) => new OpenposeKeypoint2D(
842 | p[0],
843 | p[1],
844 | p[2] > 0 ? 1.0 : 0.0,
845 | formatColor(color),
846 | name,
847 | /* opacity= */ 1.0,
848 | /* constant_radius= */ 2
849 | ));
850 |
851 | const connections = _.zipWith(OpenposeAnimal.keypoint_connections, OpenposeAnimal.colors.slice(0, 17),
852 | (connection, color) => {
853 | return new OpenposeConnection(
854 | keypoints[connection[0] - 1],
855 | keypoints[connection[1] - 1],
856 | formatColor(color),
857 | /* opacity= */ 1.0,
858 | /* strokeWidth= */ 4
859 | );
860 | });
861 |
862 | super(keypoints, connections);
863 | this.flippable = true;
864 | }
865 |
866 | static create(rawKeypoints: [number, number, number][]): OpenposeAnimal | undefined {
867 | if (rawKeypoints.length < OpenposeAnimal.keypoint_names.length) {
868 | console.warn(
869 | `Wrong number of keypoints for openpose body(Coco format).
870 | Expect ${OpenposeBody.keypoint_names.length} but got ${rawKeypoints.length}.`)
871 | return undefined;
872 | }
873 | rawKeypoints.slice(0, OpenposeAnimal.keypoint_names.length);
874 | return new OpenposeAnimal(rawKeypoints);
875 | }
876 | }
877 |
878 | interface IOpenposePersonJson {
879 | pose_keypoints_2d: number[],
880 | hand_right_keypoints_2d: number[] | null,
881 | hand_left_keypoints_2d: number[] | null,
882 | face_keypoints_2d: number[] | null,
883 | };
884 |
885 | interface IOpenposeJson {
886 | canvas_width: number;
887 | canvas_height: number;
888 | people: IOpenposePersonJson[] | undefined;
889 | animals: number[][] | undefined;
890 | };
891 |
892 | export {
893 | OpenposeBody,
894 | OpenposeConnection,
895 | OpenposeKeypoint2D,
896 | OpenposeObject,
897 | OpenposePerson,
898 | OpenposeHand,
899 | OpenposeFace,
900 | OpenposeBodyPart,
901 | OpenposeAnimal,
902 | };
903 |
904 | export type {
905 | IOpenposeJson
906 | };
907 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 |
1035 | {{ $t('ui.sendPose') }}
1036 |
1037 |
1038 | {{ $t('ui.keybinding') }}
1039 |
1040 |
1041 | {{ $t('ui.panningDescription') }}
1042 | {{ $t('ui.zoomDescription') }}
1043 | {{ $t('ui.hideDescription') }}
1044 |
1045 |
1046 | {{ $t('ui.canvas') }}
1047 |
1048 |
1049 |
1050 |
1052 |
1054 | {{ $t('ui.resizeCanvas') }}
1055 | {{ $t('ui.resetZoom') }}
1056 |
1057 |
1058 |
1059 | {{ $t('ui.backgroundImage') }}
1060 |
1061 |
1063 |
1064 |
1065 | {{ $t('ui.uploadImage') }}
1066 |
1067 |
1068 |
1069 |
1071 |
1072 | {{ file.name }}
1073 |
1075 |
1076 |
1077 |
1078 |
1079 |
1080 | {{ $t('ui.poseControl') }}
1081 |
1082 |
1083 |
1084 |
1085 | {{ $t('ui.addPerson') }}
1086 |
1087 |
1088 |
1089 |
1090 | {{ $t('ui.uploadJSON') }}
1091 |
1092 |
1093 |
1094 |
1095 | {{ $t('ui.downloadJSON') }}
1096 |
1097 |
1098 |
1099 | {{ $t('ui.downloadImage') }}
1100 |
1101 |
1102 |
1103 |
1105 |
1106 |
1107 |
1108 |
{{ $t('ui.addLeftHand')
1109 | }}
1110 |
1113 |
1114 |
1115 |
1116 |
1117 |
1118 |
1119 |
{{ $t('ui.addRightHand')
1120 | }}
1121 |
1124 |
1125 |
1126 |
1127 |
1128 |
1129 |
1130 |
1133 |
1134 | {{ $t('ui.addFace') }}
1135 |
1136 |
1137 |
1138 |
1139 |
1140 |
1143 |
1146 |
1148 |
1149 |
1150 |
1151 |
1152 |
1153 |
1154 |
1155 |
1156 |
1157 |
1158 |
1159 |
1160 |
1166 |
--------------------------------------------------------------------------------