├── .devcontainer
├── devcontainer.json
└── start.sh
├── .github
└── workflows
│ └── build-image.yml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── README_EN.md
├── README_JP.md
├── README_KO.md
├── app.py
├── app.spec
├── assets
├── comfyui.png
├── community-uniapp-wechat-miniprogram.png
├── community-web.png
├── community-wechat-miniprogram.png
├── demo.png
├── demoImage.jpg
├── face++.png
├── gradio-image.jpeg
├── harry.png
├── hivision_logo.png
└── social_template.png
├── demo
├── assets
│ ├── american-style.png
│ ├── color_list_CN.csv
│ ├── color_list_EN.csv
│ ├── size_list_CN.csv
│ ├── size_list_EN.csv
│ └── title.md
├── config.py
├── images
│ ├── test0.jpg
│ ├── test1.jpg
│ ├── test2.jpg
│ ├── test3.jpg
│ └── test4.jpg
├── locales.py
├── processor.py
├── ui.py
└── utils.py
├── deploy_api.py
├── docker-compose.yml
├── docs
├── api_CN.md
├── api_EN.md
├── face++_CN.md
└── face++_EN.md
├── hivision
├── __init__.py
├── creator
│ ├── __init__.py
│ ├── choose_handler.py
│ ├── context.py
│ ├── face_detector.py
│ ├── human_matting.py
│ ├── layout_calculator.py
│ ├── move_image.py
│ ├── photo_adjuster.py
│ ├── retinaface
│ │ ├── __init__.py
│ │ ├── box_utils.py
│ │ ├── inference.py
│ │ ├── prior_box.py
│ │ └── weights
│ │ │ └── .gitkeep
│ ├── rotation_adjust.py
│ ├── tensor2numpy.py
│ ├── utils.py
│ └── weights
│ │ └── .gitkeep
├── error.py
├── plugin
│ ├── beauty
│ │ ├── __init__.py
│ │ ├── base_adjust.py
│ │ ├── beauty_tools.py
│ │ ├── grind_skin.py
│ │ ├── handler.py
│ │ ├── lut
│ │ │ └── lut_origin.png
│ │ ├── thin_face.py
│ │ └── whitening.py
│ ├── font
│ │ ├── .gitkeep
│ │ └── 青鸟华光简琥珀.ttf
│ ├── template
│ │ ├── assets
│ │ │ ├── template_1.png
│ │ │ ├── template_2.png
│ │ │ └── template_config.json
│ │ └── template_calculator.py
│ └── watermark.py
└── utils.py
├── inference.py
├── requirements-app.txt
├── requirements-dev.txt
├── requirements.txt
├── scripts
├── build_pypi.py
└── download_model.py
└── test
├── create_id_photo.py
└── temp
└── .gitkeep
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/universal
3 | {
4 | "name": "Default Linux Universal",
5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6 | "image": "mcr.microsoft.com/devcontainers/universal:2-linux",
7 |
8 | // Features to add to the dev container. More info: https://containers.dev/features.
9 | // "features": {},
10 |
11 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
12 | // "forwardPorts": [],
13 |
14 | // Use 'postCreateCommand' to run commands after the container is created.
15 | "onCreateCommand": "sh .devcontainer/start.sh",
16 |
17 | // Configure tool-specific properties.
18 | "customizations": {
19 | "vscode": {
20 | "extensions": [
21 | "ms-python.python",
22 | "eamodio.gitlens",
23 | "mhutchie.git-graph"
24 | ]
25 | }
26 | }
27 |
28 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
29 | // "remoteUser": "root"
30 | }
31 |
--------------------------------------------------------------------------------
/.devcontainer/start.sh:
--------------------------------------------------------------------------------
1 | sudo apt-get update && sudo apt-get install ffmpeg libsm6 libxext6 -y
2 |
3 | conda create -n HivisionIDPhotos python=3.10 -y
4 | conda init
5 | echo 'conda activate HivisionIDPhotos' >> ~/.bashrc
6 |
7 | ENV_PATH="/opt/conda/envs/HivisionIDPhotos/bin"
8 | $ENV_PATH/pip install -r requirements.txt -r requirements-app.txt -r requirements-dev.txt
9 |
10 | $ENV_PATH/python scripts/download_model.py --models all
11 |
--------------------------------------------------------------------------------
/.github/workflows/build-image.yml:
--------------------------------------------------------------------------------
1 | name: build image and push
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | docker:
10 | runs-on: ubuntu-latest
11 | environment: release
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: '3.10'
20 |
21 | - name: Install dependencies
22 | run: pip install requests tqdm
23 |
24 | - name: Download models
25 | run: python scripts/download_model.py --models all
26 |
27 | - name: Set up QEMU
28 | uses: docker/setup-qemu-action@v3
29 |
30 | - name: Set up Docker Buildx
31 | uses: docker/setup-buildx-action@v3
32 |
33 | - name: Login to Docker Hub
34 | uses: docker/login-action@v3
35 | with:
36 | username: ${{ vars.DOCKERHUB_USERNAME }}
37 | password: ${{ secrets.DOCKERHUB_TOKEN }}
38 |
39 | - name: Build and push
40 | uses: docker/build-push-action@v6
41 | with:
42 | context: .
43 | platforms: linux/amd64,linux/arm64
44 | push: true
45 | tags: |
46 | ${{ vars.IMAGE_NAME }}:latest
47 | ${{ vars.IMAGE_NAME }}:${{ github.ref_name }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | **/__pycache__/
3 | .idea
4 | .vscode/*
5 | .history
6 | .DS_Store
7 | .env
8 | demo/kb_output/*.jpg
9 | demo/kb_output/*.png
10 | scripts/sync_swanhub.py
11 | scripts/sync_huggingface.py
12 | scripts/sync_modelscope.py
13 | scripts/sync_all.py
14 | **/flagged/
15 | # build outputs
16 | dist
17 | build
18 | # checkpoint
19 | *.pth
20 | *.pt
21 | *.onnx
22 | *.mnn
23 | test/temp/*
24 | !test/temp/.gitkeep
25 |
26 | .python-version
27 |
28 | # Ignore .png and .jpg files in the root directory
29 | /*.png
30 | /*.jpg
31 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-python.black-formatter",
4 | "donjayamanne.python-extension-pack",
5 | "njpwerner.autodocstring",
6 |
7 | "editorconfig.editorconfig",
8 |
9 | "gruntfuggly.todo-tree",
10 |
11 | "eamodio.gitlens",
12 |
13 | "PKief.material-icon-theme",
14 | "davidanson.vscode-markdownlint",
15 | "usernamehw.errorlens",
16 | "tamasfe.even-better-toml",
17 |
18 | "littlefoxteam.vscode-python-test-adapter"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.iconTheme": "material-icon-theme",
3 | "material-icon-theme.files.associations": {
4 | ".env.mock": "Tune",
5 | "requirements-dev.txt": "python-misc",
6 | "requirements-media.txt": "python-misc"
7 | },
8 | /** 后端代码格式化部分,python格式化 */
9 | "[python]": {
10 | "editor.defaultFormatter": "ms-python.black-formatter",
11 | "editor.formatOnSave": true
12 | },
13 | /** TODO tree 配置 */
14 | "todo-tree.general.tags": [
15 | "TODO", // 待办
16 | "FIXME", // 待修复
17 | "COMPAT", // 兼容性问题
18 | "WARNING" // 警告
19 | ],
20 | "todo-tree.highlights.customHighlight": {
21 | "TODO": {
22 | "icon": "check",
23 | "type": "tag",
24 | "foreground": "#ffff00",
25 | "iconColour": "#ffff"
26 | },
27 | "WARNING": {
28 | "icon": "alert",
29 | "type": "tag",
30 | "foreground": "#ff0000",
31 | "iconColour": "#ff0000"
32 | },
33 | "FIXME": {
34 | "icon": "flame",
35 | "type": "tag",
36 | "foreground": "#ff0000",
37 | "iconColour": "#ff0000"
38 | },
39 | "COMPAT": {
40 | "icon": "flame",
41 | "type": "tag",
42 | "foreground": "#00ff00",
43 | "iconColour": "#ffff"
44 | }
45 | },
46 |
47 | /** python代码注释 */
48 | "autoDocstring.docstringFormat": "numpy",
49 |
50 | /** markdown格式检查 */
51 | "markdownlint.config": {
52 | // 允许使用html标签
53 | "MD033": false,
54 | // 允许首行不是level1标题
55 | "MD041": false
56 | },
57 |
58 | /** 不显示文件夹 */
59 | "files.exclude": {
60 | "**/.git": true,
61 | "**/.svn": true,
62 | "**/.hg": true,
63 | "**/CVS": true,
64 | "**/.DS_Store": true,
65 | "**/Thumbs.db": true,
66 | "**/__pycache__": true,
67 | ".idea": true
68 | },
69 | "python.testing.pytestEnabled": true,
70 | "ros.distro": "humble"
71 | }
72 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-slim
2 |
3 | # Install system dependencies
4 | RUN apt-get update && apt-get install -y --no-install-recommends \
5 | ffmpeg \
6 | libgl1-mesa-glx \
7 | libglib2.0-0 \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | WORKDIR /app
11 |
12 | COPY requirements.txt requirements-app.txt ./
13 |
14 | RUN pip install --no-cache-dir -r requirements.txt -r requirements-app.txt
15 |
16 | COPY . .
17 |
18 | EXPOSE 7860
19 | EXPOSE 8080
20 |
21 | CMD ["python3", "-u", "app.py", "--host", "0.0.0.0", "--port", "7860"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | from demo.processor import IDPhotoProcessor
4 | from demo.ui import create_ui
5 | from hivision.creator.choose_handler import HUMAN_MATTING_MODELS
6 |
7 | root_dir = os.path.dirname(os.path.abspath(__file__))
8 |
9 | # 获取存在的人像分割模型列表
10 | # 通过检查 hivision/creator/weights 目录下的 .onnx 和 .mnn 文件
11 | # 只保留文件名(不包括扩展名)
12 | HUMAN_MATTING_MODELS_EXIST = [
13 | os.path.splitext(file)[0]
14 | for file in os.listdir(os.path.join(root_dir, "hivision/creator/weights"))
15 | if file.endswith(".onnx") or file.endswith(".mnn")
16 | ]
17 | # 在HUMAN_MATTING_MODELS中的模型才会被加载到Gradio中显示
18 | HUMAN_MATTING_MODELS_CHOICE = [
19 | model for model in HUMAN_MATTING_MODELS if model in HUMAN_MATTING_MODELS_EXIST
20 | ]
21 |
22 | if len(HUMAN_MATTING_MODELS_CHOICE) == 0:
23 | raise ValueError(
24 | "未找到任何存在的人像分割模型,请检查 hivision/creator/weights 目录下的文件"
25 | + "\n"
26 | + "No existing portrait segmentation model was found, please check the files in the hivision/creator/weights directory."
27 | )
28 |
29 | FACE_DETECT_MODELS = ["face++ (联网Online API)", "mtcnn"]
30 | FACE_DETECT_MODELS_EXPAND = (
31 | ["retinaface-resnet50"]
32 | if os.path.exists(
33 | os.path.join(
34 | root_dir, "hivision/creator/retinaface/weights/retinaface-resnet50.onnx"
35 | )
36 | )
37 | else []
38 | )
39 | FACE_DETECT_MODELS_CHOICE = FACE_DETECT_MODELS + FACE_DETECT_MODELS_EXPAND
40 |
41 | LANGUAGE = ["zh", "en", "ko", "ja"]
42 |
43 | if __name__ == "__main__":
44 | argparser = argparse.ArgumentParser()
45 | argparser.add_argument(
46 | "--port", type=int, default=7860, help="The port number of the server"
47 | )
48 | argparser.add_argument(
49 | "--host", type=str, default="127.0.0.1", help="The host of the server"
50 | )
51 | argparser.add_argument(
52 | "--root_path",
53 | type=str,
54 | default=None,
55 | help="The root path of the server, default is None (='/'), e.g. '/myapp'",
56 | )
57 | args = argparser.parse_args()
58 |
59 | processor = IDPhotoProcessor()
60 |
61 | demo = create_ui(
62 | processor,
63 | root_dir,
64 | HUMAN_MATTING_MODELS_CHOICE,
65 | FACE_DETECT_MODELS_CHOICE,
66 | LANGUAGE,
67 | )
68 |
69 | # 如果RUN_MODE是Beast,打印已开启野兽模式
70 | if os.getenv("RUN_MODE") == "beast":
71 | print("[Beast mode activated.] 已开启野兽模式。")
72 |
73 | demo.launch(
74 | server_name=args.host,
75 | server_port=args.port,
76 | favicon_path=os.path.join(root_dir, "assets/hivision_logo.png"),
77 | root_path=args.root_path,
78 | show_api=False,
79 | )
80 |
--------------------------------------------------------------------------------
/app.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 | from PyInstaller.utils.hooks import collect_data_files
3 |
4 | datas = [('hivisionai', 'hivisionai'), ('hivision_modnet.onnx', '.'), ('size_list_CN.csv', '.')]
5 | datas += collect_data_files('gradio_client')
6 | datas += collect_data_files('gradio')
7 |
8 |
9 | a = Analysis(
10 | ['app/web.py'],
11 | pathex=[],
12 | binaries=[],
13 | datas=datas,
14 | hiddenimports=[],
15 | hookspath=[],
16 | hooksconfig={},
17 | runtime_hooks=[],
18 | excludes=[],
19 | noarchive=False,
20 | optimize=0,
21 | )
22 | pyz = PYZ(a.pure)
23 |
24 | exe = EXE(
25 | pyz,
26 | a.scripts,
27 | a.binaries,
28 | a.datas,
29 | [],
30 | name='HivisionIDPhotos',
31 | debug=False,
32 | bootloader_ignore_signals=False,
33 | strip=False,
34 | upx=True,
35 | upx_exclude=[],
36 | runtime_tmpdir=None,
37 | console=True,
38 | disable_windowed_traceback=False,
39 | argv_emulation=False,
40 | target_arch=None,
41 | codesign_identity=None,
42 | entitlements_file=None,
43 | icon=['assets\hivisionai.ico'],
44 | )
45 |
--------------------------------------------------------------------------------
/assets/comfyui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/comfyui.png
--------------------------------------------------------------------------------
/assets/community-uniapp-wechat-miniprogram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/community-uniapp-wechat-miniprogram.png
--------------------------------------------------------------------------------
/assets/community-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/community-web.png
--------------------------------------------------------------------------------
/assets/community-wechat-miniprogram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/community-wechat-miniprogram.png
--------------------------------------------------------------------------------
/assets/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/demo.png
--------------------------------------------------------------------------------
/assets/demoImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/demoImage.jpg
--------------------------------------------------------------------------------
/assets/face++.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/face++.png
--------------------------------------------------------------------------------
/assets/gradio-image.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/gradio-image.jpeg
--------------------------------------------------------------------------------
/assets/harry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/harry.png
--------------------------------------------------------------------------------
/assets/hivision_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/hivision_logo.png
--------------------------------------------------------------------------------
/assets/social_template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/assets/social_template.png
--------------------------------------------------------------------------------
/demo/assets/american-style.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/demo/assets/american-style.png
--------------------------------------------------------------------------------
/demo/assets/color_list_CN.csv:
--------------------------------------------------------------------------------
1 | Name,Hex
2 | 蓝色,628bce
3 | 白色,ffffff
4 | 红色,d74532
5 | 黑色,000000
6 | 深蓝色,4b6190
7 | 浅灰色,f2f0f0
--------------------------------------------------------------------------------
/demo/assets/color_list_EN.csv:
--------------------------------------------------------------------------------
1 | Name,Hex
2 | Blue,628bce
3 | White,ffffff
4 | Red,d74532
5 | Black,000000
6 | Dark Blue,4b6190
7 | Light Gray,f2f0f0
--------------------------------------------------------------------------------
/demo/assets/size_list_CN.csv:
--------------------------------------------------------------------------------
1 | Name,Height,Width
2 | 一寸,413,295
3 | 二寸,626,413
4 | 小一寸,378,260
5 | 小二寸,531,413
6 | 大一寸,567,390
7 | 大二寸,626,413
8 | 五寸,1499,1050
9 | 教师资格证,413,295
10 | 国家公务员考试,413,295
11 | 初级会计考试,413,295
12 | 英语四六级考试,192,144
13 | 计算机等级考试,567,390
14 | 研究生考试,709,531
15 | 社保卡,441,358
16 | 电子驾驶证,378,260
17 | 美国签证,600,600
18 | 日本签证,413,295
19 | 韩国签证,531,413
--------------------------------------------------------------------------------
/demo/assets/size_list_EN.csv:
--------------------------------------------------------------------------------
1 | Name,Height,Width
2 | One inch,413,295
3 | Two inches,626,413
4 | Small one inch,378,260
5 | Small two inches,531,413
6 | Large one inch,567,390
7 | Large two inches,626,413
8 | Five inches,1499,1050
9 | Teacher qualification certificate,413,295
10 | National civil service exa,413,295
11 | Primary accounting exam,413,295
12 | English CET-4 and CET-6 exams,192,144
13 | Computer level exam,567,390
14 | Graduate entrance exam,709,531
15 | Social security card,441,358
16 | Electronic driver's license,378,260
17 | American visa,600,600
18 | Japanese visa,413,295
19 | Korean visa,531,413
--------------------------------------------------------------------------------
/demo/assets/title.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
HivisionIDPhotos v1.3.1
5 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | from demo.utils import csv_to_size_list, csv_to_color_list
3 |
4 |
5 | def load_configuration(root_dir):
6 | size_list_dict_CN = csv_to_size_list(
7 | os.path.join(root_dir, "assets/size_list_CN.csv")
8 | )
9 | size_list_dict_EN = csv_to_size_list(
10 | os.path.join(root_dir, "assets/size_list_EN.csv")
11 | )
12 | color_list_dict_CN = csv_to_color_list(
13 | os.path.join(root_dir, "assets/color_list_CN.csv")
14 | )
15 | color_list_dict_EN = csv_to_color_list(
16 | os.path.join(root_dir, "assets/color_list_EN.csv")
17 | )
18 |
19 | return size_list_dict_CN, size_list_dict_EN, color_list_dict_CN, color_list_dict_EN
20 |
--------------------------------------------------------------------------------
/demo/images/test0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/demo/images/test0.jpg
--------------------------------------------------------------------------------
/demo/images/test1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/demo/images/test1.jpg
--------------------------------------------------------------------------------
/demo/images/test2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/demo/images/test2.jpg
--------------------------------------------------------------------------------
/demo/images/test3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/demo/images/test3.jpg
--------------------------------------------------------------------------------
/demo/images/test4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/demo/images/test4.jpg
--------------------------------------------------------------------------------
/demo/utils.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 |
4 | def csv_to_size_list(csv_file: str) -> dict:
5 | # 初始化一个空字典
6 | size_list_dict = {}
7 |
8 | # 打开 CSV 文件并读取数据
9 | with open(csv_file, mode="r", encoding="utf-8") as file:
10 | reader = csv.reader(file)
11 | # 跳过表头
12 | next(reader)
13 | # 读取数据并填充字典
14 | for row in reader:
15 | size_name, h, w = row
16 | size_name_add_size = "{}\t\t({}, {})".format(size_name, h, w)
17 | size_list_dict[size_name_add_size] = (int(h), int(w))
18 |
19 | return size_list_dict
20 |
21 |
22 | def csv_to_color_list(csv_file: str) -> dict:
23 | # 初始化一个空字典
24 | color_list_dict = {}
25 |
26 | # 打开 CSV 文件并读取数据
27 | with open(csv_file, mode="r", encoding="utf-8") as file:
28 | reader = csv.reader(file)
29 | # 跳过表头
30 | next(reader)
31 | # 读取数据并填充字典
32 | for row in reader:
33 | color_name, hex_code = row
34 | color_list_dict[color_name] = hex_code
35 |
36 | return color_list_dict
37 |
38 |
39 | def range_check(value, min_value=0, max_value=255):
40 | value = int(value)
41 | return max(min_value, min(value, max_value))
42 |
--------------------------------------------------------------------------------
/deploy_api.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, UploadFile, Form, File
2 | from hivision import IDCreator
3 | from hivision.error import FaceError
4 | from hivision.creator.layout_calculator import (
5 | generate_layout_array,
6 | generate_layout_image,
7 | )
8 | from hivision.creator.choose_handler import choose_handler
9 | from hivision.utils import (
10 | add_background,
11 | resize_image_to_kb,
12 | bytes_2_base64,
13 | base64_2_numpy,
14 | hex_to_rgb,
15 | add_watermark,
16 | save_image_dpi_to_bytes,
17 | )
18 | import numpy as np
19 | import cv2
20 | from starlette.middleware.cors import CORSMiddleware
21 | from starlette.formparsers import MultiPartParser
22 |
23 | # 设置Starlette表单字段大小限制
24 | MultiPartParser.max_part_size = 10 * 1024 * 1024 # 10MB
25 | # 设置Starlette文件上传大小限制
26 | MultiPartParser.max_file_size = 20 * 1024 * 1024 # 20MB
27 |
28 | app = FastAPI()
29 | creator = IDCreator()
30 |
31 | # 添加 CORS 中间件 解决跨域问题
32 | app.add_middleware(
33 | CORSMiddleware,
34 | allow_origins=["*"], # 允许的请求来源
35 | allow_credentials=True, # 允许携带 Cookie
36 | allow_methods=[
37 | "*"
38 | ], # 允许的请求方法,例如:GET, POST 等,也可以指定 ["GET", "POST"]
39 | allow_headers=["*"], # 允许的请求头,也可以指定具体的头部
40 | )
41 |
42 |
43 | # 证件照智能制作接口
44 | @app.post("/idphoto")
45 | async def idphoto_inference(
46 | input_image: UploadFile = File(None),
47 | input_image_base64: str = Form(None),
48 | height: int = Form(413),
49 | width: int = Form(295),
50 | human_matting_model: str = Form("modnet_photographic_portrait_matting"),
51 | face_detect_model: str = Form("mtcnn"),
52 | hd: bool = Form(True),
53 | dpi: int = Form(300),
54 | face_align: bool = Form(False),
55 | head_measure_ratio: float = Form(0.2),
56 | head_height_ratio: float = Form(0.45),
57 | top_distance_max: float = Form(0.12),
58 | top_distance_min: float = Form(0.10),
59 | brightness_strength: float = Form(0),
60 | contrast_strength: float = Form(0),
61 | sharpen_strength: float = Form(0),
62 | saturation_strength: float = Form(0),
63 | ):
64 | # 如果传入了base64,则直接使用base64解码
65 | if input_image_base64:
66 | img = base64_2_numpy(input_image_base64)
67 | # 否则使用上传的图片
68 | else:
69 | image_bytes = await input_image.read()
70 | nparr = np.frombuffer(image_bytes, np.uint8)
71 | img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
72 |
73 | # ------------------- 选择抠图与人脸检测模型 -------------------
74 | choose_handler(creator, human_matting_model, face_detect_model)
75 |
76 | # 将字符串转为元组
77 | size = (int(height), int(width))
78 | try:
79 | result = creator(
80 | img,
81 | size=size,
82 | head_measure_ratio=head_measure_ratio,
83 | head_height_ratio=head_height_ratio,
84 | head_top_range=(top_distance_max, top_distance_min),
85 | face_alignment=face_align,
86 | brightness_strength=brightness_strength,
87 | contrast_strength=contrast_strength,
88 | sharpen_strength=sharpen_strength,
89 | saturation_strength=saturation_strength,
90 | )
91 | except FaceError:
92 | result_message = {"status": False}
93 | # 如果检测到人脸数量等于1, 则返回标准证和高清照结果(png 4通道图像)
94 | else:
95 | result_image_standard_bytes = save_image_dpi_to_bytes(cv2.cvtColor(result.standard, cv2.COLOR_RGBA2BGRA), None, dpi)
96 |
97 | result_message = {
98 | "status": True,
99 | "image_base64_standard": bytes_2_base64(result_image_standard_bytes),
100 | }
101 |
102 | # 如果hd为True, 则增加高清照结果(png 4通道图像)
103 | if hd:
104 | result_image_hd_bytes = save_image_dpi_to_bytes(cv2.cvtColor(result.hd, cv2.COLOR_RGBA2BGRA), None, dpi)
105 | result_message["image_base64_hd"] = bytes_2_base64(result_image_hd_bytes)
106 |
107 | return result_message
108 |
109 |
110 | # 人像抠图接口
111 | @app.post("/human_matting")
112 | async def human_matting_inference(
113 | input_image: UploadFile = File(None),
114 | input_image_base64: str = Form(None),
115 | human_matting_model: str = Form("hivision_modnet"),
116 | dpi: int = Form(300),
117 | ):
118 | if input_image_base64:
119 | img = base64_2_numpy(input_image_base64)
120 | else:
121 | image_bytes = await input_image.read()
122 | nparr = np.frombuffer(image_bytes, np.uint8)
123 | img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
124 |
125 | # ------------------- 选择抠图与人脸检测模型 -------------------
126 | choose_handler(creator, human_matting_model, None)
127 |
128 | try:
129 | result = creator(
130 | img,
131 | change_bg_only=True,
132 | )
133 | except FaceError:
134 | result_message = {"status": False}
135 |
136 | else:
137 | result_image_standard_bytes = save_image_dpi_to_bytes(cv2.cvtColor(result.standard, cv2.COLOR_RGBA2BGRA), None, dpi)
138 | result_message = {
139 | "status": True,
140 | "image_base64": bytes_2_base64(result_image_standard_bytes),
141 | }
142 | return result_message
143 |
144 |
145 | # 透明图像添加纯色背景接口
146 | @app.post("/add_background")
147 | async def photo_add_background(
148 | input_image: UploadFile = File(None),
149 | input_image_base64: str = Form(None),
150 | color: str = Form("000000"),
151 | kb: int = Form(None),
152 | dpi: int = Form(300),
153 | render: int = Form(0),
154 | ):
155 | render_choice = ["pure_color", "updown_gradient", "center_gradient"]
156 |
157 | if input_image_base64:
158 | img = base64_2_numpy(input_image_base64)
159 | else:
160 | image_bytes = await input_image.read()
161 | nparr = np.frombuffer(image_bytes, np.uint8)
162 | img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
163 |
164 | color = hex_to_rgb(color)
165 | color = (color[2], color[1], color[0])
166 |
167 | result_image = add_background(
168 | img,
169 | bgr=color,
170 | mode=render_choice[render],
171 | ).astype(np.uint8)
172 |
173 | result_image = cv2.cvtColor(result_image, cv2.COLOR_RGB2BGR)
174 | if kb:
175 | result_image_bytes = resize_image_to_kb(result_image, None, int(kb), dpi=dpi)
176 | else:
177 | result_image_bytes = save_image_dpi_to_bytes(result_image, None, dpi=dpi)
178 |
179 | result_messgae = {
180 | "status": True,
181 | "image_base64": bytes_2_base64(result_image_bytes),
182 | }
183 |
184 | return result_messgae
185 |
186 |
187 | # 六寸排版照生成接口
188 | @app.post("/generate_layout_photos")
189 | async def generate_layout_photos(
190 | input_image: UploadFile = File(None),
191 | input_image_base64: str = Form(None),
192 | height: int = Form(413),
193 | width: int = Form(295),
194 | kb: int = Form(None),
195 | dpi: int = Form(300),
196 | ):
197 | # try:
198 | if input_image_base64:
199 | img = base64_2_numpy(input_image_base64)
200 | else:
201 | image_bytes = await input_image.read()
202 | nparr = np.frombuffer(image_bytes, np.uint8)
203 | img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
204 |
205 | size = (int(height), int(width))
206 |
207 | typography_arr, typography_rotate = generate_layout_array(
208 | input_height=size[0], input_width=size[1]
209 | )
210 |
211 | result_layout_image = generate_layout_image(
212 | img, typography_arr, typography_rotate, height=size[0], width=size[1]
213 | ).astype(np.uint8)
214 |
215 | result_layout_image = cv2.cvtColor(result_layout_image, cv2.COLOR_RGB2BGR)
216 | if kb:
217 | result_layout_image_bytes = resize_image_to_kb(
218 | result_layout_image, None, int(kb), dpi=dpi
219 | )
220 | else:
221 | result_layout_image_bytes = save_image_dpi_to_bytes(result_layout_image, None, dpi=dpi)
222 |
223 | result_layout_image_base64 = bytes_2_base64(result_layout_image_bytes)
224 |
225 | result_messgae = {
226 | "status": True,
227 | "image_base64": result_layout_image_base64,
228 | }
229 |
230 | return result_messgae
231 |
232 |
233 | # 透明图像添加水印接口
234 | @app.post("/watermark")
235 | async def watermark(
236 | input_image: UploadFile = File(None),
237 | input_image_base64: str = Form(None),
238 | text: str = Form("Hello"),
239 | size: int = 20,
240 | opacity: float = 0.5,
241 | angle: int = 30,
242 | color: str = "#000000",
243 | space: int = 25,
244 | kb: int = Form(None),
245 | dpi: int = Form(300),
246 | ):
247 | if input_image_base64:
248 | img = base64_2_numpy(input_image_base64)
249 | else:
250 | image_bytes = await input_image.read()
251 | nparr = np.frombuffer(image_bytes, np.uint8)
252 | img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
253 |
254 | try:
255 | result_image = add_watermark(img, text, size, opacity, angle, color, space)
256 |
257 | result_image = cv2.cvtColor(result_image, cv2.COLOR_RGB2BGR)
258 | if kb:
259 | result_image_bytes = resize_image_to_kb(result_image, None, int(kb), dpi=dpi)
260 | else:
261 | result_image_bytes = save_image_dpi_to_bytes(result_image, None, dpi=dpi)
262 | result_image_base64 = bytes_2_base64(result_image_bytes)
263 |
264 | result_messgae = {
265 | "status": True,
266 | "image_base64": result_image_base64,
267 | }
268 | except Exception as e:
269 | result_messgae = {
270 | "status": False,
271 | "error": str(e),
272 | }
273 |
274 | return result_messgae
275 |
276 |
277 | # 设置照片KB值接口(RGB图)
278 | @app.post("/set_kb")
279 | async def set_kb(
280 | input_image: UploadFile = File(None),
281 | input_image_base64: str = Form(None),
282 | dpi: int = Form(300),
283 | kb: int = Form(50),
284 | ):
285 | if input_image_base64:
286 | img = base64_2_numpy(input_image_base64)
287 | else:
288 | image_bytes = await input_image.read()
289 | nparr = np.frombuffer(image_bytes, np.uint8)
290 | img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
291 |
292 | try:
293 | result_image = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
294 | result_image_bytes = resize_image_to_kb(result_image, None, int(kb), dpi=dpi)
295 | result_image_base64 = bytes_2_base64(result_image_bytes)
296 |
297 | result_messgae = {
298 | "status": True,
299 | "image_base64": result_image_base64,
300 | }
301 | except Exception as e:
302 | result_messgae = {
303 | "status": False,
304 | "error": e,
305 | }
306 |
307 | return result_messgae
308 |
309 |
310 | # 证件照智能裁剪接口
311 | @app.post("/idphoto_crop")
312 | async def idphoto_crop_inference(
313 | input_image: UploadFile = File(None),
314 | input_image_base64: str = Form(None),
315 | height: int = Form(413),
316 | width: int = Form(295),
317 | face_detect_model: str = Form("mtcnn"),
318 | hd: bool = Form(True),
319 | dpi: int = Form(300),
320 | head_measure_ratio: float = Form(0.2),
321 | head_height_ratio: float = Form(0.45),
322 | top_distance_max: float = Form(0.12),
323 | top_distance_min: float = Form(0.10),
324 | ):
325 | if input_image_base64:
326 | img = base64_2_numpy(input_image_base64)
327 | else:
328 | image_bytes = await input_image.read()
329 | nparr = np.frombuffer(image_bytes, np.uint8)
330 | img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) # 读取图像(4通道)
331 |
332 | # ------------------- 选择抠图与人脸检测模型 -------------------
333 | choose_handler(creator, face_detect_option=face_detect_model)
334 |
335 | # 将字符串转为元组
336 | size = (int(height), int(width))
337 | try:
338 | result = creator(
339 | img,
340 | size=size,
341 | head_measure_ratio=head_measure_ratio,
342 | head_height_ratio=head_height_ratio,
343 | head_top_range=(top_distance_max, top_distance_min),
344 | crop_only=True,
345 | )
346 | except FaceError:
347 | result_message = {"status": False}
348 | # 如果检测到人脸数量等于1, 则返回标准证和高清照结果(png 4通道图像)
349 | else:
350 | result_image_standard_bytes = save_image_dpi_to_bytes(cv2.cvtColor(result.standard, cv2.COLOR_RGBA2BGRA), None, dpi)
351 |
352 | result_message = {
353 | "status": True,
354 | "image_base64_standard": bytes_2_base64(result_image_standard_bytes),
355 | }
356 |
357 | # 如果hd为True, 则增加高清照结果(png 4通道图像)
358 | if hd:
359 | result_image_hd_bytes = save_image_dpi_to_bytes(cv2.cvtColor(result.hd, cv2.COLOR_RGBA2BGRA), None, dpi)
360 | result_message["image_base64_hd"] = bytes_2_base64(result_image_hd_bytes)
361 |
362 | return result_message
363 |
364 |
365 | if __name__ == "__main__":
366 | import uvicorn
367 |
368 | # 在8080端口运行推理服务
369 | uvicorn.run(app, host="0.0.0.0", port=8080)
370 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | hivision_idphotos:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | image: linzeyi/hivision_idphotos
9 | command: python3 -u app.py --host 0.0.0.0 --port 7860
10 | ports:
11 | - '7860:7860'
12 |
13 | hivision_idphotos_api:
14 | build:
15 | context: .
16 | dockerfile: Dockerfile
17 | image: linzeyi/hivision_idphotos
18 | command: python3 deploy_api.py
19 | ports:
20 | - '8080:8080'
21 |
--------------------------------------------------------------------------------
/docs/api_CN.md:
--------------------------------------------------------------------------------
1 | # API Docs
2 |
3 | [English](api_EN.md) / 中文
4 |
5 |
6 | ## 目录
7 |
8 | - [开始之前:开启后端服务](#开始之前开启后端服务)
9 | - [接口功能说明](#接口功能说明)
10 | - [1.生成证件照(底透明)](#1生成证件照底透明)
11 | - [2.添加背景色](#2添加背景色)
12 | - [3.生成六寸排版照](#3生成六寸排版照)
13 | - [4.人像抠图](#4人像抠图)
14 | - [5.图像加水印](#5图像加水印)
15 | - [6.设置图像KB大小](#6设置图像KB大小)
16 | - [7.证件照裁切](#7证件照裁切)
17 | - [cURL 请求示例](#curl-请求示例)
18 | - [Python 请求示例](#python-请求示例)
19 |
20 | ## 开始之前:开启后端服务
21 |
22 | 在请求 API 之前,请先运行后端服务
23 |
24 | ```bash
25 | python deploy_api.py
26 | ```
27 |
28 |
29 |
30 | ## 接口功能说明
31 |
32 | ### 1.生成证件照(底透明)
33 |
34 | 接口名:`idphoto`
35 |
36 | `生成证件照`接口的逻辑是发送一张 RGB 图像,输出一张标准证件照和一张高清证件照:
37 |
38 | - **高清证件照**:根据`size`的宽高比例制作的证件照,文件名为`output_image_dir`增加`_hd`后缀
39 | - **标准证件照**:尺寸等于`size`,由高清证件照缩放而来,文件名为`output_image_dir`
40 |
41 | 需要注意的是,生成的两张照片都是透明的(RGBA 四通道图像),要生成完整的证件照,还需要下面的`添加背景色`接口。
42 |
43 | > 问:为什么这么设计?
44 | > 答:因为在实际产品中,经常用户会频繁切换底色预览效果,直接给透明底图像,由前端 js 代码合成颜色是更好体验的做法。
45 |
46 | **请求参数:**
47 |
48 | | 参数名 | 类型 | 必填 | 说明 |
49 | | :--- | :--- | :--- | :--- |
50 | | input_image | file | 和`input_image_base64`二选一 | 传入的图像文件,图像文件为需为RGB三通道图像。 |
51 | | input_image_base64 | str | 和`input_image`二选一 | 传入的图像文件的base64编码,图像文件为需为RGB三通道图像。 |
52 | | height | int | 否 | 标准证件照高度,默认为`413` |
53 | | width | int | 否 | 标准证件照宽度,默认为`295` |
54 | | human_matting_model | str | 否 | 人像分割模型,默认为`modnet_photographic_portrait_matting`。可选值为`modnet_photographic_portrait_matting`、`hivision_modnet`、`rmbg-1.4`、`birefnet-v1-lite` |
55 | | face_detect_model | str | 否 | 人脸检测模型,默认为`mtcnn`。可选值为`mtcnn`、`face_plusplus`、`retinaface-resnet50` |
56 | | hd | bool | 否 | 是否生成高清证件照,默认为`true` |
57 | | dpi | int | 否 | 图像分辨率,默认为`300` |
58 | | face_alignment | bool | 否 | 是否进行人脸对齐,默认为`true` |
59 | | head_measure_ratio | float | 否 | 面部面积与照片面积的比例,默认为`0.2` |
60 | | head_height_ratio | float | 否 | 面部中心与照片顶部的高度比例,默认为`0.45` |
61 | | top_distance_max | float | 否 | 头部与照片顶部距离的比例最大值,默认为`0.12` |
62 | | top_distance_min | float | 否 | 头部与照片顶部距离的比例最小值,默认为`0.1` |
63 | | brightness_strength | float | 否 | 亮度调整强度,默认为`0` |
64 | | contrast_strength | float | 否 | 对比度调整强度,默认为`0` |
65 | | sharpen_strength | float | 否 | 锐化调整强度,默认为`0` |
66 | | saturation_strength | float | 否 | 饱和度调整强度,默认为`0` |
67 |
68 | **返回参数:**
69 |
70 | | 参数名 | 类型 | 说明 |
71 | | :--- | :--- | :--- |
72 | | status | int | 状态码,`true`表示成功 |
73 | | image_base64_standard | str | 标准证件照的base64编码 |
74 | | image_base64_hd | str | 高清证件照的base64编码。如`hd`参数为`false`,则不返回该参数 |
75 |
76 |
77 |
78 | ### 2.添加背景色
79 |
80 | 接口名:`add_background`
81 |
82 | `添加背景色`接口的逻辑是接收一张 RGBA 图像(透明图),根据`color`添加背景色,合成一张 JPG 图像。
83 |
84 | **请求参数:**
85 |
86 | | 参数名 | 类型 | 必填 | 说明 |
87 | | :--- | :--- | :--- | :--- |
88 | | input_image | file | 和`input_image_base64`二选一 | 传入的图像文件,图像文件为需为RGBA四通道图像。 |
89 | | input_image_base64 | str | 和`input_image`二选一 | 传入的图像文件的base64编码,图像文件为需为RGBA四通道图像。 |
90 | | color | str | 否 | 背景色HEX值,默认为`000000` |
91 | | kb | int | 否 | 输出照片的 KB 值,默认为`None`,即不对图像进行KB调整。|
92 | | render | int | 否 | 渲染模式,默认为`0`。可选值为`0`、`1`、`2`,分别对应`纯色`、`上下渐变`、`中心渐变`。 |
93 | | dpi | int | 否 | 图像分辨率,默认为`300` |
94 |
95 | **返回参数:**
96 |
97 | | 参数名 | 类型 | 说明 |
98 | | :--- | :--- | :--- |
99 | | status | int | 状态码,`true`表示成功 |
100 | | image_base64 | str | 添加背景色之后的图像的base64编码 |
101 |
102 |
103 |
104 | ### 3.生成六寸排版照
105 |
106 | 接口名:`generate_layout_photos`
107 |
108 | `生成六寸排版照`接口的逻辑是接收一张 RGB 图像(一般为添加背景色之后的证件照),根据`size`进行照片排布,然后生成一张六寸排版照。
109 |
110 | **请求参数:**
111 |
112 | | 参数名 | 类型 | 必填 | 说明 |
113 | | :--- | :--- | :--- | :--- |
114 | | input_image | file | 和`input_image_base64`二选一 | 传入的图像文件,图像文件为需为RGB三通道图像。 |
115 | | input_image_base64 | str | 和`input_image`二选一 | 传入的图像文件的base64编码,图像文件为需为RGB三通道图像。 |
116 | | height | int | 否 | 输入图像的高度,默认为`413` |
117 | | width | int | 否 | 输入图像的宽度,默认为`295` |
118 | | kb | int | 否 | 输出照片的 KB 值,默认为`None`,即不对图像进行KB调整。|
119 | | dpi | int | 否 | 图像分辨率,默认为`300` |
120 |
121 | **返回参数:**
122 |
123 | | 参数名 | 类型 | 说明 |
124 | | :--- | :--- | :--- |
125 | | status | int | 状态码,`true`表示成功 |
126 | | image_base64 | str | 六寸排版照的base64编码 |
127 |
128 |
129 |
130 | ### 4.人像抠图
131 |
132 | 接口名:`human_matting`
133 |
134 | `人像抠图`接口的逻辑是接收一张 RGB 图像,输出一张标准抠图人像照和高清抠图人像照(无任何背景填充)。
135 |
136 | **请求参数:**
137 |
138 | | 参数名 | 类型 | 必填 | 说明 |
139 | | :--- | :--- | :--- | :--- |
140 | | input_image | file | 是 | 传入的图像文件,图像文件为需为RGB三通道图像。 |
141 | | human_matting_model | str | 否 | 人像分割模型,默认为`modnet_photographic_portrait_matting`。可选值为`modnet_photographic_portrait_matting`、`hivision_modnet`、`rmbg-1.4`、`birefnet-v1-lite` |
142 | | dpi | int | 否 | 图像分辨率,默认为`300` |
143 |
144 | **返回参数:**
145 |
146 | | 参数名 | 类型 | 说明 |
147 | | :--- | :--- | :--- |
148 | | status | int | 状态码,`true`表示成功 |
149 | | image_base64 | str | 抠图人像照的base64编码 |
150 |
151 |
152 |
153 | ### 5.图像加水印
154 |
155 | 接口名:`watermark`
156 |
157 | `图像加水印`接口的功能是接收一个水印文本,然后在原图上添加指定的水印。用户可以指定水印的位置、透明度和大小等属性,以便将水印无缝地融合到原图中。
158 |
159 | **请求参数:**
160 |
161 | | 参数名 | 类型 | 必填 | 说明 |
162 | | :--- | :--- | :--- | :--- |
163 | | input_image | file | 和`input_image_base64`二选一 | 传入的图像文件,图像文件为需为RGB三通道图像。 |
164 | | input_image_base64 | str | 和`input_image`二选一 | 传入的图像文件的base64编码,图像文件为需为RGB三通道图像。 |
165 | | text | str | 否 | 水印文本,默认为`Hello` |
166 | | size | int | 否 | 水印字体大小,默认为`20` |
167 | | opacity | float | 否 | 水印透明度,默认为`0.5` |
168 | | angle | int | 否 | 水印旋转角度,默认为`30` |
169 | | color | str | 否 | 水印颜色,默认为`#000000` |
170 | | space | int | 否 | 水印间距,默认为`25` |
171 | | dpi | int | 否 | 图像分辨率,默认为`300` |
172 |
173 | **返回参数:**
174 |
175 | | 参数名 | 类型 | 说明 |
176 | | :--- | :--- | :--- |
177 | | status | int | 状态码,`true`表示成功 |
178 | | image_base64 | str | 添加水印之后的图像的base64编码 |
179 |
180 |
181 |
182 | ### 6.设置图像KB大小
183 |
184 | 接口名:`set_kb`
185 |
186 | `设置图像KB大小`接口的功能是接收一张图像和目标文件大小(以KB为单位),如果设置的KB值小于原文件,则调整压缩率;如果设置的KB值大于源文件,则通过给文件头添加信息的方式调大KB值,目标是让图像的最终大小与设置的KB值一致。
187 |
188 | **请求参数:**
189 |
190 | | 参数名 | 类型 | 必填 | 说明 |
191 | | :--- | :--- | :--- | :--- |
192 | | input_image | file | 和`input_image_base64`二选一 | 传入的图像文件,图像文件为需为RGB三通道图像。 |
193 | | input_image_base64 | str | 和`input_image`二选一 | 传入的图像文件的base64编码,图像文件为需为RGB三通道图像。 |
194 | | kb | int | 否 | 输出照片的 KB 值,默认为`None`,即不对图像进行KB调整。|
195 | | dpi | int | 否 | 图像分辨率,默认为`300` |
196 |
197 | **返回参数:**
198 |
199 | | 参数名 | 类型 | 说明 |
200 | | :--- | :--- | :--- |
201 | | status | int | 状态码,`true`表示成功 |
202 | | image_base64 | str | 设置KB大小之后的图像的base64编码 |
203 |
204 |
205 |
206 | ### 7.证件照裁切
207 |
208 | 接口名:`idphoto_crop`
209 |
210 | `证件照裁切`接口的功能是接收一张 RBGA 图像(透明图),输出一张标准证件照和一张高清证件照。
211 |
212 | **请求参数:**
213 |
214 | | 参数名 | 类型 | 必填 | 说明 |
215 | | :--- | :--- | :--- | :--- |
216 | | input_image | file | 和`input_image_base64`二选一 | 传入的图像文件,图像文件为需为RGBA四通道图像。 |
217 | | input_image_base64 | str | 和`input_image`二选一 | 传入的图像文件的base64编码,图像文件为需为RGBA四通道图像。 |
218 | | height | int | 否 | 标准证件照高度,默认为`413` |
219 | | width | int | 否 | 标准证件照宽度,默认为`295` |
220 | | face_detect_model | str | 否 | 人脸检测模型,默认为`mtcnn`。可选值为`mtcnn`、`face_plusplus`、`retinaface-resnet50` |
221 | | hd | bool | 否 | 是否生成高清证件照,默认为`true` |
222 | | dpi | int | 否 | 图像分辨率,默认为`300` |
223 | | head_measure_ratio | float | 否 | 面部面积与照片面积的比例,默认为`0.2` |
224 | | head_height_ratio | float | 否 | 面部中心与照片顶部的高度比例,默认为`0.45` |
225 | | top_distance_max | float | 否 | 头部与照片顶部距离的比例最大值,默认为`0.12` |
226 | | top_distance_min | float | 否 | 头部与照片顶部距离的比例最小值,默认为`0.1` |
227 |
228 | **返回参数:**
229 |
230 | | 参数名 | 类型 | 说明 |
231 | | :--- | :--- | :--- |
232 | | status | int | 状态码,`true`表示成功 |
233 | | image_base64 | str | 证件照裁切之后的图像的base64编码 |
234 | | image_base64_hd | str | 高清证件照裁切之后的图像的base64编码,如`hd`参数为`false`,则不返回该参数 |
235 |
236 |
237 |
238 | ## cURL 请求示例
239 |
240 | cURL 是一个命令行工具,用于使用各种网络协议传输数据。以下是使用 cURL 调用这些 API 的示例。
241 |
242 | ### 1. 生成证件照(底透明)
243 |
244 | ```bash
245 | curl -X POST "http://127.0.0.1:8080/idphoto" \
246 | -F "input_image=@demo/images/test0.jpg" \
247 | -F "height=413" \
248 | -F "width=295" \
249 | -F "human_matting_model=modnet_photographic_portrait_matting" \
250 | -F "face_detect_model=mtcnn" \
251 | -F "hd=true" \
252 | -F "dpi=300" \
253 | -F "face_alignment=true" \
254 | -F 'head_height_ratio=0.45' \
255 | -F 'head_measure_ratio=0.2' \
256 | -F 'top_distance_min=0.1' \
257 | -F 'top_distance_max=0.12' \
258 | -F 'sharpen_strength=0' \
259 | -F 'saturation_strength=0' \
260 | -F 'brightness_strength=10' \
261 | -F 'contrast_strength=0'
262 | ```
263 |
264 | ### 2. 添加背景色
265 |
266 | ```bash
267 | curl -X POST "http://127.0.0.1:8080/add_background" \
268 | -F "input_image=@test.png" \
269 | -F "color=638cce" \
270 | -F "kb=200" \
271 | -F "render=0" \
272 | -F "dpi=300"
273 | ```
274 |
275 | ### 3. 生成六寸排版照
276 |
277 | ```bash
278 | curl -X POST "http://127.0.0.1:8080/generate_layout_photos" \
279 | -F "input_image=@test.jpg" \
280 | -F "height=413" \
281 | -F "width=295" \
282 | -F "kb=200" \
283 | -F "dpi=300"
284 | ```
285 |
286 | ### 4. 人像抠图
287 |
288 | ```bash
289 | curl -X POST "http://127.0.0.1:8080/human_matting" \
290 | -F "input_image=@demo/images/test0.jpg" \
291 | -F "human_matting_model=modnet_photographic_portrait_matting" \
292 | -F "dpi=300"
293 | ```
294 |
295 | ### 5. 图片加水印
296 | ```bash
297 | curl -X 'POST' \
298 | 'http://127.0.0.1:8080/watermark?size=20&opacity=0.5&angle=30&color=%23000000&space=25' \
299 | -H 'accept: application/json' \
300 | -H 'Content-Type: multipart/form-data' \
301 | -F 'input_image=@demo/images/test0.jpg;type=image/jpeg' \
302 | -F 'text=Hello' \
303 | -F 'dpi=300'
304 | ```
305 |
306 | ### 6. 设置图像KB大小
307 | ```bash
308 | curl -X 'POST' \
309 | 'http://127.0.0.1:8080/set_kb' \
310 | -H 'accept: application/json' \
311 | -H 'Content-Type: multipart/form-data' \
312 | -F 'input_image=@demo/images/test0.jpg;type=image/jpeg' \
313 | -F 'kb=50' \
314 | -F 'dpi=300'
315 | ```
316 |
317 | ### 7. 证件照裁切
318 | ```bash
319 | curl -X 'POST' 'http://127.0.0.1:8080/idphoto_crop' \
320 | -H 'accept: application/json' \
321 | -H 'Content-Type: multipart/form-data' \
322 | -F 'input_image=@idphoto_matting.png;type=image/png' \
323 | -F 'height=413' \
324 | -F 'width=295' \
325 | -F 'face_detect_model=mtcnn' \
326 | -F 'hd=true' \
327 | -F 'dpi=300' \
328 | -F 'head_height_ratio=0.45' \
329 | -F 'head_measure_ratio=0.2' \
330 | -F 'top_distance_min=0.1' \
331 | -F 'top_distance_max=0.12'
332 | ```
333 |
334 |
335 |
336 | ## Python 请求示例
337 |
338 | #### 1.生成证件照(底透明)
339 | ```python
340 | import requests
341 |
342 | url = "http://127.0.0.1:8080/idphoto"
343 | input_image_path = "demo/images/test0.jpg"
344 |
345 | files = {"input_image": open(input_image_path, "rb")}
346 | data = {
347 | "height": 413,
348 | "width": 295,
349 | "human_matting_model": "modnet_photographic_portrait_matting",
350 | "face_detect_model": "mtcnn",
351 | "hd": True,
352 | "dpi": 300,
353 | "face_alignment": True,
354 | "head_measure_ratio": 0.2,
355 | "head_height_ratio": 0.45,
356 | "top_distance_max": 0.12,
357 | "top_distance_min": 0.1,
358 | "brightness_strength": 0,
359 | "contrast_strength": 0,
360 | "sharpen_strength": 0,
361 | "saturation_strength": 0,
362 | }
363 |
364 | response = requests.post(url, params=params, files=files, data=data).json()
365 |
366 | # response为一个json格式字典,包含status、image_base64_standard和image_base64_hd三项
367 | print(response)
368 | ```
369 |
370 | #### 2.添加背景色
371 |
372 | ```python
373 | import requests
374 |
375 | url = "http://127.0.0.1:8080/add_background"
376 | input_image_path = "test.png"
377 |
378 | files = {"input_image": open(input_image_path, "rb")}
379 | data = {
380 | "color": '638cce',
381 | "kb": None,
382 | "render": 0,
383 | "dpi": 300,
384 | }
385 |
386 | response = requests.post(url, files=files, data=data).json()
387 |
388 | # response为一个json格式字典,包含status和image_base64
389 | print(response)
390 | ```
391 |
392 | #### 3.生成六寸排版照
393 |
394 | ```python
395 | import requests
396 |
397 | url = "http://127.0.0.1:8080/generate_layout_photos"
398 | input_image_path = "test.jpg"
399 |
400 | files = {"input_image": open(input_image_path, "rb")}
401 | data = {
402 | "height": 413,
403 | "width": 295,
404 | "kb": 200,
405 | "dpi": 300,
406 | }
407 |
408 | response = requests.post(url, files=files, data=data).json()
409 |
410 | # response为一个json格式字典,包含status和image_base64
411 | print(response)
412 | ```
413 |
414 | #### 4.人像抠图
415 |
416 | ```python
417 | import requests
418 |
419 | url = "http://127.0.0.1:8080/human_matting"
420 | input_image_path = "test.jpg"
421 |
422 | files = {"input_image": open(input_image_path, "rb")}
423 | data = {
424 | "human_matting_model": "modnet_photographic_portrait_matting",
425 | "dpi": 300,
426 | }
427 |
428 | response = requests.post(url, files=files, data=data).json()
429 |
430 | # response为一个json格式字典,包含status和image_base64
431 | print(response)
432 | ```
433 |
434 | #### 5.图片加水印
435 |
436 | ```python
437 | import requests
438 |
439 | # 设置请求的 URL 和参数
440 | url = "http://127.0.0.1:8080/watermark"
441 | params = {
442 | "size": 20,
443 | "opacity": 0.5,
444 | "angle": 30,
445 | "color": "#000000",
446 | "space": 25,
447 | }
448 |
449 | # 设置文件和其他表单数据
450 | input_image_path = "demo/images/test0.jpg"
451 | files = {"input_image": open(input_image_path, "rb")}
452 | data = {"text": "Hello", "dpi": 300}
453 |
454 | # 发送 POST 请求
455 | response = requests.post(url, params=params, files=files, data=data)
456 |
457 | # 检查响应
458 | if response.ok:
459 | # 输出响应内容
460 | print(response.json())
461 | else:
462 | # 输出错误信息
463 | print(f"Request failed with status code {response.status_code}: {response.text}")
464 | ```
465 |
466 | ### 6. 设置图像KB大小
467 |
468 | ```python
469 | import requests
470 |
471 | # 设置请求的 URL
472 | url = "http://127.0.0.1:8080/set_kb"
473 |
474 | # 设置文件和其他表单数据
475 | input_image_path = "demo/images/test0.jpg"
476 | files = {"input_image": open(input_image_path, "rb")}
477 | data = {"kb": 50, "dpi": 300}
478 |
479 | # 发送 POST 请求
480 | response = requests.post(url, files=files, data=data)
481 |
482 | # 检查响应
483 | if response.ok:
484 | # 输出响应内容
485 | print(response.json())
486 | else:
487 | # 输出错误信息
488 | print(f"Request failed with status code {response.status_code}: {response.text}")
489 | ```
490 |
491 | ### 7. 证件照裁切
492 |
493 | ```python
494 | import requests
495 |
496 | # 设置请求的 URL
497 | url = "http://127.0.0.1:8080/idphoto_crop"
498 |
499 | # 设置文件和其他表单数据
500 | input_image_path = "idphoto_matting.png"
501 | files = {"input_image": ("idphoto_matting.png", open(input_image_path, "rb"), "image/png")}
502 | data = {
503 | "height": 413,
504 | "width": 295,
505 | "face_detect_model": "mtcnn",
506 | "hd": "true",
507 | "dpi": 300,
508 | "head_measure_ratio": 0.2,
509 | "head_height_ratio": 0.45,
510 | "top_distance_max": 0.12,
511 | "top_distance_min": 0.1,
512 | }
513 |
514 | # 发送 POST 请求
515 | response = requests.post(url, files=files, data=data)
516 |
517 | # 检查响应
518 | if response.ok:
519 | # 输出响应内容
520 | print(response.json())
521 | else:
522 | # 输出错误信息
523 | print(f"Request failed with status code {response.status_code}: {response.text}")
524 | ```
--------------------------------------------------------------------------------
/docs/face++_CN.md:
--------------------------------------------------------------------------------
1 | # Face++ 人脸检测配置文档
2 |
3 | [Face++官方文档](https://console.faceplusplus.com.cn/documents/4888373)
4 |
5 | ## 1. 注册Face++账号
6 | 要使用 Face++ 的人脸检测 API,您首先需要在 Face++ 的官方网站上注册一个账号。注册后,您将能够访问 API 控制台和相关服务。
7 |
8 | ### 步骤:
9 | 1. 访问 [Face++ 官网](https://www.faceplusplus.com.cn/)。
10 | 2. 点击“注册”按钮,填写相关信息以创建您的账号。
11 |
12 | ## 2. 获取API KEY和API SECRET
13 | 注册并登录后,您需要获取用于身份验证的 API Key 和 API Secret。这些信息是调用 API 时必需的。
14 |
15 | ### 步骤:
16 | 1. 登录到您的 Face++ 账号。
17 | 2. 进入 控制台 -> 应用管理 -> API Key。
18 | 3. 在控制台中,您将看到您的 API Key 和 API Secret。
19 |
20 | ## 3. 设置环境变量
21 | 为了在代码中安全地使用 API Key 和 API Secret,建议将它们设置为环境变量。这样可以避免在代码中硬编码敏感信息。
22 |
23 | ### 在不同操作系统中设置环境变量的步骤:
24 | - **Windows**:
25 | 1. 打开命令提示符。
26 | 2. 输入以下命令并按回车:
27 | ```cmd
28 | set FACE_PLUS_API_KEY="您的API_KEY"
29 | set FACE_PLUS_API_SECRET="您的API_SECRET"
30 | ```
31 |
32 | - **Linux / macOS**:
33 | 1. 打开终端。
34 | 2. 输入以下命令并按回车:
35 | ```bash
36 | export FACE_PLUS_API_KEY="你的API_KEY"
37 | export FACE_PLUS_API_SECRET="你的API_SECRET"
38 | ```
39 |
40 | > **注意**: 您可能需要在启动应用程序之前运行上述命令,或者将这些命令添加到您的 shell 配置文件(例如 `.bashrc` 或 `.bash_profile`)中,以便每次启动终端时自动加载。
41 |
42 | ## 4. 启动Gradio服务
43 |
44 | 运行gradio服务,在「人脸检测模型」中选择「face++」即可。
45 |
46 | ```bash
47 | python app.py
48 | ```
49 |
50 | 
51 |
52 | ## 错误码的解释
53 |
54 | https://console.faceplusplus.com.cn/documents/4888373
--------------------------------------------------------------------------------
/docs/face++_EN.md:
--------------------------------------------------------------------------------
1 | Here's the translated document in English:
2 |
3 | # Face++ Face Detection Configuration Document
4 |
5 | [Face++ Official Documentation](https://console.faceplusplus.com.cn/documents/4888373)
6 |
7 | ## 1. Register a Face++ Account
8 | To use the Face++ Face Detection API, you first need to register an account on the Face++ official website. After registration, you will be able to access the API console and related services.
9 |
10 | ### Steps:
11 | 1. Visit the [Face++ Official Website](https://www.faceplusplus.com.cn/).
12 | 2. Click the "Register" button and fill in the relevant information to create your account.
13 |
14 | ## 2. Obtain API KEY and API SECRET
15 | After registering and logging in, you need to obtain the API Key and API Secret for authentication. This information is necessary for calling the API.
16 |
17 | ### Steps:
18 | 1. Log in to your Face++ account.
19 | 2. Go to Console -> Application Management -> API Key.
20 | 3. In the console, you will see your API Key and API Secret.
21 |
22 | ## 3. Set Environment Variables
23 | To securely use the API Key and API Secret in your code, it is recommended to set them as environment variables. This avoids hardcoding sensitive information in your code.
24 |
25 | ### Steps to Set Environment Variables on Different Operating Systems:
26 | - **Windows**:
27 | 1. Open the Command Prompt.
28 | 2. Enter the following commands and press Enter:
29 | ```cmd
30 | set FACE_PLUS_API_KEY="Your_API_KEY"
31 | set FACE_PLUS_API_SECRET="Your_API_SECRET"
32 | ```
33 |
34 | - **Linux / macOS**:
35 | 1. Open the terminal.
36 | 2. Enter the following commands and press Enter:
37 | ```bash
38 | export FACE_PLUS_API_KEY="Your_API_KEY"
39 | export FACE_PLUS_API_SECRET="Your_API_SECRET"
40 | ```
41 |
42 | > **Note**: You may need to run the above commands before starting your application, or add these commands to your shell configuration file (e.g., `.bashrc` or `.bash_profile`) so that they are automatically loaded each time you start the terminal.
43 |
44 | ## 4. Start Gradio Service
45 | Run the Gradio service, and select "face++" in the "Face Detection Model".
46 |
47 | ```bash
48 | python app.py
49 | ```
50 |
51 | 
52 |
53 | ## Explanation of error codes
54 |
55 | https://console.faceplusplus.com.cn/documents/4888373
--------------------------------------------------------------------------------
/hivision/__init__.py:
--------------------------------------------------------------------------------
1 | from .creator import IDCreator, Params as IDParams, Result as IDResult
2 |
3 |
4 | __all__ = ["IDCreator", "IDParams", "IDResult", "utils", "error"]
5 |
--------------------------------------------------------------------------------
/hivision/creator/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 16:45
5 | @File: __init__.py
6 | @IDE: pycharm
7 | @Description:
8 | 创建证件照
9 | """
10 | import numpy as np
11 | from typing import Tuple
12 | import hivision.creator.utils as U
13 | from .context import Context, ContextHandler, Params, Result
14 | from .human_matting import extract_human
15 | from .face_detector import detect_face_mtcnn
16 | from hivision.plugin.beauty.handler import beauty_face
17 | from .photo_adjuster import adjust_photo
18 | import cv2
19 | import time
20 |
21 |
22 | class IDCreator:
23 | """
24 | 证件照创建类,包含完整的证件照流程
25 | """
26 |
27 | def __init__(self):
28 | # 回调时机
29 | self.before_all: ContextHandler = None
30 | """
31 | 在所有处理之前,此时图像已经被 resize 到最大边长为 2000
32 | """
33 | self.after_matting: ContextHandler = None
34 | """
35 | 在抠图之后,ctx.matting_image 被赋值
36 | """
37 | self.after_detect: ContextHandler = None
38 | """
39 | 在人脸检测之后,ctx.face 被赋值,如果为仅换底,则不会执行此回调
40 | """
41 | self.after_all: ContextHandler = None
42 | """
43 | 在所有处理之后,此时 ctx.result 被赋值
44 | """
45 | # 处理者
46 | self.matting_handler: ContextHandler = extract_human
47 | self.detection_handler: ContextHandler = detect_face_mtcnn
48 | self.beauty_handler: ContextHandler = beauty_face
49 | # 上下文
50 | self.ctx = None
51 |
52 | def __call__(
53 | self,
54 | image: np.ndarray,
55 | size: Tuple[int, int] = (413, 295),
56 | change_bg_only: bool = False,
57 | crop_only: bool = False,
58 | head_measure_ratio: float = 0.2,
59 | head_height_ratio: float = 0.45,
60 | head_top_range: float = (0.12, 0.1),
61 | face: Tuple[int, int, int, int] = None,
62 | whitening_strength: int = 0,
63 | brightness_strength: int = 0,
64 | contrast_strength: int = 0,
65 | sharpen_strength: int = 0,
66 | saturation_strength: int = 0,
67 | face_alignment: bool = False,
68 | ) -> Result:
69 | """
70 | 证件照处理函数
71 | :param image: 输入图像
72 | :param change_bg_only: 是否只需要抠图
73 | :param crop_only: 是否只需要裁剪
74 | :param size: 输出的图像大小(h,w)
75 | :param head_measure_ratio: 人脸面积与全图面积的期望比值
76 | :param head_height_ratio: 人脸中心处在全图高度的比例期望值
77 | :param head_top_range: 头距离顶部的比例(max,min)
78 | :param face: 人脸坐标
79 | :param whitening_strength: 美白强度
80 | :param brightness_strength: 亮度强度
81 | :param contrast_strength: 对比度强度
82 | :param sharpen_strength: 锐化强度
83 | :param align_face: 是否需要人脸矫正
84 |
85 | :return: 返回处理后的证件照和一系列参数
86 | """
87 | # 0.初始化上下文
88 | params = Params(
89 | size=size,
90 | change_bg_only=change_bg_only,
91 | head_measure_ratio=head_measure_ratio,
92 | head_height_ratio=head_height_ratio,
93 | head_top_range=head_top_range,
94 | crop_only=crop_only,
95 | face=face,
96 | whitening_strength=whitening_strength,
97 | brightness_strength=brightness_strength,
98 | contrast_strength=contrast_strength,
99 | sharpen_strength=sharpen_strength,
100 | saturation_strength=saturation_strength,
101 | face_alignment=face_alignment,
102 | )
103 |
104 |
105 | # 总的开始时间
106 | total_start_time = time.time()
107 |
108 | self.ctx = Context(params)
109 | ctx = self.ctx
110 | ctx.processing_image = image
111 | ctx.processing_image = U.resize_image_esp(
112 | ctx.processing_image, 2000
113 | ) # 将输入图片 resize 到最大边长为 2000
114 | ctx.origin_image = ctx.processing_image.copy()
115 | self.before_all and self.before_all(ctx)
116 |
117 | # 1. ------------------人像抠图------------------
118 | # 如果仅裁剪,则不进行抠图
119 | if not ctx.params.crop_only:
120 | # 调用抠图工作流
121 | print("[1] Start Human Matting...")
122 | start_matting_time = time.time()
123 | self.matting_handler(ctx)
124 | end_matting_time = time.time()
125 | print(f"[1] Human Matting Time: {end_matting_time - start_matting_time:.3f}s")
126 | self.after_matting and self.after_matting(ctx)
127 | # 如果进行抠图
128 | else:
129 | ctx.matting_image = ctx.processing_image
130 |
131 |
132 | # 2. ------------------美颜------------------
133 | print("[2] Start Beauty...")
134 | start_beauty_time = time.time()
135 | self.beauty_handler(ctx)
136 | end_beauty_time = time.time()
137 | print(f"[2] Beauty Time: {end_beauty_time - start_beauty_time:.3f}s")
138 |
139 | # 如果仅换底,则直接返回抠图结果
140 | if ctx.params.change_bg_only:
141 | ctx.result = Result(
142 | standard=ctx.matting_image,
143 | hd=ctx.matting_image,
144 | matting=ctx.matting_image,
145 | clothing_params=None,
146 | typography_params=None,
147 | face=None,
148 | )
149 | self.after_all and self.after_all(ctx)
150 | return ctx.result
151 |
152 | # 3. ------------------人脸检测------------------
153 | print("[3] Start Face Detection...")
154 | start_detection_time = time.time()
155 | self.detection_handler(ctx)
156 | end_detection_time = time.time()
157 | print(f"[3] Face Detection Time: {end_detection_time - start_detection_time:.3f}s")
158 | self.after_detect and self.after_detect(ctx)
159 |
160 | # 3.1 ------------------人脸对齐------------------
161 | if ctx.params.face_alignment and abs(ctx.face["roll_angle"]) > 2:
162 | print("[3.1] Start Face Alignment...")
163 | start_alignment_time = time.time()
164 | from hivision.creator.rotation_adjust import rotate_bound_4channels
165 |
166 | # 根据角度旋转原图和抠图
167 | b, g, r, a = cv2.split(ctx.matting_image)
168 | ctx.origin_image, ctx.matting_image, _, _, _, _ = rotate_bound_4channels(
169 | cv2.merge((b, g, r)),
170 | a,
171 | -1 * ctx.face["roll_angle"],
172 | )
173 |
174 | # 旋转后再执行一遍人脸检测
175 | self.detection_handler(ctx)
176 | self.after_detect and self.after_detect(ctx)
177 | end_alignment_time = time.time()
178 | print(f"[3.1] Face Alignment Time: {end_alignment_time - start_alignment_time:.3f}s")
179 |
180 | # 4. ------------------图像调整------------------
181 | print("[4] Start Image Post-Adjustment...")
182 | start_adjust_time = time.time()
183 | result_image_hd, result_image_standard, clothing_params, typography_params = (
184 | adjust_photo(ctx)
185 | )
186 | end_adjust_time = time.time()
187 | print(f"[4] Image Post-Adjustment Time: {end_adjust_time - start_adjust_time:.3f}s")
188 |
189 | # 5. ------------------返回结果------------------
190 | ctx.result = Result(
191 | standard=result_image_standard,
192 | hd=result_image_hd,
193 | matting=ctx.matting_image,
194 | clothing_params=clothing_params,
195 | typography_params=typography_params,
196 | face=ctx.face,
197 | )
198 | self.after_all and self.after_all(ctx)
199 |
200 | # 总的结束时间
201 | total_end_time = time.time()
202 | print(f"[Total] Total Time: {total_end_time - total_start_time:.3f}s")
203 |
204 | return ctx.result
205 |
--------------------------------------------------------------------------------
/hivision/creator/choose_handler.py:
--------------------------------------------------------------------------------
1 | from hivision.creator.human_matting import *
2 | from hivision.creator.face_detector import *
3 |
4 |
5 | HUMAN_MATTING_MODELS = [
6 | "modnet_photographic_portrait_matting",
7 | "birefnet-v1-lite",
8 | "hivision_modnet",
9 | "rmbg-1.4",
10 | ]
11 |
12 | FACE_DETECT_MODELS = ["face++ (联网Online API)", "mtcnn", "retinaface-resnet50"]
13 |
14 |
15 | def choose_handler(creator, matting_model_option=None, face_detect_option=None):
16 | if matting_model_option == "modnet_photographic_portrait_matting":
17 | creator.matting_handler = extract_human_modnet_photographic_portrait_matting
18 | elif matting_model_option == "mnn_hivision_modnet":
19 | creator.matting_handler = extract_human_mnn_modnet
20 | elif matting_model_option == "rmbg-1.4":
21 | creator.matting_handler = extract_human_rmbg
22 | elif matting_model_option == "birefnet-v1-lite":
23 | creator.matting_handler = extract_human_birefnet_lite
24 | else:
25 | creator.matting_handler = extract_human
26 |
27 | if (
28 | face_detect_option == "face_plusplus"
29 | or face_detect_option == "face++ (联网Online API)"
30 | ):
31 | creator.detection_handler = detect_face_face_plusplus
32 | elif face_detect_option == "retinaface-resnet50":
33 | creator.detection_handler = detect_face_retinaface
34 | else:
35 | creator.detection_handler = detect_face_mtcnn
36 |
--------------------------------------------------------------------------------
/hivision/creator/context.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 19:20
5 | @File: context.py
6 | @IDE: pycharm
7 | @Description:
8 | 证件照创建上下文类,用于同步信息
9 | """
10 | from typing import Optional, Callable, Tuple
11 | import numpy as np
12 |
13 |
14 | class Params:
15 | def __init__(
16 | self,
17 | size: Tuple[int, int] = (413, 295),
18 | change_bg_only: bool = False,
19 | crop_only: bool = False,
20 | head_measure_ratio: float = 0.2,
21 | head_height_ratio: float = 0.45,
22 | head_top_range: float = (0.12, 0.1),
23 | face: Tuple[int, int, int, int] = None,
24 | whitening_strength: int = 0,
25 | brightness_strength: int = 0,
26 | contrast_strength: int = 0,
27 | sharpen_strength: int = 0,
28 | saturation_strength: int = 0,
29 | face_alignment: bool = False,
30 | ):
31 | self.__size = size
32 | self.__change_bg_only = change_bg_only
33 | self.__crop_only = crop_only
34 | self.__head_measure_ratio = head_measure_ratio
35 | self.__head_height_ratio = head_height_ratio
36 | self.__head_top_range = head_top_range
37 | self.__face = face
38 | self.__whitening_strength = whitening_strength
39 | self.__brightness_strength = brightness_strength
40 | self.__contrast_strength = contrast_strength
41 | self.__sharpen_strength = sharpen_strength
42 | self.__saturation_strength = saturation_strength
43 | self.__face_alignment = face_alignment
44 |
45 | @property
46 | def size(self):
47 | return self.__size
48 |
49 | @property
50 | def change_bg_only(self):
51 | return self.__change_bg_only
52 |
53 | @property
54 | def head_measure_ratio(self):
55 | return self.__head_measure_ratio
56 |
57 | @property
58 | def head_height_ratio(self):
59 | return self.__head_height_ratio
60 |
61 | @property
62 | def head_top_range(self):
63 | return self.__head_top_range
64 |
65 | @property
66 | def crop_only(self):
67 | return self.__crop_only
68 |
69 | @property
70 | def face(self):
71 | return self.__face
72 |
73 | @property
74 | def whitening_strength(self):
75 | return self.__whitening_strength
76 |
77 | @property
78 | def brightness_strength(self):
79 | return self.__brightness_strength
80 |
81 | @property
82 | def contrast_strength(self):
83 | return self.__contrast_strength
84 |
85 | @property
86 | def sharpen_strength(self):
87 | return self.__sharpen_strength
88 |
89 | @property
90 | def saturation_strength(self):
91 | return self.__saturation_strength
92 |
93 | @property
94 | def face_alignment(self):
95 | return self.__face_alignment
96 |
97 |
98 | class Result:
99 | def __init__(
100 | self,
101 | standard: np.ndarray,
102 | hd: np.ndarray,
103 | matting: np.ndarray,
104 | clothing_params: Optional[dict],
105 | typography_params: Optional[dict],
106 | face: Optional[Tuple[int, int, int, int, float]],
107 | ):
108 | self.standard = standard
109 | self.hd = hd
110 | self.matting = matting
111 | self.clothing_params = clothing_params
112 | """
113 | 服装参数,仅换底时为 None
114 | """
115 | self.typography_params = typography_params
116 | """
117 | 排版参数,仅换底时为 None
118 | """
119 | self.face = face
120 |
121 | def __iter__(self):
122 | return iter(
123 | [
124 | self.standard,
125 | self.hd,
126 | self.matting,
127 | self.clothing_params,
128 | self.typography_params,
129 | self.face,
130 | ]
131 | )
132 |
133 |
134 | class Context:
135 | def __init__(self, params: Params):
136 | self.params: Params = params
137 | """
138 | 证件照处理参数
139 | """
140 | self.origin_image: Optional[np.ndarray] = None
141 | """
142 | 输入的原始图像,处理时会进行resize,长宽不一定等于输入图像
143 | """
144 | self.processing_image: Optional[np.ndarray] = None
145 | """
146 | 当前正在处理的图像
147 | """
148 | self.matting_image: Optional[np.ndarray] = None
149 | """
150 | 人像抠图结果
151 | """
152 | self.face: dict = dict(rectangle=None, roll_angle=None)
153 | """
154 | 人脸检测结果,大于一个人脸时已在上层抛出异常
155 | rectangle: 人脸矩形框,包含 x1, y1, width, height 的坐标, x1, y1 为左上角坐标, width, height 为矩形框的宽度和高度
156 | roll_angle: 人脸偏转角度,以眼睛为标准,计算的人脸偏转角度,用于人脸矫正
157 | """
158 | self.result: Optional[Result] = None
159 | """
160 | 证件照处理结果
161 | """
162 | self.align_info: Optional[dict] = None
163 | """
164 | 人脸矫正信息,仅当 align_face 为 True 时存在
165 | """
166 |
167 |
168 | ContextHandler = Optional[Callable[[Context], None]]
169 |
--------------------------------------------------------------------------------
/hivision/creator/face_detector.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 19:32
5 | @File: face_detector.py
6 | @IDE: pycharm
7 | @Description:
8 | 人脸检测器
9 | """
10 | try:
11 | from mtcnnruntime import MTCNN
12 | except ImportError:
13 | raise ImportError(
14 | "Please install mtcnn-runtime by running `pip install mtcnn-runtime`"
15 | )
16 | from .context import Context
17 | from hivision.error import FaceError, APIError
18 | from hivision.utils import resize_image_to_kb_base64
19 | from hivision.creator.retinaface import retinaface_detect_faces
20 | import requests
21 | import cv2
22 | import os
23 | import numpy as np
24 |
25 |
26 | mtcnn = None
27 | base_dir = os.path.dirname(os.path.abspath(__file__))
28 | RETINAFCE_SESS = None
29 |
30 |
31 | def detect_face_mtcnn(ctx: Context, scale: int = 2):
32 | """
33 | 基于MTCNN模型的人脸检测处理器,只进行人脸数量的检测
34 | :param ctx: 上下文,此时已获取到原始图和抠图结果,但是我们只需要原始图
35 | :param scale: 最大边长缩放比例,原图:缩放图 = 1:scale
36 | :raise FaceError: 人脸检测错误,多个人脸或者没有人脸
37 | """
38 | global mtcnn
39 | if mtcnn is None:
40 | mtcnn = MTCNN()
41 | image = cv2.resize(
42 | ctx.origin_image,
43 | (ctx.origin_image.shape[1] // scale, ctx.origin_image.shape[0] // scale),
44 | interpolation=cv2.INTER_AREA,
45 | )
46 | # landmarks 是 5 个关键点,分别是左眼、右眼、鼻子、左嘴角、右嘴角,
47 | faces, landmarks = mtcnn.detect(image, thresholds=[0.8, 0.8, 0.8])
48 |
49 | # print(len(faces))
50 | if len(faces) != 1:
51 | # 保险措施,如果检测到多个人脸或者没有人脸,用原图再检测一次
52 | faces, landmarks = mtcnn.detect(ctx.origin_image)
53 | else:
54 | # 如果只有一个人脸,将人脸坐标放大
55 | for item, param in enumerate(faces[0]):
56 | faces[0][item] = param * 2
57 | if len(faces) != 1:
58 | raise FaceError("Expected 1 face, but got {}".format(len(faces)), len(faces))
59 |
60 | # 计算人脸坐标
61 | left = faces[0][0]
62 | top = faces[0][1]
63 | width = faces[0][2] - left + 1
64 | height = faces[0][3] - top + 1
65 | ctx.face["rectangle"] = (left, top, width, height)
66 |
67 | # 根据landmarks计算人脸偏转角度,以眼睛为标准,计算的人脸偏转角度,用于人脸矫正
68 | # 示例landmarks [106.37181 150.77415 127.21012 108.369156 144.61522 105.24723 107.45625 133.62355 151.24269 153.34407 ]
69 | landmarks = landmarks[0]
70 | left_eye = np.array([landmarks[0], landmarks[5]])
71 | right_eye = np.array([landmarks[1], landmarks[6]])
72 | dy = right_eye[1] - left_eye[1]
73 | dx = right_eye[0] - left_eye[0]
74 | roll_angle = np.degrees(np.arctan2(dy, dx))
75 |
76 | ctx.face["roll_angle"] = roll_angle
77 |
78 |
79 | def detect_face_face_plusplus(ctx: Context):
80 | """
81 | 基于Face++ API接口的人脸检测处理器,只进行人脸数量的检测
82 | :param ctx: 上下文,此时已获取到原始图和抠图结果,但是我们只需要原始图
83 | :param scale: 最大边长缩放比例,原图:缩放图 = 1:scale
84 | :raise FaceError: 人脸检测错误,多个人脸或者没有人脸
85 | """
86 | url = "https://api-cn.faceplusplus.com/facepp/v3/detect"
87 | api_key = os.getenv("FACE_PLUS_API_KEY")
88 | api_secret = os.getenv("FACE_PLUS_API_SECRET")
89 |
90 | print("调用了face++")
91 |
92 | image = ctx.origin_image
93 | # 将图片转为 base64, 且不大于2MB(Face++ API接口限制)
94 | image_base64 = resize_image_to_kb_base64(image, 2000, mode="max")
95 |
96 | files = {
97 | "api_key": (None, api_key),
98 | "api_secret": (None, api_secret),
99 | "image_base64": (None, image_base64),
100 | "return_landmark": (None, "1"),
101 | "return_attributes": (None, "headpose"),
102 | }
103 |
104 | # 发送 POST 请求
105 | response = requests.post(url, files=files)
106 |
107 | # 获取响应状态码
108 | status_code = response.status_code
109 | response_json = response.json()
110 |
111 | if status_code == 200:
112 | face_num = response_json["face_num"]
113 | if face_num == 1:
114 | face_rectangle = response_json["faces"][0]["face_rectangle"]
115 |
116 | # 获取人脸关键点
117 | # landmarks = response_json["faces"][0]["landmark"]
118 | # print("face++ landmarks", landmarks)
119 |
120 | # headpose 是一个字典,包含俯仰角(pitch)、偏航角(yaw)和滚转角(roll)
121 | # headpose示例 {'pitch_angle': 6.997899, 'roll_angle': 1.8011835, 'yaw_angle': 5.043002}
122 | headpose = response_json["faces"][0]["attributes"]["headpose"]
123 | # 以眼睛为标准,计算的人脸偏转角度,用于人脸矫正
124 | roll_angle = headpose["roll_angle"] / 2
125 |
126 | ctx.face["rectangle"] = (
127 | face_rectangle["left"],
128 | face_rectangle["top"],
129 | face_rectangle["width"],
130 | face_rectangle["height"],
131 | )
132 | ctx.face["roll_angle"] = roll_angle
133 | else:
134 | raise FaceError(
135 | "Expected 1 face, but got {}".format(face_num), len(face_num)
136 | )
137 |
138 | elif status_code == 401:
139 | raise APIError(
140 | f"Face++ Status code {status_code} Authentication error: API key and secret do not match.",
141 | status_code,
142 | )
143 |
144 | elif status_code == 403:
145 | reason = response_json.get("error_message", "Unknown authorization error.")
146 | raise APIError(
147 | f"Authorization error: {reason}",
148 | status_code,
149 | )
150 |
151 | elif status_code == 400:
152 | error_message = response_json.get("error_message", "Bad request.")
153 | raise APIError(
154 | f"Bad request error: {error_message}",
155 | status_code,
156 | )
157 |
158 | elif status_code == 413:
159 | raise APIError(
160 | f"Face++ Status code {status_code} Request entity too large: The image exceeds the 2MB limit.",
161 | status_code,
162 | )
163 |
164 |
165 | def detect_face_retinaface(ctx: Context):
166 | """
167 | 基于RetinaFace模型的人脸检测处理器,只进行人脸数量的检测
168 | :param ctx: 上下文,此时已获取到原始图和抠图结果,但是我们只需要原始图
169 | :raise FaceError: 人脸检测错误,多个人脸或者没有人脸
170 | """
171 | from time import time
172 |
173 | global RETINAFCE_SESS
174 |
175 | if RETINAFCE_SESS is None:
176 | # 计算用时
177 | tic = time()
178 | faces_dets, sess = retinaface_detect_faces(
179 | ctx.origin_image,
180 | os.path.join(base_dir, "retinaface/weights/retinaface-resnet50.onnx"),
181 | sess=None,
182 | )
183 | RETINAFCE_SESS = sess
184 | else:
185 | tic = time()
186 | faces_dets, _ = retinaface_detect_faces(
187 | ctx.origin_image,
188 | os.path.join(base_dir, "retinaface/weights/retinaface-resnet50.onnx"),
189 | sess=RETINAFCE_SESS,
190 | )
191 |
192 | faces_num = len(faces_dets)
193 | faces_landmarks = []
194 | for face_det in faces_dets:
195 | faces_landmarks.append(face_det[5:])
196 |
197 | if faces_num != 1:
198 | raise FaceError("Expected 1 face, but got {}".format(faces_num), faces_num)
199 | face_det = faces_dets[0]
200 | ctx.face["rectangle"] = (
201 | face_det[0],
202 | face_det[1],
203 | face_det[2] - face_det[0] + 1,
204 | face_det[3] - face_det[1] + 1,
205 | )
206 |
207 | # 计算roll_angle
208 | face_landmarks = faces_landmarks[0]
209 | # print("face_landmarks", face_landmarks)
210 | left_eye = np.array([face_landmarks[0], face_landmarks[1]])
211 | right_eye = np.array([face_landmarks[2], face_landmarks[3]])
212 | dy = right_eye[1] - left_eye[1]
213 | dx = right_eye[0] - left_eye[0]
214 | roll_angle = np.degrees(np.arctan2(dy, dx))
215 | ctx.face["roll_angle"] = roll_angle
216 |
217 | # 如果RUN_MODE不是野兽模式,则释放模型
218 | if os.getenv("RUN_MODE") == "beast":
219 | RETINAFCE_SESS = None
--------------------------------------------------------------------------------
/hivision/creator/human_matting.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 21:21
5 | @File: human_matting.py
6 | @IDE: pycharm
7 | @Description:
8 | 人像抠图
9 | """
10 | import numpy as np
11 | from PIL import Image
12 | import onnxruntime
13 | from .tensor2numpy import NNormalize, NTo_Tensor, NUnsqueeze
14 | from .context import Context
15 | import cv2
16 | import os
17 | from time import time
18 |
19 |
20 | WEIGHTS = {
21 | "hivision_modnet": os.path.join(
22 | os.path.dirname(__file__), "weights", "hivision_modnet.onnx"
23 | ),
24 | "modnet_photographic_portrait_matting": os.path.join(
25 | os.path.dirname(__file__),
26 | "weights",
27 | "modnet_photographic_portrait_matting.onnx",
28 | ),
29 | "mnn_hivision_modnet": os.path.join(
30 | os.path.dirname(__file__),
31 | "weights",
32 | "mnn_hivision_modnet.mnn",
33 | ),
34 | "rmbg-1.4": os.path.join(os.path.dirname(__file__), "weights", "rmbg-1.4.onnx"),
35 | "birefnet-v1-lite": os.path.join(
36 | os.path.dirname(__file__), "weights", "birefnet-v1-lite.onnx"
37 | ),
38 | }
39 |
40 | ONNX_DEVICE = onnxruntime.get_device()
41 | ONNX_PROVIDER = (
42 | "CUDAExecutionProvider" if ONNX_DEVICE == "GPU" else "CPUExecutionProvider"
43 | )
44 |
45 | HIVISION_MODNET_SESS = None
46 | MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS = None
47 | RMBG_SESS = None
48 | BIREFNET_V1_LITE_SESS = None
49 |
50 |
51 | def load_onnx_model(checkpoint_path, set_cpu=False):
52 | providers = (
53 | ["CUDAExecutionProvider", "CPUExecutionProvider"]
54 | if ONNX_PROVIDER == "CUDAExecutionProvider"
55 | else ["CPUExecutionProvider"]
56 | )
57 |
58 | if set_cpu:
59 | sess = onnxruntime.InferenceSession(
60 | checkpoint_path, providers=["CPUExecutionProvider"]
61 | )
62 | else:
63 | try:
64 | sess = onnxruntime.InferenceSession(checkpoint_path, providers=providers)
65 | except Exception as e:
66 | if ONNX_DEVICE == "CUDAExecutionProvider":
67 | print(f"Failed to load model with CUDAExecutionProvider: {e}")
68 | print("Falling back to CPUExecutionProvider")
69 | # 尝试使用CPU加载模型
70 | sess = onnxruntime.InferenceSession(
71 | checkpoint_path, providers=["CPUExecutionProvider"]
72 | )
73 | else:
74 | raise e # 如果是CPU执行失败,重新抛出异常
75 |
76 | return sess
77 |
78 |
79 | def extract_human(ctx: Context):
80 | """
81 | 人像抠图
82 | :param ctx: 上下文
83 | """
84 | # 抠图
85 | matting_image = get_modnet_matting(ctx.processing_image, WEIGHTS["hivision_modnet"])
86 | # 修复抠图
87 | ctx.processing_image = hollow_out_fix(matting_image)
88 | ctx.matting_image = ctx.processing_image.copy()
89 |
90 |
91 | def extract_human_modnet_photographic_portrait_matting(ctx: Context):
92 | """
93 | 人像抠图
94 | :param ctx: 上下文
95 | """
96 | # 抠图
97 | matting_image = get_modnet_matting_photographic_portrait_matting(
98 | ctx.processing_image, WEIGHTS["modnet_photographic_portrait_matting"]
99 | )
100 | # 修复抠图
101 | ctx.processing_image = matting_image
102 | ctx.matting_image = ctx.processing_image.copy()
103 |
104 |
105 | def extract_human_mnn_modnet(ctx: Context):
106 | matting_image = get_mnn_modnet_matting(
107 | ctx.processing_image, WEIGHTS["mnn_hivision_modnet"]
108 | )
109 | ctx.processing_image = hollow_out_fix(matting_image)
110 | ctx.matting_image = ctx.processing_image.copy()
111 |
112 |
113 | def extract_human_rmbg(ctx: Context):
114 | matting_image = get_rmbg_matting(ctx.processing_image, WEIGHTS["rmbg-1.4"])
115 | ctx.processing_image = matting_image
116 | ctx.matting_image = ctx.processing_image.copy()
117 |
118 |
119 | # def extract_human_birefnet_portrait(ctx: Context):
120 | # matting_image = get_birefnet_portrait_matting(
121 | # ctx.processing_image, WEIGHTS["birefnet-portrait"]
122 | # )
123 | # ctx.processing_image = matting_image
124 | # ctx.matting_image = ctx.processing_image.copy()
125 |
126 |
127 | def extract_human_birefnet_lite(ctx: Context):
128 | matting_image = get_birefnet_portrait_matting(
129 | ctx.processing_image, WEIGHTS["birefnet-v1-lite"]
130 | )
131 | ctx.processing_image = matting_image
132 | ctx.matting_image = ctx.processing_image.copy()
133 |
134 |
135 | def hollow_out_fix(src: np.ndarray) -> np.ndarray:
136 | """
137 | 修补抠图区域,作为抠图模型精度不够的补充
138 | :param src:
139 | :return:
140 | """
141 | b, g, r, a = cv2.split(src)
142 | src_bgr = cv2.merge((b, g, r))
143 | # -----------padding---------- #
144 | add_area = np.zeros((10, a.shape[1]), np.uint8)
145 | a = np.vstack((add_area, a, add_area))
146 | add_area = np.zeros((a.shape[0], 10), np.uint8)
147 | a = np.hstack((add_area, a, add_area))
148 | # -------------end------------ #
149 | _, a_threshold = cv2.threshold(a, 127, 255, 0)
150 | a_erode = cv2.erode(
151 | a_threshold,
152 | kernel=cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)),
153 | iterations=3,
154 | )
155 | contours, hierarchy = cv2.findContours(
156 | a_erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
157 | )
158 | contours = [x for x in contours]
159 | # contours = np.squeeze(contours)
160 | contours.sort(key=lambda c: cv2.contourArea(c), reverse=True)
161 | a_contour = cv2.drawContours(np.zeros(a.shape, np.uint8), contours[0], -1, 255, 2)
162 | # a_base = a_contour[1:-1, 1:-1]
163 | h, w = a.shape[:2]
164 | mask = np.zeros(
165 | [h + 2, w + 2], np.uint8
166 | ) # mask 必须行和列都加 2,且必须为 uint8 单通道阵列
167 | cv2.floodFill(a_contour, mask=mask, seedPoint=(0, 0), newVal=255)
168 | a = cv2.add(a, 255 - a_contour)
169 | return cv2.merge((src_bgr, a[10:-10, 10:-10]))
170 |
171 |
172 | def image2bgr(input_image):
173 | if len(input_image.shape) == 2:
174 | input_image = input_image[:, :, None]
175 | if input_image.shape[2] == 1:
176 | result_image = np.repeat(input_image, 3, axis=2)
177 | elif input_image.shape[2] == 4:
178 | result_image = input_image[:, :, 0:3]
179 | else:
180 | result_image = input_image
181 |
182 | return result_image
183 |
184 |
185 | def read_modnet_image(input_image, ref_size=512):
186 | im = Image.fromarray(np.uint8(input_image))
187 | width, length = im.size[0], im.size[1]
188 | im = np.asarray(im)
189 | im = image2bgr(im)
190 | im = cv2.resize(im, (ref_size, ref_size), interpolation=cv2.INTER_AREA)
191 | im = NNormalize(im, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]))
192 | im = NUnsqueeze(NTo_Tensor(im))
193 |
194 | return im, width, length
195 |
196 |
197 | def get_modnet_matting(input_image, checkpoint_path, ref_size=512):
198 | global HIVISION_MODNET_SESS
199 |
200 | if not os.path.exists(checkpoint_path):
201 | print(f"Checkpoint file not found: {checkpoint_path}")
202 | return None
203 |
204 | # 如果RUN_MODE不是野兽模式,则不加载模型
205 | if HIVISION_MODNET_SESS is None:
206 | HIVISION_MODNET_SESS = load_onnx_model(checkpoint_path, set_cpu=True)
207 |
208 | input_name = HIVISION_MODNET_SESS.get_inputs()[0].name
209 | output_name = HIVISION_MODNET_SESS.get_outputs()[0].name
210 |
211 | im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
212 |
213 | matte = HIVISION_MODNET_SESS.run([output_name], {input_name: im})
214 | matte = (matte[0] * 255).astype("uint8")
215 | matte = np.squeeze(matte)
216 | mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
217 | b, g, r = cv2.split(np.uint8(input_image))
218 |
219 | output_image = cv2.merge((b, g, r, mask))
220 |
221 | # 如果RUN_MODE不是野兽模式,则释放模型
222 | if os.getenv("RUN_MODE") != "beast":
223 | HIVISION_MODNET_SESS = None
224 |
225 | return output_image
226 |
227 |
228 | def get_modnet_matting_photographic_portrait_matting(
229 | input_image, checkpoint_path, ref_size=512
230 | ):
231 | global MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS
232 |
233 | if not os.path.exists(checkpoint_path):
234 | print(f"Checkpoint file not found: {checkpoint_path}")
235 | return None
236 |
237 | # 如果RUN_MODE不是野兽模式,则不加载模型
238 | if MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS is None:
239 | MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS = load_onnx_model(
240 | checkpoint_path, set_cpu=True
241 | )
242 |
243 | input_name = MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS.get_inputs()[0].name
244 | output_name = MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS.get_outputs()[0].name
245 |
246 | im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
247 |
248 | matte = MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS.run(
249 | [output_name], {input_name: im}
250 | )
251 | matte = (matte[0] * 255).astype("uint8")
252 | matte = np.squeeze(matte)
253 | mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
254 | b, g, r = cv2.split(np.uint8(input_image))
255 |
256 | output_image = cv2.merge((b, g, r, mask))
257 |
258 | # 如果RUN_MODE不是野兽模式,则释放模型
259 | if os.getenv("RUN_MODE") != "beast":
260 | MODNET_PHOTOGRAPHIC_PORTRAIT_MATTING_SESS = None
261 |
262 | return output_image
263 |
264 |
265 | def get_rmbg_matting(input_image: np.ndarray, checkpoint_path, ref_size=1024):
266 | global RMBG_SESS
267 |
268 | if not os.path.exists(checkpoint_path):
269 | print(f"Checkpoint file not found: {checkpoint_path}")
270 | return None
271 |
272 | def resize_rmbg_image(image):
273 | image = image.convert("RGB")
274 | model_input_size = (ref_size, ref_size)
275 | image = image.resize(model_input_size, Image.BILINEAR)
276 | return image
277 |
278 | if RMBG_SESS is None:
279 | RMBG_SESS = load_onnx_model(checkpoint_path, set_cpu=True)
280 |
281 | orig_image = Image.fromarray(input_image)
282 | image = resize_rmbg_image(orig_image)
283 | im_np = np.array(image).astype(np.float32)
284 | im_np = im_np.transpose(2, 0, 1) # Change to CxHxW format
285 | im_np = np.expand_dims(im_np, axis=0) # Add batch dimension
286 | im_np = im_np / 255.0 # Normalize to [0, 1]
287 | im_np = (im_np - 0.5) / 0.5 # Normalize to [-1, 1]
288 |
289 | # Inference
290 | result = RMBG_SESS.run(None, {RMBG_SESS.get_inputs()[0].name: im_np})[0]
291 |
292 | # Post process
293 | result = np.squeeze(result)
294 | ma = np.max(result)
295 | mi = np.min(result)
296 | result = (result - mi) / (ma - mi) # Normalize to [0, 1]
297 |
298 | # Convert to PIL image
299 | im_array = (result * 255).astype(np.uint8)
300 | pil_im = Image.fromarray(
301 | im_array, mode="L"
302 | ) # Ensure mask is single channel (L mode)
303 |
304 | # Resize the mask to match the original image size
305 | pil_im = pil_im.resize(orig_image.size, Image.BILINEAR)
306 |
307 | # Paste the mask on the original image
308 | new_im = Image.new("RGBA", orig_image.size, (0, 0, 0, 0))
309 | new_im.paste(orig_image, mask=pil_im)
310 |
311 | # 如果RUN_MODE不是野兽模式,则释放模型
312 | if os.getenv("RUN_MODE") != "beast":
313 | RMBG_SESS = None
314 |
315 | return np.array(new_im)
316 |
317 |
318 | def get_mnn_modnet_matting(input_image, checkpoint_path, ref_size=512):
319 | if not os.path.exists(checkpoint_path):
320 | print(f"Checkpoint file not found: {checkpoint_path}")
321 | return None
322 |
323 | try:
324 | import MNN.expr as expr
325 | import MNN.nn as nn
326 | except ImportError as e:
327 | raise ImportError(
328 | "The MNN module is not installed or there was an import error. Please ensure that the MNN library is installed by using the command 'pip install mnn'."
329 | ) from e
330 |
331 | config = {}
332 | config["precision"] = "low" # 当硬件支持(armv8.2)时使用fp16推理
333 | config["backend"] = 0 # CPU
334 | config["numThread"] = 4 # 线程数
335 | im, width, length = read_modnet_image(input_image, ref_size=512)
336 | rt = nn.create_runtime_manager((config,))
337 | net = nn.load_module_from_file(
338 | checkpoint_path, ["input1"], ["output1"], runtime_manager=rt
339 | )
340 | input_var = expr.convert(im, expr.NCHW)
341 | output_var = net.forward(input_var)
342 | matte = expr.convert(output_var, expr.NCHW)
343 | matte = matte.read() # var转换为np
344 | matte = (matte * 255).astype("uint8")
345 | matte = np.squeeze(matte)
346 | mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
347 | b, g, r = cv2.split(np.uint8(input_image))
348 |
349 | output_image = cv2.merge((b, g, r, mask))
350 |
351 | return output_image
352 |
353 |
354 | def get_birefnet_portrait_matting(input_image, checkpoint_path, ref_size=512):
355 | global BIREFNET_V1_LITE_SESS
356 |
357 | if not os.path.exists(checkpoint_path):
358 | print(f"Checkpoint file not found: {checkpoint_path}")
359 | return None
360 |
361 | def transform_image(image):
362 | image = image.resize((1024, 1024)) # Resize to 1024x1024
363 | image = (
364 | np.array(image, dtype=np.float32) / 255.0
365 | ) # Convert to numpy array and normalize to [0, 1]
366 | image = (image - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] # Normalize
367 | image = np.transpose(image, (2, 0, 1)) # Change from (H, W, C) to (C, H, W)
368 | image = np.expand_dims(image, axis=0) # Add batch dimension
369 | return image.astype(np.float32) # Ensure the output is float32
370 |
371 | orig_image = Image.fromarray(input_image)
372 | input_images = transform_image(
373 | orig_image
374 | ) # This will already have the correct shape
375 |
376 | # 记录加载onnx模型的开始时间
377 | load_start_time = time()
378 |
379 | # 如果RUN_MODE不是野兽模式,则不加载模型
380 | if BIREFNET_V1_LITE_SESS is None:
381 | # print("首次加载birefnet-v1-lite模型...")
382 | if ONNX_DEVICE == "GPU":
383 | print("onnxruntime-gpu已安装,尝试使用CUDA加载模型")
384 | try:
385 | import torch
386 | except ImportError:
387 | print(
388 | "torch未安装,尝试直接使用onnxruntime-gpu加载模型,这需要配置好CUDA和cuDNN"
389 | )
390 | BIREFNET_V1_LITE_SESS = load_onnx_model(checkpoint_path)
391 | else:
392 | BIREFNET_V1_LITE_SESS = load_onnx_model(checkpoint_path, set_cpu=True)
393 |
394 | # 记录加载onnx模型的结束时间
395 | load_end_time = time()
396 |
397 | # 打印加载onnx模型所花的时间
398 | print(f"Loading ONNX model took {load_end_time - load_start_time:.4f} seconds")
399 |
400 | input_name = BIREFNET_V1_LITE_SESS.get_inputs()[0].name
401 | print(onnxruntime.get_device(), BIREFNET_V1_LITE_SESS.get_providers())
402 |
403 | time_st = time()
404 | pred_onnx = BIREFNET_V1_LITE_SESS.run(None, {input_name: input_images})[
405 | -1
406 | ] # Use float32 input
407 | pred_onnx = np.squeeze(pred_onnx) # Use numpy to squeeze
408 | result = 1 / (1 + np.exp(-pred_onnx)) # Sigmoid function using numpy
409 | print(f"Inference time: {time() - time_st:.4f} seconds")
410 |
411 | # Convert to PIL image
412 | im_array = (result * 255).astype(np.uint8)
413 | pil_im = Image.fromarray(
414 | im_array, mode="L"
415 | ) # Ensure mask is single channel (L mode)
416 |
417 | # Resize the mask to match the original image size
418 | pil_im = pil_im.resize(orig_image.size, Image.BILINEAR)
419 |
420 | # Paste the mask on the original image
421 | new_im = Image.new("RGBA", orig_image.size, (0, 0, 0, 0))
422 | new_im.paste(orig_image, mask=pil_im)
423 |
424 | # 如果RUN_MODE不是野兽模式,则释放模型
425 | if os.getenv("RUN_MODE") != "beast":
426 | BIREFNET_V1_LITE_SESS = None
427 |
428 | return np.array(new_im)
429 |
--------------------------------------------------------------------------------
/hivision/creator/layout_calculator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 21:35
5 | @File: layout_calculator.py
6 | @IDE: pycharm
7 | @Description:
8 | 布局计算器
9 | """
10 |
11 | import cv2.detail
12 | import numpy as np
13 |
14 |
15 | def judge_layout(
16 | input_width,
17 | input_height,
18 | PHOTO_INTERVAL_W,
19 | PHOTO_INTERVAL_H,
20 | LIMIT_BLOCK_W,
21 | LIMIT_BLOCK_H,
22 | ):
23 | centerBlockHeight_1, centerBlockWidth_1 = (
24 | input_height,
25 | input_width,
26 | ) # 由证件照们组成的一个中心区块(1 代表不转置排列)
27 | centerBlockHeight_2, centerBlockWidth_2 = (
28 | input_width,
29 | input_height,
30 | ) # 由证件照们组成的一个中心区块(2 代表转置排列)
31 |
32 | # 1.不转置排列的情况下:
33 | layout_col_no_transpose = 0 # 行
34 | layout_row_no_transpose = 0 # 列
35 | for i in range(1, 4):
36 | centerBlockHeight_temp = input_height * i + PHOTO_INTERVAL_H * (i - 1)
37 | if centerBlockHeight_temp < LIMIT_BLOCK_H:
38 | centerBlockHeight_1 = centerBlockHeight_temp
39 | layout_row_no_transpose = i
40 | else:
41 | break
42 | for j in range(1, 9):
43 | centerBlockWidth_temp = input_width * j + PHOTO_INTERVAL_W * (j - 1)
44 | if centerBlockWidth_temp < LIMIT_BLOCK_W:
45 | centerBlockWidth_1 = centerBlockWidth_temp
46 | layout_col_no_transpose = j
47 | else:
48 | break
49 | layout_number_no_transpose = layout_row_no_transpose * layout_col_no_transpose
50 |
51 | # 2.转置排列的情况下:
52 | layout_col_transpose = 0 # 行
53 | layout_row_transpose = 0 # 列
54 | for i in range(1, 4):
55 | centerBlockHeight_temp = input_width * i + PHOTO_INTERVAL_H * (i - 1)
56 | if centerBlockHeight_temp < LIMIT_BLOCK_H:
57 | centerBlockHeight_2 = centerBlockHeight_temp
58 | layout_row_transpose = i
59 | else:
60 | break
61 | for j in range(1, 9):
62 | centerBlockWidth_temp = input_height * j + PHOTO_INTERVAL_W * (j - 1)
63 | if centerBlockWidth_temp < LIMIT_BLOCK_W:
64 | centerBlockWidth_2 = centerBlockWidth_temp
65 | layout_col_transpose = j
66 | else:
67 | break
68 | layout_number_transpose = layout_row_transpose * layout_col_transpose
69 |
70 | if layout_number_transpose > layout_number_no_transpose:
71 | layout_mode = (layout_col_transpose, layout_row_transpose, 2)
72 | return layout_mode, centerBlockWidth_2, centerBlockHeight_2
73 | else:
74 | layout_mode = (layout_col_no_transpose, layout_row_no_transpose, 1)
75 | return layout_mode, centerBlockWidth_1, centerBlockHeight_1
76 |
77 |
78 | def generate_layout_array(input_height, input_width, LAYOUT_WIDTH=1795, LAYOUT_HEIGHT=1205):
79 | # 1.基础参数表
80 | PHOTO_INTERVAL_H = 30 # 证件照与证件照之间的垂直距离
81 | PHOTO_INTERVAL_W = 30 # 证件照与证件照之间的水平距离
82 | SIDES_INTERVAL_H = 50 # 证件照与画布边缘的垂直距离
83 | SIDES_INTERVAL_W = 70 # 证件照与画布边缘的水平距离
84 | LIMIT_BLOCK_W = LAYOUT_WIDTH - 2 * SIDES_INTERVAL_W
85 | LIMIT_BLOCK_H = LAYOUT_HEIGHT - 2 * SIDES_INTERVAL_H
86 |
87 | # 2.创建一个 1180x1746 的空白画布
88 | white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
89 | white_background.fill(255)
90 |
91 | # 3.计算照片的 layout(列、行、横竖朝向),证件照组成的中心区块的分辨率
92 | layout_mode, centerBlockWidth, centerBlockHeight = judge_layout(
93 | input_width,
94 | input_height,
95 | PHOTO_INTERVAL_W,
96 | PHOTO_INTERVAL_H,
97 | LIMIT_BLOCK_W,
98 | LIMIT_BLOCK_H,
99 | )
100 | # 4.开始排列组合
101 | x11 = (LAYOUT_WIDTH - centerBlockWidth) // 2
102 | y11 = (LAYOUT_HEIGHT - centerBlockHeight) // 2
103 | typography_arr = []
104 | typography_rotate = False
105 | if layout_mode[2] == 2:
106 | input_height, input_width = input_width, input_height
107 | typography_rotate = True
108 |
109 | for j in range(layout_mode[1]):
110 | for i in range(layout_mode[0]):
111 | xi = x11 + i * input_width + i * PHOTO_INTERVAL_W
112 | yi = y11 + j * input_height + j * PHOTO_INTERVAL_H
113 | typography_arr.append([xi, yi])
114 |
115 | return typography_arr, typography_rotate
116 |
117 |
118 | def generate_layout_image(
119 | input_image, typography_arr, typography_rotate, width=295, height=413,
120 | crop_line:bool=False,
121 | LAYOUT_WIDTH=1795,
122 | LAYOUT_HEIGHT=1205,
123 | ):
124 |
125 | # 创建一个白色背景的空白画布
126 | white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
127 | white_background.fill(255)
128 |
129 | # 如果输入图像的高度不等于指定高度,则调整图像大小
130 | if input_image.shape[0] != height:
131 | input_image = cv2.resize(input_image, (width, height))
132 |
133 | # 如果需要旋转排版,则对图像进行转置和垂直镜像
134 | if typography_rotate:
135 | input_image = cv2.transpose(input_image)
136 | input_image = cv2.flip(input_image, 0) # 0 表示垂直镜像
137 |
138 | # 交换高度和宽度
139 | height, width = width, height
140 |
141 | # 将图像按照排版数组中的位置放置到白色背景上
142 | for arr in typography_arr:
143 | locate_x, locate_y = arr[0], arr[1]
144 | white_background[locate_y : locate_y + height, locate_x : locate_x + width] = (
145 | input_image
146 | )
147 |
148 | if crop_line:
149 | # 添加裁剪线
150 | line_color = (200, 200, 200) # 浅灰色
151 | line_thickness = 1
152 |
153 | # 初始化裁剪线位置列表
154 | vertical_lines = []
155 | horizontal_lines = []
156 |
157 | # 根据排版数组添加裁剪线
158 | for arr in typography_arr:
159 | x, y = arr[0], arr[1]
160 | if x not in vertical_lines:
161 | vertical_lines.append(x)
162 | if x + width not in vertical_lines:
163 | vertical_lines.append(x + width)
164 | if y not in horizontal_lines:
165 | horizontal_lines.append(y)
166 | if y + height not in horizontal_lines:
167 | horizontal_lines.append(y + height)
168 |
169 | # 绘制垂直裁剪线
170 | for x in vertical_lines:
171 | cv2.line(white_background, (x, 0), (x, LAYOUT_HEIGHT), line_color, line_thickness)
172 |
173 | # 绘制水平裁剪线
174 | for y in horizontal_lines:
175 | cv2.line(white_background, (0, y), (LAYOUT_WIDTH, y), line_color, line_thickness)
176 |
177 | # 返回排版后的图像
178 | return white_background
179 |
--------------------------------------------------------------------------------
/hivision/creator/move_image.py:
--------------------------------------------------------------------------------
1 | """
2 | 有一些 png 图像下部也会有一些透明的区域,使得图像无法对其底部边框
3 | 本程序实现移动图像,使其下部与 png 图像实际大小相对齐
4 | """
5 | import os
6 | import cv2
7 | import numpy as np
8 | from hivisionai.hycv.utils import get_box_pro
9 |
10 | path_pre = os.path.join(os.getcwd(), 'pre')
11 | path_final = os.path.join(os.getcwd(), 'final')
12 |
13 |
14 | def merge(boxes):
15 | """
16 | 生成的边框可能不止只有一个,需要将边框合并
17 | """
18 | x, y, h, w = boxes[0]
19 | # x 和 y 应该是整个 boxes 里面最小的值
20 | if len(boxes) > 1:
21 | for tmp in boxes:
22 | x_tmp, y_tmp, h_tmp, w_tmp = tmp
23 | if x > x_tmp:
24 | x_max = x_tmp + w_tmp if x_tmp + w_tmp > x + w else x + w
25 | x = x_tmp
26 | w = x_max - x
27 | if y > y_tmp:
28 | y_max = y_tmp + h_tmp if y_tmp + h_tmp > y + h else y + h
29 | y = y_tmp
30 | h = y_max - y
31 | return tuple((x, y, h, w))
32 |
33 |
34 | def get_box(png_img):
35 | """
36 | 获取矩形边框最终返回一个元组 (x,y,h,w),分别对应矩形左上角的坐标和矩形的高和宽
37 | """
38 | r, g, b , a = cv2.split(png_img)
39 | gray_img = a
40 | th, binary = cv2.threshold(gray_img, 127 , 255, cv2.THRESH_BINARY) # 二值化
41 | # cv2.imshow("name", binary)
42 | # cv2.waitKey(0)
43 | contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 得到轮廓列表 contours
44 | bounding_boxes = merge([cv2.boundingRect(cnt) for cnt in contours]) # 轮廓合并
45 | # print(bounding_boxes)
46 | return bounding_boxes
47 |
48 |
49 | def get_box_2(png_img):
50 | """
51 | 不用 opencv 内置算法生成矩形了,改用自己的算法(for 循环)
52 | """
53 | _, _, _, a = cv2.split(png_img)
54 | _, a = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY)
55 | # 将 r,g,b 通道丢弃,只留下透明度通道
56 | # cv2.imshow("name", a)
57 | # cv2.waitKey(0)
58 | # 在透明度矩阵中,0 代表完全透明
59 | height,width=a.shape # 高和宽
60 | f=0
61 | tmp1 = 0
62 |
63 | """
64 | 获取上下
65 | """
66 | for tmp1 in range(0,height):
67 | tmp_a_high= a[tmp1:tmp1+1,:][0]
68 | for tmp2 in range(width):
69 | # a = tmp_a_low[tmp2]
70 | if tmp_a_high[tmp2]!=0:
71 | f=1
72 | if f == 1:
73 | break
74 | delta_y_high = tmp1 + 1
75 | f = 0
76 | for tmp1 in range(height,-1, -1):
77 | tmp_a_low= a[tmp1-1:tmp1+1,:][0]
78 | for tmp2 in range(width):
79 | # a = tmp_a_low[tmp2]
80 | if tmp_a_low[tmp2]!=0:
81 | f=1
82 | if f == 1:
83 | break
84 | delta_y_bottom = height - tmp1 + 3
85 | """
86 | 获取左右
87 | """
88 | f = 0
89 | for tmp1 in range(width):
90 | tmp_a_left = a[:, tmp1:tmp1+1]
91 | for tmp2 in range(height):
92 | if tmp_a_left[tmp2] != 0:
93 | f = 1
94 | if f==1:
95 | break
96 | delta_x_left = tmp1 + 1
97 | f = 0
98 | for tmp1 in range(width, -1, -1):
99 | tmp_a_left = a[:, tmp1-1:tmp1]
100 | for tmp2 in range(height):
101 | if tmp_a_left[tmp2] != 0:
102 | f = 1
103 | if f==1:
104 | break
105 | delta_x_right = width - tmp1 + 1
106 | return delta_y_high, delta_y_bottom, delta_x_left, delta_x_right
107 |
108 |
109 | def move(input_image):
110 | """
111 | 裁剪主函数,输入一张 png 图像,该图像周围是透明的
112 | """
113 | png_img = input_image # 获取图像
114 |
115 | height, width, channels = png_img.shape # 高 y、宽 x
116 | y_low,y_high, _, _ = get_box_pro(png_img, model=2) # for 循环
117 | base = np.zeros((y_high, width, channels),dtype=np.uint8) # for 循环
118 | png_img = png_img[0:height - y_high, :, :] # for 循环
119 | png_img = np.concatenate((base, png_img), axis=0)
120 | return png_img, y_high
121 |
122 |
123 | def main():
124 | if not os.path.exists(path_pre):
125 | os.makedirs(path_pre)
126 | if not os.path.exists(path_final):
127 | os.makedirs(path_final)
128 | for name in os.listdir(path_pre):
129 | pass
130 | # move(name)
131 |
132 |
133 | if __name__ == "__main__":
134 | main()
135 |
--------------------------------------------------------------------------------
/hivision/creator/photo_adjuster.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 20:02
5 | @File: photo_adjuster.py
6 | @IDE: pycharm
7 | @Description:
8 | 证件照调整
9 | """
10 | from .context import Context
11 | from .layout_calculator import generate_layout_array
12 | import hivision.creator.utils as U
13 | import numpy as np
14 | import math
15 | import cv2
16 |
17 |
18 | def adjust_photo(ctx: Context):
19 | # Step1. 准备人脸参数
20 | face_rect = ctx.face["rectangle"]
21 | standard_size = ctx.params.size
22 | params = ctx.params
23 | x, y = face_rect[0], face_rect[1]
24 | w, h = face_rect[2], face_rect[3]
25 | height, width = ctx.matting_image.shape[:2]
26 | width_height_ratio = standard_size[0] / standard_size[1]
27 | # Step2. 计算高级参数
28 | face_center = (x + w / 2, y + h / 2) # 面部中心坐标
29 | face_measure = w * h # 面部面积
30 | crop_measure = (
31 | face_measure / params.head_measure_ratio
32 | ) # 裁剪框面积:为面部面积的 5 倍
33 | resize_ratio = crop_measure / (standard_size[0] * standard_size[1]) # 裁剪框缩放率
34 | resize_ratio_single = math.sqrt(
35 | resize_ratio
36 | ) # 长和宽的缩放率(resize_ratio 的开方)
37 | crop_size = (
38 | int(standard_size[0] * resize_ratio_single),
39 | int(standard_size[1] * resize_ratio_single),
40 | ) # 裁剪框大小
41 |
42 | # 裁剪框的定位信息
43 | x1 = int(face_center[0] - crop_size[1] / 2)
44 | y1 = int(face_center[1] - crop_size[0] * params.head_height_ratio)
45 | y2 = y1 + crop_size[0]
46 | x2 = x1 + crop_size[1]
47 |
48 | # Step3, 裁剪框的调整
49 | cut_image = IDphotos_cut(x1, y1, x2, y2, ctx.matting_image)
50 | cut_image = cv2.resize(cut_image, (crop_size[1], crop_size[0]))
51 | y_top, y_bottom, x_left, x_right = U.get_box(
52 | cut_image.astype(np.uint8), model=2, correction_factor=0
53 | ) # 得到 cut_image 中人像的上下左右距离信息
54 |
55 | # Step5. 判定 cut_image 中的人像是否处于合理的位置,若不合理,则处理数据以便之后调整位置
56 | # 检测人像与裁剪框左边或右边是否存在空隙
57 | if x_left > 0 or x_right > 0:
58 | status_left_right = 1
59 | cut_value_top = int(
60 | ((x_left + x_right) * width_height_ratio) / 2
61 | ) # 减去左右,为了保持比例,上下也要相应减少 cut_value_top
62 | else:
63 | status_left_right = 0
64 | cut_value_top = 0
65 |
66 | """
67 | 检测人头顶与照片的顶部是否在合适的距离内:
68 | - status==0: 距离合适,无需移动
69 | - status=1: 距离过大,人像应向上移动
70 | - status=2: 距离过小,人像应向下移动
71 | """
72 | status_top, move_value = U.detect_distance(
73 | y_top - cut_value_top,
74 | crop_size[0],
75 | max=params.head_top_range[0],
76 | min=params.head_top_range[1],
77 | )
78 |
79 | # Step6. 对照片的第二轮裁剪
80 | if status_left_right == 0 and status_top == 0:
81 | result_image = cut_image
82 | else:
83 | result_image = IDphotos_cut(
84 | x1 + x_left,
85 | y1 + cut_value_top + status_top * move_value,
86 | x2 - x_right,
87 | y2 - cut_value_top + status_top * move_value,
88 | ctx.matting_image,
89 | )
90 |
91 | # 换装参数准备
92 | relative_x = x - (x1 + x_left)
93 | relative_y = y - (y1 + cut_value_top + status_top * move_value)
94 |
95 | # Step7. 当照片底部存在空隙时,下拉至底部
96 | result_image, y_high = move(result_image.astype(np.uint8))
97 | relative_y = relative_y + y_high # 更新换装参数
98 |
99 | # Step8. 标准照与高清照转换
100 | result_image_standard = standard_photo_resize(result_image, standard_size)
101 | result_image_hd, resize_ratio_max = resize_image_by_min(
102 | result_image, esp=max(600, standard_size[1])
103 | )
104 |
105 | # Step9. 参数准备 - 为换装服务
106 | clothing_params = {
107 | "relative_x": relative_x * resize_ratio_max,
108 | "relative_y": relative_y * resize_ratio_max,
109 | "w": w * resize_ratio_max,
110 | "h": h * resize_ratio_max,
111 | }
112 |
113 | # Step7. 排版照参数获取
114 | typography_arr, typography_rotate = generate_layout_array(
115 | input_height=standard_size[0], input_width=standard_size[1]
116 | )
117 |
118 | return (
119 | result_image_hd,
120 | result_image_standard,
121 | clothing_params,
122 | {
123 | "arr": typography_arr,
124 | "rotate": typography_rotate,
125 | },
126 | )
127 |
128 |
129 | def IDphotos_cut(x1, y1, x2, y2, img):
130 | """
131 | 在图片上进行滑动裁剪,输入输出为
132 | 输入:一张图片 img,和裁剪框信息 (x1,x2,y1,y2)
133 | 输出:裁剪好的图片,然后裁剪框超出了图像范围,那么将用 0 矩阵补位
134 | ------------------------------------
135 | x:裁剪框左上的横坐标
136 | y:裁剪框左上的纵坐标
137 | x2:裁剪框右下的横坐标
138 | y2:裁剪框右下的纵坐标
139 | crop_size:裁剪框大小
140 | img:裁剪图像(numpy.array)
141 | output_path:裁剪图片的输出路径
142 | ------------------------------------
143 | """
144 |
145 | crop_size = (y2 - y1, x2 - x1)
146 | """
147 | ------------------------------------
148 | temp_x_1:裁剪框左边超出图像部分
149 | temp_y_1:裁剪框上边超出图像部分
150 | temp_x_2:裁剪框右边超出图像部分
151 | temp_y_2:裁剪框下边超出图像部分
152 | ------------------------------------
153 | """
154 | temp_x_1 = 0
155 | temp_y_1 = 0
156 | temp_x_2 = 0
157 | temp_y_2 = 0
158 |
159 | if y1 < 0:
160 | temp_y_1 = abs(y1)
161 | y1 = 0
162 | if y2 > img.shape[0]:
163 | temp_y_2 = y2
164 | y2 = img.shape[0]
165 | temp_y_2 = temp_y_2 - y2
166 |
167 | if x1 < 0:
168 | temp_x_1 = abs(x1)
169 | x1 = 0
170 | if x2 > img.shape[1]:
171 | temp_x_2 = x2
172 | x2 = img.shape[1]
173 | temp_x_2 = temp_x_2 - x2
174 |
175 | # 生成一张全透明背景
176 | background_bgr = np.full((crop_size[0], crop_size[1]), 255, dtype=np.uint8)
177 | background_a = np.full((crop_size[0], crop_size[1]), 0, dtype=np.uint8)
178 | background = cv2.merge(
179 | (background_bgr, background_bgr, background_bgr, background_a)
180 | )
181 |
182 | background[
183 | temp_y_1 : crop_size[0] - temp_y_2, temp_x_1 : crop_size[1] - temp_x_2
184 | ] = img[y1:y2, x1:x2]
185 |
186 | return background
187 |
188 |
189 | def move(input_image):
190 | """
191 | 裁剪主函数,输入一张 png 图像,该图像周围是透明的
192 | """
193 | png_img = input_image # 获取图像
194 |
195 | height, width, channels = png_img.shape # 高 y、宽 x
196 | y_low, y_high, _, _ = U.get_box(png_img, model=2) # for 循环
197 | base = np.zeros((y_high, width, channels), dtype=np.uint8) # for 循环
198 | png_img = png_img[0 : height - y_high, :, :] # for 循环
199 | png_img = np.concatenate((base, png_img), axis=0)
200 | return png_img, y_high
201 |
202 |
203 | def standard_photo_resize(input_image: np.array, size):
204 | """
205 | input_image: 输入图像,即高清照
206 | size: 标准照的尺寸
207 | """
208 | resize_ratio = input_image.shape[0] / size[0]
209 | resize_item = int(round(input_image.shape[0] / size[0]))
210 | if resize_ratio >= 2:
211 | for i in range(resize_item - 1):
212 | if i == 0:
213 | result_image = cv2.resize(
214 | input_image,
215 | (size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
216 | interpolation=cv2.INTER_AREA,
217 | )
218 | else:
219 | result_image = cv2.resize(
220 | result_image,
221 | (size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
222 | interpolation=cv2.INTER_AREA,
223 | )
224 | else:
225 | result_image = cv2.resize(
226 | input_image, (size[1], size[0]), interpolation=cv2.INTER_AREA
227 | )
228 |
229 | return result_image
230 |
231 |
232 | def resize_image_by_min(input_image, esp=600):
233 | """
234 | 将图像缩放为最短边至少为 esp 的图像。
235 | :param input_image: 输入图像(OpenCV 矩阵)
236 | :param esp: 缩放后的最短边长
237 | :return: 缩放后的图像,缩放倍率
238 | """
239 | height, width = input_image.shape[0], input_image.shape[1]
240 | min_border = min(height, width)
241 | if min_border < esp:
242 | if height >= width:
243 | new_width = esp
244 | new_height = height * esp // width
245 | else:
246 | new_height = esp
247 | new_width = width * esp // height
248 |
249 | return (
250 | cv2.resize(
251 | input_image, (new_width, new_height), interpolation=cv2.INTER_AREA
252 | ),
253 | new_height / height,
254 | )
255 |
256 | else:
257 | return input_image, 1
258 |
--------------------------------------------------------------------------------
/hivision/creator/retinaface/__init__.py:
--------------------------------------------------------------------------------
1 | from .inference import retinaface_detect_faces
2 |
--------------------------------------------------------------------------------
/hivision/creator/retinaface/box_utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | def decode(loc, priors, variances):
5 | """Decode locations from predictions using priors to undo
6 | the encoding we did for offset regression at train time.
7 | Args:
8 | loc (tensor): location predictions for loc layers,
9 | Shape: [num_priors,4]
10 | priors (tensor): Prior boxes in center-offset form.
11 | Shape: [num_priors,4].
12 | variances: (list[float]) Variances of priorboxes
13 | Return:
14 | decoded bounding box predictions
15 | """
16 |
17 | boxes = None
18 |
19 | boxes = np.concatenate(
20 | (
21 | priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
22 | priors[:, 2:] * np.exp(loc[:, 2:] * variances[1]),
23 | ),
24 | axis=1,
25 | )
26 |
27 | boxes[:, :2] -= boxes[:, 2:] / 2
28 | boxes[:, 2:] += boxes[:, :2]
29 | return boxes
30 |
31 |
32 | def decode_landm(pre, priors, variances):
33 | """Decode landm from predictions using priors to undo
34 | the encoding we did for offset regression at train time.
35 | Args:
36 | pre (tensor): landm predictions for loc layers,
37 | Shape: [num_priors,10]
38 | priors (tensor): Prior boxes in center-offset form.
39 | Shape: [num_priors,4].
40 | variances: (list[float]) Variances of priorboxes
41 | Return:
42 | decoded landm predictions
43 | """
44 | landms = None
45 |
46 | landms = np.concatenate(
47 | (
48 | priors[:, :2] + pre[:, :2] * variances[0] * priors[:, 2:],
49 | priors[:, :2] + pre[:, 2:4] * variances[0] * priors[:, 2:],
50 | priors[:, :2] + pre[:, 4:6] * variances[0] * priors[:, 2:],
51 | priors[:, :2] + pre[:, 6:8] * variances[0] * priors[:, 2:],
52 | priors[:, :2] + pre[:, 8:10] * variances[0] * priors[:, 2:],
53 | ),
54 | axis=1,
55 | )
56 |
57 | return landms
58 |
--------------------------------------------------------------------------------
/hivision/creator/retinaface/inference.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import cv2
3 | import onnxruntime
4 | from hivision.creator.retinaface.box_utils import decode, decode_landm
5 | from hivision.creator.retinaface.prior_box import PriorBox
6 |
7 |
8 | def py_cpu_nms(dets, thresh):
9 | """Pure Python NMS baseline."""
10 | x1 = dets[:, 0]
11 | y1 = dets[:, 1]
12 | x2 = dets[:, 2]
13 | y2 = dets[:, 3]
14 | scores = dets[:, 4]
15 |
16 | areas = (x2 - x1 + 1) * (y2 - y1 + 1)
17 | order = scores.argsort()[::-1]
18 |
19 | keep = []
20 | while order.size > 0:
21 | i = order[0]
22 | keep.append(i)
23 | xx1 = np.maximum(x1[i], x1[order[1:]])
24 | yy1 = np.maximum(y1[i], y1[order[1:]])
25 | xx2 = np.minimum(x2[i], x2[order[1:]])
26 | yy2 = np.minimum(y2[i], y2[order[1:]])
27 |
28 | w = np.maximum(0.0, xx2 - xx1 + 1)
29 | h = np.maximum(0.0, yy2 - yy1 + 1)
30 | inter = w * h
31 | ovr = inter / (areas[i] + areas[order[1:]] - inter)
32 |
33 | inds = np.where(ovr <= thresh)[0]
34 | order = order[inds + 1]
35 |
36 | return keep
37 |
38 |
39 | # 替换掉 argparse 的部分,直接使用普通变量
40 | network = "resnet50"
41 | use_cpu = False
42 | confidence_threshold = 0.8
43 | top_k = 5000
44 | nms_threshold = 0.2
45 | keep_top_k = 750
46 | save_image = True
47 | vis_thres = 0.6
48 |
49 | ONNX_DEVICE = (
50 | "CUDAExecutionProvider"
51 | if onnxruntime.get_device() == "GPU"
52 | else "CPUExecutionProvider"
53 | )
54 |
55 |
56 | def load_onnx_model(checkpoint_path, set_cpu=False):
57 | providers = (
58 | ["CUDAExecutionProvider", "CPUExecutionProvider"]
59 | if ONNX_DEVICE == "CUDAExecutionProvider"
60 | else ["CPUExecutionProvider"]
61 | )
62 |
63 | if set_cpu:
64 | sess = onnxruntime.InferenceSession(
65 | checkpoint_path, providers=["CPUExecutionProvider"]
66 | )
67 | else:
68 | try:
69 | sess = onnxruntime.InferenceSession(checkpoint_path, providers=providers)
70 | except Exception as e:
71 | if ONNX_DEVICE == "CUDAExecutionProvider":
72 | print(f"Failed to load model with CUDAExecutionProvider: {e}")
73 | print("Falling back to CPUExecutionProvider")
74 | # 尝试使用CPU加载模型
75 | sess = onnxruntime.InferenceSession(
76 | checkpoint_path, providers=["CPUExecutionProvider"]
77 | )
78 | else:
79 | raise e # 如果是CPU执行失败,重新抛出异常
80 |
81 | return sess
82 |
83 |
84 | def retinaface_detect_faces(image, model_path: str, sess=None):
85 | cfg = {
86 | "name": "Resnet50",
87 | "min_sizes": [[16, 32], [64, 128], [256, 512]],
88 | "steps": [8, 16, 32],
89 | "variance": [0.1, 0.2],
90 | "clip": False,
91 | "loc_weight": 2.0,
92 | "gpu_train": True,
93 | "batch_size": 24,
94 | "ngpu": 4,
95 | "epoch": 100,
96 | "decay1": 70,
97 | "decay2": 90,
98 | "image_size": 840,
99 | "pretrain": True,
100 | "return_layers": {"layer2": 1, "layer3": 2, "layer4": 3},
101 | "in_channel": 256,
102 | "out_channel": 256,
103 | }
104 |
105 | # Load ONNX model
106 | if sess is None:
107 | retinaface = load_onnx_model(model_path, set_cpu=False)
108 | else:
109 | retinaface = sess
110 |
111 | resize = 1
112 |
113 | # Read and preprocess the image
114 | img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
115 | img = np.float32(img_rgb)
116 |
117 | im_height, im_width, _ = img.shape
118 | scale = np.array([img.shape[1], img.shape[0], img.shape[1], img.shape[0]])
119 | img -= (104, 117, 123)
120 | img = img.transpose(2, 0, 1)
121 | img = np.expand_dims(img, axis=0)
122 |
123 | # Run the model
124 | inputs = {"input": img}
125 | loc, conf, landms = retinaface.run(None, inputs)
126 |
127 | priorbox = PriorBox(cfg, image_size=(im_height, im_width))
128 | priors = priorbox.forward()
129 |
130 | prior_data = priors
131 |
132 | boxes = decode(np.squeeze(loc, axis=0), prior_data, cfg["variance"])
133 | boxes = boxes * scale / resize
134 | scores = np.squeeze(conf, axis=0)[:, 1]
135 |
136 | landms = decode_landm(np.squeeze(landms.data, axis=0), prior_data, cfg["variance"])
137 |
138 | scale1 = np.array(
139 | [
140 | img.shape[3],
141 | img.shape[2],
142 | img.shape[3],
143 | img.shape[2],
144 | img.shape[3],
145 | img.shape[2],
146 | img.shape[3],
147 | img.shape[2],
148 | img.shape[3],
149 | img.shape[2],
150 | ]
151 | )
152 | landms = landms * scale1 / resize
153 |
154 | # ignore low scores
155 | inds = np.where(scores > confidence_threshold)[0]
156 | boxes = boxes[inds]
157 | landms = landms[inds]
158 | scores = scores[inds]
159 |
160 | # keep top-K before NMS
161 | order = scores.argsort()[::-1][:top_k]
162 | boxes = boxes[order]
163 | landms = landms[order]
164 | scores = scores[order]
165 |
166 | # do NMS
167 | dets = np.hstack((boxes, scores[:, np.newaxis])).astype(np.float32, copy=False)
168 | keep = py_cpu_nms(dets, nms_threshold)
169 | dets = dets[keep, :]
170 | landms = landms[keep]
171 |
172 | # keep top-K faster NMS
173 | dets = dets[:keep_top_k, :]
174 | landms = landms[:keep_top_k, :]
175 |
176 | dets = np.concatenate((dets, landms), axis=1)
177 |
178 | return dets, retinaface
179 |
180 |
181 | if __name__ == "__main__":
182 | import gradio as gr
183 |
184 | # Create Gradio interface
185 | iface = gr.Interface(
186 | fn=retinaface_detect_faces,
187 | inputs=[
188 | gr.Image(
189 | type="numpy", label="上传图片", height=400
190 | ), # Set the height to 400
191 | gr.Textbox(value="./FaceDetector.onnx", label="ONNX模型路径"),
192 | ],
193 | outputs=gr.Number(label="检测到的人脸数量"),
194 | title="人脸检测",
195 | description="上传图片并提供ONNX模型路径以检测人脸数量。",
196 | )
197 |
198 | # Launch the Gradio app
199 | iface.launch()
200 |
--------------------------------------------------------------------------------
/hivision/creator/retinaface/prior_box.py:
--------------------------------------------------------------------------------
1 | from itertools import product as product
2 | import numpy as np
3 | from math import ceil
4 |
5 |
6 | class PriorBox(object):
7 | def __init__(self, cfg, image_size=None):
8 | super(PriorBox, self).__init__()
9 | self.min_sizes = cfg["min_sizes"]
10 | self.steps = cfg["steps"]
11 | self.clip = cfg["clip"]
12 | self.image_size = image_size
13 | self.feature_maps = [
14 | [ceil(self.image_size[0] / step), ceil(self.image_size[1] / step)]
15 | for step in self.steps
16 | ]
17 | self.name = "s"
18 |
19 | def forward(self):
20 | anchors = []
21 | for k, f in enumerate(self.feature_maps):
22 | min_sizes = self.min_sizes[k]
23 | for i, j in product(range(f[0]), range(f[1])):
24 | for min_size in min_sizes:
25 | s_kx = min_size / self.image_size[1]
26 | s_ky = min_size / self.image_size[0]
27 | dense_cx = [
28 | x * self.steps[k] / self.image_size[1] for x in [j + 0.5]
29 | ]
30 | dense_cy = [
31 | y * self.steps[k] / self.image_size[0] for y in [i + 0.5]
32 | ]
33 | for cy, cx in product(dense_cy, dense_cx):
34 | anchors += [cx, cy, s_kx, s_ky]
35 |
36 | output = np.array(anchors).reshape(-1, 4)
37 |
38 | if self.clip:
39 | output = np.clip(output, 0, 1)
40 |
41 | return output
42 |
--------------------------------------------------------------------------------
/hivision/creator/retinaface/weights/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/creator/retinaface/weights/.gitkeep
--------------------------------------------------------------------------------
/hivision/creator/rotation_adjust.py:
--------------------------------------------------------------------------------
1 | """
2 | 人脸旋转矫正模块
3 |
4 | 本模块提供了用于旋转图像的函数,主要用于人脸旋转矫正。
5 | 包含了处理3通道和4通道图像的旋转函数。
6 | """
7 |
8 | import cv2
9 | import numpy as np
10 |
11 |
12 | def rotate_bound(image: np.ndarray, angle: float, center=None):
13 | """
14 | 旋转图像而不损失信息的函数
15 |
16 | Args:
17 | image (np.ndarray): 输入图像,3通道numpy数组
18 | angle (float): 旋转角度(度)
19 | center (tuple, optional): 旋转中心坐标,默认为图像中心
20 |
21 | Returns:
22 | tuple: 包含以下元素的元组:
23 | - rotated (np.ndarray): 旋转后的图像
24 | - cos (float): 旋转角度的余弦值
25 | - sin (float): 旋转角度的正弦值
26 | - dW (int): 宽度变化量
27 | - dH (int): 高度变化量
28 | """
29 | (h, w) = image.shape[:2]
30 | if center is None:
31 | (cX, cY) = (w / 2, h / 2)
32 | else:
33 | (cX, cY) = center
34 |
35 | M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
36 | cos = np.abs(M[0, 0])
37 | sin = np.abs(M[0, 1])
38 |
39 | nW = int((h * sin) + (w * cos))
40 | nH = int((h * cos) + (w * sin))
41 |
42 | M[0, 2] += (nW / 2) - cX
43 | M[1, 2] += (nH / 2) - cY
44 |
45 | rotated = cv2.warpAffine(image, M, (nW, nH))
46 |
47 | # 计算偏移量
48 | dW = nW - w
49 | dH = nH - h
50 |
51 | return rotated, cos, sin, dW, dH
52 |
53 |
54 | def rotate_bound_4channels(image: np.ndarray, a: np.ndarray, angle: float, center=None):
55 | """
56 | 旋转4通道图像的函数
57 |
58 | 这是rotate_bound函数的4通道版本,可以同时处理RGB图像和其对应的alpha通道。
59 |
60 | Args:
61 | image (np.ndarray): 输入的3通道RGB图像
62 | a (np.ndarray): 输入图像的alpha通道
63 | angle (float): 旋转角度(度)
64 | center (tuple, optional): 旋转中心坐标,默认为图像中心
65 |
66 | Returns:
67 | tuple: 包含以下元素的元组:
68 | - input_image (np.ndarray): 旋转后的3通道RGB图像
69 | - result_image (np.ndarray): 旋转后的4通道RGBA图像
70 | - cos (float): 旋转角度的余弦值
71 | - sin (float): 旋转角度的正弦值
72 | - dW (int): 宽度变化量
73 | - dH (int): 高度变化量
74 | """
75 | input_image, cos, sin, dW, dH = rotate_bound(image, angle, center)
76 | new_a, _, _, _, _ = rotate_bound(a, angle, center) # 对alpha通道进行旋转
77 | b, g, r = cv2.split(input_image)
78 | result_image = cv2.merge((b, g, r, new_a)) # 合并旋转后的RGB通道和alpha通道
79 |
80 | return input_image, result_image, cos, sin, dW, dH
81 |
--------------------------------------------------------------------------------
/hivision/creator/tensor2numpy.py:
--------------------------------------------------------------------------------
1 | """
2 | 作者:林泽毅
3 | 建这个开源库的起源呢,是因为在做 onnx 推理的时候,需要将原来的 tensor 转换成 numpy.array
4 | 问题是 Tensor 和 Numpy 的矩阵排布逻辑不同
5 | 包括 Tensor 推理经常会进行 Transform,比如 ToTensor,Normalize 等
6 | 就想做一些等价转换的函数。
7 | """
8 | import numpy as np
9 |
10 |
11 | def NTo_Tensor(array):
12 | """
13 | :param array: opencv/PIL读取的numpy矩阵
14 | :return:返回一个形如 Tensor 的 numpy 矩阵
15 | Example:
16 | Inputs:array.shape = (512,512,3)
17 | Outputs:output.shape = (3,512,512)
18 | """
19 | output = array.transpose((2, 0, 1))
20 | return output
21 |
22 |
23 | def NNormalize(array, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]), dtype=np.float32):
24 | """
25 | :param array: opencv/PIL读取的numpy矩阵
26 | mean: 归一化均值,np.array 格式
27 | std: 归一化标准差,np.array 格式
28 | dtype:输出的 numpy 数据格式,一般 onnx 需要 float32
29 | :return:numpy 矩阵
30 | Example:
31 | Inputs:array 为 opencv/PIL 读取的一张图片
32 | mean=np.array([0.5,0.5,0.5])
33 | std=np.array([0.5,0.5,0.5])
34 | dtype=np.float32
35 | Outputs:output 为归一化后的 numpy 矩阵
36 | """
37 | im = array / 255.0
38 | im = np.divide(np.subtract(im, mean), std)
39 | output = np.asarray(im, dtype=dtype)
40 |
41 | return output
42 |
43 |
44 | def NUnsqueeze(array, axis=0):
45 | """
46 | :param array: opencv/PIL读取的numpy矩阵
47 | axis:要增加的维度
48 | :return:numpy 矩阵
49 | Example:
50 | Inputs:array 为 opencv/PIL 读取的一张图片,array.shape 为 [512,512,3]
51 | axis=0
52 | Outputs:output 为 array 在第 0 维增加一个维度,shape 转为 [1,512,512,3]
53 | """
54 | if axis == 0:
55 | output = array[None, :, :, :]
56 | elif axis == 1:
57 | output = array[:, None, :, :]
58 | elif axis == 2:
59 | output = array[:, :, None, :]
60 | else:
61 | output = array[:, :, :, None]
62 |
63 | return output
64 |
--------------------------------------------------------------------------------
/hivision/creator/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 19:25
5 | @File: utils.py
6 | @IDE: pycharm
7 | @Description:
8 | 通用图像处理工具
9 | """
10 | import cv2
11 | import numpy as np
12 |
13 |
14 | def resize_image_esp(input_image, esp=2000):
15 | """
16 | 输入:
17 | input_path:numpy 图片
18 | esp:限制的最大边长
19 | """
20 | # resize 函数=>可以让原图压缩到最大边为 esp 的尺寸 (不改变比例)
21 | width = input_image.shape[0]
22 |
23 | length = input_image.shape[1]
24 | max_num = max(width, length)
25 |
26 | if max_num > esp:
27 | print("Image resizing...")
28 | if width == max_num:
29 | length = int((esp / width) * length)
30 | width = esp
31 |
32 | else:
33 | width = int((esp / length) * width)
34 | length = esp
35 | print(length, width)
36 | im_resize = cv2.resize(
37 | input_image, (length, width), interpolation=cv2.INTER_AREA
38 | )
39 | return im_resize
40 | else:
41 | return input_image
42 |
43 |
44 | def get_box(
45 | image: np.ndarray,
46 | model: int = 1,
47 | correction_factor=None,
48 | thresh: int = 127,
49 | ):
50 | """
51 | 本函数能够实现输入一张四通道图像,返回图像中最大连续非透明面积的区域的矩形坐标
52 | 本函数将采用 opencv 内置函数来解析整个图像的 mask,并提供一些参数,用于读取图像的位置信息
53 | Args:
54 | image: 四通道矩阵图像
55 | model: 返回值模式
56 | correction_factor: 提供一些边缘扩张接口,输入格式为 list 或者 int:[up, down, left, right]。
57 | 举个例子,假设我们希望剪切出的矩形框左边能够偏左 1 个像素,则输入 [0, 0, 1, 0];
58 | 如果希望右边偏右 1 个像素,则输入 [0, 0, 0, 1]
59 | 如果输入为 int,则默认只会对左右两边做拓展,比如输入 2,则和 [0, 0, 2, 2] 是等效的
60 | thresh: 二值化阈值,为了保持一些羽化效果,thresh 必须要小
61 | Returns:
62 | model 为 1 时,将会返回切割出的矩形框的四个坐标点信息
63 | model 为 2 时,将会返回矩形框四边相距于原图四边的距离
64 | """
65 | # ------------ 数据格式规范部分 -------------- #
66 | # 输入必须为四通道
67 | if correction_factor is None:
68 | correction_factor = [0, 0, 0, 0]
69 | if not isinstance(image, np.ndarray) or len(cv2.split(image)) != 4:
70 | raise TypeError("输入的图像必须为四通道 np.ndarray 类型矩阵!")
71 | # correction_factor 规范化
72 | if isinstance(correction_factor, int):
73 | correction_factor = [0, 0, correction_factor, correction_factor]
74 | elif not isinstance(correction_factor, list):
75 | raise TypeError("correction_factor 必须为 int 或者 list 类型!")
76 | # ------------ 数据格式规范完毕 -------------- #
77 | # 分离 mask
78 | _, _, _, mask = cv2.split(image)
79 | # mask 二值化处理
80 | _, mask = cv2.threshold(mask, thresh=thresh, maxval=255, type=0)
81 | contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
82 | temp = np.ones(image.shape, np.uint8) * 255
83 | cv2.drawContours(temp, contours, -1, (0, 0, 255), -1)
84 | contours_area = []
85 | for cnt in contours:
86 | contours_area.append(cv2.contourArea(cnt))
87 | idx = contours_area.index(max(contours_area))
88 | x, y, w, h = cv2.boundingRect(contours[idx]) # 框出图像
89 | # ------------ 开始输出数据 -------------- #
90 | height, width, _ = image.shape
91 | y_up = y - correction_factor[0] if y - correction_factor[0] >= 0 else 0
92 | y_down = (
93 | y + h + correction_factor[1]
94 | if y + h + correction_factor[1] < height
95 | else height - 1
96 | )
97 | x_left = x - correction_factor[2] if x - correction_factor[2] >= 0 else 0
98 | x_right = (
99 | x + w + correction_factor[3]
100 | if x + w + correction_factor[3] < width
101 | else width - 1
102 | )
103 | if model == 1:
104 | # model=1,将会返回切割出的矩形框的四个坐标点信息
105 | return [y_up, y_down, x_left, x_right]
106 | elif model == 2:
107 | # model=2, 将会返回矩形框四边相距于原图四边的距离
108 | return [y_up, height - y_down, x_left, width - x_right]
109 | else:
110 | raise EOFError("请选择正确的模式!")
111 |
112 |
113 | def detect_distance(value, crop_height, max=0.06, min=0.04):
114 | """
115 | 检测人头顶与照片顶部的距离是否在适当范围内。
116 | 输入:与顶部的差值
117 | 输出:(status, move_value)
118 | status=0 不动
119 | status=1 人脸应向上移动(裁剪框向下移动)
120 | status-2 人脸应向下移动(裁剪框向上移动)
121 | ---------------------------------------
122 | value:头顶与照片顶部的距离
123 | crop_height: 裁剪框的高度
124 | max: 距离的最大值
125 | min: 距离的最小值
126 | ---------------------------------------
127 | """
128 | value = value / crop_height # 头顶往上的像素占图像的比例
129 | if min <= value <= max:
130 | return 0, 0
131 | elif value > max:
132 | # 头顶往上的像素比例高于 max
133 | move_value = value - max
134 | move_value = int(move_value * crop_height)
135 | # print("上移{}".format(move_value))
136 | return 1, move_value
137 | else:
138 | # 头顶往上的像素比例低于 min
139 | move_value = min - value
140 | move_value = int(move_value * crop_height)
141 | # print("下移{}".format(move_value))
142 | return -1, move_value
143 |
144 |
145 | def cutting_rect_pan(
146 | x1, y1, x2, y2, width, height, L1, L2, L3, clockwise, standard_size
147 | ):
148 | """
149 | 本函数的功能是对旋转矫正结果图的裁剪框进行修正 ———— 解决"旋转三角形"现象。
150 | Args:
151 | - x1: int, 裁剪框左上角的横坐标
152 | - y1: int, 裁剪框左上角的纵坐标
153 | - x2: int, 裁剪框右下角的横坐标
154 | - y2: int, 裁剪框右下角的纵坐标
155 | - width: int, 待裁剪图的宽度
156 | - height:int, 待裁剪图的高度
157 | - L1: CLassObject, 根据旋转点连线所构造函数
158 | - L2: CLassObject, 根据旋转点连线所构造函数
159 | - L3: ClassObject, 一个特殊裁切点的坐标
160 | - clockwise: int, 旋转时针状态
161 | - standard_size: tuple, 标准照的尺寸
162 |
163 | Returns:
164 | - x1: int, 新的裁剪框左上角的横坐标
165 | - y1: int, 新的裁剪框左上角的纵坐标
166 | - x2: int, 新的裁剪框右下角的横坐标
167 | - y2: int, 新的裁剪框右下角的纵坐标
168 | - x_bias: int, 裁剪框横坐标方向上的计算偏置量
169 | - y_bias: int, 裁剪框纵坐标方向上的计算偏置量
170 | """
171 | # 用于计算的裁剪框坐标x1_cal,x2_cal,y1_cal,y2_cal(如果裁剪框超出了图像范围,则缩小直至在范围内)
172 | x1_std = x1 if x1 > 0 else 0
173 | x2_std = x2 if x2 < width else width
174 | # y1_std = y1 if y1 > 0 else 0
175 | y2_std = y2 if y2 < height else height
176 |
177 | # 初始化x和y的计算偏置项x_bias和y_bias
178 | x_bias = 0
179 | y_bias = 0
180 |
181 | # 如果顺时针偏转
182 | if clockwise == 1:
183 | if y2 > L1.forward_x(x1_std):
184 | y_bias = int(-(y2_std - L1.forward_x(x1_std)))
185 | if y2 > L2.forward_x(x2_std):
186 | x_bias = int(-(x2_std - L2.forward_y(y2_std)))
187 | x2 = x2_std + x_bias
188 | if x1 < L3.x:
189 | x1 = L3.x
190 | # 如果逆时针偏转
191 | else:
192 | if y2 > L1.forward_x(x1_std):
193 | x_bias = int(L1.forward_y(y2_std) - x1_std)
194 | if y2 > L2.forward_x(x2_std):
195 | y_bias = int(-(y2_std - L2.forward_x(x2_std)))
196 | x1 = x1_std + x_bias
197 | if x2 > L3.x:
198 | x2 = L3.x
199 |
200 | # 计算裁剪框的y的变化
201 | y2 = int(y2_std + y_bias)
202 | new_cut_width = x2 - x1
203 | new_cut_height = int(new_cut_width / standard_size[1] * standard_size[0])
204 | y1 = y2 - new_cut_height
205 |
206 | return x1, y1, x2, y2, x_bias, y_bias
207 |
--------------------------------------------------------------------------------
/hivision/creator/weights/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/creator/weights/.gitkeep
--------------------------------------------------------------------------------
/hivision/error.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 18:32
5 | @File: error.py
6 | @IDE: pycharm
7 | @Description:
8 | 错误处理
9 | """
10 |
11 |
12 | class FaceError(Exception):
13 | def __init__(self, err, face_num):
14 | """
15 | 证件照人脸错误,此时人脸检测失败,可能是没有检测到人脸或者检测到多个人脸
16 | Args:
17 | err: 错误描述
18 | face_num: 告诉此时识别到的人像个数
19 | """
20 | super().__init__(err)
21 | self.face_num = face_num
22 |
23 |
24 | class APIError(Exception):
25 | def __init__(self, err, status_code):
26 | """
27 | API错误
28 | Args:
29 | err: 错误描述
30 | status_code: 告诉此时的错误状态码
31 | """
32 | super().__init__(err)
33 | self.status_code = status_code
34 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/__init__.py:
--------------------------------------------------------------------------------
1 | from .beauty_tools import BeautyTools
2 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/base_adjust.py:
--------------------------------------------------------------------------------
1 | """
2 | 亮度、对比度、锐化、饱和度调整模块
3 | """
4 |
5 | import cv2
6 | import numpy as np
7 |
8 |
9 | def adjust_brightness_contrast_sharpen_saturation(
10 | image,
11 | brightness_factor=0,
12 | contrast_factor=0,
13 | sharpen_strength=0,
14 | saturation_factor=0,
15 | ):
16 | """
17 | 调整图像的亮度、对比度、锐度和饱和度。
18 |
19 | 参数:
20 | image (numpy.ndarray): 输入的图像数组。
21 | brightness_factor (float): 亮度调整因子。大于0增加亮度,小于0降低亮度。
22 | contrast_factor (float): 对比度调整因子。大于0增加对比度,小于0降低对比度。
23 | sharpen_strength (float): 锐化强度。
24 | saturation_factor (float): 饱和度调整因子。大于0增加饱和度,小于0降低饱和度。
25 |
26 | 返回:
27 | numpy.ndarray: 调整后的图像。
28 | """
29 | if (
30 | brightness_factor == 0
31 | and contrast_factor == 0
32 | and sharpen_strength == 0
33 | and saturation_factor == 0
34 | ):
35 | return image.copy()
36 |
37 | adjusted_image = image.copy()
38 |
39 | # 调整饱和度
40 | if saturation_factor != 0:
41 | adjusted_image = adjust_saturation(adjusted_image, saturation_factor)
42 |
43 | # 调整亮度和对比度
44 | alpha = 1.0 + (contrast_factor / 100.0)
45 | beta = brightness_factor
46 | adjusted_image = cv2.convertScaleAbs(adjusted_image, alpha=alpha, beta=beta)
47 |
48 | # 增强锐化
49 | adjusted_image = sharpen_image(adjusted_image, sharpen_strength)
50 |
51 | return adjusted_image
52 |
53 |
54 | def adjust_saturation(image, saturation_factor):
55 | """
56 | 调整图像的饱和度。
57 |
58 | 参数:
59 | image (numpy.ndarray): 输入的图像数组。
60 | saturation_factor (float): 饱和度调整因子。大于0增加饱和度,小于0降低饱和度。
61 |
62 | 返回:
63 | numpy.ndarray: 调整后的图像。
64 | """
65 | hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
66 | h, s, v = cv2.split(hsv)
67 | s = s.astype(np.float32)
68 | s = s + s * (saturation_factor / 100.0)
69 | s = np.clip(s, 0, 255).astype(np.uint8)
70 | hsv = cv2.merge([h, s, v])
71 | return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
72 |
73 |
74 | def sharpen_image(image, strength=0):
75 | """
76 | 对图像进行锐化处理。
77 |
78 | 参数:
79 | image (numpy.ndarray): 输入的图像数组。
80 | strength (float): 锐化强度,范围建议为0-5。0表示不进行锐化。
81 |
82 | 返回:
83 | numpy.ndarray: 锐化后的图像。
84 | """
85 | print(f"Sharpen strength: {strength}")
86 | if strength == 0:
87 | return image.copy()
88 |
89 | strength = strength * 20
90 | kernel_strength = 1 + (strength / 500)
91 |
92 | kernel = (
93 | np.array([[-0.5, -0.5, -0.5], [-0.5, 5, -0.5], [-0.5, -0.5, -0.5]])
94 | * kernel_strength
95 | )
96 |
97 | sharpened = cv2.filter2D(image, -1, kernel)
98 | sharpened = np.clip(sharpened, 0, 255).astype(np.uint8)
99 |
100 | alpha = strength / 200
101 | blended = cv2.addWeighted(image, 1 - alpha, sharpened, alpha, 0)
102 |
103 | return blended
104 |
105 |
106 | # Gradio接口
107 | def base_adjustment(image, brightness, contrast, sharpen, saturation):
108 | adjusted = adjust_brightness_contrast_sharpen_saturation(
109 | image, brightness, contrast, sharpen, saturation
110 | )
111 | return adjusted
112 |
113 |
114 | if __name__ == "__main__":
115 | import gradio as gr
116 |
117 | iface = gr.Interface(
118 | fn=base_adjustment,
119 | inputs=[
120 | gr.Image(label="Input Image", height=400),
121 | gr.Slider(
122 | minimum=-20,
123 | maximum=20,
124 | value=0,
125 | step=1,
126 | label="Brightness",
127 | ),
128 | gr.Slider(
129 | minimum=-100,
130 | maximum=100,
131 | value=0,
132 | step=1,
133 | label="Contrast",
134 | ),
135 | gr.Slider(
136 | minimum=0,
137 | maximum=5,
138 | value=0,
139 | step=1,
140 | label="Sharpen",
141 | ),
142 | gr.Slider(
143 | minimum=-100,
144 | maximum=100,
145 | value=0,
146 | step=1,
147 | label="Saturation",
148 | ),
149 | ],
150 | outputs=gr.Image(label="Adjusted Image"),
151 | title="Image Adjustment",
152 | description="Adjust the brightness, contrast, sharpness, and saturation of an image using sliders.",
153 | )
154 | iface.launch()
155 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/beauty_tools.py:
--------------------------------------------------------------------------------
1 | """
2 | @author: cuny
3 | @file: MakeBeautiful.py
4 | @time: 2022/7/7 20:23
5 | @description:
6 | 美颜工具集合文件,作为暴露在外的插件接口
7 | """
8 |
9 | from .grind_skin import grindSkin
10 | from .whitening import MakeWhiter
11 | from .thin_face import thinFace
12 | import numpy as np
13 |
14 |
15 | def BeautyTools(
16 | input_image: np.ndarray,
17 | landmark,
18 | thinStrength: int,
19 | thinPlace: int,
20 | grindStrength: int,
21 | whiterStrength: int,
22 | ) -> np.ndarray:
23 | """
24 | 美颜工具的接口函数,用于实现美颜效果
25 | Args:
26 | input_image: 输入的图像
27 | landmark: 瘦脸需要的人脸关键点信息,为fd68返回的第二个参数
28 | thinStrength: 瘦脸强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不瘦脸
29 | thinPlace: 选择瘦脸区域,为0-2之间的值,越大瘦脸的点越靠下
30 | grindStrength: 磨皮强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不磨皮
31 | whiterStrength: 美白强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不美白
32 | Returns:
33 | output_image 输出图像
34 | """
35 | try:
36 | _, _, _ = input_image.shape
37 | except ValueError:
38 | raise TypeError("输入图像必须为3通道或者4通道!")
39 | # 三通道或者四通道图像
40 | # 首先进行瘦脸
41 | input_image = thinFace(
42 | input_image, landmark, place=thinPlace, strength=thinStrength
43 | )
44 | # 其次进行磨皮
45 | input_image = grindSkin(src=input_image, strength=grindStrength)
46 | # 最后进行美白
47 | makeWhiter = MakeWhiter()
48 | input_image = makeWhiter.run(input_image, strength=whiterStrength)
49 | return input_image
50 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/grind_skin.py:
--------------------------------------------------------------------------------
1 | # Required Libraries
2 | import cv2
3 | import numpy as np
4 | import gradio as gr
5 |
6 |
7 | def annotate_image(image, grind_degree, detail_degree, strength):
8 | """Annotates the image with parameters in the lower-left corner."""
9 | font = cv2.FONT_HERSHEY_SIMPLEX
10 | font_scale = 0.5
11 | color = (0, 0, 255)
12 | thickness = 1
13 | line_type = cv2.LINE_AA
14 |
15 | # Text positions
16 | y_offset = 20
17 | x_offset = 10
18 | y_base = image.shape[0] - 10
19 |
20 | # Define each line of the annotation
21 | lines = [
22 | f"Grind Degree: {grind_degree}",
23 | f"Detail Degree: {detail_degree}",
24 | f"Strength: {strength}",
25 | ]
26 |
27 | # Draw the text lines on the image
28 | for i, line in enumerate(lines):
29 | y_position = y_base - (i * y_offset)
30 | cv2.putText(
31 | image,
32 | line,
33 | (x_offset, y_position),
34 | font,
35 | font_scale,
36 | color,
37 | thickness,
38 | line_type,
39 | )
40 |
41 | return image
42 |
43 |
44 | def grindSkin(src, grindDegree: int = 3, detailDegree: int = 1, strength: int = 9):
45 | """
46 | Dest =(Src * (100 - Opacity) + (Src + 2 * GaussBlur(EPFFilter(Src) - Src)) * Opacity) / 100
47 | 人像磨皮方案
48 | Args:
49 | src: 原图
50 | grindDegree: 磨皮程度调节参数
51 | detailDegree: 细节程度调节参数
52 | strength: 融合程度,作为磨皮强度(0 - 10)
53 |
54 | Returns:
55 | 磨皮后的图像
56 | """
57 | if strength <= 0:
58 | return src
59 | dst = src.copy()
60 | opacity = min(10.0, strength) / 10.0
61 | dx = grindDegree * 5
62 | fc = grindDegree * 12.5
63 | temp1 = cv2.bilateralFilter(src[:, :, :3], dx, fc, fc)
64 | temp2 = cv2.subtract(temp1, src[:, :, :3])
65 | temp3 = cv2.GaussianBlur(temp2, (2 * detailDegree - 1, 2 * detailDegree - 1), 0)
66 | temp4 = cv2.add(cv2.add(temp3, temp3), src[:, :, :3])
67 | dst[:, :, :3] = cv2.addWeighted(temp4, opacity, src[:, :, :3], 1 - opacity, 0.0)
68 | return dst
69 |
70 |
71 | def process_image(input_img, grind_degree, detail_degree, strength):
72 | # Reading the image using OpenCV
73 | img = cv2.cvtColor(input_img, cv2.COLOR_RGB2BGR)
74 | # Processing the image
75 | output_img = grindSkin(img, grind_degree, detail_degree, strength)
76 | # Annotating the processed image with parameters
77 | output_img_annotated = annotate_image(
78 | output_img.copy(), grind_degree, detail_degree, strength
79 | )
80 | # Horizontal stacking of input and processed images
81 | combined_img = cv2.hconcat([img, output_img_annotated])
82 | # Convert the combined image back to RGB for display
83 | combined_img_rgb = cv2.cvtColor(combined_img, cv2.COLOR_BGR2RGB)
84 | return combined_img_rgb
85 |
86 |
87 | with gr.Blocks(title="Skin Grinding") as iface:
88 | gr.Markdown("## Skin Grinding Application")
89 |
90 | with gr.Row():
91 | image_input = gr.Image(type="numpy", label="Input Image")
92 | image_output = gr.Image(label="Output Image")
93 |
94 | grind_degree_slider = gr.Slider(
95 | minimum=1, maximum=10, value=3, step=1, label="Grind Degree"
96 | )
97 | detail_degree_slider = gr.Slider(
98 | minimum=1, maximum=10, value=1, step=1, label="Detail Degree"
99 | )
100 | strength_slider = gr.Slider(
101 | minimum=0, maximum=10, value=9, step=1, label="Strength"
102 | )
103 |
104 | gr.Button("Process Image").click(
105 | fn=process_image,
106 | inputs=[
107 | image_input,
108 | grind_degree_slider,
109 | detail_degree_slider,
110 | strength_slider,
111 | ],
112 | outputs=image_output,
113 | )
114 |
115 | if __name__ == "__main__":
116 | iface.launch()
117 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/handler.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | from hivision.creator.context import Context
3 | from hivision.plugin.beauty.whitening import make_whitening
4 | from hivision.plugin.beauty.base_adjust import (
5 | adjust_brightness_contrast_sharpen_saturation,
6 | )
7 |
8 |
9 | def beauty_face(ctx: Context):
10 | """
11 | 对人脸进行美颜处理
12 | 1. 美白
13 | 2. 亮度
14 |
15 | :param ctx: Context对象,包含处理参数和图像
16 | """
17 | middle_image = ctx.origin_image.copy()
18 | processed = False
19 |
20 | # 如果美白强度大于0,进行美白处理
21 | if ctx.params.whitening_strength > 0:
22 | middle_image = make_whitening(middle_image, ctx.params.whitening_strength)
23 | processed = True
24 |
25 | # 如果亮度、对比度、锐化强度不为0,进行亮度、对比度、锐化处理
26 | if (
27 | ctx.params.brightness_strength != 0
28 | or ctx.params.contrast_strength != 0
29 | or ctx.params.sharpen_strength != 0
30 | or ctx.params.saturation_strength != 0
31 | ):
32 | middle_image = adjust_brightness_contrast_sharpen_saturation(
33 | middle_image,
34 | ctx.params.brightness_strength,
35 | ctx.params.contrast_strength,
36 | ctx.params.sharpen_strength,
37 | ctx.params.saturation_strength,
38 | )
39 | processed = True
40 |
41 | # 如果进行了美颜处理,更新matting_image
42 | if processed:
43 | # 分离中间图像的BGR通道
44 | b, g, r = cv2.split(middle_image)
45 | # 从原始matting_image中获取alpha通道
46 | _, _, _, alpha = cv2.split(ctx.matting_image)
47 | # 合并处理后的BGR通道和原始alpha通道
48 | ctx.matting_image = cv2.merge((b, g, r, alpha))
49 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/lut/lut_origin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/plugin/beauty/lut/lut_origin.png
--------------------------------------------------------------------------------
/hivision/plugin/beauty/thin_face.py:
--------------------------------------------------------------------------------
1 | """
2 | @author: cuny
3 | @file: ThinFace.py
4 | @time: 2022/7/2 15:50
5 | @description:
6 | 瘦脸算法,用到了图像局部平移法
7 | 先使用人脸关键点检测,然后再使用图像局部平移法
8 | 需要注意的是,这部分不会包含dlib人脸关键点检测,因为考虑到模型载入的问题
9 | """
10 |
11 | import cv2
12 | import math
13 | import numpy as np
14 |
15 |
16 | class TranslationWarp(object):
17 | """
18 | 本类包含瘦脸算法,由于瘦脸算法包含了很多个版本,所以以类的方式呈现
19 | 前两个算法没什么好讲的,网上资料很多
20 | 第三个采用numpy内部的自定义函数处理,在处理速度上有一些提升
21 | 最后采用cv2.map算法,处理速度大幅度提升
22 | """
23 |
24 | # 瘦脸
25 | @staticmethod
26 | def localTranslationWarp(srcImg, startX, startY, endX, endY, radius):
27 | # 双线性插值法
28 | def BilinearInsert(src, ux, uy):
29 | w, h, c = src.shape
30 | if c == 3:
31 | x1 = int(ux)
32 | x2 = x1 + 1
33 | y1 = int(uy)
34 | y2 = y1 + 1
35 | part1 = (
36 | src[y1, x1].astype(np.float64) * (float(x2) - ux) * (float(y2) - uy)
37 | )
38 | part2 = (
39 | src[y1, x2].astype(np.float64) * (ux - float(x1)) * (float(y2) - uy)
40 | )
41 | part3 = (
42 | src[y2, x1].astype(np.float64) * (float(x2) - ux) * (uy - float(y1))
43 | )
44 | part4 = (
45 | src[y2, x2].astype(np.float64) * (ux - float(x1)) * (uy - float(y1))
46 | )
47 | insertValue = part1 + part2 + part3 + part4
48 | return insertValue.astype(np.int8)
49 |
50 | ddradius = float(radius * radius) # 圆的半径
51 | copyImg = srcImg.copy() # copy后的图像矩阵
52 | # 计算公式中的|m-c|^2
53 | ddmc = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)
54 | H, W, C = srcImg.shape # 获取图像的形状
55 | for i in range(W):
56 | for j in range(H):
57 | # # 计算该点是否在形变圆的范围之内
58 | # # 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
59 | if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
60 | continue
61 | distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
62 | if distance < ddradius:
63 | # 计算出(i,j)坐标的原坐标
64 | # 计算公式中右边平方号里的部分
65 | ratio = (ddradius - distance) / (ddradius - distance + ddmc)
66 | ratio = ratio * ratio
67 | # 映射原位置
68 | UX = i - ratio * (endX - startX)
69 | UY = j - ratio * (endY - startY)
70 |
71 | # 根据双线性插值法得到UX,UY的值
72 | # start_ = time.time()
73 | value = BilinearInsert(srcImg, UX, UY)
74 | # print(f"双线性插值耗时;{time.time() - start_}")
75 | # 改变当前 i ,j的值
76 | copyImg[j, i] = value
77 | return copyImg
78 |
79 | # 瘦脸pro1, 限制了for循环的遍历次数
80 | @staticmethod
81 | def localTranslationWarpLimitFor(
82 | srcImg, startP: np.matrix, endP: np.matrix, radius: float
83 | ):
84 | startX, startY = startP[0, 0], startP[0, 1]
85 | endX, endY = endP[0, 0], endP[0, 1]
86 |
87 | # 双线性插值法
88 | def BilinearInsert(src, ux, uy):
89 | w, h, c = src.shape
90 | if c == 3:
91 | x1 = int(ux)
92 | x2 = x1 + 1
93 | y1 = int(uy)
94 | y2 = y1 + 1
95 | part1 = (
96 | src[y1, x1].astype(np.float64) * (float(x2) - ux) * (float(y2) - uy)
97 | )
98 | part2 = (
99 | src[y1, x2].astype(np.float64) * (ux - float(x1)) * (float(y2) - uy)
100 | )
101 | part3 = (
102 | src[y2, x1].astype(np.float64) * (float(x2) - ux) * (uy - float(y1))
103 | )
104 | part4 = (
105 | src[y2, x2].astype(np.float64) * (ux - float(x1)) * (uy - float(y1))
106 | )
107 | insertValue = part1 + part2 + part3 + part4
108 | return insertValue.astype(np.int8)
109 |
110 | ddradius = float(radius * radius) # 圆的半径
111 | copyImg = srcImg.copy() # copy后的图像矩阵
112 | # 计算公式中的|m-c|^2
113 | ddmc = (endX - startX) ** 2 + (endY - startY) ** 2
114 | # 计算正方形的左上角起始点
115 | startTX, startTY = (
116 | startX - math.floor(radius + 1),
117 | startY - math.floor((radius + 1)),
118 | )
119 | # 计算正方形的右下角的结束点
120 | endTX, endTY = (
121 | startX + math.floor(radius + 1),
122 | startY + math.floor((radius + 1)),
123 | )
124 | # 剪切srcImg
125 | srcImg = srcImg[startTY : endTY + 1, startTX : endTX + 1, :]
126 | # db.cv_show(srcImg)
127 | # 裁剪后的图像相当于在x,y都减少了startX - math.floor(radius + 1)
128 | # 原本的endX, endY在切后的坐标点
129 | endX, endY = (
130 | endX - startX + math.floor(radius + 1),
131 | endY - startY + math.floor(radius + 1),
132 | )
133 | # 原本的startX, startY剪切后的坐标点
134 | startX, startY = (math.floor(radius + 1), math.floor(radius + 1))
135 | H, W, C = srcImg.shape # 获取图像的形状
136 | for i in range(W):
137 | for j in range(H):
138 | # 计算该点是否在形变圆的范围之内
139 | # 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
140 | # if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
141 | # continue
142 | distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
143 | if distance < ddradius:
144 | # 计算出(i,j)坐标的原坐标
145 | # 计算公式中右边平方号里的部分
146 | ratio = (ddradius - distance) / (ddradius - distance + ddmc)
147 | ratio = ratio * ratio
148 | # 映射原位置
149 | UX = i - ratio * (endX - startX)
150 | UY = j - ratio * (endY - startY)
151 |
152 | # 根据双线性插值法得到UX,UY的值
153 | # start_ = time.time()
154 | value = BilinearInsert(srcImg, UX, UY)
155 | # print(f"双线性插值耗时;{time.time() - start_}")
156 | # 改变当前 i ,j的值
157 | copyImg[j + startTY, i + startTX] = value
158 | return copyImg
159 |
160 | # # 瘦脸pro2,采用了numpy自定义函数做处理
161 | # def localTranslationWarpNumpy(self, srcImg, startP: np.matrix, endP: np.matrix, radius: float):
162 | # startX , startY = startP[0, 0], startP[0, 1]
163 | # endX, endY = endP[0, 0], endP[0, 1]
164 | # ddradius = float(radius * radius) # 圆的半径
165 | # copyImg = srcImg.copy() # copy后的图像矩阵
166 | # # 计算公式中的|m-c|^2
167 | # ddmc = (endX - startX)**2 + (endY - startY)**2
168 | # # 计算正方形的左上角起始点
169 | # startTX, startTY = (startX - math.floor(radius + 1), startY - math.floor((radius + 1)))
170 | # # 计算正方形的右下角的结束点
171 | # endTX, endTY = (startX + math.floor(radius + 1), startY + math.floor((radius + 1)))
172 | # # 剪切srcImg
173 | # self.thinImage = srcImg[startTY : endTY + 1, startTX : endTX + 1, :]
174 | # # s = self.thinImage
175 | # # db.cv_show(srcImg)
176 | # # 裁剪后的图像相当于在x,y都减少了startX - math.floor(radius + 1)
177 | # # 原本的endX, endY在切后的坐标点
178 | # endX, endY = (endX - startX + math.floor(radius + 1), endY - startY + math.floor(radius + 1))
179 | # # 原本的startX, startY剪切后的坐标点
180 | # startX ,startY = (math.floor(radius + 1), math.floor(radius + 1))
181 | # H, W, C = self.thinImage.shape # 获取图像的形状
182 | # index_m = np.arange(H * W).reshape((H, W))
183 | # triangle_ufunc = np.frompyfunc(self.process, 9, 3)
184 | # # start_ = time.time()
185 | # finalImgB, finalImgG, finalImgR = triangle_ufunc(index_m, self, W, ddradius, ddmc, startX, startY, endX, endY)
186 | # finaleImg = np.dstack((finalImgB, finalImgG, finalImgR)).astype(np.uint8)
187 | # finaleImg = np.fliplr(np.rot90(finaleImg, -1))
188 | # copyImg[startTY: endTY + 1, startTX: endTX + 1, :] = finaleImg
189 | # # print(f"图像处理耗时;{time.time() - start_}")
190 | # # db.cv_show(copyImg)
191 | # return copyImg
192 |
193 | # 瘦脸pro3,采用opencv内置函数
194 | @staticmethod
195 | def localTranslationWarpFastWithStrength(
196 | srcImg, startP: np.matrix, endP: np.matrix, radius, strength: float = 100.0
197 | ):
198 | """
199 | 采用opencv内置函数
200 | Args:
201 | srcImg: 源图像
202 | startP: 起点位置
203 | endP: 终点位置
204 | radius: 处理半径
205 | strength: 瘦脸强度,一般取100以上
206 |
207 | Returns:
208 |
209 | """
210 | startX, startY = startP[0, 0], startP[0, 1]
211 | endX, endY = endP[0, 0], endP[0, 1]
212 | ddradius = float(radius * radius)
213 | # copyImg = np.zeros(srcImg.shape, np.uint8)
214 | # copyImg = srcImg.copy()
215 |
216 | maskImg = np.zeros(srcImg.shape[:2], np.uint8)
217 | cv2.circle(maskImg, (startX, startY), math.ceil(radius), (255, 255, 255), -1)
218 |
219 | K0 = 100 / strength
220 |
221 | # 计算公式中的|m-c|^2
222 | ddmc_x = (endX - startX) * (endX - startX)
223 | ddmc_y = (endY - startY) * (endY - startY)
224 | H, W, C = srcImg.shape
225 |
226 | mapX = np.vstack([np.arange(W).astype(np.float32).reshape(1, -1)] * H)
227 | mapY = np.hstack([np.arange(H).astype(np.float32).reshape(-1, 1)] * W)
228 |
229 | distance_x = (mapX - startX) * (mapX - startX)
230 | distance_y = (mapY - startY) * (mapY - startY)
231 | distance = distance_x + distance_y
232 | K1 = np.sqrt(distance)
233 | ratio_x = (ddradius - distance_x) / (ddradius - distance_x + K0 * ddmc_x)
234 | ratio_y = (ddradius - distance_y) / (ddradius - distance_y + K0 * ddmc_y)
235 | ratio_x = ratio_x * ratio_x
236 | ratio_y = ratio_y * ratio_y
237 |
238 | UX = mapX - ratio_x * (endX - startX) * (1 - K1 / radius)
239 | UY = mapY - ratio_y * (endY - startY) * (1 - K1 / radius)
240 |
241 | np.copyto(UX, mapX, where=maskImg == 0)
242 | np.copyto(UY, mapY, where=maskImg == 0)
243 | UX = UX.astype(np.float32)
244 | UY = UY.astype(np.float32)
245 | copyImg = cv2.remap(srcImg, UX, UY, interpolation=cv2.INTER_LINEAR)
246 | return copyImg
247 |
248 |
249 | def thinFace(src, landmark, place: int = 0, strength=30.0):
250 | """
251 | 瘦脸程序接口,输入人脸关键点信息和强度,即可实现瘦脸
252 | 注意处理四通道图像
253 | Args:
254 | src: 原图
255 | landmark: 关键点信息
256 | place: 选择瘦脸区域,为0-4之间的值
257 | strength: 瘦脸强度,输入值在0-10之间,如果小于或者等于0,则不瘦脸
258 |
259 | Returns:
260 | 瘦脸后的图像
261 | """
262 | strength = min(100.0, strength * 10.0)
263 | if strength <= 0.0:
264 | return src
265 | # 也可以设置瘦脸区域
266 | place = max(0, min(4, int(place)))
267 | left_landmark = landmark[4 + place]
268 | left_landmark_down = landmark[6 + place]
269 | right_landmark = landmark[13 + place]
270 | right_landmark_down = landmark[15 + place]
271 | endPt = landmark[58]
272 | # 计算第4个点到第6个点的距离作为瘦脸距离
273 | r_left = math.sqrt(
274 | (left_landmark[0, 0] - left_landmark_down[0, 0]) ** 2
275 | + (left_landmark[0, 1] - left_landmark_down[0, 1]) ** 2
276 | )
277 |
278 | # 计算第14个点到第16个点的距离作为瘦脸距离
279 | r_right = math.sqrt(
280 | (right_landmark[0, 0] - right_landmark_down[0, 0]) ** 2
281 | + (right_landmark[0, 1] - right_landmark_down[0, 1]) ** 2
282 | )
283 | # 瘦左边脸
284 | thin_image = TranslationWarp.localTranslationWarpFastWithStrength(
285 | src, left_landmark[0], endPt[0], r_left, strength
286 | )
287 | # 瘦右边脸
288 | thin_image = TranslationWarp.localTranslationWarpFastWithStrength(
289 | thin_image, right_landmark[0], endPt[0], r_right, strength
290 | )
291 | return thin_image
292 |
293 |
294 | # if __name__ == "__main__":
295 | # import os
296 | # from hycv.FaceDetection68.faceDetection68 import FaceDetection68
297 |
298 | # local_file = os.path.dirname(__file__)
299 | # PREDICTOR_PATH = f"{local_file}/weights/shape_predictor_68_face_landmarks.dat" # 关键点检测模型路径
300 | # fd68 = FaceDetection68(model_path=PREDICTOR_PATH)
301 | # input_image = cv2.imread("test_image/4.jpg", -1)
302 | # _, landmark_, _ = fd68.facePoints(input_image)
303 | # output_image = thinFace(input_image, landmark_, strength=30.2)
304 | # cv2.imwrite("thinFaceCompare.png", np.hstack((input_image, output_image)))
305 |
--------------------------------------------------------------------------------
/hivision/plugin/beauty/whitening.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | import os
4 | import gradio as gr
5 |
6 |
7 | class LutWhite:
8 | CUBE64_ROWS = 8
9 | CUBE64_SIZE = 64
10 | CUBE256_SIZE = 256
11 | CUBE_SCALE = CUBE256_SIZE // CUBE64_SIZE
12 |
13 | def __init__(self, lut_image):
14 | self.lut = self._create_lut(lut_image)
15 |
16 | def _create_lut(self, lut_image):
17 | reshape_lut = np.zeros(
18 | (self.CUBE256_SIZE, self.CUBE256_SIZE, self.CUBE256_SIZE, 3), dtype=np.uint8
19 | )
20 | for i in range(self.CUBE64_SIZE):
21 | tmp = i // self.CUBE64_ROWS
22 | cx = (i % self.CUBE64_ROWS) * self.CUBE64_SIZE
23 | cy = tmp * self.CUBE64_SIZE
24 | cube64 = lut_image[cy : cy + self.CUBE64_SIZE, cx : cx + self.CUBE64_SIZE]
25 | if cube64.size == 0:
26 | continue
27 | cube256 = cv2.resize(cube64, (self.CUBE256_SIZE, self.CUBE256_SIZE))
28 | reshape_lut[i * self.CUBE_SCALE : (i + 1) * self.CUBE_SCALE] = cube256
29 | return reshape_lut
30 |
31 | def apply(self, src):
32 | b, g, r = src[:, :, 0], src[:, :, 1], src[:, :, 2]
33 | return self.lut[b, g, r]
34 |
35 |
36 | class MakeWhiter:
37 | def __init__(self, lut_image):
38 | self.lut_white = LutWhite(lut_image)
39 |
40 | def run(self, src: np.ndarray, strength: int) -> np.ndarray:
41 | strength = np.clip(strength / 10.0, 0, 1)
42 | if strength <= 0:
43 | return src
44 | img = self.lut_white.apply(src[:, :, :3])
45 | return cv2.addWeighted(src[:, :, :3], 1 - strength, img, strength, 0)
46 |
47 |
48 | base_dir = os.path.dirname(os.path.abspath(__file__))
49 | default_lut = cv2.imread(os.path.join(base_dir, "lut/lut_origin.png"))
50 | make_whiter = MakeWhiter(default_lut)
51 |
52 |
53 | def make_whitening(image, strength):
54 | image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
55 |
56 | iteration = strength // 10
57 | bias = strength % 10
58 |
59 | for i in range(iteration):
60 | image = make_whiter.run(image, 10)
61 |
62 | image = make_whiter.run(image, bias)
63 |
64 | return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
65 |
66 |
67 | def make_whitening_png(image, strength):
68 | image = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA)
69 |
70 | b, g, r, a = cv2.split(image)
71 | bgr_image = cv2.merge((b, g, r))
72 |
73 | b_w, g_w, r_w = cv2.split(make_whiter.run(bgr_image, strength))
74 | output_image = cv2.merge((b_w, g_w, r_w, a))
75 |
76 | return cv2.cvtColor(output_image, cv2.COLOR_RGBA2BGRA)
77 |
78 |
79 | # 启动Gradio应用
80 | if __name__ == "__main__":
81 | demo = gr.Interface(
82 | fn=make_whitening,
83 | inputs=[
84 | gr.Image(type="pil", image_mode="RGBA", label="Input Image"),
85 | gr.Slider(0, 30, step=1, label="Whitening Strength"),
86 | ],
87 | outputs=gr.Image(type="pil"),
88 | title="Image Whitening Demo",
89 | description="Upload an image and adjust the whitening strength to see the effect.",
90 | )
91 | demo.launch()
92 |
--------------------------------------------------------------------------------
/hivision/plugin/font/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/plugin/font/.gitkeep
--------------------------------------------------------------------------------
/hivision/plugin/font/青鸟华光简琥珀.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/plugin/font/青鸟华光简琥珀.ttf
--------------------------------------------------------------------------------
/hivision/plugin/template/assets/template_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/plugin/template/assets/template_1.png
--------------------------------------------------------------------------------
/hivision/plugin/template/assets/template_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zeyi-Lin/HivisionIDPhotos/d993cfb1d8453383254db6cbce2bab8173ac3ae0/hivision/plugin/template/assets/template_2.png
--------------------------------------------------------------------------------
/hivision/plugin/template/assets/template_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template_1": {
3 | "width": 1080,
4 | "height": 1400,
5 | "anchor_points": {
6 | "left_top": [358, 153],
7 | "right_top": [1017, 353],
8 | "left_bottom": [56, 1134],
9 | "right_bottom": [747, 1332],
10 | "rotation": -16.42
11 | }
12 | },
13 | "template_2": {
14 | "width": 1080,
15 | "height": 1440,
16 | "anchor_points": {
17 | "left_top": [199, 199],
18 | "right_top": [921, 216],
19 | "left_bottom": [163, 1129],
20 | "right_bottom": [876, 1153],
21 | "rotation": -2.2
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/hivision/plugin/template/template_calculator.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | import json
4 | from hivision.creator.rotation_adjust import rotate_bound
5 | import os
6 |
7 | base_path = os.path.dirname(os.path.abspath(__file__))
8 | template_config_path = os.path.join(base_path, 'assets', 'template_config.json')
9 |
10 | def generte_template_photo(template_name: str, input_image: np.ndarray) -> np.ndarray:
11 | """
12 | 生成模板照片
13 | :param template_name: 模板名称
14 | :param input_image: 输入图像
15 | :return: 模板照片
16 | """
17 | # 读取模板配置json
18 | with open(template_config_path, 'r') as f:
19 | template_config_dict = json.load(f)
20 | # 获取对应该模板的配置
21 | template_config = template_config_dict[template_name]
22 |
23 | template_width = template_config['width']
24 | template_height = template_config['height']
25 |
26 | anchor_points = template_config['anchor_points']
27 | rotation = anchor_points['rotation']
28 | left_top = anchor_points['left_top']
29 | right_top = anchor_points['right_top']
30 | left_bottom = anchor_points['left_bottom']
31 | right_bottom = anchor_points['right_bottom']
32 |
33 | if rotation < 0:
34 | height = right_bottom[1] - left_top[1]
35 | width = right_top[0] - left_bottom[0]
36 | else:
37 | height = left_top[1] - right_bottom[1]
38 | width = left_bottom[0] - right_top[0]
39 |
40 | # 读取模板图像
41 | template_image_path = os.path.join(base_path, 'assets', f'{template_name}.png')
42 | template_image = cv2.imread(template_image_path, cv2.IMREAD_UNCHANGED)
43 |
44 | # 无损旋转
45 | rotated_image = rotate_bound(input_image, -1 * rotation)[0]
46 | rotated_image_height, rotated_image_width, _ = rotated_image.shape
47 |
48 | # 计算缩放比例
49 | scale_x = width / rotated_image_width
50 | scale_y = height / rotated_image_height
51 | scale = max(scale_x, scale_y)
52 |
53 | resized_image = cv2.resize(rotated_image, None, fx=scale, fy=scale)
54 | resized_height, resized_width, _ = resized_image.shape
55 |
56 | # 创建一个与template_image大小相同的背景,使用白色填充
57 | result = np.full((template_height, template_width, 3), 255, dtype=np.uint8)
58 |
59 | # 计算粘贴位置
60 | paste_x = left_bottom[0]
61 | paste_y = left_top[1]
62 |
63 | # 确保不会超出边界
64 | paste_height = min(resized_height, template_height - paste_y)
65 | paste_width = min(resized_width, template_width - paste_x)
66 |
67 | # 将旋转后的图像粘贴到结果图像上
68 | result[paste_y:paste_y+paste_height, paste_x:paste_x+paste_width] = resized_image[:paste_height, :paste_width]
69 |
70 | template_image = cv2.cvtColor(template_image, cv2.COLOR_BGRA2RGBA)
71 |
72 | # 将template_image叠加到结果图像上
73 | if template_image.shape[2] == 4: # 确保template_image有alpha通道
74 | alpha = template_image[:, :, 3] / 255.0
75 | for c in range(0, 3):
76 | result[:, :, c] = result[:, :, c] * (1 - alpha) + template_image[:, :, c] * alpha
77 |
78 | return result
79 |
--------------------------------------------------------------------------------
/hivision/plugin/watermark.py:
--------------------------------------------------------------------------------
1 | """
2 | Reference: https://gist.github.com/Deali-Axy/e22ea79bfbe785f9017b2e3cd7fdb3eb
3 | """
4 |
5 | import enum
6 | import os
7 | import math
8 | import textwrap
9 | from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageChops
10 | import os
11 |
12 | base_path = os.path.abspath(os.path.dirname(__file__))
13 |
14 |
15 | class WatermarkerStyles(enum.Enum):
16 | """水印样式"""
17 |
18 | STRIPED = 1 # 斜向重复
19 | CENTRAL = 2 # 居中
20 |
21 |
22 | class Watermarker(object):
23 | """图片水印工具"""
24 |
25 | def __init__(
26 | self,
27 | input_image: Image.Image,
28 | text: str,
29 | style: WatermarkerStyles,
30 | angle=30,
31 | color="#8B8B1B",
32 | font_file="青鸟华光简琥珀.ttf",
33 | opacity=0.15,
34 | size=50,
35 | space=75,
36 | chars_per_line=8,
37 | font_height_crop=1.2,
38 | ):
39 | """_summary_
40 |
41 | Parameters
42 | ----------
43 | input_image : Image.Image
44 | PIL图片对象
45 | text : str
46 | 水印文字
47 | style : WatermarkerStyles
48 | 水印样式
49 | angle : int, optional
50 | 水印角度, by default 30
51 | color : str, optional
52 | 水印颜色, by default "#8B8B1B"
53 | font_file : str, optional
54 | 字体文件, by default "青鸟华光简琥珀.ttf"
55 | font_height_crop : float, optional
56 | 字体高度裁剪比例, by default 1.2
57 | opacity : float, optional
58 | 水印透明度, by default 0.15
59 | size : int, optional
60 | 字体大小, by default 50
61 | space : int, optional
62 | 水印间距, by default 75
63 | chars_per_line : int, optional
64 | 每行字符数, by default 8
65 | """
66 | self.input_image = input_image
67 | self.text = text
68 | self.style = style
69 | self.angle = angle
70 | self.color = color
71 | self.font_file = os.path.join(base_path, "font", font_file)
72 | self.font_height_crop = font_height_crop
73 | self.opacity = opacity
74 | self.size = size
75 | self.space = space
76 | self.chars_per_line = chars_per_line
77 | self._result_image = None
78 |
79 | @staticmethod
80 | def set_image_opacity(image: Image, opacity: float):
81 | alpha = image.split()[3]
82 | alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
83 | image.putalpha(alpha)
84 | return image
85 |
86 | @staticmethod
87 | def crop_image_edge(image: Image):
88 | bg = Image.new(mode="RGBA", size=image.size)
89 | diff = ImageChops.difference(image, bg)
90 | bbox = diff.getbbox()
91 | if bbox:
92 | return image.crop(bbox)
93 | return image
94 |
95 | def _add_mark_striped(self):
96 | origin_image = self.input_image.convert("RGBA")
97 | width = len(self.text) * self.size
98 | height = round(self.size * self.font_height_crop)
99 | watermark_image = Image.new(mode="RGBA", size=(width, height))
100 | draw_table = ImageDraw.Draw(watermark_image)
101 | draw_table.text(
102 | (0, 0),
103 | self.text,
104 | fill=self.color,
105 | font=ImageFont.truetype(self.font_file, size=self.size),
106 | )
107 | watermark_image = Watermarker.crop_image_edge(watermark_image)
108 | Watermarker.set_image_opacity(watermark_image, self.opacity)
109 |
110 | c = int(math.sqrt(origin_image.size[0] ** 2 + origin_image.size[1] ** 2))
111 | watermark_mask = Image.new(mode="RGBA", size=(c, c))
112 | y, idx = 0, 0
113 | while y < c:
114 | x = -int((watermark_image.size[0] + self.space) * 0.5 * idx)
115 | idx = (idx + 1) % 2
116 | while x < c:
117 | watermark_mask.paste(watermark_image, (x, y))
118 | x += watermark_image.size[0] + self.space
119 | y += watermark_image.size[1] + self.space
120 |
121 | watermark_mask = watermark_mask.rotate(self.angle)
122 | origin_image.paste(
123 | watermark_mask,
124 | (int((origin_image.size[0] - c) / 2), int((origin_image.size[1] - c) / 2)),
125 | mask=watermark_mask.split()[3],
126 | )
127 | return origin_image
128 |
129 | def _add_mark_central(self):
130 | origin_image = self.input_image.convert("RGBA")
131 | text_lines = textwrap.wrap(self.text, width=self.chars_per_line)
132 | text = "\n".join(text_lines)
133 | width = len(text) * self.size
134 | height = round(self.size * self.font_height_crop * len(text_lines))
135 | watermark_image = Image.new(mode="RGBA", size=(width, height))
136 | draw_table = ImageDraw.Draw(watermark_image)
137 | draw_table.text(
138 | (0, 0),
139 | text,
140 | fill=self.color,
141 | font=ImageFont.truetype(self.font_file, size=self.size),
142 | )
143 | watermark_image = Watermarker.crop_image_edge(watermark_image)
144 | Watermarker.set_image_opacity(watermark_image, self.opacity)
145 |
146 | c = int(math.sqrt(origin_image.size[0] ** 2 + origin_image.size[1] ** 2))
147 | watermark_mask = Image.new(mode="RGBA", size=(c, c))
148 | watermark_mask.paste(
149 | watermark_image,
150 | (
151 | int((watermark_mask.width - watermark_image.width) / 2),
152 | int((watermark_mask.height - watermark_image.height) / 2),
153 | ),
154 | )
155 | watermark_mask = watermark_mask.rotate(self.angle)
156 |
157 | origin_image.paste(
158 | watermark_mask,
159 | (
160 | int((origin_image.width - watermark_mask.width) / 2),
161 | int((origin_image.height - watermark_mask.height) / 2),
162 | ),
163 | mask=watermark_mask.split()[3],
164 | )
165 | return origin_image
166 |
167 | @property
168 | def image(self):
169 | if not self._result_image:
170 | if self.style == WatermarkerStyles.STRIPED:
171 | self._result_image = self._add_mark_striped()
172 | elif self.style == WatermarkerStyles.CENTRAL:
173 | self._result_image = self._add_mark_central()
174 | return self._result_image
175 |
176 | def save(self, file_path: str, image_format: str = "png"):
177 | with open(file_path, "wb") as f:
178 | self.image.save(f, image_format)
179 |
180 |
181 | # Gradio 接口
182 | def watermark_image(
183 | image,
184 | text,
185 | style,
186 | angle,
187 | color,
188 | opacity,
189 | size,
190 | space,
191 | ):
192 | # 创建 Watermarker 实例
193 | watermarker = Watermarker(
194 | input_image=image,
195 | text=text,
196 | style=(
197 | WatermarkerStyles.STRIPED
198 | if style == "STRIPED"
199 | else WatermarkerStyles.CENTRAL
200 | ),
201 | angle=angle,
202 | color=color,
203 | opacity=opacity,
204 | size=size,
205 | space=space,
206 | )
207 |
208 | # 返回带水印的图片
209 | return watermarker.image
210 |
211 |
212 | if __name__ == "__main__":
213 | import gradio as gr
214 |
215 | iface = gr.Interface(
216 | fn=watermark_image,
217 | inputs=[
218 | gr.Image(type="pil", label="上传图片", height=400),
219 | gr.Textbox(label="水印文字"),
220 | gr.Radio(choices=["STRIPED", "CENTRAL"], label="水印样式"),
221 | gr.Slider(minimum=0, maximum=360, value=30, label="水印角度"),
222 | gr.ColorPicker(label="水印颜色"),
223 | gr.Slider(minimum=0, maximum=1, value=0.15, label="水印透明度"),
224 | gr.Slider(minimum=10, maximum=100, value=50, label="字体大小"),
225 | gr.Slider(minimum=10, maximum=200, value=75, label="水印间距"),
226 | ],
227 | outputs=gr.Image(type="pil", label="带水印的图片", height=400),
228 | title="图片水印工具",
229 | description="上传一张图片,添加水印并下载。",
230 | )
231 |
232 | iface.launch()
233 |
--------------------------------------------------------------------------------
/hivision/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from PIL import Image
4 | import io
5 | import numpy as np
6 | import cv2
7 | import base64
8 | from hivision.plugin.watermark import Watermarker, WatermarkerStyles
9 |
10 |
11 | def save_image_dpi_to_bytes(image: np.ndarray, output_image_path: str = None, dpi: int = 300):
12 | """
13 | 设置图像的DPI(每英寸点数)并返回字节流
14 |
15 | :param image: numpy.ndarray, 输入的图像数组
16 | :param output_image_path: Path to save the resized image. 保存调整大小后的图像的路径。
17 | :param dpi: int, 要设置的DPI值,默认为300
18 | """
19 | image = Image.fromarray(image)
20 | # 创建一个字节流对象
21 | byte_stream = io.BytesIO()
22 | # 将图像保存到字节流
23 | image.save(byte_stream, format="PNG", dpi=(dpi, dpi))
24 | # 获取字节流的内容
25 | image_bytes = byte_stream.getvalue()
26 |
27 | # Save the image to the output path
28 | if output_image_path:
29 | with open(output_image_path, "wb") as f:
30 | f.write(image_bytes)
31 |
32 | return image_bytes
33 |
34 |
35 | def resize_image_to_kb(input_image: np.ndarray, output_image_path: str = None, target_size_kb: int = 100, dpi: int = 300):
36 | """
37 | Resize an image to a target size in KB.
38 | 将图像调整大小至目标文件大小(KB)。
39 |
40 | :param input_image_path: Path to the input image. 输入图像的路径。
41 | :param output_image_path: Path to save the resized image. 保存调整大小后的图像的路径。
42 | :param target_size_kb: Target size in KB. 目标文件大小(KB)。
43 |
44 | Example:
45 | resize_image_to_kb('input_image.jpg', 'output_image.jpg', 50)
46 | """
47 |
48 | if isinstance(input_image, np.ndarray):
49 | img = Image.fromarray(input_image)
50 | elif isinstance(input_image, Image.Image):
51 | img = input_image
52 | else:
53 | raise ValueError("input_image must be a NumPy array or PIL Image.")
54 |
55 | # Convert image to RGB mode if it's not
56 | if img.mode != "RGB":
57 | img = img.convert("RGB")
58 |
59 | # Initial quality value
60 | quality = 95
61 |
62 | while True:
63 | # Create a BytesIO object to hold the image data in memory
64 | img_byte_arr = io.BytesIO()
65 |
66 | # Save the image to the BytesIO object with the current quality
67 | img.save(img_byte_arr, format="JPEG", quality=quality, dpi=(dpi, dpi))
68 |
69 | # Get the size of the image in KB
70 | img_size_kb = len(img_byte_arr.getvalue()) / 1024
71 |
72 | # Check if the image size is within the target size
73 | if img_size_kb <= target_size_kb or quality == 1:
74 | # If the image is smaller than the target size, add padding
75 | if img_size_kb < target_size_kb:
76 | padding_size = int(
77 | (target_size_kb * 1024) - len(img_byte_arr.getvalue())
78 | )
79 | padding = b"\x00" * padding_size
80 | img_byte_arr.write(padding)
81 |
82 | # Save the image to the output path
83 | if output_image_path:
84 | with open(output_image_path, "wb") as f:
85 | f.write(img_byte_arr.getvalue())
86 |
87 | return img_byte_arr.getvalue()
88 |
89 | # Reduce the quality if the image is still too large
90 | quality -= 5
91 |
92 | # Ensure quality does not go below 1
93 | if quality < 1:
94 | quality = 1
95 |
96 |
97 | def resize_image_to_kb_base64(input_image, target_size_kb, mode="exact"):
98 | """
99 | Resize an image to a target size in KB and return it as a base64 encoded string.
100 | 将图像调整大小至目标文件大小(KB)并返回base64编码的字符串。
101 |
102 | :param input_image: Input image as a NumPy array or PIL Image. 输入图像,可以是NumPy数组或PIL图像。
103 | :param target_size_kb: Target size in KB. 目标文件大小(KB)。
104 | :param mode: Mode of resizing ('exact', 'max', 'min'). 模式:'exact'(精确大小)、'max'(不大于)、'min'(不小于)。
105 |
106 | :return: Base64 encoded string of the resized image. 调整大小后的图像的base64编码字符串。
107 | """
108 |
109 | if isinstance(input_image, np.ndarray):
110 | img = Image.fromarray(input_image)
111 | elif isinstance(input_image, Image.Image):
112 | img = input_image
113 | else:
114 | raise ValueError("input_image must be a NumPy array or PIL Image.")
115 |
116 | # Convert image to RGB mode if it's not
117 | if img.mode != "RGB":
118 | img = img.convert("RGB")
119 |
120 | # Initial quality value
121 | quality = 95
122 |
123 | while True:
124 | # Create a BytesIO object to hold the image data in memory
125 | img_byte_arr = io.BytesIO()
126 |
127 | # Save the image to the BytesIO object with the current quality
128 | img.save(img_byte_arr, format="JPEG", quality=quality)
129 |
130 | # Get the size of the image in KB
131 | img_size_kb = len(img_byte_arr.getvalue()) / 1024
132 |
133 | # Check based on the mode
134 | if mode == "exact":
135 | # If the image size is equal to the target size, we can return it
136 | if img_size_kb == target_size_kb:
137 | break
138 |
139 | # If the image is smaller than the target size, add padding
140 | elif img_size_kb < target_size_kb:
141 | padding_size = int(
142 | (target_size_kb * 1024) - len(img_byte_arr.getvalue())
143 | )
144 | padding = b"\x00" * padding_size
145 | img_byte_arr.write(padding)
146 | break
147 |
148 | elif mode == "max":
149 | # If the image size is within the target size, we can return it
150 | if img_size_kb <= target_size_kb or quality == 1:
151 | break
152 |
153 | elif mode == "min":
154 | # If the image size is greater than or equal to the target size, we can return it
155 | if img_size_kb >= target_size_kb:
156 | break
157 |
158 | # Reduce the quality if the image is still too large
159 | quality -= 5
160 |
161 | # Ensure quality does not go below 1
162 | if quality < 1:
163 | quality = 1
164 |
165 | # Encode the image data to base64
166 | img_base64 = base64.b64encode(img_byte_arr.getvalue()).decode("utf-8")
167 | return "data:image/png;base64," + img_base64
168 |
169 |
170 | def numpy_2_base64(img: np.ndarray) -> str:
171 | _, buffer = cv2.imencode(".png", img)
172 | base64_image = base64.b64encode(buffer).decode("utf-8")
173 |
174 | return "data:image/png;base64," + base64_image
175 |
176 |
177 | def base64_2_numpy(base64_image: str) -> np.ndarray:
178 | # Remove the data URL prefix if present
179 | if base64_image.startswith('data:image'):
180 | base64_image = base64_image.split(',')[1]
181 |
182 | # Decode base64 string to bytes
183 | img_bytes = base64.b64decode(base64_image)
184 |
185 | # Convert bytes to numpy array
186 | img_array = np.frombuffer(img_bytes, dtype=np.uint8)
187 |
188 | # Decode the image array
189 | img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
190 |
191 | return img
192 |
193 | # 字节流转base64
194 | def bytes_2_base64(img_byte_arr: bytes) -> str:
195 | base64_image = base64.b64encode(img_byte_arr).decode("utf-8")
196 | return "data:image/png;base64," + base64_image
197 |
198 |
199 | def save_numpy_image(numpy_img, file_path):
200 | # 检查数组的形状
201 | if numpy_img.shape[2] == 4:
202 | # 将 BGR 转换为 RGB,并保留透明通道
203 | rgb_img = np.concatenate(
204 | (np.flip(numpy_img[:, :, :3], axis=-1), numpy_img[:, :, 3:]), axis=-1
205 | ).astype(np.uint8)
206 | img = Image.fromarray(rgb_img, mode="RGBA")
207 | else:
208 | # 将 BGR 转换为 RGB
209 | rgb_img = np.flip(numpy_img, axis=-1).astype(np.uint8)
210 | img = Image.fromarray(rgb_img, mode="RGB")
211 |
212 | img.save(file_path)
213 |
214 |
215 | def numpy_to_bytes(numpy_img):
216 | img = Image.fromarray(numpy_img)
217 | img_byte_arr = io.BytesIO()
218 | img.save(img_byte_arr, format="PNG")
219 | img_byte_arr.seek(0)
220 | return img_byte_arr
221 |
222 |
223 | def hex_to_rgb(value):
224 | value = value.lstrip("#")
225 | length = len(value)
226 | return tuple(
227 | int(value[i : i + length // 3], 16) for i in range(0, length, length // 3)
228 | )
229 |
230 |
231 | def generate_gradient(start_color, width, height, mode="updown"):
232 | # 定义背景颜色
233 | end_color = (255, 255, 255) # 白色
234 |
235 | # 创建一个空白图像
236 | r_out = np.zeros((height, width), dtype=int)
237 | g_out = np.zeros((height, width), dtype=int)
238 | b_out = np.zeros((height, width), dtype=int)
239 |
240 | if mode == "updown":
241 | # 生成上下渐变色
242 | for y in range(height):
243 | r = int(
244 | (y / height) * end_color[0] + ((height - y) / height) * start_color[0]
245 | )
246 | g = int(
247 | (y / height) * end_color[1] + ((height - y) / height) * start_color[1]
248 | )
249 | b = int(
250 | (y / height) * end_color[2] + ((height - y) / height) * start_color[2]
251 | )
252 | r_out[y, :] = r
253 | g_out[y, :] = g
254 | b_out[y, :] = b
255 |
256 | else:
257 | # 生成中心渐变色
258 | img = np.zeros((height, width, 3))
259 | # 定义椭圆中心和半径
260 | center = (width // 2, height // 2)
261 | end_axies = max(height, width)
262 | # 定义渐变色
263 | end_color = (255, 255, 255)
264 | # 绘制椭圆
265 | for y in range(end_axies):
266 | axes = (end_axies - y, end_axies - y)
267 | r = int(
268 | (y / end_axies) * end_color[0]
269 | + ((end_axies - y) / end_axies) * start_color[0]
270 | )
271 | g = int(
272 | (y / end_axies) * end_color[1]
273 | + ((end_axies - y) / end_axies) * start_color[1]
274 | )
275 | b = int(
276 | (y / end_axies) * end_color[2]
277 | + ((end_axies - y) / end_axies) * start_color[2]
278 | )
279 |
280 | cv2.ellipse(img, center, axes, 0, 0, 360, (b, g, r), -1)
281 | b_out, g_out, r_out = cv2.split(np.uint64(img))
282 |
283 | return r_out, g_out, b_out
284 |
285 |
286 | def add_background(input_image, bgr=(0, 0, 0), mode="pure_color"):
287 | """
288 | 本函数的功能为为透明图像加上背景。
289 | :param input_image: numpy.array(4 channels), 透明图像
290 | :param bgr: tuple, 合成纯色底时的 BGR 值
291 | :param new_background: numpy.array(3 channels),合成自定义图像底时的背景图
292 | :return: output: 合成好的输出图像
293 | """
294 | height, width = input_image.shape[0], input_image.shape[1]
295 | try:
296 | b, g, r, a = cv2.split(input_image)
297 | except ValueError:
298 | raise ValueError(
299 | "The input image must have 4 channels. 输入图像必须有4个通道,即透明图像。"
300 | )
301 |
302 | a_cal = a / 255
303 | if mode == "pure_color":
304 | # 纯色填充
305 | b2 = np.full([height, width], bgr[0], dtype=int)
306 | g2 = np.full([height, width], bgr[1], dtype=int)
307 | r2 = np.full([height, width], bgr[2], dtype=int)
308 | elif mode == "updown_gradient":
309 | b2, g2, r2 = generate_gradient(bgr, width, height, mode="updown")
310 | else:
311 | b2, g2, r2 = generate_gradient(bgr, width, height, mode="center")
312 |
313 | output = cv2.merge(
314 | ((b - b2) * a_cal + b2, (g - g2) * a_cal + g2, (r - r2) * a_cal + r2)
315 | )
316 |
317 | return output
318 |
319 | def add_background_with_image(input_image: np.ndarray, background_image: np.ndarray) -> np.ndarray:
320 | """
321 | 本函数的功能为为透明图像加上背景。
322 | :param input_image: numpy.array(4 channels), 透明图像
323 | :param background_image: numpy.array(3 channels), 背景图像
324 | :return: output: 合成好的输出图像
325 | """
326 | height, width = input_image.shape[:2]
327 | try:
328 | b, g, r, a = cv2.split(input_image)
329 | except ValueError:
330 | raise ValueError(
331 | "The input image must have 4 channels. 输入图像必须有4个通道,即透明图像。"
332 | )
333 |
334 | # 确保背景图像与输入图像大小一致
335 | background_image = cv2.resize(background_image, (width, height), cv2.INTER_AREA)
336 | background_image = cv2.cvtColor(background_image, cv2.COLOR_BGR2RGB)
337 | b2, g2, r2 = cv2.split(background_image)
338 |
339 | a_cal = a / 255.0
340 |
341 | # 修正混合公式
342 | output = cv2.merge(
343 | (b * a_cal + b2 * (1 - a_cal),
344 | g * a_cal + g2 * (1 - a_cal),
345 | r * a_cal + r2 * (1 - a_cal))
346 | )
347 |
348 | return output.astype(np.uint8)
349 |
350 | def add_watermark(
351 | image, text, size=50, opacity=0.5, angle=45, color="#8B8B1B", space=75
352 | ):
353 | image = Image.fromarray(image)
354 | watermarker = Watermarker(
355 | input_image=image,
356 | text=text,
357 | style=WatermarkerStyles.STRIPED,
358 | angle=angle,
359 | color=color,
360 | opacity=opacity,
361 | size=size,
362 | space=space,
363 | )
364 | return np.array(watermarker.image.convert("RGB"))
365 |
--------------------------------------------------------------------------------
/inference.py:
--------------------------------------------------------------------------------
1 | import os
2 | import cv2
3 | import argparse
4 | import numpy as np
5 | from hivision.error import FaceError
6 | from hivision.utils import hex_to_rgb, resize_image_to_kb, add_background, save_image_dpi_to_bytes
7 | from hivision import IDCreator
8 | from hivision.creator.layout_calculator import (
9 | generate_layout_array,
10 | generate_layout_image,
11 | )
12 | from hivision.creator.choose_handler import choose_handler
13 | from hivision.utils import hex_to_rgb, resize_image_to_kb
14 |
15 |
16 | INFERENCE_TYPE = [
17 | "idphoto",
18 | "human_matting",
19 | "add_background",
20 | "generate_layout_photos",
21 | "idphoto_crop",
22 | ]
23 | MATTING_MODEL = [
24 | "hivision_modnet",
25 | "modnet_photographic_portrait_matting",
26 | "mnn_hivision_modnet",
27 | "rmbg-1.4",
28 | "birefnet-v1-lite",
29 | ]
30 | FACE_DETECT_MODEL = [
31 | "mtcnn",
32 | "face_plusplus",
33 | "retinaface-resnet50",
34 | ]
35 | RENDER = [0, 1, 2]
36 |
37 | parser = argparse.ArgumentParser(description="HivisionIDPhotos 证件照制作推理程序。")
38 | parser.add_argument(
39 | "-t",
40 | "--type",
41 | help="请求 API 的种类",
42 | choices=INFERENCE_TYPE,
43 | default="idphoto",
44 | )
45 | parser.add_argument("-i", "--input_image_dir", help="输入图像路径", required=True)
46 | parser.add_argument("-o", "--output_image_dir", help="保存图像路径", required=True)
47 | parser.add_argument("--height", help="证件照尺寸-高", default=413)
48 | parser.add_argument("--width", help="证件照尺寸-宽", default=295)
49 | parser.add_argument("-c", "--color", help="证件照背景色", default="638cce")
50 | parser.add_argument("--hd", type=bool, help="是否输出高清照", default=True)
51 | parser.add_argument(
52 | "-k", "--kb", help="输出照片的 KB 值,仅对换底和制作排版照生效", default=None
53 | )
54 | parser.add_argument(
55 | "-r",
56 | "--render",
57 | type=int,
58 | help="底色合成的模式,有 0:纯色、1:上下渐变、2:中心渐变 可选",
59 | choices=RENDER,
60 | default=0,
61 | )
62 | parser.add_argument(
63 | "--dpi",
64 | type=int,
65 | help="输出照片的 DPI 值",
66 | default=300,
67 | )
68 | parser.add_argument(
69 | "--face_align",
70 | type=bool,
71 | help="是否进行人脸旋转矫正",
72 | default=False,
73 | )
74 | parser.add_argument(
75 | "--matting_model",
76 | help="抠图模型权重",
77 | default="modnet_photographic_portrait_matting",
78 | choices=MATTING_MODEL,
79 | )
80 | parser.add_argument(
81 | "--face_detect_model",
82 | help="人脸检测模型",
83 | default="mtcnn",
84 | choices=FACE_DETECT_MODEL,
85 | )
86 |
87 | args = parser.parse_args()
88 |
89 | # ------------------- 选择抠图与人脸检测模型 -------------------
90 | creator = IDCreator()
91 | choose_handler(creator, args.matting_model, args.face_detect_model)
92 |
93 | root_dir = os.path.dirname(os.path.abspath(__file__))
94 | input_image = cv2.imread(args.input_image_dir, cv2.IMREAD_UNCHANGED)
95 |
96 | # 如果模式是生成证件照
97 | if args.type == "idphoto":
98 | # 将字符串转为元组
99 | size = (int(args.height), int(args.width))
100 | try:
101 | result = creator(input_image, size=size, face_alignment=args.face_align)
102 | except FaceError:
103 | print("人脸数量不等于 1,请上传单张人脸的图像。")
104 | else:
105 | # 保存标准照
106 | save_image_dpi_to_bytes(cv2.cvtColor(result.standard, cv2.COLOR_RGBA2BGRA), args.output_image_dir, dpi=args.dpi)
107 |
108 | # 保存高清照
109 | file_name, file_extension = os.path.splitext(args.output_image_dir)
110 | new_file_name = file_name + "_hd" + file_extension
111 | save_image_dpi_to_bytes(cv2.cvtColor(result.hd, cv2.COLOR_RGBA2BGRA), new_file_name, dpi=args.dpi)
112 |
113 | # 如果模式是人像抠图
114 | elif args.type == "human_matting":
115 | result = creator(input_image, change_bg_only=True)
116 | cv2.imwrite(args.output_image_dir, result.hd)
117 |
118 | # 如果模式是添加背景
119 | elif args.type == "add_background":
120 |
121 | render_choice = ["pure_color", "updown_gradient", "center_gradient"]
122 |
123 | # 将字符串转为元组
124 | color = hex_to_rgb(args.color)
125 | # 将元祖的 0 和 2 号数字交换
126 | color = (color[2], color[1], color[0])
127 |
128 | result_image = add_background(
129 | input_image, bgr=color, mode=render_choice[args.render]
130 | )
131 | result_image = result_image.astype(np.uint8)
132 | result_image = cv2.cvtColor(result_image, cv2.COLOR_RGBA2BGRA)
133 |
134 | if args.kb:
135 | resize_image_to_kb(result_image, args.output_image_dir, int(args.kb), dpi=args.dpi)
136 | else:
137 | save_image_dpi_to_bytes(cv2.cvtColor(result_image, cv2.COLOR_RGBA2BGRA), args.output_image_dir, dpi=args.dpi)
138 |
139 | # 如果模式是生成排版照
140 | elif args.type == "generate_layout_photos":
141 |
142 | size = (int(args.height), int(args.width))
143 |
144 | typography_arr, typography_rotate = generate_layout_array(
145 | input_height=size[0], input_width=size[1]
146 | )
147 |
148 | result_layout_image = generate_layout_image(
149 | input_image,
150 | typography_arr,
151 | typography_rotate,
152 | height=size[0],
153 | width=size[1],
154 | )
155 |
156 | if args.kb:
157 | result_layout_image = cv2.cvtColor(result_layout_image, cv2.COLOR_RGB2BGR)
158 | result_layout_image = resize_image_to_kb(
159 | result_layout_image, args.output_image_dir, int(args.kb), dpi=args.dpi
160 | )
161 | else:
162 | save_image_dpi_to_bytes(cv2.cvtColor(result_layout_image, cv2.COLOR_RGBA2BGRA), args.output_image_dir, dpi=args.dpi)
163 |
164 | # 如果模式是证件照裁切
165 | elif args.type == "idphoto_crop":
166 | # 将字符串转为元组
167 | size = (int(args.height), int(args.width))
168 | try:
169 | result = creator(input_image, size=size, crop_only=True)
170 | except FaceError:
171 | print("人脸数量不等于 1,请上传单张人脸的图像。")
172 | else:
173 | # 保存标准照
174 | save_image_dpi_to_bytes(cv2.cvtColor(result.standard, cv2.COLOR_RGBA2BGRA), args.output_image_dir, dpi=args.dpi)
175 |
176 | # 保存高清照
177 | file_name, file_extension = os.path.splitext(args.output_image_dir)
178 | new_file_name = file_name + "_hd" + file_extension
179 | save_image_dpi_to_bytes(cv2.cvtColor(result.hd, cv2.COLOR_RGBA2BGRA), new_file_name, dpi=args.dpi)
--------------------------------------------------------------------------------
/requirements-app.txt:
--------------------------------------------------------------------------------
1 | gradio>=4.43.0
2 | fastapi
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | black
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | opencv-python>=4.8.1.78
2 | onnxruntime>=1.15.0
3 | numpy<=1.26.4
4 | requests
5 | mtcnn-runtime
6 | tqdm
7 | starlette
8 |
--------------------------------------------------------------------------------
/scripts/build_pypi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 16:56
5 | @File: build_pypi.py
6 | @IDE: pycharm
7 | @Description:
8 | 构建pypi包
9 | """
10 |
--------------------------------------------------------------------------------
/scripts/download_model.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import argparse
4 | from tqdm import tqdm # 导入 tqdm 库
5 |
6 | # 获取当前脚本所在目录的上一级目录
7 | base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8 |
9 |
10 | def download_file(url, save_path):
11 | try:
12 | print(f"Begin downloading: {url}")
13 | response = requests.get(url, stream=True)
14 | response.raise_for_status() # 检查请求是否成功
15 |
16 | # 获取文件总大小
17 | total_size = int(response.headers.get("content-length", 0))
18 | # 使用 tqdm 显示进度条
19 | with open(save_path, "wb") as file, tqdm(
20 | total=total_size,
21 | unit="B",
22 | unit_scale=True,
23 | unit_divisor=1024,
24 | desc=os.path.basename(save_path),
25 | ) as bar:
26 | for chunk in response.iter_content(chunk_size=8192):
27 | file.write(chunk)
28 | bar.update(len(chunk)) # 更新进度条
29 | print(f"Download completed. Save to: {save_path}")
30 | except requests.exceptions.RequestException as e:
31 | print(f"Download failed: {e}")
32 |
33 |
34 | def download_models(model_urls):
35 | # 下载每个模型
36 | for model_name, model_info in model_urls.items():
37 | # 指定下载保存的目录
38 | save_dir = model_info["location"]
39 |
40 | # 创建目录(如果不存在的话)
41 | os.makedirs(os.path.join(base_path, save_dir), exist_ok=True)
42 |
43 | url = model_info["url"]
44 | file_format = model_info["format"]
45 |
46 | # 特殊处理 rmbg-1.4 模型的文件名
47 | file_name = f"{model_name}.{file_format}"
48 |
49 | save_path = os.path.join(base_path, save_dir, file_name)
50 |
51 | # 检查文件是否已经存在
52 | if os.path.exists(save_path):
53 | print(f"File already exists, skipping download: {save_path}")
54 | continue
55 |
56 | # 下载文件
57 | download_file(url, save_path)
58 |
59 |
60 | def main(models_to_download):
61 | # 模型权重的下载链接
62 | model_urls = {
63 | "hivision_modnet": {
64 | "url": "https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/download/pretrained-model/hivision_modnet.onnx",
65 | "format": "onnx",
66 | "location": "hivision/creator/weights",
67 | },
68 | "modnet_photographic_portrait_matting": {
69 | "url": "https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/download/pretrained-model/modnet_photographic_portrait_matting.onnx",
70 | "format": "onnx",
71 | "location": "hivision/creator/weights",
72 | },
73 | # "mnn_hivision_modnet": {
74 | # "url": "https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/download/pretrained-model/mnn_hivision_modnet.mnn",
75 | # "format": "mnn",
76 | # },
77 | "rmbg-1.4": {
78 | "url": "https://huggingface.co/briaai/RMBG-1.4/resolve/main/onnx/model.onnx?download=true",
79 | "format": "onnx",
80 | "location": "hivision/creator/weights",
81 | },
82 | "birefnet-v1-lite": {
83 | "url": "https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet-general-bb_swin_v1_tiny-epoch_232.onnx",
84 | "format": "onnx",
85 | "location": "hivision/creator/weights",
86 | },
87 | "retinaface-resnet50": {
88 | "url": "https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/download/pretrained-model/retinaface-resnet50.onnx",
89 | "format": "onnx",
90 | "location": "hivision/creator/retinaface/weights",
91 | },
92 | }
93 |
94 | # 如果选择下载所有模型
95 | if "all" in models_to_download:
96 | selected_urls = model_urls
97 | else:
98 | selected_urls = {model: model_urls[model] for model in models_to_download}
99 |
100 | if not selected_urls:
101 | print("No valid models selected for download.")
102 | return
103 |
104 | download_models(selected_urls)
105 |
106 |
107 | if __name__ == "__main__":
108 | MODEL_CHOICES = [
109 | "hivision_modnet",
110 | "modnet_photographic_portrait_matting",
111 | # "mnn_hivision_modnet",
112 | "rmbg-1.4",
113 | "birefnet-lite",
114 | "all",
115 | ]
116 |
117 | parser = argparse.ArgumentParser(description="Download matting models.")
118 | parser.add_argument(
119 | "--models",
120 | nargs="+",
121 | required=True,
122 | choices=MODEL_CHOICES,
123 | help='Specify which models to download (options: hivision_modnet, modnet_photographic_portrait_matting, mnn_hivision_modnet, rmbg-1.4, all). Only "all" will download all models.',
124 | )
125 | args = parser.parse_args()
126 |
127 | models_to_download = args.models if args.models else ["all"]
128 | main(models_to_download)
129 |
--------------------------------------------------------------------------------
/test/create_id_photo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | r"""
4 | @DATE: 2024/9/5 21:39
5 | @File: create_id_photo.py
6 | @IDE: pycharm
7 | @Description:
8 | 用于测试创建证件照
9 | """
10 | from hivision.creator import IDCreator
11 | import cv2
12 | import os
13 |
14 | now_dir = os.path.dirname(__file__)
15 | image_path = os.path.join(os.path.dirname(now_dir), "app", "images", "test.jpg")
16 | output_dir = os.path.join(now_dir, "temp")
17 |
18 | image = cv2.imread(image_path)
19 | creator = IDCreator()
20 | result = creator(image)
21 | cv2.imwrite(os.path.join(output_dir, "result.png"), result.standard)
22 | cv2.imwrite(os.path.join(output_dir, "result_hd.png"), result.hd)
23 |
--------------------------------------------------------------------------------
/test/temp/.gitkeep:
--------------------------------------------------------------------------------
1 | 存放一些测试临时文件
--------------------------------------------------------------------------------