Spaces:
Running
Running
TheEeeeLin
commited on
Commit
•
ca46a75
1
Parent(s):
22af0df
refactor
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore copy +16 -0
- .vscode/extensions.json +20 -0
- .vscode/settings.json +71 -0
- Dockerfile +7 -6
- LICENSE +201 -0
- README.md +221 -44
- README_EN.md +0 -155
- app.py +107 -107
- app.spec +44 -0
- assets/hivisionai.ico +0 -0
- assets/title.md +11 -0
- beautyPlugin/GrindSkin.py +0 -43
- beautyPlugin/MakeBeautiful.py +0 -45
- beautyPlugin/MakeWhiter.py +0 -108
- beautyPlugin/ThinFace.py +0 -267
- beautyPlugin/__init__.py +0 -4
- beautyPlugin/lut_image/1.png +0 -0
- beautyPlugin/lut_image/3.png +0 -0
- beautyPlugin/lut_image/lutOrigin.png +0 -0
- {images → demo/images}/test.jpg +0 -0
- {images → demo/images}/test2.jpg +0 -0
- {images → demo/images}/test3.jpg +0 -0
- {images → demo/images}/test4.jpg +0 -0
- {output → demo/kb_output}/.gitkeep +0 -0
- size_list_CN.csv → demo/size_list_CN.csv +0 -0
- demo/size_list_EN.csv +19 -0
- data_utils.py → demo/utils.py +4 -3
- deploy_api.py +86 -67
- docker-compose.yml +11 -0
- docs/api_CN.md +552 -0
- docs/api_EN.md +551 -0
- hivision/__init__.py +4 -0
- hivision/creator/__init__.py +110 -0
- hivision/creator/context.py +104 -0
- hivision/creator/face_detector.py +42 -0
- hivision/creator/human_matting.py +116 -0
- src/layoutCreate.py → hivision/creator/layout_calculator.py +56 -31
- {src → hivision/creator}/move_image.py +13 -13
- hivision/creator/photo_adjuster.py +258 -0
- {hivisionai/hycv → hivision/creator}/tensor2numpy.py +14 -14
- hivision/creator/utils.py +142 -0
- hivisionai/__init__.py → hivision/creator/weights/.gitkeep +0 -0
- hivision_modnet.onnx → hivision/creator/weights/modnet_photographic_portrait_matting.onnx +2 -2
- hivision/error.py +21 -0
- hivision/utils.py +253 -0
- hivisionai/app.py +0 -452
- hivisionai/hyService/__init__.py +0 -0
- hivisionai/hyService/cloudService.py +0 -406
- hivisionai/hyService/dbTools.py +0 -337
- hivisionai/hyService/error.py +0 -20
.gitignore copy
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
*.pyc
|
2 |
+
**/__pycache__
|
3 |
+
.idea
|
4 |
+
.vscode/*
|
5 |
+
.DS_Store
|
6 |
+
app/output/*.jpg
|
7 |
+
demo/kb_output/*.jpg
|
8 |
+
# build outputs
|
9 |
+
dist
|
10 |
+
build
|
11 |
+
# checkpoint
|
12 |
+
*.pth
|
13 |
+
*.pt
|
14 |
+
*.onnx
|
15 |
+
test/temp/*
|
16 |
+
!test/temp/.gitkeep
|
.vscode/extensions.json
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|
.vscode/settings.json
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|
Dockerfile
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
FROM ubuntu:22.04
|
2 |
|
3 |
# apt换源,安装pip
|
4 |
-
RUN echo "==>
|
5 |
-
sed -i s@/archive.ubuntu.com/@/mirrors.
|
6 |
-
sed -i s@/security.ubuntu.com/@/mirrors.
|
7 |
apt-get clean && \
|
8 |
apt-get update
|
9 |
|
@@ -22,13 +22,14 @@ WORKDIR /app
|
|
22 |
|
23 |
COPY . .
|
24 |
|
25 |
-
RUN pip3 install -r requirements.txt
|
|
|
26 |
|
27 |
RUN echo "==> Clean up..." && \
|
28 |
rm -rf ~/.cache/pip
|
29 |
|
30 |
# 指定工作目录
|
31 |
|
32 |
-
EXPOSE
|
33 |
|
34 |
-
|
|
|
1 |
FROM ubuntu:22.04
|
2 |
|
3 |
# apt换源,安装pip
|
4 |
+
RUN echo "==> 换成清华源,并更新..." && \
|
5 |
+
sed -i s@/archive.ubuntu.com/@/mirrors.tuna.tsinghua.edu.cn/@g /etc/apt/sources.list && \
|
6 |
+
sed -i s@/security.ubuntu.com/@/mirrors.tuna.tsinghua.edu.cn/@g /etc/apt/sources.list && \
|
7 |
apt-get clean && \
|
8 |
apt-get update
|
9 |
|
|
|
22 |
|
23 |
COPY . .
|
24 |
|
25 |
+
RUN pip3 install -r requirements.txt && \
|
26 |
+
pip3 install -r requirements-app.txt
|
27 |
|
28 |
RUN echo "==> Clean up..." && \
|
29 |
rm -rf ~/.cache/pip
|
30 |
|
31 |
# 指定工作目录
|
32 |
|
33 |
+
EXPOSE 7860
|
34 |
|
35 |
+
CMD [ "python3", "app.py", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
README.md
CHANGED
@@ -4,47 +4,59 @@ emoji: 🌖
|
|
4 |
colorFrom: green
|
5 |
colorTo: purple
|
6 |
sdk: gradio
|
7 |
-
sdk_version:
|
8 |
app_file: app.py
|
9 |
pinned: true
|
10 |
---
|
11 |
|
|
|
12 |
<div align="center">
|
13 |
<h1>HivisionIDPhoto</h1>
|
14 |
|
15 |
-
[English](README_EN.md) / 中文
|
16 |
|
17 |
[![GitHub](https://img.shields.io/static/v1?label=GitHub&message=GitHub&color=black)](https://github.com/xiaolin199912/HivisionIDPhotos)
|
18 |
-
[![SwanHub Demo](https://
|
19 |
[![zhihu](https://img.shields.io/static/v1?label=知乎&message=知乎&color=blue)](https://zhuanlan.zhihu.com/p/638254028)
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
<img src="assets/demoImage.png" width=900>
|
|
|
22 |
</div>
|
23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
-
|
26 |
-
-
|
27 |
-
- 2023.12.1: 更新**API部署(基于fastapi)**
|
28 |
- 2023.6.20: 更新**预设尺寸菜单**
|
29 |
- 2023.6.19: 更新**排版照**
|
30 |
-
- 2023.6.13: 更新**中心渐变色**
|
31 |
-
- 2023.6.11: 更新**上下渐变色**
|
32 |
-
- 2023.6.8: 更新**自定义尺寸**
|
33 |
-
- 2023.6.4: 更新**自定义底色、人脸检测Bug通知**
|
34 |
-
- 2023.5.10: 更新**不改尺寸只换底**
|
35 |
|
36 |
# Overview
|
37 |
|
38 |
-
>
|
39 |
|
40 |
-
HivisionIDPhoto旨在开发一种实用的证件照智能制作算法。
|
41 |
|
42 |
它利用一套完善的模型工作流程,实现对多种用户拍照场景的识别、抠图与证件照生成。
|
43 |
|
|
|
44 |
|
45 |
-
**
|
46 |
-
|
47 |
-
1. 轻量级抠图
|
48 |
2. 根据不同尺寸规格生成不同的标准证件照、六寸排版照
|
49 |
3. 美颜(waiting)
|
50 |
4. 智能换正装(waiting)
|
@@ -53,16 +65,15 @@ HivisionIDPhoto旨在开发一种实用的证件照智能制作算法。
|
|
53 |
<img src="assets/gradio-image.jpeg" width=900>
|
54 |
</div>
|
55 |
|
56 |
-
|
57 |
---
|
58 |
|
59 |
-
如果HivisionIDPhoto对你有帮助,请star这个repo或推荐给你的朋友,解决证件照应急制作问题!
|
60 |
|
61 |
<br>
|
62 |
|
63 |
-
#
|
64 |
|
65 |
-
- Python >= 3.7(项目主要测试在python 3.10)
|
66 |
- onnxruntime
|
67 |
- OpenCV
|
68 |
- Option: Linux, Windows, MacOS
|
@@ -74,84 +85,250 @@ git clone https://github.com/Zeyi-Lin/HivisionIDPhotos.git
|
|
74 |
cd HivisionIDPhotos
|
75 |
```
|
76 |
|
77 |
-
**2.
|
|
|
|
|
78 |
|
79 |
```bash
|
80 |
pip install -r requirements.txt
|
|
|
81 |
```
|
82 |
|
83 |
**3. 下载权重文件**
|
84 |
|
85 |
-
在我们的[Release](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model)下载权重文件`hivision_modnet.onnx
|
86 |
|
87 |
<br>
|
88 |
|
89 |
-
# 运行Gradio Demo
|
90 |
|
91 |
```bash
|
92 |
python app.py
|
93 |
```
|
94 |
|
95 |
-
运行程序将生成一个本地Web页面,在页面中可完成证件照的操作与交互。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
|
97 |
<br>
|
98 |
|
99 |
-
# 部署API服务
|
|
|
|
|
100 |
|
101 |
```
|
102 |
python deploy_api.py
|
103 |
```
|
104 |
|
105 |
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
|
108 |
-
|
109 |
|
110 |
-
|
111 |
|
112 |
```bash
|
113 |
-
|
|
|
114 |
```
|
115 |
|
116 |
-
|
117 |
|
118 |
```bash
|
119 |
-
|
|
|
120 |
```
|
121 |
|
122 |
-
|
|
|
|
|
123 |
|
124 |
```bash
|
125 |
-
|
126 |
```
|
127 |
|
128 |
-
|
129 |
|
130 |
-
|
|
|
|
|
|
|
|
|
131 |
|
132 |
-
|
133 |
|
134 |
```bash
|
135 |
-
docker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
```
|
137 |
|
138 |
-
|
|
|
|
|
139 |
|
140 |
```bash
|
141 |
-
docker run -p 8080:8080 hivision_idphotos
|
142 |
```
|
143 |
|
144 |
<br>
|
145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
|
147 |
-
|
148 |
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
|
152 |
<br>
|
153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
|
155 |
-
#
|
156 |
|
157 |
-
|
|
|
4 |
colorFrom: green
|
5 |
colorTo: purple
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 4.43.0
|
8 |
app_file: app.py
|
9 |
pinned: true
|
10 |
---
|
11 |
|
12 |
+
|
13 |
<div align="center">
|
14 |
<h1>HivisionIDPhoto</h1>
|
15 |
|
16 |
+
[English](README_EN.md) / 中文 / [日本語](README_JP.md) / [한국어](README_KO.md)
|
17 |
|
18 |
[![GitHub](https://img.shields.io/static/v1?label=GitHub&message=GitHub&color=black)](https://github.com/xiaolin199912/HivisionIDPhotos)
|
19 |
+
[![SwanHub Demo](https://swanhub.co/git/repo/SwanHub%2FAuto-README/file/preview?ref=main&path=swanhub.svg)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)
|
20 |
[![zhihu](https://img.shields.io/static/v1?label=知乎&message=知乎&color=blue)](https://zhuanlan.zhihu.com/p/638254028)
|
21 |
+
[![Spaces](https://img.shields.io/badge/🤗-Open%20in%20Spaces-blue)](https://huggingface.co/spaces/TheEeeeLin/HivisionIDPhotos)
|
22 |
+
<a href="https://docs.qq.com/doc/DUkpBdk90eWZFS2JW" target="_blank">
|
23 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/WeChat-微信-4cb55e"></a>
|
24 |
+
|
25 |
+
<a href="https://trendshift.io/repositories/11622" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11622" alt="Zeyi-Lin%2FHivisionIDPhotos | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
26 |
|
27 |
<img src="assets/demoImage.png" width=900>
|
28 |
+
|
29 |
</div>
|
30 |
|
31 |
+
<br>
|
32 |
+
|
33 |
+
> **相关项目**:
|
34 |
+
>
|
35 |
+
> - [SwanLab](https://github.com/SwanHubX/SwanLab):训练人像抠图模型全程用它来分析和监控,以及和实验室同学协作交流,大幅提升了训练效率。
|
36 |
+
|
37 |
+
<br>
|
38 |
+
|
39 |
+
# 🤩 项目更新
|
40 |
+
|
41 |
+
- 在线体验: [![SwanHub Demo](https://img.shields.io/static/v1?label=Demo&message=SwanHub%20Demo&color=blue)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)、[![Spaces](https://img.shields.io/badge/🤗-Open%20in%20Spaces-blue)](https://huggingface.co/spaces/TheEeeeLin/HivisionIDPhotos)
|
42 |
|
43 |
+
- 2024.9.5: 更新 [Restful API 文档](docs/api_CN.md)
|
44 |
+
- 2024.9.2: 更新**调整照片 KB 大小**,[DockerHub](https://hub.docker.com/r/linzeyi/hivision_idphotos/tags)
|
45 |
+
- 2023.12.1: 更新**API 部署(基于 fastapi)**
|
46 |
- 2023.6.20: 更新**预设尺寸菜单**
|
47 |
- 2023.6.19: 更新**排版照**
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
# Overview
|
50 |
|
51 |
+
> 🚀 谢谢你对我们的工作感兴趣。您可能还想查看我们在图像领域的其他成果,欢迎来信:zeyi.lin@swanhub.co.
|
52 |
|
53 |
+
HivisionIDPhoto 旨在开发一种实用的证件照智能制作算法。
|
54 |
|
55 |
它利用一套完善的模型工作流程,实现对多种用户拍照场景的识别、抠图与证件照生成。
|
56 |
|
57 |
+
**HivisionIDPhoto 可以做到:**
|
58 |
|
59 |
+
1. 轻量级抠图(仅需 **CPU** 即可快速推理)
|
|
|
|
|
60 |
2. 根据不同尺寸规格生成不同的标准证件照、六寸排版照
|
61 |
3. 美颜(waiting)
|
62 |
4. 智能换正装(waiting)
|
|
|
65 |
<img src="assets/gradio-image.jpeg" width=900>
|
66 |
</div>
|
67 |
|
|
|
68 |
---
|
69 |
|
70 |
+
如果 HivisionIDPhoto 对你有帮助,请 star 这个 repo 或推荐给你的朋友,解决证件照应急制作问题!
|
71 |
|
72 |
<br>
|
73 |
|
74 |
+
# 🔧 环境安装与依赖
|
75 |
|
76 |
+
- Python >= 3.7(项目主要测试在 python 3.10)
|
77 |
- onnxruntime
|
78 |
- OpenCV
|
79 |
- Option: Linux, Windows, MacOS
|
|
|
85 |
cd HivisionIDPhotos
|
86 |
```
|
87 |
|
88 |
+
**2. (重要)安装依赖环境**
|
89 |
+
|
90 |
+
> 建议 conda 创建一个 python3.10 虚拟环境后,执行以下命令
|
91 |
|
92 |
```bash
|
93 |
pip install -r requirements.txt
|
94 |
+
pip install -r requirements-app.txt
|
95 |
```
|
96 |
|
97 |
**3. 下载权重文件**
|
98 |
|
99 |
+
在我们的[Release](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model)下载权重文件`hivision_modnet.onnx` (24.7MB),存到项目的`hivision/creator/weights`目录下。
|
100 |
|
101 |
<br>
|
102 |
|
103 |
+
# 🚀 运行 Gradio Demo
|
104 |
|
105 |
```bash
|
106 |
python app.py
|
107 |
```
|
108 |
|
109 |
+
运行程序将生成一个本地 Web 页面,在页面中可完成证件照的操作与交互。
|
110 |
+
|
111 |
+
<br>
|
112 |
+
|
113 |
+
# 🚀 Python 推理
|
114 |
+
|
115 |
+
## 1. 证件照制作
|
116 |
+
|
117 |
+
输入 1 张照片,获得 1 张标准证件照和 1 张高清证件照的 4 通道透明 png
|
118 |
+
|
119 |
+
```python
|
120 |
+
python inference.py -i demo/images/test.jpg -o ./idphoto.png --height 413 --width 295
|
121 |
+
```
|
122 |
+
|
123 |
+
## 2. 增加底色
|
124 |
+
|
125 |
+
输入 1 张 4 通道透明 png,获得 1 张增加了底色的图像)
|
126 |
+
|
127 |
+
```python
|
128 |
+
python inference.py -t add_background -i ./idphoto.png -o ./idhoto_ab.jpg -c 000000 -k 30
|
129 |
+
```
|
130 |
+
|
131 |
+
## 3. 得到六寸排版照
|
132 |
+
|
133 |
+
输入 1 张 3 通道照片,获得 1 张六寸排版照
|
134 |
+
|
135 |
+
```python
|
136 |
+
python inference.py -t generate_layout_photos -i ./idhoto_ab.jpg -o ./idhoto_layout.jpg --height 413 --width 295 -k 200
|
137 |
+
```
|
138 |
|
139 |
<br>
|
140 |
|
141 |
+
# ⚡️ 部署 API 服务
|
142 |
+
|
143 |
+
## 启动后端
|
144 |
|
145 |
```
|
146 |
python deploy_api.py
|
147 |
```
|
148 |
|
149 |
|
150 |
+
## 请求 API 服务 - Python Request
|
151 |
+
|
152 |
+
> 请求方式请参考 [API 文档](docs/api_CN.md),含 [cURL](docs/api_CN.md#curl-请求示例)、[Python](docs/api_CN.md#python-请求示例)、[Java](docs/api_CN.md#java-请求示例)、[Javascript](docs/api_CN.md#javascript-请求示例) 请求示例。
|
153 |
+
|
154 |
+
### 1. 证件照制作
|
155 |
+
|
156 |
+
输入 1 张照片,获得 1 张标准证件照和 1 张高清证件照的 4 通道透明 png
|
157 |
+
|
158 |
+
```python
|
159 |
+
import requests
|
160 |
+
|
161 |
+
url = "http://127.0.0.1:8080/idphoto"
|
162 |
+
input_image_path = "demo/images/test.jpg"
|
163 |
+
|
164 |
+
files = {"input_image": open(input_image_path, "rb")}
|
165 |
+
data = {"height": 413, "width": 295}
|
166 |
+
|
167 |
+
response = requests.post(url, files=files, data=data).json()
|
168 |
+
|
169 |
+
# response为一个json格式字典,包含status、image_base64_standard和image_base64_hd三项
|
170 |
+
print(response)
|
171 |
+
|
172 |
+
```
|
173 |
+
|
174 |
+
### 2. 增加底色
|
175 |
+
|
176 |
+
输入 1 张 4 通道透明 png,获得 1 张增加了底色的图像
|
177 |
+
|
178 |
+
```python
|
179 |
+
import requests
|
180 |
+
|
181 |
+
url = "http://127.0.0.1:8080/add_background"
|
182 |
+
input_image_path = "test.png"
|
183 |
+
|
184 |
+
files = {"input_image": open(input_image_path, "rb")}
|
185 |
+
data = {"color": '638cce', 'kb': None}
|
186 |
+
|
187 |
+
response = requests.post(url, files=files, data=data).json()
|
188 |
+
|
189 |
+
# response为一个json格式字典,包含status和image_base64
|
190 |
+
print(response)
|
191 |
+
```
|
192 |
+
|
193 |
+
### 3. 得到六寸排版照
|
194 |
+
|
195 |
+
输入 1 张 3 通道照片,获得 1 张六寸排版照
|
196 |
+
|
197 |
+
```python
|
198 |
+
import requests
|
199 |
+
|
200 |
+
url = "http://127.0.0.1:8080/generate_layout_photos"
|
201 |
+
input_image_path = "test.jpg"
|
202 |
+
|
203 |
+
files = {"input_image": open(input_image_path, "rb")}
|
204 |
+
data = {"height": 413, "width": 295, "kb": 200}
|
205 |
+
|
206 |
+
response = requests.post(url, files=files, data=data).json()
|
207 |
+
|
208 |
+
# response为一个json格式字典,包含status和image_base64
|
209 |
+
print(response)
|
210 |
+
```
|
211 |
+
|
212 |
+
<br>
|
213 |
+
|
214 |
+
# 🐳 Docker 部署
|
215 |
+
|
216 |
+
## 1. 拉取或构建镜像
|
217 |
|
218 |
+
> 以下方式三选一
|
219 |
|
220 |
+
**方式一:拉取镜像:**
|
221 |
|
222 |
```bash
|
223 |
+
docker pull linzeyi/hivision_idphotos:v1
|
224 |
+
docker tag linzeyi/hivision_idphotos:v1 hivision_idphotos
|
225 |
```
|
226 |
|
227 |
+
国内拉取加速:
|
228 |
|
229 |
```bash
|
230 |
+
docker pull registry.cn-hangzhou.aliyuncs.com/swanhub/hivision_idphotos:v1
|
231 |
+
docker tag registry.cn-hangzhou.aliyuncs.com/swanhub/hivision_idphotos:v1 hivision_idphotos
|
232 |
```
|
233 |
|
234 |
+
**方式二:Dockrfile 直接构建镜像:**
|
235 |
+
|
236 |
+
在确保将模型权重文件[hivision_modnet.onnx](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model)放到`hivision/creator/weights`下后,在项目根目录执行:
|
237 |
|
238 |
```bash
|
239 |
+
docker build -t hivision_idphotos .
|
240 |
```
|
241 |
|
242 |
+
**方式三:Docker compose 构建:**
|
243 |
|
244 |
+
确保将模型权重文件 [hivision_modnet.onnx](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model) 放在`hivision/creator/weights`下后,在项目根目录下执行:
|
245 |
+
|
246 |
+
```bash
|
247 |
+
docker compose build
|
248 |
+
```
|
249 |
|
250 |
+
镜像打包完成后,运行以下命令启动 Gradio 服务:
|
251 |
|
252 |
```bash
|
253 |
+
docker compose up -d
|
254 |
+
```
|
255 |
+
|
256 |
+
## 2. 运行 Gradio Demo
|
257 |
+
|
258 |
+
等待镜像封装完毕后,运行以下指令,即可开启 Gradio Demo 服务:
|
259 |
+
|
260 |
+
```bash
|
261 |
+
docker run -p 7860:7860 hivision_idphotos
|
262 |
```
|
263 |
|
264 |
+
在你的本地访问 [http://127.0.0.1:7860](http://127.0.0.1:7860/) 即可使用。
|
265 |
+
|
266 |
+
## 3. 运行 API 后端服务
|
267 |
|
268 |
```bash
|
269 |
+
docker run -p 8080:8080 hivision_idphotos python3 deploy_api.py
|
270 |
```
|
271 |
|
272 |
<br>
|
273 |
|
274 |
+
# 🌲 友情链接
|
275 |
+
|
276 |
+
- [HivisionIDPhotos-windows-GUI](https://github.com/zhaoyun0071/HivisionIDPhotos-windows-GUI)
|
277 |
+
|
278 |
+
<br>
|
279 |
+
|
280 |
+
# 📖 引用项目
|
281 |
|
282 |
+
1. MTCNN:
|
283 |
|
284 |
+
```bibtex
|
285 |
+
@software{ipazc_mtcnn_2021,
|
286 |
+
author = {ipazc},
|
287 |
+
title = {{MTCNN}},
|
288 |
+
url = {https://github.com/ipazc/mtcnn},
|
289 |
+
year = {2021},
|
290 |
+
publisher = {GitHub}
|
291 |
+
}
|
292 |
+
```
|
293 |
+
|
294 |
+
2. ModNet:
|
295 |
+
|
296 |
+
```bibtex
|
297 |
+
@software{zhkkke_modnet_2021,
|
298 |
+
author = {ZHKKKe},
|
299 |
+
title = {{ModNet}},
|
300 |
+
url = {https://github.com/ZHKKKe/MODNet},
|
301 |
+
year = {2021},
|
302 |
+
publisher = {GitHub}
|
303 |
+
}
|
304 |
+
```
|
305 |
|
306 |
<br>
|
307 |
|
308 |
+
# 💻 开发小贴士
|
309 |
+
|
310 |
+
**1. 如何修改预设尺寸?**
|
311 |
+
|
312 |
+
修改[size_list_CN.csv](demo/size_list_CN.csv)后再次运行 app.py 即可,其中第一列为尺寸名,第二列为高度,第三列为宽度。
|
313 |
+
|
314 |
+
<br>
|
315 |
+
|
316 |
+
# 📧 联系我们
|
317 |
+
|
318 |
+
如果您有任何问题,请发邮件至 zeyi.lin@swanhub.co
|
319 |
+
|
320 |
+
<br>
|
321 |
+
|
322 |
+
# 贡献者
|
323 |
+
|
324 |
+
<a href="https://github.com/Zeyi-Lin/HivisionIDPhotos/graphs/contributors">
|
325 |
+
<img src="https://contrib.rocks/image?repo=Zeyi-Lin/HivisionIDPhotos" />
|
326 |
+
</a>
|
327 |
+
|
328 |
+
[Zeyi-Lin](https://github.com/Zeyi-Lin)、[SAKURA-CAT](https://github.com/SAKURA-CAT)、[Feudalman](https://github.com/Feudalman)、[swpfY](https://github.com/swpfY)、[Kaikaikaifang](https://github.com/Kaikaikaifang)、[ShaohonChen](https://github.com/ShaohonChen)、[KashiwaByte](https://github.com/KashiwaByte)
|
329 |
+
|
330 |
+
<br>
|
331 |
|
332 |
+
# StarHistory
|
333 |
|
334 |
+
[![Star History Chart](https://api.star-history.com/svg?repos=Zeyi-Lin/HivisionIDPhotos&type=Date)](https://star-history.com/#Zeyi-Lin/HivisionIDPhotos&Date)
|
README_EN.md
DELETED
@@ -1,155 +0,0 @@
|
|
1 |
-
<div align="center">
|
2 |
-
<h1>HivisionIDPhoto</h1>
|
3 |
-
|
4 |
-
|
5 |
-
English / [中文](README.md)
|
6 |
-
|
7 |
-
[![GitHub](https://img.shields.io/static/v1?label=Github&message=GitHub&color=black)](https://github.com/xiaolin199912/HivisionIDPhotos)
|
8 |
-
[![SwanHub Demo](https://swanhub.co/git/repo/SwanHub%2FAuto-README/file/preview?ref=main&path=swanhub.svg)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)
|
9 |
-
[![zhihu](https://img.shields.io/static/v1?label=知乎&message=zhihu&color=blue)](https://zhuanlan.zhihu.com/p/638254028)
|
10 |
-
|
11 |
-
|
12 |
-
<img src="assets/demoImage.png" width=900>
|
13 |
-
</div>
|
14 |
-
|
15 |
-
</div>
|
16 |
-
|
17 |
-
|
18 |
-
# 🤩Project Update
|
19 |
-
|
20 |
-
- Online Demo: [![SwanHub Demo](https://swanhub.co/git/repo/SwanHub%2FAuto-README/file/preview?ref=main&path=swanhub.svg)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)
|
21 |
-
- 2023.12.1: Update **API deployment (based on fastapi)**
|
22 |
-
- 2023.6.20: Update **Preset size menu**
|
23 |
-
- 2023.6.19: Update **Layout photos**
|
24 |
-
- 2023.6.13: Update **Center gradient color**
|
25 |
-
- 2023.6.11: Update **Top and bottom gradient color**
|
26 |
-
- 2023.6.8: Update **Custom size**
|
27 |
-
- 2023.6.4: Update **Custom background color, face detection bug notification**
|
28 |
-
- 2023.5.10: Update **Change the background without changing the size**
|
29 |
-
|
30 |
-
|
31 |
-
<br>
|
32 |
-
|
33 |
-
# Overview
|
34 |
-
|
35 |
-
> 🚀Thank you for your interest in our work. You may also want to check out our other achievements in the field of image processing. Please feel free to contact us at zeyi.lin@swanhub.co.
|
36 |
-
|
37 |
-
HivisionIDPhoto aims to develop a practical intelligent algorithm for producing ID photos. It uses a complete set of model workflows to recognize various user photo scenarios, perform image segmentation, and generate ID photos.
|
38 |
-
|
39 |
-
**HivisionIDPhoto can:**
|
40 |
-
|
41 |
-
1. Perform lightweight image segmentation
|
42 |
-
2. Generate standard ID photos and six-inch layout photos according to different size specifications
|
43 |
-
3. Provide beauty features (waiting)
|
44 |
-
4. Provide intelligent formal wear replacement (waiting)
|
45 |
-
|
46 |
-
<div align="center">
|
47 |
-
<img src="assets/gradio-image.jpeg" width=900>
|
48 |
-
</div>
|
49 |
-
|
50 |
-
|
51 |
-
---
|
52 |
-
|
53 |
-
If HivisionIDPhoto is helpful to you, please star this repo or recommend it to your friends to solve the problem of emergency ID photo production!
|
54 |
-
|
55 |
-
<br>
|
56 |
-
|
57 |
-
# 🔧Environment Dependencies and Installation
|
58 |
-
|
59 |
-
- Python >= 3.7(The main test of the project is in Python 3.10.)
|
60 |
-
- onnxruntime
|
61 |
-
- OpenCV
|
62 |
-
- Option: Linux, Windows, MacOS
|
63 |
-
|
64 |
-
### Installation
|
65 |
-
|
66 |
-
1. Clone repo
|
67 |
-
|
68 |
-
```bash
|
69 |
-
git clone https://github.com/Zeyi-Lin/HivisionIDPhotos.git
|
70 |
-
cd HivisionIDPhotos
|
71 |
-
```
|
72 |
-
|
73 |
-
2. Install dependent packages
|
74 |
-
|
75 |
-
```
|
76 |
-
pip install -r requirements.txt
|
77 |
-
```
|
78 |
-
|
79 |
-
**3. Download Pretrain file**
|
80 |
-
|
81 |
-
Download the weight file `hivision_modnet.onnx` from our [Release](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model) and save it to the root directory.
|
82 |
-
|
83 |
-
<br>
|
84 |
-
|
85 |
-
# Gradio Demo
|
86 |
-
|
87 |
-
```bash
|
88 |
-
python app.py
|
89 |
-
```
|
90 |
-
|
91 |
-
Running the program will generate a local web page, where operations and interactions with ID photos can be completed.
|
92 |
-
|
93 |
-
<br>
|
94 |
-
|
95 |
-
# Deploy API service
|
96 |
-
|
97 |
-
```
|
98 |
-
python deploy_api.py
|
99 |
-
```
|
100 |
-
|
101 |
-
**Request API service (Python)**
|
102 |
-
|
103 |
-
Use Python to send a request to the service:
|
104 |
-
|
105 |
-
ID photo production (input 1 photo, get 1 standard ID photo and 1 high-definition ID photo 4-channel transparent png):
|
106 |
-
|
107 |
-
```bash
|
108 |
-
python requests_api.py -u http://127.0.0.1:8080 -i test.jpg -o ./idphoto.png -s '(413,295)'
|
109 |
-
```
|
110 |
-
|
111 |
-
Add background color (input 1 4-channel transparent png, get 1 image with added background color):
|
112 |
-
|
113 |
-
```bash
|
114 |
-
python requests_api.py -u http://127.0.0.1:8080 -t add_background -i ./idphoto.png -o ./idhoto_ab.jpg -c '(0,0,0)'
|
115 |
-
```
|
116 |
-
|
117 |
-
Get a six-inch layout photo (input a 3-channel photo, get a six-inch layout photo):
|
118 |
-
|
119 |
-
```bash
|
120 |
-
python requests_api.py -u http://127.0.0.1:8080 -t generate_layout_photos -i ./idhoto_ab.jpg -o ./idhoto_layout.jpg -s '(413,295)'
|
121 |
-
```
|
122 |
-
|
123 |
-
<br>
|
124 |
-
|
125 |
-
# 🐳Docker deployment
|
126 |
-
|
127 |
-
After ensuring that the model weight file [hivision_modnet.onnx](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model) is placed in the root directory, execute in the root directory:
|
128 |
-
|
129 |
-
```bash
|
130 |
-
docker build -t hivision_idphotos .
|
131 |
-
```
|
132 |
-
|
133 |
-
After the image is packaged, run the following command to start the API service:
|
134 |
-
|
135 |
-
```bash
|
136 |
-
docker run -p 8080:8080 hivision_idphotos
|
137 |
-
```
|
138 |
-
|
139 |
-
<br>
|
140 |
-
|
141 |
-
# Reference Projects
|
142 |
-
|
143 |
-
1. MTCNN: https://github.com/ipazc/mtcnn
|
144 |
-
2. ModNet: https://github.com/ZHKKKe/MODNet
|
145 |
-
|
146 |
-
|
147 |
-
<br>
|
148 |
-
|
149 |
-
# 📧Contact
|
150 |
-
|
151 |
-
If you have any questions, please email Zeyi.lin@swanhub.co
|
152 |
-
|
153 |
-
|
154 |
-
Copyright © 2023, ZeYiLin. All Rights Reserved.
|
155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -1,20 +1,21 @@
|
|
1 |
import os
|
2 |
import gradio as gr
|
3 |
-
import
|
4 |
-
from
|
5 |
-
from
|
6 |
-
from
|
|
|
|
|
|
|
7 |
import pathlib
|
8 |
import numpy as np
|
9 |
-
from
|
10 |
-
from data_utils import csv_to_size_list
|
11 |
import argparse
|
12 |
|
13 |
-
|
14 |
# 获取尺寸列表
|
15 |
root_dir = os.path.dirname(os.path.abspath(__file__))
|
16 |
-
size_list_dict_CN = csv_to_size_list(os.path.join(root_dir, "size_list_CN.csv"))
|
17 |
-
size_list_dict_EN = csv_to_size_list(os.path.join(root_dir, "size_list_EN.csv"))
|
18 |
|
19 |
color_list_dict_CN = {
|
20 |
"蓝色": (86, 140, 212),
|
@@ -29,11 +30,6 @@ color_list_dict_EN = {
|
|
29 |
}
|
30 |
|
31 |
|
32 |
-
# 设置 Gradio examples
|
33 |
-
def set_example_image(example: list) -> dict:
|
34 |
-
return gr.Image.update(value=example[0])
|
35 |
-
|
36 |
-
|
37 |
# 检测 RGB 是否超出范围,如果超出则约束到 0~255 之间
|
38 |
def range_check(value, min_value=0, max_value=255):
|
39 |
value = int(value)
|
@@ -149,35 +145,19 @@ def idphoto_inference(
|
|
149 |
else:
|
150 |
idphoto_json["custom_image_kb"] = None
|
151 |
|
|
|
|
|
152 |
# 生成证件照
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
) = IDphotos_create(
|
164 |
-
input_image,
|
165 |
-
mode=idphoto_json["size_mode"],
|
166 |
-
size=idphoto_json["size"],
|
167 |
-
head_measure_ratio=head_measure_ratio,
|
168 |
-
head_height_ratio=head_height_ratio,
|
169 |
-
align=False,
|
170 |
-
beauty=False,
|
171 |
-
fd68=None,
|
172 |
-
human_sess=sess,
|
173 |
-
IS_DEBUG=False,
|
174 |
-
top_distance_max=top_distance_max,
|
175 |
-
top_distance_min=top_distance_min,
|
176 |
-
)
|
177 |
-
|
178 |
-
# 如果检测到人脸数量不等于 1
|
179 |
-
if status == 0:
|
180 |
-
result_messgae = {
|
181 |
img_output_standard: gr.update(value=None),
|
182 |
img_output_standard_hd: gr.update(value=None),
|
183 |
notification: gr.update(
|
@@ -185,9 +165,8 @@ def idphoto_inference(
|
|
185 |
visible=True,
|
186 |
),
|
187 |
}
|
188 |
-
|
189 |
-
# 如果检测到人脸数量等于 1
|
190 |
else:
|
|
|
191 |
if idphoto_json["render_mode"] == text_lang_map[language]["Solid Color"]:
|
192 |
result_image_standard = np.uint8(
|
193 |
add_background(result_image_standard, bgr=idphoto_json["color_bgr"])
|
@@ -240,12 +219,15 @@ def idphoto_inference(
|
|
240 |
input_width=idphoto_json["size"][1],
|
241 |
)
|
242 |
|
243 |
-
result_layout_image =
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
|
|
|
|
|
|
249 |
)
|
250 |
|
251 |
# 如果输出 KB 大小选择的是自定义
|
@@ -255,7 +237,7 @@ def idphoto_inference(
|
|
255 |
# 输出路径为一个根据时间戳 + 哈希值生成的随机文件名
|
256 |
import time
|
257 |
|
258 |
-
output_image_path = f"
|
259 |
resize_image_to_kb(
|
260 |
result_image_standard,
|
261 |
output_image_path,
|
@@ -265,7 +247,7 @@ def idphoto_inference(
|
|
265 |
output_image_path = None
|
266 |
|
267 |
if output_image_path:
|
268 |
-
|
269 |
img_output_standard: result_image_standard,
|
270 |
img_output_standard_hd: result_image_hd,
|
271 |
img_output_layout: result_layout_image,
|
@@ -273,7 +255,7 @@ def idphoto_inference(
|
|
273 |
file_download: gr.update(visible=True, value=output_image_path),
|
274 |
}
|
275 |
else:
|
276 |
-
|
277 |
img_output_standard: result_image_standard,
|
278 |
img_output_standard_hd: result_image_hd,
|
279 |
img_output_layout: result_layout_image,
|
@@ -281,14 +263,10 @@ def idphoto_inference(
|
|
281 |
file_download: gr.update(visible=False),
|
282 |
}
|
283 |
|
284 |
-
return
|
285 |
|
286 |
|
287 |
if __name__ == "__main__":
|
288 |
-
# 预加载 ONNX 模型
|
289 |
-
HY_HUMAN_MATTING_WEIGHTS_PATH = os.path.join(root_dir, "hivision_modnet.onnx")
|
290 |
-
sess = onnxruntime.InferenceSession(HY_HUMAN_MATTING_WEIGHTS_PATH)
|
291 |
-
|
292 |
language = ["中文", "English"]
|
293 |
size_mode_CN = ["尺寸列表", "只换底", "自定义尺寸"]
|
294 |
size_mode_EN = ["Size List", "Only Change Background", "Custom Size"]
|
@@ -305,40 +283,66 @@ if __name__ == "__main__":
|
|
305 |
image_kb_CN = ["不设置", "自定义"]
|
306 |
image_kb_EN = ["Not Set", "Custom"]
|
307 |
|
308 |
-
title = "<h1 id='title'>HivisionIDPhotos</h1>"
|
309 |
-
description = "<h3>😎9.2 Update: Add photo size KB adjustment</h3>"
|
|
|
|
|
|
|
|
|
|
|
|
|
310 |
css = """
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
|
316 |
demo = gr.Blocks(css=css)
|
317 |
|
318 |
with demo:
|
319 |
-
gr.
|
320 |
-
gr.Markdown(description)
|
321 |
with gr.Row():
|
322 |
# ------------ 左半边 UI ----------------
|
323 |
with gr.Column():
|
324 |
-
img_input = gr.Image(
|
325 |
language_options = gr.Dropdown(
|
326 |
-
choices=language, label="Language", value="
|
327 |
)
|
328 |
|
329 |
mode_options = gr.Radio(
|
330 |
-
choices=
|
331 |
-
label="
|
332 |
-
value="
|
333 |
elem_id="size",
|
334 |
)
|
335 |
|
336 |
# 预设尺寸下拉菜单
|
337 |
with gr.Row(visible=True) as size_list_row:
|
338 |
size_list_options = gr.Dropdown(
|
339 |
-
choices=
|
340 |
-
label="
|
341 |
-
value=
|
342 |
elem_id="size_list",
|
343 |
)
|
344 |
|
@@ -352,7 +356,7 @@ if __name__ == "__main__":
|
|
352 |
|
353 |
# 左:背景色选项
|
354 |
color_options = gr.Radio(
|
355 |
-
choices=
|
356 |
)
|
357 |
|
358 |
# 左:如果选择「自定义底色」,显示 RGB 输入框
|
@@ -363,17 +367,17 @@ if __name__ == "__main__":
|
|
363 |
|
364 |
# 左:渲染方式选项
|
365 |
render_options = gr.Radio(
|
366 |
-
choices=
|
367 |
-
label="
|
368 |
-
value="
|
369 |
elem_id="render",
|
370 |
)
|
371 |
|
372 |
# 左:输出 KB 大小选项
|
373 |
image_kb_options = gr.Radio(
|
374 |
-
choices=
|
375 |
-
label="
|
376 |
-
value="
|
377 |
elem_id="image_kb",
|
378 |
)
|
379 |
|
@@ -383,19 +387,19 @@ if __name__ == "__main__":
|
|
383 |
minimum=10,
|
384 |
maximum=1000,
|
385 |
value=50,
|
386 |
-
label="KB
|
387 |
interactive=True,
|
388 |
)
|
389 |
|
390 |
-
img_but = gr.Button("
|
391 |
|
392 |
# 案例图片
|
393 |
-
example_images = gr.
|
394 |
-
|
395 |
-
|
396 |
[path.as_posix()]
|
397 |
for path in sorted(
|
398 |
-
pathlib.Path(os.path.join(root_dir, "images")).rglob(
|
399 |
"*.jpg"
|
400 |
)
|
401 |
)
|
@@ -404,12 +408,12 @@ if __name__ == "__main__":
|
|
404 |
|
405 |
# ---------------- 右半边 UI ----------------
|
406 |
with gr.Column():
|
407 |
-
notification = gr.Text(label="
|
408 |
with gr.Row():
|
409 |
-
img_output_standard = gr.Image(label="
|
410 |
-
img_output_standard_hd = gr.Image(label="
|
411 |
-
img_output_layout = gr.Image(label="
|
412 |
-
file_download = gr.File(label="
|
413 |
|
414 |
# ---------------- 设置隐藏/显示组件 ----------------
|
415 |
def change_language(language):
|
@@ -419,7 +423,7 @@ if __name__ == "__main__":
|
|
419 |
size_list_options: gr.update(
|
420 |
label="预设尺寸",
|
421 |
choices=size_list_CN,
|
422 |
-
value=
|
423 |
),
|
424 |
mode_options: gr.update(
|
425 |
label="证件照尺寸选项",
|
@@ -454,7 +458,7 @@ if __name__ == "__main__":
|
|
454 |
size_list_options: gr.update(
|
455 |
label="Default size",
|
456 |
choices=size_list_EN,
|
457 |
-
value=
|
458 |
),
|
459 |
mode_options: gr.update(
|
460 |
label="ID photo size options",
|
@@ -582,17 +586,13 @@ if __name__ == "__main__":
|
|
582 |
],
|
583 |
)
|
584 |
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
# argparser.add_argument(
|
594 |
-
# "--host", type=str, default="127.0.0.1", help="The host of the server"
|
595 |
-
# )
|
596 |
-
# args = argparser.parse_args()
|
597 |
|
598 |
-
demo.launch()
|
|
|
1 |
import os
|
2 |
import gradio as gr
|
3 |
+
from hivision import IDCreator
|
4 |
+
from hivision.error import FaceError
|
5 |
+
from hivision.utils import add_background, resize_image_to_kb
|
6 |
+
from hivision.creator.layout_calculator import (
|
7 |
+
generate_layout_photo,
|
8 |
+
generate_layout_image,
|
9 |
+
)
|
10 |
import pathlib
|
11 |
import numpy as np
|
12 |
+
from demo.utils import csv_to_size_list
|
|
|
13 |
import argparse
|
14 |
|
|
|
15 |
# 获取尺寸列表
|
16 |
root_dir = os.path.dirname(os.path.abspath(__file__))
|
17 |
+
size_list_dict_CN = csv_to_size_list(os.path.join(root_dir, "demo/size_list_CN.csv"))
|
18 |
+
size_list_dict_EN = csv_to_size_list(os.path.join(root_dir, "demo/size_list_EN.csv"))
|
19 |
|
20 |
color_list_dict_CN = {
|
21 |
"蓝色": (86, 140, 212),
|
|
|
30 |
}
|
31 |
|
32 |
|
|
|
|
|
|
|
|
|
|
|
33 |
# 检测 RGB 是否超出范围,如果超出则约束到 0~255 之间
|
34 |
def range_check(value, min_value=0, max_value=255):
|
35 |
value = int(value)
|
|
|
145 |
else:
|
146 |
idphoto_json["custom_image_kb"] = None
|
147 |
|
148 |
+
creator = IDCreator()
|
149 |
+
change_bg_only = idphoto_json["size_mode"] in ["只换底", "Only Change Background"]
|
150 |
# 生成证件照
|
151 |
+
try:
|
152 |
+
result = creator(
|
153 |
+
input_image,
|
154 |
+
change_bg_only=change_bg_only,
|
155 |
+
size=idphoto_json["size"],
|
156 |
+
head_measure_ratio=head_measure_ratio,
|
157 |
+
head_height_ratio=head_height_ratio,
|
158 |
+
)
|
159 |
+
except FaceError:
|
160 |
+
result_message = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
img_output_standard: gr.update(value=None),
|
162 |
img_output_standard_hd: gr.update(value=None),
|
163 |
notification: gr.update(
|
|
|
165 |
visible=True,
|
166 |
),
|
167 |
}
|
|
|
|
|
168 |
else:
|
169 |
+
(result_image_hd, result_image_standard, _, _) = result
|
170 |
if idphoto_json["render_mode"] == text_lang_map[language]["Solid Color"]:
|
171 |
result_image_standard = np.uint8(
|
172 |
add_background(result_image_standard, bgr=idphoto_json["color_bgr"])
|
|
|
219 |
input_width=idphoto_json["size"][1],
|
220 |
)
|
221 |
|
222 |
+
result_layout_image = gr.update(
|
223 |
+
value=generate_layout_image(
|
224 |
+
result_image_standard,
|
225 |
+
typography_arr,
|
226 |
+
typography_rotate,
|
227 |
+
height=idphoto_json["size"][0],
|
228 |
+
width=idphoto_json["size"][1],
|
229 |
+
),
|
230 |
+
visible=True,
|
231 |
)
|
232 |
|
233 |
# 如果输出 KB 大小选择的是自定义
|
|
|
237 |
# 输出路径为一个根据时间戳 + 哈希值生成的随机文件名
|
238 |
import time
|
239 |
|
240 |
+
output_image_path = f"{os.path.join(os.path.dirname(__file__), 'demo/kb_output')}/{int(time.time())}.jpg"
|
241 |
resize_image_to_kb(
|
242 |
result_image_standard,
|
243 |
output_image_path,
|
|
|
247 |
output_image_path = None
|
248 |
|
249 |
if output_image_path:
|
250 |
+
result_message = {
|
251 |
img_output_standard: result_image_standard,
|
252 |
img_output_standard_hd: result_image_hd,
|
253 |
img_output_layout: result_layout_image,
|
|
|
255 |
file_download: gr.update(visible=True, value=output_image_path),
|
256 |
}
|
257 |
else:
|
258 |
+
result_message = {
|
259 |
img_output_standard: result_image_standard,
|
260 |
img_output_standard_hd: result_image_hd,
|
261 |
img_output_layout: result_layout_image,
|
|
|
263 |
file_download: gr.update(visible=False),
|
264 |
}
|
265 |
|
266 |
+
return result_message
|
267 |
|
268 |
|
269 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
270 |
language = ["中文", "English"]
|
271 |
size_mode_CN = ["尺寸列表", "只换底", "自定义尺寸"]
|
272 |
size_mode_EN = ["Size List", "Only Change Background", "Custom Size"]
|
|
|
283 |
image_kb_CN = ["不设置", "自定义"]
|
284 |
image_kb_EN = ["Not Set", "Custom"]
|
285 |
|
286 |
+
# title = "<h1 id='title'>HivisionIDPhotos</h1>"
|
287 |
+
# description = "<h3>😎9.2 Update: Add photo size KB adjustment</h3>"
|
288 |
+
# css = """
|
289 |
+
# h1#title, h3 {
|
290 |
+
# text-align: center;
|
291 |
+
# }
|
292 |
+
# """
|
293 |
+
|
294 |
css = """
|
295 |
+
#col-left {
|
296 |
+
margin: 0 auto;
|
297 |
+
max-width: 430px;
|
298 |
+
}
|
299 |
+
#col-mid {
|
300 |
+
margin: 0 auto;
|
301 |
+
max-width: 430px;
|
302 |
+
}
|
303 |
+
#col-right {
|
304 |
+
margin: 0 auto;
|
305 |
+
max-width: 430px;
|
306 |
+
}
|
307 |
+
#col-showcase {
|
308 |
+
margin: 0 auto;
|
309 |
+
max-width: 1100px;
|
310 |
+
}
|
311 |
+
#button {
|
312 |
+
color: blue;
|
313 |
+
}
|
314 |
+
"""
|
315 |
+
|
316 |
+
def load_description(fp):
|
317 |
+
with open(fp, "r", encoding="utf-8") as f:
|
318 |
+
content = f.read()
|
319 |
+
return content
|
320 |
|
321 |
demo = gr.Blocks(css=css)
|
322 |
|
323 |
with demo:
|
324 |
+
gr.HTML(load_description(os.path.join(root_dir, "assets/title.md")))
|
|
|
325 |
with gr.Row():
|
326 |
# ------------ 左半边 UI ----------------
|
327 |
with gr.Column():
|
328 |
+
img_input = gr.Image(height=400)
|
329 |
language_options = gr.Dropdown(
|
330 |
+
choices=language, label="Language", value="中文", elem_id="language"
|
331 |
)
|
332 |
|
333 |
mode_options = gr.Radio(
|
334 |
+
choices=size_mode_CN,
|
335 |
+
label="证件照尺寸选项",
|
336 |
+
value="尺寸列表",
|
337 |
elem_id="size",
|
338 |
)
|
339 |
|
340 |
# 预设尺寸下拉菜单
|
341 |
with gr.Row(visible=True) as size_list_row:
|
342 |
size_list_options = gr.Dropdown(
|
343 |
+
choices=size_list_CN,
|
344 |
+
label="预设尺寸",
|
345 |
+
value=size_list_CN[0],
|
346 |
elem_id="size_list",
|
347 |
)
|
348 |
|
|
|
356 |
|
357 |
# 左:背景色选项
|
358 |
color_options = gr.Radio(
|
359 |
+
choices=colors_CN, label="背景色", value="蓝色", elem_id="color"
|
360 |
)
|
361 |
|
362 |
# 左:如果选择「自定义底色」,显示 RGB 输入框
|
|
|
367 |
|
368 |
# 左:渲染方式选项
|
369 |
render_options = gr.Radio(
|
370 |
+
choices=render_CN,
|
371 |
+
label="渲染方式",
|
372 |
+
value="纯色",
|
373 |
elem_id="render",
|
374 |
)
|
375 |
|
376 |
# 左:输出 KB 大小选项
|
377 |
image_kb_options = gr.Radio(
|
378 |
+
choices=image_kb_CN,
|
379 |
+
label="设置 KB 大小(结果在右边最底的组件下载)",
|
380 |
+
value="不设置",
|
381 |
elem_id="image_kb",
|
382 |
)
|
383 |
|
|
|
387 |
minimum=10,
|
388 |
maximum=1000,
|
389 |
value=50,
|
390 |
+
label="KB 大小",
|
391 |
interactive=True,
|
392 |
)
|
393 |
|
394 |
+
img_but = gr.Button("开始制作")
|
395 |
|
396 |
# 案例图片
|
397 |
+
example_images = gr.Examples(
|
398 |
+
inputs=[img_input],
|
399 |
+
examples=[
|
400 |
[path.as_posix()]
|
401 |
for path in sorted(
|
402 |
+
pathlib.Path(os.path.join(root_dir, "demo/images")).rglob(
|
403 |
"*.jpg"
|
404 |
)
|
405 |
)
|
|
|
408 |
|
409 |
# ---------------- 右半边 UI ----------------
|
410 |
with gr.Column():
|
411 |
+
notification = gr.Text(label="状态", visible=False)
|
412 |
with gr.Row():
|
413 |
+
img_output_standard = gr.Image(label="标准照", height=350)
|
414 |
+
img_output_standard_hd = gr.Image(label="高清照", height=350)
|
415 |
+
img_output_layout = gr.Image(label="六寸排版照", height=350)
|
416 |
+
file_download = gr.File(label="下载调整 KB 大小后的照片", visible=False)
|
417 |
|
418 |
# ---------------- 设置隐藏/显示组件 ----------------
|
419 |
def change_language(language):
|
|
|
423 |
size_list_options: gr.update(
|
424 |
label="预设尺寸",
|
425 |
choices=size_list_CN,
|
426 |
+
value=size_list_CN[0],
|
427 |
),
|
428 |
mode_options: gr.update(
|
429 |
label="证件照尺寸选项",
|
|
|
458 |
size_list_options: gr.update(
|
459 |
label="Default size",
|
460 |
choices=size_list_EN,
|
461 |
+
value=size_list_EN[0],
|
462 |
),
|
463 |
mode_options: gr.update(
|
464 |
label="ID photo size options",
|
|
|
586 |
],
|
587 |
)
|
588 |
|
589 |
+
argparser = argparse.ArgumentParser()
|
590 |
+
argparser.add_argument(
|
591 |
+
"--port", type=int, default=7860, help="The port number of the server"
|
592 |
+
)
|
593 |
+
argparser.add_argument(
|
594 |
+
"--host", type=str, default="127.0.0.1", help="The host of the server"
|
595 |
+
)
|
596 |
+
args = argparser.parse_args()
|
|
|
|
|
|
|
|
|
597 |
|
598 |
+
demo.launch(server_name=args.host, server_port=args.port)
|
app.spec
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
)
|
assets/hivisionai.ico
ADDED
assets/title.md
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div style="display: flex; justify-content: center; align-items: center; text-align: center; font-size: 40px;">
|
2 |
+
<div>
|
3 |
+
<b>HivisionIDPhotos</b>
|
4 |
+
<br>
|
5 |
+
<div style="display: flex; justify-content: center; align-items: center; text-align: center;">
|
6 |
+
<a href="https://github.com/xiaolin199912/HivisionIDPhotos"><img alt="Github" src="https://img.shields.io/static/v1?label=GitHub&message=GitHub&color=black"></a>  
|
7 |
+
<a href="https://docs.qq.com/doc/DUkpBdk90eWZFS2JW" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/WeChat-微信-4cb55e"></a>  
|
8 |
+
<a href="https://github.com/Zeyi-Lin/HivisionIDPhotos/blob/master/docs/api_EN.md" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/API_Docs-API文档-315bce"></a>
|
9 |
+
</div>
|
10 |
+
</div>
|
11 |
+
</div>
|
beautyPlugin/GrindSkin.py
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
@author: cuny
|
3 |
-
@file: GrindSkin.py
|
4 |
-
@time: 2022/7/2 14:44
|
5 |
-
@description:
|
6 |
-
磨皮算法
|
7 |
-
"""
|
8 |
-
import cv2
|
9 |
-
import numpy as np
|
10 |
-
|
11 |
-
|
12 |
-
def grindSkin(src, grindDegree: int = 3, detailDegree: int = 1, strength: int = 9):
|
13 |
-
"""
|
14 |
-
Dest =(Src * (100 - Opacity) + (Src + 2 * GaussBlur(EPFFilter(Src) - Src)) * Opacity) /100
|
15 |
-
人像磨皮方案,后续会考虑使用一些皮肤区域检测算法来实现仅皮肤区域磨皮,增加算法的精细程度——或者使用人脸关键点
|
16 |
-
https://www.cnblogs.com/Imageshop/p/4709710.html
|
17 |
-
Args:
|
18 |
-
src: 原图
|
19 |
-
grindDegree: 磨皮程度调节参数
|
20 |
-
detailDegree: 细节程度调节参数
|
21 |
-
strength: 融合程度,作为磨皮强度(0 - 10)
|
22 |
-
|
23 |
-
Returns:
|
24 |
-
磨皮后的图像
|
25 |
-
"""
|
26 |
-
if strength <= 0:
|
27 |
-
return src
|
28 |
-
dst = src.copy()
|
29 |
-
opacity = min(10., strength) / 10.
|
30 |
-
dx = grindDegree * 5 # 双边滤波参数之一
|
31 |
-
fc = grindDegree * 12.5 # 双边滤波参数之一
|
32 |
-
temp1 = cv2.bilateralFilter(src[:, :, :3], dx, fc, fc)
|
33 |
-
temp2 = cv2.subtract(temp1, src[:, :, :3])
|
34 |
-
temp3 = cv2.GaussianBlur(temp2, (2 * detailDegree - 1, 2 * detailDegree - 1), 0)
|
35 |
-
temp4 = cv2.add(cv2.add(temp3, temp3), src[:, :, :3])
|
36 |
-
dst[:, :, :3] = cv2.addWeighted(temp4, opacity, src[:, :, :3], 1 - opacity, 0.0)
|
37 |
-
return dst
|
38 |
-
|
39 |
-
|
40 |
-
if __name__ == "__main__":
|
41 |
-
input_image = cv2.imread("test_image/7.jpg")
|
42 |
-
output_image = grindSkin(src=input_image)
|
43 |
-
cv2.imwrite("grindSkinCompare.png", np.hstack((input_image, output_image)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
beautyPlugin/MakeBeautiful.py
DELETED
@@ -1,45 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
@author: cuny
|
3 |
-
@file: MakeBeautiful.py
|
4 |
-
@time: 2022/7/7 20:23
|
5 |
-
@description:
|
6 |
-
美颜工具集合文件,作为暴露在外的插件接口
|
7 |
-
"""
|
8 |
-
from .GrindSkin import grindSkin
|
9 |
-
from .MakeWhiter import MakeWhiter
|
10 |
-
from .ThinFace import thinFace
|
11 |
-
import numpy as np
|
12 |
-
|
13 |
-
|
14 |
-
def makeBeautiful(input_image: np.ndarray,
|
15 |
-
landmark,
|
16 |
-
thinStrength: int,
|
17 |
-
thinPlace: int,
|
18 |
-
grindStrength: int,
|
19 |
-
whiterStrength: int
|
20 |
-
) -> np.ndarray:
|
21 |
-
"""
|
22 |
-
美颜工具的接口函数,用于实现美颜效果
|
23 |
-
Args:
|
24 |
-
input_image: 输入的图像
|
25 |
-
landmark: 瘦脸需要的人脸关键点信息,为fd68返回的第二个参数
|
26 |
-
thinStrength: 瘦脸强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不瘦脸
|
27 |
-
thinPlace: 选择瘦脸区域,为0-2之间的值,越大瘦脸的点越靠下
|
28 |
-
grindStrength: 磨皮强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不磨皮
|
29 |
-
whiterStrength: 美白强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不美白
|
30 |
-
Returns:
|
31 |
-
output_image 输出图像
|
32 |
-
"""
|
33 |
-
try:
|
34 |
-
_, _, _ = input_image.shape
|
35 |
-
except ValueError:
|
36 |
-
raise TypeError("输入图像必须为3通道或者4通道!")
|
37 |
-
# 三通道或者四通道图像
|
38 |
-
# 首先进行瘦脸
|
39 |
-
input_image = thinFace(input_image, landmark, place=thinPlace, strength=thinStrength)
|
40 |
-
# 其次进行磨皮
|
41 |
-
input_image = grindSkin(src=input_image, strength=grindStrength)
|
42 |
-
# 最后进行美白
|
43 |
-
makeWhiter = MakeWhiter()
|
44 |
-
input_image = makeWhiter.run(input_image, strength=whiterStrength)
|
45 |
-
return input_image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
beautyPlugin/MakeWhiter.py
DELETED
@@ -1,108 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
@author: cuny
|
3 |
-
@file: MakeWhiter.py
|
4 |
-
@time: 2022/7/2 14:28
|
5 |
-
@description:
|
6 |
-
美白算法
|
7 |
-
"""
|
8 |
-
import os
|
9 |
-
import cv2
|
10 |
-
import math
|
11 |
-
import numpy as np
|
12 |
-
local_path = os.path.dirname(__file__)
|
13 |
-
|
14 |
-
|
15 |
-
class MakeWhiter(object):
|
16 |
-
class __LutWhite:
|
17 |
-
"""
|
18 |
-
美白的内部类
|
19 |
-
"""
|
20 |
-
|
21 |
-
def __init__(self, lut):
|
22 |
-
cube64rows = 8
|
23 |
-
cube64size = 64
|
24 |
-
cube256size = 256
|
25 |
-
cubeScale = int(cube256size / cube64size) # 4
|
26 |
-
|
27 |
-
reshapeLut = np.zeros((cube256size, cube256size, cube256size, 3))
|
28 |
-
for i in range(cube64size):
|
29 |
-
tmp = math.floor(i / cube64rows)
|
30 |
-
cx = int((i - tmp * cube64rows) * cube64size)
|
31 |
-
cy = int(tmp * cube64size)
|
32 |
-
cube64 = lut[cy:cy + cube64size, cx:cx + cube64size] # cube64 in lut(512*512 (512=8*64))
|
33 |
-
_rows, _cols, _ = cube64.shape
|
34 |
-
if _rows == 0 or _cols == 0:
|
35 |
-
continue
|
36 |
-
cube256 = cv2.resize(cube64, (cube256size, cube256size))
|
37 |
-
i = i * cubeScale
|
38 |
-
for k in range(cubeScale):
|
39 |
-
reshapeLut[i + k] = cube256
|
40 |
-
self.lut = reshapeLut
|
41 |
-
|
42 |
-
def imageInLut(self, src):
|
43 |
-
arr = src.copy()
|
44 |
-
bs = arr[:, :, 0]
|
45 |
-
gs = arr[:, :, 1]
|
46 |
-
rs = arr[:, :, 2]
|
47 |
-
arr[:, :] = self.lut[bs, gs, rs]
|
48 |
-
return arr
|
49 |
-
|
50 |
-
def __init__(self, lutImage: np.ndarray = None):
|
51 |
-
self.__lutWhiten = None
|
52 |
-
if lutImage is not None:
|
53 |
-
self.__lutWhiten = self.__LutWhite(lutImage)
|
54 |
-
|
55 |
-
def setLut(self, lutImage: np.ndarray):
|
56 |
-
self.__lutWhiten = self.__LutWhite(lutImage)
|
57 |
-
|
58 |
-
@staticmethod
|
59 |
-
def generate_identify_color_matrix(size: int = 512, channel: int = 3) -> np.ndarray:
|
60 |
-
"""
|
61 |
-
用于生成一张初始的查找表
|
62 |
-
Args:
|
63 |
-
size: 查找表尺寸,默认为512
|
64 |
-
channel: 查找表通道数,默认为3
|
65 |
-
|
66 |
-
Returns:
|
67 |
-
返回生成的查找表图像
|
68 |
-
"""
|
69 |
-
img = np.zeros((size, size, channel), dtype=np.uint8)
|
70 |
-
for by in range(size // 64):
|
71 |
-
for bx in range(size // 64):
|
72 |
-
for g in range(64):
|
73 |
-
for r in range(64):
|
74 |
-
x = r + bx * 64
|
75 |
-
y = g + by * 64
|
76 |
-
img[y][x][0] = int(r * 255.0 / 63.0 + 0.5)
|
77 |
-
img[y][x][1] = int(g * 255.0 / 63.0 + 0.5)
|
78 |
-
img[y][x][2] = int((bx + by * 8.0) * 255.0 / 63.0 + 0.5)
|
79 |
-
return cv2.cvtColor(img, cv2.COLOR_RGB2BGR).clip(0, 255).astype('uint8')
|
80 |
-
|
81 |
-
def run(self, src: np.ndarray, strength: int) -> np.ndarray:
|
82 |
-
"""
|
83 |
-
美白图像
|
84 |
-
Args:
|
85 |
-
src: 原图
|
86 |
-
strength: 美白强度,0 - 10
|
87 |
-
Returns:
|
88 |
-
美白后的图像
|
89 |
-
"""
|
90 |
-
dst = src.copy()
|
91 |
-
strength = min(10, int(strength)) / 10.
|
92 |
-
if strength <= 0:
|
93 |
-
return dst
|
94 |
-
self.setLut(cv2.imread(f"{local_path}/lut_image/3.png", -1))
|
95 |
-
_, _, c = src.shape
|
96 |
-
img = self.__lutWhiten.imageInLut(src[:, :, :3])
|
97 |
-
dst[:, :, :3] = cv2.addWeighted(src[:, :, :3], 1 - strength, img, strength, 0)
|
98 |
-
return dst
|
99 |
-
|
100 |
-
|
101 |
-
if __name__ == "__main__":
|
102 |
-
# makeLut = MakeWhiter()
|
103 |
-
# cv2.imwrite("lutOrigin.png", makeLut.generate_identify_color_matrix())
|
104 |
-
input_image = cv2.imread("test_image/7.jpg", -1)
|
105 |
-
lut_image = cv2.imread("lut_image/3.png")
|
106 |
-
makeWhiter = MakeWhiter(lut_image)
|
107 |
-
output_image = makeWhiter.run(input_image, 10)
|
108 |
-
cv2.imwrite("makeWhiterCompare.png", np.hstack((input_image, output_image)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
beautyPlugin/ThinFace.py
DELETED
@@ -1,267 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
@author: cuny
|
3 |
-
@file: ThinFace.py
|
4 |
-
@time: 2022/7/2 15:50
|
5 |
-
@description:
|
6 |
-
瘦脸算法,用到了图像局部平移法
|
7 |
-
先使用人脸关键点检测,然后再使用图像局部平移法
|
8 |
-
需要注意的是,这部分不会包含dlib人脸关键点检测,因为考虑到模型载入的问题
|
9 |
-
"""
|
10 |
-
import cv2
|
11 |
-
import math
|
12 |
-
import numpy as np
|
13 |
-
|
14 |
-
|
15 |
-
class TranslationWarp(object):
|
16 |
-
"""
|
17 |
-
本类包含瘦脸算法,由于瘦脸算法包含了很多个版本,所以以类的方式呈现
|
18 |
-
前两个算法没什么好讲的,网上资料很多
|
19 |
-
第三个采用numpy内部的自定义函数处理,在处理速度上有一些提升
|
20 |
-
最后采用cv2.map算法,处理速度大幅度提升
|
21 |
-
"""
|
22 |
-
|
23 |
-
# 瘦脸
|
24 |
-
@staticmethod
|
25 |
-
def localTranslationWarp(srcImg, startX, startY, endX, endY, radius):
|
26 |
-
# 双线性插值法
|
27 |
-
def BilinearInsert(src, ux, uy):
|
28 |
-
w, h, c = src.shape
|
29 |
-
if c == 3:
|
30 |
-
x1 = int(ux)
|
31 |
-
x2 = x1 + 1
|
32 |
-
y1 = int(uy)
|
33 |
-
y2 = y1 + 1
|
34 |
-
part1 = src[y1, x1].astype(np.float64) * (float(x2) - ux) * (float(y2) - uy)
|
35 |
-
part2 = src[y1, x2].astype(np.float64) * (ux - float(x1)) * (float(y2) - uy)
|
36 |
-
part3 = src[y2, x1].astype(np.float64) * (float(x2) - ux) * (uy - float(y1))
|
37 |
-
part4 = src[y2, x2].astype(np.float64) * (ux - float(x1)) * (uy - float(y1))
|
38 |
-
insertValue = part1 + part2 + part3 + part4
|
39 |
-
return insertValue.astype(np.int8)
|
40 |
-
|
41 |
-
ddradius = float(radius * radius) # 圆的半径
|
42 |
-
copyImg = srcImg.copy() # copy后的图像矩阵
|
43 |
-
# 计算公式中的|m-c|^2
|
44 |
-
ddmc = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)
|
45 |
-
H, W, C = srcImg.shape # 获取图像的形状
|
46 |
-
for i in range(W):
|
47 |
-
for j in range(H):
|
48 |
-
# # 计算该点是否在形变圆的范围之内
|
49 |
-
# # 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
|
50 |
-
if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
|
51 |
-
continue
|
52 |
-
distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
|
53 |
-
if distance < ddradius:
|
54 |
-
# 计算出(i,j)坐标的原坐标
|
55 |
-
# 计算公式中右边平方号里的部分
|
56 |
-
ratio = (ddradius - distance) / (ddradius - distance + ddmc)
|
57 |
-
ratio = ratio * ratio
|
58 |
-
# 映射原位置
|
59 |
-
UX = i - ratio * (endX - startX)
|
60 |
-
UY = j - ratio * (endY - startY)
|
61 |
-
|
62 |
-
# 根据双线性插值法得到UX,UY的值
|
63 |
-
# start_ = time.time()
|
64 |
-
value = BilinearInsert(srcImg, UX, UY)
|
65 |
-
# print(f"双线性插值耗时;{time.time() - start_}")
|
66 |
-
# 改变当前 i ,j的值
|
67 |
-
copyImg[j, i] = value
|
68 |
-
return copyImg
|
69 |
-
|
70 |
-
# 瘦脸pro1, 限制了for循环的遍历次数
|
71 |
-
@staticmethod
|
72 |
-
def localTranslationWarpLimitFor(srcImg, startP: np.matrix, endP: np.matrix, radius: float):
|
73 |
-
startX, startY = startP[0, 0], startP[0, 1]
|
74 |
-
endX, endY = endP[0, 0], endP[0, 1]
|
75 |
-
|
76 |
-
# 双线性插值法
|
77 |
-
def BilinearInsert(src, ux, uy):
|
78 |
-
w, h, c = src.shape
|
79 |
-
if c == 3:
|
80 |
-
x1 = int(ux)
|
81 |
-
x2 = x1 + 1
|
82 |
-
y1 = int(uy)
|
83 |
-
y2 = y1 + 1
|
84 |
-
part1 = src[y1, x1].astype(np.float64) * (float(x2) - ux) * (float(y2) - uy)
|
85 |
-
part2 = src[y1, x2].astype(np.float64) * (ux - float(x1)) * (float(y2) - uy)
|
86 |
-
part3 = src[y2, x1].astype(np.float64) * (float(x2) - ux) * (uy - float(y1))
|
87 |
-
part4 = src[y2, x2].astype(np.float64) * (ux - float(x1)) * (uy - float(y1))
|
88 |
-
insertValue = part1 + part2 + part3 + part4
|
89 |
-
return insertValue.astype(np.int8)
|
90 |
-
|
91 |
-
ddradius = float(radius * radius) # 圆的半径
|
92 |
-
copyImg = srcImg.copy() # copy后的图像矩阵
|
93 |
-
# 计算公式中的|m-c|^2
|
94 |
-
ddmc = (endX - startX) ** 2 + (endY - startY) ** 2
|
95 |
-
# 计算正方形的左上角起始点
|
96 |
-
startTX, startTY = (startX - math.floor(radius + 1), startY - math.floor((radius + 1)))
|
97 |
-
# 计算正方形的右下角的结束点
|
98 |
-
endTX, endTY = (startX + math.floor(radius + 1), startY + math.floor((radius + 1)))
|
99 |
-
# 剪切srcImg
|
100 |
-
srcImg = srcImg[startTY: endTY + 1, startTX: endTX + 1, :]
|
101 |
-
# db.cv_show(srcImg)
|
102 |
-
# 裁剪后的图像相当于在x,y都减少了startX - math.floor(radius + 1)
|
103 |
-
# 原本的endX, endY在切后的坐标点
|
104 |
-
endX, endY = (endX - startX + math.floor(radius + 1), endY - startY + math.floor(radius + 1))
|
105 |
-
# 原本的startX, startY剪切后的坐标点
|
106 |
-
startX, startY = (math.floor(radius + 1), math.floor(radius + 1))
|
107 |
-
H, W, C = srcImg.shape # 获取图像的形状
|
108 |
-
for i in range(W):
|
109 |
-
for j in range(H):
|
110 |
-
# 计算该点是否在形变圆的范围之内
|
111 |
-
# 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
|
112 |
-
# if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
|
113 |
-
# continue
|
114 |
-
distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
|
115 |
-
if distance < ddradius:
|
116 |
-
# 计算出(i,j)坐标的原坐标
|
117 |
-
# 计算公式中右边平方号里的部分
|
118 |
-
ratio = (ddradius - distance) / (ddradius - distance + ddmc)
|
119 |
-
ratio = ratio * ratio
|
120 |
-
# 映射原位置
|
121 |
-
UX = i - ratio * (endX - startX)
|
122 |
-
UY = j - ratio * (endY - startY)
|
123 |
-
|
124 |
-
# 根据双线性插值法得到UX,UY的值
|
125 |
-
# start_ = time.time()
|
126 |
-
value = BilinearInsert(srcImg, UX, UY)
|
127 |
-
# print(f"双线性插值耗时;{time.time() - start_}")
|
128 |
-
# 改变当前 i ,j的值
|
129 |
-
copyImg[j + startTY, i + startTX] = value
|
130 |
-
return copyImg
|
131 |
-
|
132 |
-
# # 瘦脸pro2,采用了numpy自定义函数做处理
|
133 |
-
# def localTranslationWarpNumpy(self, srcImg, startP: np.matrix, endP: np.matrix, radius: float):
|
134 |
-
# startX , startY = startP[0, 0], startP[0, 1]
|
135 |
-
# endX, endY = endP[0, 0], endP[0, 1]
|
136 |
-
# ddradius = float(radius * radius) # 圆的半径
|
137 |
-
# copyImg = srcImg.copy() # copy后的图像矩阵
|
138 |
-
# # 计算公式中的|m-c|^2
|
139 |
-
# ddmc = (endX - startX)**2 + (endY - startY)**2
|
140 |
-
# # 计算正方形的左上角起始点
|
141 |
-
# startTX, startTY = (startX - math.floor(radius + 1), startY - math.floor((radius + 1)))
|
142 |
-
# # 计算正方形的右下角的结束点
|
143 |
-
# endTX, endTY = (startX + math.floor(radius + 1), startY + math.floor((radius + 1)))
|
144 |
-
# # 剪切srcImg
|
145 |
-
# self.thinImage = srcImg[startTY : endTY + 1, startTX : endTX + 1, :]
|
146 |
-
# # s = self.thinImage
|
147 |
-
# # db.cv_show(srcImg)
|
148 |
-
# # 裁剪后的图像相当于在x,y都减少了startX - math.floor(radius + 1)
|
149 |
-
# # 原本的endX, endY在切后的坐标点
|
150 |
-
# endX, endY = (endX - startX + math.floor(radius + 1), endY - startY + math.floor(radius + 1))
|
151 |
-
# # 原本的startX, startY剪切后的坐标点
|
152 |
-
# startX ,startY = (math.floor(radius + 1), math.floor(radius + 1))
|
153 |
-
# H, W, C = self.thinImage.shape # 获取图像的形状
|
154 |
-
# index_m = np.arange(H * W).reshape((H, W))
|
155 |
-
# triangle_ufunc = np.frompyfunc(self.process, 9, 3)
|
156 |
-
# # start_ = time.time()
|
157 |
-
# finalImgB, finalImgG, finalImgR = triangle_ufunc(index_m, self, W, ddradius, ddmc, startX, startY, endX, endY)
|
158 |
-
# finaleImg = np.dstack((finalImgB, finalImgG, finalImgR)).astype(np.uint8)
|
159 |
-
# finaleImg = np.fliplr(np.rot90(finaleImg, -1))
|
160 |
-
# copyImg[startTY: endTY + 1, startTX: endTX + 1, :] = finaleImg
|
161 |
-
# # print(f"图像处理耗时;{time.time() - start_}")
|
162 |
-
# # db.cv_show(copyImg)
|
163 |
-
# return copyImg
|
164 |
-
|
165 |
-
# 瘦脸pro3,采用opencv内置函数
|
166 |
-
@staticmethod
|
167 |
-
def localTranslationWarpFastWithStrength(srcImg, startP: np.matrix, endP: np.matrix, radius, strength: float = 100.):
|
168 |
-
"""
|
169 |
-
采用opencv内置函数
|
170 |
-
Args:
|
171 |
-
srcImg: 源图像
|
172 |
-
startP: 起点位置
|
173 |
-
endP: 终点位置
|
174 |
-
radius: 处理半径
|
175 |
-
strength: 瘦脸强度,一般取100以上
|
176 |
-
|
177 |
-
Returns:
|
178 |
-
|
179 |
-
"""
|
180 |
-
startX, startY = startP[0, 0], startP[0, 1]
|
181 |
-
endX, endY = endP[0, 0], endP[0, 1]
|
182 |
-
ddradius = float(radius * radius)
|
183 |
-
# copyImg = np.zeros(srcImg.shape, np.uint8)
|
184 |
-
# copyImg = srcImg.copy()
|
185 |
-
|
186 |
-
maskImg = np.zeros(srcImg.shape[:2], np.uint8)
|
187 |
-
cv2.circle(maskImg, (startX, startY), math.ceil(radius), (255, 255, 255), -1)
|
188 |
-
|
189 |
-
K0 = 100 / strength
|
190 |
-
|
191 |
-
# 计算公式中的|m-c|^2
|
192 |
-
ddmc_x = (endX - startX) * (endX - startX)
|
193 |
-
ddmc_y = (endY - startY) * (endY - startY)
|
194 |
-
H, W, C = srcImg.shape
|
195 |
-
|
196 |
-
mapX = np.vstack([np.arange(W).astype(np.float32).reshape(1, -1)] * H)
|
197 |
-
mapY = np.hstack([np.arange(H).astype(np.float32).reshape(-1, 1)] * W)
|
198 |
-
|
199 |
-
distance_x = (mapX - startX) * (mapX - startX)
|
200 |
-
distance_y = (mapY - startY) * (mapY - startY)
|
201 |
-
distance = distance_x + distance_y
|
202 |
-
K1 = np.sqrt(distance)
|
203 |
-
ratio_x = (ddradius - distance_x) / (ddradius - distance_x + K0 * ddmc_x)
|
204 |
-
ratio_y = (ddradius - distance_y) / (ddradius - distance_y + K0 * ddmc_y)
|
205 |
-
ratio_x = ratio_x * ratio_x
|
206 |
-
ratio_y = ratio_y * ratio_y
|
207 |
-
|
208 |
-
UX = mapX - ratio_x * (endX - startX) * (1 - K1 / radius)
|
209 |
-
UY = mapY - ratio_y * (endY - startY) * (1 - K1 / radius)
|
210 |
-
|
211 |
-
np.copyto(UX, mapX, where=maskImg == 0)
|
212 |
-
np.copyto(UY, mapY, where=maskImg == 0)
|
213 |
-
UX = UX.astype(np.float32)
|
214 |
-
UY = UY.astype(np.float32)
|
215 |
-
copyImg = cv2.remap(srcImg, UX, UY, interpolation=cv2.INTER_LINEAR)
|
216 |
-
return copyImg
|
217 |
-
|
218 |
-
|
219 |
-
def thinFace(src, landmark, place: int = 0, strength=30.):
|
220 |
-
"""
|
221 |
-
瘦脸程序接口,输入人脸关键点信息和强度,即可实现瘦脸
|
222 |
-
注意处理四通道图像
|
223 |
-
Args:
|
224 |
-
src: 原图
|
225 |
-
landmark: 关键点信息
|
226 |
-
place: 选择瘦脸区域,为0-4之间的值
|
227 |
-
strength: 瘦脸强度,输入值在0-10之间,如果小于或者等于0,则不瘦脸
|
228 |
-
|
229 |
-
Returns:
|
230 |
-
瘦脸后的图像
|
231 |
-
"""
|
232 |
-
strength = min(100., strength * 10.)
|
233 |
-
if strength <= 0.:
|
234 |
-
return src
|
235 |
-
# 也可以设置瘦脸区域
|
236 |
-
place = max(0, min(4, int(place)))
|
237 |
-
left_landmark = landmark[4 + place]
|
238 |
-
left_landmark_down = landmark[6 + place]
|
239 |
-
right_landmark = landmark[13 + place]
|
240 |
-
right_landmark_down = landmark[15 + place]
|
241 |
-
endPt = landmark[58]
|
242 |
-
# 计算第4个点到第6个点的距离作为瘦脸距离
|
243 |
-
r_left = math.sqrt(
|
244 |
-
(left_landmark[0, 0] - left_landmark_down[0, 0]) ** 2 +
|
245 |
-
(left_landmark[0, 1] - left_landmark_down[0, 1]) ** 2
|
246 |
-
)
|
247 |
-
|
248 |
-
# 计算第14个点到第16个点的距离作为瘦脸距离
|
249 |
-
r_right = math.sqrt((right_landmark[0, 0] - right_landmark_down[0, 0]) ** 2 +
|
250 |
-
(right_landmark[0, 1] - right_landmark_down[0, 1]) ** 2)
|
251 |
-
# 瘦左边脸
|
252 |
-
thin_image = TranslationWarp.localTranslationWarpFastWithStrength(src, left_landmark[0], endPt[0], r_left, strength)
|
253 |
-
# 瘦右边脸
|
254 |
-
thin_image = TranslationWarp.localTranslationWarpFastWithStrength(thin_image, right_landmark[0], endPt[0], r_right, strength)
|
255 |
-
return thin_image
|
256 |
-
|
257 |
-
|
258 |
-
if __name__ == "__main__":
|
259 |
-
import os
|
260 |
-
from hycv.FaceDetection68.faceDetection68 import FaceDetection68
|
261 |
-
local_file = os.path.dirname(__file__)
|
262 |
-
PREDICTOR_PATH = f"{local_file}/weights/shape_predictor_68_face_landmarks.dat" # 关键点检测模型路径
|
263 |
-
fd68 = FaceDetection68(model_path=PREDICTOR_PATH)
|
264 |
-
input_image = cv2.imread("test_image/4.jpg", -1)
|
265 |
-
_, landmark_, _ = fd68.facePoints(input_image)
|
266 |
-
output_image = thinFace(input_image, landmark_, strength=30.2)
|
267 |
-
cv2.imwrite("thinFaceCompare.png", np.hstack((input_image, output_image)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
beautyPlugin/__init__.py
DELETED
@@ -1,4 +0,0 @@
|
|
1 |
-
from .MakeBeautiful import makeBeautiful
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
beautyPlugin/lut_image/1.png
DELETED
Binary file (102 kB)
|
|
beautyPlugin/lut_image/3.png
DELETED
Binary file (106 kB)
|
|
beautyPlugin/lut_image/lutOrigin.png
DELETED
Binary file (136 kB)
|
|
{images → demo/images}/test.jpg
RENAMED
File without changes
|
{images → demo/images}/test2.jpg
RENAMED
File without changes
|
{images → demo/images}/test3.jpg
RENAMED
File without changes
|
{images → demo/images}/test4.jpg
RENAMED
File without changes
|
{output → demo/kb_output}/.gitkeep
RENAMED
File without changes
|
size_list_CN.csv → demo/size_list_CN.csv
RENAMED
File without changes
|
demo/size_list_EN.csv
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
data_utils.py → demo/utils.py
RENAMED
@@ -5,14 +5,15 @@ def csv_to_size_list(csv_file: str) -> dict:
|
|
5 |
# 初始化一个空字典
|
6 |
size_list_dict = {}
|
7 |
|
8 |
-
# 打开CSV文件并读取数据
|
9 |
-
with open(csv_file, mode="r") as file:
|
10 |
reader = csv.reader(file)
|
11 |
# 跳过表头
|
12 |
next(reader)
|
13 |
# 读取数据并填充字典
|
14 |
for row in reader:
|
15 |
size_name, h, w = row
|
16 |
-
|
|
|
17 |
|
18 |
return size_list_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
|
deploy_api.py
CHANGED
@@ -1,89 +1,92 @@
|
|
1 |
from fastapi import FastAPI, UploadFile, Form
|
2 |
import onnxruntime
|
3 |
-
from
|
4 |
-
from
|
5 |
-
from
|
|
|
|
|
|
|
|
|
6 |
import base64
|
7 |
import numpy as np
|
8 |
import cv2
|
9 |
-
import
|
10 |
|
11 |
app = FastAPI()
|
|
|
12 |
|
13 |
|
14 |
# 将图像转换为Base64编码
|
15 |
-
|
16 |
def numpy_2_base64(img: np.ndarray):
|
17 |
-
retval, buffer = cv2.imencode(
|
18 |
-
base64_image = base64.b64encode(buffer).decode(
|
19 |
|
20 |
return base64_image
|
21 |
|
22 |
|
23 |
# 证件照智能制作接口
|
24 |
@app.post("/idphoto")
|
25 |
-
async def idphoto_inference(
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
31 |
image_bytes = await input_image.read()
|
32 |
nparr = np.frombuffer(image_bytes, np.uint8)
|
33 |
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
34 |
|
35 |
# 将字符串转为元组
|
36 |
-
size =
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
human_sess=sess,
|
47 |
-
IS_DEBUG=False,
|
48 |
-
top_distance_max=top_distance_max,
|
49 |
-
top_distance_min=top_distance_min)
|
50 |
-
|
51 |
-
# 如果检测到人脸数量不等于1(照片无人脸 or 多人脸)
|
52 |
-
if status == 0:
|
53 |
-
result_messgae = {
|
54 |
-
"status": False
|
55 |
-
}
|
56 |
-
|
57 |
# 如果检测到人脸数量等于1, 则返回标准证和高清照结果(png 4通道图像)
|
58 |
else:
|
59 |
-
|
60 |
"status": True,
|
61 |
-
"
|
62 |
-
"
|
63 |
}
|
64 |
|
65 |
-
return
|
66 |
|
67 |
|
68 |
# 透明图像添加纯色背景接口
|
69 |
@app.post("/add_background")
|
70 |
-
async def photo_add_background(
|
71 |
-
|
72 |
-
|
73 |
-
# 读取图像
|
74 |
image_bytes = await input_image.read()
|
75 |
nparr = np.frombuffer(image_bytes, np.uint8)
|
76 |
img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
|
77 |
|
78 |
-
|
79 |
-
color = ast.literal_eval(color)
|
80 |
-
# 将元祖的0和2号数字交换
|
81 |
color = (color[2], color[1], color[0])
|
82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
# try:
|
|
|
84 |
result_messgae = {
|
85 |
"status": True,
|
86 |
-
"
|
87 |
}
|
88 |
|
89 |
# except Exception as e:
|
@@ -98,31 +101,44 @@ async def photo_add_background(input_image: UploadFile,
|
|
98 |
|
99 |
# 六寸排版照生成接口
|
100 |
@app.post("/generate_layout_photos")
|
101 |
-
async def generate_layout_photos(
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
|
|
|
|
|
|
|
|
|
|
106 |
|
107 |
-
|
108 |
|
109 |
-
|
110 |
-
|
|
|
111 |
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
width=size[1])
|
116 |
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
121 |
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
|
|
|
|
|
|
126 |
|
127 |
return result_messgae
|
128 |
|
@@ -131,7 +147,10 @@ if __name__ == "__main__":
|
|
131 |
import uvicorn
|
132 |
|
133 |
# 加载权重文件
|
134 |
-
|
|
|
|
|
|
|
135 |
sess = onnxruntime.InferenceSession(HY_HUMAN_MATTING_WEIGHTS_PATH)
|
136 |
|
137 |
# 在8080端口运行推理服务
|
|
|
1 |
from fastapi import FastAPI, UploadFile, Form
|
2 |
import onnxruntime
|
3 |
+
from hivision import IDCreator
|
4 |
+
from hivision.error import FaceError
|
5 |
+
from hivision.creator.layout_calculator import (
|
6 |
+
generate_layout_photo,
|
7 |
+
generate_layout_image,
|
8 |
+
)
|
9 |
+
from hivision.utils import add_background, resize_image_to_kb_base64, hex_to_rgb
|
10 |
import base64
|
11 |
import numpy as np
|
12 |
import cv2
|
13 |
+
import os
|
14 |
|
15 |
app = FastAPI()
|
16 |
+
creator = IDCreator()
|
17 |
|
18 |
|
19 |
# 将图像转换为Base64编码
|
|
|
20 |
def numpy_2_base64(img: np.ndarray):
|
21 |
+
retval, buffer = cv2.imencode(".png", img)
|
22 |
+
base64_image = base64.b64encode(buffer).decode("utf-8")
|
23 |
|
24 |
return base64_image
|
25 |
|
26 |
|
27 |
# 证件照智能制作接口
|
28 |
@app.post("/idphoto")
|
29 |
+
async def idphoto_inference(
|
30 |
+
input_image: UploadFile,
|
31 |
+
height: str = Form(...),
|
32 |
+
width: str = Form(...),
|
33 |
+
head_measure_ratio=0.2,
|
34 |
+
head_height_ratio=0.45,
|
35 |
+
top_distance_max=0.12,
|
36 |
+
top_distance_min=0.10,
|
37 |
+
):
|
38 |
+
|
39 |
image_bytes = await input_image.read()
|
40 |
nparr = np.frombuffer(image_bytes, np.uint8)
|
41 |
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
42 |
|
43 |
# 将字符串转为元组
|
44 |
+
size = (int(height), int(width))
|
45 |
+
try:
|
46 |
+
result = creator(
|
47 |
+
img,
|
48 |
+
size=size,
|
49 |
+
head_measure_ratio=head_measure_ratio,
|
50 |
+
head_height_ratio=head_height_ratio,
|
51 |
+
)
|
52 |
+
except FaceError:
|
53 |
+
result_message = {"status": False}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
# 如果检测到人脸数量等于1, 则返回标准证和高清照结果(png 4通道图像)
|
55 |
else:
|
56 |
+
result_message = {
|
57 |
"status": True,
|
58 |
+
"image_base64_standard": numpy_2_base64(result.standard),
|
59 |
+
"image_base64_hd": numpy_2_base64(result.hd),
|
60 |
}
|
61 |
|
62 |
+
return result_message
|
63 |
|
64 |
|
65 |
# 透明图像添加纯色背景接口
|
66 |
@app.post("/add_background")
|
67 |
+
async def photo_add_background(
|
68 |
+
input_image: UploadFile, color: str = Form(...), kb: str = Form(None)
|
69 |
+
):
|
|
|
70 |
image_bytes = await input_image.read()
|
71 |
nparr = np.frombuffer(image_bytes, np.uint8)
|
72 |
img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
|
73 |
|
74 |
+
color = hex_to_rgb(color)
|
|
|
|
|
75 |
color = (color[2], color[1], color[0])
|
76 |
|
77 |
+
result_image = add_background(img, bgr=color).astype(np.uint8)
|
78 |
+
|
79 |
+
if kb:
|
80 |
+
result_image = cv2.cvtColor(result_image, cv2.COLOR_RGB2BGR)
|
81 |
+
result_image_base64 = resize_image_to_kb_base64(result_image, int(kb))
|
82 |
+
else:
|
83 |
+
result_image_base64 = numpy_2_base64(result_image)
|
84 |
+
|
85 |
# try:
|
86 |
+
|
87 |
result_messgae = {
|
88 |
"status": True,
|
89 |
+
"image_base64": result_image_base64,
|
90 |
}
|
91 |
|
92 |
# except Exception as e:
|
|
|
101 |
|
102 |
# 六寸排版照生成接口
|
103 |
@app.post("/generate_layout_photos")
|
104 |
+
async def generate_layout_photos(
|
105 |
+
input_image: UploadFile,
|
106 |
+
height: str = Form(...),
|
107 |
+
width: str = Form(...),
|
108 |
+
kb: str = Form(None),
|
109 |
+
):
|
110 |
+
# try:
|
111 |
+
image_bytes = await input_image.read()
|
112 |
+
nparr = np.frombuffer(image_bytes, np.uint8)
|
113 |
+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
114 |
|
115 |
+
size = (int(height), int(width))
|
116 |
|
117 |
+
typography_arr, typography_rotate = generate_layout_photo(
|
118 |
+
input_height=size[0], input_width=size[1]
|
119 |
+
)
|
120 |
|
121 |
+
result_layout_image = generate_layout_image(
|
122 |
+
img, typography_arr, typography_rotate, height=size[0], width=size[1]
|
123 |
+
).astype(np.uint8)
|
|
|
124 |
|
125 |
+
if kb:
|
126 |
+
result_layout_image = cv2.cvtColor(result_layout_image, cv2.COLOR_RGB2BGR)
|
127 |
+
result_layout_image_base64 = resize_image_to_kb_base64(
|
128 |
+
result_layout_image, int(kb)
|
129 |
+
)
|
130 |
+
else:
|
131 |
+
result_layout_image_base64 = numpy_2_base64(result_layout_image)
|
132 |
|
133 |
+
result_messgae = {
|
134 |
+
"status": True,
|
135 |
+
"image_base64": result_layout_image_base64,
|
136 |
+
}
|
137 |
+
|
138 |
+
# except Exception as e:
|
139 |
+
# result_messgae = {
|
140 |
+
# "status": False,
|
141 |
+
# }
|
142 |
|
143 |
return result_messgae
|
144 |
|
|
|
147 |
import uvicorn
|
148 |
|
149 |
# 加载权重文件
|
150 |
+
root_dir = os.path.dirname(os.path.abspath(__file__))
|
151 |
+
HY_HUMAN_MATTING_WEIGHTS_PATH = os.path.join(
|
152 |
+
root_dir, "hivision/creator/weights/hivision_modnet.onnx"
|
153 |
+
)
|
154 |
sess = onnxruntime.InferenceSession(HY_HUMAN_MATTING_WEIGHTS_PATH)
|
155 |
|
156 |
# 在8080端口运行推理服务
|
docker-compose.yml
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: "3.8"
|
2 |
+
|
3 |
+
services:
|
4 |
+
hivision_idphotos:
|
5 |
+
build:
|
6 |
+
context: .
|
7 |
+
dockerfile: Dockerfile
|
8 |
+
image: hivision_idphotos2
|
9 |
+
command: python3 app.py --host 0.0.0.0 --port 7860
|
10 |
+
ports:
|
11 |
+
- "7860:7860"
|
docs/api_CN.md
ADDED
@@ -0,0 +1,552 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# API Docs
|
2 |
+
|
3 |
+
## 目录
|
4 |
+
|
5 |
+
- [开始之前:开启后端服务](#开始之前开启后端服务)
|
6 |
+
- [接口功能说明](#接口功能说明)
|
7 |
+
- [cURL 请求示例](#curl-请求示例)
|
8 |
+
- [Python 请求示例](#python-请求示例)
|
9 |
+
- [Python Requests 请求方法](#1️⃣-python-requests-请求方法)
|
10 |
+
- [Python 脚本请求方法](#2️⃣-python-脚本请求方法)
|
11 |
+
- [Java 请求示例](#java-请求示例)
|
12 |
+
- [Javascript 请求示例](#javascript-请求示例)
|
13 |
+
|
14 |
+
## 开始之前:开启后端服务
|
15 |
+
|
16 |
+
在请求 API 之前,请先运行后端服务
|
17 |
+
|
18 |
+
```bash
|
19 |
+
python delopy_api.py
|
20 |
+
```
|
21 |
+
|
22 |
+
<br>
|
23 |
+
|
24 |
+
## 接口功能说明
|
25 |
+
|
26 |
+
### 1.生成证件照(底透明)
|
27 |
+
|
28 |
+
接口名:`idphoto`
|
29 |
+
|
30 |
+
`生成证件照`接口的逻辑是发送一张 RGB 图像,输出一张标准证件照和一张高清证件照:
|
31 |
+
|
32 |
+
- **高清证件照**:根据`size`的宽高比例制作的证件照,文件名为`output_image_dir`增加`_hd`后缀
|
33 |
+
- **标准证件照**:尺寸等于`size`,由高清证件照缩放而来,文件名为`output_image_dir`
|
34 |
+
|
35 |
+
需要注意的是,生成的两张照片都是透明的(RGBA 四通道图像),要生成完整的证件照,还需要下面的`添加背景色`接口。
|
36 |
+
|
37 |
+
> 问:为什么这么设计?
|
38 |
+
> 答:因为在实际产品中,经常用户会频繁切换底色预览效果,直接给透明底图像,由前端 js 代码合成颜色是更好体验的做法。
|
39 |
+
|
40 |
+
### 2.添加背景色
|
41 |
+
|
42 |
+
接口名:`add_background`
|
43 |
+
|
44 |
+
`添加背景色`接口的逻辑是发送一张 RGBA 图像,根据`color`添加背景色,合成一张 JPG 图像。
|
45 |
+
|
46 |
+
### 3.生成六寸排版照
|
47 |
+
|
48 |
+
接口名:`generate_layout_photos`
|
49 |
+
|
50 |
+
`生成六寸排版照`接口的逻辑是发送一张 RGB 图像(一般为添加背景色之后的证件照),根据`size`进行照片排布,然后生成一张六寸排版照。
|
51 |
+
|
52 |
+
<br>
|
53 |
+
|
54 |
+
|
55 |
+
## cURL 请求示例
|
56 |
+
|
57 |
+
cURL 是一个命令行工具,用于使用各种网络协议传输数据。以下是使用 cURL 调用这些 API 的示例。
|
58 |
+
|
59 |
+
### 1. 生成证件照(底透明)
|
60 |
+
|
61 |
+
```bash
|
62 |
+
curl -X POST "http://127.0.0.1:8080/idphoto" \
|
63 |
+
-F "input_image=@demo/images/test.jpg" \
|
64 |
+
-F "height=413" \
|
65 |
+
-F "width=295"
|
66 |
+
```
|
67 |
+
|
68 |
+
### 2. 添加背景色
|
69 |
+
|
70 |
+
```bash
|
71 |
+
curl -X POST "http://127.0.0.1:8080/add_background" \
|
72 |
+
-F "input_image=@test.png" \
|
73 |
+
-F "color=638cce" \
|
74 |
+
-F "kb=200"
|
75 |
+
```
|
76 |
+
|
77 |
+
### 3. 生成六寸排版照
|
78 |
+
|
79 |
+
```bash
|
80 |
+
curl -X POST "http://127.0.0.1:8080/generate_layout_photos" \
|
81 |
+
-F "input_image=@test.jpg" \
|
82 |
+
-F "height=413" \
|
83 |
+
-F "width=295" \
|
84 |
+
-F "kb=200"
|
85 |
+
```
|
86 |
+
|
87 |
+
|
88 |
+
## Python 请求示例
|
89 |
+
|
90 |
+
### 1️⃣ Python Requests 请求方法
|
91 |
+
|
92 |
+
#### 1.生成证件照(底透明)
|
93 |
+
|
94 |
+
```python
|
95 |
+
import requests
|
96 |
+
|
97 |
+
url = "http://127.0.0.1:8080/idphoto"
|
98 |
+
input_image_path = "images/test.jpg"
|
99 |
+
|
100 |
+
files = {"input_image": open(input_image_path, "rb")}
|
101 |
+
data = {"height": 413, "width": 295}
|
102 |
+
|
103 |
+
response = requests.post(url, files=files, data=data).json()
|
104 |
+
|
105 |
+
# response为一个json格式字典,包含status、image_base64_standard和image_base64_hd三项
|
106 |
+
print(response)
|
107 |
+
|
108 |
+
```
|
109 |
+
|
110 |
+
#### 2.添加背景色
|
111 |
+
|
112 |
+
```python
|
113 |
+
import requests
|
114 |
+
|
115 |
+
url = "http://127.0.0.1:8080/add_background"
|
116 |
+
input_image_path = "test.png"
|
117 |
+
|
118 |
+
files = {"input_image": open(input_image_path, "rb")}
|
119 |
+
data = {"color": '638cce', 'kb': None}
|
120 |
+
|
121 |
+
response = requests.post(url, files=files, data=data).json()
|
122 |
+
|
123 |
+
# response为一个json格式字典,包含status和image_base64
|
124 |
+
print(response)
|
125 |
+
```
|
126 |
+
|
127 |
+
#### 3.生成六寸排版照
|
128 |
+
|
129 |
+
```python
|
130 |
+
import requests
|
131 |
+
|
132 |
+
url = "http://127.0.0.1:8080/generate_layout_photos"
|
133 |
+
input_image_path = "test.jpg"
|
134 |
+
|
135 |
+
files = {"input_image": open(input_image_path, "rb")}
|
136 |
+
data = {"height": 413, "width": 295, "kb": 200}
|
137 |
+
|
138 |
+
response = requests.post(url, files=files, data=data).json()
|
139 |
+
|
140 |
+
# response为一个json格式字典,包含status和image_base64
|
141 |
+
print(response)
|
142 |
+
```
|
143 |
+
|
144 |
+
<br>
|
145 |
+
|
146 |
+
### 2️⃣ Python 脚本请求方法
|
147 |
+
|
148 |
+
```bash
|
149 |
+
python requests_api.py -u <URL> -t <TYPE> -i <INPUT_IMAGE_DIR> -o <OUTPUT_IMAGE_DIR> [--height <HEIGHT>] [--width <WIDTH>] [-c <COLOR>] [-k <KB>]
|
150 |
+
```
|
151 |
+
|
152 |
+
#### 参数说明
|
153 |
+
|
154 |
+
##### 基本参数
|
155 |
+
|
156 |
+
- `-u`, `--url`
|
157 |
+
|
158 |
+
- **描述**: API 服务的 URL。
|
159 |
+
- **默认值**: `http://127.0.0.1:8080`
|
160 |
+
|
161 |
+
- `-t`, `--type`
|
162 |
+
|
163 |
+
- **描述**: 请求 API 的种类,可选值有 `idphoto`、`add_background` 和 `generate_layout_photos`。分别代表证件照制作、透明图加背景和排版照生成。
|
164 |
+
- **默认值**: `idphoto`
|
165 |
+
|
166 |
+
- `-i`, `--input_image_dir`
|
167 |
+
|
168 |
+
- **描述**: 输入图像路径。
|
169 |
+
- **必需**: 是
|
170 |
+
- **示例**: `./input_images/photo.jpg`
|
171 |
+
|
172 |
+
- `-o`, `--output_image_dir`
|
173 |
+
- **描述**: 保存图像路径。
|
174 |
+
- **必需**: 是
|
175 |
+
- **示例**: `./output_images/processed_photo.jpg`
|
176 |
+
|
177 |
+
##### 可选参数
|
178 |
+
|
179 |
+
- `--height`,
|
180 |
+
- **描述**: 标准证件照的输出尺寸的高度。
|
181 |
+
- **默认值**: 413
|
182 |
+
- `--width`,
|
183 |
+
|
184 |
+
- **描述**: 标准证件照的输出尺寸的宽度。
|
185 |
+
- **默认值**: 295
|
186 |
+
|
187 |
+
- `-c`, `--color`
|
188 |
+
|
189 |
+
- **描述**: 给透明图增加背景色,格式为 Hex(如#638cce),仅在 type 为`add_background`时生效
|
190 |
+
- **默认值**: `638cce`
|
191 |
+
|
192 |
+
- `-k`, `--kb`
|
193 |
+
- **描述**: 输出照片的 KB 值,仅在 type 为`add_background`和`generate_layout_photos`时生效,值为 None 时不做设置。
|
194 |
+
- **默认值**: `None`
|
195 |
+
- **示例**: `50`
|
196 |
+
|
197 |
+
### 1.生成证件照(底透明)
|
198 |
+
|
199 |
+
```bash
|
200 |
+
python requests_api.py \
|
201 |
+
-u http://127.0.0.1:8080 \
|
202 |
+
-t idphoto \
|
203 |
+
-i ./photo.jpg \
|
204 |
+
-o ./idphoto.png \
|
205 |
+
--height 413 \
|
206 |
+
--width 295
|
207 |
+
```
|
208 |
+
|
209 |
+
### 2.添加背景色
|
210 |
+
|
211 |
+
```bash
|
212 |
+
python requests_api.py \
|
213 |
+
-u http://127.0.0.1:8080 \
|
214 |
+
-t add_background \
|
215 |
+
-i ./idphoto.png \
|
216 |
+
-o ./idphoto_with_background.jpg \
|
217 |
+
-c 638cce \
|
218 |
+
-k 50
|
219 |
+
```
|
220 |
+
|
221 |
+
### 3.生成六寸排版照
|
222 |
+
|
223 |
+
```bash
|
224 |
+
python requests_api.py \
|
225 |
+
-u http://127.0.0.1:8080 \
|
226 |
+
-t generate_layout_photos \
|
227 |
+
-i ./idphoto_with_background.jpg \
|
228 |
+
-o ./layout_photo.jpg \
|
229 |
+
--height 413 \
|
230 |
+
--width 295 \
|
231 |
+
-k 200
|
232 |
+
```
|
233 |
+
|
234 |
+
### 请求失败的情况
|
235 |
+
|
236 |
+
- 照片中检测到的人脸大于 1,则失败
|
237 |
+
|
238 |
+
## Java 请求示例
|
239 |
+
|
240 |
+
### 添加 maven 依赖
|
241 |
+
|
242 |
+
```java
|
243 |
+
<dependency>
|
244 |
+
<groupId>cn.hutool</groupId>
|
245 |
+
<artifactId>hutool-all</artifactId>
|
246 |
+
<version>5.8.16</version>
|
247 |
+
</dependency>
|
248 |
+
|
249 |
+
<dependency>
|
250 |
+
<groupId>commons-io</groupId>
|
251 |
+
<artifactId>commons-io</artifactId>
|
252 |
+
<version>2.6</version>
|
253 |
+
</dependency>
|
254 |
+
```
|
255 |
+
|
256 |
+
### 运行代码
|
257 |
+
|
258 |
+
#### 1.生成证件照(底透明)
|
259 |
+
|
260 |
+
```java
|
261 |
+
/**
|
262 |
+
* 生成证件照(底透明) /idphoto 接口
|
263 |
+
* @param inputImageDir 文件地址
|
264 |
+
* @return
|
265 |
+
* @throws IOException
|
266 |
+
*/
|
267 |
+
public static String requestIdPhoto(String inputImageDir) throws IOException {
|
268 |
+
String url = BASE_URL+"/idphoto";
|
269 |
+
// 创建文件对象
|
270 |
+
File inputFile = new File(inputImageDir);
|
271 |
+
Map<String, Object> paramMap=new HashMap<>();
|
272 |
+
paramMap.put("input_image",inputFile);
|
273 |
+
paramMap.put("height","413");
|
274 |
+
paramMap.put("width","295");
|
275 |
+
//包含status、image_base64_standard和image_base64_hd三项
|
276 |
+
return HttpUtil.post(url, paramMap);
|
277 |
+
}
|
278 |
+
```
|
279 |
+
|
280 |
+
#### 2.添加背景色
|
281 |
+
|
282 |
+
```java
|
283 |
+
/**
|
284 |
+
* 添加背景色 /add_background 接口
|
285 |
+
* @param inputImageDir 文件地址
|
286 |
+
* @return
|
287 |
+
* @throws IOException
|
288 |
+
*/
|
289 |
+
public static String requestAddBackground(String inputImageDir) throws IOException {
|
290 |
+
String url = BASE_URL+"/add_background";
|
291 |
+
// 创建文件对象
|
292 |
+
File inputFile = new File(inputImageDir);
|
293 |
+
Map<String, Object> paramMap=new HashMap<>();
|
294 |
+
paramMap.put("input_image",inputFile);
|
295 |
+
paramMap.put("color","638cce");
|
296 |
+
paramMap.put("kb","200");
|
297 |
+
// response为一个json格式字典,包含status和image_base64
|
298 |
+
return HttpUtil.post(url, paramMap);
|
299 |
+
}
|
300 |
+
```
|
301 |
+
|
302 |
+
#### 3.生成六寸排版照
|
303 |
+
|
304 |
+
```java
|
305 |
+
/**
|
306 |
+
* 生成六寸排版照 /generate_layout_photos 接口
|
307 |
+
* @param inputImageDir 文件地址
|
308 |
+
* @return
|
309 |
+
* @throws IOException
|
310 |
+
*/
|
311 |
+
public static String requestGenerateLayoutPhotos(String inputImageDir) throws IOException {
|
312 |
+
String url = BASE_URL+"/generate_layout_photos";
|
313 |
+
// 创建文件对象
|
314 |
+
File inputFile = new File(inputImageDir);
|
315 |
+
Map<String, Object> paramMap=new HashMap<>();
|
316 |
+
paramMap.put("input_image",inputFile);
|
317 |
+
paramMap.put("height","413");
|
318 |
+
paramMap.put("width","295");
|
319 |
+
paramMap.put("kb","200");
|
320 |
+
//response为一个json格式字典,包含status和image_base64
|
321 |
+
return HttpUtil.post(url, paramMap);
|
322 |
+
}
|
323 |
+
```
|
324 |
+
|
325 |
+
#### 4.汇总
|
326 |
+
|
327 |
+
```java
|
328 |
+
|
329 |
+
import cn.hutool.http.HttpUtil;
|
330 |
+
import cn.hutool.json.JSONObject;
|
331 |
+
import cn.hutool.json.JSONUtil;
|
332 |
+
import org.apache.commons.io.FileUtils;
|
333 |
+
import org.springframework.util.StringUtils;
|
334 |
+
import java.io.File;
|
335 |
+
import java.io.IOException;
|
336 |
+
import java.util.Base64;
|
337 |
+
import java.util.HashMap;
|
338 |
+
import java.util.Map;
|
339 |
+
|
340 |
+
/**
|
341 |
+
* @author: qingshuang
|
342 |
+
* @createDate: 2024/09/05
|
343 |
+
* @description: java生成证件照,测试用例
|
344 |
+
*/
|
345 |
+
public class Test {
|
346 |
+
/**
|
347 |
+
* 接口地址
|
348 |
+
*/
|
349 |
+
private final static String BASE_URL = "http://127.0.0.1:8080";
|
350 |
+
|
351 |
+
/**
|
352 |
+
* 生成证件照(底透明) /idphoto 接口
|
353 |
+
* @param inputImageDir 文件地址
|
354 |
+
* @return
|
355 |
+
* @throws IOException
|
356 |
+
*/
|
357 |
+
public static String requestIdPhoto(String inputImageDir) throws IOException {
|
358 |
+
String url = BASE_URL+"/idphoto";
|
359 |
+
// 创建文件对象
|
360 |
+
File inputFile = new File(inputImageDir);
|
361 |
+
Map<String, Object> paramMap=new HashMap<>();
|
362 |
+
paramMap.put("input_image",inputFile);
|
363 |
+
paramMap.put("height","413");
|
364 |
+
paramMap.put("width","295");
|
365 |
+
return HttpUtil.post(url, paramMap);
|
366 |
+
}
|
367 |
+
/**
|
368 |
+
* 添加背景色 /add_background 接口
|
369 |
+
* @param inputImageDir 文件地址
|
370 |
+
* @return
|
371 |
+
* @throws IOException
|
372 |
+
*/
|
373 |
+
public static String requestAddBackground(String inputImageDir) throws IOException {
|
374 |
+
String url = BASE_URL+"/add_background";
|
375 |
+
// 创建文件对象
|
376 |
+
File inputFile = new File(inputImageDir);
|
377 |
+
Map<String, Object> paramMap=new HashMap<>();
|
378 |
+
paramMap.put("input_image",inputFile);
|
379 |
+
paramMap.put("color","638cce");
|
380 |
+
paramMap.put("kb","200");
|
381 |
+
return HttpUtil.post(url, paramMap);
|
382 |
+
}
|
383 |
+
/**
|
384 |
+
* 生成六寸排版照 /generate_layout_photos 接口
|
385 |
+
* @param inputImageDir 文件地址
|
386 |
+
* @return
|
387 |
+
* @throws IOException
|
388 |
+
*/
|
389 |
+
public static String requestGenerateLayoutPhotos(String inputImageDir) throws IOException {
|
390 |
+
String url = BASE_URL+"/generate_layout_photos";
|
391 |
+
// 创建文件对象
|
392 |
+
File inputFile = new File(inputImageDir);
|
393 |
+
Map<String, Object> paramMap=new HashMap<>();
|
394 |
+
paramMap.put("input_image",inputFile);
|
395 |
+
paramMap.put("height","413");
|
396 |
+
paramMap.put("width","295");
|
397 |
+
paramMap.put("kb","200");
|
398 |
+
return HttpUtil.post(url, paramMap);
|
399 |
+
}
|
400 |
+
/**
|
401 |
+
* 生成证件照(底透明)
|
402 |
+
* @param inputImageDir 源文件地址
|
403 |
+
* @param outputImageDir 输出文件地址
|
404 |
+
* @throws IOException
|
405 |
+
*/
|
406 |
+
private static void requestIdPhotoToImage(String inputImageDir, String outputImageDir) throws IOException {
|
407 |
+
String res =requestIdPhoto(inputImageDir);
|
408 |
+
//转成json
|
409 |
+
JSONObject response= JSONUtil.parseObj(res);
|
410 |
+
if(response.getBool("status")){//请求接口成功
|
411 |
+
String image_base64_standard= response.getStr("image_base64_standard");
|
412 |
+
String image_base64_hd =response.getStr("image_base64_hd");
|
413 |
+
String[] outputImageDirArr= StringUtils.split(outputImageDir,".");
|
414 |
+
// Base64 保存为图片
|
415 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_standard."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64_standard));
|
416 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_hd."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64_hd));
|
417 |
+
}
|
418 |
+
}
|
419 |
+
/**
|
420 |
+
* 添加背景色
|
421 |
+
* @param inputImageDir 源文件地址
|
422 |
+
* @param outputImageDir 输出文件地址
|
423 |
+
* @throws IOException
|
424 |
+
*/
|
425 |
+
private static void requestAddBackgroundToImage(String inputImageDir, String outputImageDir) throws IOException {
|
426 |
+
String res =requestAddBackground(inputImageDir);
|
427 |
+
//转成json
|
428 |
+
JSONObject response= JSONUtil.parseObj(res);
|
429 |
+
if(response.getBool("status")){//请求接口成功
|
430 |
+
String image_base64= response.getStr("image_base64");
|
431 |
+
String[] outputImageDirArr= StringUtils.split(outputImageDir,".");
|
432 |
+
// Base64 保存为图片
|
433 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_background."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64));
|
434 |
+
}
|
435 |
+
}
|
436 |
+
/**
|
437 |
+
* 生成六寸排版照
|
438 |
+
* @param inputImageDir 源文件地址
|
439 |
+
* @param outputImageDir 输出文件地址
|
440 |
+
* @throws IOException
|
441 |
+
*/
|
442 |
+
private static void requestGenerateLayoutPhotosToImage(String inputImageDir, String outputImageDir) throws IOException {
|
443 |
+
String res =requestGenerateLayoutPhotos(inputImageDir);
|
444 |
+
//转成json
|
445 |
+
JSONObject response= JSONUtil.parseObj(res);
|
446 |
+
if(response.getBool("status")){//请求接口成功
|
447 |
+
String image_base64= response.getStr("image_base64");
|
448 |
+
String[] outputImageDirArr= StringUtils.split(outputImageDir,".");
|
449 |
+
// Base64 保存为图片
|
450 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_layout."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64));
|
451 |
+
}
|
452 |
+
}
|
453 |
+
|
454 |
+
public static void main(String[] args) {
|
455 |
+
try {
|
456 |
+
//生成证件照(底透明)
|
457 |
+
requestIdPhotoToImage("C:\\Users\\Administrator\\Desktop\\1111.jpg","C:\\Users\\Administrator\\Desktop\\2222.png");
|
458 |
+
//添加背景色
|
459 |
+
requestAddBackgroundToImage("C:\\Users\\Administrator\\Desktop\\2222_hd.png","C:\\Users\\Administrator\\Desktop\\idphoto_with_background.jpg");
|
460 |
+
//生成六寸排版照
|
461 |
+
requestGenerateLayoutPhotosToImage("C:\\Users\\Administrator\\Desktop\\1111.jpg","C:\\Users\\Administrator\\Desktop\\2222.png");
|
462 |
+
|
463 |
+
} catch (IOException e) {
|
464 |
+
e.printStackTrace();
|
465 |
+
}
|
466 |
+
}
|
467 |
+
}
|
468 |
+
|
469 |
+
```
|
470 |
+
|
471 |
+
## JavaScript 请求示例
|
472 |
+
|
473 |
+
在JavaScript中,我们可以使用`fetch` API来发送HTTP请求。以下是如何使用JavaScript调用这些API的示例。
|
474 |
+
|
475 |
+
### 1. 生成证件照(底透明)
|
476 |
+
|
477 |
+
```javascript
|
478 |
+
async function generateIdPhoto(inputImagePath, height, width) {
|
479 |
+
const url = "http://127.0.0.1:8080/idphoto";
|
480 |
+
const formData = new FormData();
|
481 |
+
formData.append("input_image", new File([await fetch(inputImagePath).then(res => res.blob())], "test.jpg"));
|
482 |
+
formData.append("height", height);
|
483 |
+
formData.append("width", width);
|
484 |
+
|
485 |
+
const response = await fetch(url, {
|
486 |
+
method: 'POST',
|
487 |
+
body: formData
|
488 |
+
});
|
489 |
+
|
490 |
+
const result = await response.json();
|
491 |
+
console.log(result);
|
492 |
+
return result;
|
493 |
+
}
|
494 |
+
|
495 |
+
// 示例调用
|
496 |
+
generateIdPhoto("images/test.jpg", 413, 295).then(response => {
|
497 |
+
console.log(response);
|
498 |
+
});
|
499 |
+
```
|
500 |
+
|
501 |
+
### 2. 添加背景色
|
502 |
+
|
503 |
+
```javascript
|
504 |
+
async function addBackground(inputImagePath, color, kb) {
|
505 |
+
const url = "http://127.0.0.1:8080/add_background";
|
506 |
+
const formData = new FormData();
|
507 |
+
formData.append("input_image", new File([await fetch(inputImagePath).then(res => res.blob())], "test.png"));
|
508 |
+
formData.append("color", color);
|
509 |
+
formData.append("kb", kb);
|
510 |
+
|
511 |
+
const response = await fetch(url, {
|
512 |
+
method: 'POST',
|
513 |
+
body: formData
|
514 |
+
});
|
515 |
+
|
516 |
+
const result = await response.json();
|
517 |
+
console.log(result);
|
518 |
+
return result;
|
519 |
+
}
|
520 |
+
|
521 |
+
// 示例调用
|
522 |
+
addBackground("test.png", "638cce", 200).then(response => {
|
523 |
+
console.log(response);
|
524 |
+
});
|
525 |
+
```
|
526 |
+
|
527 |
+
### 3. 生成六寸排版照
|
528 |
+
|
529 |
+
```javascript
|
530 |
+
async function generateLayoutPhotos(inputImagePath, height, width, kb) {
|
531 |
+
const url = "http://127.0.0.1:8080/generate_layout_photos";
|
532 |
+
const formData = new FormData();
|
533 |
+
formData.append("input_image", new File([await fetch(inputImagePath).then(res => res.blob())], "test.jpg"));
|
534 |
+
formData.append("height", height);
|
535 |
+
formData.append("width", width);
|
536 |
+
formData.append("kb", kb);
|
537 |
+
|
538 |
+
const response = await fetch(url, {
|
539 |
+
method: 'POST',
|
540 |
+
body: formData
|
541 |
+
});
|
542 |
+
|
543 |
+
const result = await response.json();
|
544 |
+
console.log(result);
|
545 |
+
return result;
|
546 |
+
}
|
547 |
+
|
548 |
+
// 示例调用
|
549 |
+
generateLayoutPhotos("test.jpg", 413, 295, 200).then(response => {
|
550 |
+
console.log(response);
|
551 |
+
});
|
552 |
+
```
|
docs/api_EN.md
ADDED
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# API Documentation
|
2 |
+
|
3 |
+
## TOC
|
4 |
+
|
5 |
+
- [Before You Start: Launch the Backend Service](#before-you-start-launch-the-backend-service)
|
6 |
+
- [Interface Function Descriptions](#interface-function-descriptions)
|
7 |
+
- [cURL Request Example](#curl-request-examples)
|
8 |
+
- [Python Request Example](#python-request-example)
|
9 |
+
- [Python Requests Method](#1️⃣-python-requests-method)
|
10 |
+
- [Python Script Method](#2️⃣-python-script-request-method)
|
11 |
+
- [Java Request Example](#java-request-example)
|
12 |
+
- [Javascript Request Example](#javascript-request-examples)
|
13 |
+
|
14 |
+
## Before You Start: Launch the Backend Service
|
15 |
+
|
16 |
+
Before making API requests, please run the backend service:
|
17 |
+
|
18 |
+
```bash
|
19 |
+
python deploy_api.py
|
20 |
+
```
|
21 |
+
|
22 |
+
<br>
|
23 |
+
|
24 |
+
## Interface Function Descriptions
|
25 |
+
|
26 |
+
### 1. Generate ID Photo (Transparent Background)
|
27 |
+
|
28 |
+
Interface Name: `idphoto`
|
29 |
+
|
30 |
+
The `Generate ID Photo` interface logic involves sending an RGB image and receiving a standard ID photo and a high-definition ID photo:
|
31 |
+
|
32 |
+
- **High-Definition ID Photo**: An ID photo made according to the aspect ratio of `size`, with the filename being `output_image_dir` appended with `_hd` suffix.
|
33 |
+
- **Standard ID Photo**: A photo with dimensions equal to `size`, scaled from the high-definition ID photo, with the filename being `output_image_dir`.
|
34 |
+
|
35 |
+
It should be noted that both generated photos are transparent (RGBA four-channel images). To produce a complete ID photo, the following `Add Background Color` interface is also required.
|
36 |
+
|
37 |
+
> Q: Why is this design used?
|
38 |
+
> A: In actual products, users often need to frequently switch background colors to preview effects. Providing a transparent background image and allowing the front-end JavaScript code to synthesize the color offers a better user experience.
|
39 |
+
|
40 |
+
### 2. Add Background Color
|
41 |
+
|
42 |
+
Interface Name: `add_background`
|
43 |
+
|
44 |
+
The `Add Background Color` interface logic involves sending an RGBA image, adding a background color based on `color`, and synthesizing a JPG image.
|
45 |
+
|
46 |
+
### 3. Generate 6-inch Layout Photo
|
47 |
+
|
48 |
+
Interface Name: `generate_layout_photos`
|
49 |
+
|
50 |
+
The `Generate 6-inch Layout Photo` interface logic involves sending an RGB image (usually an ID photo after adding a background color), arranging the photos according to `size`, and then generating a 6-inch layout photo.
|
51 |
+
|
52 |
+
<br>
|
53 |
+
|
54 |
+
|
55 |
+
## cURL Request Examples
|
56 |
+
|
57 |
+
cURL is a command-line tool used to transfer data using various network protocols. Below are examples of how to use cURL to call these APIs.
|
58 |
+
|
59 |
+
### 1. Generate ID Photo (Transparent Background)
|
60 |
+
|
61 |
+
```bash
|
62 |
+
curl -X POST "http://127.0.0.1:8080/idphoto" \
|
63 |
+
-F "input_image=@demo/images/test.jpg" \
|
64 |
+
-F "height=413" \
|
65 |
+
-F "width=295"
|
66 |
+
```
|
67 |
+
|
68 |
+
### 2. Add Background Color
|
69 |
+
|
70 |
+
```bash
|
71 |
+
curl -X POST "http://127.0.0.1:8080/add_background" \
|
72 |
+
-F "input_image=@test.png" \
|
73 |
+
-F "color=638cce" \
|
74 |
+
-F "kb=200"
|
75 |
+
```
|
76 |
+
|
77 |
+
### 3. Generate Six-Inch Layout Photo
|
78 |
+
|
79 |
+
```bash
|
80 |
+
curl -X POST "http://127.0.0.1:8080/generate_layout_photos" \
|
81 |
+
-F "input_image=@test.jpg" \
|
82 |
+
-F "height=413" \
|
83 |
+
-F "width=295" \
|
84 |
+
-F "kb=200"
|
85 |
+
```
|
86 |
+
|
87 |
+
## Python Request Example
|
88 |
+
|
89 |
+
### 1️⃣ Python Requests Method
|
90 |
+
|
91 |
+
#### 1. Generate ID Photo (Transparent Background)
|
92 |
+
|
93 |
+
```python
|
94 |
+
import requests
|
95 |
+
|
96 |
+
url = "http://127.0.0.1:8080/idphoto"
|
97 |
+
input_image_path = "images/test.jpg"
|
98 |
+
|
99 |
+
files = {"input_image": open(input_image_path, "rb")}
|
100 |
+
data = {"height": 413, "width": 295}
|
101 |
+
|
102 |
+
response = requests.post(url, files=files, data=data).json()
|
103 |
+
|
104 |
+
# response is a JSON dictionary containing status, image_base64_standard, and image_base64_hd
|
105 |
+
print(response)
|
106 |
+
```
|
107 |
+
|
108 |
+
#### 2. Add Background Color
|
109 |
+
|
110 |
+
```python
|
111 |
+
import requests
|
112 |
+
|
113 |
+
url = "http://127.0.0.1:8080/add_background"
|
114 |
+
input_image_path = "test.png"
|
115 |
+
|
116 |
+
files = {"input_image": open(input_image_path, "rb")}
|
117 |
+
data = {"color": '638cce', 'kb': None}
|
118 |
+
|
119 |
+
response = requests.post(url, files=files, data=data).json()
|
120 |
+
|
121 |
+
# response is a JSON dictionary containing status and image_base64
|
122 |
+
print(response)
|
123 |
+
```
|
124 |
+
|
125 |
+
#### 3. Generate 6-inch Layout Photo
|
126 |
+
|
127 |
+
```python
|
128 |
+
import requests
|
129 |
+
|
130 |
+
url = "http://127.0.0.1:8080/generate_layout_photos"
|
131 |
+
input_image_path = "test.jpg"
|
132 |
+
|
133 |
+
files = {"input_image": open(input_image_path, "rb")}
|
134 |
+
data = {"height": 413, "width": 295, "kb": 200}
|
135 |
+
|
136 |
+
response = requests.post(url, files=files, data=data).json()
|
137 |
+
|
138 |
+
# response is a JSON dictionary containing status and image_base64
|
139 |
+
print(response)
|
140 |
+
```
|
141 |
+
|
142 |
+
<br>
|
143 |
+
|
144 |
+
### 2️⃣ Python Script Request Method
|
145 |
+
|
146 |
+
```bash
|
147 |
+
python requests_api.py -u <URL> -t <TYPE> -i <INPUT_IMAGE_DIR> -o <OUTPUT_IMAGE_DIR> [--height <HEIGHT>] [--width <WIDTH>] [-c <COLOR>] [-k <KB>]
|
148 |
+
```
|
149 |
+
|
150 |
+
#### Parameter Descriptions
|
151 |
+
|
152 |
+
##### Basic Parameters
|
153 |
+
|
154 |
+
- `-u`, `--url`
|
155 |
+
|
156 |
+
- **Description**: The URL of the API service.
|
157 |
+
- **Default Value**: `http://127.0.0.1:8080`
|
158 |
+
|
159 |
+
- `-t`, `--type`
|
160 |
+
|
161 |
+
- **Description**: The type of API request, with optional values being `idphoto`, `add_background`, and `generate_layout_photos`. They represent ID photo creation, transparent image background addition, and layout photo generation, respectively.
|
162 |
+
- **Default Value**: `idphoto`
|
163 |
+
|
164 |
+
- `-i`, `--input_image_dir`
|
165 |
+
|
166 |
+
- **Description**: The path of the input image.
|
167 |
+
- **Required**: Yes
|
168 |
+
- **Example**: `./input_images/photo.jpg`
|
169 |
+
|
170 |
+
- `-o`, `--output_image_dir`
|
171 |
+
- **Description**: The path to save the image.
|
172 |
+
- **Required**: Yes
|
173 |
+
- **Example**: `./output_images/processed_photo.jpg`
|
174 |
+
|
175 |
+
##### Optional Parameters
|
176 |
+
|
177 |
+
- `--height`
|
178 |
+
|
179 |
+
- **Description**: The height of the output size for the standard ID photo.
|
180 |
+
- **Default Value**: 413
|
181 |
+
|
182 |
+
- `--width`
|
183 |
+
|
184 |
+
- **Description**: The width of the output size for the standard ID photo.
|
185 |
+
- **Default Value**: 295
|
186 |
+
|
187 |
+
- `-c`, `--color`
|
188 |
+
|
189 |
+
- **Description**: Adds a background color to the transparent image, in Hex format (e.g., #638cce), only effective when the type is `add_background`.
|
190 |
+
- **Default Value**: `638cce`
|
191 |
+
|
192 |
+
- `-k`, `--kb`
|
193 |
+
- **Description**: The KB value of the output photo, only effective when the type is `add_background` or `generate_layout_photos`, and no setting is made when the value is None.
|
194 |
+
- **Default Value**: `None`
|
195 |
+
- **Example**: `50`
|
196 |
+
|
197 |
+
#### 1. Generate ID Photo (Transparent Background)
|
198 |
+
|
199 |
+
```bash
|
200 |
+
python requests_api.py \
|
201 |
+
-u http://127.0.0.1:8080 \
|
202 |
+
-t idphoto \
|
203 |
+
-i ./photo.jpg \
|
204 |
+
-o ./idphoto.png \
|
205 |
+
--height 413 \
|
206 |
+
--width 295
|
207 |
+
```
|
208 |
+
|
209 |
+
#### 2. Add Background Color
|
210 |
+
|
211 |
+
```bash
|
212 |
+
python requests_api.py \
|
213 |
+
-u http://127.0.0.1:8080 \
|
214 |
+
-t add_background \
|
215 |
+
-i ./idphoto.png \
|
216 |
+
-o ./idphoto_with_background.jpg \
|
217 |
+
-c 638cce \
|
218 |
+
-k 50
|
219 |
+
```
|
220 |
+
|
221 |
+
#### 3. Generate 6-inch Layout Photo
|
222 |
+
|
223 |
+
```bash
|
224 |
+
python requests_api.py \
|
225 |
+
-u http://127.0.0.1:8080 \
|
226 |
+
-t generate_layout_photos \
|
227 |
+
-i ./idphoto_with_background.jpg \
|
228 |
+
-o ./layout_photo.jpg \
|
229 |
+
--height 413 \
|
230 |
+
--width 295 \
|
231 |
+
-k 200
|
232 |
+
```
|
233 |
+
|
234 |
+
#### Request Failure Scenarios
|
235 |
+
|
236 |
+
- The request fails if more than one face is detected in the photo.
|
237 |
+
|
238 |
+
## Java Request Example
|
239 |
+
|
240 |
+
### Add Maven Dependency
|
241 |
+
|
242 |
+
```java
|
243 |
+
<dependency>
|
244 |
+
<groupId>cn.hutool</groupId>
|
245 |
+
<artifactId>hutool-all</artifactId>
|
246 |
+
<version>5.8.16</version>
|
247 |
+
</dependency>
|
248 |
+
|
249 |
+
<dependency>
|
250 |
+
<groupId>commons-io</groupId>
|
251 |
+
<artifactId>commons-io</artifactId>
|
252 |
+
<version>2.6</version>
|
253 |
+
</dependency>
|
254 |
+
```
|
255 |
+
|
256 |
+
### Running the Code
|
257 |
+
|
258 |
+
#### 1. Generate ID Photo (Transparent Background)
|
259 |
+
|
260 |
+
```java
|
261 |
+
/**
|
262 |
+
* Generate ID Photo (Transparent Background) /idphoto interface
|
263 |
+
* @param inputImageDir File address
|
264 |
+
* @return
|
265 |
+
* @throws IOException
|
266 |
+
*/
|
267 |
+
public static String requestIdPhoto(String inputImageDir) throws IOException {
|
268 |
+
String url = BASE_URL+"/idphoto";
|
269 |
+
// Create file object
|
270 |
+
File inputFile = new File(inputImageDir);
|
271 |
+
Map<String, Object> paramMap=new HashMap<>();
|
272 |
+
paramMap.put("input_image",inputFile);
|
273 |
+
paramMap.put("height","413");
|
274 |
+
paramMap.put("width","295");
|
275 |
+
// Contains status, image_base64_standard, and image_base64_hd
|
276 |
+
return HttpUtil.post(url, paramMap);
|
277 |
+
}
|
278 |
+
```
|
279 |
+
|
280 |
+
#### 2. Add Background Color
|
281 |
+
|
282 |
+
```java
|
283 |
+
/**
|
284 |
+
* Add Background Color /add_background interface
|
285 |
+
* @param inputImageDir File address
|
286 |
+
* @return
|
287 |
+
* @throws IOException
|
288 |
+
*/
|
289 |
+
public static String requestAddBackground(String inputImageDir) throws IOException {
|
290 |
+
String url = BASE_URL+"/add_background";
|
291 |
+
// Create file object
|
292 |
+
File inputFile = new File(inputImageDir);
|
293 |
+
Map<String, Object> paramMap=new HashMap<>();
|
294 |
+
paramMap.put("input_image",inputFile);
|
295 |
+
paramMap.put("color","638cce");
|
296 |
+
paramMap.put("kb","200");
|
297 |
+
// Response is a JSON dictionary containing status and image_base64
|
298 |
+
return HttpUtil.post(url, paramMap);
|
299 |
+
}
|
300 |
+
```
|
301 |
+
|
302 |
+
#### 3. Generate 6-inch Layout Photo
|
303 |
+
|
304 |
+
```java
|
305 |
+
/**
|
306 |
+
* Generate 6-inch Layout Photo /generate_layout_photos interface
|
307 |
+
* @param inputImageDir File address
|
308 |
+
* @return
|
309 |
+
* @throws IOException
|
310 |
+
*/
|
311 |
+
public static String requestGenerateLayoutPhotos(String inputImageDir) throws IOException {
|
312 |
+
String url = BASE_URL+"/generate_layout_photos";
|
313 |
+
// Create file object
|
314 |
+
File inputFile = new File(inputImageDir);
|
315 |
+
Map<String, Object> paramMap=new HashMap<>();
|
316 |
+
paramMap.put("input_image",inputFile);
|
317 |
+
paramMap.put("height","413");
|
318 |
+
paramMap.put("width","295");
|
319 |
+
paramMap.put("kb","200");
|
320 |
+
// Response is a JSON dictionary containing status and image_base64
|
321 |
+
return HttpUtil.post(url, paramMap);
|
322 |
+
}
|
323 |
+
```
|
324 |
+
|
325 |
+
#### 4. Summary
|
326 |
+
|
327 |
+
```java
|
328 |
+
|
329 |
+
import cn.hutool.http.HttpUtil;
|
330 |
+
import cn.hutool.json.JSONObject;
|
331 |
+
import cn.hutool.json.JSONUtil;
|
332 |
+
import org.apache.commons.io.FileUtils;
|
333 |
+
import org.springframework.util.StringUtils;
|
334 |
+
import java.io.File;
|
335 |
+
import java.io.IOException;
|
336 |
+
import java.util.Base64;
|
337 |
+
import java.util.HashMap;
|
338 |
+
import java.util.Map;
|
339 |
+
|
340 |
+
/**
|
341 |
+
* @author: qingshuang
|
342 |
+
* @createDate: 2024/09/05
|
343 |
+
* @description: Java generate ID photo, test case
|
344 |
+
*/
|
345 |
+
public class Test {
|
346 |
+
/**
|
347 |
+
* Interface address
|
348 |
+
*/
|
349 |
+
private final static String BASE_URL = "http://127.0.0.1:8080";
|
350 |
+
|
351 |
+
/**
|
352 |
+
* Generate ID Photo (Transparent Background) /idphoto interface
|
353 |
+
* @param inputImageDir File address
|
354 |
+
* @return
|
355 |
+
* @throws IOException
|
356 |
+
*/
|
357 |
+
public static String requestIdPhoto(String inputImageDir) throws IOException {
|
358 |
+
String url = BASE_URL+"/idphoto";
|
359 |
+
// Create file object
|
360 |
+
File inputFile = new File(inputImageDir);
|
361 |
+
Map<String, Object> paramMap=new HashMap<>();
|
362 |
+
paramMap.put("input_image",inputFile);
|
363 |
+
paramMap.put("height","413");
|
364 |
+
paramMap.put("width","295");
|
365 |
+
return HttpUtil.post(url, paramMap);
|
366 |
+
}
|
367 |
+
/**
|
368 |
+
* Add Background Color /add_background interface
|
369 |
+
* @param inputImageDir File address
|
370 |
+
* @return
|
371 |
+
* @throws IOException
|
372 |
+
*/
|
373 |
+
public static String requestAddBackground(String inputImageDir) throws IOException {
|
374 |
+
String url = BASE_URL+"/add_background";
|
375 |
+
// Create file object
|
376 |
+
File inputFile = new File(inputImageDir);
|
377 |
+
Map<String, Object> paramMap=new HashMap<>();
|
378 |
+
paramMap.put("input_image",inputFile);
|
379 |
+
paramMap.put("color","638cce");
|
380 |
+
paramMap.put("kb","200");
|
381 |
+
return HttpUtil.post(url, paramMap);
|
382 |
+
}
|
383 |
+
/**
|
384 |
+
* Generate 6-inch Layout Photo /generate_layout_photos interface
|
385 |
+
* @param inputImageDir File address
|
386 |
+
* @return
|
387 |
+
* @throws IOException
|
388 |
+
*/
|
389 |
+
public static String requestGenerateLayoutPhotos(String inputImageDir) throws IOException {
|
390 |
+
String url = BASE_URL+"/generate_layout_photos";
|
391 |
+
// Create file object
|
392 |
+
File inputFile = new File(inputImageDir);
|
393 |
+
Map<String, Object> paramMap=new HashMap<>();
|
394 |
+
paramMap.put("input_image",inputFile);
|
395 |
+
paramMap.put("height","413");
|
396 |
+
paramMap.put("width","295");
|
397 |
+
paramMap.put("kb","200");
|
398 |
+
return HttpUtil.post(url, paramMap);
|
399 |
+
}
|
400 |
+
/**
|
401 |
+
* Generate ID Photo (Transparent Background)
|
402 |
+
* @param inputImageDir Source file address
|
403 |
+
* @param outputImageDir Output file address
|
404 |
+
* @throws IOException
|
405 |
+
*/
|
406 |
+
private static void requestIdPhotoToImage(String inputImageDir, String outputImageDir) throws IOException {
|
407 |
+
String res =requestIdPhoto(inputImageDir);
|
408 |
+
// Convert to JSON
|
409 |
+
JSONObject response= JSONUtil.parseObj(res);
|
410 |
+
if(response.getBool("status")){// Request interface success
|
411 |
+
String image_base64_standard= response.getStr("image_base64_standard");
|
412 |
+
String image_base64_hd =response.getStr("image_base64_hd");
|
413 |
+
String[] outputImageDirArr= StringUtils.split(outputImageDir,".");
|
414 |
+
// Base64 save as image
|
415 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_standard."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64_standard));
|
416 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_hd."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64_hd));
|
417 |
+
}
|
418 |
+
}
|
419 |
+
/**
|
420 |
+
* Add Background Color
|
421 |
+
* @param inputImageDir Source file address
|
422 |
+
* @param outputImageDir Output file address
|
423 |
+
* @throws IOException
|
424 |
+
*/
|
425 |
+
private static void requestAddBackgroundToImage(String inputImageDir, String outputImageDir) throws IOException {
|
426 |
+
String res =requestAddBackground(inputImageDir);
|
427 |
+
// Convert to JSON
|
428 |
+
JSONObject response= JSONUtil.parseObj(res);
|
429 |
+
if(response.getBool("status")){// Request interface success
|
430 |
+
String image_base64= response.getStr("image_base64");
|
431 |
+
String[] outputImageDirArr= StringUtils.split(outputImageDir,".");
|
432 |
+
// Base64 save as image
|
433 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_background."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64));
|
434 |
+
}
|
435 |
+
}
|
436 |
+
/**
|
437 |
+
* Generate 6-inch Layout Photo
|
438 |
+
* @param inputImageDir Source file address
|
439 |
+
* @param outputImageDir Output file address
|
440 |
+
* @throws IOException
|
441 |
+
*/
|
442 |
+
private static void requestGenerateLayoutPhotosToImage(String inputImageDir, String outputImageDir) throws IOException {
|
443 |
+
String res =requestGenerateLayoutPhotos(inputImageDir);
|
444 |
+
// Convert to JSON
|
445 |
+
JSONObject response= JSONUtil.parseObj(res);
|
446 |
+
if(response.getBool("status")){// Request interface success
|
447 |
+
String image_base64= response.getStr("image_base64");
|
448 |
+
String[] outputImageDirArr= StringUtils.split(outputImageDir,".");
|
449 |
+
// Base64 save as image
|
450 |
+
FileUtils.writeByteArrayToFile(new File(outputImageDirArr[0]+"_layout."+outputImageDirArr[1]), Base64.getDecoder().decode(image_base64));
|
451 |
+
}
|
452 |
+
}
|
453 |
+
|
454 |
+
public static void main(String[] args) {
|
455 |
+
try {
|
456 |
+
// Generate ID Photo (Transparent Background)
|
457 |
+
requestIdPhotoToImage("C:\\Users\\Administrator\\Desktop\\1111.jpg","C:\\Users\\Administrator\\Desktop\\2222.png");
|
458 |
+
// Add Background Color
|
459 |
+
requestAddBackgroundToImage("C:\\Users\\Administrator\\Desktop\\2222_hd.png","C:\\Users\\Administrator\\Desktop\\idphoto_with_background.jpg");
|
460 |
+
// Generate 6-inch Layout Photo
|
461 |
+
requestGenerateLayoutPhotosToImage("C:\\Users\\Administrator\\Desktop\\1111.jpg","C:\\Users\\Administrator\\Desktop\\2222.png");
|
462 |
+
|
463 |
+
} catch (IOException e) {
|
464 |
+
e.printStackTrace();
|
465 |
+
}
|
466 |
+
}
|
467 |
+
}
|
468 |
+
```
|
469 |
+
|
470 |
+
## JavaScript Request Examples
|
471 |
+
|
472 |
+
In JavaScript, we can use the `fetch` API to send HTTP requests. Below are examples of how to call these APIs using JavaScript.
|
473 |
+
|
474 |
+
### 1. Generate ID Photo (Transparent Background)
|
475 |
+
|
476 |
+
```javascript
|
477 |
+
async function generateIdPhoto(inputImagePath, height, width) {
|
478 |
+
const url = "http://127.0.0.1:8080/idphoto";
|
479 |
+
const formData = new FormData();
|
480 |
+
formData.append("input_image", new File([await fetch(inputImagePath).then(res => res.blob())], "test.jpg"));
|
481 |
+
formData.append("height", height);
|
482 |
+
formData.append("width", width);
|
483 |
+
|
484 |
+
const response = await fetch(url, {
|
485 |
+
method: 'POST',
|
486 |
+
body: formData
|
487 |
+
});
|
488 |
+
|
489 |
+
const result = await response.json();
|
490 |
+
console.log(result);
|
491 |
+
return result;
|
492 |
+
}
|
493 |
+
|
494 |
+
// Example call
|
495 |
+
generateIdPhoto("images/test.jpg", 413, 295).then(response => {
|
496 |
+
console.log(response);
|
497 |
+
});
|
498 |
+
```
|
499 |
+
|
500 |
+
### 2. Add Background Color
|
501 |
+
|
502 |
+
```javascript
|
503 |
+
async function addBackground(inputImagePath, color, kb) {
|
504 |
+
const url = "http://127.0.0.1:8080/add_background";
|
505 |
+
const formData = new FormData();
|
506 |
+
formData.append("input_image", new File([await fetch(inputImagePath).then(res => res.blob())], "test.png"));
|
507 |
+
formData.append("color", color);
|
508 |
+
formData.append("kb", kb);
|
509 |
+
|
510 |
+
const response = await fetch(url, {
|
511 |
+
method: 'POST',
|
512 |
+
body: formData
|
513 |
+
});
|
514 |
+
|
515 |
+
const result = await response.json();
|
516 |
+
console.log(result);
|
517 |
+
return result;
|
518 |
+
}
|
519 |
+
|
520 |
+
// Example call
|
521 |
+
addBackground("test.png", "638cce", 200).then(response => {
|
522 |
+
console.log(response);
|
523 |
+
});
|
524 |
+
```
|
525 |
+
|
526 |
+
### 3. Generate Six-Inch Layout Photo
|
527 |
+
|
528 |
+
```javascript
|
529 |
+
async function generateLayoutPhotos(inputImagePath, height, width, kb) {
|
530 |
+
const url = "http://127.0.0.1:8080/generate_layout_photos";
|
531 |
+
const formData = new FormData();
|
532 |
+
formData.append("input_image", new File([await fetch(inputImagePath).then(res => res.blob())], "test.jpg"));
|
533 |
+
formData.append("height", height);
|
534 |
+
formData.append("width", width);
|
535 |
+
formData.append("kb", kb);
|
536 |
+
|
537 |
+
const response = await fetch(url, {
|
538 |
+
method: 'POST',
|
539 |
+
body: formData
|
540 |
+
});
|
541 |
+
|
542 |
+
const result = await response.json();
|
543 |
+
console.log(result);
|
544 |
+
return result;
|
545 |
+
}
|
546 |
+
|
547 |
+
// Example call
|
548 |
+
generateLayoutPhotos("test.jpg", 413, 295, 200).then(response => {
|
549 |
+
console.log(response);
|
550 |
+
});
|
551 |
+
```
|
hivision/__init__.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .creator import IDCreator, Params as IDParams, Result as IDResult
|
2 |
+
|
3 |
+
|
4 |
+
__all__ = ["IDCreator", "IDParams", "IDResult", "utils", "error"]
|
hivision/creator/__init__.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
16 |
+
from .photo_adjuster import adjust_photo
|
17 |
+
|
18 |
+
|
19 |
+
class IDCreator:
|
20 |
+
"""
|
21 |
+
证件照创建类,包含完整的证件照流程
|
22 |
+
"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
# 回调时机
|
26 |
+
self.before_all: ContextHandler = None
|
27 |
+
"""
|
28 |
+
在所有处理之前,此时图像已经被 resize 到最大边长为 2000
|
29 |
+
"""
|
30 |
+
self.after_matting: ContextHandler = None
|
31 |
+
"""
|
32 |
+
在抠图之后,ctx.matting_image 被赋值
|
33 |
+
"""
|
34 |
+
self.after_detect: ContextHandler = None
|
35 |
+
"""
|
36 |
+
在人脸检测之后,ctx.face 被赋值,如果为仅换底,则不会执行此回调
|
37 |
+
"""
|
38 |
+
self.after_all: ContextHandler = None
|
39 |
+
"""
|
40 |
+
在所有处理之后,此时 ctx.result 被赋值
|
41 |
+
"""
|
42 |
+
# 处理者
|
43 |
+
self.matting_handler: ContextHandler = extract_human
|
44 |
+
self.detection_handler: ContextHandler = detect_face
|
45 |
+
# 上下文
|
46 |
+
self.ctx = None
|
47 |
+
|
48 |
+
def __call__(
|
49 |
+
self,
|
50 |
+
image: np.ndarray,
|
51 |
+
size: Tuple[int, int] = (413, 295),
|
52 |
+
change_bg_only: bool = False,
|
53 |
+
head_measure_ratio: float = 0.2,
|
54 |
+
head_height_ratio: float = 0.45,
|
55 |
+
head_top_range: float = (0.12, 0.1),
|
56 |
+
) -> Result:
|
57 |
+
"""
|
58 |
+
证件照处理函数
|
59 |
+
:param image: 输入图像
|
60 |
+
:param change_bg_only: 是否只需要换底
|
61 |
+
:param size: 输出的图像大小(h,w)
|
62 |
+
:param head_measure_ratio: 人脸面积与全图面积的期望比值
|
63 |
+
:param head_height_ratio: 人脸中心处在全图高度的比例期望值
|
64 |
+
:param head_top_range: 头距离顶部的比例(max,min)
|
65 |
+
|
66 |
+
:return: 返回处理后的证件照和一系列参数
|
67 |
+
"""
|
68 |
+
# 0.初始化上下文
|
69 |
+
params = Params(
|
70 |
+
size=size,
|
71 |
+
change_bg_only=change_bg_only,
|
72 |
+
head_measure_ratio=head_measure_ratio,
|
73 |
+
head_height_ratio=head_height_ratio,
|
74 |
+
head_top_range=head_top_range,
|
75 |
+
)
|
76 |
+
self.ctx = Context(params)
|
77 |
+
ctx = self.ctx
|
78 |
+
ctx.processing_image = image
|
79 |
+
ctx.processing_image = U.resize_image_esp(
|
80 |
+
ctx.processing_image, 2000
|
81 |
+
) # 将输入图片 resize 到最大边长为 2000
|
82 |
+
ctx.origin_image = ctx.processing_image.copy()
|
83 |
+
self.before_all and self.before_all(ctx)
|
84 |
+
# 1. 人像抠图
|
85 |
+
self.matting_handler(ctx)
|
86 |
+
self.after_matting and self.after_matting(ctx)
|
87 |
+
if ctx.params.change_bg_only:
|
88 |
+
ctx.result = Result(
|
89 |
+
standard=ctx.matting_image,
|
90 |
+
hd=ctx.matting_image,
|
91 |
+
clothing_params=None,
|
92 |
+
typography_params=None,
|
93 |
+
)
|
94 |
+
self.after_all and self.after_all(ctx)
|
95 |
+
return ctx.result
|
96 |
+
# 2. 人脸检测
|
97 |
+
self.detection_handler(ctx)
|
98 |
+
self.after_detect and self.after_detect(ctx)
|
99 |
+
# 3. 图像调整
|
100 |
+
result_image_hd, result_image_standard, clothing_params, typography_params = (
|
101 |
+
adjust_photo(ctx)
|
102 |
+
)
|
103 |
+
ctx.result = Result(
|
104 |
+
standard=result_image_standard,
|
105 |
+
hd=result_image_hd,
|
106 |
+
clothing_params=clothing_params,
|
107 |
+
typography_params=typography_params,
|
108 |
+
)
|
109 |
+
self.after_all and self.after_all(ctx)
|
110 |
+
return ctx.result
|
hivision/creator/context.py
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
head_measure_ratio: float = 0.2,
|
20 |
+
head_height_ratio: float = 0.45,
|
21 |
+
head_top_range: float = (0.12, 0.1),
|
22 |
+
):
|
23 |
+
self.__size = size
|
24 |
+
self.__change_bg_only = change_bg_only
|
25 |
+
self.__head_measure_ratio = head_measure_ratio
|
26 |
+
self.__head_height_ratio = head_height_ratio
|
27 |
+
self.__head_top_range = head_top_range
|
28 |
+
|
29 |
+
@property
|
30 |
+
def size(self):
|
31 |
+
return self.__size
|
32 |
+
|
33 |
+
@property
|
34 |
+
def change_bg_only(self):
|
35 |
+
return self.__change_bg_only
|
36 |
+
|
37 |
+
@property
|
38 |
+
def head_measure_ratio(self):
|
39 |
+
return self.__head_measure_ratio
|
40 |
+
|
41 |
+
@property
|
42 |
+
def head_height_ratio(self):
|
43 |
+
return self.__head_height_ratio
|
44 |
+
|
45 |
+
@property
|
46 |
+
def head_top_range(self):
|
47 |
+
return self.__head_top_range
|
48 |
+
|
49 |
+
|
50 |
+
class Result:
|
51 |
+
def __init__(
|
52 |
+
self,
|
53 |
+
standard: np.ndarray,
|
54 |
+
hd: np.ndarray,
|
55 |
+
clothing_params: Optional[dict],
|
56 |
+
typography_params: Optional[dict],
|
57 |
+
):
|
58 |
+
self.standard = standard
|
59 |
+
self.hd = hd
|
60 |
+
self.clothing_params = clothing_params
|
61 |
+
"""
|
62 |
+
服装参数,仅换底时为 None
|
63 |
+
"""
|
64 |
+
self.typography_params = typography_params
|
65 |
+
"""
|
66 |
+
排版参数,仅换底时为 None
|
67 |
+
"""
|
68 |
+
|
69 |
+
def __iter__(self):
|
70 |
+
return iter(
|
71 |
+
[self.standard, self.hd, self.clothing_params, self.typography_params]
|
72 |
+
)
|
73 |
+
|
74 |
+
|
75 |
+
class Context:
|
76 |
+
def __init__(self, params: Params):
|
77 |
+
self.params: Params = params
|
78 |
+
"""
|
79 |
+
证件照处理参数
|
80 |
+
"""
|
81 |
+
self.origin_image: Optional[np.ndarray] = None
|
82 |
+
"""
|
83 |
+
输入的原始图像,处理时会进行resize,长宽不一定等于输入图像
|
84 |
+
"""
|
85 |
+
self.processing_image: Optional[np.ndarray] = None
|
86 |
+
"""
|
87 |
+
当前正在处理的图像
|
88 |
+
"""
|
89 |
+
self.matting_image: Optional[np.ndarray] = None
|
90 |
+
"""
|
91 |
+
人像抠图结果
|
92 |
+
"""
|
93 |
+
self.face: Optional[Tuple[int, int, int, int, float]] = None
|
94 |
+
"""
|
95 |
+
人脸检测结果,大于一个人脸时已在上层抛出异常
|
96 |
+
元组长度为5,包含 x1, y1, x2, y2, score 的坐标, (x1, y1)为左上角坐标,(x2, y2)为右下角坐标, score为置信度, 最大值为1
|
97 |
+
"""
|
98 |
+
self.result: Optional[Result] = None
|
99 |
+
"""
|
100 |
+
证件照处理结果
|
101 |
+
"""
|
102 |
+
|
103 |
+
|
104 |
+
ContextHandler = Optional[Callable[[Context], None]]
|
hivision/creator/face_detector.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from mtcnnruntime import MTCNN
|
11 |
+
from .context import Context
|
12 |
+
from hivision.error import FaceError
|
13 |
+
import cv2
|
14 |
+
|
15 |
+
mtcnn = None
|
16 |
+
|
17 |
+
|
18 |
+
def detect_face(ctx: Context, scale: int = 2):
|
19 |
+
"""
|
20 |
+
人脸检测处理者,只进行人脸数量的检测
|
21 |
+
:param ctx: 上下文,此时已获取到原始图和抠图结果,但是我们只需要原始图
|
22 |
+
:param scale: 最大边长缩放比例,原图:缩放图 = 1:scale
|
23 |
+
:raise FaceError: 人脸检测错误,多个人脸或者没有人脸
|
24 |
+
"""
|
25 |
+
global mtcnn
|
26 |
+
if mtcnn is None:
|
27 |
+
mtcnn = MTCNN()
|
28 |
+
image = cv2.resize(
|
29 |
+
ctx.origin_image,
|
30 |
+
(ctx.origin_image.shape[1] // scale, ctx.origin_image.shape[0] // scale),
|
31 |
+
interpolation=cv2.INTER_AREA,
|
32 |
+
)
|
33 |
+
faces, _ = mtcnn.detect(image)
|
34 |
+
if len(faces) != 1:
|
35 |
+
# 保险措施,如果检测到多个人脸或者没有人脸,用原图再检测一次
|
36 |
+
faces, _ = mtcnn.detect(ctx.origin_image)
|
37 |
+
else:
|
38 |
+
for item, param in enumerate(faces[0]):
|
39 |
+
faces[0][item] = param * 2
|
40 |
+
if len(faces) != 1:
|
41 |
+
raise FaceError("Expected 1 face, but got {}".format(len(faces)), len(faces))
|
42 |
+
ctx.face = (faces[0][0], faces[0][1], faces[0][2], faces[0][3], faces[0][4])
|
hivision/creator/human_matting.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
|
18 |
+
weight_path = os.path.join(os.path.dirname(__file__), "weights", "hivision_modnet.onnx")
|
19 |
+
|
20 |
+
|
21 |
+
def extract_human(ctx: Context):
|
22 |
+
"""
|
23 |
+
人像抠图
|
24 |
+
:param ctx: 上下文
|
25 |
+
"""
|
26 |
+
# 抠图
|
27 |
+
matting_image = get_modnet_matting(ctx.processing_image, weight_path)
|
28 |
+
# 修复抠图
|
29 |
+
ctx.processing_image = hollow_out_fix(matting_image)
|
30 |
+
ctx.matting_image = ctx.processing_image.copy()
|
31 |
+
|
32 |
+
|
33 |
+
def hollow_out_fix(src: np.ndarray) -> np.ndarray:
|
34 |
+
"""
|
35 |
+
修补抠图区域,作为抠图模型精度不够的补充
|
36 |
+
:param src:
|
37 |
+
:return:
|
38 |
+
"""
|
39 |
+
b, g, r, a = cv2.split(src)
|
40 |
+
src_bgr = cv2.merge((b, g, r))
|
41 |
+
# -----------padding---------- #
|
42 |
+
add_area = np.zeros((10, a.shape[1]), np.uint8)
|
43 |
+
a = np.vstack((add_area, a, add_area))
|
44 |
+
add_area = np.zeros((a.shape[0], 10), np.uint8)
|
45 |
+
a = np.hstack((add_area, a, add_area))
|
46 |
+
# -------------end------------ #
|
47 |
+
_, a_threshold = cv2.threshold(a, 127, 255, 0)
|
48 |
+
a_erode = cv2.erode(
|
49 |
+
a_threshold,
|
50 |
+
kernel=cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)),
|
51 |
+
iterations=3,
|
52 |
+
)
|
53 |
+
contours, hierarchy = cv2.findContours(
|
54 |
+
a_erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
|
55 |
+
)
|
56 |
+
contours = [x for x in contours]
|
57 |
+
# contours = np.squeeze(contours)
|
58 |
+
contours.sort(key=lambda c: cv2.contourArea(c), reverse=True)
|
59 |
+
a_contour = cv2.drawContours(np.zeros(a.shape, np.uint8), contours[0], -1, 255, 2)
|
60 |
+
# a_base = a_contour[1:-1, 1:-1]
|
61 |
+
h, w = a.shape[:2]
|
62 |
+
mask = np.zeros(
|
63 |
+
[h + 2, w + 2], np.uint8
|
64 |
+
) # mask 必须行和列都加 2,且必须为 uint8 单通道阵列
|
65 |
+
cv2.floodFill(a_contour, mask=mask, seedPoint=(0, 0), newVal=255)
|
66 |
+
a = cv2.add(a, 255 - a_contour)
|
67 |
+
return cv2.merge((src_bgr, a[10:-10, 10:-10]))
|
68 |
+
|
69 |
+
|
70 |
+
def image2bgr(input_image):
|
71 |
+
if len(input_image.shape) == 2:
|
72 |
+
input_image = input_image[:, :, None]
|
73 |
+
if input_image.shape[2] == 1:
|
74 |
+
result_image = np.repeat(input_image, 3, axis=2)
|
75 |
+
elif input_image.shape[2] == 4:
|
76 |
+
result_image = input_image[:, :, 0:3]
|
77 |
+
else:
|
78 |
+
result_image = input_image
|
79 |
+
|
80 |
+
return result_image
|
81 |
+
|
82 |
+
|
83 |
+
def read_modnet_image(input_image, ref_size=512):
|
84 |
+
im = Image.fromarray(np.uint8(input_image))
|
85 |
+
width, length = im.size[0], im.size[1]
|
86 |
+
im = np.asarray(im)
|
87 |
+
im = image2bgr(im)
|
88 |
+
im = cv2.resize(im, (ref_size, ref_size), interpolation=cv2.INTER_AREA)
|
89 |
+
im = NNormalize(im, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]))
|
90 |
+
im = NUnsqueeze(NTo_Tensor(im))
|
91 |
+
|
92 |
+
return im, width, length
|
93 |
+
|
94 |
+
|
95 |
+
sess = None
|
96 |
+
|
97 |
+
|
98 |
+
def get_modnet_matting(input_image, checkpoint_path, ref_size=512):
|
99 |
+
global sess
|
100 |
+
if sess is None:
|
101 |
+
sess = onnxruntime.InferenceSession(checkpoint_path)
|
102 |
+
|
103 |
+
input_name = sess.get_inputs()[0].name
|
104 |
+
output_name = sess.get_outputs()[0].name
|
105 |
+
|
106 |
+
im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
|
107 |
+
|
108 |
+
matte = sess.run([output_name], {input_name: im})
|
109 |
+
matte = (matte[0] * 255).astype("uint8")
|
110 |
+
matte = np.squeeze(matte)
|
111 |
+
mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
|
112 |
+
b, g, r = cv2.split(np.uint8(input_image))
|
113 |
+
|
114 |
+
output_image = cv2.merge((b, g, r, mask))
|
115 |
+
|
116 |
+
return output_image
|
src/layoutCreate.py → hivision/creator/layout_calculator.py
RENAMED
@@ -1,47 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import cv2.detail
|
2 |
import numpy as np
|
3 |
|
4 |
-
|
5 |
-
|
6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
# 1.不转置排列的情况下:
|
9 |
layout_col_no_transpose = 0 # 行
|
10 |
layout_row_no_transpose = 0 # 列
|
11 |
for i in range(1, 4):
|
12 |
-
centerBlockHeight_temp = input_height * i + PHOTO_INTERVAL_H * (i-1)
|
13 |
if centerBlockHeight_temp < LIMIT_BLOCK_H:
|
14 |
centerBlockHeight_1 = centerBlockHeight_temp
|
15 |
layout_row_no_transpose = i
|
16 |
else:
|
17 |
break
|
18 |
for j in range(1, 9):
|
19 |
-
centerBlockWidth_temp = input_width * j + PHOTO_INTERVAL_W * (j-1)
|
20 |
if centerBlockWidth_temp < LIMIT_BLOCK_W:
|
21 |
centerBlockWidth_1 = centerBlockWidth_temp
|
22 |
layout_col_no_transpose = j
|
23 |
else:
|
24 |
break
|
25 |
-
layout_number_no_transpose = layout_row_no_transpose*layout_col_no_transpose
|
26 |
|
27 |
# 2.转置排列的情况下:
|
28 |
layout_col_transpose = 0 # 行
|
29 |
layout_row_transpose = 0 # 列
|
30 |
for i in range(1, 4):
|
31 |
-
centerBlockHeight_temp = input_width * i + PHOTO_INTERVAL_H * (i-1)
|
32 |
if centerBlockHeight_temp < LIMIT_BLOCK_H:
|
33 |
centerBlockHeight_2 = centerBlockHeight_temp
|
34 |
layout_row_transpose = i
|
35 |
else:
|
36 |
break
|
37 |
for j in range(1, 9):
|
38 |
-
centerBlockWidth_temp = input_height * j + PHOTO_INTERVAL_W * (j-1)
|
39 |
if centerBlockWidth_temp < LIMIT_BLOCK_W:
|
40 |
centerBlockWidth_2 = centerBlockWidth_temp
|
41 |
layout_col_transpose = j
|
42 |
else:
|
43 |
break
|
44 |
-
layout_number_transpose = layout_row_transpose*layout_col_transpose
|
45 |
|
46 |
if layout_number_transpose > layout_number_no_transpose:
|
47 |
layout_mode = (layout_col_transpose, layout_row_transpose, 2)
|
@@ -59,19 +83,25 @@ def generate_layout_photo(input_height, input_width):
|
|
59 |
PHOTO_INTERVAL_W = 30 # 证件照与证件照之间的水平距离
|
60 |
SIDES_INTERVAL_H = 50 # 证件照与画布边缘的垂直距离
|
61 |
SIDES_INTERVAL_W = 70 # 证件照与画布边缘的水平距离
|
62 |
-
LIMIT_BLOCK_W = LAYOUT_WIDTH - 2*SIDES_INTERVAL_W
|
63 |
-
LIMIT_BLOCK_H = LAYOUT_HEIGHT - 2*SIDES_INTERVAL_H
|
64 |
|
65 |
-
# 2.创建一个1180x1746的空白画布
|
66 |
white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
|
67 |
white_background.fill(255)
|
68 |
|
69 |
-
# 3.计算照片的layout(列、行、横竖朝向),证件照组成的中心区块的分辨率
|
70 |
-
layout_mode, centerBlockWidth, centerBlockHeight = judge_layout(
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
# 4.开始排列组合
|
73 |
-
x11 = (LAYOUT_WIDTH - centerBlockWidth)//2
|
74 |
-
y11 = (LAYOUT_HEIGHT - centerBlockHeight)//2
|
75 |
typography_arr = []
|
76 |
typography_rotate = False
|
77 |
if layout_mode[2] == 2:
|
@@ -80,13 +110,16 @@ def generate_layout_photo(input_height, input_width):
|
|
80 |
|
81 |
for j in range(layout_mode[1]):
|
82 |
for i in range(layout_mode[0]):
|
83 |
-
xi = x11 + i*input_width + i*PHOTO_INTERVAL_W
|
84 |
-
yi = y11 + j*input_height + j*PHOTO_INTERVAL_H
|
85 |
typography_arr.append([xi, yi])
|
86 |
|
87 |
return typography_arr, typography_rotate
|
88 |
|
89 |
-
|
|
|
|
|
|
|
90 |
LAYOUT_WIDTH = 1746
|
91 |
LAYOUT_HEIGHT = 1180
|
92 |
white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
|
@@ -98,16 +131,8 @@ def generate_layout_image(input_image, typography_arr, typography_rotate, width=
|
|
98 |
height, width = width, height
|
99 |
for arr in typography_arr:
|
100 |
locate_x, locate_y = arr[0], arr[1]
|
101 |
-
white_background[locate_y:locate_y+height, locate_x:locate_x+width] =
|
|
|
|
|
102 |
|
103 |
return white_background
|
104 |
-
|
105 |
-
|
106 |
-
if __name__ == "__main__":
|
107 |
-
typography_arr, typography_rotate = generate_layout_photo(input_height=413, input_width=295)
|
108 |
-
print("typography_arr:", typography_arr)
|
109 |
-
print("typography_rotate:", typography_rotate)
|
110 |
-
result_image = generate_layout_image(cv2.imread("./32.jpg"), typography_arr, typography_rotate, width=295, height=413)
|
111 |
-
cv2.imwrite("./result_image.jpg", result_image)
|
112 |
-
|
113 |
-
|
|
|
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)
|
|
|
83 |
PHOTO_INTERVAL_W = 30 # 证件照与证件照之间的水平距离
|
84 |
SIDES_INTERVAL_H = 50 # 证件照与画布边缘的垂直距离
|
85 |
SIDES_INTERVAL_W = 70 # 证件照与画布边缘的水平距离
|
86 |
+
LIMIT_BLOCK_W = LAYOUT_WIDTH - 2 * SIDES_INTERVAL_W
|
87 |
+
LIMIT_BLOCK_H = LAYOUT_HEIGHT - 2 * SIDES_INTERVAL_H
|
88 |
|
89 |
+
# 2.创建一个 1180x1746 的空白画布
|
90 |
white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
|
91 |
white_background.fill(255)
|
92 |
|
93 |
+
# 3.计算照片的 layout(列、行、横竖朝向),证件照组成的中心区块的分辨率
|
94 |
+
layout_mode, centerBlockWidth, centerBlockHeight = judge_layout(
|
95 |
+
input_width,
|
96 |
+
input_height,
|
97 |
+
PHOTO_INTERVAL_W,
|
98 |
+
PHOTO_INTERVAL_H,
|
99 |
+
LIMIT_BLOCK_W,
|
100 |
+
LIMIT_BLOCK_H,
|
101 |
+
)
|
102 |
# 4.开始排列组合
|
103 |
+
x11 = (LAYOUT_WIDTH - centerBlockWidth) // 2
|
104 |
+
y11 = (LAYOUT_HEIGHT - centerBlockHeight) // 2
|
105 |
typography_arr = []
|
106 |
typography_rotate = False
|
107 |
if layout_mode[2] == 2:
|
|
|
110 |
|
111 |
for j in range(layout_mode[1]):
|
112 |
for i in range(layout_mode[0]):
|
113 |
+
xi = x11 + i * input_width + i * PHOTO_INTERVAL_W
|
114 |
+
yi = y11 + j * input_height + j * PHOTO_INTERVAL_H
|
115 |
typography_arr.append([xi, yi])
|
116 |
|
117 |
return typography_arr, typography_rotate
|
118 |
|
119 |
+
|
120 |
+
def generate_layout_image(
|
121 |
+
input_image, typography_arr, typography_rotate, width=295, height=413
|
122 |
+
):
|
123 |
LAYOUT_WIDTH = 1746
|
124 |
LAYOUT_HEIGHT = 1180
|
125 |
white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
|
|
|
131 |
height, width = width, height
|
132 |
for arr in typography_arr:
|
133 |
locate_x, locate_y = arr[0], arr[1]
|
134 |
+
white_background[locate_y : locate_y + height, locate_x : locate_x + width] = (
|
135 |
+
input_image
|
136 |
+
)
|
137 |
|
138 |
return white_background
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{src → hivision/creator}/move_image.py
RENAMED
@@ -1,6 +1,6 @@
|
|
1 |
"""
|
2 |
-
有一些png图像下部也会有一些透明的区域,使得图像无法对其底部边框
|
3 |
-
本程序实现移动图像,使其下部与png图像实际大小相对齐
|
4 |
"""
|
5 |
import os
|
6 |
import cv2
|
@@ -16,7 +16,7 @@ def merge(boxes):
|
|
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
|
@@ -33,14 +33,14 @@ def merge(boxes):
|
|
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
|
@@ -48,14 +48,14 @@ def get_box(png_img):
|
|
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
|
@@ -108,14 +108,14 @@ def get_box_2(png_img):
|
|
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 |
|
|
|
1 |
"""
|
2 |
+
有一些 png 图像下部也会有一些透明的区域,使得图像无法对其底部边框
|
3 |
+
本程序实现移动图像,使其下部与 png 图像实际大小相对齐
|
4 |
"""
|
5 |
import os
|
6 |
import cv2
|
|
|
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
|
|
|
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
|
|
|
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
|
|
|
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 |
|
hivision/creator/photo_adjuster.py
ADDED
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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_photo
|
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
|
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] - x + 1, face_rect[3] - y + 1
|
25 |
+
height, width = ctx.processing_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.processing_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.processing_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_photo(
|
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 |
+
print("crop_size:", crop_size)
|
177 |
+
background_bgr = np.full((crop_size[0], crop_size[1]), 255, dtype=np.uint8)
|
178 |
+
background_a = np.full((crop_size[0], crop_size[1]), 0, dtype=np.uint8)
|
179 |
+
background = cv2.merge(
|
180 |
+
(background_bgr, background_bgr, background_bgr, background_a)
|
181 |
+
)
|
182 |
+
|
183 |
+
background[
|
184 |
+
temp_y_1 : crop_size[0] - temp_y_2, temp_x_1 : crop_size[1] - temp_x_2
|
185 |
+
] = img[y1:y2, x1:x2]
|
186 |
+
|
187 |
+
return background
|
188 |
+
|
189 |
+
|
190 |
+
def move(input_image):
|
191 |
+
"""
|
192 |
+
裁剪主函数,输入一张 png 图像,该图像周围是透明的
|
193 |
+
"""
|
194 |
+
png_img = input_image # 获取图像
|
195 |
+
|
196 |
+
height, width, channels = png_img.shape # 高 y、宽 x
|
197 |
+
y_low, y_high, _, _ = U.get_box(png_img, model=2) # for 循环
|
198 |
+
base = np.zeros((y_high, width, channels), dtype=np.uint8) # for 循环
|
199 |
+
png_img = png_img[0 : height - y_high, :, :] # for 循环
|
200 |
+
png_img = np.concatenate((base, png_img), axis=0)
|
201 |
+
return png_img, y_high
|
202 |
+
|
203 |
+
|
204 |
+
def standard_photo_resize(input_image: np.array, size):
|
205 |
+
"""
|
206 |
+
input_image: 输入图像,即高清照
|
207 |
+
size: 标准照的尺寸
|
208 |
+
"""
|
209 |
+
resize_ratio = input_image.shape[0] / size[0]
|
210 |
+
resize_item = int(round(input_image.shape[0] / size[0]))
|
211 |
+
if resize_ratio >= 2:
|
212 |
+
for i in range(resize_item - 1):
|
213 |
+
if i == 0:
|
214 |
+
result_image = cv2.resize(
|
215 |
+
input_image,
|
216 |
+
(size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
|
217 |
+
interpolation=cv2.INTER_AREA,
|
218 |
+
)
|
219 |
+
else:
|
220 |
+
result_image = cv2.resize(
|
221 |
+
result_image,
|
222 |
+
(size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
|
223 |
+
interpolation=cv2.INTER_AREA,
|
224 |
+
)
|
225 |
+
else:
|
226 |
+
result_image = cv2.resize(
|
227 |
+
input_image, (size[1], size[0]), interpolation=cv2.INTER_AREA
|
228 |
+
)
|
229 |
+
|
230 |
+
return result_image
|
231 |
+
|
232 |
+
|
233 |
+
def resize_image_by_min(input_image, esp=600):
|
234 |
+
"""
|
235 |
+
将图像缩放为最短边至少为 esp 的图像。
|
236 |
+
:param input_image: 输入图像(OpenCV 矩阵)
|
237 |
+
:param esp: 缩放后的最短边长
|
238 |
+
:return: 缩放后的图像,缩放倍率
|
239 |
+
"""
|
240 |
+
height, width = input_image.shape[0], input_image.shape[1]
|
241 |
+
min_border = min(height, width)
|
242 |
+
if min_border < esp:
|
243 |
+
if height >= width:
|
244 |
+
new_width = esp
|
245 |
+
new_height = height * esp // width
|
246 |
+
else:
|
247 |
+
new_height = esp
|
248 |
+
new_width = width * esp // height
|
249 |
+
|
250 |
+
return (
|
251 |
+
cv2.resize(
|
252 |
+
input_image, (new_width, new_height), interpolation=cv2.INTER_AREA
|
253 |
+
),
|
254 |
+
new_height / height,
|
255 |
+
)
|
256 |
+
|
257 |
+
else:
|
258 |
+
return input_image, 1
|
{hivisionai/hycv → hivision/creator}/tensor2numpy.py
RENAMED
@@ -1,8 +1,8 @@
|
|
1 |
"""
|
2 |
-
|
3 |
-
|
4 |
-
问题是Tensor和Numpy的矩阵排布逻辑不同
|
5 |
-
包括Tensor推理经常会进行Transform
|
6 |
就想做一些等价转换的函数。
|
7 |
"""
|
8 |
import numpy as np
|
@@ -11,7 +11,7 @@ import numpy as np
|
|
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)
|
@@ -23,16 +23,16 @@ def NTo_Tensor(array):
|
|
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:
|
27 |
-
std:
|
28 |
-
dtype:输出的numpy
|
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)
|
@@ -45,11 +45,11 @@ def NUnsqueeze(array, axis=0):
|
|
45 |
"""
|
46 |
:param array: opencv/PIL读取的numpy矩阵
|
47 |
axis:要增加的维度
|
48 |
-
:return:numpy矩阵
|
49 |
Example:
|
50 |
-
Inputs:array为opencv/PIL
|
51 |
axis=0
|
52 |
-
Outputs:output为array在第0
|
53 |
"""
|
54 |
if axis == 0:
|
55 |
output = array[None, :, :, :]
|
|
|
1 |
"""
|
2 |
+
作者:林泽毅
|
3 |
+
建这个开源库的起源呢,是因为在做 onnx 推理的时候,需要将原来的 tensor 转换成 numpy.array
|
4 |
+
问题是 Tensor 和 Numpy 的矩阵排布逻辑不同
|
5 |
+
包括 Tensor 推理经常会进行 Transform,比如 ToTensor,Normalize 等
|
6 |
就想做一些等价转换的函数。
|
7 |
"""
|
8 |
import numpy as np
|
|
|
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)
|
|
|
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)
|
|
|
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, :, :, :]
|
hivision/creator/utils.py
ADDED
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
hivisionai/__init__.py → hivision/creator/weights/.gitkeep
RENAMED
File without changes
|
hivision_modnet.onnx → hivision/creator/weights/modnet_photographic_portrait_matting.onnx
RENAMED
@@ -1,3 +1,3 @@
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:
|
3 |
-
size
|
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:07c308cf0fc7e6e8b2065a12ed7fc07e1de8febb7dc7839d7b7f15dd66584df9
|
3 |
+
size 25888640
|
hivision/error.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
hivision/utils.py
ADDED
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
r"""
|
4 |
+
@DATE: 2024/9/5 21:52
|
5 |
+
@File: utils.py
|
6 |
+
@IDE: pycharm
|
7 |
+
@Description:
|
8 |
+
hivision提供的工具函数
|
9 |
+
"""
|
10 |
+
from PIL import Image
|
11 |
+
import io
|
12 |
+
import numpy as np
|
13 |
+
import cv2
|
14 |
+
import base64
|
15 |
+
|
16 |
+
|
17 |
+
def resize_image_to_kb(input_image, output_image_path, target_size_kb):
|
18 |
+
"""
|
19 |
+
Resize an image to a target size in KB.
|
20 |
+
将图像调整大小至目标文件大小(KB)。
|
21 |
+
|
22 |
+
:param input_image_path: Path to the input image. 输入图像的路径。
|
23 |
+
:param output_image_path: Path to save the resized image. 保存调整大小后的图像的路径。
|
24 |
+
:param target_size_kb: Target size in KB. 目标文件大小(KB)。
|
25 |
+
|
26 |
+
Example:
|
27 |
+
resize_image_to_kb('input_image.jpg', 'output_image.jpg', 50)
|
28 |
+
"""
|
29 |
+
|
30 |
+
if isinstance(input_image, np.ndarray):
|
31 |
+
img = Image.fromarray(input_image)
|
32 |
+
elif isinstance(input_image, Image.Image):
|
33 |
+
img = input_image
|
34 |
+
else:
|
35 |
+
raise ValueError("input_image must be a NumPy array or PIL Image.")
|
36 |
+
|
37 |
+
# Convert image to RGB mode if it's not
|
38 |
+
if img.mode != "RGB":
|
39 |
+
img = img.convert("RGB")
|
40 |
+
|
41 |
+
# Initial quality value
|
42 |
+
quality = 95
|
43 |
+
|
44 |
+
while True:
|
45 |
+
# Create a BytesIO object to hold the image data in memory
|
46 |
+
img_byte_arr = io.BytesIO()
|
47 |
+
|
48 |
+
# Save the image to the BytesIO object with the current quality
|
49 |
+
img.save(img_byte_arr, format="JPEG", quality=quality)
|
50 |
+
|
51 |
+
# Get the size of the image in KB
|
52 |
+
img_size_kb = len(img_byte_arr.getvalue()) / 1024
|
53 |
+
|
54 |
+
# Check if the image size is within the target size
|
55 |
+
if img_size_kb <= target_size_kb or quality == 1:
|
56 |
+
# If the image is smaller than the target size, add padding
|
57 |
+
if img_size_kb < target_size_kb:
|
58 |
+
padding_size = int(
|
59 |
+
(target_size_kb * 1024) - len(img_byte_arr.getvalue())
|
60 |
+
)
|
61 |
+
padding = b"\x00" * padding_size
|
62 |
+
img_byte_arr.write(padding)
|
63 |
+
|
64 |
+
# Save the image to the output path
|
65 |
+
with open(output_image_path, "wb") as f:
|
66 |
+
f.write(img_byte_arr.getvalue())
|
67 |
+
break
|
68 |
+
|
69 |
+
# Reduce the quality if the image is still too large
|
70 |
+
quality -= 5
|
71 |
+
|
72 |
+
# Ensure quality does not go below 1
|
73 |
+
if quality < 1:
|
74 |
+
quality = 1
|
75 |
+
|
76 |
+
|
77 |
+
def resize_image_to_kb_base64(input_image, target_size_kb):
|
78 |
+
"""
|
79 |
+
Resize an image to a target size in KB and return it as a base64 encoded string.
|
80 |
+
将图像调整大小至目标文件大小(KB)并返回base64编码的字符串。
|
81 |
+
|
82 |
+
:param input_image: Input image as a NumPy array or PIL Image. 输入图像,可以是NumPy数组或PIL图像。
|
83 |
+
:param target_size_kb: Target size in KB. 目标文件大小(KB)。
|
84 |
+
|
85 |
+
:return: Base64 encoded string of the resized image. 调整大小后的图像的base64编码字符串。
|
86 |
+
"""
|
87 |
+
|
88 |
+
if isinstance(input_image, np.ndarray):
|
89 |
+
img = Image.fromarray(input_image)
|
90 |
+
elif isinstance(input_image, Image.Image):
|
91 |
+
img = input_image
|
92 |
+
else:
|
93 |
+
raise ValueError("input_image must be a NumPy array or PIL Image.")
|
94 |
+
|
95 |
+
# Convert image to RGB mode if it's not
|
96 |
+
if img.mode != "RGB":
|
97 |
+
img = img.convert("RGB")
|
98 |
+
|
99 |
+
# Initial quality value
|
100 |
+
quality = 95
|
101 |
+
|
102 |
+
while True:
|
103 |
+
# Create a BytesIO object to hold the image data in memory
|
104 |
+
img_byte_arr = io.BytesIO()
|
105 |
+
|
106 |
+
# Save the image to the BytesIO object with the current quality
|
107 |
+
img.save(img_byte_arr, format="JPEG", quality=quality)
|
108 |
+
|
109 |
+
# Get the size of the image in KB
|
110 |
+
img_size_kb = len(img_byte_arr.getvalue()) / 1024
|
111 |
+
|
112 |
+
# Check if the image size is within the target size
|
113 |
+
if img_size_kb <= target_size_kb or quality == 1:
|
114 |
+
# If the image is smaller than the target size, add padding
|
115 |
+
if img_size_kb < target_size_kb:
|
116 |
+
padding_size = int(
|
117 |
+
(target_size_kb * 1024) - len(img_byte_arr.getvalue())
|
118 |
+
)
|
119 |
+
padding = b"\x00" * padding_size
|
120 |
+
img_byte_arr.write(padding)
|
121 |
+
|
122 |
+
# Encode the image data to base64
|
123 |
+
img_base64 = base64.b64encode(img_byte_arr.getvalue()).decode("utf-8")
|
124 |
+
return img_base64
|
125 |
+
|
126 |
+
# Reduce the quality if the image is still too large
|
127 |
+
quality -= 5
|
128 |
+
|
129 |
+
# Ensure quality does not go below 1
|
130 |
+
if quality < 1:
|
131 |
+
quality = 1
|
132 |
+
|
133 |
+
|
134 |
+
def numpy_2_base64(img: np.ndarray):
|
135 |
+
_, buffer = cv2.imencode(".png", img)
|
136 |
+
base64_image = base64.b64encode(buffer).decode("utf-8")
|
137 |
+
|
138 |
+
return base64_image
|
139 |
+
|
140 |
+
|
141 |
+
def save_numpy_image(numpy_img, file_path):
|
142 |
+
# 检查数组的形状
|
143 |
+
if numpy_img.shape[2] == 4:
|
144 |
+
# 将 BGR 转换为 RGB,并保留透明通道
|
145 |
+
rgb_img = np.concatenate(
|
146 |
+
(np.flip(numpy_img[:, :, :3], axis=-1), numpy_img[:, :, 3:]), axis=-1
|
147 |
+
).astype(np.uint8)
|
148 |
+
img = Image.fromarray(rgb_img, mode="RGBA")
|
149 |
+
else:
|
150 |
+
# 将 BGR 转换为 RGB
|
151 |
+
rgb_img = np.flip(numpy_img, axis=-1).astype(np.uint8)
|
152 |
+
img = Image.fromarray(rgb_img, mode="RGB")
|
153 |
+
|
154 |
+
img.save(file_path)
|
155 |
+
|
156 |
+
|
157 |
+
def numpy_to_bytes(numpy_img):
|
158 |
+
img = Image.fromarray(numpy_img)
|
159 |
+
img_byte_arr = io.BytesIO()
|
160 |
+
img.save(img_byte_arr, format="PNG")
|
161 |
+
img_byte_arr.seek(0)
|
162 |
+
return img_byte_arr
|
163 |
+
|
164 |
+
|
165 |
+
def hex_to_rgb(value):
|
166 |
+
value = value.lstrip("#")
|
167 |
+
length = len(value)
|
168 |
+
return tuple(
|
169 |
+
int(value[i : i + length // 3], 16) for i in range(0, length, length // 3)
|
170 |
+
)
|
171 |
+
|
172 |
+
|
173 |
+
def generate_gradient(start_color, width, height, mode="updown"):
|
174 |
+
# 定义背景颜色
|
175 |
+
end_color = (255, 255, 255) # 白色
|
176 |
+
|
177 |
+
# 创建一个空白图像
|
178 |
+
r_out = np.zeros((height, width), dtype=int)
|
179 |
+
g_out = np.zeros((height, width), dtype=int)
|
180 |
+
b_out = np.zeros((height, width), dtype=int)
|
181 |
+
|
182 |
+
if mode == "updown":
|
183 |
+
# 生成上下渐变色
|
184 |
+
for y in range(height):
|
185 |
+
r = int(
|
186 |
+
(y / height) * end_color[0] + ((height - y) / height) * start_color[0]
|
187 |
+
)
|
188 |
+
g = int(
|
189 |
+
(y / height) * end_color[1] + ((height - y) / height) * start_color[1]
|
190 |
+
)
|
191 |
+
b = int(
|
192 |
+
(y / height) * end_color[2] + ((height - y) / height) * start_color[2]
|
193 |
+
)
|
194 |
+
r_out[y, :] = r
|
195 |
+
g_out[y, :] = g
|
196 |
+
b_out[y, :] = b
|
197 |
+
|
198 |
+
else:
|
199 |
+
# 生成中心渐变色
|
200 |
+
img = np.zeros((height, width, 3))
|
201 |
+
# 定义椭圆中心和半径
|
202 |
+
center = (width // 2, height // 2)
|
203 |
+
end_axies = max(height, width)
|
204 |
+
# 定义渐变色
|
205 |
+
end_color = (255, 255, 255)
|
206 |
+
# 绘制椭圆
|
207 |
+
for y in range(end_axies):
|
208 |
+
axes = (end_axies - y, end_axies - y)
|
209 |
+
r = int(
|
210 |
+
(y / end_axies) * end_color[0]
|
211 |
+
+ ((end_axies - y) / end_axies) * start_color[0]
|
212 |
+
)
|
213 |
+
g = int(
|
214 |
+
(y / end_axies) * end_color[1]
|
215 |
+
+ ((end_axies - y) / end_axies) * start_color[1]
|
216 |
+
)
|
217 |
+
b = int(
|
218 |
+
(y / end_axies) * end_color[2]
|
219 |
+
+ ((end_axies - y) / end_axies) * start_color[2]
|
220 |
+
)
|
221 |
+
|
222 |
+
cv2.ellipse(img, center, axes, 0, 0, 360, (b, g, r), -1)
|
223 |
+
b_out, g_out, r_out = cv2.split(np.uint64(img))
|
224 |
+
|
225 |
+
return r_out, g_out, b_out
|
226 |
+
|
227 |
+
|
228 |
+
def add_background(input_image, bgr=(0, 0, 0), mode="pure_color"):
|
229 |
+
"""
|
230 |
+
本函数的功能为为透明图像加上背景。
|
231 |
+
:param input_image: numpy.array(4 channels), 透明图像
|
232 |
+
:param bgr: tuple, 合成纯色底时的 BGR 值
|
233 |
+
:param new_background: numpy.array(3 channels),合成自定义图像底时的背景图
|
234 |
+
:return: output: 合成好的输出图像
|
235 |
+
"""
|
236 |
+
height, width = input_image.shape[0], input_image.shape[1]
|
237 |
+
b, g, r, a = cv2.split(input_image)
|
238 |
+
a_cal = a / 255
|
239 |
+
if mode == "pure_color":
|
240 |
+
# 纯色填充
|
241 |
+
b2 = np.full([height, width], bgr[0], dtype=int)
|
242 |
+
g2 = np.full([height, width], bgr[1], dtype=int)
|
243 |
+
r2 = np.full([height, width], bgr[2], dtype=int)
|
244 |
+
elif mode == "updown_gradient":
|
245 |
+
b2, g2, r2 = generate_gradient(bgr, width, height, mode="updown")
|
246 |
+
else:
|
247 |
+
b2, g2, r2 = generate_gradient(bgr, width, height, mode="center")
|
248 |
+
|
249 |
+
output = cv2.merge(
|
250 |
+
((b - b2) * a_cal + b2, (g - g2) * a_cal + g2, (r - r2) * a_cal + r2)
|
251 |
+
)
|
252 |
+
|
253 |
+
return output
|
hivisionai/app.py
DELETED
@@ -1,452 +0,0 @@
|
|
1 |
-
# -*- coding: utf-8 -*-
|
2 |
-
|
3 |
-
"""
|
4 |
-
@Time : 2022/8/27 14:17
|
5 |
-
@Author : cuny
|
6 |
-
@File : app.py
|
7 |
-
@Software : PyCharm
|
8 |
-
@Introduce:
|
9 |
-
查看包版本等一系列操作
|
10 |
-
"""
|
11 |
-
import os
|
12 |
-
import sys
|
13 |
-
import json
|
14 |
-
import shutil
|
15 |
-
import zipfile
|
16 |
-
import requests
|
17 |
-
from argparse import ArgumentParser
|
18 |
-
from importlib.metadata import version
|
19 |
-
try: # 加上这个try的原因在于本地环境和云函数端的import形式有所不同
|
20 |
-
from qcloud_cos import CosConfig
|
21 |
-
from qcloud_cos import CosS3Client
|
22 |
-
except ImportError:
|
23 |
-
try:
|
24 |
-
from qcloud_cos_v5 import CosConfig
|
25 |
-
from qcloud_cos_v5 import CosS3Client
|
26 |
-
from qcloud_cos.cos_exception import CosServiceError
|
27 |
-
except ImportError:
|
28 |
-
raise ImportError("请下载腾讯云COS相关代码包:pip install cos-python-sdk-v5")
|
29 |
-
|
30 |
-
|
31 |
-
class HivisionaiParams(object):
|
32 |
-
"""
|
33 |
-
定义一些基本常量
|
34 |
-
"""
|
35 |
-
# 文件所在路径
|
36 |
-
# 包名称
|
37 |
-
package_name = "HY-sdk"
|
38 |
-
# 腾讯云相关变量
|
39 |
-
region = "ap-beijing"
|
40 |
-
zip_key = "HY-sdk/" # zip存储的云端文件夹路径,这里改了publish.yml也需要更改
|
41 |
-
# 云端用户配置,如果在cloud_config_save不存在,就需要下载此文件
|
42 |
-
user_url = "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/sdk-user/user_config.json"
|
43 |
-
bucket = "cloud-public-static-1306602019"
|
44 |
-
# 压缩包类型
|
45 |
-
file_format = ".zip"
|
46 |
-
# 下载路径(.hivisionai文件夹路径)
|
47 |
-
download_path = os.path.expandvars('$HOME')
|
48 |
-
# zip文件、zip解压缩文件的存放路径
|
49 |
-
save_folder = f"{os.path.expandvars('$HOME')}/.hivisionai/sdk"
|
50 |
-
# 腾讯云配置文件存放路径
|
51 |
-
cloud_config_save = f"{os.path.expandvars('$HOME')}/.hivisionai/user_config.json"
|
52 |
-
# 项目路径
|
53 |
-
hivisionai_path = os.path.dirname(os.path.dirname(__file__))
|
54 |
-
# 使用hivisionai的路径
|
55 |
-
getcwd = os.getcwd()
|
56 |
-
# HY-func的依赖配置
|
57 |
-
# 每个依赖会包含三个参数,保存路径(save_path,相对于HY_func的路径)、下载url(url)
|
58 |
-
functionDependence = {
|
59 |
-
"configs": [
|
60 |
-
# --------- 配置文件部分
|
61 |
-
# _lib
|
62 |
-
{
|
63 |
-
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/aliyun-human-matting-api.json",
|
64 |
-
"save_path": "_lib/config/aliyun-human-matting-api.json"
|
65 |
-
},
|
66 |
-
{
|
67 |
-
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/megvii-face-plus-api.json",
|
68 |
-
"save_path": "_lib/config/megvii-face-plus-api.json"
|
69 |
-
},
|
70 |
-
{
|
71 |
-
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/volcano-face-change-api.json",
|
72 |
-
"save_path": "_lib/config/volcano-face-change-api.json"
|
73 |
-
},
|
74 |
-
# _service
|
75 |
-
{
|
76 |
-
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/func_error_conf.json",
|
77 |
-
"save_path": "_service/utils/config/func_error_conf.json"
|
78 |
-
},
|
79 |
-
{
|
80 |
-
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/service_config.json",
|
81 |
-
"save_path": "_service/utils/config/service_config.json"
|
82 |
-
},
|
83 |
-
# --------- 模型部分
|
84 |
-
# 模型部分存储在Notion文档当中
|
85 |
-
# https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f
|
86 |
-
],
|
87 |
-
"weights": "https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f"
|
88 |
-
}
|
89 |
-
|
90 |
-
|
91 |
-
class HivisionaiUtils(object):
|
92 |
-
"""
|
93 |
-
本类为一些基本工具类,包含代码复用相关内容
|
94 |
-
"""
|
95 |
-
@staticmethod
|
96 |
-
def get_client():
|
97 |
-
"""获取cos客户端对象"""
|
98 |
-
def get_secret():
|
99 |
-
# 首先判断cloud_config_save下是否存在
|
100 |
-
if not os.path.exists(HivisionaiParams.cloud_config_save):
|
101 |
-
print("Downloading user_config...")
|
102 |
-
resp = requests.get(HivisionaiParams.user_url)
|
103 |
-
open(HivisionaiParams.cloud_config_save, "wb").write(resp.content)
|
104 |
-
config = json.load(open(HivisionaiParams.cloud_config_save, "r"))
|
105 |
-
return config["secret_id"], config["secret_key"]
|
106 |
-
# todo 接入HY-Auth-Sync
|
107 |
-
secret_id, secret_key = get_secret()
|
108 |
-
return CosS3Client(CosConfig(Region=HivisionaiParams.region, Secret_id=secret_id, Secret_key=secret_key))
|
109 |
-
|
110 |
-
def get_all_versions(self):
|
111 |
-
"""获取云端的所有版本号"""
|
112 |
-
def getAllVersion_base():
|
113 |
-
"""
|
114 |
-
返回cos存储桶内部的某个文件夹的内部名称
|
115 |
-
ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称 (s是对象实例)
|
116 |
-
返回的内容存储在response["Content"],不过返回的数据大小是有限制的,具体内容还是请看官方文档。
|
117 |
-
Returns:
|
118 |
-
[版本列表]
|
119 |
-
"""
|
120 |
-
resp = client.list_objects(
|
121 |
-
Bucket=HivisionaiParams.bucket,
|
122 |
-
Prefix=HivisionaiParams.zip_key,
|
123 |
-
Marker=marker
|
124 |
-
)
|
125 |
-
versions_list.extend([x["Key"].split("/")[-1].split(HivisionaiParams.file_format)[0] for x in resp["Contents"] if int(x["Size"]) > 0])
|
126 |
-
if resp['IsTruncated'] == 'false': # 接下来没有数据了,就退出
|
127 |
-
return ""
|
128 |
-
else:
|
129 |
-
return resp['NextMarker']
|
130 |
-
client = self.get_client()
|
131 |
-
marker = ""
|
132 |
-
versions_list = []
|
133 |
-
while True: # 轮询
|
134 |
-
try:
|
135 |
-
marker = getAllVersion_base()
|
136 |
-
except KeyError as e:
|
137 |
-
print(e)
|
138 |
-
raise
|
139 |
-
if len(marker) == 0: # 没有数据了
|
140 |
-
break
|
141 |
-
return versions_list
|
142 |
-
|
143 |
-
def get_newest_version(self):
|
144 |
-
"""获取最新的版本号"""
|
145 |
-
versions_list = self.get_all_versions()
|
146 |
-
# reverse=True,降序
|
147 |
-
versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
|
148 |
-
versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
|
149 |
-
versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
|
150 |
-
return versions_list[0]
|
151 |
-
|
152 |
-
def download_version(self, v):
|
153 |
-
"""
|
154 |
-
在存储桶中下载文件,将下载好的文件解压至本地
|
155 |
-
Args:
|
156 |
-
v: 版本号,x.x.x
|
157 |
-
|
158 |
-
Returns:
|
159 |
-
None
|
160 |
-
"""
|
161 |
-
file_name = v + HivisionaiParams.file_format
|
162 |
-
client = self.get_client()
|
163 |
-
print(f"Download to {HivisionaiParams.save_folder}...")
|
164 |
-
try:
|
165 |
-
resp = client.get_object(HivisionaiParams.bucket, HivisionaiParams.zip_key + "/" + file_name)
|
166 |
-
contents = resp["Body"].get_raw_stream().read()
|
167 |
-
except CosServiceError:
|
168 |
-
print(f"[{file_name}.zip] does not exist, please check your version!")
|
169 |
-
sys.exit()
|
170 |
-
if not os.path.exists(HivisionaiParams.save_folder):
|
171 |
-
os.makedirs(HivisionaiParams.save_folder)
|
172 |
-
open(os.path.join(HivisionaiParams.save_folder, file_name), "wb").write(contents)
|
173 |
-
print("Download success!")
|
174 |
-
|
175 |
-
@staticmethod
|
176 |
-
def download_dependence(path=None):
|
177 |
-
"""
|
178 |
-
一键下载HY-sdk所需要的所有依赖,需要注意的是,本方法必须在运行pip install之后使用(运行完pip install之后才会出现hivisionai文件夹)
|
179 |
-
Args:
|
180 |
-
path: 文件路径,精确到hivisionai文件夹的上一个目录,如果为None,则默认下载到python环境下hivisionai安装的目录
|
181 |
-
|
182 |
-
Returns:
|
183 |
-
下载相应内容到指定位置
|
184 |
-
"""
|
185 |
-
# print("指定的下载路径:", path) # 此时在path路径下必然存在一个hivisionai文件夹
|
186 |
-
# print("系统安装的hivisionai库的路径:", HivisionaiParams.hivisionai_path)
|
187 |
-
print("Dependence downloading...")
|
188 |
-
if path is None:
|
189 |
-
path = HivisionaiParams.hivisionai_path
|
190 |
-
# ----------------下载mtcnn模型文件
|
191 |
-
mtcnn_path = os.path.join(path, "hivisionai/hycv/mtcnn_onnx/weights")
|
192 |
-
base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
|
193 |
-
onnx_files = ["pnet.onnx", "rnet.onnx", "onet.onnx"]
|
194 |
-
print(f"Downloading mtcnn model in {mtcnn_path}")
|
195 |
-
if not os.path.exists(mtcnn_path):
|
196 |
-
os.mkdir(mtcnn_path)
|
197 |
-
for onnx_file in onnx_files:
|
198 |
-
if not os.path.exists(os.path.join(mtcnn_path, onnx_file)):
|
199 |
-
# download onnx model
|
200 |
-
onnx_url = base_url + onnx_file
|
201 |
-
print("Downloading Onnx Model in:", onnx_url)
|
202 |
-
r = requests.get(onnx_url, stream=True)
|
203 |
-
if r.status_code == 200:
|
204 |
-
open(os.path.join(mtcnn_path, onnx_file), 'wb').write(r.content) # 将内容写入文件
|
205 |
-
print(f"Download finished -- {onnx_file}")
|
206 |
-
del r
|
207 |
-
# ----------------
|
208 |
-
print("Dependence download finished...")
|
209 |
-
|
210 |
-
|
211 |
-
class HivisionaiApps(object):
|
212 |
-
"""
|
213 |
-
本类为app对外暴露的接口,为了代码规整性,这里使用类来对暴露接口进行调整
|
214 |
-
"""
|
215 |
-
@staticmethod
|
216 |
-
def show_cloud_version():
|
217 |
-
"""查看在cos中的所有HY-sdk版本"""
|
218 |
-
print("Connect to COS...")
|
219 |
-
versions_list = hivisionai_utils.get_all_versions()
|
220 |
-
# reverse=True,降序
|
221 |
-
versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
|
222 |
-
versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
|
223 |
-
versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
|
224 |
-
if len(versions_list) == 0:
|
225 |
-
print("There is no version currently, please release it first!")
|
226 |
-
sys.exit()
|
227 |
-
versions = "The currently existing versions (Keep 10): \n"
|
228 |
-
for i, v in enumerate(versions_list):
|
229 |
-
versions += str(v) + " "
|
230 |
-
if i == 9:
|
231 |
-
break
|
232 |
-
print(versions)
|
233 |
-
|
234 |
-
@staticmethod
|
235 |
-
def upgrade(v: str, enforce: bool = False, save_cached: bool = False):
|
236 |
-
"""
|
237 |
-
自动升级HY-sdk到指定版本
|
238 |
-
Args:
|
239 |
-
v: 指定的版本号,格式为x.x.x
|
240 |
-
enforce: 是否需要强制执行更新命令
|
241 |
-
save_cached: 是否保存下载的wheel文件,默认为否
|
242 |
-
Returns:
|
243 |
-
None
|
244 |
-
"""
|
245 |
-
def check_format():
|
246 |
-
# noinspection PyBroadException
|
247 |
-
try:
|
248 |
-
major, minor, patch = v.split(".")
|
249 |
-
int(major)
|
250 |
-
int(minor)
|
251 |
-
int(patch)
|
252 |
-
except Exception as e:
|
253 |
-
print(f"Illegal version number!\n{e}")
|
254 |
-
pass
|
255 |
-
print("Upgrading, please wait a moment...")
|
256 |
-
if v == "-1":
|
257 |
-
v = hivisionai_utils.get_newest_version()
|
258 |
-
# 检查format的格式
|
259 |
-
check_format()
|
260 |
-
if v == version(HivisionaiParams.package_name) and not enforce:
|
261 |
-
print(f"Current version: {v} already exists, skip installation.")
|
262 |
-
sys.exit()
|
263 |
-
hivisionai_utils.download_version(v)
|
264 |
-
# 下载完毕(下载至save_folder),解压文件
|
265 |
-
target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
|
266 |
-
assert zipfile.is_zipfile(target_zip), "Decompression failed, and the target was not a zip file."
|
267 |
-
new_dir = target_zip.replace('.zip', '') # 解压的文件名
|
268 |
-
if os.path.exists(new_dir): # 判断文件夹是否存在
|
269 |
-
shutil.rmtree(new_dir)
|
270 |
-
os.mkdir(new_dir) # 新建文件夹
|
271 |
-
f = zipfile.ZipFile(target_zip)
|
272 |
-
f.extractall(new_dir) # 提取zip文件
|
273 |
-
print("Decompressed, begin to install...")
|
274 |
-
os.system(f'pip3 install {os.path.join(new_dir, "**.whl")}')
|
275 |
-
# 开始自动下载必要的模型依赖
|
276 |
-
hivisionai_utils.download_dependence()
|
277 |
-
# 安装完毕,如果save_cached为真,删除"$HOME/.hivisionai/sdk"内部的所有文件元素
|
278 |
-
if save_cached is True:
|
279 |
-
os.system(f'rm -rf {HivisionaiParams.save_folder}/**')
|
280 |
-
|
281 |
-
@staticmethod
|
282 |
-
def export(path):
|
283 |
-
"""
|
284 |
-
输出最新版本的文件到命令运行的path目录
|
285 |
-
Args:
|
286 |
-
path: 用户输入的路径
|
287 |
-
|
288 |
-
Returns:
|
289 |
-
输出最新的hivisionai到path目录
|
290 |
-
"""
|
291 |
-
# print(f"当前路径: {os.path.join(HivisionaiParams.getcwd, path)}")
|
292 |
-
# print(f"文件路径: {os.path.dirname(__file__)}")
|
293 |
-
export_path = os.path.join(HivisionaiParams.getcwd, path)
|
294 |
-
# 判断输出路径存不存在,如果不存在,就报错
|
295 |
-
assert os.path.exists(export_path), f"{export_path} dose not Exists!"
|
296 |
-
v = hivisionai_utils.get_newest_version()
|
297 |
-
# 下载文件到.hivisionai/sdk当中
|
298 |
-
hivisionai_utils.download_version(v)
|
299 |
-
# 下载完毕(下载至save_folder),解压文件
|
300 |
-
target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
|
301 |
-
assert zipfile.is_zipfile(target_zip), "Decompression failed, and the target was not a zip file."
|
302 |
-
new_dir = os.path.basename(target_zip.replace('.zip', '')) # 解压的文件名
|
303 |
-
new_dir = os.path.join(export_path, new_dir) # 解压的文件路径
|
304 |
-
if os.path.exists(new_dir): # 判断文件夹是否存在
|
305 |
-
shutil.rmtree(new_dir)
|
306 |
-
os.mkdir(new_dir) # 新建文件夹
|
307 |
-
f = zipfile.ZipFile(target_zip)
|
308 |
-
f.extractall(new_dir) # 提取zip文件
|
309 |
-
print("Decompressed, begin to export...")
|
310 |
-
# 强制删除bin/hivisionai和hivisionai/以及HY_sdk-**
|
311 |
-
bin_path = os.path.join(export_path, "bin")
|
312 |
-
hivisionai_path = os.path.join(export_path, "hivisionai")
|
313 |
-
sdk_path = os.path.join(export_path, "HY_sdk-**")
|
314 |
-
os.system(f"rm -rf {bin_path} {hivisionai_path} {sdk_path}")
|
315 |
-
# 删除完毕,开始export
|
316 |
-
os.system(f'pip3 install {os.path.join(new_dir, "**.whl")} -t {export_path}')
|
317 |
-
hivisionai_utils.download_dependence(export_path)
|
318 |
-
# 将下载下来的文件夹删除
|
319 |
-
os.system(f'rm -rf {target_zip} && rm -rf {new_dir}')
|
320 |
-
print("Done.")
|
321 |
-
|
322 |
-
@staticmethod
|
323 |
-
def hy_func_init(force):
|
324 |
-
"""
|
325 |
-
在HY-func目录下使用hivisionai --init,可以自动将需要的依赖下载到指定位置
|
326 |
-
不过对于比较大的模型——修复模型而言,需要手动下载
|
327 |
-
Args:
|
328 |
-
force: 如果force为True,则会强制重新下载所有的内容,包括修复模型这种比较大的模型
|
329 |
-
Returns:
|
330 |
-
程序执行完毕,会将一些必要的依赖也下载完毕
|
331 |
-
"""
|
332 |
-
cwd = HivisionaiParams.getcwd
|
333 |
-
# 判断当前文件夹是否是HY-func
|
334 |
-
dirName = os.path.basename(cwd)
|
335 |
-
assert dirName == "HY-func", "请在正确的文件目录下初始化HY-func!"
|
336 |
-
# 需要下载的内容会存放在HivisionaiParams的functionDependence���量下
|
337 |
-
functionDependence = HivisionaiParams.functionDependence
|
338 |
-
# 下载配置文件
|
339 |
-
configs = functionDependence["configs"]
|
340 |
-
print("正在下载配置文件...")
|
341 |
-
for config in configs:
|
342 |
-
if not force and os.path.exists(config['save_path']):
|
343 |
-
print(f"[pass]: {os.path.basename(config['url'])}")
|
344 |
-
continue
|
345 |
-
print(f"[Download]: {config['url']}")
|
346 |
-
resp = requests.get(config['url'])
|
347 |
-
# json文件存储在text区域,但是其他的不一定
|
348 |
-
open(os.path.join(cwd, config['save_path']), 'w').write(resp.text)
|
349 |
-
# 其他文件,提示访问notion文档
|
350 |
-
print(f"[NOTICE]: 一切准备就绪,请访问下面的文档下载剩下的模型文件:\n{functionDependence['weights']}")
|
351 |
-
|
352 |
-
@staticmethod
|
353 |
-
def hy_func_deploy(functionName: str = None, functionPath: str = None):
|
354 |
-
"""
|
355 |
-
在HY-func目录下使用此命令,并且随附功能函数的名称,就可以将HY-func的部署版放到桌面上
|
356 |
-
但是需要注意的是,本方式不适合修复功能使用,修复功能依旧需要手动制作镜像
|
357 |
-
Args:
|
358 |
-
functionName: 功能函数名称
|
359 |
-
functionPath: 需要注册的HY-func路径
|
360 |
-
|
361 |
-
Returns:
|
362 |
-
程序执行完毕,桌面会出现一个同名文件夹
|
363 |
-
"""
|
364 |
-
# 为了代码撰写的方便,这里仅仅把模型文件删除,其余配置文件保留
|
365 |
-
# 为了实现在任意位置输入hivisionai --deploy funcName都能成功,在使用前需要在.hivisionai/user_config.json中注册
|
366 |
-
# print(functionName, functionPath)
|
367 |
-
if functionPath is not None:
|
368 |
-
# 更新/添加路径
|
369 |
-
# functionPath为相对于使用路径的路径
|
370 |
-
assert os.path.basename(functionPath) == "HY-func", "所指向路径非HY-func!"
|
371 |
-
func_path = os.path.join(HivisionaiParams.getcwd, functionPath)
|
372 |
-
assert os.path.join(func_path), f"路径不存在: {func_path}"
|
373 |
-
# functionPath的路径写到user_config当中
|
374 |
-
user_config = json.load(open(HivisionaiParams.cloud_config_save, 'rb'))
|
375 |
-
user_config["func_path"] = func_path
|
376 |
-
open(HivisionaiParams.cloud_config_save, 'w').write(json.dumps(user_config))
|
377 |
-
print("HY-func全局路径保存成功!")
|
378 |
-
try:
|
379 |
-
user_config = json.load(open(HivisionaiParams.cloud_config_save, 'rb'))
|
380 |
-
func_path = user_config['func_path']
|
381 |
-
except KeyError:
|
382 |
-
return print("请先使用-p命令注册全局HY-func路径!")
|
383 |
-
# 此时func_path必然存在
|
384 |
-
# print(os.listdir(func_path))
|
385 |
-
assert functionName in os.listdir(func_path), functionName + "功能不存在!"
|
386 |
-
func_path_deploy = os.path.join(func_path, functionName)
|
387 |
-
# 开始复制文件到指定目录
|
388 |
-
# 我们默认移动到Desktop目录下,如果没有此目录,需要先创建一个
|
389 |
-
target_dir = os.path.join(HivisionaiParams.download_path, "Desktop")
|
390 |
-
assert os.path.exists(target_dir), target_dir + "文件路径不存在,你需要先创建一下!"
|
391 |
-
# 开始移动
|
392 |
-
target_dir = os.path.join(target_dir, functionName)
|
393 |
-
print("正在复制需要部署的文件...")
|
394 |
-
os.system(f"rm -rf {target_dir}")
|
395 |
-
os.system(f'cp -rf {func_path_deploy} {target_dir}')
|
396 |
-
os.system(f"cp -rf {os.path.join(func_path, '_lib')} {target_dir}")
|
397 |
-
os.system(f"cp -rf {os.path.join(func_path, '_service')} {target_dir}")
|
398 |
-
# 生成最新的hivisionai
|
399 |
-
print("正在生成hivisionai代码包...")
|
400 |
-
os.system(f'hivisionai -t {target_dir}')
|
401 |
-
# 移动完毕,删除模型文件
|
402 |
-
print("移动完毕,正在删除不需要的文件...")
|
403 |
-
# 模型文件
|
404 |
-
os.system(f"rm -rf {os.path.join(target_dir, '_lib', 'weights', '**')}")
|
405 |
-
# hivisionai生成时的多余文件
|
406 |
-
os.system(f"rm -rf {os.path.join(target_dir, 'bin')} {os.path.join(target_dir, 'HY_sdk**')}")
|
407 |
-
print("部署文件生成成功,你可以开始部署了!")
|
408 |
-
|
409 |
-
|
410 |
-
hivisionai_utils = HivisionaiUtils()
|
411 |
-
|
412 |
-
|
413 |
-
def entry_point():
|
414 |
-
parser = ArgumentParser()
|
415 |
-
# 查看版本号
|
416 |
-
parser.add_argument("-v", "--version", action="store_true", help="View the current HY-sdk version, which does not represent the final cloud version.")
|
417 |
-
# 自动更新
|
418 |
-
parser.add_argument("-u", "--upgrade", nargs='?', const="-1", type=str, help="Automatically update HY-sdk to the latest version")
|
419 |
-
# 查找云端的HY-sdk版本
|
420 |
-
parser.add_argument("-l", "--list", action="store_true", help="Find HY-sdk versions of the cloud, and keep up to ten")
|
421 |
-
# 下载云端的版本到本地路径
|
422 |
-
parser.add_argument("-t", "--export", nargs='?', const="./", help="Add a path parameter to automatically download the latest version of sdk to this path. If there are no parameters, the default is the current path")
|
423 |
-
# 强制更新附带参数,当一个功能需要强制执行一遍的时候,需要附带此参数
|
424 |
-
parser.add_argument("-f", "--force", action="store_true", help="Enforcement of other functions, execution of a single parameter is meaningless")
|
425 |
-
# 初始化HY-func
|
426 |
-
parser.add_argument("--init", action="store_true", help="Initialization HY-func")
|
427 |
-
# 部署HY-func
|
428 |
-
parser.add_argument("-d", "--deploy", nargs='?', const="-1", type=str, help="Deploy HY-func")
|
429 |
-
# 涉及注册一些自定义内容的时候,需要附带此参数,并写上自定义内容
|
430 |
-
parser.add_argument("-p", "--param", nargs='?', const="-1", type=str, help="When registering some custom content, you need to attach this parameter and write the custom content.")
|
431 |
-
args = parser.parse_args()
|
432 |
-
if args.version:
|
433 |
-
print(version(HivisionaiParams.package_name))
|
434 |
-
sys.exit()
|
435 |
-
if args.upgrade:
|
436 |
-
HivisionaiApps.upgrade(args.upgrade, args.force)
|
437 |
-
sys.exit()
|
438 |
-
if args.list:
|
439 |
-
HivisionaiApps.show_cloud_version()
|
440 |
-
sys.exit()
|
441 |
-
if args.export:
|
442 |
-
HivisionaiApps.export(args.export)
|
443 |
-
sys.exit()
|
444 |
-
if args.init:
|
445 |
-
HivisionaiApps.hy_func_init(args.force)
|
446 |
-
sys.exit()
|
447 |
-
if args.deploy:
|
448 |
-
HivisionaiApps.hy_func_deploy(args.deploy, args.param)
|
449 |
-
|
450 |
-
|
451 |
-
if __name__ == "__main__":
|
452 |
-
entry_point()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hivisionai/hyService/__init__.py
DELETED
File without changes
|
hivisionai/hyService/cloudService.py
DELETED
@@ -1,406 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
焕影小程序功能服务端的基本工具函数,以类的形式封装
|
3 |
-
"""
|
4 |
-
try: # 加上这个try的原因在于本地环境和云函数端的import形式有所不同
|
5 |
-
from qcloud_cos import CosConfig
|
6 |
-
from qcloud_cos import CosS3Client
|
7 |
-
except ImportError:
|
8 |
-
try:
|
9 |
-
from qcloud_cos_v5 import CosConfig
|
10 |
-
from qcloud_cos_v5 import CosS3Client
|
11 |
-
except ImportError:
|
12 |
-
raise ImportError("请下载腾讯云COS相关代码包:pip install cos-python-sdk-v5")
|
13 |
-
import requests
|
14 |
-
import datetime
|
15 |
-
import json
|
16 |
-
from .error import ProcessError
|
17 |
-
import os
|
18 |
-
local_path_ = os.path.dirname(__file__)
|
19 |
-
|
20 |
-
|
21 |
-
class GetConfig(object):
|
22 |
-
@staticmethod
|
23 |
-
def hy_sdk_client(Id:str, Key:str):
|
24 |
-
# 从cos中寻找文件
|
25 |
-
REGION: str = 'ap-beijing'
|
26 |
-
TOKEN = None
|
27 |
-
SCHEME: str = 'https'
|
28 |
-
BUCKET: str = 'hy-sdk-config-1305323352'
|
29 |
-
client_config = CosConfig(Region=REGION,
|
30 |
-
SecretId=Id,
|
31 |
-
SecretKey=Key,
|
32 |
-
Token=TOKEN,
|
33 |
-
Scheme=SCHEME)
|
34 |
-
return CosS3Client(client_config), BUCKET
|
35 |
-
|
36 |
-
def load_json(self, path:str, default_download=False):
|
37 |
-
try:
|
38 |
-
if os.path.isdir(path):
|
39 |
-
raise ProcessError("请输入具体的配置文件路径,而非文件夹!")
|
40 |
-
if default_download is True:
|
41 |
-
print(f"\033[34m 默认强制重新下载配置文件...\033[0m")
|
42 |
-
raise FileNotFoundError
|
43 |
-
with open(path) as f:
|
44 |
-
config = json.load(f)
|
45 |
-
return config
|
46 |
-
except FileNotFoundError:
|
47 |
-
dir_name = os.path.dirname(path)
|
48 |
-
try:
|
49 |
-
os.makedirs(dir_name)
|
50 |
-
except FileExistsError:
|
51 |
-
pass
|
52 |
-
base_name = os.path.basename(path)
|
53 |
-
print(f"\033[34m 正在从COS中下载配置文件...\033[0m")
|
54 |
-
print(f"\033[31m 请注意,接下来会在{dir_name}路径下生成文件{base_name}...\033[0m")
|
55 |
-
Id = input("请输入SecretId:")
|
56 |
-
Key = input("请输入SecretKey:")
|
57 |
-
client, bucket = self.hy_sdk_client(Id, Key)
|
58 |
-
data_bytes = client.get_object(Bucket=bucket,Key=base_name)["Body"].get_raw_stream().read()
|
59 |
-
data = json.loads(data_bytes.decode("utf-8"))
|
60 |
-
# data["SecretId"] = Id # 未来可以把这个加上
|
61 |
-
# data["SecretKey"] = Key
|
62 |
-
with open(path, "w") as f:
|
63 |
-
data_str = json.dumps(data, ensure_ascii=False)
|
64 |
-
# 如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。
|
65 |
-
# 如果 ensure_ascii 是 false,这些字符会原样输出。
|
66 |
-
f.write(data_str)
|
67 |
-
f.close()
|
68 |
-
print(f"\033[32m 配置文件保存成功\033[0m")
|
69 |
-
return data
|
70 |
-
except json.decoder.JSONDecodeError:
|
71 |
-
print(f"\033[31m WARNING: 配置文件为空!\033[0m")
|
72 |
-
return {}
|
73 |
-
|
74 |
-
def load_file(self, cloud_path:str, local_path:str):
|
75 |
-
"""
|
76 |
-
从COS中下载文件到本地,本函数将会被默认执行的,在使用的时候建议加一些限制.
|
77 |
-
:param cloud_path: 云端的文件路径
|
78 |
-
:param local_path: 将云端文件保存在本地的路径
|
79 |
-
"""
|
80 |
-
if os.path.isdir(cloud_path):
|
81 |
-
raise ProcessError("请输入具体的云端文件路径,而非文件夹!")
|
82 |
-
if os.path.isdir(local_path):
|
83 |
-
raise ProcessError("请输入具体的本地文件路径,而非文件夹!")
|
84 |
-
dir_name = os.path.dirname(local_path)
|
85 |
-
base_name = os.path.basename(local_path)
|
86 |
-
try:
|
87 |
-
os.makedirs(dir_name)
|
88 |
-
except FileExistsError:
|
89 |
-
pass
|
90 |
-
cloud_name = os.path.basename(cloud_path)
|
91 |
-
print(f"\033[31m 请注意,接下来会在{dir_name}路径下生成文件{base_name}\033[0m")
|
92 |
-
Id = input("请输入SecretId:")
|
93 |
-
Key = input("请输入SecretKey:")
|
94 |
-
client, bucket = self.hy_sdk_client(Id, Key)
|
95 |
-
print(f"\033[34m 正在从COS中下载文件: {cloud_name}, 此过程可能耗费一些时间...\033[0m")
|
96 |
-
data_bytes = client.get_object(Bucket=bucket,Key=cloud_path)["Body"].get_raw_stream().read()
|
97 |
-
# data["SecretId"] = Id # 未来可以把这个加上
|
98 |
-
# data["SecretKey"] = Key
|
99 |
-
with open(local_path, "wb") as f:
|
100 |
-
# 如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。
|
101 |
-
# 如果 ensure_ascii 是 false,这些字符会原样输出。
|
102 |
-
f.write(data_bytes)
|
103 |
-
f.close()
|
104 |
-
print(f"\033[32m 文件保存成功\033[0m")
|
105 |
-
|
106 |
-
|
107 |
-
class CosConf(GetConfig):
|
108 |
-
"""
|
109 |
-
从安全的角度出发,将一些默认配置文件上传至COS中,接下来使用COS和它的子类的时候,在第一次使用时需要输入Cuny给的id和key
|
110 |
-
用于连接cos存储桶,下载配置文��.
|
111 |
-
当然,在service_default_download = False的时候,如果在运行路径下已经有conf/service_config.json文件了,
|
112 |
-
那么就不用再次下载了,也不用输入id和key
|
113 |
-
事实上这只需要运行一次,因为配置文件将会被下载至源码文件夹中
|
114 |
-
如果要自定义路径,请在继承的子类中编写__init__函数,将service_path定向到指定路径
|
115 |
-
"""
|
116 |
-
def __init__(self) -> None:
|
117 |
-
# 下面这些参数是类的共享参数
|
118 |
-
self.__SECRET_ID: str = None # 服务的id
|
119 |
-
self.__SECRET_KEY: str = None # 服务的key
|
120 |
-
self.__REGION: str = None # 服务的存储桶地区
|
121 |
-
self.__TOKEN: str = None # 服务的token,目前一直是None
|
122 |
-
self.__SCHEME: str = None # 服务的访问协议,默认实际上是https
|
123 |
-
self.__BUCKET: str = None # 服务的存储桶
|
124 |
-
self.__SERVICE_CONFIG: dict = None # 服务的配置文件
|
125 |
-
self.service_path: str = f"{local_path_}/conf/service_config.json"
|
126 |
-
# 配置文件路径,默认是函数运行的路径下的conf文件夹
|
127 |
-
self.service_default_download = False # 是否在每次访问配置的时候都重新下载文件
|
128 |
-
|
129 |
-
@property
|
130 |
-
def service_config(self):
|
131 |
-
if self.__SERVICE_CONFIG is None or self.service_default_download is True:
|
132 |
-
self.__SERVICE_CONFIG = self.load_json(self.service_path, self.service_default_download)
|
133 |
-
return self.__SERVICE_CONFIG
|
134 |
-
|
135 |
-
@property
|
136 |
-
def client(self):
|
137 |
-
client_config = CosConfig(Region=self.region,
|
138 |
-
SecretId=self.secret_id,
|
139 |
-
SecretKey=self.secret_key,
|
140 |
-
Token=self.token,
|
141 |
-
Scheme=self.scheme)
|
142 |
-
return CosS3Client(client_config)
|
143 |
-
|
144 |
-
def get_key(self, key:str):
|
145 |
-
try:
|
146 |
-
data = self.service_config[key]
|
147 |
-
if data == "None":
|
148 |
-
return None
|
149 |
-
else:
|
150 |
-
return data
|
151 |
-
except KeyError:
|
152 |
-
print(f"\033[31m没有对应键值{key},默认返回None\033[0m")
|
153 |
-
return None
|
154 |
-
|
155 |
-
@property
|
156 |
-
def secret_id(self):
|
157 |
-
if self.__SECRET_ID is None:
|
158 |
-
self.__SECRET_ID = self.get_key("SECRET_ID")
|
159 |
-
return self.__SECRET_ID
|
160 |
-
|
161 |
-
@secret_id.setter
|
162 |
-
def secret_id(self, value:str):
|
163 |
-
self.__SECRET_ID = value
|
164 |
-
|
165 |
-
@property
|
166 |
-
def secret_key(self):
|
167 |
-
if self.__SECRET_KEY is None:
|
168 |
-
self.__SECRET_KEY = self.get_key("SECRET_KEY")
|
169 |
-
return self.__SECRET_KEY
|
170 |
-
|
171 |
-
@secret_key.setter
|
172 |
-
def secret_key(self, value:str):
|
173 |
-
self.__SECRET_KEY = value
|
174 |
-
|
175 |
-
@property
|
176 |
-
def region(self):
|
177 |
-
if self.__REGION is None:
|
178 |
-
self.__REGION = self.get_key("REGION")
|
179 |
-
return self.__REGION
|
180 |
-
|
181 |
-
@region.setter
|
182 |
-
def region(self, value:str):
|
183 |
-
self.__REGION = value
|
184 |
-
|
185 |
-
@property
|
186 |
-
def token(self):
|
187 |
-
# if self.__TOKEN is None:
|
188 |
-
# self.__TOKEN = self.get_key("TOKEN")
|
189 |
-
# 这里可以注释掉
|
190 |
-
return self.__TOKEN
|
191 |
-
|
192 |
-
@token.setter
|
193 |
-
def token(self, value:str):
|
194 |
-
self.__TOKEN= value
|
195 |
-
|
196 |
-
@property
|
197 |
-
def scheme(self):
|
198 |
-
if self.__SCHEME is None:
|
199 |
-
self.__SCHEME = self.get_key("SCHEME")
|
200 |
-
return self.__SCHEME
|
201 |
-
|
202 |
-
@scheme.setter
|
203 |
-
def scheme(self, value:str):
|
204 |
-
self.__SCHEME = value
|
205 |
-
|
206 |
-
@property
|
207 |
-
def bucket(self):
|
208 |
-
if self.__BUCKET is None:
|
209 |
-
self.__BUCKET = self.get_key("BUCKET")
|
210 |
-
return self.__BUCKET
|
211 |
-
|
212 |
-
@bucket.setter
|
213 |
-
def bucket(self, value):
|
214 |
-
self.__BUCKET = value
|
215 |
-
|
216 |
-
def downloadFile_COS(self, key, bucket:str=None, if_read:bool=False):
|
217 |
-
"""
|
218 |
-
从COS下载对象(二进制数据), 如果下载失败就返回None
|
219 |
-
"""
|
220 |
-
CosBucket = self.bucket if bucket is None else bucket
|
221 |
-
try:
|
222 |
-
# 将本类的Debug继承给抛弃了
|
223 |
-
# self.debug_print(f"Download from {CosBucket}", font_color="blue")
|
224 |
-
obj = self.client.get_object(
|
225 |
-
Bucket=CosBucket,
|
226 |
-
Key=key
|
227 |
-
)
|
228 |
-
if if_read is True:
|
229 |
-
data = obj["Body"].get_raw_stream().read() # byte
|
230 |
-
return data
|
231 |
-
else:
|
232 |
-
return obj
|
233 |
-
except Exception as e:
|
234 |
-
print(f"\033[31m下载失败! 错误描述:{e}\033[0m")
|
235 |
-
return None
|
236 |
-
|
237 |
-
def showFileList_COS_base(self, key, bucket, marker:str=""):
|
238 |
-
"""
|
239 |
-
返回cos存储桶内部的某个文件夹的内部名称
|
240 |
-
:param key: cos云端的存储路径
|
241 |
-
:param bucket: cos存储桶名称,如果没指定名称(None)就会寻找默认的存储桶
|
242 |
-
:param marker: 标记,用于记录上次查询到哪里了
|
243 |
-
ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称 (s是对象实例)
|
244 |
-
返回的内容存储在response["Content"],不过返回的��据大小是有限制的,具体内容还是请看官方文档。
|
245 |
-
"""
|
246 |
-
response = self.client.list_objects(
|
247 |
-
Bucket=bucket,
|
248 |
-
Prefix=key,
|
249 |
-
Marker=marker
|
250 |
-
)
|
251 |
-
return response
|
252 |
-
|
253 |
-
def showFileList_COS(self, key, bucket:str=None)->list:
|
254 |
-
"""
|
255 |
-
实现查询存储桶中所有对象的操作,因为cos的sdk有返回数据包大小的限制,所以我们需要进行一定的改动
|
256 |
-
"""
|
257 |
-
marker = ""
|
258 |
-
file_list = []
|
259 |
-
CosBucket = self.bucket if bucket is None else bucket
|
260 |
-
while True: # 轮询
|
261 |
-
response = self.showFileList_COS_base(key, CosBucket, marker)
|
262 |
-
try:
|
263 |
-
file_list.extend(response["Contents"])
|
264 |
-
except KeyError as e:
|
265 |
-
print(e)
|
266 |
-
raise
|
267 |
-
if response['IsTruncated'] == 'false': # 接下来没有数据了,就退出
|
268 |
-
break
|
269 |
-
marker = response['NextMarker']
|
270 |
-
return file_list
|
271 |
-
|
272 |
-
def uploadFile_COS(self, buffer, key, bucket:str=None):
|
273 |
-
"""
|
274 |
-
从COS上传数据,需要注意的是必须得是二进制文件
|
275 |
-
"""
|
276 |
-
CosBucket = self.bucket if bucket is None else bucket
|
277 |
-
try:
|
278 |
-
self.client.put_object(
|
279 |
-
Bucket=CosBucket,
|
280 |
-
Body=buffer,
|
281 |
-
Key=key
|
282 |
-
)
|
283 |
-
return True
|
284 |
-
except Exception as e:
|
285 |
-
print(e)
|
286 |
-
return False
|
287 |
-
|
288 |
-
|
289 |
-
class FuncDiary(CosConf):
|
290 |
-
filter_dict = {"60a5e13da00e6e0001fd53c8": "Cuny",
|
291 |
-
"612c290f3a9af4000170faad": "守望平凡",
|
292 |
-
"614de96e1259260001506d6c": "林泽毅-焕影一新"}
|
293 |
-
|
294 |
-
def __init__(self, func_name: str, uid: str, error_conf_path: str = f"{local_path_}/conf/func_error_conf.json"):
|
295 |
-
"""
|
296 |
-
日志类的实例化
|
297 |
-
Args:
|
298 |
-
func_name: 功能名称,影响了日志投递的路径
|
299 |
-
"""
|
300 |
-
super().__init__()
|
301 |
-
# 配置文件路径,默认是函数运行的路径下的conf文件夹
|
302 |
-
self.service_path: str = os.path.join(os.path.dirname(error_conf_path), "service_config.json")
|
303 |
-
self.error_dict = self.load_json(path=error_conf_path)
|
304 |
-
self.__up: str = f"wx/invokeFunction_c/{datetime.datetime.now().strftime('%Y/%m/%d/%H')}/{func_name}/"
|
305 |
-
self.func_name: str = func_name
|
306 |
-
# 下面这个属性是的日志名称的前缀
|
307 |
-
self.__start_time = datetime.datetime.now().timestamp()
|
308 |
-
h_point = datetime.datetime.strptime(datetime.datetime.now().strftime('%Y/%m/%d/%H'), '%Y/%m/%d/%H')
|
309 |
-
h_point_timestamp = h_point.timestamp()
|
310 |
-
self.__prefix = int(self.__start_time - h_point_timestamp).__str__() + "_"
|
311 |
-
self.__uid = uid
|
312 |
-
self.__diary = None
|
313 |
-
|
314 |
-
def __str__(self):
|
315 |
-
return f"<{self.func_name}> DIARY for {self.__uid}"
|
316 |
-
|
317 |
-
@property
|
318 |
-
def content(self):
|
319 |
-
return self.__diary
|
320 |
-
|
321 |
-
@content.setter
|
322 |
-
def content(self, value: str):
|
323 |
-
if not isinstance(value, dict):
|
324 |
-
raise TypeError("content 只能是字典!")
|
325 |
-
if "status" in value:
|
326 |
-
raise KeyError("status字段已被默认占用,请在日志信息中更换字段名称!")
|
327 |
-
if self.__diary is None:
|
328 |
-
self.__diary = value
|
329 |
-
else:
|
330 |
-
raise PermissionError("为了减小日志对整体代码的影响,<content>只能被覆写一次!")
|
331 |
-
|
332 |
-
def uploadDiary_COS(self, status_id: str, suffix: str = "", bucket: str = "hy-hcy-data-logs-1306602019"):
|
333 |
-
if self.__diary is None:
|
334 |
-
self.__diary = {"status": self.error_dict[status_id]}
|
335 |
-
if status_id == "0000":
|
336 |
-
self.__up += f"True/{self.__uid}/"
|
337 |
-
else:
|
338 |
-
self.__up += f"False/{self.__uid}/"
|
339 |
-
interval = int(10 * (datetime.datetime.now().timestamp() - self.__start_time))
|
340 |
-
prefix = self.__prefix + status_id + "_" + interval.__str__()
|
341 |
-
self.__diary["status"] = self.error_dict[status_id]
|
342 |
-
name = prefix + "_" + suffix if len(suffix) != 0 else prefix
|
343 |
-
self.uploadFile_COS(buffer=json.dumps(self.__diary), key=self.__up + name, bucket=bucket)
|
344 |
-
print(f"{self}上传成功.")
|
345 |
-
|
346 |
-
|
347 |
-
class ResponseWebSocket(CosConf):
|
348 |
-
# 网关推送地址
|
349 |
-
__HOST:str = None
|
350 |
-
@property
|
351 |
-
def sendBackHost(self):
|
352 |
-
if self.__HOST is None:
|
353 |
-
self.__HOST = self.get_key("HOST")
|
354 |
-
return self.__HOST
|
355 |
-
|
356 |
-
@sendBackHost.setter
|
357 |
-
def sendBackHost(self, value):
|
358 |
-
self.__HOST = value
|
359 |
-
|
360 |
-
def sendMsg_toWebSocket(self, message,connectionID:str = None):
|
361 |
-
if connectionID is not None:
|
362 |
-
retmsg = {'websocket': {}}
|
363 |
-
retmsg['websocket']['action'] = "data send"
|
364 |
-
retmsg['websocket']['secConnectionID'] = connectionID
|
365 |
-
retmsg['websocket']['dataType'] = 'text'
|
366 |
-
retmsg['websocket']['data'] = json.dumps(message)
|
367 |
-
requests.post(self.sendBackHost, json=retmsg)
|
368 |
-
print("send success!")
|
369 |
-
else:
|
370 |
-
pass
|
371 |
-
|
372 |
-
@staticmethod
|
373 |
-
def create_Msg(status, msg):
|
374 |
-
"""
|
375 |
-
本方法用于创建一个用于发送到WebSocket客户端的数据
|
376 |
-
输入的信息部分,需要有如下几个参数:
|
377 |
-
1. id,固定为"return-result"
|
378 |
-
2. status,如果输入为1则status=true, 如果输入为-1则status=false
|
379 |
-
3. obj_key, 图片的云端路径, 这是输入的msg本身自带的
|
380 |
-
"""
|
381 |
-
msg['status'] = "false" if status == -1 else 'true' # 其实最好还是用bool
|
382 |
-
msg['id'] = "async-back-msg"
|
383 |
-
msg['type'] = "funcType"
|
384 |
-
msg["format"] = "imageType"
|
385 |
-
return msg
|
386 |
-
|
387 |
-
|
388 |
-
# 功能服务类
|
389 |
-
class Service(ResponseWebSocket):
|
390 |
-
"""
|
391 |
-
服务的主函数,封装了cos上传/下载功能以及与api网关的一键通讯
|
392 |
-
将类的实例变成一个可被调用的对象,在服务运行的时候,只需要运行该对象即可
|
393 |
-
当然,因为是类,所以支持继承和修改
|
394 |
-
"""
|
395 |
-
@classmethod
|
396 |
-
def process(cls, *args, **kwargs):
|
397 |
-
"""
|
398 |
-
处理函数,在使用的时候请将之重构
|
399 |
-
"""
|
400 |
-
pass
|
401 |
-
|
402 |
-
@classmethod
|
403 |
-
def __call__(cls, *args, **kwargs):
|
404 |
-
pass
|
405 |
-
|
406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hivisionai/hyService/dbTools.py
DELETED
@@ -1,337 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import pymongo
|
3 |
-
import datetime
|
4 |
-
import time
|
5 |
-
from .cloudService import GetConfig
|
6 |
-
local_path = os.path.dirname(__file__)
|
7 |
-
|
8 |
-
|
9 |
-
class DBUtils(GetConfig):
|
10 |
-
"""
|
11 |
-
从安全的角度出发,将一些默认配置文件上传至COS中,接下来使用COS和它的子类的时候,在第一次使用时需要输入Cuny给的id和key
|
12 |
-
用于连接数据库等对象
|
13 |
-
当然,在db_default_download = False的时候,如果在运行路径下已经有配置文件了,
|
14 |
-
那么就不用再次下载了,也不用输入id和key
|
15 |
-
事实上这只需要运行一次,因为配置文件将会被下载至源码文件夹中
|
16 |
-
如果要自定义路径,请在继承的子类中编写__init__函数,将service_path定向到指定路径
|
17 |
-
"""
|
18 |
-
__BASE_DIR: dict = None
|
19 |
-
__PARAMS_DIR: dict = None
|
20 |
-
db_base_path: str = f"{local_path}/conf/base_config.json"
|
21 |
-
db_params_path: str = f"{local_path}/conf/params.json"
|
22 |
-
db_default_download: bool = False
|
23 |
-
|
24 |
-
@property
|
25 |
-
def base_config(self):
|
26 |
-
if self.__BASE_DIR is None:
|
27 |
-
self.__BASE_DIR = self.load_json(self.db_base_path, self.db_default_download)
|
28 |
-
return self.__BASE_DIR
|
29 |
-
|
30 |
-
@property
|
31 |
-
def db_config(self):
|
32 |
-
return self.base_config["database_config"]
|
33 |
-
|
34 |
-
@property
|
35 |
-
def params_config(self):
|
36 |
-
if self.__PARAMS_DIR is None:
|
37 |
-
self.__PARAMS_DIR = self.load_json(self.db_params_path, self.db_default_download)
|
38 |
-
return self.__PARAMS_DIR
|
39 |
-
|
40 |
-
@property
|
41 |
-
def size_dir(self):
|
42 |
-
return self.params_config["size_config"]
|
43 |
-
|
44 |
-
@property
|
45 |
-
def func_dir(self):
|
46 |
-
return self.params_config["func_config"]
|
47 |
-
|
48 |
-
@property
|
49 |
-
def wx_config(self):
|
50 |
-
return self.base_config["wx_config"]
|
51 |
-
|
52 |
-
def get_dbClient(self):
|
53 |
-
return pymongo.MongoClient(self.db_config["connect_url"])
|
54 |
-
|
55 |
-
@staticmethod
|
56 |
-
def get_time(yyyymmdd=None, delta_date=0):
|
57 |
-
"""
|
58 |
-
给出当前的时间
|
59 |
-
:param yyyymmdd: 以yyyymmdd给出的日期时间
|
60 |
-
:param delta_date: 获取减去delta_day后的时间,默认为0就是当天
|
61 |
-
时间格式:yyyy_mm_dd
|
62 |
-
"""
|
63 |
-
if yyyymmdd is None:
|
64 |
-
now_time = (datetime.datetime.now() - datetime.timedelta(delta_date)).strftime("%Y-%m-%d")
|
65 |
-
return now_time
|
66 |
-
# 输入了yyyymmdd的数据和delta_date,通过这两个数据返回距离yyyymmdd delta_date天的时间
|
67 |
-
pre_time = datetime.datetime(int(yyyymmdd[0:4]), int(yyyymmdd[4:6]), int(yyyymmdd[6:8]))
|
68 |
-
return (pre_time - datetime.timedelta(delta_date)).strftime("%Y-%m-%d")
|
69 |
-
|
70 |
-
# 获得时间戳
|
71 |
-
def get_timestamp(self, date_time:str=None) -> int:
|
72 |
-
"""
|
73 |
-
输入的日期形式为:"2021-11-29 16:39:45.999"
|
74 |
-
真正必须输入的是前十个字符,及精确到日期,后面的时间可以不输入,不输入则默认置零
|
75 |
-
"""
|
76 |
-
def standardDateTime(dt:str) -> str:
|
77 |
-
"""
|
78 |
-
规范化时间字符串
|
79 |
-
"""
|
80 |
-
if len(dt) < 10:
|
81 |
-
raise ValueError("你必须至少输入准确到天的日期!比如:2021-11-29")
|
82 |
-
elif len(dt) == 10:
|
83 |
-
return dt + " 00:00:00.0"
|
84 |
-
else:
|
85 |
-
try:
|
86 |
-
date, time = dt.split(" ")
|
87 |
-
except ValueError:
|
88 |
-
raise ValueError("你只能也必须在日期与具体时间之间增加一个空格,其他地方不能出现空格!")
|
89 |
-
while len(time) < 10:
|
90 |
-
if len(time) in (2, 5):
|
91 |
-
time += ":"
|
92 |
-
elif len(time) == 8:
|
93 |
-
time += "."
|
94 |
-
else:
|
95 |
-
time += "0"
|
96 |
-
return date + " " + time
|
97 |
-
if date_time is None:
|
98 |
-
# 默认返回当前时间(str), date_time精确到毫秒
|
99 |
-
date_time = datetime.datetime.now()
|
100 |
-
# 转换成时间戳
|
101 |
-
else:
|
102 |
-
date_time = standardDateTime(dt=date_time)
|
103 |
-
date_time = datetime.datetime.strptime(date_time, "%Y-%m-%d %H:%M:%S.%f")
|
104 |
-
timestamp_ms = int(time.mktime(date_time.timetuple()) * 1000.0 + date_time.microsecond / 1000.0)
|
105 |
-
return timestamp_ms
|
106 |
-
|
107 |
-
@staticmethod
|
108 |
-
def get_standardTime(yyyy_mm_dd: str):
|
109 |
-
return yyyy_mm_dd[0:4] + yyyy_mm_dd[5:7] + yyyy_mm_dd[8:10]
|
110 |
-
|
111 |
-
def find_oneDay_data(self, db_name: str, collection_name: str, date: str = None) -> dict:
|
112 |
-
"""
|
113 |
-
获取指定天数的数据,如果date is None,就自动寻找距今最近的有数据的那一天的数据
|
114 |
-
"""
|
115 |
-
df = None # 应该被返回的数据
|
116 |
-
collection = self.get_dbClient()[db_name][collection_name]
|
117 |
-
if date is None: # 自动寻找前几天的数据,最多三十天
|
118 |
-
for delta_date in range(1, 31):
|
119 |
-
date_yyyymmdd = self.get_standardTime(self.get_time(delta_date=delta_date))
|
120 |
-
filter_ = {"date": date_yyyymmdd}
|
121 |
-
df = collection.find_one(filter=filter_)
|
122 |
-
if df is not None:
|
123 |
-
del df["_id"]
|
124 |
-
break
|
125 |
-
else:
|
126 |
-
filter_ = {"date": date}
|
127 |
-
df = collection.find_one(filter=filter_)
|
128 |
-
if df is not None:
|
129 |
-
del df["_id"]
|
130 |
-
return df
|
131 |
-
|
132 |
-
def find_daysData_byPeriod(self, date_period: tuple, db_name: str, col_name: str):
|
133 |
-
# 给出一个指定的范围日期,返回相应的数据(日期的两头都会被寻找)
|
134 |
-
# 这个函数我们默认数据库中的数据是连续的,即不会出现在 20211221 到 20211229 之间有一天没有数据的情况
|
135 |
-
if len(date_period) != 2:
|
136 |
-
raise ValueError("date_period数据结构:(开始日期,截止日期)")
|
137 |
-
start, end = date_period # yyyymmdd
|
138 |
-
delta_date = int(end) - int(start)
|
139 |
-
if delta_date < 0:
|
140 |
-
raise ValueError("传入的日期有误!")
|
141 |
-
collection = self.get_dbClient()[db_name][col_name]
|
142 |
-
date = start
|
143 |
-
while int(date) <= int(end):
|
144 |
-
yield collection.find_one(filter={"date": date})
|
145 |
-
date = self.get_standardTime(self.get_time(date, -1))
|
146 |
-
|
147 |
-
@staticmethod
|
148 |
-
def find_biggest_valueDict(dict_: dict):
|
149 |
-
# 寻找字典中数值最大的字段,要求输入的字典的字段值全为数字
|
150 |
-
while len(dict_) > 0:
|
151 |
-
max_value = 0
|
152 |
-
p = None
|
153 |
-
for key in dict_:
|
154 |
-
if dict_[key] > max_value:
|
155 |
-
p = key
|
156 |
-
max_value = dict_[key]
|
157 |
-
yield p, max_value
|
158 |
-
del dict_[p]
|
159 |
-
|
160 |
-
def copy_andAdd_dict(self, dict_base, dict_):
|
161 |
-
# 深度拷贝字典,将后者赋值给前者
|
162 |
-
# 如果后者的键名在前者已经存在,则直接相加。这就要求两者的数据是数值型
|
163 |
-
for key in dict_:
|
164 |
-
if key not in dict_base:
|
165 |
-
dict_base[key] = dict_[key]
|
166 |
-
else:
|
167 |
-
if isinstance(dict_[key], int) or isinstance(dict_[key], float):
|
168 |
-
dict_base[key] = round(dict_[key] + dict_base[key], 2)
|
169 |
-
else:
|
170 |
-
dict_base[key] = self.copy_andAdd_dict(dict_base[key], dict_[key])
|
171 |
-
return dict_base
|
172 |
-
|
173 |
-
@staticmethod
|
174 |
-
def compare_data(dict1: dict, dict2: dict, suffix: str, save: int, **kwargs):
|
175 |
-
"""
|
176 |
-
有两个字典,并且通过kwargs会传输一个新的字典,根据字典中的键值我们进行比对,处理成相应的数据格式
|
177 |
-
并且在dict1中,生成一个新的键值,为kwargs中的元素+suffix
|
178 |
-
save:保留几位小数
|
179 |
-
"""
|
180 |
-
new_dict = dict1.copy()
|
181 |
-
for key in kwargs:
|
182 |
-
try:
|
183 |
-
if kwargs[key] not in dict2 or int(dict2[kwargs[key]]) == -1 or float(dict1[kwargs[key]]) <= 0.0:
|
184 |
-
# 数据不存在
|
185 |
-
data_new = 5002
|
186 |
-
else:
|
187 |
-
try:
|
188 |
-
data_new = round(
|
189 |
-
((float(dict1[kwargs[key]]) - float(dict2[kwargs[key]])) / float(dict2[kwargs[key]])) * 100
|
190 |
-
, save)
|
191 |
-
except ZeroDivisionError:
|
192 |
-
data_new = 5002
|
193 |
-
if data_new == 0.0:
|
194 |
-
data_new = 0
|
195 |
-
except TypeError as e:
|
196 |
-
print(e)
|
197 |
-
data_new = 5002 # 如果没有之前的数据,默认返回0
|
198 |
-
new_dict[kwargs[key] + suffix] = data_new
|
199 |
-
return new_dict
|
200 |
-
|
201 |
-
@staticmethod
|
202 |
-
def sum_dictList_byKey(dictList: list, **kwargs) -> dict:
|
203 |
-
"""
|
204 |
-
有一个列表,列表中的元素为字典,并且所有字典都有一个键值为key的字段,字段值为数字
|
205 |
-
我们将每一个字典的key字段提取后相加,得到该字段值之和.
|
206 |
-
"""
|
207 |
-
sum_num = {}
|
208 |
-
if kwargs is None:
|
209 |
-
raise ImportError("Please input at least ONE key")
|
210 |
-
for key in kwargs:
|
211 |
-
sum_num[kwargs[key]] = 0
|
212 |
-
for dict_ in dictList:
|
213 |
-
if not isinstance(dict_, dict):
|
214 |
-
raise TypeError("object is not DICT!")
|
215 |
-
for key in kwargs:
|
216 |
-
sum_num[kwargs[key]] += dict_[kwargs[key]]
|
217 |
-
return sum_num
|
218 |
-
|
219 |
-
@staticmethod
|
220 |
-
def sum_2ListDict(list_dict1: list, list_dict2: list, key_name, data_name):
|
221 |
-
"""
|
222 |
-
有两个列表,列表内的元素为字典,我们根据key所对应的键值寻找列表中键值相同的两个元素,将他们的data对应的键值相加
|
223 |
-
生成新的列表字典(其余键值被删除)
|
224 |
-
key仅在一个列表中存在,则直接加入新的列表字典
|
225 |
-
"""
|
226 |
-
sum_list = []
|
227 |
-
|
228 |
-
def find_sameKey(kn, key_, ld: list) -> int:
|
229 |
-
for dic_ in ld:
|
230 |
-
if dic_[kn] == key_:
|
231 |
-
post_ = ld.index(dic_)
|
232 |
-
return post_
|
233 |
-
return -1
|
234 |
-
|
235 |
-
for dic in list_dict1:
|
236 |
-
key = dic[key_name] # 键名
|
237 |
-
post = find_sameKey(key_name, key, list_dict2) # 在list2中寻找相同的位置
|
238 |
-
data = dic[data_name] + list_dict2[post][data_name] if post != -1 else dic[data_name]
|
239 |
-
sum_list.append({key_name: key, data_name: data})
|
240 |
-
return sum_list
|
241 |
-
|
242 |
-
@staticmethod
|
243 |
-
def find_biggest_dictList(dictList: list, key: str = "key", data: str = "value"):
|
244 |
-
"""
|
245 |
-
有一个列表,里面每一个元素都是一个字典
|
246 |
-
这些字典有一些共通性质,那就是里面都有一个key键名和一个data键名,后者的键值必须是数字
|
247 |
-
我们根据data键值的大小进行生成,每一次返回列表中data键值最大的数和它的key键值
|
248 |
-
"""
|
249 |
-
while len(dictList) > 0:
|
250 |
-
point = 0
|
251 |
-
biggest_num = int(dictList[0][data])
|
252 |
-
biggest_key = dictList[0][key]
|
253 |
-
for i in range(len(dictList)):
|
254 |
-
num = int(dictList[i][data])
|
255 |
-
if num > biggest_num:
|
256 |
-
point = i
|
257 |
-
biggest_num = int(dictList[i][data])
|
258 |
-
biggest_key = dictList[i][key]
|
259 |
-
yield str(biggest_key), biggest_num
|
260 |
-
del dictList[point]
|
261 |
-
|
262 |
-
def get_share_data(self, date_yyyymmdd: str):
|
263 |
-
# 获得用户界面情况
|
264 |
-
visitPage = self.find_oneDay_data(date=date_yyyymmdd,
|
265 |
-
db_name="cuny-user-analysis",
|
266 |
-
collection_name="daily-userVisitPage")
|
267 |
-
if visitPage is not None:
|
268 |
-
# 这一部分没有得到数据是可以容忍的.不用抛出模态框错误
|
269 |
-
# 获得昨日用户分享情况
|
270 |
-
sum_num = self.sum_dictList_byKey(dictList=visitPage["data_list"],
|
271 |
-
key1="page_share_pv",
|
272 |
-
key2="page_share_uv")
|
273 |
-
else:
|
274 |
-
# 此时将分享次数等置为-1
|
275 |
-
sum_num = {"page_share_pv": -1, "page_share_uv": -1}
|
276 |
-
return sum_num
|
277 |
-
|
278 |
-
@staticmethod
|
279 |
-
def compare_date(date1_yyyymmdd: str, date2_yyyymmdd: str):
|
280 |
-
# 如果date1是date2的昨天,那么就返回True
|
281 |
-
date1 = int(date1_yyyymmdd)
|
282 |
-
date2 = int(date2_yyyymmdd)
|
283 |
-
return True if date2 - date1 == 1 else False
|
284 |
-
|
285 |
-
def change_time(self, date_yyyymmdd: str, mode: int):
|
286 |
-
# 将yyyymmdd的数据分开为相应的数据形式
|
287 |
-
if mode == 1:
|
288 |
-
if self.compare_date(date_yyyymmdd, self.get_standardTime(self.get_time(delta_date=0))) is False:
|
289 |
-
return date_yyyymmdd[0:4] + "年" + date_yyyymmdd[4:6] + "月" + date_yyyymmdd[6:8] + "日"
|
290 |
-
else:
|
291 |
-
return "昨日"
|
292 |
-
elif mode == 2:
|
293 |
-
date = date_yyyymmdd[0:4] + "." + date_yyyymmdd[4:6] + "." + date_yyyymmdd[6:8]
|
294 |
-
if self.compare_date(date_yyyymmdd, self.get_standardTime(self.get_time(delta_date=0))) is True:
|
295 |
-
return date + "~" + date + " | 昨日"
|
296 |
-
else:
|
297 |
-
return date + "~" + date
|
298 |
-
|
299 |
-
@staticmethod
|
300 |
-
def changeList_dict2List_list(dl: list, order: list):
|
301 |
-
"""
|
302 |
-
列表内是一个个字典,本函数将字典拆解,以order的形式排列键值为列表
|
303 |
-
考虑到一些格式的问题,这里我采用生成器的形式封装
|
304 |
-
"""
|
305 |
-
for dic in dl:
|
306 |
-
# dic是列表内的字典元素
|
307 |
-
tmp = []
|
308 |
-
for key_name in order:
|
309 |
-
key = dic[key_name]
|
310 |
-
tmp.append(key)
|
311 |
-
yield tmp
|
312 |
-
|
313 |
-
def dict_mapping(self, dict_name: str, id_: str):
|
314 |
-
"""
|
315 |
-
进行字典映射,输入字典名称和键名,返回具体的键值
|
316 |
-
如果不存在,则原路返回键名
|
317 |
-
"""
|
318 |
-
try:
|
319 |
-
return getattr(self, dict_name)[id_]
|
320 |
-
except KeyError:
|
321 |
-
return id_
|
322 |
-
except AttributeError:
|
323 |
-
print(f"[WARNING]: 本对象内部不存在{dict_name}!")
|
324 |
-
return id_
|
325 |
-
|
326 |
-
@staticmethod
|
327 |
-
def dictAddKey(dic: dict, dic_tmp: dict, **kwargs):
|
328 |
-
"""
|
329 |
-
往字典中加入参数,可迭代
|
330 |
-
"""
|
331 |
-
for key in kwargs:
|
332 |
-
dic[key] = dic_tmp[key]
|
333 |
-
return dic
|
334 |
-
|
335 |
-
|
336 |
-
if __name__ == "__main__":
|
337 |
-
dbu = DBUtils()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hivisionai/hyService/error.py
DELETED
@@ -1,20 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
@author: cuny
|
3 |
-
@fileName: error.py
|
4 |
-
@create_time: 2022/03/10 下午3:14
|
5 |
-
@introduce:
|
6 |
-
保存一些定义的错误类型
|
7 |
-
"""
|
8 |
-
class ProcessError(Exception):
|
9 |
-
def __init__(self, err):
|
10 |
-
super().__init__(err)
|
11 |
-
self.err = err
|
12 |
-
def __str__(self):
|
13 |
-
return self.err
|
14 |
-
|
15 |
-
class WrongImageType(TypeError):
|
16 |
-
def __init__(self, err):
|
17 |
-
super().__init__(err)
|
18 |
-
self.err = err
|
19 |
-
def __str__(self):
|
20 |
-
return self.err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|