TheEeeeLin commited on
Commit
ca46a75
1 Parent(s): 22af0df
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore copy +16 -0
  2. .vscode/extensions.json +20 -0
  3. .vscode/settings.json +71 -0
  4. Dockerfile +7 -6
  5. LICENSE +201 -0
  6. README.md +221 -44
  7. README_EN.md +0 -155
  8. app.py +107 -107
  9. app.spec +44 -0
  10. assets/hivisionai.ico +0 -0
  11. assets/title.md +11 -0
  12. beautyPlugin/GrindSkin.py +0 -43
  13. beautyPlugin/MakeBeautiful.py +0 -45
  14. beautyPlugin/MakeWhiter.py +0 -108
  15. beautyPlugin/ThinFace.py +0 -267
  16. beautyPlugin/__init__.py +0 -4
  17. beautyPlugin/lut_image/1.png +0 -0
  18. beautyPlugin/lut_image/3.png +0 -0
  19. beautyPlugin/lut_image/lutOrigin.png +0 -0
  20. {images → demo/images}/test.jpg +0 -0
  21. {images → demo/images}/test2.jpg +0 -0
  22. {images → demo/images}/test3.jpg +0 -0
  23. {images → demo/images}/test4.jpg +0 -0
  24. {output → demo/kb_output}/.gitkeep +0 -0
  25. size_list_CN.csv → demo/size_list_CN.csv +0 -0
  26. demo/size_list_EN.csv +19 -0
  27. data_utils.py → demo/utils.py +4 -3
  28. deploy_api.py +86 -67
  29. docker-compose.yml +11 -0
  30. docs/api_CN.md +552 -0
  31. docs/api_EN.md +551 -0
  32. hivision/__init__.py +4 -0
  33. hivision/creator/__init__.py +110 -0
  34. hivision/creator/context.py +104 -0
  35. hivision/creator/face_detector.py +42 -0
  36. hivision/creator/human_matting.py +116 -0
  37. src/layoutCreate.py → hivision/creator/layout_calculator.py +56 -31
  38. {src → hivision/creator}/move_image.py +13 -13
  39. hivision/creator/photo_adjuster.py +258 -0
  40. {hivisionai/hycv → hivision/creator}/tensor2numpy.py +14 -14
  41. hivision/creator/utils.py +142 -0
  42. hivisionai/__init__.py → hivision/creator/weights/.gitkeep +0 -0
  43. hivision_modnet.onnx → hivision/creator/weights/modnet_photographic_portrait_matting.onnx +2 -2
  44. hivision/error.py +21 -0
  45. hivision/utils.py +253 -0
  46. hivisionai/app.py +0 -452
  47. hivisionai/hyService/__init__.py +0 -0
  48. hivisionai/hyService/cloudService.py +0 -406
  49. hivisionai/hyService/dbTools.py +0 -337
  50. 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.aliyun.com/@g /etc/apt/sources.list && \
6
- sed -i s@/security.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \
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 8080
33
 
34
- ENTRYPOINT ["python3", "deploy_api.py"]
 
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: 3.38.0
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://img.shields.io/static/v1?label=在线体验&message=SwanHub%20Demo&color=blue)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)
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
- - 在线体验: [![SwanHub Demo](https://img.shields.io/static/v1?label=Demo&message=SwanHub%20Demo&color=blue)](https://swanhub.co/ZeYiLin/HivisionIDPhotos/demo)
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
- > 🚀谢谢你对我们的工作感兴趣。您可能还想查看我们在图像领域的其他成果,欢迎来信:zeyi.lin@swanhub.co.
39
 
40
- HivisionIDPhoto旨在开发一种实用的证件照智能制作算法。
41
 
42
  它利用一套完善的模型工作流程,实现对多种用户拍照场景的识别、抠图与证件照生成。
43
 
 
44
 
45
- **HivisionIDPhoto可以做到:**
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
- **请求API服务(Python)**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- 用Python给服务发送请求:
109
 
110
- 证件照制作(输入1张照片,获得1张标准证件照和1张高清证件照的4通道透明png):
111
 
112
  ```bash
113
- python requests_api.py -u http://127.0.0.1:8080 -i test.jpg -o ./idphoto.png -s '(413,295)'
 
114
  ```
115
 
116
- 增加底色(输入1张4通道透明png,获得1张增加了底色的图像):
117
 
118
  ```bash
119
- 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)'
 
120
  ```
121
 
122
- 得到六寸排版照(输入1张3通道照片,获得1张六寸排版照):
 
 
123
 
124
  ```bash
125
- 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)'
126
  ```
127
 
128
- <br>
129
 
130
- # 🐳Docker部署
 
 
 
 
131
 
132
- 在确保将模型权重文件[hivision_modnet.onnx](https://github.com/Zeyi-Lin/HivisionIDPhotos/releases/tag/pretrained-model)放到根目录下后,在根目录执行:
133
 
134
  ```bash
135
- docker build -t hivision_idphotos .
 
 
 
 
 
 
 
 
136
  ```
137
 
138
- 等待镜像封装完毕后,运行以下指令,即可开启API服务:
 
 
139
 
140
  ```bash
141
- docker run -p 8080:8080 hivision_idphotos
142
  ```
143
 
144
  <br>
145
 
 
 
 
 
 
 
 
146
 
147
- # 引用项目
148
 
149
- 1. MTCNN: https://github.com/ipazc/mtcnn
150
- 2. ModNet: https://github.com/ZHKKKe/MODNet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  <br>
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- # 📧联系我们
156
 
157
- 如果您有任何问题,请发邮件至 zeyi.lin@swanhub.co
 
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 onnxruntime
4
- from src.face_judgement_align import IDphotos_create
5
- from hivisionai.hycv.vision import add_background
6
- from src.layoutCreate import generate_layout_photo, generate_layout_image
 
 
 
7
  import pathlib
8
  import numpy as np
9
- from image_utils import resize_image_to_kb
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
- result_image_hd,
155
- result_image_standard,
156
- typography_arr,
157
- typography_rotate,
158
- _,
159
- _,
160
- _,
161
- _,
162
- status,
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 = generate_layout_image(
244
- result_image_standard,
245
- typography_arr,
246
- typography_rotate,
247
- height=idphoto_json["size"][0],
248
- width=idphoto_json["size"][1],
 
 
 
249
  )
250
 
251
  # 如果输出 KB 大小选择的是自定义
@@ -255,7 +237,7 @@ def idphoto_inference(
255
  # 输出路径为一个根据时间戳 + 哈希值生成的随机文件名
256
  import time
257
 
258
- output_image_path = f"./output/{int(time.time())}.jpg"
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
- result_messgae = {
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
- result_messgae = {
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 result_messgae
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
- h1#title, h3 {
312
- text-align: center;
313
- }
314
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
  demo = gr.Blocks(css=css)
317
 
318
  with demo:
319
- gr.Markdown(title)
320
- gr.Markdown(description)
321
  with gr.Row():
322
  # ------------ 左半边 UI ----------------
323
  with gr.Column():
324
- img_input = gr.Image().style(height=350)
325
  language_options = gr.Dropdown(
326
- choices=language, label="Language", value="English", elem_id="language"
327
  )
328
 
329
  mode_options = gr.Radio(
330
- choices=size_mode_EN,
331
- label="ID photo size options",
332
- value="Size List",
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=size_list_EN,
340
- label="Default size",
341
- value="One inch",
342
  elem_id="size_list",
343
  )
344
 
@@ -352,7 +356,7 @@ if __name__ == "__main__":
352
 
353
  # 左:背景色选项
354
  color_options = gr.Radio(
355
- choices=colors_EN, label="Background color", value="Blue", elem_id="color"
356
  )
357
 
358
  # 左:如果选择「自定义底色」,显示 RGB 输入框
@@ -363,17 +367,17 @@ if __name__ == "__main__":
363
 
364
  # 左:渲染方式选项
365
  render_options = gr.Radio(
366
- choices=render_EN,
367
- label="Rendering mode",
368
- value="Solid Color",
369
  elem_id="render",
370
  )
371
 
372
  # 左:输出 KB 大小选项
373
  image_kb_options = gr.Radio(
374
- choices=image_kb_EN,
375
- label="Set KB size (Download in the bottom right)",
376
- value="Not Set",
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 size",
387
  interactive=True,
388
  )
389
 
390
- img_but = gr.Button("Start", elem_id="start")
391
 
392
  # 案例图片
393
- example_images = gr.Dataset(
394
- components=[img_input],
395
- samples=[
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="Status", visible=False)
408
  with gr.Row():
409
- img_output_standard = gr.Image(label="Standard photo").style(height=350)
410
- img_output_standard_hd = gr.Image(label="HD photo").style(height=350)
411
- img_output_layout = gr.Image(label="Layout photo").style(height=350)
412
- file_download = gr.File(label="Download the photo after adjusting the KB size", visible=False)
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="One inch",
458
  ),
459
  mode_options: gr.update(
460
  label="ID photo size options",
@@ -582,17 +586,13 @@ if __name__ == "__main__":
582
  ],
583
  )
584
 
585
- example_images.click(
586
- fn=set_example_image, inputs=[example_images], outputs=[img_input]
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()
 
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> &ensp;
7
+ <a href="https://docs.qq.com/doc/DUkpBdk90eWZFS2JW" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/WeChat-微信-4cb55e"></a> &ensp;
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
- size_list_dict[size_name] = (int(h), int(w))
 
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 src.face_judgement_align import IDphotos_create
4
- from src.layoutCreate import generate_layout_photo, generate_layout_image
5
- from hivisionai.hycv.vision import add_background
 
 
 
 
6
  import base64
7
  import numpy as np
8
  import cv2
9
- import ast
10
 
11
  app = FastAPI()
 
12
 
13
 
14
  # 将图像转换为Base64编码
15
-
16
  def numpy_2_base64(img: np.ndarray):
17
- retval, buffer = cv2.imencode('.png', img)
18
- base64_image = base64.b64encode(buffer).decode('utf-8')
19
 
20
  return base64_image
21
 
22
 
23
  # 证件照智能制作接口
24
  @app.post("/idphoto")
25
- async def idphoto_inference(input_image: UploadFile,
26
- size: str = Form(...),
27
- head_measure_ratio=0.2,
28
- head_height_ratio=0.45,
29
- top_distance_max=0.12,
30
- top_distance_min=0.10):
 
 
 
 
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 = ast.literal_eval(size)
37
-
38
- result_image_hd, result_image_standard, typography_arr, typography_rotate, \
39
- _, _, _, _, status = IDphotos_create(img,
40
- size=size,
41
- head_measure_ratio=head_measure_ratio,
42
- head_height_ratio=head_height_ratio,
43
- align=False,
44
- beauty=False,
45
- fd68=None,
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
- result_messgae = {
60
  "status": True,
61
- "img_output_standard": numpy_2_base64(result_image_standard),
62
- "img_output_standard_hd": numpy_2_base64(result_image_hd),
63
  }
64
 
65
- return result_messgae
66
 
67
 
68
  # 透明图像添加纯色背景接口
69
  @app.post("/add_background")
70
- async def photo_add_background(input_image: UploadFile,
71
- color: str = Form(...)):
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
- "image": numpy_2_base64(add_background(img, bgr=color)),
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(input_image: UploadFile, size: str = Form(...)):
102
- try:
103
- image_bytes = await input_image.read()
104
- nparr = np.frombuffer(image_bytes, np.uint8)
105
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 
 
 
 
 
106
 
107
- size = ast.literal_eval(size)
108
 
109
- typography_arr, typography_rotate = generate_layout_photo(input_height=size[0],
110
- input_width=size[1])
 
111
 
112
- result_layout_image = generate_layout_image(img, typography_arr,
113
- typography_rotate,
114
- height=size[0],
115
- width=size[1])
116
 
117
- result_messgae = {
118
- "status": True,
119
- "image": numpy_2_base64(result_layout_image),
120
- }
 
 
 
121
 
122
- except Exception as e:
123
- result_messgae = {
124
- "status": False,
125
- }
 
 
 
 
 
126
 
127
  return result_messgae
128
 
@@ -131,7 +147,10 @@ if __name__ == "__main__":
131
  import uvicorn
132
 
133
  # 加载权重文件
134
- HY_HUMAN_MATTING_WEIGHTS_PATH = "./hivision_modnet.onnx"
 
 
 
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
- def judge_layout(input_width, input_height, PHOTO_INTERVAL_W, PHOTO_INTERVAL_H, LIMIT_BLOCK_W, LIMIT_BLOCK_H):
5
- centerBlockHeight_1, centerBlockWidth_1 = input_height, input_width # 由证件照们组成的一个中心区块(1代表不转置排列)
6
- centerBlockHeight_2, centerBlockWidth_2 = input_width, input_height # 由证件照们组成的一个中心区块(2代表转置排列)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(input_width, input_height, PHOTO_INTERVAL_W,
71
- PHOTO_INTERVAL_H, LIMIT_BLOCK_W, LIMIT_BLOCK_H)
 
 
 
 
 
 
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
- def generate_layout_image(input_image, typography_arr, typography_rotate, width=295, height=413):
 
 
 
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] = input_image
 
 
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
- 建这个开源库的起源呢,是因为在做onnx推理的时候,需要将原来的tensor转换成numpy.array
4
- 问题是Tensor和Numpy的矩阵排布逻辑不同
5
- 包括Tensor推理经常会进行Transform,比如ToTensor,Normalize等
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: 归一化均值,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,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读取的一张图片,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, :, :, :]
 
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:7e0cb9a2a841b426dd0daf1a788ec398dab059bc039041d62b15636c0783bc56
3
- size 25888609
 
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