Spaces:
Sleeping
Sleeping
Upload 194 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +14 -0
- Dockerfile +16 -0
- face_parsing-main/.Rhistory +0 -0
- face_parsing-main/.gitignore +162 -0
- face_parsing-main/1.docx +3 -0
- face_parsing-main/LICENSE +21 -0
- face_parsing-main/README.md +47 -0
- face_parsing-main/__pycache__/model.cpython-39.pyc +0 -0
- face_parsing-main/__pycache__/resnet.cpython-39.pyc +0 -0
- face_parsing-main/__pycache__/softmask.cpython-39.pyc +0 -0
- face_parsing-main/__pycache__/test_nose_mask_single.cpython-39.pyc +0 -0
- face_parsing-main/crop_b_512_keepname.py +58 -0
- face_parsing-main/expand_mask.py +52 -0
- face_parsing-main/expand_masks_512_to_1536.py +37 -0
- face_parsing-main/face_dataset.py +60 -0
- face_parsing-main/fp_512.pth +3 -0
- face_parsing-main/images/2020-09-06_17-01-01_835030.jpg +0 -0
- face_parsing-main/images/2020-09-06_17-09-01_763315.jpg +0 -0
- face_parsing-main/images/2020-09-06_20-31-54_377040.jpg +0 -0
- face_parsing-main/inference.py +120 -0
- face_parsing-main/loss.py +75 -0
- face_parsing-main/make_nose_masks.py +79 -0
- face_parsing-main/model.py +283 -0
- face_parsing-main/prepropess_data.py +47 -0
- face_parsing-main/python test.docx +0 -0
- face_parsing-main/resnet.py +109 -0
- face_parsing-main/samples/sample.gif +3 -0
- face_parsing-main/samples/t.jpg +0 -0
- face_parsing-main/softmask.py +145 -0
- face_parsing-main/test_nose_mask.py +227 -0
- face_parsing-main/test_nose_mask_single.py +145 -0
- face_parsing-main/train.py +157 -0
- face_parsing-main/transform.py +128 -0
- flask_GAN/app.py +294 -0
- flask_GAN/app_auto_mask.py +306 -0
- flask_GAN/app_mask.py +258 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.06.34 AM.png +0 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.33 AM (1).png +0 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.34 AM (2).png +0 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.35 AM (5).png +0 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.36 AM (1).png +0 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.36 AM (3).png +0 -0
- flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.30.12 AM (2).png +0 -0
- flask_GAN/resize.py +40 -0
- flask_GAN/result/vis/tmp.png +3 -0
- flask_GAN/runtime/outputs/in_3f084cf9775a45d9b21c686ae18ecdf0.png +3 -0
- flask_GAN/runtime/outputs/in_986cd78ee11a4f1fa99385e0efac4563.png +3 -0
- flask_GAN/runtime/outputs/in_c04f130d833c47559f3c1e76c95f8a55.png +3 -0
- flask_GAN/runtime/outputs/in_c6f89cfcf3f047999eb74eb4804f3be9.png +3 -0
- flask_GAN/runtime/outputs/in_f77c29d6f9174b54b5678d30d4834fbf.png +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,17 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
face_parsing-main/1.docx filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
face_parsing-main/samples/sample.gif filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
flask_GAN/result/vis/tmp.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
flask_GAN/runtime/outputs/in_3f084cf9775a45d9b21c686ae18ecdf0.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
flask_GAN/runtime/outputs/in_986cd78ee11a4f1fa99385e0efac4563.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
flask_GAN/runtime/outputs/in_c04f130d833c47559f3c1e76c95f8a55.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
flask_GAN/runtime/outputs/in_c6f89cfcf3f047999eb74eb4804f3be9.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
flask_GAN/runtime/outputs/in_f77c29d6f9174b54b5678d30d4834fbf.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
flask_GAN/runtime/outputs/out_085f535a5a4a4e8f80f38ac6125cc7ff.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
flask_GAN/runtime/outputs/out_3a41682e7cb74fe2abaa51f5754dd5c1.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
flask_GAN/runtime/outputs/out_b7239cb192984fd781750350c16b9d0d.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
flask_GAN/runtime/outputs/out_ebdffbbdd31b4516b8241f9907be8f15.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
flask_GAN/runtime/outputs/out_f73c6c5c48f84ece9fa6b28b2c180af7.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
pix2pix_vgg/imgs/horse2zebra.gif filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9
|
| 2 |
+
|
| 3 |
+
# 设置工作目录
|
| 4 |
+
WORKDIR /code
|
| 5 |
+
|
| 6 |
+
# 拷贝所有项目文件
|
| 7 |
+
COPY . /code
|
| 8 |
+
|
| 9 |
+
# 安装依赖
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
# Hugging Face 会注入 PORT,我们设置默认值
|
| 13 |
+
ENV PORT=7860
|
| 14 |
+
|
| 15 |
+
# 启动你的 Flask 应用
|
| 16 |
+
CMD ["python", "flask_GAN/app_auto_mask.py"]
|
face_parsing-main/.Rhistory
ADDED
|
File without changes
|
face_parsing-main/.gitignore
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# poetry
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 102 |
+
#poetry.lock
|
| 103 |
+
|
| 104 |
+
# pdm
|
| 105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 106 |
+
#pdm.lock
|
| 107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 108 |
+
# in version control.
|
| 109 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 110 |
+
.pdm.toml
|
| 111 |
+
.pdm-python
|
| 112 |
+
.pdm-build/
|
| 113 |
+
|
| 114 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 115 |
+
__pypackages__/
|
| 116 |
+
|
| 117 |
+
# Celery stuff
|
| 118 |
+
celerybeat-schedule
|
| 119 |
+
celerybeat.pid
|
| 120 |
+
|
| 121 |
+
# SageMath parsed files
|
| 122 |
+
*.sage.py
|
| 123 |
+
|
| 124 |
+
# Environments
|
| 125 |
+
.env
|
| 126 |
+
.venv
|
| 127 |
+
env/
|
| 128 |
+
venv/
|
| 129 |
+
ENV/
|
| 130 |
+
env.bak/
|
| 131 |
+
venv.bak/
|
| 132 |
+
|
| 133 |
+
# Spyder project settings
|
| 134 |
+
.spyderproject
|
| 135 |
+
.spyproject
|
| 136 |
+
|
| 137 |
+
# Rope project settings
|
| 138 |
+
.ropeproject
|
| 139 |
+
|
| 140 |
+
# mkdocs documentation
|
| 141 |
+
/site
|
| 142 |
+
|
| 143 |
+
# mypy
|
| 144 |
+
.mypy_cache/
|
| 145 |
+
.dmypy.json
|
| 146 |
+
dmypy.json
|
| 147 |
+
|
| 148 |
+
# Pyre type checker
|
| 149 |
+
.pyre/
|
| 150 |
+
|
| 151 |
+
# pytype static type analyzer
|
| 152 |
+
.pytype/
|
| 153 |
+
|
| 154 |
+
# Cython debug symbols
|
| 155 |
+
cython_debug/
|
| 156 |
+
|
| 157 |
+
# PyCharm
|
| 158 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 159 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 160 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 161 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 162 |
+
#.idea/
|
face_parsing-main/1.docx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b3956a0b58d8c649aab1a66396bff72b4cf8a746e46c8d421e6175303d602de5
|
| 3 |
+
size 1141639
|
face_parsing-main/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 XIAN-HHappy
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
face_parsing-main/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# face parsing
|
| 2 |
+
人脸区域分割
|
| 3 |
+
|
| 4 |
+
## 项目介绍
|
| 5 |
+
注意:该项目不包括人脸检测部分,人脸检测项目地址:https://github.com/XIAN-HHappy/yolo_v3
|
| 6 |
+
|
| 7 |
+
* 图片示例:
|
| 8 |
+

|
| 9 |
+
|
| 10 |
+
* 视频示例:
|
| 11 |
+

|
| 12 |
+
|
| 13 |
+
## 项目配置
|
| 14 |
+
* 作者开发环境:
|
| 15 |
+
* Python 3.7
|
| 16 |
+
* PyTorch >= 1.5.1
|
| 17 |
+
|
| 18 |
+
## 数据集
|
| 19 |
+
* CelebAMask-HQ dataset,数据下载地址:
|
| 20 |
+
https://github.com/switchablenorms/CelebAMask-HQ
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
• The CelebAMask-HQ dataset is available for non-commercial research purposes only.
|
| 24 |
+
• You agree not to reproduce, duplicate, copy, sell, trade, resell or exploit for any commercial purposes, any portion of the images and any portion of derived data.
|
| 25 |
+
• You agree not to further copy, publish or distribute any portion of the CelebAMask-HQ dataset. Except, for internal use at a single site within the same organization it is allowed to make copies of the dataset.
|
| 26 |
+
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
* 数据集制作
|
| 30 |
+
下载数据集并解压,然后运行脚本 prepropess_data.py,生成训练用的mask,注意脚本内相关参数配置。
|
| 31 |
+
|
| 32 |
+
## 预训练模型
|
| 33 |
+
提供512,256两种分辨率的预训练模型。
|
| 34 |
+
* [预训练模型下载地址(百度网盘 Password: ri6m )](https://pan.baidu.com/s/1I5fPAyXDfIh9M5POs80ovg)
|
| 35 |
+
|
| 36 |
+
## 项目使用方法
|
| 37 |
+
|
| 38 |
+
### 步骤1:生成训练数据
|
| 39 |
+
* 目前建议输出2种样本分辨率 512 和 256 (提供512,256两种分辨率的预训练模型),分辨率太小返回原图尺寸时会出现掩码锯齿状,需要后处理解决。
|
| 40 |
+
注意训练、推理脚本也要做相应的分辨率对应设置。
|
| 41 |
+
* 运行脚本:prepropess_data.py (注意脚本内相关参数配置 )
|
| 42 |
+
|
| 43 |
+
### 步骤2:模型训练
|
| 44 |
+
* 根目录下运行命令: python train.py (注意脚本内相关参数配置 )
|
| 45 |
+
|
| 46 |
+
### 步骤3:模型推理
|
| 47 |
+
* 根目录下运行命令: python inference.py (注意脚本内相关参数配置 )
|
face_parsing-main/__pycache__/model.cpython-39.pyc
ADDED
|
Binary file (9.21 kB). View file
|
|
|
face_parsing-main/__pycache__/resnet.cpython-39.pyc
ADDED
|
Binary file (3.66 kB). View file
|
|
|
face_parsing-main/__pycache__/softmask.cpython-39.pyc
ADDED
|
Binary file (5.46 kB). View file
|
|
|
face_parsing-main/__pycache__/test_nose_mask_single.cpython-39.pyc
ADDED
|
Binary file (4.68 kB). View file
|
|
|
face_parsing-main/crop_b_512_keepname.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from PIL import Image
|
| 3 |
+
import argparse
|
| 4 |
+
|
| 5 |
+
def is_img(p):
|
| 6 |
+
return os.path.splitext(p)[1].lower() in {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}
|
| 7 |
+
|
| 8 |
+
def main(in_dir, out_dir, y_top=400, patch=512, strict_w=1024, min_h=None):
|
| 9 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 10 |
+
if min_h is None:
|
| 11 |
+
min_h = y_top + patch # 默认至少要容纳裁剪高度
|
| 12 |
+
|
| 13 |
+
files = [f for f in os.listdir(in_dir) if is_img(f)]
|
| 14 |
+
if not files:
|
| 15 |
+
print(f"[WARN] 没在 {in_dir} 里找到图像文件")
|
| 16 |
+
return
|
| 17 |
+
|
| 18 |
+
kept, skipped = 0, 0
|
| 19 |
+
for fn in sorted(files):
|
| 20 |
+
ip = os.path.join(in_dir, fn)
|
| 21 |
+
try:
|
| 22 |
+
im = Image.open(ip).convert("RGB")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"[SKIP] 打不开:{ip} ({e})")
|
| 25 |
+
skipped += 1
|
| 26 |
+
continue
|
| 27 |
+
|
| 28 |
+
w, h = im.size
|
| 29 |
+
# 期望 A|B 横向拼接(每半边 512 宽):总宽至少 1024,高度至少 y_top+patch
|
| 30 |
+
if w < strict_w or h < min_h:
|
| 31 |
+
print(f"[SKIP] 尺寸不符:{fn} size={w}x{h},需要 >= {strict_w}x{min_h}")
|
| 32 |
+
skipped += 1
|
| 33 |
+
continue
|
| 34 |
+
|
| 35 |
+
# 右半边 B(假定 AB 横拼):[w//2, 0, w, h],应为 512x1536
|
| 36 |
+
x0 = w // 2
|
| 37 |
+
b = im.crop((x0, 0, w, h))
|
| 38 |
+
|
| 39 |
+
# 从 y_top 处裁 512x512
|
| 40 |
+
y0, y1 = y_top, y_top + patch
|
| 41 |
+
b512 = b.crop((0, y0, patch, y1)) # (left, top, right, bottom)
|
| 42 |
+
|
| 43 |
+
# 输出到 out_dir,文件名与原图一致(不加后缀)
|
| 44 |
+
op = os.path.join(out_dir, fn)
|
| 45 |
+
b512.save(op)
|
| 46 |
+
kept += 1
|
| 47 |
+
print(f"[OK] {fn} -> {op}")
|
| 48 |
+
|
| 49 |
+
print(f"\n完成:保存 {kept} 张,跳过 {skipped} 张。输出目录:{out_dir}")
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
ap = argparse.ArgumentParser(description="从 A|B 拼接图裁出右半 B 的 512x512,小图文件名保持不变")
|
| 53 |
+
ap.add_argument("--in_dir", required=True, help="输入目录(含 1024x1536 的 A|B 拼图)")
|
| 54 |
+
ap.add_argument("--out_dir", required=True, help="输出目录(文件名与输入相同)")
|
| 55 |
+
ap.add_argument("--y_top", type=int, default=400, help="裁剪起始 y(默认 400)")
|
| 56 |
+
ap.add_argument("--patch", type=int, default=512, help="裁剪边长(默认 512)")
|
| 57 |
+
args = ap.parse_args()
|
| 58 |
+
main(args.in_dir, args.out_dir, args.y_top, args.patch)
|
face_parsing-main/expand_mask.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
将鼻子 mask 向上和向右扩展为矩形区域。
|
| 5 |
+
适合 Pix2Pix 测试或 inpainting 掩膜增强使用。
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
def expand_mask_up_right(mask: np.ndarray, top_pad=30, right_pad=150):
|
| 13 |
+
"""
|
| 14 |
+
向上 (top_pad) 和向右 (right_pad) 扩充白色区域成矩形。
|
| 15 |
+
参数:
|
| 16 |
+
mask: 灰度 mask (0/255)
|
| 17 |
+
top_pad: 向上扩展像素(少量,例如 20-50)
|
| 18 |
+
right_pad: 向右扩展像素(较多,例如 100-200)
|
| 19 |
+
返回:
|
| 20 |
+
新的扩充 mask (uint8)
|
| 21 |
+
"""
|
| 22 |
+
ys, xs = np.where(mask > 0)
|
| 23 |
+
if len(ys) == 0:
|
| 24 |
+
return mask
|
| 25 |
+
|
| 26 |
+
top, bottom = ys.min(), ys.max()
|
| 27 |
+
left, right = xs.min(), xs.max()
|
| 28 |
+
H, W = mask.shape
|
| 29 |
+
|
| 30 |
+
new_top = max(0, top - top_pad)
|
| 31 |
+
new_bottom = min(H, bottom)
|
| 32 |
+
new_right = min(W, right + right_pad)
|
| 33 |
+
|
| 34 |
+
new_mask = np.zeros_like(mask)
|
| 35 |
+
new_mask[new_top:new_bottom, left:new_right] = 255
|
| 36 |
+
return new_mask
|
| 37 |
+
|
| 38 |
+
def batch_process(in_dir="./result_test", out_dir="./result_expanded", top_pad=30, right_pad=150):
|
| 39 |
+
in_dir = Path(in_dir)
|
| 40 |
+
out_dir = Path(out_dir)
|
| 41 |
+
out_dir.mkdir(exist_ok=True)
|
| 42 |
+
|
| 43 |
+
for p in in_dir.glob("*.png"):
|
| 44 |
+
mask = cv2.imread(str(p), cv2.IMREAD_GRAYSCALE)
|
| 45 |
+
new_mask = expand_mask_up_right(mask, top_pad=top_pad, right_pad=right_pad)
|
| 46 |
+
out_path = out_dir / p.name
|
| 47 |
+
cv2.imwrite(str(out_path), new_mask)
|
| 48 |
+
print(f"[OK] saved: {out_path}")
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
# 按实际情况调整扩展范围
|
| 52 |
+
batch_process(in_dir="./result_test", out_dir="./result_expanded", top_pad=30, right_pad=30)
|
face_parsing-main/expand_masks_512_to_1536.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# expand_masks_512_to_1536.py
|
| 2 |
+
# 把 512x512 的鼻子二值掩码,贴回到 1536x512(竖向位置 y=[y_top, y_top+512)),文件名不变
|
| 3 |
+
import os
|
| 4 |
+
import argparse
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
def is_img(p): return os.path.splitext(p)[1].lower() in {".png", ".jpg", ".jpeg", ".bmp"}
|
| 9 |
+
|
| 10 |
+
def expand_dir(in_dir, out_dir, y_top=400, H=1536, W=512):
|
| 11 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 12 |
+
n = 0
|
| 13 |
+
for fn in sorted(os.listdir(in_dir)):
|
| 14 |
+
if not is_img(fn):
|
| 15 |
+
continue
|
| 16 |
+
m = Image.open(os.path.join(in_dir, fn)).convert("L") # 读灰度
|
| 17 |
+
m = np.array(m)
|
| 18 |
+
if m.shape != (512, 512):
|
| 19 |
+
raise ValueError(f"{fn} 不是 512x512,而是 {m.shape}")
|
| 20 |
+
|
| 21 |
+
m = (m > 127).astype(np.uint8) # 二值化到 {0,1}
|
| 22 |
+
big = np.zeros((H, W), dtype=np.uint8) # 1536x512 全黑
|
| 23 |
+
big[y_top:y_top+512, 0:512] = m # 贴回到指定位置
|
| 24 |
+
|
| 25 |
+
Image.fromarray(big * 255).save(
|
| 26 |
+
os.path.join(out_dir, os.path.splitext(fn)[0] + ".png")
|
| 27 |
+
)
|
| 28 |
+
n += 1
|
| 29 |
+
print(f"[DONE] {in_dir} -> {out_dir} 共 {n} 张,y_top={y_top}, 输出尺寸={H}x{W}")
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
ap = argparse.ArgumentParser()
|
| 33 |
+
ap.add_argument("--in_dir", required=True, help="输入 512x512 鼻子 mask 目录(文件名与原图一致)")
|
| 34 |
+
ap.add_argument("--out_dir", required=True, help="输出 1536x512 掩码目录(文件名不变)")
|
| 35 |
+
ap.add_argument("--y_top", type=int, default=400, help="竖向贴回起始 y,默认 400")
|
| 36 |
+
args = ap.parse_args()
|
| 37 |
+
expand_dir(args.in_dir, args.out_dir, y_top=args.y_top)
|
face_parsing-main/face_dataset.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python
|
| 2 |
+
# -*- encoding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch.utils.data import Dataset
|
| 6 |
+
import torchvision.transforms as transforms
|
| 7 |
+
|
| 8 |
+
import os.path as osp
|
| 9 |
+
import os
|
| 10 |
+
from PIL import Image
|
| 11 |
+
import numpy as np
|
| 12 |
+
import json
|
| 13 |
+
import cv2
|
| 14 |
+
|
| 15 |
+
from transform import *
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class FaceMask(Dataset):
|
| 20 |
+
def __init__(self, rootpth,img_size, cropsize=(640, 480), mode='train', *args, **kwargs):
|
| 21 |
+
super(FaceMask, self).__init__(*args, **kwargs)
|
| 22 |
+
assert mode in ('train', 'val', 'test')
|
| 23 |
+
self.mode = mode
|
| 24 |
+
self.ignore_lb = 255
|
| 25 |
+
self.rootpth = rootpth
|
| 26 |
+
self.img_size = img_size
|
| 27 |
+
|
| 28 |
+
self.imgs = os.listdir(os.path.join(self.rootpth, 'CelebA-HQ-img'))
|
| 29 |
+
|
| 30 |
+
# pre-processing
|
| 31 |
+
self.to_tensor = transforms.Compose([
|
| 32 |
+
transforms.ToTensor(),
|
| 33 |
+
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
|
| 34 |
+
])
|
| 35 |
+
self.trans_train = Compose([
|
| 36 |
+
ColorJitter(
|
| 37 |
+
brightness=0.5,
|
| 38 |
+
contrast=0.5,
|
| 39 |
+
saturation=0.5),
|
| 40 |
+
HorizontalFlip(),
|
| 41 |
+
RandomScale((0.75, 1.0, 1.25, 1.5, 1.75, 2.0)),
|
| 42 |
+
RandomCrop(cropsize)
|
| 43 |
+
])
|
| 44 |
+
|
| 45 |
+
def __getitem__(self, idx):
|
| 46 |
+
impth = self.imgs[idx]
|
| 47 |
+
img = Image.open(osp.join(self.rootpth, 'CelebA-HQ-img', impth))
|
| 48 |
+
img = img.resize((self.img_size,self.img_size), Image.BILINEAR)
|
| 49 |
+
label = Image.open(osp.join(self.rootpth, 'mask_{}'.format(self.img_size), impth[:-3]+'png')).convert('P')
|
| 50 |
+
# print(np.unique(np.array(label)))
|
| 51 |
+
if self.mode == 'train':
|
| 52 |
+
im_lb = dict(im=img, lb=label)
|
| 53 |
+
im_lb = self.trans_train(im_lb)
|
| 54 |
+
img, label = im_lb['im'], im_lb['lb']
|
| 55 |
+
img = self.to_tensor(img)
|
| 56 |
+
label = np.array(label).astype(np.int64)[np.newaxis, :]
|
| 57 |
+
return img, label
|
| 58 |
+
|
| 59 |
+
def __len__(self):
|
| 60 |
+
return len(self.imgs)
|
face_parsing-main/fp_512.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c6f392bafa99c8b3fb3408a5f9d35d6623eb26893018ea7022625e8b0c76484
|
| 3 |
+
size 53289081
|
face_parsing-main/images/2020-09-06_17-01-01_835030.jpg
ADDED
|
face_parsing-main/images/2020-09-06_17-09-01_763315.jpg
ADDED
|
face_parsing-main/images/2020-09-06_20-31-54_377040.jpg
ADDED
|
face_parsing-main/inference.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import os, os.path as osp
|
| 3 |
+
import argparse
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import torchvision.transforms as T
|
| 7 |
+
import cv2
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
# 你的工程里:from model import BiSeNet
|
| 11 |
+
from model import BiSeNet
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# 颜色表(19 类;按你的仓库类别顺序)
|
| 15 |
+
PART_COLORS = np.array([
|
| 16 |
+
[255, 0, 0], [255, 85, 0], [255, 170, 0],
|
| 17 |
+
[255, 0, 85], [255, 0, 170],
|
| 18 |
+
[ 0, 255, 0], [ 85, 255, 0], [170, 255, 0],
|
| 19 |
+
[ 0, 255, 85], [ 0, 255, 170],
|
| 20 |
+
[ 0, 0, 255], [ 85, 0, 255], [170, 0, 255],
|
| 21 |
+
[ 0, 85, 255], [ 0, 170, 255],
|
| 22 |
+
[255, 255, 0], [255, 255, 85], [255, 255, 170],
|
| 23 |
+
[255, 0, 255]
|
| 24 |
+
], dtype=np.uint8)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def is_img(p):
|
| 28 |
+
return osp.splitext(p)[1].lower() in {".jpg",".jpeg",".png",".bmp",".tif",".tiff",".webp"}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def overlay_parsing(rgb, parsing, alpha=0.45):
|
| 32 |
+
"""parsing: (H,W) int; rgb: (H,W,3) uint8 -> overlay (H,W,3) uint8"""
|
| 33 |
+
h, w = parsing.shape
|
| 34 |
+
color = PART_COLORS[np.clip(parsing, 0, len(PART_COLORS)-1)]
|
| 35 |
+
blend = (alpha*color + (1-alpha)*rgb).astype(np.uint8)
|
| 36 |
+
return blend
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def save_index_png(parsing, path):
|
| 40 |
+
"""把类别 id 存成 ‘索引图’(单通道 PNG,便于后处理)"""
|
| 41 |
+
Image.fromarray(parsing.astype(np.uint8), mode='L').save(path)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main(args):
|
| 45 |
+
device = torch.device(args.device if torch.cuda.is_available() and args.device.startswith("cuda")
|
| 46 |
+
else "cpu")
|
| 47 |
+
print(f"[INFO] device = {device}")
|
| 48 |
+
|
| 49 |
+
# 模型
|
| 50 |
+
n_classes = 19
|
| 51 |
+
net = BiSeNet(n_classes=n_classes).to(device)
|
| 52 |
+
print(f"[INFO] load weights: {args.weights}")
|
| 53 |
+
state = torch.load(args.weights, map_location=device)
|
| 54 |
+
net.load_state_dict(state, strict=True)
|
| 55 |
+
net.eval()
|
| 56 |
+
|
| 57 |
+
tfm = T.Compose([
|
| 58 |
+
T.ToTensor(),
|
| 59 |
+
T.Normalize((0.485,0.456,0.406), (0.229,0.224,0.225)),
|
| 60 |
+
])
|
| 61 |
+
|
| 62 |
+
os.makedirs(args.save_vis, exist_ok=True)
|
| 63 |
+
if args.save_idx:
|
| 64 |
+
os.makedirs(args.save_idx, exist_ok=True)
|
| 65 |
+
|
| 66 |
+
names = [f for f in sorted(os.listdir(args.image_dir)) if is_img(f)]
|
| 67 |
+
if not names:
|
| 68 |
+
print(f"[WARN] no images in {args.image_dir}")
|
| 69 |
+
return
|
| 70 |
+
|
| 71 |
+
with torch.no_grad():
|
| 72 |
+
for i, fn in enumerate(names, 1):
|
| 73 |
+
ip = osp.join(args.image_dir, fn)
|
| 74 |
+
bgr = cv2.imread(ip, cv2.IMREAD_COLOR)
|
| 75 |
+
if bgr is None:
|
| 76 |
+
print(f"[SKIP] read fail: {ip}")
|
| 77 |
+
continue
|
| 78 |
+
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
| 79 |
+
|
| 80 |
+
# 网络输入大小
|
| 81 |
+
pil = Image.fromarray(rgb).resize((args.img_size, args.img_size), Image.BILINEAR)
|
| 82 |
+
x = tfm(pil).unsqueeze(0).to(device)
|
| 83 |
+
|
| 84 |
+
out = net(x)
|
| 85 |
+
# 兼容返回 tuple/list 的实现
|
| 86 |
+
if isinstance(out, (list, tuple)):
|
| 87 |
+
out = out[0]
|
| 88 |
+
parsing_small = out.squeeze(0).argmax(0).detach().cpu().numpy().astype(np.uint8)
|
| 89 |
+
# 还原回原图大小
|
| 90 |
+
parsing = cv2.resize(parsing_small, (rgb.shape[1], rgb.shape[0]), interpolation=cv2.INTER_NEAREST)
|
| 91 |
+
|
| 92 |
+
# 保存可视化
|
| 93 |
+
vis = overlay_parsing(rgb, parsing, alpha=args.alpha)
|
| 94 |
+
vis_bgr = cv2.cvtColor(vis, cv2.COLOR_RGB2BGR)
|
| 95 |
+
cv2.imwrite(osp.join(args.save_vis, fn), vis_bgr)
|
| 96 |
+
|
| 97 |
+
# 可选:保存索引图(单通道 id)
|
| 98 |
+
if args.save_idx:
|
| 99 |
+
save_index_png(parsing, osp.join(args.save_idx, osp.splitext(fn)[0] + ".png"))
|
| 100 |
+
|
| 101 |
+
uniq = np.unique(parsing)
|
| 102 |
+
print(f"[{i:04d}/{len(names)}] {fn} classes={uniq.tolist()} -> vis:{args.save_vis}")
|
| 103 |
+
|
| 104 |
+
print("[DONE]")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
ap = argparse.ArgumentParser()
|
| 109 |
+
ap.add_argument("--image_dir", type=str, default="./images", help="输入图片目录")
|
| 110 |
+
ap.add_argument("--weights", type=str, default="./fp_512.pth", help="权重路径")
|
| 111 |
+
ap.add_argument("--img_size", dest="img_size", type=int, default=512, help="推理分辨率(方形)")
|
| 112 |
+
ap.add_argument("--device", type=str, default="cuda:0", help="'cuda:0' 或 'cpu'")
|
| 113 |
+
ap.add_argument("--save_vis", type=str, default="./result/vis", help="彩色叠加图输出目录")
|
| 114 |
+
ap.add_argument("--save_idx", type=str, default="./result/idx", help="可选:索引图输出目录(单通道PNG),为空则不保存")
|
| 115 |
+
ap.add_argument("--alpha", type=float, default=0.45, help="可视化叠加不透明度")
|
| 116 |
+
args = ap.parse_args()
|
| 117 |
+
# 若不想保存索引图,传空字符串即可
|
| 118 |
+
if args.save_idx == "":
|
| 119 |
+
args.save_idx = None
|
| 120 |
+
main(args)
|
face_parsing-main/loss.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python
|
| 2 |
+
# -*- encoding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class OhemCELoss(nn.Module):
|
| 13 |
+
def __init__(self, thresh, n_min, ignore_lb=255, *args, **kwargs):
|
| 14 |
+
super(OhemCELoss, self).__init__()
|
| 15 |
+
self.thresh = -torch.log(torch.tensor(thresh, dtype=torch.float)).cuda()
|
| 16 |
+
self.n_min = n_min
|
| 17 |
+
self.ignore_lb = ignore_lb
|
| 18 |
+
self.criteria = nn.CrossEntropyLoss(ignore_index=ignore_lb, reduction='none')
|
| 19 |
+
|
| 20 |
+
def forward(self, logits, labels):
|
| 21 |
+
N, C, H, W = logits.size()
|
| 22 |
+
loss = self.criteria(logits, labels).view(-1)
|
| 23 |
+
loss, _ = torch.sort(loss, descending=True)
|
| 24 |
+
if loss[self.n_min] > self.thresh:
|
| 25 |
+
loss = loss[loss>self.thresh]
|
| 26 |
+
else:
|
| 27 |
+
loss = loss[:self.n_min]
|
| 28 |
+
return torch.mean(loss)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class SoftmaxFocalLoss(nn.Module):
|
| 32 |
+
def __init__(self, gamma, ignore_lb=255, *args, **kwargs):
|
| 33 |
+
super(SoftmaxFocalLoss, self).__init__()
|
| 34 |
+
self.gamma = gamma
|
| 35 |
+
self.nll = nn.NLLLoss(ignore_index=ignore_lb)
|
| 36 |
+
|
| 37 |
+
def forward(self, logits, labels):
|
| 38 |
+
scores = F.softmax(logits, dim=1)
|
| 39 |
+
factor = torch.pow(1.-scores, self.gamma)
|
| 40 |
+
log_score = F.log_softmax(logits, dim=1)
|
| 41 |
+
log_score = factor * log_score
|
| 42 |
+
loss = self.nll(log_score, labels)
|
| 43 |
+
return loss
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == '__main__':
|
| 47 |
+
torch.manual_seed(15)
|
| 48 |
+
criteria1 = OhemCELoss(thresh=0.7, n_min=16*20*20//16).cuda()
|
| 49 |
+
criteria2 = OhemCELoss(thresh=0.7, n_min=16*20*20//16).cuda()
|
| 50 |
+
net1 = nn.Sequential(
|
| 51 |
+
nn.Conv2d(3, 19, kernel_size=3, stride=2, padding=1),
|
| 52 |
+
)
|
| 53 |
+
net1.cuda()
|
| 54 |
+
net1.train()
|
| 55 |
+
net2 = nn.Sequential(
|
| 56 |
+
nn.Conv2d(3, 19, kernel_size=3, stride=2, padding=1),
|
| 57 |
+
)
|
| 58 |
+
net2.cuda()
|
| 59 |
+
net2.train()
|
| 60 |
+
|
| 61 |
+
with torch.no_grad():
|
| 62 |
+
inten = torch.randn(16, 3, 20, 20).cuda()
|
| 63 |
+
lbs = torch.randint(0, 19, [16, 20, 20]).cuda()
|
| 64 |
+
lbs[1, :, :] = 255
|
| 65 |
+
|
| 66 |
+
logits1 = net1(inten)
|
| 67 |
+
logits1 = F.interpolate(logits1, inten.size()[2:], mode='bilinear')
|
| 68 |
+
logits2 = net2(inten)
|
| 69 |
+
logits2 = F.interpolate(logits2, inten.size()[2:], mode='bilinear')
|
| 70 |
+
|
| 71 |
+
loss1 = criteria1(logits1, lbs)
|
| 72 |
+
loss2 = criteria2(logits2, lbs)
|
| 73 |
+
loss = loss1 + loss2
|
| 74 |
+
print(loss.detach().cpu())
|
| 75 |
+
loss.backward()
|
face_parsing-main/make_nose_masks.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# make_nose_masks.py
|
| 2 |
+
import os
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
import argparse
|
| 6 |
+
|
| 7 |
+
def is_img(fn):
|
| 8 |
+
return os.path.splitext(fn)[1].lower() in {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"}
|
| 9 |
+
|
| 10 |
+
def read_idx(path):
|
| 11 |
+
"""兼容灰度/调色板/彩色三种读法,返回 uint8 的类别ID矩阵"""
|
| 12 |
+
arr = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
| 13 |
+
if arr is None:
|
| 14 |
+
raise RuntimeError(f"read fail: {path}")
|
| 15 |
+
# 如果是彩色(3通道),通常三通道值一致;取任一通道即可
|
| 16 |
+
if arr.ndim == 3:
|
| 17 |
+
arr = arr[:, :, 0]
|
| 18 |
+
return arr.astype(np.uint8)
|
| 19 |
+
|
| 20 |
+
def extract_nose(idx, nose_id=10):
|
| 21 |
+
"""从 idx 中提取鼻子为 255, 其他为 0"""
|
| 22 |
+
return np.where(idx == nose_id, 255, 0).astype(np.uint8)
|
| 23 |
+
|
| 24 |
+
def paste_to_canvas(mask_512, target_h=1536, target_w=512, y_top=400):
|
| 25 |
+
"""把 512×512 的 mask 贴回 512×1536 的画布(竖向),默认贴在 [y_top:y_top+512]"""
|
| 26 |
+
h, w = mask_512.shape[:2]
|
| 27 |
+
if (h, w) != (512, 512):
|
| 28 |
+
raise ValueError(f"mask size must be 512x512, got {w}x{h}")
|
| 29 |
+
if y_top < 0 or y_top + 512 > target_h:
|
| 30 |
+
raise ValueError(f"y_top out of range: y_top={y_top}, target_h={target_h}")
|
| 31 |
+
|
| 32 |
+
canvas = np.zeros((target_h, target_w), dtype=np.uint8)
|
| 33 |
+
# 你的 512×512 小图本来就是整张的宽度,所以横向从 0 到 512 直接贴就可以
|
| 34 |
+
canvas[y_top:y_top + 512, 0:512] = mask_512
|
| 35 |
+
return canvas
|
| 36 |
+
|
| 37 |
+
def main(args):
|
| 38 |
+
os.makedirs(args.out_dir, exist_ok=True)
|
| 39 |
+
|
| 40 |
+
files = [f for f in os.listdir(args.idx_dir) if is_img(f)]
|
| 41 |
+
if not files:
|
| 42 |
+
print(f"[WARN] 在 {args.idx_dir} 没有找到图片文件")
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
kept = 0
|
| 46 |
+
for i, fn in enumerate(sorted(files), 1):
|
| 47 |
+
in_path = os.path.join(args.idx_dir, fn)
|
| 48 |
+
try:
|
| 49 |
+
idx = read_idx(in_path)
|
| 50 |
+
nose_512 = extract_nose(idx, args.nose_id)
|
| 51 |
+
canvas = paste_to_canvas(
|
| 52 |
+
nose_512,
|
| 53 |
+
target_h=args.target_h,
|
| 54 |
+
target_w=args.target_w,
|
| 55 |
+
y_top=args.y_top,
|
| 56 |
+
)
|
| 57 |
+
out_path = os.path.join(args.out_dir, os.path.splitext(fn)[0] + ".png")
|
| 58 |
+
cv2.imwrite(out_path, canvas)
|
| 59 |
+
kept += 1
|
| 60 |
+
if args.verbose and (i % 10 == 1 or i == len(files)):
|
| 61 |
+
print(f"[{i:04d}/{len(files)}] {fn} -> {out_path}")
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print(f"[SKIP] {fn}: {e}")
|
| 64 |
+
|
| 65 |
+
print(f"\n✅ 完成:保存 {kept} 张。输出目录:{args.out_dir}")
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
p = argparse.ArgumentParser(
|
| 69 |
+
description="把 idx 标签图中的鼻子(默认id=10)抠出来,并贴回到 512x1536 的黑白 mask(文件名保持不变)"
|
| 70 |
+
)
|
| 71 |
+
p.add_argument("--idx_dir", required=True, help="idx 输入目录(每像素=类别ID,512x512)")
|
| 72 |
+
p.add_argument("--out_dir", required=True, help="输出目录(512x1536 黑白图)")
|
| 73 |
+
p.add_argument("--nose_id", type=int, default=10, help="鼻子类别ID(默认10)")
|
| 74 |
+
p.add_argument("--y_top", type=int, default=400, help="贴回时的起始高度(默认400)")
|
| 75 |
+
p.add_argument("--target_h", type=int, default=1536, help="目标高度(默认 1536)")
|
| 76 |
+
p.add_argument("--target_w", type=int, default=512, help="目标宽度(默认 512)")
|
| 77 |
+
p.add_argument("--verbose", action="store_true", help="打印处理进度")
|
| 78 |
+
args = p.parse_args()
|
| 79 |
+
main(args)
|
face_parsing-main/model.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python
|
| 2 |
+
# -*- encoding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import torchvision
|
| 9 |
+
|
| 10 |
+
from resnet import Resnet18
|
| 11 |
+
# from modules.bn import InPlaceABNSync as BatchNorm2d
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ConvBNReLU(nn.Module):
|
| 15 |
+
def __init__(self, in_chan, out_chan, ks=3, stride=1, padding=1, *args, **kwargs):
|
| 16 |
+
super(ConvBNReLU, self).__init__()
|
| 17 |
+
self.conv = nn.Conv2d(in_chan,
|
| 18 |
+
out_chan,
|
| 19 |
+
kernel_size = ks,
|
| 20 |
+
stride = stride,
|
| 21 |
+
padding = padding,
|
| 22 |
+
bias = False)
|
| 23 |
+
self.bn = nn.BatchNorm2d(out_chan)
|
| 24 |
+
self.init_weight()
|
| 25 |
+
|
| 26 |
+
def forward(self, x):
|
| 27 |
+
x = self.conv(x)
|
| 28 |
+
x = F.relu(self.bn(x))
|
| 29 |
+
return x
|
| 30 |
+
|
| 31 |
+
def init_weight(self):
|
| 32 |
+
for ly in self.children():
|
| 33 |
+
if isinstance(ly, nn.Conv2d):
|
| 34 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 35 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 36 |
+
|
| 37 |
+
class BiSeNetOutput(nn.Module):
|
| 38 |
+
def __init__(self, in_chan, mid_chan, n_classes, *args, **kwargs):
|
| 39 |
+
super(BiSeNetOutput, self).__init__()
|
| 40 |
+
self.conv = ConvBNReLU(in_chan, mid_chan, ks=3, stride=1, padding=1)
|
| 41 |
+
self.conv_out = nn.Conv2d(mid_chan, n_classes, kernel_size=1, bias=False)
|
| 42 |
+
self.init_weight()
|
| 43 |
+
|
| 44 |
+
def forward(self, x):
|
| 45 |
+
x = self.conv(x)
|
| 46 |
+
x = self.conv_out(x)
|
| 47 |
+
return x
|
| 48 |
+
|
| 49 |
+
def init_weight(self):
|
| 50 |
+
for ly in self.children():
|
| 51 |
+
if isinstance(ly, nn.Conv2d):
|
| 52 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 53 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 54 |
+
|
| 55 |
+
def get_params(self):
|
| 56 |
+
wd_params, nowd_params = [], []
|
| 57 |
+
for name, module in self.named_modules():
|
| 58 |
+
if isinstance(module, nn.Linear) or isinstance(module, nn.Conv2d):
|
| 59 |
+
wd_params.append(module.weight)
|
| 60 |
+
if not module.bias is None:
|
| 61 |
+
nowd_params.append(module.bias)
|
| 62 |
+
elif isinstance(module, nn.BatchNorm2d):
|
| 63 |
+
nowd_params += list(module.parameters())
|
| 64 |
+
return wd_params, nowd_params
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class AttentionRefinementModule(nn.Module):
|
| 68 |
+
def __init__(self, in_chan, out_chan, *args, **kwargs):
|
| 69 |
+
super(AttentionRefinementModule, self).__init__()
|
| 70 |
+
self.conv = ConvBNReLU(in_chan, out_chan, ks=3, stride=1, padding=1)
|
| 71 |
+
self.conv_atten = nn.Conv2d(out_chan, out_chan, kernel_size= 1, bias=False)
|
| 72 |
+
self.bn_atten = nn.BatchNorm2d(out_chan)
|
| 73 |
+
self.sigmoid_atten = nn.Sigmoid()
|
| 74 |
+
self.init_weight()
|
| 75 |
+
|
| 76 |
+
def forward(self, x):
|
| 77 |
+
feat = self.conv(x)
|
| 78 |
+
atten = F.avg_pool2d(feat, feat.size()[2:])
|
| 79 |
+
atten = self.conv_atten(atten)
|
| 80 |
+
atten = self.bn_atten(atten)
|
| 81 |
+
atten = self.sigmoid_atten(atten)
|
| 82 |
+
out = torch.mul(feat, atten)
|
| 83 |
+
return out
|
| 84 |
+
|
| 85 |
+
def init_weight(self):
|
| 86 |
+
for ly in self.children():
|
| 87 |
+
if isinstance(ly, nn.Conv2d):
|
| 88 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 89 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ContextPath(nn.Module):
|
| 93 |
+
def __init__(self, *args, **kwargs):
|
| 94 |
+
super(ContextPath, self).__init__()
|
| 95 |
+
self.resnet = Resnet18()
|
| 96 |
+
self.arm16 = AttentionRefinementModule(256, 128)
|
| 97 |
+
self.arm32 = AttentionRefinementModule(512, 128)
|
| 98 |
+
self.conv_head32 = ConvBNReLU(128, 128, ks=3, stride=1, padding=1)
|
| 99 |
+
self.conv_head16 = ConvBNReLU(128, 128, ks=3, stride=1, padding=1)
|
| 100 |
+
self.conv_avg = ConvBNReLU(512, 128, ks=1, stride=1, padding=0)
|
| 101 |
+
|
| 102 |
+
self.init_weight()
|
| 103 |
+
|
| 104 |
+
def forward(self, x):
|
| 105 |
+
H0, W0 = x.size()[2:]
|
| 106 |
+
feat8, feat16, feat32 = self.resnet(x)
|
| 107 |
+
H8, W8 = feat8.size()[2:]
|
| 108 |
+
H16, W16 = feat16.size()[2:]
|
| 109 |
+
H32, W32 = feat32.size()[2:]
|
| 110 |
+
|
| 111 |
+
avg = F.avg_pool2d(feat32, feat32.size()[2:])
|
| 112 |
+
avg = self.conv_avg(avg)
|
| 113 |
+
avg_up = F.interpolate(avg, (H32, W32), mode='nearest')
|
| 114 |
+
|
| 115 |
+
feat32_arm = self.arm32(feat32)
|
| 116 |
+
feat32_sum = feat32_arm + avg_up
|
| 117 |
+
feat32_up = F.interpolate(feat32_sum, (H16, W16), mode='nearest')
|
| 118 |
+
feat32_up = self.conv_head32(feat32_up)
|
| 119 |
+
|
| 120 |
+
feat16_arm = self.arm16(feat16)
|
| 121 |
+
feat16_sum = feat16_arm + feat32_up
|
| 122 |
+
feat16_up = F.interpolate(feat16_sum, (H8, W8), mode='nearest')
|
| 123 |
+
feat16_up = self.conv_head16(feat16_up)
|
| 124 |
+
|
| 125 |
+
return feat8, feat16_up, feat32_up # x8, x8, x16
|
| 126 |
+
|
| 127 |
+
def init_weight(self):
|
| 128 |
+
for ly in self.children():
|
| 129 |
+
if isinstance(ly, nn.Conv2d):
|
| 130 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 131 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 132 |
+
|
| 133 |
+
def get_params(self):
|
| 134 |
+
wd_params, nowd_params = [], []
|
| 135 |
+
for name, module in self.named_modules():
|
| 136 |
+
if isinstance(module, (nn.Linear, nn.Conv2d)):
|
| 137 |
+
wd_params.append(module.weight)
|
| 138 |
+
if not module.bias is None:
|
| 139 |
+
nowd_params.append(module.bias)
|
| 140 |
+
elif isinstance(module, nn.BatchNorm2d):
|
| 141 |
+
nowd_params += list(module.parameters())
|
| 142 |
+
return wd_params, nowd_params
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
### This is not used, since I replace this with the resnet feature with the same size
|
| 146 |
+
class SpatialPath(nn.Module):
|
| 147 |
+
def __init__(self, *args, **kwargs):
|
| 148 |
+
super(SpatialPath, self).__init__()
|
| 149 |
+
self.conv1 = ConvBNReLU(3, 64, ks=7, stride=2, padding=3)
|
| 150 |
+
self.conv2 = ConvBNReLU(64, 64, ks=3, stride=2, padding=1)
|
| 151 |
+
self.conv3 = ConvBNReLU(64, 64, ks=3, stride=2, padding=1)
|
| 152 |
+
self.conv_out = ConvBNReLU(64, 128, ks=1, stride=1, padding=0)
|
| 153 |
+
self.init_weight()
|
| 154 |
+
|
| 155 |
+
def forward(self, x):
|
| 156 |
+
feat = self.conv1(x)
|
| 157 |
+
feat = self.conv2(feat)
|
| 158 |
+
feat = self.conv3(feat)
|
| 159 |
+
feat = self.conv_out(feat)
|
| 160 |
+
return feat
|
| 161 |
+
|
| 162 |
+
def init_weight(self):
|
| 163 |
+
for ly in self.children():
|
| 164 |
+
if isinstance(ly, nn.Conv2d):
|
| 165 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 166 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 167 |
+
|
| 168 |
+
def get_params(self):
|
| 169 |
+
wd_params, nowd_params = [], []
|
| 170 |
+
for name, module in self.named_modules():
|
| 171 |
+
if isinstance(module, nn.Linear) or isinstance(module, nn.Conv2d):
|
| 172 |
+
wd_params.append(module.weight)
|
| 173 |
+
if not module.bias is None:
|
| 174 |
+
nowd_params.append(module.bias)
|
| 175 |
+
elif isinstance(module, nn.BatchNorm2d):
|
| 176 |
+
nowd_params += list(module.parameters())
|
| 177 |
+
return wd_params, nowd_params
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class FeatureFusionModule(nn.Module):
|
| 181 |
+
def __init__(self, in_chan, out_chan, *args, **kwargs):
|
| 182 |
+
super(FeatureFusionModule, self).__init__()
|
| 183 |
+
self.convblk = ConvBNReLU(in_chan, out_chan, ks=1, stride=1, padding=0)
|
| 184 |
+
self.conv1 = nn.Conv2d(out_chan,
|
| 185 |
+
out_chan//4,
|
| 186 |
+
kernel_size = 1,
|
| 187 |
+
stride = 1,
|
| 188 |
+
padding = 0,
|
| 189 |
+
bias = False)
|
| 190 |
+
self.conv2 = nn.Conv2d(out_chan//4,
|
| 191 |
+
out_chan,
|
| 192 |
+
kernel_size = 1,
|
| 193 |
+
stride = 1,
|
| 194 |
+
padding = 0,
|
| 195 |
+
bias = False)
|
| 196 |
+
self.relu = nn.ReLU(inplace=True)
|
| 197 |
+
self.sigmoid = nn.Sigmoid()
|
| 198 |
+
self.init_weight()
|
| 199 |
+
|
| 200 |
+
def forward(self, fsp, fcp):
|
| 201 |
+
fcat = torch.cat([fsp, fcp], dim=1)
|
| 202 |
+
feat = self.convblk(fcat)
|
| 203 |
+
atten = F.avg_pool2d(feat, feat.size()[2:])
|
| 204 |
+
atten = self.conv1(atten)
|
| 205 |
+
atten = self.relu(atten)
|
| 206 |
+
atten = self.conv2(atten)
|
| 207 |
+
atten = self.sigmoid(atten)
|
| 208 |
+
feat_atten = torch.mul(feat, atten)
|
| 209 |
+
feat_out = feat_atten + feat
|
| 210 |
+
return feat_out
|
| 211 |
+
|
| 212 |
+
def init_weight(self):
|
| 213 |
+
for ly in self.children():
|
| 214 |
+
if isinstance(ly, nn.Conv2d):
|
| 215 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 216 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 217 |
+
|
| 218 |
+
def get_params(self):
|
| 219 |
+
wd_params, nowd_params = [], []
|
| 220 |
+
for name, module in self.named_modules():
|
| 221 |
+
if isinstance(module, nn.Linear) or isinstance(module, nn.Conv2d):
|
| 222 |
+
wd_params.append(module.weight)
|
| 223 |
+
if not module.bias is None:
|
| 224 |
+
nowd_params.append(module.bias)
|
| 225 |
+
elif isinstance(module, nn.BatchNorm2d):
|
| 226 |
+
nowd_params += list(module.parameters())
|
| 227 |
+
return wd_params, nowd_params
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
class BiSeNet(nn.Module):
|
| 231 |
+
def __init__(self, n_classes, *args, **kwargs):
|
| 232 |
+
super(BiSeNet, self).__init__()
|
| 233 |
+
self.cp = ContextPath()
|
| 234 |
+
## here self.sp is deleted
|
| 235 |
+
self.ffm = FeatureFusionModule(256, 256)
|
| 236 |
+
self.conv_out = BiSeNetOutput(256, 256, n_classes)
|
| 237 |
+
self.conv_out16 = BiSeNetOutput(128, 64, n_classes)
|
| 238 |
+
self.conv_out32 = BiSeNetOutput(128, 64, n_classes)
|
| 239 |
+
self.init_weight()
|
| 240 |
+
|
| 241 |
+
def forward(self, x):
|
| 242 |
+
H, W = x.size()[2:]
|
| 243 |
+
feat_res8, feat_cp8, feat_cp16 = self.cp(x) # here return res3b1 feature
|
| 244 |
+
feat_sp = feat_res8 # use res3b1 feature to replace spatial path feature
|
| 245 |
+
feat_fuse = self.ffm(feat_sp, feat_cp8)
|
| 246 |
+
|
| 247 |
+
feat_out = self.conv_out(feat_fuse)
|
| 248 |
+
feat_out16 = self.conv_out16(feat_cp8)
|
| 249 |
+
feat_out32 = self.conv_out32(feat_cp16)
|
| 250 |
+
|
| 251 |
+
feat_out = F.interpolate(feat_out, (H, W), mode='bilinear', align_corners=True)
|
| 252 |
+
feat_out16 = F.interpolate(feat_out16, (H, W), mode='bilinear', align_corners=True)
|
| 253 |
+
feat_out32 = F.interpolate(feat_out32, (H, W), mode='bilinear', align_corners=True)
|
| 254 |
+
return feat_out, feat_out16, feat_out32
|
| 255 |
+
|
| 256 |
+
def init_weight(self):
|
| 257 |
+
for ly in self.children():
|
| 258 |
+
if isinstance(ly, nn.Conv2d):
|
| 259 |
+
nn.init.kaiming_normal_(ly.weight, a=1)
|
| 260 |
+
if not ly.bias is None: nn.init.constant_(ly.bias, 0)
|
| 261 |
+
|
| 262 |
+
def get_params(self):
|
| 263 |
+
wd_params, nowd_params, lr_mul_wd_params, lr_mul_nowd_params = [], [], [], []
|
| 264 |
+
for name, child in self.named_children():
|
| 265 |
+
child_wd_params, child_nowd_params = child.get_params()
|
| 266 |
+
if isinstance(child, FeatureFusionModule) or isinstance(child, BiSeNetOutput):
|
| 267 |
+
lr_mul_wd_params += child_wd_params
|
| 268 |
+
lr_mul_nowd_params += child_nowd_params
|
| 269 |
+
else:
|
| 270 |
+
wd_params += child_wd_params
|
| 271 |
+
nowd_params += child_nowd_params
|
| 272 |
+
return wd_params, nowd_params, lr_mul_wd_params, lr_mul_nowd_params
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
if __name__ == "__main__":
|
| 276 |
+
net = BiSeNet(19)
|
| 277 |
+
net.cuda()
|
| 278 |
+
net.eval()
|
| 279 |
+
in_ten = torch.randn(16, 3, 640, 480).cuda()
|
| 280 |
+
out, out16, out32 = net(in_ten)
|
| 281 |
+
print(out.shape)
|
| 282 |
+
|
| 283 |
+
net.get_params()
|
face_parsing-main/prepropess_data.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- encoding: utf-8 -*-
|
| 2 |
+
#function : 训练样本预处理
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import os.path as osp
|
| 6 |
+
import cv2
|
| 7 |
+
from transform import *
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
if __name__ == "__main__":
|
| 11 |
+
|
| 12 |
+
image_size = 512# 样本分辨率
|
| 13 |
+
|
| 14 |
+
face_data = './CelebAMask-HQ/CelebA-HQ-img'
|
| 15 |
+
face_sep_mask = './CelebAMask-HQ/CelebAMask-HQ-mask-anno'
|
| 16 |
+
mask_path = './CelebAMask-HQ/mask_{}'.format(image_size)
|
| 17 |
+
|
| 18 |
+
if not os.path.exists(mask_path):
|
| 19 |
+
os.mkdir(mask_path)
|
| 20 |
+
|
| 21 |
+
counter = 0
|
| 22 |
+
total = 0
|
| 23 |
+
for i in range(15):
|
| 24 |
+
|
| 25 |
+
atts = ['skin', 'l_brow', 'r_brow', 'l_eye', 'r_eye', 'eye_g', 'l_ear', 'r_ear', 'ear_r',
|
| 26 |
+
'nose', 'mouth', 'u_lip', 'l_lip', 'neck', 'neck_l', 'cloth', 'hair', 'hat']
|
| 27 |
+
|
| 28 |
+
for j in range(i * 2000, (i + 1) * 2000):
|
| 29 |
+
|
| 30 |
+
mask = np.zeros((512, 512))
|
| 31 |
+
|
| 32 |
+
for l, att in enumerate(atts, 1):
|
| 33 |
+
total += 1
|
| 34 |
+
file_name = ''.join([str(j).rjust(5, '0'), '_', att, '.png'])
|
| 35 |
+
path = osp.join(face_sep_mask, str(i), file_name)
|
| 36 |
+
|
| 37 |
+
if os.path.exists(path):
|
| 38 |
+
counter += 1
|
| 39 |
+
sep_mask = np.array(Image.open(path).convert('P'))
|
| 40 |
+
|
| 41 |
+
mask[sep_mask == 225] = l
|
| 42 |
+
if image_size != 512:
|
| 43 |
+
mask = cv2.resize(mask,(image_size,image_size),interpolation=cv2.INTER_NEAREST)
|
| 44 |
+
cv2.imwrite('{}/{}.png'.format(mask_path, j), mask)
|
| 45 |
+
print(j)
|
| 46 |
+
|
| 47 |
+
print(counter, total)
|
face_parsing-main/python test.docx
ADDED
|
Binary file (16.1 kB). View file
|
|
|
face_parsing-main/resnet.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python
|
| 2 |
+
# -*- encoding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
import torch.utils.model_zoo as modelzoo
|
| 8 |
+
|
| 9 |
+
# from modules.bn import InPlaceABNSync as BatchNorm2d
|
| 10 |
+
|
| 11 |
+
resnet18_url = 'https://download.pytorch.org/models/resnet18-5c106cde.pth'
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def conv3x3(in_planes, out_planes, stride=1):
|
| 15 |
+
"""3x3 convolution with padding"""
|
| 16 |
+
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
|
| 17 |
+
padding=1, bias=False)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class BasicBlock(nn.Module):
|
| 21 |
+
def __init__(self, in_chan, out_chan, stride=1):
|
| 22 |
+
super(BasicBlock, self).__init__()
|
| 23 |
+
self.conv1 = conv3x3(in_chan, out_chan, stride)
|
| 24 |
+
self.bn1 = nn.BatchNorm2d(out_chan)
|
| 25 |
+
self.conv2 = conv3x3(out_chan, out_chan)
|
| 26 |
+
self.bn2 = nn.BatchNorm2d(out_chan)
|
| 27 |
+
self.relu = nn.ReLU(inplace=True)
|
| 28 |
+
self.downsample = None
|
| 29 |
+
if in_chan != out_chan or stride != 1:
|
| 30 |
+
self.downsample = nn.Sequential(
|
| 31 |
+
nn.Conv2d(in_chan, out_chan,
|
| 32 |
+
kernel_size=1, stride=stride, bias=False),
|
| 33 |
+
nn.BatchNorm2d(out_chan),
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
def forward(self, x):
|
| 37 |
+
residual = self.conv1(x)
|
| 38 |
+
residual = F.relu(self.bn1(residual))
|
| 39 |
+
residual = self.conv2(residual)
|
| 40 |
+
residual = self.bn2(residual)
|
| 41 |
+
|
| 42 |
+
shortcut = x
|
| 43 |
+
if self.downsample is not None:
|
| 44 |
+
shortcut = self.downsample(x)
|
| 45 |
+
|
| 46 |
+
out = shortcut + residual
|
| 47 |
+
out = self.relu(out)
|
| 48 |
+
return out
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def create_layer_basic(in_chan, out_chan, bnum, stride=1):
|
| 52 |
+
layers = [BasicBlock(in_chan, out_chan, stride=stride)]
|
| 53 |
+
for i in range(bnum-1):
|
| 54 |
+
layers.append(BasicBlock(out_chan, out_chan, stride=1))
|
| 55 |
+
return nn.Sequential(*layers)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class Resnet18(nn.Module):
|
| 59 |
+
def __init__(self):
|
| 60 |
+
super(Resnet18, self).__init__()
|
| 61 |
+
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
|
| 62 |
+
bias=False)
|
| 63 |
+
self.bn1 = nn.BatchNorm2d(64)
|
| 64 |
+
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
|
| 65 |
+
self.layer1 = create_layer_basic(64, 64, bnum=2, stride=1)
|
| 66 |
+
self.layer2 = create_layer_basic(64, 128, bnum=2, stride=2)
|
| 67 |
+
self.layer3 = create_layer_basic(128, 256, bnum=2, stride=2)
|
| 68 |
+
self.layer4 = create_layer_basic(256, 512, bnum=2, stride=2)
|
| 69 |
+
self.init_weight()
|
| 70 |
+
|
| 71 |
+
def forward(self, x):
|
| 72 |
+
x = self.conv1(x)
|
| 73 |
+
x = F.relu(self.bn1(x))
|
| 74 |
+
x = self.maxpool(x)
|
| 75 |
+
|
| 76 |
+
x = self.layer1(x)
|
| 77 |
+
feat8 = self.layer2(x) # 1/8
|
| 78 |
+
feat16 = self.layer3(feat8) # 1/16
|
| 79 |
+
feat32 = self.layer4(feat16) # 1/32
|
| 80 |
+
return feat8, feat16, feat32
|
| 81 |
+
|
| 82 |
+
def init_weight(self):
|
| 83 |
+
state_dict = modelzoo.load_url(resnet18_url)
|
| 84 |
+
self_state_dict = self.state_dict()
|
| 85 |
+
for k, v in state_dict.items():
|
| 86 |
+
if 'fc' in k: continue
|
| 87 |
+
self_state_dict.update({k: v})
|
| 88 |
+
self.load_state_dict(self_state_dict)
|
| 89 |
+
|
| 90 |
+
def get_params(self):
|
| 91 |
+
wd_params, nowd_params = [], []
|
| 92 |
+
for name, module in self.named_modules():
|
| 93 |
+
if isinstance(module, (nn.Linear, nn.Conv2d)):
|
| 94 |
+
wd_params.append(module.weight)
|
| 95 |
+
if not module.bias is None:
|
| 96 |
+
nowd_params.append(module.bias)
|
| 97 |
+
elif isinstance(module, nn.BatchNorm2d):
|
| 98 |
+
nowd_params += list(module.parameters())
|
| 99 |
+
return wd_params, nowd_params
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
if __name__ == "__main__":
|
| 103 |
+
net = Resnet18()
|
| 104 |
+
x = torch.randn(16, 3, 224, 224)
|
| 105 |
+
out = net(x)
|
| 106 |
+
print(out[0].size())
|
| 107 |
+
print(out[1].size())
|
| 108 |
+
print(out[2].size())
|
| 109 |
+
net.get_params()
|
face_parsing-main/samples/sample.gif
ADDED
|
Git LFS Details
|
face_parsing-main/samples/t.jpg
ADDED
|
face_parsing-main/softmask.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Module Function:
|
| 5 |
+
Generates a soft-edge nose mask (grayscale 0..255) from a single 512x1536 (WxH) image.
|
| 6 |
+
|
| 7 |
+
Wrapper Function:
|
| 8 |
+
make_softmask_pil(img: PIL.Image, weights: Path, inference_fp: Path,
|
| 9 |
+
device="cpu", y_top=400, sigma=4.0, nose_id=10)
|
| 10 |
+
Command Line:
|
| 11 |
+
python softmask_module.py --in_path ./datasets/train --weights ./fp_512.pth \
|
| 12 |
+
--device cpu --out_dir ./result_soft --y_top 400 --sigma 4
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import sys
|
| 17 |
+
import subprocess
|
| 18 |
+
import tempfile
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
import numpy as np
|
| 21 |
+
from PIL import Image, ImageFilter
|
| 22 |
+
|
| 23 |
+
# ---------- Utility Functions ----------
|
| 24 |
+
def ensure_dir(p: Path):
|
| 25 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
def load_rgb(path: Path) -> Image.Image:
|
| 28 |
+
return Image.open(path).convert("RGB")
|
| 29 |
+
|
| 30 |
+
def run_face_parsing(inference_fp: Path, images_dir: Path, weights: Path,
|
| 31 |
+
device: str, save_idx_dir: Path, img_size: int = 512):
|
| 32 |
+
cmd = [
|
| 33 |
+
sys.executable, str(inference_fp),
|
| 34 |
+
"--image_dir", str(images_dir),
|
| 35 |
+
"--weights", str(weights),
|
| 36 |
+
"--img_size", str(img_size),
|
| 37 |
+
"--device", device,
|
| 38 |
+
"--save_idx", str(save_idx_dir),
|
| 39 |
+
]
|
| 40 |
+
print("[INFO] face parsing:", " ".join(cmd))
|
| 41 |
+
subprocess.run(cmd, check=True)
|
| 42 |
+
|
| 43 |
+
def load_idx(idx_path: Path) -> np.ndarray:
|
| 44 |
+
if idx_path.suffix.lower() == ".npy":
|
| 45 |
+
return np.load(idx_path)
|
| 46 |
+
return np.array(Image.open(idx_path).convert("L"))
|
| 47 |
+
|
| 48 |
+
def build_full_mask(idx_512: np.ndarray, H: int, y_top: int, nose_id: int) -> Image.Image:
|
| 49 |
+
"""Back-project the 512x512 nose idx image to the 512xH (H=1536) image location"""
|
| 50 |
+
assert idx_512.shape == (512, 512), f"expect 512x512, got {idx_512.shape}"
|
| 51 |
+
out = np.zeros((H, 512), dtype=np.uint8)
|
| 52 |
+
nose_bin = (idx_512 == nose_id).astype(np.uint8) * 255
|
| 53 |
+
y1 = max(0, min(H - 512, y_top))
|
| 54 |
+
out[y1:y1+512, :] = nose_bin
|
| 55 |
+
return Image.fromarray(out, mode="L")
|
| 56 |
+
|
| 57 |
+
# ---------- Encapsulated function (direct external call) ----------
|
| 58 |
+
def make_softmask_pil(img: Image.Image, weights: Path, inference_fp: Path,
|
| 59 |
+
device="cpu", y_top=400, sigma=4.0, nose_id=10) -> Image.Image:
|
| 60 |
+
"""
|
| 61 |
+
Input:
|
| 62 |
+
img: PIL.Image, must be 512x1536 (WxH)
|
| 63 |
+
weights: Path, face parsing weights
|
| 64 |
+
inference_fp: Path, inference.py path
|
| 65 |
+
device: "cpu" / "cuda:0"
|
| 66 |
+
y_top: 512x512 square starting at row y_top
|
| 67 |
+
sigma: Gaussian softening radius
|
| 68 |
+
nose_id: ID of the nose class in face parsing (default 10)
|
| 69 |
+
Output:
|
| 70 |
+
512x1536 grayscale PIL.Image (white = nose, 0-255 soft edges)
|
| 71 |
+
"""
|
| 72 |
+
img = img.convert("RGB")
|
| 73 |
+
W, H = img.size
|
| 74 |
+
if (W, H) != (512, 1536):
|
| 75 |
+
raise ValueError(f"expect 512x1536 (WxH), got {W}x{H}")
|
| 76 |
+
|
| 77 |
+
# Cut out 512x512 blocks
|
| 78 |
+
y1 = max(0, min(1536 - 512, y_top))
|
| 79 |
+
patch = img.crop((0, y1, 512, y1 + 512))
|
| 80 |
+
|
| 81 |
+
# Run inference in a temporary directory
|
| 82 |
+
with tempfile.TemporaryDirectory(prefix="fp_soft_") as td:
|
| 83 |
+
td = Path(td)
|
| 84 |
+
d_in = td / "in"; d_idx = td / "idx"
|
| 85 |
+
ensure_dir(d_in); ensure_dir(d_idx)
|
| 86 |
+
sq_path = d_in / "tmp.png"
|
| 87 |
+
patch.save(sq_path)
|
| 88 |
+
|
| 89 |
+
run_face_parsing(inference_fp, d_in, weights, device, d_idx, img_size=512)
|
| 90 |
+
|
| 91 |
+
idx_npy = d_idx / "tmp.npy"
|
| 92 |
+
idx_png = d_idx / "tmp.png"
|
| 93 |
+
if idx_npy.exists():
|
| 94 |
+
idx = load_idx(idx_npy)
|
| 95 |
+
elif idx_png.exists():
|
| 96 |
+
idx = load_idx(idx_png)
|
| 97 |
+
else:
|
| 98 |
+
raise RuntimeError(f"no idx file in {d_idx}")
|
| 99 |
+
|
| 100 |
+
hard_mask = build_full_mask(idx, H=1536, y_top=y1, nose_id=nose_id)
|
| 101 |
+
|
| 102 |
+
# ---- Softened part ----
|
| 103 |
+
soft = hard_mask.filter(ImageFilter.GaussianBlur(radius=float(sigma)))
|
| 104 |
+
return soft
|
| 105 |
+
|
| 106 |
+
# ---------- Command line entry ----------
|
| 107 |
+
def main():
|
| 108 |
+
ap = argparse.ArgumentParser(description="生成 512x1536 软边鼻部 mask(支持批量)")
|
| 109 |
+
ap.add_argument("--in_path", required=True, help="单图或目录路径(512x1536)")
|
| 110 |
+
ap.add_argument("--weights", required=True, help="fp_512.pth 路径")
|
| 111 |
+
ap.add_argument("--inference_fp", default="inference.py", help="face parsing 脚本路径")
|
| 112 |
+
ap.add_argument("--device", default="cpu", help="cpu / cuda:0 / cuda:1")
|
| 113 |
+
ap.add_argument("--out_dir", required=True, help="输出目录")
|
| 114 |
+
ap.add_argument("--y_top", type=int, default=400, help="起始 y 坐标")
|
| 115 |
+
ap.add_argument("--sigma", type=float, default=4.0, help="高斯软化半径")
|
| 116 |
+
ap.add_argument("--nose_id", type=int, default=10, help="face parsing 中鼻子类别 ID")
|
| 117 |
+
args = ap.parse_args()
|
| 118 |
+
|
| 119 |
+
in_path = Path(args.in_path)
|
| 120 |
+
ensure_dir(Path(args.out_dir))
|
| 121 |
+
|
| 122 |
+
if in_path.is_file():
|
| 123 |
+
img = load_rgb(in_path)
|
| 124 |
+
mask = make_softmask_pil(img, Path(args.weights), Path(args.inference_fp),
|
| 125 |
+
device=args.device, y_top=args.y_top,
|
| 126 |
+
sigma=args.sigma, nose_id=args.nose_id)
|
| 127 |
+
out_path = Path(args.out_dir) / f"{in_path.stem}.png"
|
| 128 |
+
mask.save(out_path)
|
| 129 |
+
print(f"[OK] saved: {out_path}")
|
| 130 |
+
|
| 131 |
+
elif in_path.is_dir():
|
| 132 |
+
imgs = sorted([p for p in in_path.iterdir() if p.suffix.lower() in [".png", ".jpg", ".jpeg", ".bmp", ".webp"]])
|
| 133 |
+
for p in imgs:
|
| 134 |
+
img = load_rgb(p)
|
| 135 |
+
mask = make_softmask_pil(img, Path(args.weights), Path(args.inference_fp),
|
| 136 |
+
device=args.device, y_top=args.y_top,
|
| 137 |
+
sigma=args.sigma, nose_id=args.nose_id)
|
| 138 |
+
out_path = Path(args.out_dir) / f"{p.stem}.png"
|
| 139 |
+
mask.save(out_path)
|
| 140 |
+
print(f"[OK] saved: {out_path}")
|
| 141 |
+
else:
|
| 142 |
+
raise SystemExit(f"[ERR] invalid in_path: {in_path}")
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
main()
|
face_parsing-main/test_nose_mask.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
从 1024x1536(宽x高)拼接图片中,直接生成左半边 real_A 的 512x1536 鼻部二值掩膜(白=255,黑=0)。
|
| 5 |
+
内部自动:
|
| 6 |
+
1) 从左半边 A 截取 y_top 开始的 512x512 方块;
|
| 7 |
+
2) 调用你们现有的 inference.py 做 face parsing(ID=10 为鼻子);
|
| 8 |
+
3) 将鼻子区域回投影到 512x1536 的正确位置,输出 PNG(二值)。
|
| 9 |
+
|
| 10 |
+
用法示例:
|
| 11 |
+
python test_nose_mask.py \
|
| 12 |
+
--in_path ./datasets/test \
|
| 13 |
+
--weights ./fp_512.pth \
|
| 14 |
+
--device cpu \
|
| 15 |
+
--out_dir ./result_test \
|
| 16 |
+
--y_top 400 --patch 512
|
| 17 |
+
|
| 18 |
+
支持:输入是单图文件,或目录(批量处理所有常见图片后缀)。
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import shutil
|
| 23 |
+
import subprocess
|
| 24 |
+
import sys
|
| 25 |
+
import tempfile
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
from PIL import Image
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
IMG_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_image_rgb(path: Path) -> Image.Image:
|
| 36 |
+
im = Image.open(path).convert("RGB")
|
| 37 |
+
return im
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def ensure_dir(p: Path):
|
| 41 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def run_face_parsing_batch(
|
| 45 |
+
inference_fp: Path,
|
| 46 |
+
images_dir: Path,
|
| 47 |
+
weights: Path,
|
| 48 |
+
device: str,
|
| 49 |
+
save_idx_dir: Path,
|
| 50 |
+
img_size: int = 512,
|
| 51 |
+
):
|
| 52 |
+
"""
|
| 53 |
+
以命令行方式调用你们现有的 inference.py
|
| 54 |
+
只保存 idx(每像素类别图),我们随后读取其中的鼻子 ID=10。
|
| 55 |
+
"""
|
| 56 |
+
cmd = [
|
| 57 |
+
sys.executable, # 当前 python
|
| 58 |
+
str(inference_fp),
|
| 59 |
+
"--image_dir", str(images_dir),
|
| 60 |
+
"--weights", str(weights),
|
| 61 |
+
"--img_size", str(img_size),
|
| 62 |
+
"--device", device,
|
| 63 |
+
"--save_idx", str(save_idx_dir),
|
| 64 |
+
]
|
| 65 |
+
print("[INFO] Running face parsing:", " ".join(cmd))
|
| 66 |
+
subprocess.run(cmd, check=True)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def load_idx_map(idx_path: Path) -> np.ndarray:
|
| 70 |
+
"""
|
| 71 |
+
既兼容 .npy(很多 face parsing 脚本的做法),也兼容灰度 .png。
|
| 72 |
+
返回 HxW 的整型 numpy 数组。
|
| 73 |
+
"""
|
| 74 |
+
if idx_path.suffix.lower() == ".npy":
|
| 75 |
+
return np.load(idx_path)
|
| 76 |
+
# 假设是灰度 PNG,像素值就是类别 id
|
| 77 |
+
arr = np.array(Image.open(idx_path).convert("L"))
|
| 78 |
+
return arr
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def build_leftA_nose_mask_fullh(idx_map: np.ndarray, H: int, y_top: int, nose_id: int) -> np.ndarray:
|
| 82 |
+
"""
|
| 83 |
+
idx_map: 512x512(face parsing 的方块输出,ID=10 为鼻子)
|
| 84 |
+
H: 1536;输出为 512xH 的二值 mask(0/255),鼻子区域放在 [y_top:y_top+512]
|
| 85 |
+
"""
|
| 86 |
+
assert idx_map.shape == (512, 512), f"idx_map shape must be 512x512, got {idx_map.shape}"
|
| 87 |
+
out = np.zeros((H, 512), dtype=np.uint8) # HxW,左半边宽=512
|
| 88 |
+
nose_bin = (idx_map == nose_id).astype(np.uint8) * 255
|
| 89 |
+
y1 = max(0, min(H - 512, y_top))
|
| 90 |
+
out[y1:y1 + 512, :] = nose_bin
|
| 91 |
+
return out
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def process_one(
|
| 95 |
+
img_path: Path,
|
| 96 |
+
out_dir: Path,
|
| 97 |
+
inference_fp: Path,
|
| 98 |
+
weights: Path,
|
| 99 |
+
device: str,
|
| 100 |
+
y_top: int,
|
| 101 |
+
patch: int,
|
| 102 |
+
nose_id: int,
|
| 103 |
+
):
|
| 104 |
+
"""
|
| 105 |
+
单图处理:
|
| 106 |
+
- 读入 1024x1536 宽x高图片
|
| 107 |
+
- 取左半边 A(0:512, 0:1536)
|
| 108 |
+
- 截取 A[y_top:y_top+512, 0:512] → 512x512
|
| 109 |
+
- face parsing,取 ID=10
|
| 110 |
+
- 回填到 512x1536 高度位置
|
| 111 |
+
- 存为 PNG(白=255,黑=0)
|
| 112 |
+
"""
|
| 113 |
+
stem = img_path.stem
|
| 114 |
+
print(f"[INFO] Processing {img_path.name}")
|
| 115 |
+
|
| 116 |
+
# 读原图并校验尺寸(宽x高)
|
| 117 |
+
im = load_image_rgb(img_path)
|
| 118 |
+
W, H = im.size # PIL: (width, height)
|
| 119 |
+
if (W, H) != (1024, 1536):
|
| 120 |
+
raise SystemExit(f"[ERR] {img_path.name}: expected 1024x1536 (WxH), got {W}x{H}")
|
| 121 |
+
|
| 122 |
+
# 取左半边 A(x:0~512, y:0~1536)
|
| 123 |
+
A = im.crop((0, 0, 512, 1536)) # (left, top, right, bottom)
|
| 124 |
+
|
| 125 |
+
# 取 512x512 的方块(从 y_top 开始)
|
| 126 |
+
if patch != 512:
|
| 127 |
+
raise SystemExit("[ERR] 当前实现假设 patch=512 以匹配模型输入。")
|
| 128 |
+
y1 = max(0, min(1536 - 512, y_top))
|
| 129 |
+
A_sq = A.crop((0, y1, 512, y1 + 512)) # 512x512
|
| 130 |
+
|
| 131 |
+
# 在临时目录里做推理
|
| 132 |
+
with tempfile.TemporaryDirectory(prefix="fp_leftA_") as td:
|
| 133 |
+
td = Path(td)
|
| 134 |
+
tmp_in = td / "in"
|
| 135 |
+
tmp_idx = td / "idx"
|
| 136 |
+
ensure_dir(tmp_in)
|
| 137 |
+
ensure_dir(tmp_idx)
|
| 138 |
+
|
| 139 |
+
# 保存 512x512 方块
|
| 140 |
+
sq_path = tmp_in / f"{stem}.png"
|
| 141 |
+
A_sq.save(sq_path)
|
| 142 |
+
|
| 143 |
+
# 调用现有 face parsing 推理
|
| 144 |
+
run_face_parsing_batch(
|
| 145 |
+
inference_fp=inference_fp,
|
| 146 |
+
images_dir=tmp_in,
|
| 147 |
+
weights=weights,
|
| 148 |
+
device=device,
|
| 149 |
+
save_idx_dir=tmp_idx,
|
| 150 |
+
img_size=512,
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# 读取 idx 图(支持 .npy 或 .png)
|
| 154 |
+
# 可能文件名就是 stem.npy / stem.png
|
| 155 |
+
cand_npy = tmp_idx / f"{stem}.npy"
|
| 156 |
+
cand_png = tmp_idx / f"{stem}.png"
|
| 157 |
+
if cand_npy.exists():
|
| 158 |
+
idx_map = load_idx_map(cand_npy)
|
| 159 |
+
elif cand_png.exists():
|
| 160 |
+
idx_map = load_idx_map(cand_png)
|
| 161 |
+
else:
|
| 162 |
+
raise SystemExit(f"[ERR] 未在 {tmp_idx} 找到 {stem}.npy 或 {stem}.png")
|
| 163 |
+
|
| 164 |
+
# 构建 512x1536 ��值 mask(鼻子=255)
|
| 165 |
+
mask_full = build_leftA_nose_mask_fullh(idx_map, H=1536, y_top=y1, nose_id=nose_id)
|
| 166 |
+
|
| 167 |
+
# 保存(保持与 A 同宽高:512x1536)
|
| 168 |
+
ensure_dir(out_dir)
|
| 169 |
+
out_path = out_dir / f"{stem}.png"
|
| 170 |
+
Image.fromarray(mask_full, mode="L").save(out_path)
|
| 171 |
+
print(f"[OK] Saved: {out_path}")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def main():
|
| 175 |
+
ap = argparse.ArgumentParser(description="Make 512x1536 left-A nasal masks directly from 1024x1536 images.")
|
| 176 |
+
ap.add_argument("--in_path", required=True, help="单图路径或目录路径")
|
| 177 |
+
ap.add_argument("--weights", required=True, help="fp_512.pth 路径")
|
| 178 |
+
ap.add_argument("--inference_fp", default="inference.py", help="face parsing 推理脚本路径(默认当前目录下)")
|
| 179 |
+
ap.add_argument("--device", default="cpu", help="cpu 或 cuda:0 / cuda:1 ...")
|
| 180 |
+
ap.add_argument("--out_dir", required=True, help="输出目录(生成 512x1536 二值鼻部掩膜)")
|
| 181 |
+
ap.add_argument("--y_top", type=int, default=400, help="竖向起点(默认 400)")
|
| 182 |
+
ap.add_argument("--patch", type=int, default=512, help="方块高度(固定 512,匹配模型输入)")
|
| 183 |
+
ap.add_argument("--nose_id", type=int, default=10, help="face parsing 中鼻子类别 ID(默认 10)")
|
| 184 |
+
args = ap.parse_args()
|
| 185 |
+
|
| 186 |
+
in_path = Path(args.in_path)
|
| 187 |
+
out_dir = Path(args.out_dir)
|
| 188 |
+
weights = Path(args.weights)
|
| 189 |
+
inference_fp = Path(args.inference_fp)
|
| 190 |
+
|
| 191 |
+
if not inference_fp.exists():
|
| 192 |
+
raise SystemExit(f"[ERR] inference.py 不存在:{inference_fp}")
|
| 193 |
+
if not weights.exists():
|
| 194 |
+
raise SystemExit(f"[ERR] 权重不存在:{weights}")
|
| 195 |
+
|
| 196 |
+
if in_path.is_file():
|
| 197 |
+
process_one(
|
| 198 |
+
img_path=in_path,
|
| 199 |
+
out_dir=out_dir,
|
| 200 |
+
inference_fp=inference_fp,
|
| 201 |
+
weights=weights,
|
| 202 |
+
device=args.device,
|
| 203 |
+
y_top=args.y_top,
|
| 204 |
+
patch=args.patch,
|
| 205 |
+
nose_id=args.nose_id,
|
| 206 |
+
)
|
| 207 |
+
elif in_path.is_dir():
|
| 208 |
+
imgs = sorted([p for p in in_path.iterdir() if p.suffix.lower() in IMG_EXTS])
|
| 209 |
+
if not imgs:
|
| 210 |
+
raise SystemExit(f"[ERR] 目录下未找到图片:{in_path}")
|
| 211 |
+
for p in imgs:
|
| 212 |
+
process_one(
|
| 213 |
+
img_path=p,
|
| 214 |
+
out_dir=out_dir,
|
| 215 |
+
inference_fp=inference_fp,
|
| 216 |
+
weights=weights,
|
| 217 |
+
device=args.device,
|
| 218 |
+
y_top=args.y_top,
|
| 219 |
+
patch=args.patch,
|
| 220 |
+
nose_id=args.nose_id,
|
| 221 |
+
)
|
| 222 |
+
else:
|
| 223 |
+
raise SystemExit(f"[ERR] --in_path 非法:{in_path}")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
if __name__ == "__main__":
|
| 227 |
+
main()
|
face_parsing-main/test_nose_mask_single.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
从单张 512x1536(宽x高)的图片,生成对应的 512x1536 鼻部二值掩膜(白=255,黑=0)。
|
| 5 |
+
|
| 6 |
+
两种用法:
|
| 7 |
+
1) 作为模块被 import: from test_nose_mask_single import make_nose_mask_pil
|
| 8 |
+
2) 命令行:python test_nose_mask_single.py --img xxx.png --weights fp_512.pth --inference_fp inference.py --device cpu --out_dir result_single --y_top 400
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import subprocess
|
| 13 |
+
import sys
|
| 14 |
+
import tempfile
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
from PIL import Image
|
| 19 |
+
|
| 20 |
+
# ----------------- 工具函数 -----------------
|
| 21 |
+
def ensure_dir(p: Path):
|
| 22 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
|
| 24 |
+
def run_face_parsing(
|
| 25 |
+
inference_fp: Path,
|
| 26 |
+
image_dir: Path,
|
| 27 |
+
weights: Path,
|
| 28 |
+
device: str,
|
| 29 |
+
save_idx_dir: Path,
|
| 30 |
+
img_size: int = 512,
|
| 31 |
+
):
|
| 32 |
+
"""调用 inference.py 做 face parsing,并把语义索引图(.npy 或 .png)保存到 save_idx_dir。"""
|
| 33 |
+
cmd = [
|
| 34 |
+
sys.executable, str(inference_fp),
|
| 35 |
+
"--image_dir", str(image_dir),
|
| 36 |
+
"--weights", str(weights),
|
| 37 |
+
"--img_size", str(img_size),
|
| 38 |
+
"--device", device,
|
| 39 |
+
"--save_idx", str(save_idx_dir),
|
| 40 |
+
]
|
| 41 |
+
print("[INFO] Run:", " ".join(cmd))
|
| 42 |
+
subprocess.run(cmd, check=True)
|
| 43 |
+
|
| 44 |
+
def load_idx(idx_path: Path) -> np.ndarray:
|
| 45 |
+
"""读取语义索引图(支持 .npy 或 单通道 .png)。"""
|
| 46 |
+
if idx_path.suffix.lower() == ".npy":
|
| 47 |
+
return np.load(idx_path)
|
| 48 |
+
arr = np.array(Image.open(idx_path).convert("L"))
|
| 49 |
+
return arr
|
| 50 |
+
|
| 51 |
+
def build_mask_full(idx_512: np.ndarray, H: int, y_top: int, nose_id: int) -> np.ndarray:
|
| 52 |
+
"""把 512x512 的鼻子索引区域回投影到整幅高 H=1536 的图像上。"""
|
| 53 |
+
assert idx_512.shape == (512, 512), f"idx must be 512x512, got {idx_512.shape}"
|
| 54 |
+
out = np.zeros((H, 512), dtype=np.uint8)
|
| 55 |
+
nose = (idx_512 == nose_id).astype(np.uint8) * 255
|
| 56 |
+
y1 = max(0, min(H - 512, y_top))
|
| 57 |
+
out[y1:y1+512, :] = nose
|
| 58 |
+
return out
|
| 59 |
+
|
| 60 |
+
# ----------------- 封装函数(给 Flask 直接调用) -----------------
|
| 61 |
+
def make_nose_mask_pil(
|
| 62 |
+
img: Image.Image,
|
| 63 |
+
weights: Path,
|
| 64 |
+
inference_fp: Path,
|
| 65 |
+
device: str = "cpu",
|
| 66 |
+
y_top: int = 400,
|
| 67 |
+
nose_id: int = 10,
|
| 68 |
+
) -> Image.Image:
|
| 69 |
+
"""
|
| 70 |
+
输入:一张 512x1536 的 PIL.Image(RGB)
|
| 71 |
+
输出:同尺寸 512x1536 的单通道鼻子掩码 PIL.Image(mode="L", 白=255)
|
| 72 |
+
"""
|
| 73 |
+
img = img.convert("RGB")
|
| 74 |
+
W, H = img.size
|
| 75 |
+
if (W, H) != (512, 1536):
|
| 76 |
+
raise ValueError(f"期望输入尺寸为 512x1536(宽x高),但收到 {W}x{H}")
|
| 77 |
+
|
| 78 |
+
# 1) 按 y_top 裁出 512x512 方块
|
| 79 |
+
y1 = max(0, min(1536 - 512, y_top))
|
| 80 |
+
patch = img.crop((0, y1, 512, y1 + 512))
|
| 81 |
+
|
| 82 |
+
# 2) 临时目录跑 inference.py
|
| 83 |
+
with tempfile.TemporaryDirectory(prefix="fp_one_") as td:
|
| 84 |
+
td = Path(td)
|
| 85 |
+
tmp_in = td / "in"
|
| 86 |
+
tmp_idx = td / "idx"
|
| 87 |
+
ensure_dir(tmp_in); ensure_dir(tmp_idx)
|
| 88 |
+
|
| 89 |
+
sq_path = tmp_in / "tmp.png"
|
| 90 |
+
patch.save(sq_path)
|
| 91 |
+
|
| 92 |
+
run_face_parsing(
|
| 93 |
+
inference_fp=inference_fp,
|
| 94 |
+
image_dir=tmp_in,
|
| 95 |
+
weights=weights,
|
| 96 |
+
device=device,
|
| 97 |
+
save_idx_dir=tmp_idx,
|
| 98 |
+
img_size=512,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# 3) 读取 parsing 输出
|
| 102 |
+
npy = tmp_idx / "tmp.npy"
|
| 103 |
+
png = tmp_idx / "tmp.png"
|
| 104 |
+
if npy.exists():
|
| 105 |
+
idx = load_idx(npy)
|
| 106 |
+
elif png.exists():
|
| 107 |
+
idx = load_idx(png)
|
| 108 |
+
else:
|
| 109 |
+
raise RuntimeError(f"未在 {tmp_idx} 找到 tmp.npy 或 tmp.png")
|
| 110 |
+
|
| 111 |
+
# 4) 组装为整幅 512x1536 掩码
|
| 112 |
+
mask_full = build_mask_full(idx, H=1536, y_top=y1, nose_id=nose_id)
|
| 113 |
+
return Image.fromarray(mask_full, mode="L")
|
| 114 |
+
|
| 115 |
+
# ----------------- 保留命令行入口(你原来的用法照常可用) -----------------
|
| 116 |
+
def main():
|
| 117 |
+
ap = argparse.ArgumentParser(description="Make 512x1536 nasal mask from a single image.")
|
| 118 |
+
ap.add_argument("--img", required=True, help="输入单图路径(必须 512x1536,宽x高)")
|
| 119 |
+
ap.add_argument("--weights", required=True, help="fp_512.pth 权重路径")
|
| 120 |
+
ap.add_argument("--inference_fp", default="inference.py", help="face parsing 推理脚本路径")
|
| 121 |
+
ap.add_argument("--device", default="cpu", help="cpu 或 cuda:0 等")
|
| 122 |
+
ap.add_argument("--out_dir", required=True, help="输出目录")
|
| 123 |
+
ap.add_argument("--y_top", type=int, default=400, help="从何处向下裁 512 方块")
|
| 124 |
+
ap.add_argument("--nose_id", type=int, default=10, help="鼻子类别 ID")
|
| 125 |
+
args = ap.parse_args()
|
| 126 |
+
|
| 127 |
+
img_path = Path(args.img)
|
| 128 |
+
im = Image.open(img_path).convert("RGB")
|
| 129 |
+
|
| 130 |
+
mask_pil = make_nose_mask_pil(
|
| 131 |
+
img=im,
|
| 132 |
+
weights=Path(args.weights),
|
| 133 |
+
inference_fp=Path(args.inference_fp),
|
| 134 |
+
device=args.device,
|
| 135 |
+
y_top=args.y_top,
|
| 136 |
+
nose_id=args.nose_id,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
out_dir = Path(args.out_dir); ensure_dir(out_dir)
|
| 140 |
+
out_path = out_dir / (img_path.stem + ".png")
|
| 141 |
+
mask_pil.save(out_path)
|
| 142 |
+
print(f"[OK] Saved: {out_path}")
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
main()
|
face_parsing-main/train.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- encoding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
|
| 5 |
+
from model import BiSeNet
|
| 6 |
+
from face_dataset import FaceMask
|
| 7 |
+
from loss import OhemCELoss
|
| 8 |
+
import torch.optim as Optimizer
|
| 9 |
+
import cv2
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn as nn
|
| 14 |
+
from torch.utils.data import DataLoader
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
|
| 17 |
+
import os.path as osp
|
| 18 |
+
import time
|
| 19 |
+
import datetime
|
| 20 |
+
import argparse
|
| 21 |
+
|
| 22 |
+
def set_learning_rate(optimizer, lr):
|
| 23 |
+
for param_group in optimizer.param_groups:
|
| 24 |
+
param_group['lr'] = lr
|
| 25 |
+
|
| 26 |
+
def train(fintune_model,image_size,lr0,path_data,model_exp):
|
| 27 |
+
|
| 28 |
+
# config 训练配置
|
| 29 |
+
max_epoch = 1000
|
| 30 |
+
n_classes = 19
|
| 31 |
+
n_img_per_gpu = 16
|
| 32 |
+
n_workers = 12
|
| 33 |
+
cropsize = [int(image_size*0.8),int(image_size*0.8)]
|
| 34 |
+
|
| 35 |
+
# DataLoader 数据迭代器
|
| 36 |
+
ds = FaceMask(path_data,img_size = image_size, cropsize=cropsize, mode='train')
|
| 37 |
+
|
| 38 |
+
dl = DataLoader(ds,
|
| 39 |
+
batch_size = n_img_per_gpu,
|
| 40 |
+
shuffle = True,
|
| 41 |
+
num_workers = n_workers,
|
| 42 |
+
pin_memory = True,
|
| 43 |
+
drop_last = True)
|
| 44 |
+
|
| 45 |
+
# model
|
| 46 |
+
ignore_idx = -100
|
| 47 |
+
# 构建模型
|
| 48 |
+
use_cuda = torch.cuda.is_available()
|
| 49 |
+
device = torch.device("cuda:0" if use_cuda else "cpu")
|
| 50 |
+
net = BiSeNet(n_classes=n_classes)
|
| 51 |
+
net = net.to(device)
|
| 52 |
+
# 加载预训练模型
|
| 53 |
+
if os.access(fintune_model,os.F_OK) and (fintune_model is not None):# checkpoint
|
| 54 |
+
chkpt = torch.load(fintune_model, map_location=device)
|
| 55 |
+
net.load_state_dict(chkpt)
|
| 56 |
+
print('load fintune model : {}'.format(fintune_model))
|
| 57 |
+
else:
|
| 58 |
+
print('no fintune model')
|
| 59 |
+
# 构建损失函数
|
| 60 |
+
score_thres = 0.7
|
| 61 |
+
n_min = n_img_per_gpu * cropsize[0] * cropsize[1]//16
|
| 62 |
+
LossP = OhemCELoss(thresh=score_thres, n_min=n_min, ignore_lb=ignore_idx)
|
| 63 |
+
Loss2 = OhemCELoss(thresh=score_thres, n_min=n_min, ignore_lb=ignore_idx)
|
| 64 |
+
Loss3 = OhemCELoss(thresh=score_thres, n_min=n_min, ignore_lb=ignore_idx)
|
| 65 |
+
|
| 66 |
+
## optimizer
|
| 67 |
+
momentum = 0.9
|
| 68 |
+
weight_decay = 5e-4
|
| 69 |
+
lr_start = lr0
|
| 70 |
+
# 构建优化器
|
| 71 |
+
optim = Optimizer.SGD(
|
| 72 |
+
net.parameters(),
|
| 73 |
+
lr = lr_start,
|
| 74 |
+
momentum = momentum,
|
| 75 |
+
weight_decay = weight_decay)
|
| 76 |
+
|
| 77 |
+
# train loop
|
| 78 |
+
msg_iter = 50
|
| 79 |
+
loss_avg = []
|
| 80 |
+
st = glob_st = time.time()
|
| 81 |
+
# diter = iter(dl)
|
| 82 |
+
epoch = 0
|
| 83 |
+
flag_change_lr_cnt = 0 # 学习率更新计数器
|
| 84 |
+
init_lr = lr_start # 学习率
|
| 85 |
+
|
| 86 |
+
best_loss = np.inf
|
| 87 |
+
loss_mean = 0. # 损失均值
|
| 88 |
+
loss_idx = 0. # 损失计算计数器
|
| 89 |
+
# 训练
|
| 90 |
+
print('start training ~')
|
| 91 |
+
it = 0
|
| 92 |
+
for epoch in range(max_epoch):
|
| 93 |
+
net.train()
|
| 94 |
+
# 学习率更新策略
|
| 95 |
+
if loss_mean!=0.:
|
| 96 |
+
if best_loss > (loss_mean/loss_idx):
|
| 97 |
+
flag_change_lr_cnt = 0
|
| 98 |
+
best_loss = (loss_mean/loss_idx)
|
| 99 |
+
else:
|
| 100 |
+
flag_change_lr_cnt += 1
|
| 101 |
+
|
| 102 |
+
if flag_change_lr_cnt > 30:
|
| 103 |
+
init_lr = init_lr*0.1
|
| 104 |
+
set_learning_rate(optim, init_lr)
|
| 105 |
+
flag_change_lr_cnt = 0
|
| 106 |
+
|
| 107 |
+
loss_mean = 0. # 损失均值
|
| 108 |
+
loss_idx = 0. # 损失计算计数器
|
| 109 |
+
|
| 110 |
+
for i, (im, lb) in enumerate(dl):
|
| 111 |
+
|
| 112 |
+
im = im.cuda()
|
| 113 |
+
lb = lb.cuda()
|
| 114 |
+
H, W = im.size()[2:]
|
| 115 |
+
lb = torch.squeeze(lb, 1)
|
| 116 |
+
|
| 117 |
+
optim.zero_grad()
|
| 118 |
+
out, out16, out32 = net(im)
|
| 119 |
+
lossp = LossP(out, lb)
|
| 120 |
+
loss2 = Loss2(out16, lb)
|
| 121 |
+
loss3 = Loss3(out32, lb)
|
| 122 |
+
loss = lossp + loss2 + loss3
|
| 123 |
+
|
| 124 |
+
loss_mean += loss.item()
|
| 125 |
+
loss_idx += 1.
|
| 126 |
+
|
| 127 |
+
loss.backward()
|
| 128 |
+
optim.step()
|
| 129 |
+
|
| 130 |
+
if it % msg_iter == 0:
|
| 131 |
+
|
| 132 |
+
print('epoch <{}/{}> -->> <{}/{}> -> iter {} : loss {:.5f}, loss_mean :{:.5f}, best_loss :{:.5f},lr :{:.6f},batch_size : {},img_size :{}'.\
|
| 133 |
+
format(epoch,max_epoch,i,int(ds.__len__()/n_img_per_gpu),it,loss.item(),loss_mean/loss_idx,best_loss,init_lr,n_img_per_gpu,image_size))
|
| 134 |
+
|
| 135 |
+
if (it) % 500 == 0:
|
| 136 |
+
state = net.module.state_dict() if hasattr(net, 'module') else net.state_dict()
|
| 137 |
+
torch.save(state, model_exp+'fp_{}_latest.pth'.format(image_size))
|
| 138 |
+
it += 1
|
| 139 |
+
torch.save(state, model_exp+'fp_{}_epoch-{}.pth'.format(image_size,epoch))
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
image_size = 512
|
| 143 |
+
lr0 = 1e-4
|
| 144 |
+
model_exp = './model_exp/'
|
| 145 |
+
path_data = './CelebAMask-HQ/'
|
| 146 |
+
if not osp.exists(model_exp):
|
| 147 |
+
os.makedirs(model_exp)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
loc_time = time.localtime()
|
| 151 |
+
model_exp += time.strftime("%Y-%m-%d_%H-%M-%S", loc_time)+'/'
|
| 152 |
+
if not osp.exists(model_exp):
|
| 153 |
+
os.makedirs(model_exp)
|
| 154 |
+
|
| 155 |
+
fintune_model = './weights/fp0.pth'
|
| 156 |
+
|
| 157 |
+
train(fintune_model,image_size,lr0,path_data,model_exp)
|
face_parsing-main/transform.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- encoding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import PIL.ImageEnhance as ImageEnhance
|
| 6 |
+
import random
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
class RandomCrop(object):
|
| 10 |
+
def __init__(self, size, *args, **kwargs):
|
| 11 |
+
self.size = size
|
| 12 |
+
|
| 13 |
+
def __call__(self, im_lb):
|
| 14 |
+
im = im_lb['im']
|
| 15 |
+
lb = im_lb['lb']
|
| 16 |
+
assert im.size == lb.size
|
| 17 |
+
W, H = self.size
|
| 18 |
+
w, h = im.size
|
| 19 |
+
|
| 20 |
+
if (W, H) == (w, h): return dict(im=im, lb=lb)
|
| 21 |
+
if w < W or h < H:
|
| 22 |
+
scale = float(W) / w if w < h else float(H) / h
|
| 23 |
+
w, h = int(scale * w + 1), int(scale * h + 1)
|
| 24 |
+
im = im.resize((w, h), Image.BILINEAR)
|
| 25 |
+
lb = lb.resize((w, h), Image.NEAREST)
|
| 26 |
+
sw, sh = random.random() * (w - W), random.random() * (h - H)
|
| 27 |
+
crop = int(sw), int(sh), int(sw) + W, int(sh) + H
|
| 28 |
+
return dict(
|
| 29 |
+
im = im.crop(crop),
|
| 30 |
+
lb = lb.crop(crop)
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class HorizontalFlip(object):
|
| 35 |
+
def __init__(self, p=0.5, *args, **kwargs):
|
| 36 |
+
self.p = p
|
| 37 |
+
|
| 38 |
+
def __call__(self, im_lb):
|
| 39 |
+
if random.random() > self.p:
|
| 40 |
+
return im_lb
|
| 41 |
+
else:
|
| 42 |
+
im = im_lb['im']
|
| 43 |
+
lb = im_lb['lb']
|
| 44 |
+
|
| 45 |
+
# atts = [1 'skin', 2 'l_brow', 3 'r_brow', 4 'l_eye', 5 'r_eye', 6 'eye_g', 7 'l_ear', 8 'r_ear', 9 'ear_r',
|
| 46 |
+
# 10 'nose', 11 'mouth', 12 'u_lip', 13 'l_lip', 14 'neck', 15 'neck_l', 16 'cloth', 17 'hair', 18 'hat']
|
| 47 |
+
|
| 48 |
+
flip_lb = np.array(lb)
|
| 49 |
+
flip_lb[lb == 2] = 3
|
| 50 |
+
flip_lb[lb == 3] = 2
|
| 51 |
+
flip_lb[lb == 4] = 5
|
| 52 |
+
flip_lb[lb == 5] = 4
|
| 53 |
+
flip_lb[lb == 7] = 8
|
| 54 |
+
flip_lb[lb == 8] = 7
|
| 55 |
+
flip_lb = Image.fromarray(flip_lb)
|
| 56 |
+
return dict(im = im.transpose(Image.FLIP_LEFT_RIGHT),
|
| 57 |
+
lb = flip_lb.transpose(Image.FLIP_LEFT_RIGHT),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class RandomScale(object):
|
| 62 |
+
def __init__(self, scales=(1, ), *args, **kwargs):
|
| 63 |
+
self.scales = scales
|
| 64 |
+
|
| 65 |
+
def __call__(self, im_lb):
|
| 66 |
+
im = im_lb['im']
|
| 67 |
+
lb = im_lb['lb']
|
| 68 |
+
W, H = im.size
|
| 69 |
+
scale = random.choice(self.scales)
|
| 70 |
+
w, h = int(W * scale), int(H * scale)
|
| 71 |
+
return dict(im = im.resize((w, h), Image.BILINEAR),
|
| 72 |
+
lb = lb.resize((w, h), Image.NEAREST),
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ColorJitter(object):
|
| 77 |
+
def __init__(self, brightness=None, contrast=None, saturation=None, *args, **kwargs):
|
| 78 |
+
if not brightness is None and brightness>0:
|
| 79 |
+
self.brightness = [max(1-brightness, 0), 1+brightness]
|
| 80 |
+
if not contrast is None and contrast>0:
|
| 81 |
+
self.contrast = [max(1-contrast, 0), 1+contrast]
|
| 82 |
+
if not saturation is None and saturation>0:
|
| 83 |
+
self.saturation = [max(1-saturation, 0), 1+saturation]
|
| 84 |
+
|
| 85 |
+
def __call__(self, im_lb):
|
| 86 |
+
im = im_lb['im']
|
| 87 |
+
lb = im_lb['lb']
|
| 88 |
+
r_brightness = random.uniform(self.brightness[0], self.brightness[1])
|
| 89 |
+
r_contrast = random.uniform(self.contrast[0], self.contrast[1])
|
| 90 |
+
r_saturation = random.uniform(self.saturation[0], self.saturation[1])
|
| 91 |
+
im = ImageEnhance.Brightness(im).enhance(r_brightness)
|
| 92 |
+
im = ImageEnhance.Contrast(im).enhance(r_contrast)
|
| 93 |
+
im = ImageEnhance.Color(im).enhance(r_saturation)
|
| 94 |
+
return dict(im = im,
|
| 95 |
+
lb = lb,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class MultiScale(object):
|
| 100 |
+
def __init__(self, scales):
|
| 101 |
+
self.scales = scales
|
| 102 |
+
|
| 103 |
+
def __call__(self, img):
|
| 104 |
+
W, H = img.size
|
| 105 |
+
sizes = [(int(W*ratio), int(H*ratio)) for ratio in self.scales]
|
| 106 |
+
imgs = []
|
| 107 |
+
[imgs.append(img.resize(size, Image.BILINEAR)) for size in sizes]
|
| 108 |
+
return imgs
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class Compose(object):
|
| 112 |
+
def __init__(self, do_list):
|
| 113 |
+
self.do_list = do_list
|
| 114 |
+
|
| 115 |
+
def __call__(self, im_lb):
|
| 116 |
+
for comp in self.do_list:
|
| 117 |
+
im_lb = comp(im_lb)
|
| 118 |
+
return im_lb
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
flip = HorizontalFlip(p = 1)
|
| 125 |
+
crop = RandomCrop((321, 321))
|
| 126 |
+
rscales = RandomScale((0.75, 1.0, 1.5, 1.75, 2.0))
|
| 127 |
+
img = Image.open('data/img.jpg')
|
| 128 |
+
lb = Image.open('data/label.png')
|
flask_GAN/app.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import os, sys, io, uuid, time, traceback
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
from flask import Flask, request, jsonify, render_template_string, send_from_directory
|
| 8 |
+
from werkzeug.utils import secure_filename
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
import torchvision.transforms as T
|
| 13 |
+
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
# ========= 你训练仓库的路径:改成你的实际路径 =========
|
| 17 |
+
PIX2PIX_ROOT = "/Users/liutao/Downloads/python/pix2pix_local"
|
| 18 |
+
sys.path.insert(0, PIX2PIX_ROOT)
|
| 19 |
+
|
| 20 |
+
# 从你的训练仓库导入“网络构造函数”
|
| 21 |
+
# 官方实现一般是 models/networks.py 里提供 define_G
|
| 22 |
+
from models.networks import define_G
|
| 23 |
+
|
| 24 |
+
# ---------------- 基本配置 ----------------
|
| 25 |
+
WEIGHTS_PATH = "./weights/150_net_G.pth" # 你的权重
|
| 26 |
+
UPLOAD_DIR = "./runtime/uploads"
|
| 27 |
+
OUTPUT_DIR = "./runtime/outputs"
|
| 28 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 29 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 30 |
+
|
| 31 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 32 |
+
IMG_H, IMG_W = 1536, 512
|
| 33 |
+
NORM_TO_MINUS1_1 = True # Pix2Pix 通常使用 [-1,1] 归一化
|
| 34 |
+
|
| 35 |
+
# ---------------- Flask ----------------
|
| 36 |
+
app = Flask(__name__)
|
| 37 |
+
|
| 38 |
+
# 放在 app = Flask(__name__) 下面
|
| 39 |
+
from flask_cors import CORS
|
| 40 |
+
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
|
| 41 |
+
|
| 42 |
+
# 允许较大上传(比如 50MB)
|
| 43 |
+
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024
|
| 44 |
+
|
| 45 |
+
# 统一给响应加上允许头(有时浏览器仍会要)
|
| 46 |
+
@app.after_request
|
| 47 |
+
def add_cors_headers(resp):
|
| 48 |
+
resp.headers["Access-Control-Allow-Origin"] = "*"
|
| 49 |
+
resp.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"
|
| 50 |
+
resp.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization"
|
| 51 |
+
return resp
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
INDEX_HTML = r"""
|
| 55 |
+
<!doctype html>
|
| 56 |
+
<html lang="zh-CN">
|
| 57 |
+
<head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
|
| 58 |
+
<title>Pix2Pix 4通道推理 Demo</title>
|
| 59 |
+
<style>
|
| 60 |
+
body {
|
| 61 |
+
font-family: system-ui;
|
| 62 |
+
max-width: 980px;
|
| 63 |
+
margin: 24px auto;
|
| 64 |
+
}
|
| 65 |
+
.card {
|
| 66 |
+
border: 1px solid #e5e7eb;
|
| 67 |
+
border-radius: 12px;
|
| 68 |
+
padding: 16px;
|
| 69 |
+
margin: 12px 0;
|
| 70 |
+
}
|
| 71 |
+
.btn {
|
| 72 |
+
padding: 10px 16px;
|
| 73 |
+
border-radius: 8px;
|
| 74 |
+
border: 1px solid #e5e7eb;
|
| 75 |
+
background: #10b981;
|
| 76 |
+
color: #fff;
|
| 77 |
+
cursor: pointer;
|
| 78 |
+
}
|
| 79 |
+
.mono {
|
| 80 |
+
font-family: ui-monospace, Menlo, Consolas, monospace;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* ✅ 新增部分:限制结果图片的显示尺寸 */
|
| 84 |
+
#result img {
|
| 85 |
+
max-width: 128px; /* 最大显示宽度为 512px */
|
| 86 |
+
max-height: 600px; /* 超过则等比例缩放 */
|
| 87 |
+
/* 自适应容器宽度 */
|
| 88 |
+
height: auto;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
border: 1px solid #e5e7eb;
|
| 91 |
+
display: block;
|
| 92 |
+
margin-top: 10px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* 可选:添加“查看原图”链接样式 */
|
| 96 |
+
.view-full {
|
| 97 |
+
display: inline-block;
|
| 98 |
+
margin-top: 8px;
|
| 99 |
+
color: #2563eb;
|
| 100 |
+
font-size: 14px;
|
| 101 |
+
}
|
| 102 |
+
</style>
|
| 103 |
+
</head>
|
| 104 |
+
<body>
|
| 105 |
+
<h2>Pix2Pix 4通道(RGB+黑mask, 512×1536)在线推理</h2>
|
| 106 |
+
|
| 107 |
+
<div class="card">
|
| 108 |
+
<p>当前权重:<code id="w"></code></p>
|
| 109 |
+
<p>上传一张图片(将被缩放到 512×1536,并在后端叠加全黑 mask 作为第4通道):</p>
|
| 110 |
+
<input id="img" type="file" accept="image/*"/>
|
| 111 |
+
<button id="run" class="btn">生成</button>
|
| 112 |
+
<span id="s" class="mono"></span>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div class="card">
|
| 116 |
+
<h3>输出</h3>
|
| 117 |
+
<div id="out"></div>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<script>
|
| 121 |
+
async function postForm(url, formData){
|
| 122 |
+
const r = await fetch(url, {method:"POST", body:formData});
|
| 123 |
+
if(!r.ok) throw new Error("HTTP "+r.status);
|
| 124 |
+
return await r.json();
|
| 125 |
+
}
|
| 126 |
+
document.getElementById("w").textContent = "{{ weight_name }}";
|
| 127 |
+
document.getElementById("run").onclick = async ()=>{
|
| 128 |
+
const s = document.getElementById("s");
|
| 129 |
+
const out = document.getElementById("out");
|
| 130 |
+
out.innerHTML = "";
|
| 131 |
+
const f = document.getElementById("img").files[0];
|
| 132 |
+
if(!f){ s.textContent = "请先选择图片"; return; }
|
| 133 |
+
s.textContent = "推理中...";
|
| 134 |
+
const fd = new FormData();
|
| 135 |
+
fd.append("image", f);
|
| 136 |
+
try{
|
| 137 |
+
const j = await postForm("/api/predict", fd);
|
| 138 |
+
s.textContent = "耗时 "+j.latency_ms+" ms";
|
| 139 |
+
const img = document.createElement("img");
|
| 140 |
+
img.src = j.output_url+"?t="+Date.now();
|
| 141 |
+
img.style.maxWidth = "100%";
|
| 142 |
+
out.appendChild(img);
|
| 143 |
+
}catch(e){
|
| 144 |
+
s.textContent = "失败:"+e.message;
|
| 145 |
+
}
|
| 146 |
+
};
|
| 147 |
+
</script>
|
| 148 |
+
</body></html>
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
# ---------------- 模型加载(关键) ----------------
|
| 152 |
+
NETG: Optional[nn.Module] = None
|
| 153 |
+
|
| 154 |
+
def build_netG() -> nn.Module:
|
| 155 |
+
"""
|
| 156 |
+
复用你训练时的参数:
|
| 157 |
+
--netG unet_512 --norm instance --no_dropout
|
| 158 |
+
且你是 4通道输入(RGB+mask),3通道输出
|
| 159 |
+
官方实现 define_G 的典型签名:
|
| 160 |
+
define_G(input_nc, output_nc, ngf=64, netG='unet_256', norm='batch', use_dropout=False, init_type='normal', init_gain=0.02, gpu_ids=[])
|
| 161 |
+
"""
|
| 162 |
+
netG = define_G(
|
| 163 |
+
input_nc=4, # 你的输入 RGB+mask = 4
|
| 164 |
+
output_nc=3, # 输出 RGB = 3
|
| 165 |
+
ngf=64,
|
| 166 |
+
netG='unet_512', # 训练用的 unet_512
|
| 167 |
+
norm='instance', # 训练用的 instance
|
| 168 |
+
use_dropout=False # 训练时 --no_dropout
|
| 169 |
+
)
|
| 170 |
+
return netG
|
| 171 |
+
|
| 172 |
+
def load_weights_into(net: nn.Module, weight_path: str):
|
| 173 |
+
state = torch.load(weight_path, map_location="cpu")
|
| 174 |
+
# 有些保存方式会套一层
|
| 175 |
+
for key in ["state_dict", "netG", "model", "module"]:
|
| 176 |
+
if isinstance(state, dict) and key in state and isinstance(state[key], (dict, torch.nn.modules.module.Module)):
|
| 177 |
+
# 常见:{'state_dict': {...}} 或 {'netG': state_dict}
|
| 178 |
+
state = state[key]
|
| 179 |
+
break
|
| 180 |
+
# 如果键名带 'module.' 前缀,去掉
|
| 181 |
+
if isinstance(state, dict):
|
| 182 |
+
new_state = {}
|
| 183 |
+
for k, v in state.items():
|
| 184 |
+
nk = k.replace("module.", "") # DataParallel 保存会多这个前缀
|
| 185 |
+
new_state[nk] = v
|
| 186 |
+
state = new_state
|
| 187 |
+
missing, unexpected = net.load_state_dict(state, strict=False)
|
| 188 |
+
print(f"[load_state_dict] missing: {missing}, unexpected: {unexpected}")
|
| 189 |
+
|
| 190 |
+
def init_model():
|
| 191 |
+
global NETG
|
| 192 |
+
netG = build_netG()
|
| 193 |
+
load_weights_into(netG, WEIGHTS_PATH)
|
| 194 |
+
netG.eval().to(DEVICE)
|
| 195 |
+
NETG = netG
|
| 196 |
+
print(f"[OK] netG loaded on {DEVICE} from {WEIGHTS_PATH}")
|
| 197 |
+
|
| 198 |
+
# ---------------- 预处理/后处理 ----------------
|
| 199 |
+
def pil_to_tensor_4ch(img: Image.Image, mask_img: Optional[Image.Image] = None) -> torch.Tensor:
|
| 200 |
+
# 1) 尺寸对齐:只有不一致才调整
|
| 201 |
+
img = img.convert("RGB")
|
| 202 |
+
if img.size != (IMG_W, IMG_H):
|
| 203 |
+
# 你的数据就是 512x1536;不一致时才处理:等比按高缩放,再左裁/右补到 512
|
| 204 |
+
new_w = round(img.width * IMG_H / img.height)
|
| 205 |
+
img = img.resize((new_w, IMG_H), Image.BICUBIC)
|
| 206 |
+
if new_w >= IMG_W:
|
| 207 |
+
img = img.crop((0, 0, IMG_W, IMG_H))
|
| 208 |
+
else:
|
| 209 |
+
pad = Image.new("RGB", (IMG_W, IMG_H), (0,0,0))
|
| 210 |
+
pad.paste(img, (0,0))
|
| 211 |
+
img = pad
|
| 212 |
+
|
| 213 |
+
if mask_img is not None:
|
| 214 |
+
mask_img = mask_img.convert("L")
|
| 215 |
+
if mask_img.size != (IMG_W, IMG_H):
|
| 216 |
+
# 对 mask 只能用最近邻,避免灰边
|
| 217 |
+
new_w = round(mask_img.width * IMG_H / mask_img.height)
|
| 218 |
+
mask_img = mask_img.resize((new_w, IMG_H), Image.NEAREST)
|
| 219 |
+
if new_w >= IMG_W:
|
| 220 |
+
mask_img = mask_img.crop((0, 0, IMG_W, IMG_H))
|
| 221 |
+
else:
|
| 222 |
+
padm = Image.new("L", (IMG_W, IMG_H), 0)
|
| 223 |
+
padm.paste(mask_img, (0,0))
|
| 224 |
+
mask_img = padm
|
| 225 |
+
m = T.ToTensor()(mask_img) # [1,H,W], 值∈[0,1]
|
| 226 |
+
else:
|
| 227 |
+
m = torch.zeros(1, IMG_H, IMG_W)
|
| 228 |
+
|
| 229 |
+
# 2) 与 test.py 同分布:ToTensor->[0,1] 再 *2-1
|
| 230 |
+
x3 = T.ToTensor()(img) # [3,H,W], [0,1]
|
| 231 |
+
x3 = x3 * 2.0 - 1.0 # [-1,1] 等价于 Normalize(0.5,0.5)
|
| 232 |
+
|
| 233 |
+
x4 = torch.cat([x3, m], dim=0) # [4,H,W]
|
| 234 |
+
return x4
|
| 235 |
+
|
| 236 |
+
def tensor_to_pil(y: torch.Tensor) -> Image.Image:
|
| 237 |
+
if y.dim() == 4:
|
| 238 |
+
y = y[0]
|
| 239 |
+
y = y.detach().cpu()
|
| 240 |
+
if NORM_TO_MINUS1_1:
|
| 241 |
+
y = (y.clamp(-1,1) + 1.0)/2.0
|
| 242 |
+
else:
|
| 243 |
+
y = y.clamp(0,1)
|
| 244 |
+
y = (y*255.0).byte().numpy().transpose(1,2,0)
|
| 245 |
+
return Image.fromarray(y, mode="RGB")
|
| 246 |
+
|
| 247 |
+
# ---------------- 路由 ----------------
|
| 248 |
+
@app.route("/", methods=["GET"])
|
| 249 |
+
def home():
|
| 250 |
+
return render_template_string(INDEX_HTML, weight_name=os.path.basename(WEIGHTS_PATH))
|
| 251 |
+
|
| 252 |
+
@app.route("/api/predict", methods=["OPTIONS"])
|
| 253 |
+
def predict_preflight():
|
| 254 |
+
return ("", 204)
|
| 255 |
+
|
| 256 |
+
import time, uuid, os
|
| 257 |
+
@app.route("/api/predict", methods=["POST"])
|
| 258 |
+
@torch.no_grad()
|
| 259 |
+
def predict():
|
| 260 |
+
app.logger.info("== /api/predict called at %s ==", time.time())
|
| 261 |
+
if NETG is None:
|
| 262 |
+
return jsonify({"ok": False, "error": "model not ready"}), 500
|
| 263 |
+
f = request.files.get("image")
|
| 264 |
+
if not f:
|
| 265 |
+
return jsonify({"ok": False, "error": "no image"}), 400
|
| 266 |
+
fname = secure_filename(f.filename or f"{uuid.uuid4().hex}.png")
|
| 267 |
+
inpath = os.path.join(UPLOAD_DIR, fname)
|
| 268 |
+
f.save(inpath)
|
| 269 |
+
|
| 270 |
+
img = Image.open(inpath)
|
| 271 |
+
x = pil_to_tensor_4ch(img, mask_img=None).unsqueeze(0).to(DEVICE) # [1,4,H,W]
|
| 272 |
+
t0 = time.time()
|
| 273 |
+
y = NETG(x) # 期待 [1,3,H,W]
|
| 274 |
+
latency = int((time.time() - t0)*1000)
|
| 275 |
+
out_img = tensor_to_pil(y)
|
| 276 |
+
out_name = f"{uuid.uuid4().hex}.png"
|
| 277 |
+
out_path = os.path.join(OUTPUT_DIR, out_name)
|
| 278 |
+
out_img.save(out_path)
|
| 279 |
+
return jsonify({"ok": True, "latency_ms": latency, "output_url": f"/outputs/{out_name}"})
|
| 280 |
+
|
| 281 |
+
@app.route("/outputs/<path:name>")
|
| 282 |
+
def outputs(name):
|
| 283 |
+
return send_from_directory(OUTPUT_DIR, name)
|
| 284 |
+
|
| 285 |
+
# ---------------- 主入口 ----------------
|
| 286 |
+
if __name__ == "__main__":
|
| 287 |
+
print(f"Device: {DEVICE}")
|
| 288 |
+
init_model()
|
| 289 |
+
app.run(host="127.0.0.1", port=5000, debug=True)
|
| 290 |
+
|
| 291 |
+
@app.errorhandler(403)
|
| 292 |
+
def handle_403(e):
|
| 293 |
+
app.logger.warning("403 Forbidden: %s", repr(e))
|
| 294 |
+
return jsonify({"ok": False, "error": "forbidden", "detail": str(e)}), 403
|
flask_GAN/app_auto_mask.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app_auto_mask.py
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import os, sys, io, uuid, time
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from PIL import Image
|
| 7 |
+
|
| 8 |
+
from flask import Flask, request, jsonify, render_template_string, send_from_directory
|
| 9 |
+
from flask_cors import CORS
|
| 10 |
+
from werkzeug.utils import secure_filename
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn as nn
|
| 14 |
+
import torchvision.transforms as T
|
| 15 |
+
# ========== Paths ==========
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
# 以当前 app_auto_mask.py 所在目录为基准
|
| 19 |
+
THIS_DIR = Path(__file__).resolve().parent
|
| 20 |
+
PROJECT_ROOT = THIS_DIR.parent # 就是 Nose Generation 这个目录
|
| 21 |
+
|
| 22 |
+
# 1) pix2pix repo
|
| 23 |
+
PIX2PIX_ROOT = PROJECT_ROOT / "pix2pix_vgg"
|
| 24 |
+
sys.path.insert(0, str(PIX2PIX_ROOT))
|
| 25 |
+
from models.networks import define_G
|
| 26 |
+
|
| 27 |
+
# 2) face_parsing repo (确认文件夹名字是 face_parsing-main 还是 face_parsing_main)
|
| 28 |
+
FACE_PARSING_ROOT = PROJECT_ROOT / "face_parsing-main"
|
| 29 |
+
sys.path.insert(0, str(FACE_PARSING_ROOT))
|
| 30 |
+
from test_nose_mask_single import make_nose_mask_pil
|
| 31 |
+
from softmask import make_softmask_pil
|
| 32 |
+
|
| 33 |
+
FP_WEIGHTS = FACE_PARSING_ROOT / "fp_512.pth"
|
| 34 |
+
INFERENCE_PY = FACE_PARSING_ROOT / "inference.py"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ========== Runtime & constants ==========
|
| 38 |
+
WEIGHTS_PATH = "./weights/245_net_G.pth"
|
| 39 |
+
RUNTIME_DIR = Path("./runtime")
|
| 40 |
+
UPLOAD_DIR = RUNTIME_DIR / "uploads"
|
| 41 |
+
OUTPUT_DIR = RUNTIME_DIR / "outputs"
|
| 42 |
+
for d in (RUNTIME_DIR, UPLOAD_DIR, OUTPUT_DIR):
|
| 43 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 44 |
+
|
| 45 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 46 |
+
IMG_W, IMG_H = 512, 1536 # (width, height)
|
| 47 |
+
NORM_TO_MINUS1_1 = True
|
| 48 |
+
|
| 49 |
+
# ========== Flask ==========
|
| 50 |
+
app = Flask(__name__)
|
| 51 |
+
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
|
| 52 |
+
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024
|
| 53 |
+
|
| 54 |
+
@app.after_request
|
| 55 |
+
def add_cors_headers(resp):
|
| 56 |
+
resp.headers["Access-Control-Allow-Origin"] = "*"
|
| 57 |
+
resp.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"
|
| 58 |
+
resp.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization"
|
| 59 |
+
return resp
|
| 60 |
+
|
| 61 |
+
INDEX_HTML = r"""
|
| 62 |
+
<!doctype html>
|
| 63 |
+
<html lang="en">
|
| 64 |
+
<head>
|
| 65 |
+
<meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
|
| 66 |
+
<title>Pix2Pix (Auto Nose Mask, 512×1536) – Online Inference</title>
|
| 67 |
+
<style>
|
| 68 |
+
:root{--card:#e5e7eb}
|
| 69 |
+
body{font-family:system-ui, -apple-system, Segoe UI, Roboto;max-width:1100px;margin:24px auto;padding:0 12px}
|
| 70 |
+
h2{margin:8px 0 16px}
|
| 71 |
+
.card{border:1px solid var(--card);border-radius:12px;padding:16px;margin:12px 0}
|
| 72 |
+
.btn{padding:10px 16px;border-radius:8px;border:1px solid var(--card);background:#10b981;color:#fff;cursor:pointer}
|
| 73 |
+
.mono{font-family:ui-monospace, Menlo, Consolas, monospace}
|
| 74 |
+
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
|
| 75 |
+
.pane h3{margin:0 0 8px}
|
| 76 |
+
img.result-img{max-width:min(512px, 44vw);max-height:80vh;width:auto;height:auto;display:block;border:1px solid var(--card);border-radius:8px;margin-top:8px;background:#000}
|
| 77 |
+
.muted{color:#6b7280}
|
| 78 |
+
.inline{display:inline-block;margin-left:8px}
|
| 79 |
+
</style>
|
| 80 |
+
</head>
|
| 81 |
+
<body>
|
| 82 |
+
<h2>Pix2Pix (Auto Nose Mask, 512×1536) – Online Inference</h2>
|
| 83 |
+
|
| 84 |
+
<div class="card">
|
| 85 |
+
<p>Current weights: <code>{{ weight_name }}</code></p>
|
| 86 |
+
<input id="img" type="file" accept="image/*"/>
|
| 87 |
+
<button id="run" class="btn">Run</button>
|
| 88 |
+
<span id="s" class="mono inline"></span>
|
| 89 |
+
<div class="muted" style="margin-top:6px">
|
| 90 |
+
The uploaded image is <b>fit inside 512×1536</b> (keep aspect ratio).
|
| 91 |
+
We downscale only if needed; remaining area is padded with black. No second resize later.
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="card">
|
| 96 |
+
<div class="grid">
|
| 97 |
+
<div class="pane">
|
| 98 |
+
<h3>Input (aligned to 512×1536)</h3>
|
| 99 |
+
<img id="imgIn" class="result-img" alt="Aligned input will appear here"/>
|
| 100 |
+
</div>
|
| 101 |
+
<div class="pane">
|
| 102 |
+
<h3>Output</h3>
|
| 103 |
+
<img id="imgOut" class="result-img" alt="Model output will appear here"/>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<script>
|
| 109 |
+
async function postForm(url, fd){
|
| 110 |
+
const r = await fetch(url, {method:"POST", body:fd});
|
| 111 |
+
if(!r.ok) throw new Error("HTTP "+r.status);
|
| 112 |
+
return await r.json();
|
| 113 |
+
}
|
| 114 |
+
const elStatus = document.getElementById("s");
|
| 115 |
+
const elIn = document.getElementById("imgIn");
|
| 116 |
+
const elOut = document.getElementById("imgOut");
|
| 117 |
+
|
| 118 |
+
document.getElementById("run").onclick = async ()=>{
|
| 119 |
+
const f = document.getElementById("img").files[0];
|
| 120 |
+
if(!f){ elStatus.textContent = "Please choose an image."; return; }
|
| 121 |
+
elStatus.textContent = "Running...";
|
| 122 |
+
const fd = new FormData(); fd.append("image", f);
|
| 123 |
+
try{
|
| 124 |
+
const j = await postForm("/api/predict", fd);
|
| 125 |
+
elStatus.textContent = (j.used_mask ? "Auto mask ✓" : "No mask") + " | " + j.latency_ms + " ms";
|
| 126 |
+
if (j.input_url) elIn.src = j.input_url + "?t=" + Date.now();
|
| 127 |
+
if (j.output_url) elOut.src = j.output_url + "?t=" + Date.now();
|
| 128 |
+
}catch(e){
|
| 129 |
+
elStatus.textContent = "Failed: " + e.message;
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
</script>
|
| 133 |
+
</body></html>
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
# ========== Model loading ==========
|
| 137 |
+
NETG: Optional[nn.Module] = None
|
| 138 |
+
|
| 139 |
+
def build_netG() -> nn.Module:
|
| 140 |
+
return define_G(
|
| 141 |
+
input_nc=4, output_nc=3, ngf=64,
|
| 142 |
+
netG='unet_512', norm='instance', use_dropout=False
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def load_weights_into(net: nn.Module, weight_path: str):
|
| 146 |
+
state = torch.load(weight_path, map_location="cpu")
|
| 147 |
+
if isinstance(state, dict) and "state_dict" in state:
|
| 148 |
+
state = state["state_dict"]
|
| 149 |
+
state = {k.replace("module.", ""): v for k, v in state.items()}
|
| 150 |
+
missing, unexpected = net.load_state_dict(state, strict=False)
|
| 151 |
+
print(f"[load_state_dict] missing: {missing}, unexpected: {unexpected}")
|
| 152 |
+
|
| 153 |
+
def init_model():
|
| 154 |
+
global NETG
|
| 155 |
+
net = build_netG()
|
| 156 |
+
load_weights_into(net, WEIGHTS_PATH)
|
| 157 |
+
net.eval().to(DEVICE)
|
| 158 |
+
NETG = net
|
| 159 |
+
print(f"[OK] netG loaded on {DEVICE} from {WEIGHTS_PATH}")
|
| 160 |
+
|
| 161 |
+
# ========== Pre/Post ==========
|
| 162 |
+
def fit_inside_and_pad(img: Image.Image, W: int, H: int, pad_color=(40,40,40)) -> Image.Image:
|
| 163 |
+
"""
|
| 164 |
+
Keep aspect ratio:
|
| 165 |
+
- If either side > target -> scale down by min(W/w, H/h).
|
| 166 |
+
- If both sides <= target -> keep size (no upscaling).
|
| 167 |
+
Then pad to (W,H) with pad_color. Paste at (0,0).
|
| 168 |
+
Only one interpolation (BICUBIC) happens here.
|
| 169 |
+
"""
|
| 170 |
+
img = img.convert("RGB")
|
| 171 |
+
w, h = img.size
|
| 172 |
+
if w > W or h > H:
|
| 173 |
+
s = min(W / w, H / h)
|
| 174 |
+
new_w = max(1, int(round(w * s)))
|
| 175 |
+
new_h = max(1, int(round(h * s)))
|
| 176 |
+
img = img.resize((new_w, new_h), Image.BICUBIC)
|
| 177 |
+
canvas = Image.new("RGB", (W, H), pad_color)
|
| 178 |
+
canvas.paste(img, (0, 0))
|
| 179 |
+
return canvas
|
| 180 |
+
|
| 181 |
+
def align_img(img: Image.Image) -> Image.Image:
|
| 182 |
+
return fit_inside_and_pad(img, IMG_W, IMG_H, pad_color=(40,40,40))
|
| 183 |
+
|
| 184 |
+
def ensure_mask_same_size(mask: Image.Image) -> Image.Image:
|
| 185 |
+
"""
|
| 186 |
+
Expect mask already 512×1536. If not, do a single NEAREST correction.
|
| 187 |
+
Then binarize to {0,255}.
|
| 188 |
+
"""
|
| 189 |
+
mask = mask.convert("L")
|
| 190 |
+
if mask.size != (IMG_W, IMG_H):
|
| 191 |
+
# Single correction only if mismatch (rare)
|
| 192 |
+
w, h = mask.size
|
| 193 |
+
s = min(IMG_W / w, IMG_H / h)
|
| 194 |
+
new_w = max(1, int(round(w * s)))
|
| 195 |
+
new_h = max(1, int(round(h * s)))
|
| 196 |
+
mask = mask.resize((new_w, new_h), Image.NEAREST)
|
| 197 |
+
canvas = Image.new("L", (IMG_W, IMG_H), 0)
|
| 198 |
+
canvas.paste(mask, (0, 0))
|
| 199 |
+
mask = canvas
|
| 200 |
+
# binarize
|
| 201 |
+
mask = mask.point(lambda p: 255 if p > 127 else 0, mode="L")
|
| 202 |
+
return mask
|
| 203 |
+
|
| 204 |
+
def pil_to_tensor_4ch(img_aligned: Image.Image, mask_img: Optional[Image.Image]) -> torch.Tensor:
|
| 205 |
+
"""
|
| 206 |
+
img_aligned must already be 512×1536.
|
| 207 |
+
mask_img should be same size; we only binarize -> [0,1], no resize.
|
| 208 |
+
"""
|
| 209 |
+
assert img_aligned.size == (IMG_W, IMG_H)
|
| 210 |
+
x3 = T.ToTensor()(img_aligned) # [0,1]
|
| 211 |
+
if NORM_TO_MINUS1_1: x3 = x3 * 2 - 1 # [-1,1]
|
| 212 |
+
|
| 213 |
+
if mask_img is not None:
|
| 214 |
+
mask_fixed = ensure_mask_same_size(mask_img)
|
| 215 |
+
m = T.ToTensor()(mask_fixed) # [1,H,W], in [0,1]
|
| 216 |
+
# Explicitly clamp to {0,1} to avoid gray
|
| 217 |
+
m = (m > 0.5).float()
|
| 218 |
+
x4 = torch.cat([x3, m], dim=0)
|
| 219 |
+
else:
|
| 220 |
+
x4 = torch.cat([x3, torch.zeros(1, IMG_H, IMG_W)], dim=0)
|
| 221 |
+
return x4
|
| 222 |
+
|
| 223 |
+
def tensor_to_pil(y: torch.Tensor) -> Image.Image:
|
| 224 |
+
if y.dim() == 4: y = y[0]
|
| 225 |
+
y = y.detach().cpu().clamp(-1,1)
|
| 226 |
+
y = (y + 1)/2
|
| 227 |
+
y = (y*255).byte().numpy().transpose(1,2,0)
|
| 228 |
+
return Image.fromarray(y, "RGB")
|
| 229 |
+
|
| 230 |
+
# ========== Routes ==========
|
| 231 |
+
@app.route("/", methods=["GET"])
|
| 232 |
+
def home():
|
| 233 |
+
return render_template_string(INDEX_HTML, weight_name=os.path.basename(WEIGHTS_PATH))
|
| 234 |
+
|
| 235 |
+
@app.route("/outputs/<path:name>")
|
| 236 |
+
def outputs(name):
|
| 237 |
+
return send_from_directory(str(OUTPUT_DIR), name)
|
| 238 |
+
|
| 239 |
+
@app.route("/api/predict", methods=["OPTIONS"])
|
| 240 |
+
def predict_preflight():
|
| 241 |
+
return ("", 204)
|
| 242 |
+
|
| 243 |
+
@app.route("/api/predict", methods=["POST"])
|
| 244 |
+
@torch.no_grad()
|
| 245 |
+
def predict():
|
| 246 |
+
if NETG is None:
|
| 247 |
+
return jsonify({"ok": False, "error": "model not ready"}), 500
|
| 248 |
+
|
| 249 |
+
f = request.files.get("image")
|
| 250 |
+
if not f:
|
| 251 |
+
return jsonify({"ok": False, "error": "no image"}), 400
|
| 252 |
+
|
| 253 |
+
# 1) read & align ONCE
|
| 254 |
+
raw_name = f.filename or f"{uuid.uuid4().hex}.png"
|
| 255 |
+
_ = secure_filename(raw_name)
|
| 256 |
+
img_raw = Image.open(io.BytesIO(f.read())).convert("RGB")
|
| 257 |
+
img_aligned = align_img(img_raw)
|
| 258 |
+
|
| 259 |
+
# save aligned input as PNG (avoid JPEG blur)
|
| 260 |
+
in_name = f"in_{uuid.uuid4().hex}.png"
|
| 261 |
+
in_path = OUTPUT_DIR / in_name
|
| 262 |
+
img_aligned.save(in_path, format="PNG")
|
| 263 |
+
|
| 264 |
+
# 2) generate mask on the aligned image
|
| 265 |
+
try:
|
| 266 |
+
mask_img = make_softmask_pil(
|
| 267 |
+
img=img_aligned, # EXACTLY 512×1536
|
| 268 |
+
weights=FP_WEIGHTS,
|
| 269 |
+
inference_fp=INFERENCE_PY,
|
| 270 |
+
device="cpu", # change to "cuda:0" if available
|
| 271 |
+
y_top=400,
|
| 272 |
+
sigma=4.0,
|
| 273 |
+
nose_id=10,
|
| 274 |
+
)
|
| 275 |
+
used_mask = True
|
| 276 |
+
except Exception as e:
|
| 277 |
+
print("[mask] generation failed:", e)
|
| 278 |
+
mask_img = None
|
| 279 |
+
used_mask = False
|
| 280 |
+
|
| 281 |
+
# 3) inference (no more resizing)
|
| 282 |
+
x = pil_to_tensor_4ch(img_aligned, mask_img=mask_img).unsqueeze(0).to(DEVICE)
|
| 283 |
+
t0 = time.time()
|
| 284 |
+
y = NETG(x)
|
| 285 |
+
latency = int((time.time() - t0) * 1000)
|
| 286 |
+
|
| 287 |
+
out_img = tensor_to_pil(y)
|
| 288 |
+
out_name = f"out_{uuid.uuid4().hex}.png"
|
| 289 |
+
out_img.save(OUTPUT_DIR / out_name, format="PNG")
|
| 290 |
+
|
| 291 |
+
return jsonify({
|
| 292 |
+
"ok": True,
|
| 293 |
+
"used_mask": used_mask,
|
| 294 |
+
"latency_ms": latency,
|
| 295 |
+
"input_url": f"/outputs/{in_name}",
|
| 296 |
+
"output_url": f"/outputs/{out_name}",
|
| 297 |
+
})
|
| 298 |
+
|
| 299 |
+
# ========== Entry ==========
|
| 300 |
+
if __name__ == "__main__":
|
| 301 |
+
print("Device:", DEVICE)
|
| 302 |
+
for p in [PIX2PIX_ROOT, FACE_PARSING_ROOT, FP_WEIGHTS, INFERENCE_PY]:
|
| 303 |
+
print("[check]", p, "=>", p.exists())
|
| 304 |
+
init_model()
|
| 305 |
+
port = int(os.environ.get("PORT", 7860))
|
| 306 |
+
app.run(host="0.0.0.0", port=port, debug=False)
|
flask_GAN/app_mask.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app_mask.py
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import os, sys, io, uuid, time, re, unicodedata, traceback
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
from flask import Flask, request, jsonify, render_template_string, send_from_directory
|
| 8 |
+
from flask_cors import CORS
|
| 9 |
+
from werkzeug.utils import secure_filename
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
import torchvision.transforms as T
|
| 14 |
+
|
| 15 |
+
# ========= 1) 指定你的 pix2pix 仓库路径(按实际修改) =========
|
| 16 |
+
home = os.path.expanduser("~")
|
| 17 |
+
PIX2PIX_ROOT = os.path.join(home, "Downloads", "python", "pix2pix_local") # ← 改成你的路径
|
| 18 |
+
sys.path.insert(0, PIX2PIX_ROOT)
|
| 19 |
+
from models.networks import define_G # 来自你的训练仓库
|
| 20 |
+
|
| 21 |
+
# ========= 2) 目录/常量配置 =========
|
| 22 |
+
WEIGHTS_PATH = "./weights/150_net_G.pth"
|
| 23 |
+
UPLOAD_DIR = "./runtime/uploads"
|
| 24 |
+
OUTPUT_DIR = "./runtime/outputs"
|
| 25 |
+
MASK_DIR = "./mask" # 掩码目录(同名匹配)
|
| 26 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 27 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 28 |
+
|
| 29 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 30 |
+
IMG_H, IMG_W = 1536, 512 # 高、宽(注意顺序)
|
| 31 |
+
NORM_TO_MINUS1_1 = True
|
| 32 |
+
|
| 33 |
+
# ========= 3) Flask 基础 =========
|
| 34 |
+
app = Flask(__name__)
|
| 35 |
+
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
|
| 36 |
+
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024 # 50MB
|
| 37 |
+
|
| 38 |
+
@app.after_request
|
| 39 |
+
def add_cors_headers(resp):
|
| 40 |
+
resp.headers["Access-Control-Allow-Origin"] = "*"
|
| 41 |
+
resp.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"
|
| 42 |
+
resp.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization"
|
| 43 |
+
return resp
|
| 44 |
+
|
| 45 |
+
INDEX_HTML = r"""
|
| 46 |
+
<!doctype html>
|
| 47 |
+
<html lang="zh-CN">
|
| 48 |
+
<head>
|
| 49 |
+
<meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
|
| 50 |
+
<title>Pix2Pix(RGB + 同名掩码,512×1536)</title>
|
| 51 |
+
<style>
|
| 52 |
+
body{font-family:system-ui;max-width:980px;margin:24px auto}
|
| 53 |
+
.card{border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin:12px 0}
|
| 54 |
+
.btn{padding:10px 16px;border-radius:8px;border:1px solid #e5e7eb;background:#10b981;color:#fff;cursor:pointer}
|
| 55 |
+
.mono{font-family:ui-monospace,Menlo,Consolas,monospace}
|
| 56 |
+
#result{max-width:540px}
|
| 57 |
+
img.result-img{max-width:min(512px,90vw);max-height:80vh;width:auto;height:auto;display:block;border:1px solid #e5e7eb;border-radius:8px;margin-top:10px}
|
| 58 |
+
</style>
|
| 59 |
+
</head>
|
| 60 |
+
<body>
|
| 61 |
+
<h2>Pix2Pix(RGB + mask, 512×1536)Online Testing</h2>
|
| 62 |
+
<div class="card">
|
| 63 |
+
<p>Current weight:<code>{{ weight_name }}</code></p>
|
| 64 |
+
<p>Upload only pictures. The server will use<code>./mask/</code> contents to match mask(png/jpg/jpeg/bmp/webp/tif)。If found, it is used as the 4th channel; if not found, no mask is used.</p>
|
| 65 |
+
<input id="img" type="file" accept="image/*"/>
|
| 66 |
+
<button id="run" class="btn">Generate</button>
|
| 67 |
+
<span id="s" class="mono"></span>
|
| 68 |
+
</div>
|
| 69 |
+
<div class="card"><h3>output</h3><div id="result"></div></div>
|
| 70 |
+
<script>
|
| 71 |
+
async function postForm(url, fd){
|
| 72 |
+
const r = await fetch(url, {method:"POST", body:fd});
|
| 73 |
+
if(!r.ok) throw new Error("HTTP "+r.status);
|
| 74 |
+
return await r.json();
|
| 75 |
+
}
|
| 76 |
+
const result = document.getElementById("result");
|
| 77 |
+
document.getElementById("run").onclick = async ()=>{
|
| 78 |
+
const s = document.getElementById("s");
|
| 79 |
+
const f = document.getElementById("img").files[0];
|
| 80 |
+
if(!f){ s.textContent = "please upload image"; return; }
|
| 81 |
+
s.textContent = "推理中...";
|
| 82 |
+
const fd = new FormData(); fd.append("image", f);
|
| 83 |
+
try{
|
| 84 |
+
const j = await postForm("/api/predict", fd);
|
| 85 |
+
s.textContent = (j.used_mask ? "use mask ✓" : "未使用掩码") + " |cost " + j.latency_ms + " ms";
|
| 86 |
+
const img = document.createElement("img");
|
| 87 |
+
img.className = "result-img";
|
| 88 |
+
img.src = j.output_url + "?t=" + Date.now();
|
| 89 |
+
result.innerHTML = ""; result.appendChild(img);
|
| 90 |
+
}catch(e){
|
| 91 |
+
s.textContent = "失败:" + e.message;
|
| 92 |
+
}
|
| 93 |
+
};
|
| 94 |
+
</script>
|
| 95 |
+
</body></html>
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
# ========= 4) 模型加载 =========
|
| 99 |
+
NETG: Optional[nn.Module] = None
|
| 100 |
+
|
| 101 |
+
def build_netG() -> nn.Module:
|
| 102 |
+
# 与训练一致:unet_512 + instance + no_dropout,输入4/输出3
|
| 103 |
+
netG = define_G(
|
| 104 |
+
input_nc=4,
|
| 105 |
+
output_nc=3,
|
| 106 |
+
ngf=64,
|
| 107 |
+
netG='unet_512',
|
| 108 |
+
norm='instance',
|
| 109 |
+
use_dropout=False
|
| 110 |
+
)
|
| 111 |
+
return netG
|
| 112 |
+
|
| 113 |
+
def load_weights_into(net: nn.Module, weight_path: str):
|
| 114 |
+
state = torch.load(weight_path, map_location="cpu")
|
| 115 |
+
if isinstance(state, dict) and "state_dict" in state:
|
| 116 |
+
state = state["state_dict"]
|
| 117 |
+
new_state = {k.replace("module.", ""): v for k, v in state.items()}
|
| 118 |
+
missing, unexpected = net.load_state_dict(new_state, strict=False)
|
| 119 |
+
print(f"[load_state_dict] missing: {missing}, unexpected: {unexpected}")
|
| 120 |
+
|
| 121 |
+
def init_model():
|
| 122 |
+
global NETG
|
| 123 |
+
netG = build_netG()
|
| 124 |
+
load_weights_into(netG, WEIGHTS_PATH)
|
| 125 |
+
netG.eval().to(DEVICE)
|
| 126 |
+
NETG = netG
|
| 127 |
+
print(f"[OK] netG loaded on {DEVICE} from {WEIGHTS_PATH}")
|
| 128 |
+
|
| 129 |
+
# ========= 5) 预处理 =========
|
| 130 |
+
def align_img(img: Image.Image) -> Image.Image:
|
| 131 |
+
"""高度对齐到 1536;��宽>=512取左侧512,否则右侧补黑到512。"""
|
| 132 |
+
img = img.convert("RGB")
|
| 133 |
+
if img.size == (IMG_W, IMG_H):
|
| 134 |
+
return img
|
| 135 |
+
new_w = round(img.width * IMG_H / img.height)
|
| 136 |
+
img = img.resize((new_w, IMG_H), Image.BICUBIC)
|
| 137 |
+
if new_w >= IMG_W:
|
| 138 |
+
return img.crop((0, 0, IMG_W, IMG_H))
|
| 139 |
+
pad = Image.new("RGB", (IMG_W, IMG_H), (0,0,0))
|
| 140 |
+
pad.paste(img, (0,0))
|
| 141 |
+
return pad
|
| 142 |
+
|
| 143 |
+
def align_mask(mask: Image.Image) -> Image.Image:
|
| 144 |
+
"""与图片同策略但用最近邻,避免灰边。"""
|
| 145 |
+
mask = mask.convert("L")
|
| 146 |
+
if mask.size == (IMG_W, IMG_H):
|
| 147 |
+
return mask
|
| 148 |
+
new_w = round(mask.width * IMG_H / mask.height)
|
| 149 |
+
mask = mask.resize((new_w, IMG_H), Image.NEAREST)
|
| 150 |
+
if new_w >= IMG_W:
|
| 151 |
+
return mask.crop((0, 0, IMG_W, IMG_H))
|
| 152 |
+
pad = Image.new("L", (IMG_W, IMG_H), 0)
|
| 153 |
+
pad.paste(mask, (0,0))
|
| 154 |
+
return pad
|
| 155 |
+
|
| 156 |
+
def pil_to_tensor_4ch(img: Image.Image, mask_img: Optional[Image.Image]) -> torch.Tensor:
|
| 157 |
+
img = align_img(img)
|
| 158 |
+
x3 = T.ToTensor()(img) # [0,1]
|
| 159 |
+
if NORM_TO_MINUS1_1: x3 = x3 * 2 - 1 # [-1,1]
|
| 160 |
+
if mask_img is not None:
|
| 161 |
+
m = T.ToTensor()(align_mask(mask_img)) # [1,H,W] ∈ [0,1]
|
| 162 |
+
x4 = torch.cat([x3, m], dim=0)
|
| 163 |
+
else:
|
| 164 |
+
# 不使用掩码:仍然保持 4 通道(最后一通道为 0)
|
| 165 |
+
x4 = torch.cat([x3, torch.zeros(1, IMG_H, IMG_W)], dim=0)
|
| 166 |
+
return x4
|
| 167 |
+
|
| 168 |
+
def tensor_to_pil(y: torch.Tensor) -> Image.Image:
|
| 169 |
+
if y.dim() == 4: y = y[0]
|
| 170 |
+
y = y.detach().cpu().clamp(-1,1)
|
| 171 |
+
y = (y + 1)/2
|
| 172 |
+
y = (y*255).byte().numpy().transpose(1,2,0)
|
| 173 |
+
return Image.fromarray(y, "RGB")
|
| 174 |
+
|
| 175 |
+
# ========= 6) 掩码匹配(鲁棒) =========
|
| 176 |
+
MASK_EXTS = [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tif", ".tiff"]
|
| 177 |
+
|
| 178 |
+
def _norm_stem(s: str) -> str:
|
| 179 |
+
s = unicodedata.normalize("NFKD", s).lower()
|
| 180 |
+
return re.sub(r"[^a-z0-9]+", "", s) # 仅保留字母数字
|
| 181 |
+
|
| 182 |
+
def find_mask_by_stems(candidates: list[str]) -> Optional[str]:
|
| 183 |
+
norm_targets = {_norm_stem(st) for st in candidates if st}
|
| 184 |
+
print("[MASK] target stems:", candidates, "=>", list(norm_targets))
|
| 185 |
+
try:
|
| 186 |
+
for name in os.listdir(MASK_DIR):
|
| 187 |
+
p = os.path.join(MASK_DIR, name)
|
| 188 |
+
if not os.path.isfile(p):
|
| 189 |
+
continue
|
| 190 |
+
stem, ext = os.path.splitext(name)
|
| 191 |
+
if ext.lower() not in MASK_EXTS:
|
| 192 |
+
continue
|
| 193 |
+
if _norm_stem(stem) in norm_targets:
|
| 194 |
+
print(f"[MASK] matched: {name}")
|
| 195 |
+
return p
|
| 196 |
+
except FileNotFoundError:
|
| 197 |
+
print("[MASK] MASK_DIR not found:", MASK_DIR)
|
| 198 |
+
print("[MASK] no match in:", MASK_DIR)
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
# ========= 7) 路由 =========
|
| 202 |
+
@app.route("/", methods=["GET"])
|
| 203 |
+
def home():
|
| 204 |
+
return render_template_string(INDEX_HTML, weight_name=os.path.basename(WEIGHTS_PATH))
|
| 205 |
+
|
| 206 |
+
@app.route("/outputs/<path:name>")
|
| 207 |
+
def outputs(name):
|
| 208 |
+
return send_from_directory(OUTPUT_DIR, name)
|
| 209 |
+
|
| 210 |
+
@app.route("/api/predict", methods=["OPTIONS"])
|
| 211 |
+
def predict_preflight():
|
| 212 |
+
return ("", 204)
|
| 213 |
+
|
| 214 |
+
@app.route("/api/predict", methods=["POST"])
|
| 215 |
+
@torch.no_grad()
|
| 216 |
+
def predict():
|
| 217 |
+
if NETG is None:
|
| 218 |
+
return jsonify({"ok": False, "error": "model not ready"}), 500
|
| 219 |
+
|
| 220 |
+
f = request.files.get("image")
|
| 221 |
+
if not f:
|
| 222 |
+
return jsonify({"ok": False, "error": "no image"}), 400
|
| 223 |
+
|
| 224 |
+
raw_name = f.filename or f"{uuid.uuid4().hex}.png" # 原始文件名
|
| 225 |
+
safe_name = secure_filename(raw_name) # 安全化后
|
| 226 |
+
stem_raw, _ = os.path.splitext(raw_name)
|
| 227 |
+
stem_safe, _ = os.path.splitext(safe_name)
|
| 228 |
+
|
| 229 |
+
# 读图
|
| 230 |
+
img_bytes = f.read()
|
| 231 |
+
img = Image.open(io.BytesIO(img_bytes))
|
| 232 |
+
|
| 233 |
+
# 匹配掩码
|
| 234 |
+
mask_path = find_mask_by_stems([stem_raw, stem_safe])
|
| 235 |
+
mask_img = Image.open(mask_path) if mask_path else None
|
| 236 |
+
used_mask = bool(mask_img)
|
| 237 |
+
|
| 238 |
+
# 预处理 & 推理
|
| 239 |
+
x = pil_to_tensor_4ch(img, mask_img=mask_img).unsqueeze(0).to(DEVICE)
|
| 240 |
+
t0 = time.time()
|
| 241 |
+
y = NETG(x)
|
| 242 |
+
latency = int((time.time() - t0) * 1000)
|
| 243 |
+
|
| 244 |
+
# 保存输出
|
| 245 |
+
out_img = tensor_to_pil(y)
|
| 246 |
+
out_name = f"{uuid.uuid4().hex}.png"
|
| 247 |
+
out_path = os.path.join(OUTPUT_DIR, out_name)
|
| 248 |
+
out_img.save(out_path)
|
| 249 |
+
|
| 250 |
+
return jsonify({"ok": True, "used_mask": used_mask, "latency_ms": latency,
|
| 251 |
+
"output_url": f"/outputs/{out_name}"})
|
| 252 |
+
|
| 253 |
+
# ========= 8) 入口 =========
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
print("Device:", DEVICE)
|
| 256 |
+
init_model()
|
| 257 |
+
# 为避免与你原来的 app 冲突,这里用 5001;需要可改回 5000
|
| 258 |
+
app.run(host="127.0.0.1", port=5001, debug=True)
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.06.34 AM.png
ADDED
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.33 AM (1).png
ADDED
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.34 AM (2).png
ADDED
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.35 AM (5).png
ADDED
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.36 AM (1).png
ADDED
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.36 AM (3).png
ADDED
|
flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.30.12 AM (2).png
ADDED
|
flask_GAN/resize.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# crop_left_512.py
|
| 2 |
+
import os
|
| 3 |
+
from PIL import Image
|
| 4 |
+
|
| 5 |
+
INPUT_DIR = "./picture" # 原图文件夹
|
| 6 |
+
OUTPUT_DIR = "./picture_resized" # 输出文件夹
|
| 7 |
+
TARGET_H = 1536 # 目标高度(像素)
|
| 8 |
+
CROP_W = 512 # 只保留左侧 [0, 512) 宽度
|
| 9 |
+
|
| 10 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 11 |
+
|
| 12 |
+
def is_img(name):
|
| 13 |
+
return name.lower().endswith((".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"))
|
| 14 |
+
|
| 15 |
+
for fname in os.listdir(INPUT_DIR):
|
| 16 |
+
if not is_img(fname):
|
| 17 |
+
continue
|
| 18 |
+
src = os.path.join(INPUT_DIR, fname)
|
| 19 |
+
dst = os.path.join(OUTPUT_DIR, fname)
|
| 20 |
+
try:
|
| 21 |
+
with Image.open(src) as im:
|
| 22 |
+
im = im.convert("RGB")
|
| 23 |
+
|
| 24 |
+
# 1) 若高度不是 1536,先按比例把高度调整到 1536(宽度等比变化)
|
| 25 |
+
if im.height != TARGET_H:
|
| 26 |
+
new_w = round(im.width * TARGET_H / im.height)
|
| 27 |
+
im = im.resize((new_w, TARGET_H), Image.BICUBIC)
|
| 28 |
+
|
| 29 |
+
# 2) 若宽度不足 512,跳过并提示;否则裁剪左侧 0~512 宽度
|
| 30 |
+
if im.width < CROP_W:
|
| 31 |
+
print(f"[SKIP] {fname}: width {im.width} < {CROP_W}")
|
| 32 |
+
continue
|
| 33 |
+
|
| 34 |
+
left_crop = im.crop((0, 0, CROP_W, TARGET_H)) # (left, top, right, bottom)
|
| 35 |
+
left_crop.save(dst)
|
| 36 |
+
print(f"[OK] {fname} -> {dst}")
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"[ERR] {fname}: {e}")
|
| 39 |
+
|
| 40 |
+
print("✅ 完成:已将左侧 0-512 宽度裁剪并输出到", OUTPUT_DIR)
|
flask_GAN/result/vis/tmp.png
ADDED
|
Git LFS Details
|
flask_GAN/runtime/outputs/in_3f084cf9775a45d9b21c686ae18ecdf0.png
ADDED
|
Git LFS Details
|
flask_GAN/runtime/outputs/in_986cd78ee11a4f1fa99385e0efac4563.png
ADDED
|
Git LFS Details
|
flask_GAN/runtime/outputs/in_c04f130d833c47559f3c1e76c95f8a55.png
ADDED
|
Git LFS Details
|
flask_GAN/runtime/outputs/in_c6f89cfcf3f047999eb74eb4804f3be9.png
ADDED
|
Git LFS Details
|
flask_GAN/runtime/outputs/in_f77c29d6f9174b54b5678d30d4834fbf.png
ADDED
|
Git LFS Details
|