PaulZjy commited on
Commit
fa213f2
·
verified ·
1 Parent(s): 67221c3

Upload 194 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +14 -0
  2. Dockerfile +16 -0
  3. face_parsing-main/.Rhistory +0 -0
  4. face_parsing-main/.gitignore +162 -0
  5. face_parsing-main/1.docx +3 -0
  6. face_parsing-main/LICENSE +21 -0
  7. face_parsing-main/README.md +47 -0
  8. face_parsing-main/__pycache__/model.cpython-39.pyc +0 -0
  9. face_parsing-main/__pycache__/resnet.cpython-39.pyc +0 -0
  10. face_parsing-main/__pycache__/softmask.cpython-39.pyc +0 -0
  11. face_parsing-main/__pycache__/test_nose_mask_single.cpython-39.pyc +0 -0
  12. face_parsing-main/crop_b_512_keepname.py +58 -0
  13. face_parsing-main/expand_mask.py +52 -0
  14. face_parsing-main/expand_masks_512_to_1536.py +37 -0
  15. face_parsing-main/face_dataset.py +60 -0
  16. face_parsing-main/fp_512.pth +3 -0
  17. face_parsing-main/images/2020-09-06_17-01-01_835030.jpg +0 -0
  18. face_parsing-main/images/2020-09-06_17-09-01_763315.jpg +0 -0
  19. face_parsing-main/images/2020-09-06_20-31-54_377040.jpg +0 -0
  20. face_parsing-main/inference.py +120 -0
  21. face_parsing-main/loss.py +75 -0
  22. face_parsing-main/make_nose_masks.py +79 -0
  23. face_parsing-main/model.py +283 -0
  24. face_parsing-main/prepropess_data.py +47 -0
  25. face_parsing-main/python test.docx +0 -0
  26. face_parsing-main/resnet.py +109 -0
  27. face_parsing-main/samples/sample.gif +3 -0
  28. face_parsing-main/samples/t.jpg +0 -0
  29. face_parsing-main/softmask.py +145 -0
  30. face_parsing-main/test_nose_mask.py +227 -0
  31. face_parsing-main/test_nose_mask_single.py +145 -0
  32. face_parsing-main/train.py +157 -0
  33. face_parsing-main/transform.py +128 -0
  34. flask_GAN/app.py +294 -0
  35. flask_GAN/app_auto_mask.py +306 -0
  36. flask_GAN/app_mask.py +258 -0
  37. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.06.34 AM.png +0 -0
  38. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.33 AM (1).png +0 -0
  39. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.34 AM (2).png +0 -0
  40. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.35 AM (5).png +0 -0
  41. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.36 AM (1).png +0 -0
  42. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.09.36 AM (3).png +0 -0
  43. flask_GAN/mask/WhatsApp Image 2025-07-12 at 1.30.12 AM (2).png +0 -0
  44. flask_GAN/resize.py +40 -0
  45. flask_GAN/result/vis/tmp.png +3 -0
  46. flask_GAN/runtime/outputs/in_3f084cf9775a45d9b21c686ae18ecdf0.png +3 -0
  47. flask_GAN/runtime/outputs/in_986cd78ee11a4f1fa99385e0efac4563.png +3 -0
  48. flask_GAN/runtime/outputs/in_c04f130d833c47559f3c1e76c95f8a55.png +3 -0
  49. flask_GAN/runtime/outputs/in_c6f89cfcf3f047999eb74eb4804f3be9.png +3 -0
  50. 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
+ ![image](samples/t.jpg)
9
+
10
+ * 视频示例:
11
+ ![video](samples/sample.gif)
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

  • SHA256: fc06ce18fb791b94e868a4230b0953499a21a20f88703aa9b51ac3ef7a953e3e
  • Pointer size: 133 Bytes
  • Size of remote file: 17.9 MB
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

  • SHA256: f76636e096e8e2020c95a86a9ebcb48fe9ca886f3a47a94ac743cd4ab89b4411
  • Pointer size: 131 Bytes
  • Size of remote file: 253 kB
flask_GAN/runtime/outputs/in_3f084cf9775a45d9b21c686ae18ecdf0.png ADDED

Git LFS Details

  • SHA256: d3f5debfee68fc9b2301a1e763553f089e9f33c4833cc82a60c5dac09bfbb141
  • Pointer size: 131 Bytes
  • Size of remote file: 375 kB
flask_GAN/runtime/outputs/in_986cd78ee11a4f1fa99385e0efac4563.png ADDED

Git LFS Details

  • SHA256: 2dfefdb1758b9f08fada6992809d501f63854a61cae4acc8c279d9ad5b660d10
  • Pointer size: 131 Bytes
  • Size of remote file: 572 kB
flask_GAN/runtime/outputs/in_c04f130d833c47559f3c1e76c95f8a55.png ADDED

Git LFS Details

  • SHA256: d3799ad4f5086c21091201cd1a02979f1fb03ef18ff52f9aed4174cc2a08c2ea
  • Pointer size: 131 Bytes
  • Size of remote file: 610 kB
flask_GAN/runtime/outputs/in_c6f89cfcf3f047999eb74eb4804f3be9.png ADDED

Git LFS Details

  • SHA256: d3799ad4f5086c21091201cd1a02979f1fb03ef18ff52f9aed4174cc2a08c2ea
  • Pointer size: 131 Bytes
  • Size of remote file: 610 kB
flask_GAN/runtime/outputs/in_f77c29d6f9174b54b5678d30d4834fbf.png ADDED

Git LFS Details

  • SHA256: 1b3c1ca70b7ee493700704f18dab2d8ce0387e7b30c009a1556545b9db1c7613
  • Pointer size: 131 Bytes
  • Size of remote file: 635 kB