├── .eslintrc.cjs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ └── static.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.ja.md ├── README.md ├── README.zh.md ├── env.d.ts ├── index.html ├── main.d.ts ├── output_pose.json ├── package-lock.json ├── package.json ├── pose_hand_fixed.json ├── preload.py ├── process_keypoints.py ├── public └── favicon.ico ├── readme_assets ├── install_guide.png └── restart_ui_guide.png ├── scripts └── openpose_editor.py ├── src ├── App.vue ├── Notification.ts ├── Openpose.ts ├── assets │ ├── base.css │ ├── logo.svg │ └── main.css ├── components │ ├── FlipOutlined.vue │ ├── GroupSwitch.vue │ ├── Header.vue │ ├── IconSwitch.vue │ ├── LockSwitch.vue │ ├── OpenposeObjectPanel.vue │ ├── VisibleSwitch.vue │ ├── __tests__ │ │ └── Openpose.spec.ts │ └── icons │ │ ├── IconCommunity.vue │ │ ├── IconDocumentation.vue │ │ ├── IconEcosystem.vue │ │ ├── IconSupport.vue │ │ └── IconTooling.vue ├── i18n.ts ├── main.ts └── stores │ └── counter.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts ├── vitest.config.ts └── zip_dist.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # Stable Diffusion WebUIのためのControlNet内Openposeエディタ 2 | この拡張機能は、特にStable Diffusion WebUIのControlNet拡張機能に統合するために開発されました。 3 | 4 | ![editor_in_modal](/readme_assets/editor_in_modal.png) 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 | ![installation_guide](/readme_assets/install_guide.png) 14 | ![restart_ui_guide](/readme_assets/restart_ui_guide.png) 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://img.youtube.com/vi/WEHVpPNIh8M/0.jpg)](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 | -------------------------------------------------------------------------------- /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 | ![editor](https://github.com/huchenlei/sd-webui-openpose-editor/assets/20929282/c69199e2-5676-4609-87bc-af7499b1c4bd) 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 | ![installation_guide](/readme_assets/install_guide.png) 24 | ![restart_ui_guide](/readme_assets/restart_ui_guide.png) 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 | [![Basic Workflow](http://img.youtube.com/vi/WEHVpPNIh8M/0.jpg)](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 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # Openpose Editor for ControlNet in Stable Diffusion WebUI 2 | 3 | 这个扩展专门为整合到 Stable Diffusion WebUI 的 ControlNet 扩展中而设计。 4 | 5 | ![editor_in_modal](/readme_assets/editor_in_modal.png) 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 | ![installation_guide](/readme_assets/install_guide.png) 15 | ![restart_ui_guide](/readme_assets/restart_ui_guide.png) 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 | [![Basic Workflow](http://img.youtube.com/vi/WEHVpPNIh8M/0.jpg)](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 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Openpose Editor 8 | 9 | 10 |
11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenlei/sd-webui-openpose-editor/0026f453f9ac9b7dec64129d2f2b83b04030c645/public/favicon.ico -------------------------------------------------------------------------------- /readme_assets/install_guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenlei/sd-webui-openpose-editor/0026f453f9ac9b7dec64129d2f2b83b04030c645/readme_assets/install_guide.png -------------------------------------------------------------------------------- /readme_assets/restart_ui_guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenlei/sd-webui-openpose-editor/0026f453f9ac9b7dec64129d2f2b83b04030c645/readme_assets/restart_ui_guide.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 1029 | 1030 | 1159 | 1160 | 1166 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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/FlipOutlined.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/GroupSwitch.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 56 | -------------------------------------------------------------------------------- /src/components/IconSwitch.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/LockSwitch.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /src/components/OpenposeObjectPanel.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 175 | 176 | 181 | -------------------------------------------------------------------------------- /src/components/VisibleSwitch.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /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 | 8 | -------------------------------------------------------------------------------- /src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); --------------------------------------------------------------------------------