初始化:换发型/换发色/训练发型服务

包含:
- hair_service_sd: 主服务(换发型/换发色/生发,端口8801)
- photo_service: LoRA调度+训练(端口32678)
- hair_grow_service: 调试测试页(端口8888,含4个测试页)
- 批量训练脚本(batch_train_hairstyles.py)
- 发际线mask自动识别(hairline_mask.py,4种方案)
- 手绘mask换发型(hair_swap_manual.py)
- 文档:README.md + LARGE_FILES.md + docs/

大文件(模型权重200G、训练数据123G)已排除,见 LARGE_FILES.md
OSS/COS密钥已脱敏为环境变量,原文件备份在本地
This commit is contained in:
xsl
2026-07-07 13:53:52 +08:00
commit 443cfa298f
312 changed files with 67065 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
# ==============================================================
# 换发型服务 .gitignore
# 大文件(模型权重/训练数据/日志/缓存)全部排除,后续上传网盘
# ==============================================================
# ===== 模型权重(共~200G,网盘保存)=====
*.safetensors
*.pth
*.pt
*.ckpt
*.bin
*.onnx
*.tflite
*.task
# so/dll/pack 等编译产物
*.so
*.dll
*.pack
# ===== 大数据目录(共~123G,网盘保存)=====
# 训练素材(341个发型的LoRA+训练图)
data/
# webuiSD模型+228个LoRA+ControlNet~52G,第三方)
project/onediff/
# kohya训练框架(~13G,第三方)
project/kohya_ss_home/
# hair_service_sd 内的模型权重目录(~9.9G
project/hair_service_sd/weights/
# photo_service 临时数据
project/photo_service/data/
# ref/材质/上传图等运行时生成数据(~5G)
project/data/ref_hairstyle/
project/data/ref_haircolor/
project/data/hair_template_material/
project/data/upload_train_imgs/
project/data/tmp/
project/data/res_dir/
project/data/userImage/
project/data/user_info/
project/data/refImage/
project/data/ref_color*/
project/data/ref_user_imgs/
project/data/ref_online/
# 训练原图(本次30张发型图,网盘保存)
hair_type_images/
train_images/
huaban1/
huaban2/
gz-fs/
# ===== Python 缓存 =====
__pycache__/
*.pyc
*.pyo
*.pyd
*.egg-info/
.Python
build/
dist/
# ===== 日志/临时文件 =====
*.log
nohup.out
*.pid
project/logs/
project/hair_service_sd/hair_service.log
project/photo_service/photo_service.log
# ===== 编辑器/系统 =====
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# ===== 测试残留/运行时样图(非代码)=====
project/photo_service/crop_origin.png
project/photo_service/crop_result.png
project/photo_service/image.png
project/photo_service/mask.png
project/photo_service/request_json.json
project/photo_service/photo_service.log
project/photo_service/webd/
project/hair_service_sd/nohup.out
project/hair_service_sd/hair_service.log
hair_grow_service/static/previews/
# ===== 其他运行时临时 =====
*.tmp
tmp/
.cache/
+344
View File
@@ -0,0 +1,344 @@
# 换发型服务 - 本地部署文档
> 本文档说明换发型服务(hair_service_sd)在本机的部署架构、接口用法、训练功能与日常运维操作。
> 该服务从 `szlc@192.168.101.63:/home/szlc/project` 克隆部署至本机 `/home/xsl/change_hair`。
---
## 一、总体架构
服务由三个独立进程组成,存在调用依赖关系:
```
┌─────────────────────────────┐
客户端 ──HTTP──▶ │ hair_service_sd (:8801) │ 主服务,对外提供所有接口
│ conda env: my_hair │
└──────────┬──────────────────┘
│ 换发型/训练时内部调用
┌──────────────┴───────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ photo_service (:32678)│ │ webui (:57860) │
│ conda env: py310 │ │ conda env: sdwebui │
│ LoRA加载/训练调度 │────▶ │ SD模型 + ControlNet │
│ 训练: conda env:kohya│ 推理 │ LoRA权重 │
└──────────────────────┘ └──────────────────────┘
```
| 进程 | 端口 | conda 环境 | 职责 |
|------|------|-----------|------|
| **hair_service_sd** | 8801 | my_hair | 主服务,对外提供换发色/换发型/上传发型/训练回调接口 |
| **photo_service** | 32678 | py310 (推理) / kohya (训练) | LoRA 权重加载、webui 推理调度、LoRA 训练执行 |
| **webui** | 57860 | sdwebui | Stable Diffusion img2img 推理(SD模型 + ControlNet + LoRA |
**启动顺序**(存在依赖):webui → photo_service → hair_service_sd
---
## 二、目录结构
所有数据部署在 `/home/xsl/change_hair`(对应原服务器 `/home/szlc/project` + `/data`):
```
/home/xsl/change_hair/
├── README.md # 本文档
├── start_all.sh # 统一启动/停止/状态脚本
├── start_photo.sh # photo_service 启动包装脚本
├── start_hair.sh # hair_service_sd 启动包装脚本
├── adapt_paths.sh # 路径适配脚本(部署时一次性执行)
├── fix_conda_paths.py # conda 环境路径修复脚本(部署时一次性执行)
├── test_haircolor.py # 换发色接口测试脚本
├── test_swaphair.py # 换发型接口测试脚本
├── test_train_api.py # 训练接口测试脚本
├── project/ # ← 原服务器 /home/szlc/project
│ ├── hair_service_sd/ # 主服务(9.9G,含 weights 模型权重)
│ ├── onediff/ # webui49G,含SD模型+228个LoRA+ControlNet
│ │ └── stable-diffusion-webui/
│ ├── photo_service/ # LoRA调度+训练服务(4.1M
│ ├── kohya_ss_home/ # kohya LoRA训练框架(13G
│ │ └── kohya_ss/ # train_network.py 所在目录
│ ├── data/ # 发型素材(4.9G)
│ │ ├── ref_hairstyle/ # 换发型发型材质(183个发型)
│ │ ├── ref_haircolor/ # 换发色参考
│ │ ├── hair_template_material/ # 发型模板(matting+pkl
│ │ ├── upload_train_imgs/ # 上传发型训练图
│ │ ├── ref_user_imgs/ # ⚠️ 训练素材生成用的参考图(当前为空,见下文说明)
│ │ └── ...
│ └── logs/ # 所有服务日志
└── data/ # ← 原服务器 /data
└── train_material/ # 338个发型的LoRA训练素材(121G
└── <hair_id>/
├── images/<N>_hairstyle/*.png # 训练图片
└── model/hairstyle_hd_lora.safetensors # 训练产出的LoRA权重
```
conda 环境位于 `/home/xsl/miniconda3/envs/`
| 环境 | Python | 用途 |
|------|--------|------|
| my_hair | 3.10.13 | hair_service_sd 主服务 |
| sdwebui | 3.10.19 | webui SD 推理 |
| py310 | 3.10.14 | photo_service LoRA调度 |
| kohya | 3.10.20 | LoRA 训练(**本次部署新建**,原服务用 Docker) |
---
## 三、日常运维操作
### 启动 / 停止 / 查看状态
```bash
cd /home/xsl/change_hair
bash start_all.sh start # 启动全部服务(按依赖顺序)
bash start_all.sh stop # 停止全部服务
bash start_all.sh restart # 重启全部服务
bash start_all.sh status # 查看运行状态与端口
```
启动后 webui 加载 SD 模型需约 1-2 分钟,`status` 显示端口就绪即表示可用。
### 查看日志
```bash
tail -f project/logs/webui.log # webui 日志
tail -f project/logs/photo_service.log # photo_service 日志
tail -f project/logs/hair_service.log # 主服务日志
```
### 运行接口测试
```bash
# 换发色测试(生成红色头发)
/home/xsl/miniconda3/envs/my_hair/bin/python test_haircolor.py
# 换发型测试(用已有发型)
/home/xsl/miniconda3/envs/my_hair/bin/python test_swaphair.py
```
测试结果图保存在 `project/logs/test_*_result.jpg`
---
## 四、接口文档
主服务地址:`http://127.0.0.1:8801`
### 1. 换发色 `POST /hairColor/v2`
把照片中头发的颜色改为指定 RGB。
**请求参数**JSON):
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| img | string | 是 | 图片 base64,需带 `data:image/jpeg;base64,` 前缀 |
| userId | string | 是 | 用户标识 |
| rgb | [int,int,int] | 是 | 目标颜色,长度3的数组,如 `[255,0,0]` 红色 |
| output_format | string | 否 | `base64`(返回base64图片)或 `url`(上传OSS返回链接,默认) |
**请求示例**
```python
import base64, requests
with open("photo.jpg", "rb") as f:
img_b64 = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
resp = requests.post("http://127.0.0.1:8801/hairColor/v2", json={
"img": img_b64,
"userId": "user001",
"rgb": [255, 0, 0], # 红色
"output_format": "base64"
})
result = resp.json()
# {"msg": "success", "state": 0, "result": "<base64图片>", "umd": ""}
```
### 2. 换发型 `POST /api/swapHair/v1`
把照片中人物的发型换成指定发型(需该发型已训练好 LoRA)。
**请求参数**JSON):
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| hair_id | string | 是 | 发型ID(对应 train_material 和 ref_hairstyle 中的目录名) |
| task_id | string | 是 | 任务ID(任意唯一字符串) |
| is_hr | string | 是 | `"true"` 高清版 / `"false"` 普通版 |
| user_img_path | string | 是 | 用户照片 base64,需带 `data:image/jpeg;base64,` 前缀 |
| output_format | string | 否 | `base64``url`(默认) |
**请求示例**
```python
resp = requests.post("http://127.0.0.1:8801/api/swapHair/v1", json={
"hair_id": "1907641420518580226",
"task_id": "task_001",
"is_hr": "true",
"user_img_path": img_b64, # data:image/jpeg;base64,...
"output_format": "base64"
})
result = resp.json()
# {"state": 0, "msg": "success", "data": "<base64图片>", "task_id": "task_001"}
```
**可用发型**:发型需同时具备 `ref_hairstyle/<hair_id>` 材质 + `train_material/<hair_id>/model/hairstyle_hd_lora.safetensors` 权重。查看可用发型:
```bash
# 列出材质+权重都齐全的发型
for hid in $(ls project/data/ref_hairstyle); do
[ -f data/train_material/$hid/model/hairstyle_hd_lora.safetensors ] && echo "$hid"
done
```
### 3. 新增发型训练 `POST /api/uploadHair/v1`
上传新发型图片,异步训练 LoRA。训练完成后发型即可用于换发型。
**请求参数**JSON):
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| hair_id | string | 是 | 新发型ID(自定义唯一ID) |
| img_lists | [string] | 是 | 发型图片 URL 列表(至少1张,需 HTTP 可访问的URL) |
**注意**:此接口 `img_lists` 必须是 HTTP URL(内部用 requests 下载),不支持 base64。如用本地图片,需先启动一个 HTTP 服务托管。
**响应**:立即返回 `{"msg": "OK", "state": 0}`,训练在后台异步进行(约 30-40 分钟)。
### 4. 训练回调 `POST /api/hair/trainCallBack`
photo_service 训练完成后自动调用此接口通知主服务,一般不由外部直接调用。
---
## 五、新增发型训练(LoRA Training
### 工作原理
原服务通过 Docker 容器(`chinatszrn/ubuntu:kohya_ss`)运行 kohya 的 `train_network.py` 训练 LoRA。**本机部署已改为本地 conda 环境直接执行**,无需 Docker。
完整链路:
```
uploadHair → 下载图片 → 生成训练素材(matting/pkl/训练图)
→ photo_service 入队
→ 后台 train_thread 执行:
accelerate launch train_network.py (1500步, 约30-40分钟)
→ 产出 hairstyle_hd_lora.safetensors
→ 回调 trainCallBack
→ 该发型即可用于换发型
```
### 训练配置(已适配本机)
photo_service 的训练命令(`lora_train_service_1.py``train_thread` 函数)关键参数:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| 训练脚本 | `kohya_ss_home/kohya_ss/train_network.py` | kohya LoRA 训练 |
| conda 环境 | `/home/xsl/miniconda3/envs/kohya` | torch 2.0.1+cu118 |
| 基础模型 | `v1-5-pruned-emaonly.safetensors` | SD底模(原为 majicmix,本机不存在,已替换) |
| 优化器 | `AdamW` | 原为 AdamW8bit(依赖 bitsandbytes,有兼容问题,已改) |
| 训练步数 | 1500 | |
| LoRA维度 | 128 (network_dim) | |
| GPU | device 0 | 单卡 |
### 手动触发训练(不经过 uploadHair
如果已有训练素材(`train_material/<hair_id>/images/<N>_hairstyle/*.png`),可直接调用 photo_service 训练:
```bash
curl -X POST http://127.0.0.1:32678/api/hair/train \
-H "Content-Type: application/json" \
-d '{
"task_id": "manual_train_001",
"hair_id": "1905785164224868354",
"hair_material_dir": "/home/xsl/change_hair/data/train_material/1905785164224868354",
"is_tj": "1",
"device_id": "0",
"webui_addr": "http://0.0.0.0:32678/"
}'
```
训练进度查看:`tail -f project/logs/photo_service.log`(搜索 `steps:`
### ⚠️ 训练素材生成依赖说明
`uploadHair` 的完整流程中,**生成训练素材这一步依赖 `project/data/ref_user_imgs/` 参考图目录**(用于人脸对齐生成训练对)。该目录在原服务器上即为空(这是原项目状态)。
-`ref_user_imgs` 为空,uploadHair 能下载图片并触发 LoRA 训练,但**生成的新训练素材质量会受限**
- 要完整使用 uploadHair 上传全新发型训练,需往 `ref_user_imgs/` 补充参考人像图 + 对应的 `.pkl` 关键点文件
---
## 六、部署改动记录
以下是对原服务代码/配置的改动,供维护参考:
### 1. 路径适配(所有配置和代码)
原服务器路径 → 本机路径的映射:
| 原路径 | 本机路径 |
|--------|---------|
| `/home/szlc/project/...` | `/home/xsl/change_hair/project/...` |
| `/data/train_material` | `/home/xsl/change_hair/data/train_material` |
| `/home/szlc/miniconda3` | `/home/xsl/miniconda3` |
涉及文件:`hair_service_sd/config/configure.ini``hair_service_sd/*.py``photo_service/*.py``onediff/.../config.json` 等。
执行脚本:`adapt_paths.sh`(业务代码)+ `fix_conda_paths.py`conda环境)。
### 2. webui 配置改动
| 改动 | 说明 |
|------|------|
| `config.json` 的 onediff compiler 路径 | 指向本机 onediff 目录 |
| 启动加 `HF_HUB_OFFLINE=1` | 本机无法访问 huggingface.co,用本地缓存离线加载 CLIP |
| SD 模型 | 实际加载 `v1-5-pruned-emaonly`config 里写的 majicmix 不存在,webui 自动 fallback |
### 3. 换发型推理改动(`hair_service_sd/gen_super_image.py`
`refiner_checkpoint` 从不存在的 `majicmixRealistic_v7.safetensors` 改为 `v1-5-pruned-emaonly.safetensors`(第185、213行)。否则 webui 报 `Could not find checkpoint` 错误。
### 4. photo_service 训练改动(`photo_service/lora_train_service_1.py`
| 改动 | 原值 | 新值 |
|------|------|------|
| LoRA 输出目录 | `/gz-fs/Lora` | webui 的 models/Lora 目录(实际路径) |
| kohya 训练目录 | `/root/project/kohya_ss_home` | 本机 kohya_ss_home |
| 训练方式 | `sudo docker run chinatszrn/ubuntu:kohya_ss accelerate launch ...` | 本地 `accelerate launch ...`conda kohya 环境) |
| 基础模型 | `/mnt/nas_hdd/.../majicmixRealistic_v7.safetensors` | 本机 `v1-5-pruned-emaonly.safetensors` |
| 优化器 | AdamW8bitbitsandbytes | AdamWtorch 原生) |
| tokenizer | `--tokenizer_cache_dir=/home/chinatszrn/.cache/clip` | 删除(用 HF 缓存 + 离线模式) |
---
## 七、已知限制与注意事项
1. **OSS 上传**:换发色/换发型默认 `output_format=url` 会把结果图上传到**生产阿里云 OSS**(密钥硬编码在代码中)。仅本地测试时建议用 `output_format=base64` 避免污染生产环境。
2. **网络限制**:本机无法访问 `huggingface.co`。webui 和训练都依赖本地 HF 缓存(`~/.cache/huggingface`),已配置离线模式。如需新增 HF 模型需手动准备缓存。
3. **ref_user_imgs 为空**:影响 uploadHair 生成训练素材的完整性(见第五节说明)。
4. **单卡部署**GPU 固定使用 device 0。`CUDA_VISIBLE_DEVICES=0` 已写入启动脚本。如有多卡需求需调整配置。
5. **服务进程保活**:服务通过 nohup + wrapper 脚本启动。**切勿直接 `python xxx.py &` 启动**,会被 shell 会话退出时清理。请始终用 `start_all.sh start``start_*.sh` 脚本。
6. **webui 启动较慢**:加载 SD 模型需 1-2 分钟,启动后需等待 `start_all.sh status` 显示端口就绪才能正常服务。
---
## 八、快速排障
| 现象 | 可能原因 | 解决 |
|------|---------|------|
| 换发型报 `推理发型失败` | webui 未就绪或 LoRA 缺失 | `start_all.sh status` 检查 webui 端口;确认发型 LoRA 存在 |
| webui 报 `Could not find checkpoint` | refiner 模型名不对 | 确认 `gen_super_image.py` 的 refiner_checkpoint 为 `v1-5-pruned-emaonly.safetensors` |
| 训练报 bitsandbytes 错误 | bnb 版本/优化器问题 | 确认用 `AdamW`(非 AdamW8bit |
| webui 卡在 huggingface 重试 | HF 离线模式未启用 | 启动脚本已设 `HF_HUB_OFFLINE=1`,确认未丢失 |
| 服务启动后立即退出 | 未用 wrapper 脚本启动 | 用 `start_all.sh start` 启动 |
| conda 环境 python 段错误 | 环境二进制损坏 | 重新 rsync 环境并用 `fix_conda_paths.py` 修复(勿用 sed |
+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# 路径适配脚本:修正所有硬编码路径 + conda环境修复 + 端口适配
# 在数据同步完成后执行
set -u
BASE="/home/xsl/change_hair"
PROJ="$BASE/project"
LOG="$PROJ/logs/adapt.log"
mkdir -p "$PROJ/logs"
echo "路径适配开始: $(date '+%Y-%m-%d %H:%M:%S')" | tee "$LOG"
# ============================================================
# 第一部分:conda 环境硬编码路径修复
# ============================================================
echo "=== [1/4] 修复 conda 环境 shebang/路径前缀 ===" | tee -a "$LOG"
for ENV in py310 my_hair sdwebui; do
ENVDIR="/home/xsl/miniconda3/envs/$ENV"
if [ -d "$ENVDIR" ]; then
# 替换所有文本文件里的旧前缀为新前缀
# 旧前缀有两种:/home/szlc/miniconda3 和 /usr/local/miniconda3
grep -rl '/home/szlc/miniconda3\|/usr/local/miniconda3' "$ENVDIR" 2>/dev/null | while read -r f; do
sed -i 's|/home/szlc/miniconda3|/home/xsl/miniconda3|g; s|/usr/local/miniconda3|/home/xsl/miniconda3|g' "$f"
done
echo " [$ENV] 修复完成" | tee -a "$LOG"
else
echo " [$ENV] 目录不存在,跳过" | tee -a "$LOG"
fi
done
# ============================================================
# 第二部分:hair_service_sd 配置和代码路径适配
# ============================================================
echo "=== [2/4] 适配 hair_service_sd 路径 ===" | tee -a "$LOG"
HS="$PROJ/hair_service_sd"
# 安全替换函数:只处理 .py / .ini / .conf 文件,跳过权重/图片/二进制
adapt_hair_paths() {
local f="$1"
[ -f "$f" ] || return 0
case "$f" in
*.py|*.ini|*.conf|*.sh|*.json|*.yaml|*.yml|*.txt) ;;
*) return 0 ;;
esac
sed -i \
-e 's|/home/szlc/project|/home/xsl/change_hair/project|g' \
-e 's|/data/train_material|/home/xsl/change_hair/data/train_material|g' \
-e 's|/home/szlc/miniconda3|/home/xsl/miniconda3|g' \
-e 's|/usr/local/miniconda3|/home/xsl/miniconda3|g' \
"$f"
}
# 处理 hair_service_sd 顶层文件和 common/config 子目录
for f in "$HS"/*.py "$HS"/*.ini "$HS"/*.sh "$HS"/*.conf "$HS"/common/*.py "$HS"/config/*.ini "$HS"/config/*.py "$HS"/core/*.py "$HS"/utils/*.py; do
adapt_hair_paths "$f"
done
echo " hair_service_sd 配置/代码路径已适配" | tee -a "$LOG"
# ============================================================
# 第三部分:photo_service 路径适配
# 注意:photo_service 的 chinatszrn/kohya 路径属于 Docker 训练链路,
# 本机不配 Docker,这些路径保留不动(不影响换发色/换发型)
# 但基础路径和 callback_url 要适配
# ============================================================
echo "=== [3/4] 适配 photo_service 路径 ===" | tee -a "$LOG"
PS="$PROJ/photo_service"
for f in "$PS"/*.py; do
[ -f "$f" ] || continue
sed -i \
-e 's|/home/szlc/project|/home/xsl/change_hair/project|g' \
-e 's|/home/szlc/miniconda3|/home/xsl/miniconda3|g' \
"$f"
done
echo " photo_service 基础路径已适配(kohya/Docker路径保留)" | tee -a "$LOG"
# ============================================================
# 第四部分:onediff webui 路径适配
# ============================================================
echo "=== [4/4] 适配 onediff webui 路径 ===" | tee -a "$LOG"
OD="$PROJ/onediff/stable-diffusion-webui"
# webui config.json 里的 onediff compiler_caches 路径
if [ -f "$OD/config.json" ]; then
sed -i 's|/home/student/Documents/workspace_cxt_tianjing_hair/onediff|/home/xsl/change_hair/project/onediff|g; s|/home/szlc/project/onediff|/home/xsl/change_hair/project/onediff|g' "$OD/config.json"
echo " webui config.json 已适配" | tee -a "$LOG"
fi
# webui 启动脚本
for f in "$OD"/webui.sh "$OD"/webui-user.sh "$PROJ"/onediff/onediff/*.sh; do
[ -f "$f" ] && sed -i 's|/home/szlc/miniconda3|/home/xsl/miniconda3|g' "$f"
done
# ============================================================
# 第五部分:Lora 软链接(photo_service 训练产物输出位置)
# 本机用真实目录,不走 /gz-fs 软链
# ============================================================
echo "=== [5/5] 配置 Lora 目录 ===" | tee -a "$LOG"
mkdir -p "$BASE/gz-fs/Lora"
# webui 的 models/Lora 已随 onediff 拷贝过来(真实目录),无需软链
echo " Lora 目录就绪: $BASE/gz-fs/Lora" | tee -a "$LOG"
echo "路径适配完成: $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "$LOG"
echo "请检查 $LOG 确认无误" | tee -a "$LOG"
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""批量训练发型 LoRA
对 hair_type_images/ 下每张图(一种发型):
1. 拷贝到独立输入目录(train_hairstyle_full.py 要求 --input 是目录)
2. 调用 train_hairstyle_full.py 完整流程:数据增强→训练→材质→女生预览
3. 串行执行,记录每个发型成功/失败
用法:
cd /home/xsl/change_hair/project/hair_service_sd
python /home/xsl/change_hair/batch_train_hairstyles.py --src /home/xsl/change_hair/hair_type_images --gender girl [--only 圆-心形] [--start-from 心形-心形]
注意:
- 中文 hair_id 直接用文件名(去扩展名)作为 ID
- 每个发型约 30-40 分钟,30 个串行约 15-20 小时
- 失败的发型记录到失败清单,最后可单独重跑
"""
import os
import sys
import time
import shutil
import argparse
import subprocess
from datetime import datetime
SRC_DEFAULT = "/home/xsl/change_hair/hair_type_images"
WORK_DIR = "/home/xsl/change_hair/data/batch_train_inputs"
LOG_FILE = "/home/xsl/change_hair/data/batch_train_log.txt"
TRAIN_SCRIPT = "/home/xsl/change_hair/train_hairstyle_full.py"
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
LOCK_FILE = "/home/xsl/change_hair/data/batch_train.pid"
DONE_FILE = "/home/xsl/change_hair/data/batch_train_done.txt" # 已完成发型清单(断点续跑)
LORA_DIR = "/home/xsl/change_hair/data/train_material" # LoRA 输出根目录
def acquire_lock():
"""PID 锁,防止重复启动多个实例互相干扰"""
import psutil
if os.path.exists(LOCK_FILE):
try:
old_pid = int(open(LOCK_FILE).read().strip())
if psutil.pid_exists(old_pid):
# 检查是不是真的本脚本进程
try:
proc = psutil.Process(old_pid)
if "batch_train_hairstyles" in " ".join(proc.cmdline()):
print(f"✗ 已有批量训练实例在运行 (PID={old_pid}),请先停止它再启动。")
print(f" 锁文件: {LOCK_FILE}")
sys.exit(2)
except Exception:
pass
except (ValueError, OSError):
pass
os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
open(LOCK_FILE, "w").write(str(os.getpid()))
def release_lock():
try:
os.remove(LOCK_FILE)
except OSError:
pass
def load_done():
"""读取已完成的发型集合"""
if not os.path.exists(DONE_FILE):
return set()
return set(l.strip() for l in open(DONE_FILE, encoding="utf-8") if l.strip())
def mark_done(hair_id):
with open(DONE_FILE, "a", encoding="utf-8") as f:
f.write(hair_id + "\n")
def log(msg):
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
print(line, flush=True)
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
def prepare_input_dir(hair_id, src_img, work_dir):
"""为单个发型创建独立输入目录,里面只放这一张图"""
d = os.path.join(work_dir, hair_id)
if os.path.exists(d):
shutil.rmtree(d)
os.makedirs(d, exist_ok=True)
# 统一用 png 拷入,避免扩展名问题
dst = os.path.join(d, hair_id + ".png")
shutil.copy(src_img, dst)
return d, dst
def run_one(hair_id, src_img, gender, py, done_set, force=False):
"""跑单个发型的完整训练流程,返回 (ok, elapsed, skipped)"""
# 断点续跑:LoRA 已存在视为完成
lora_path = os.path.join(LORA_DIR, hair_id, "model", "hairstyle_hd_lora.safetensors")
if not force and (hair_id in done_set or
(os.path.exists(lora_path) and os.path.getsize(lora_path) > 100000000)):
log(f"{hair_id} 已完成(LoRA 存在),跳过")
return True, 0, True
t0 = time.time()
log(f"==== 开始训练: {hair_id} ====")
# 步骤1:准备输入目录
try:
in_dir, tpl_img = prepare_input_dir(hair_id, src_img, WORK_DIR)
log(f" 输入目录就绪: {in_dir}")
except Exception as e:
log(f" ✗ 输入目录准备失败: {e}")
return False, time.time() - t0, False
# 步骤2-5:调用 train_hairstyle_full.py
cmd = [
py, TRAIN_SCRIPT,
"--hair-id", hair_id,
"--input", in_dir,
"--gender", gender,
"--template-img", tpl_img,
]
log(f" 调用: {' '.join(cmd[:2])} ... --hair-id {hair_id}")
try:
# 子进程输出实时写到日志文件
proc_log = os.path.join("/home/xsl/change_hair/data", f"subprocess_{hair_id}.log")
with open(proc_log, "w", encoding="utf-8") as f:
r = subprocess.run(
cmd, cwd=HAIR_SERVICE_DIR,
stdout=f, stderr=subprocess.STDOUT,
timeout=5400, # 单发型最长 90 分钟
)
elapsed = time.time() - t0
if r.returncode == 0:
log(f" ✓ 训练成功: {hair_id} (耗时 {elapsed/60:.1f} 分钟)")
mark_done(hair_id)
return True, elapsed, False
else:
log(f" ✗ 训练失败(returncode={r.returncode}): {hair_id}, 详见 {proc_log}")
return False, elapsed, False
except subprocess.TimeoutExpired:
log(f" ✗ 训练超时(>90分钟): {hair_id}")
return False, time.time() - t0, False
except Exception as e:
log(f" ✗ 异常: {hair_id}: {e}")
return False, time.time() - t0, False
def main():
ap = argparse.ArgumentParser(description="批量训练发型")
ap.add_argument("--src", default=SRC_DEFAULT, help="原图目录")
ap.add_argument("--gender", default="girl", choices=["boy", "girl"])
ap.add_argument("--py", default="/home/xsl/miniconda3/envs/my_hair/bin/python",
help="python 解释器")
ap.add_argument("--only", default=None, help="只训练指定 hair_id(调试用)")
ap.add_argument("--start-from", default=None,
help="从指定 hair_id 开始(含),跳过之前的(断点续跑)")
ap.add_argument("--force", action="store_true",
help="强制重训(忽略已完成清单和已存在的 LoRA)")
args = ap.parse_args()
acquire_lock()
done_set = load_done() if not args.force else set()
# 收集发型清单(按文件名排序)
files = []
for f in sorted(os.listdir(args.src)):
if f.lower().endswith((".jpg", ".jpeg", ".png")):
files.append(f)
if not files:
log(f"✗ 源目录无图片: {args.src}")
release_lock(); sys.exit(1)
# 构造 (hair_id, src_img)
tasks = []
for f in files:
hair_id = os.path.splitext(f)[0]
tasks.append((hair_id, os.path.join(args.src, f)))
# 过滤
if args.only:
tasks = [t for t in tasks if t[0] == args.only]
if not tasks:
log(f"✗ --only {args.only} 未匹配到发型"); release_lock(); sys.exit(1)
if args.start_from:
idx = next((i for i, t in enumerate(tasks) if t[0] == args.start_from), 0)
tasks = tasks[idx:]
log(f"\n{'#'*60}")
log(f"# 批量训练启动 (PID={os.getpid()})")
log(f"# 共 {len(tasks)} 个发型 | gender={args.gender} | force={args.force}")
if done_set:
log(f"# 已完成 {len(done_set)} 个(将跳过)")
log(f"# 预计总耗时约 {len(tasks)*35} 分钟")
log(f"{'#'*60}\n")
os.makedirs(WORK_DIR, exist_ok=True)
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
results = [] # (hair_id, ok, elapsed, skipped)
t_total = time.time()
try:
for i, (hair_id, src) in enumerate(tasks, 1):
log(f"\n[进度 {i}/{len(tasks)}] === {hair_id} ===")
ok, el, skipped = run_one(hair_id, src, args.gender, args.py, done_set, args.force)
results.append((hair_id, ok, el, skipped))
finally:
release_lock()
# 汇总
log(f"\n{'#'*60}")
log(f"# 批量训练完成汇总")
log(f"{'#'*60}")
succ = [r for r in results if r[1]]
fail = [r for r in results if not r[1]]
skip = [r for r in results if r[3]]
total_time = time.time() - t_total
for r in results:
hid, ok, el = r[0], r[1], r[2]
flag = "SKIP" if r[3] else ("OK" if ok else "FAIL")
log(f" [{flag}] {hid:20s} ({el/60:.1f} 分钟)")
log(f"\n成功 {len(succ)}/{len(results)}(含跳过 {len(skip)}),失败 {len(fail)},总耗时 {total_time/3600:.1f} 小时")
if fail:
log(f"失败清单(可重跑): {' '.join(f[0] for f in fail)}")
log(f"重跑命令: python {__file__} --gender {args.gender} --only <hair_id>")
log(f"详细日志: {LOG_FILE}")
if __name__ == "__main__":
main()
+301
View File
@@ -0,0 +1,301 @@
# 区域生发功能 - 技术调研报告
> **功能定义**:输入一张头发稀少/发际线偏高的人头照片,在照片上指定一个区域或遮罩,输出在该区域内"生发"(补全/填充头发)的结果图。
>
> 调研日期:2026-06-21
---
## 一、需求分析与技术挑战
### 1.1 需求拆解
本功能本质是一个 **"受限区域头发补全"**constrained hair inpainting)任务,可拆为三个子问题:
| 子问题 | 描述 | 难度 |
|--------|------|------|
| ① 在哪生发 | 确定缺发区域(发际线后退区、头顶稀疏区),即 mask | 中(可人工画,也可自动检测) |
| ② 生成什么 | 在 mask 区域内生成与本人发色、发质、走向自然衔接的头发 | 高(核心难点) |
| ③ 如何融合 | 把生成结果无缝贴回原图,消除边缘拼接痕迹 | 低(已有成熟工具) |
### 1.2 关键挑战
1. **生成真实感**:新生头发必须符合该人的发色/卷曲度/走向,不能突兀。SD1.5 对细密发丝的生成能力有限,容易出现"塑料感"或糊。
2. **边缘自然衔接**:新生头发与原有头发、额头皮质的交界处不能有硬边或色差。
3. **发丝纹理一致性**:新头发应延续原头发的纹理方向(从发旋/发根向外)。
4. **不破坏五官**:生发区域靠近额头/眉毛,生成时不能改变五官。
5. **mask 边界合理性**:mask 区域应只覆盖"该长头发但目前没长"的地方,不能覆盖到脸上或背景。
---
## 二、技术方案全景
综合代码库已有能力和业界方案,归纳出 **4 种实现路径**,按推荐度排序。
### 方案 A:复用现有 webui Inpainting(★★★★★ 强烈推荐)
**核心思路**:把"生发"当作一次 SD inpainting——mask 区域重绘为头发,非 mask 区域保持不变。这正是换发型服务里 `webui_img2img` 已经在做的事,几乎可原样复用。
```
[原图] → [人检+1k关键点] → [构造生发mask] → [webui_img2img(mask内重绘为头发)] → [泊松融合] → [结果]
```
**可复用的现成组件**(均在代码库内,已验证可用):
| 环节 | 复用组件 | 文件:行号 | 说明 |
|------|---------|-----------|------|
| 人脸检测 | `RetinaFaceDetector.forward` | models/detector.py:148 | 定位人脸框 |
| 1k关键点 | `MomocvFaceAlignment1K.stable_forward` | utils/MomocvFaceAlignment1K.py:404 | 发际线定位的关键 |
| 头发mask | `Generator_Matte.matte_inference` | hair_matting/Generator_Matte.py:81 | 现有头发alpha |
| **发际线/头皮分割** | `Generator_BaldSeg_5c.forward` | models/Generator_BaldSeg.py:45 | **自动定位缺发区,权重存在** |
| 额头凸包mask | `draw_bigger_hull_mask` | utils/landmark_processor.py:1079 | 关键点→额头区mask |
| **SD生成** | `webui_img2img(img, mask_img, ...)` | gen_super_image.py:416 | **核心生成,已验证可用** |
| 泊松融合 | `cv2.seamlessClone` | core/hairstyle_model.py:1380 | 消除拼接缝 |
**mask 构造方法(两种交互模式)**
- **手动模式**(用户画遮罩):前端提供画笔工具,用户在发际线/稀疏区涂抹 → 直接作为 inpainting mask。最简单可靠。
- **自动模式**(自动检测缺发区):`BaldSeg` 的 5 类分割直接区分"有发区/头皮区",或用 `额头凸包mask - 现有头发mask = 缺发带`。无需用户画,但需调参。
**生成参数建议**(基于 `build_body_v2`gen_super_image.py:86):
- `denoising_strength = 0.5~0.7`(重绘强度,过低生不出新发,过高破坏原有头发)
- `mask_blur = 8~12`(边缘羽化,避免硬边)
- `prompt = "thick natural hair, detailed hair strands, realistic hairline, high detail"`
- `negative_prompt = "bald, receding hairline, thinning hair, bald spot, skin, low quality"`
- `inpainting_fill = 1`mask 区域填噪声重绘)
- `steps = 20~30`
**优势**
- ✅ 90% 代码已存在且验证过,开发量最小(主要是拼装 + mask 构造逻辑)
- ✅ 与现有服务架构完全一致,可作为一个新接口 `/api/hairGrow/v1` 接入
- ✅ 无需新增任何模型权重
**劣势**
- ⚠️ SD1.5v1-5-pruned)生成细密发丝的真实感有限,发丝可能偏糊
- ⚠️ 同一个人多次生发结果可能不一致(生成模型随机性)
**预估工作量**:2-3 天(mask 构造逻辑 + 接口封装 + 调参)
---
### 方案 BControlNet Canny 约束发丝走向(★★★★ 推荐)
**核心思路**:在方案 A 基础上,引入 **ControlNet Canny** 约束新生头发的走向,使其与已有头发的纹理方向一致,提升真实感。
```
[原图] → 提取已有头发的 canny 边缘 → [mask内: SD inpainting + ControlNet canny约束] → [结果]
```
**关键改动**(相对方案A):
- 启用 webui 的 ControlNet(代码库已装 `control_v11p_sd15_canny` 模型,可用)
- 参照 `gen_super_image.py:244 build_body``alwayson_scripts` 结构,把 openpose 改成 canny
- canny 输入图:对原图做 `cv2.Canny`,或在 mask 外保留原头发边缘作为约束
**ControlNet 配置示例**(需新增到 build_body):
```python
"alwayson_scripts": {
"controlnet": {
"args": [{
"model": "control_v11p_sd15_canny",
"module": "canny",
"weight": 0.5, # 约束强度,0.3-0.7
"input_image": <原图base64>, # webui自动做canny
"resize_mode": "just_resize"
}]
}
}
```
**优势**
- ✅ 发丝走向更自然、与原发衔接更好
- ✅ ControlNet 模型和扩展都已在 webui 里,无需额外部署
**劣势**
- ⚠️ canny 约束在"完全无发的光头区"无法提供有效边缘(那里本来就没头发),主要对"稀疏→加密"有效
- ⚠️ 需要调 `weight`,过强会让新生头发僵硬
**预估工作量**3-4 天(方案A + ControlNet 调试)
---
### 方案 CSAM 自动分割 + SD Inpainting(★★★ 可选)
**核心思路**:用 **Segment Anything (SAM)** 替代手动画 mask——用户点击缺发区域,SAM 自动分割出精确边界,再做 inpainting。
```
[原图] → [用户点击发际线区域] → [SAM分割出该区域mask] → [SD inpainting] → [结果]
```
**业界实践**:这是目前社区主流的"点击式 inpainting"范式,已有成熟工具:
- **sd-webui-inpaint-anything** 扩展([GitHub](https://github.com/Uminosachi/sd-webui-inpaint-anything)):webui 集成 SAM + inpainting
- 学术:[Segment Anything Meets Image Inpainting (arXiv:2304.06790)](https://arxiv.org/abs/2304.06790) 提出 click-and-fill 范式
**是否需要 SAM**
- 若用户**手动画 mask**(方案A)→ 不需要 SAM
- 若要**点击式自动分割** → 需要 SAM(代码库没有,需新增 ~2.5G 模型)
**优势**
- ✅ 交互体验好(点一下即可),mask 边界比手画精确
**劣势**
- ⚠️ 需新增 SAM 模型(vit_h 约 2.5G),增加部署复杂度
- ⚠️ SAM 对"发际线缺发带"这种渐变区域的分割不一定准确(头发与皮肤对比度低时)
- ⚠️ 代码库已有 `Generator_BaldSeg`(头皮分割)+ 1k关键点(发际线定位),**可达到类似自动 mask 效果,不一定非要用 SAM**
**预估工作量**5-7 天(含 SAM 集成)
**结论**:**不优先推荐**。代码库的 BaldSeg + 关键点方案已能实现"自动定位缺发区",SAM 的边际价值不大,除非未来需要通用点击分割。
---
### 方案 D:专项 LoRA 微调 / 训练生发模型(★★ 备选)
**核心思路**:收集"稀疏发/高发际线 → 浓密发"的配对数据,训练一个生发专用 LoRA 或微调模型,专门优化生发效果。
**业界相关研究**
- **HairDiffusion (NeurIPS 2024)**:把发型/发色编辑重新定义为 latent diffusion inpainting 任务
- **AnyBald (WACV 2026)**diffusion inpainting + 可学习文本提示做头发去除(反向任务,架构可借鉴)
- **Stable-Hair (arXiv 2024)**diffusion 真实世界发型迁移
**适用场景**:当方案 A/B 用通用 SD 的生发效果不够好(发丝太糊/不真实),且业务对质量要求高时。
**优势**
- ✅ 可针对性优化生发质量,效果上限最高
**劣势**
-**需要大量配对训练数据**(稀疏发→浓密发),数据收集是最大瓶颈
- ❌ 训练+调优周期长(2-4 周)
- ❌ 代码库的 LoRA 训练流程(kohya)是针对"发型模板"的,生发是不同任务,需重新组织数据
**预估工作量**:3-4 周(含数据收集、训练、调优)
**结论**:**作为后续优化手段备选**,不作为首期方案。先用方案 A/B 跑通,效果不达标再考虑。
---
## 三、推荐方案与实施路径
### 3.1 推荐:方案 A 为主,方案 B 为增强
**首期实现方案 A**(复用 webui inpainting),最快验证可行性;**若效果不足,叠加方案 B**ControlNet canny)。方案 C/D 暂不引入。
理由:
1. 方案 A 的 90% 组件已存在且验证过,风险最低、见效最快
2. 代码库的 `Generator_BaldSeg` 能自动定位缺发区,免去了 SAM 的额外部署
3. 与现有服务架构一致,可作为 hair_service_sd 的新接口无缝接入
### 3.2 推荐的功能管线(方案A)
```
输入: 人头图 + (可选)手绘mask
├─ 若无手绘mask(自动模式):
│ 1. RetinaFace 检测人脸 → 1k关键点
│ 2. Generator_BaldSeg 分割 → 提取"头皮/发际线"区
│ 3. 或: 额头凸包mask(关键点) - 现有头发mask(matting) = 缺发带
│ 4. mask 形态学处理(dilate/erode 羽化)
├─ 若有手绘mask(手动模式):
│ 直接使用,轻微 dilate 羽化
mask_blur=10 的 inpainting:
webui_img2img(
img = 原图,
mask_img = 生发mask,
tag = "thick natural hair, detailed hair strands, hairline",
denoising_strength = 0.6,
is_hr = True
)
泊松融合贴回(cv2.seamlessClone + GaussianBlur)
输出: 生发结果图
```
### 3.3 接口设计建议
```
POST /api/hairGrow/v1
{
"img": "data:image/jpeg;base64,...", # 人头图
"mask": "data:image/png;base64,...", # 可选: 手绘mask(白色=生发区)
"auto_mask": true, # 可选: 无mask时自动检测缺发区
"userId": "xxx",
"output_format": "base64"
}
```
返回(同换发型):
```json
{"state": 0, "msg": "success", "data": "<base64结果图>", "task_id": "..."}
```
---
## 四、代码库可复用能力清单(详细)
以下能力**全部已在本机部署中验证可用**,无需新增模型:
| 能力 | 模块 | 文件:行号 | 权重 | 对生发的作用 |
|------|------|-----------|------|-------------|
| 人脸检测 | RetinaFace | models/detector.py:148 | Resnet50_Final.pth (109M) | 入口,定位人脸 |
| 1k关键点 | MomocvFaceAlignment1K | utils/MomocvFaceAlignment1K.py:404 | face_alignment_1k.pth (49M) | **发际线定位** |
| 头发抠图 | Generator_Matte | hair_matting/Generator_Matte.py:81 | deeplabv3+gca (535M) | 现有头发边界 |
| **头皮分割** | **Generator_BaldSeg_5c** | **models/Generator_BaldSeg.py:45** | **ori_hair_checkpoint (134M)** | **自动定位缺发区** |
| 人脸分割 | FaceSeg | core/faceseg/face_seg.py:32 | faceseg_20210927 (229M) | 排除五官区 |
| SD inpainting | webui_img2img | gen_super_image.py:416 | v1-5 SD模型 (4.2G) | **核心生成** |
| 额头mask构造 | draw_bigger_hull_mask | utils/landmark_processor.py:1079 | - | 关键点→额头区 |
| 泊松融合 | cv2.seamlessClone | core/hairstyle_model.py:1380 | - | 边缘融合 |
| 形态学处理 | cv2.dilate/erode | utils/landmark_processor.py 多处 | - | mask羽化 |
| ControlNet canny | sd-webui-controlnet | webui扩展(已装) | control_v11p_sd15_canny | 发丝走向约束(方案B) |
---
## 五、风险与质量评估
| 风险点 | 严重度 | 应对 |
|--------|--------|------|
| SD1.5生发发丝偏糊 | 中 | 方案B加canny约束;或后续换SDXL inpainting模型(需升级webui) |
| 自动mask不准(误判缺发区) | 中 | 首期优先支持手绘mask;自动模式作为可选增强 |
| 生发破坏五官 | 高 | mask构造时必须减去FaceSeg的人脸区;inpainting的mask严格限定 |
| 多次结果不一致 | 低 | 固定seed(代码已用seed=123456789);可返回多张供选 |
| 边缘拼接痕迹 | 低 | 泊松融合+GaussianBlur已有成熟方案 |
**质量预期**:方案A对手绘mask场景(用户明确指定生发区)效果应较好;对稀疏加密(发际线整体下移)效果中等;对完全光头大区域生发效果有限(建议用方案D专项优化)。
---
## 六、实施建议(分阶段)
**阶段一(2-3天):MVP 手动 mask 生发**
- 实现 `/api/hairGrow/v1` 接口,接收手绘 mask
- 复用 `webui_img2img`prompt 写生发描述
- 加泊松融合后处理
- 用几张发际线高的测试图调参(denoising/mask_blur/prompt
**阶段二(2-3天):自动 mask + ControlNet**
- 接入 `Generator_BaldSeg` 自动检测缺发区
- 叠加 ControlNet canny 约束发丝走向(方案B)
- 对比有无 canny 的效果差异
**阶段三(按需):质量优化**
- 若效果不足:收集生发配对数据,训练专项 LoRA(方案D)
- 或升级到 SDXL inpainting 模型提升发丝真实感
---
## 七、参考资料
**学术论文**
- [HairDiffusion: Vivid Multi-Colored Hair Editing via Latent Diffusion (NeurIPS 2024)](https://openreview.net/forum?id=UQflshLbZv) — 发型编辑的 inpainting 范式
- [Stable-Hair: Real-World Hair Transfer via Diffusion Model (arXiv 2024)](https://arxiv.org/html/2407.14078v1) — 扩散发型迁移
- [Segment Anything Meets Image Inpainting (arXiv:2304.06790)](https://arxiv.org/abs/2304.06790) — SAM click-and-fill 范式
**实践工具/社区**
- [sd-webui-inpaint-anything (GitHub)](https://github.com/Uminosachi/sd-webui-inpaint-anything) — SAM + inpainting webui 扩展
- [Stable Diffusion Inpainting with SAM (HuggingFace Space)](https://huggingface.co/spaces/Sanshruth/Stable-Diffusion-Inpainting_with_SAM) — 在线 demo
- [Reddit: 用 inpainting 添加头发的技巧](https://www.reddit.com/r/StableDiffusion/comments/15x9yvs/) — 社区经验:先预填底色再 inpainting
- [Evoto Hair Editor](https://www.evoto.ai/features/hair-part) — 商业产品效果参考(稀疏加密/发际线调整)
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""安全修复 conda 环境内的硬编码路径前缀。
只处理文本文件(通过二进制检测),跳过 .so/.pyc/权重等二进制文件。
"""
import os
import sys
OLD_PREFIXES = [
b"/home/szlc/miniconda3",
b"/usr/local/miniconda3",
]
NEW_PREFIX = b"/home/xsl/miniconda3"
def is_probably_binary(filepath):
"""通过文件扩展名和内容判断是否为二进制文件"""
# 明确的文本扩展名直接处理
text_exts = ('.py', '.sh', '.txt', '.cfg', '.ini', '.json', '.yaml', '.yml',
'.cmake', '.make', '.pc', '.la', '.template', '.desktop',
'.service', '.conf', '.md', '.rst', '.in', '.bashrc', '.profile')
binary_exts = ('.so', '.pyc', '.pyo', '.a', '.o', '.dylib', '.dll',
'.pth', '.pt', '.bin', '.safetensors', '.ckpt', '.npz',
'.npy', '.pkl', '.zip', '.tar', '.gz', '.bz2', '.png',
'.jpg', '.jpeg', '.woff', '.ttf', '.eot', '.ico')
if filepath.endswith(binary_exts):
return True
if filepath.endswith(text_exts):
return False
# 无明确扩展名:读取前 8KB 检测是否含 null 字节
try:
with open(filepath, 'rb') as f:
chunk = f.read(8192)
return b'\x00' in chunk
except Exception:
return True
def fix_env(env_dir):
fixed = 0
scanned = 0
for root, dirs, files in os.walk(env_dir):
# 跳过明显的二进制/缓存目录
dirs[:] = [d for d in dirs if d not in ('__pycache__', '.git')]
for fname in files:
fpath = os.path.join(root, fname)
scanned += 1
try:
if is_probably_binary(fpath):
continue
with open(fpath, 'rb') as f:
content = f.read()
original = content
for old in OLD_PREFIXES:
content = content.replace(old, NEW_PREFIX)
if content != original:
with open(fpath, 'wb') as f:
f.write(content)
fixed += 1
except (PermissionError, OSError):
continue
print(f" 扫描 {scanned} 文件, 修改 {fixed} 文件")
return fixed
if __name__ == "__main__":
for env in sys.argv[1:]:
env_path = f"/home/xsl/miniconda3/envs/{env}"
if os.path.isdir(env_path):
print(f"[{env}] 修复中...")
fix_env(env_path)
else:
print(f"[{env}] 目录不存在,跳过")
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""为测试图自动生成发际线/额头区域的 mask(模拟用户手画遮罩)。
用 RetinaFace + 1k关键点定位额头,生成该区域的白色 mask。
"""
import os
import sys
import cv2
import numpy as np
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
sys.path.insert(0, HAIR_SERVICE_DIR)
os.environ.setdefault("HF_HUB_OFFLINE", "1")
import torch
from utils.landmark_processor import get_max_rect
def gen_hairline_mask(img_path, mask_path):
"""用关键点定位额头,生成发际线区域 mask"""
from models.detector import RetinaFaceDetector
from utils.MomocvFaceAlignment1K import MomocvFaceAlignment1K
img = cv2.imread(img_path)
if img is None:
print(f"❌ 读取失败: {img_path}")
return False
print(f"原图: {img_path} shape={img.shape}")
gpu_id = 0
# 1. RetinaFace 检测
detector = RetinaFaceDetector(gpu_id=gpu_id)
dets, landms = detector.forward(img)
if len(dets) == 0:
print("❌ 未检测到人脸")
return False
max_idx = get_max_rect(dets)
det = dets[max_idx]
print(f"检测到人脸: {det}")
# 2. 1k 关键点
aligner = MomocvFaceAlignment1K(gpu_id=gpu_id)
landmarks_1k = aligner.stable_forward(img.copy(), [det])
if landmarks_1k is None or len(landmarks_1k) == 0:
print("❌ 关键点检测失败")
return False
pts = landmarks_1k[0] # (1000, 2)
print(f"1k关键点 shape: {pts.shape}")
# 3. 构造发际线/额头 mask
# 1k点的前312点是整脸外轮廓(含发际线)
# 额头区:用眉骨以上的外轮廓点 + 发际线点构造凸包
h, w = img.shape[:2]
mask = np.zeros((h, w), dtype=np.uint8)
# 取脸部上半部分轮廓点(发际线到太阳穴)构造额头区域
# 索引 0-311 是脸部外轮廓,取上半部分(额头/太阳穴)
forehead_pts = []
# 发际线区域:取轮廓的上半段 + 稍微往下扩展
for i in range(0, 312, 3): # 隔点采样,减少密度
pt = pts[i]
# 只取上半脸(y < 图像中线偏上)
if pt[1] < h * 0.55:
forehead_pts.append(pt)
# 加入额头中心区域一些点,确保覆盖额头
forehead_pts = np.array(forehead_pts, dtype=np.int32)
if len(forehead_pts) < 3:
print("❌ 额头点不足")
return False
# 凸包填充
hull = cv2.convexHull(forehead_pts)
cv2.fillConvexPoly(mask, hull, 255)
# 膨胀让区域稍大一点(模拟用户涂抹)
mask = cv2.dilate(mask, np.ones((5, 5), np.uint8), iterations=2)
# 统计 mask 区域
nonzero = cv2.countNonZero(mask)
print(f"mask 生成完成: 白色区域={nonzero}px ({nonzero/(h*w)*100:.1f}%)")
cv2.imwrite(mask_path, mask)
print(f"✅ mask 已保存: {mask_path}")
return True
if __name__ == "__main__":
img_path = sys.argv[1] if len(sys.argv) > 1 else \
"/home/xsl/change_hair/project/data/userImage/8488902485_20250630055547.jpg"
mask_path = sys.argv[2] if len(sys.argv) > 2 else \
"/home/xsl/change_hair/project/logs/hairgrow_test_mask.png"
gen_hairline_mask(img_path, mask_path)
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""批量生成发型预览图
用 boy.png 给男款发型、girl.png 给女款发型跑换发型服务,
生成每个发型套在标准脸型上的效果图,存为缩略图供前端展示。
用法: python gen_hairstyle_previews.py [--limit N] (limit用于测试)
"""
import os
import sys
import json
import time
import base64
import requests
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
sys.path.insert(0, HAIR_SERVICE_DIR)
os.chdir(HAIR_SERVICE_DIR)
BOY_IMG = "/home/xsl/change_hair/images/boy.png"
GIRL_IMG = "/home/xsl/change_hair/images/girl.png"
HAIRSTYLE_DIR = "/home/xsl/change_hair/project/data/ref_hairstyle"
TRAIN_DIR = "/home/xsl/change_hair/data/train_material"
PREVIEW_DIR = "/home/xsl/change_hair/hair_grow_service/static/previews" # 预览图存储目录
SWAP_API = "http://127.0.0.1:8801/api/swapHair/v1"
def load_hairstyles():
"""返回 {hair_id: gender}"""
styles = {}
for hid in os.listdir(HAIRSTYLE_DIR):
cfg = os.path.join(HAIRSTYLE_DIR, hid, "config.json")
lora = os.path.join(TRAIN_DIR, hid, "model", "hairstyle_hd_lora.safetensors")
if not (os.path.exists(cfg) and os.path.exists(lora)):
continue
g = json.load(open(cfg)).get("gender", "?")
styles[hid] = g
return styles
def gen_one(hair_id, gender, img_path):
"""给一个发型生成预览图,返回是否成功"""
out_path = os.path.join(PREVIEW_DIR, f"{hair_id}.jpg")
if os.path.exists(out_path):
return True, "已存在跳过"
with open(img_path, "rb") as f:
img_b64 = "data:image/png;base64," + base64.b64encode(f.read()).decode()
payload = {
"hair_id": hair_id,
"task_id": f"preview_{hair_id}",
"is_hr": "false", # 非高清,快一点
"user_img_path": img_b64,
"output_format": "base64"
}
try:
r = requests.post(SWAP_API, json=payload, timeout=180)
d = r.json()
if d.get("state") == 0 and d.get("data"):
import cv2
import numpy as np
img = cv2.imdecode(np.frombuffer(base64.b64decode(d["data"]), np.uint8), cv2.IMREAD_COLOR)
# 缩小到 300x400 当缩略图
if img is not None:
h, w = img.shape[:2]
scale = min(300 / w, 400 / h)
img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
cv2.imwrite(out_path, img, [cv2.IMWRITE_JPEG_QUALITY, 85])
return True, "生成成功"
return False, f"state={d.get('state')} msg={d.get('msg','')}"
except Exception as e:
return False, str(e)
def main():
import cv2
limit = None
if "--limit" in sys.argv:
limit = int(sys.argv[sys.argv.index("--limit") + 1])
os.makedirs(PREVIEW_DIR, exist_ok=True)
styles = load_hairstyles()
print(f"{len(styles)} 个发型需要生成预览图")
# 按性别选基准图
boy_styles = [(h, g) for h, g in styles.items() if g == "boy"]
girl_styles = [(h, g) for h, g in styles.items() if g == "girl"]
print(f" boy: {len(boy_styles)} (用 boy.png)")
print(f" girl: {len(girl_styles)} (用 girl.png)")
tasks = boy_styles + girl_styles
if limit:
tasks = tasks[:limit]
print(f" [测试模式] 只处理前 {limit}")
ok, fail, skip = 0, 0, 0
t0 = time.time()
for i, (hair_id, gender) in enumerate(tasks):
img_path = BOY_IMG if gender == "boy" else GIRL_IMG
out_path = os.path.join(PREVIEW_DIR, f"{hair_id}.jpg")
if os.path.exists(out_path):
skip += 1
continue
elapsed = time.time() - t0
eta = elapsed / max(ok + fail, 1) * (len(tasks) - i) if (ok + fail) > 0 else 0
print(f"[{i+1}/{len(tasks)}] {hair_id}({gender})...", end=" ", flush=True)
success, msg = gen_one(hair_id, gender, img_path)
if success:
ok += 1
print(f"✓ ({msg})")
else:
fail += 1
print(f"✗ ({msg})")
print(f"\n=== 完成: 成功{ok} 失败{fail} 跳过{skip} 总耗时{time.time()-t0:.0f}s ===")
print(f"预览图目录: {PREVIEW_DIR}")
if __name__ == "__main__":
main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""为新发型生成 hair_template_materialfirst##_matting.png + .pkl
换发型功能4 需要:
hair_template_material/<hair_id>/first##<name>.png 原图
hair_template_material/<hair_id>/first##<name>_matting.png 头发抠图mask
hair_template_material/<hair_id>/first##<name>.pkl 1k关键点
用法:
cd /home/xsl/change_hair/project/hair_service_sd
python gen_template_material.py --hair-id new_test_001 --img /path/to/template.jpg
"""
import os
import sys
import ssl
import uuid
import pickle
import argparse
ssl._create_default_https_context = ssl._create_unverified_context
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
os.chdir(HAIR_SERVICE_DIR)
sys.path.insert(0, HAIR_SERVICE_DIR)
os.environ.setdefault("HF_HUB_OFFLINE", "1")
import cv2
import numpy as np
import torch
_orig_torch_load = torch.load
def _patched_torch_load(*args, **kwargs):
if 'map_location' in kwargs and callable(kwargs['map_location']) and not isinstance(kwargs['map_location'], str):
kwargs['map_location'] = 'cpu'
return _orig_torch_load(*args, **kwargs)
torch.load = _patched_torch_load
from models.detector import RetinaFaceDetector
from models.MomocvFaceAlignment1K import MomocvFaceAlignment1K
from utils.landmark_processor import get_max_rect
from core.process_modules import Generator_Matte
from common.logger import config
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--hair-id", required=True)
parser.add_argument("--img", required=True, help="模板图(发型图)")
args = parser.parse_args()
template_dir = config.get('default', 'hair_template_material_dir')
out_dir = os.path.join(template_dir, args.hair_id)
os.makedirs(out_dir, exist_ok=True)
img = cv2.imread(args.img)
if img is None:
print(f"✗ 读取失败: {args.img}"); sys.exit(1)
print(f"模板图: {img.shape}")
# 1. 人脸检测 + 1k关键点
print("加载模型...")
detector = RetinaFaceDetector(gpu_id=0)
aligner = MomocvFaceAlignment1K(gpu_id=0)
matte_gen = Generator_Matte(gpu=True, device_id=0)
print("模型加载完成")
dets, landms = detector.forward(img)
if len(dets) == 0:
print("✗ 未检测到人脸"); sys.exit(1)
det = dets[get_max_rect(dets)]
landmarks_1k = aligner.stable_forward(img.copy(), [det])
pt1k = landmarks_1k[0]
# 2. 头发抠图
with torch.no_grad():
_, alpha, _ = matte_gen.matte_inference(img, pt1k)
if alpha.shape != img.shape[:2]:
alpha = cv2.resize(alpha, (img.shape[1], img.shape[0]))
# 3. 保存三个文件(first##前缀)
file_uuid = f"first##{uuid.uuid4()}"
png_path = os.path.join(out_dir, f"{file_uuid}.png")
matting_path = os.path.join(out_dir, f"{file_uuid}_matting.png")
pkl_path = os.path.join(out_dir, f"{file_uuid}.pkl")
cv2.imwrite(png_path, img)
cv2.imwrite(matting_path, alpha)
with open(pkl_path, 'wb') as f:
# 存 numpy array(不能用 tolist(),代码用 landmarks[index, :] 索引需要 ndarray
pickle.dump({'human_pt1k': np.asarray(pt1k, dtype=np.float32)}, f)
print(f"✓ 生成完成:")
print(f" {png_path}")
print(f" {matting_path}")
print(f" {pkl_path}")
if __name__ == "__main__":
main()
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""区域生发 - 独立验证脚本
不依赖 hair_service 主服务,直接调用 hair_grow.hair_grow()。
需要在 hair_service_sd 目录下运行(依赖其模块导入)。
用法:
cd /home/xsl/change_hair/project/hair_service_sd
/home/xsl/miniconda3/envs/my_hair/bin/python /home/xsl/change_hair/hair_grow_cli.py \
--img test.jpg --mask mask.png --strength 0.5 -o result.jpg
# 批量跑三档强度对比
/home/xsl/miniconda3/envs/my_hair/bin/python /home/xsl/change_hair/hair_grow_cli.py \
--img test.jpg --mask mask.png --compare
"""
import os
import sys
import cv2
import argparse
# 把 hair_service_sd 加入路径,使其模块可被导入
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
sys.path.insert(0, HAIR_SERVICE_DIR)
# 设置离线模式(本机无法访问 huggingface.co
os.environ.setdefault("HF_HUB_OFFLINE", "1")
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
def run_once(img_path, mask_path, strength, out_path):
"""单次生发"""
from hair_grow import hair_grow
img = cv2.imread(img_path)
mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
if img is None:
print(f"❌ 无法读取图片: {img_path}")
return False
if mask is None:
print(f"❌ 无法读取mask: {mask_path}")
return False
print(f"输入: img={img.shape}, mask={mask.shape}, strength={strength}")
result = hair_grow(img, mask, strength=strength)
cv2.imwrite(out_path, result)
print(f"✅ 生发结果已保存: {out_path}")
return True
def run_compare(img_path, mask_path, out_dir):
"""三档强度对比"""
os.makedirs(out_dir, exist_ok=True)
for strength in [0.2, 0.5, 0.8]:
out_path = os.path.join(out_dir, f"result_s{strength}.jpg")
print(f"\n{'='*50}")
print(f"生发强度 strength={strength}")
print(f"{'='*50}")
if not run_once(img_path, mask_path, strength, out_path):
return False
print(f"\n✅ 三档对比完成,结果在: {out_dir}")
print(" - result_s0.2.jpg (低强度,轻微生发)")
print(" - result_s0.5.jpg (中强度,明显生发)")
print(" - result_s0.8.jpg (高强度,浓密生发)")
return True
def main():
parser = argparse.ArgumentParser(description="区域生发验证脚本")
parser.add_argument("--img", required=True, help="人头像图片路径")
parser.add_argument("--mask", required=True, help="遮罩图片路径(白=生发区)")
parser.add_argument("--strength", type=float, default=0.5,
help="生发强度 0.1~1.0 (默认0.5)")
parser.add_argument("-o", "--output", default="result_hairgrow.jpg",
help="输出图片路径")
parser.add_argument("--compare", action="store_true",
help="跑三档强度(0.2/0.5/0.8)对比")
args = parser.parse_args()
if args.compare:
out_dir = os.path.dirname(args.output) or "."
run_compare(args.img, args.mask, out_dir)
else:
run_once(args.img, args.mask, args.strength, args.output)
if __name__ == "__main__":
main()
+68
View File
@@ -0,0 +1,68 @@
# 区域生发测试服务
提供带「画笔涂抹遮罩」的 Web 测试页面,在用户涂抹的区域内生发。
## 快速开始
### 1. 确认 webui 已启动(生发依赖 SD inpainting
```bash
ss -tlnp | grep 57860 # 确认 webui 在跑
bash /home/xsl/change_hair/start_all.sh status # 看 webui 是否就绪
```
### 2. 启动生发服务(端口 8899)
```bash
nohup /home/xsl/change_hair/start_hairgrow.sh > /home/xsl/change_hair/project/logs/hair_grow_service.log 2>&1 &
```
### 3. 打开测试页面
浏览器访问:
```
http://172.21.246.70:8899
```
(如从 Windows 宿主机访问 WSL,用上面地址;或用 localhost:8899
### 4. 使用流程
1. **上传图片**:点击或拖拽一张人头像照片(正面、发际线/稀疏区清晰)
2. **涂抹遮罩**:用画笔在需要生发的区域涂抹(白色 = 生发区)
- 左侧可调画笔粗细、切换橡皮、清空
3. **调强度**:生发强度滑块(0.1~1.0,默认 0.5
- 低强度:轻微生发,严格保持遮罩边界
- 高强度:浓密生发,边缘自然过渡
4. **点「生成」**:等待约 5 秒
5. **看结果**:原图与生发结果并排对比,可下载
## 文件说明
```
hair_grow_service/
├── app.py # Flask 服务(端口 8899
├── static/index.html # 测试页面(画笔遮罩 + 结果对比)
└── README.md
start_hairgrow.sh # 启动脚本
```
## API 接口(可单独调用)
```
POST http://<host>:8899/api/grow
Content-Type: application/json
{
"img": "data:image/jpeg;base64,...", # 原图 base64
"mask": "data:image/png;base64,...", # 遮罩 base64,白色(255)=生发区,与img同分辨率
"strength": 0.5 # 生发强度 0.1~1.0
}
→ {"state": 0, "result": "<base64结果图>"}
{"state": -1, "msg": "错误信息"}
```
## 停止服务
```bash
pkill -f "hair_grow_service/app.py"
```
## 注意事项
- 生发服务**依赖 webui(57860)**webui 未启动会报连接失败
- 图片过大(>1024px)会自动缩放并对齐到 8 的倍数
- 生发结果是确定性的(固定 seed=123456789),同输入同输出
+515
View File
@@ -0,0 +1,515 @@
# -*- coding: utf-8 -*-
"""区域生发服务(走换发型工作流,端口 8888)
工作流与换发型一致:infer_hairstyle_diy_jy → warpAffine → photo_service+LoRA → webui → 贴回。
mask = origin_matting new_matting 手绘mask(不减刘海)。
启动: python app.py
页面: http://<host>:8888
"""
import os
import sys
import time
import base64
import traceback
# 把 hair_service_sd 加入 path,使其模块可被导入
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
sys.path.insert(0, HAIR_SERVICE_DIR)
# 切换到 hair_service_sd 目录,使 common.logger 能读到 config/configure.ini(相对路径)
os.chdir(HAIR_SERVICE_DIR)
os.environ.setdefault("HF_HUB_OFFLINE", "1")
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
os.environ.setdefault("CRYPTOGRAPHY_OPENSSL_NO_LEGACY", "1")
import cv2
import numpy as np
from flask import Flask, request, jsonify, send_from_directory
from gevent import pywsgi
PORT = 8888
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, static_folder="static", static_url_path="/static")
# ===== 模型懒加载(首次请求时初始化,避免启动卡死)=====
_hairstyle_process = None
_landmark_processor = None
def _get_models():
"""懒加载换发型全套模型"""
global _hairstyle_process, _landmark_processor
if _hairstyle_process is None:
print("[init] 加载换发型模型(首次,约30-90秒)...")
t0 = time.time()
import torch
from core.hairstyle_model import HairStyle_Model
from utils import landmark_processor
_hairstyle_process = HairStyle_Model(gpu=True, use_enhance=True)
_landmark_processor = landmark_processor
print(f"[init] 模型加载完成,耗时 {time.time()-t0:.1f}s")
return _hairstyle_process, _landmark_processor
@app.route("/")
def index():
return send_from_directory(os.path.join(BASE_DIR, "static"), "index.html")
@app.route("/swap")
def swap_page():
"""换发型测试页(独立,无遮罩)"""
return send_from_directory(os.path.join(BASE_DIR, "static"), "swap.html")
@app.route("/test_new")
def test_new_page():
"""新发型效果测试页(聚焦本次训练的30款发型)"""
return send_from_directory(os.path.join(BASE_DIR, "static"), "test_new.html")
@app.route("/debug")
def debug_page():
"""换发型调试页(全参数可视化)"""
return send_from_directory(os.path.join(BASE_DIR, "static"), "debug.html")
@app.route("/hairline")
def hairline_page():
"""发际线带重绘实验页"""
return send_from_directory(os.path.join(BASE_DIR, "static"), "hairline.html")
@app.route("/manual")
def manual_page():
"""手绘mask重绘测试页"""
return send_from_directory(os.path.join(BASE_DIR, "static"), "manual.html")
@app.route("/api/swap", methods=["POST"])
def api_swap_proxy():
"""代理换发型请求到 8801(避免前端跨域问题)"""
import requests as req
try:
data = request.json
resp = req.post("http://127.0.0.1:8801/api/swapHair/v1",
json=data, timeout=600)
return jsonify(resp.json())
except Exception as e:
return jsonify({"state": -1, "msg": f"换发型代理失败: {e}"}), 500
@app.route("/api/swap_viz", methods=["POST"])
def api_swap_viz():
"""换发型(带可视化中间产物)
入参 JSON:
img: 原图 base64
hair_id: 发型ID
is_hr: "true"/"false"
返回:
{state, result: 最终图b64, steps: [{title, desc, images:[{label, b64}]}]}
"""
t0 = time.time()
try:
data = request.json
img_b64 = data.get("img", "")
hair_id = data.get("hair_id", "")
is_hr = str(data.get("is_hr", "false")).lower() == "true"
strict_mask = data.get("strict_mask", False)
if not img_b64 or not hair_id:
return jsonify({"state": -1, "msg": "img 和 hair_id 不能为空"}), 400
img = _b64_to_ndarray(img_b64, color=True)
if img is None:
return jsonify({"state": -1, "msg": "img 解析失败"}), 400
print(f"[swap_viz] img={img.shape}, hair_id={hair_id}, is_hr={is_hr}, strict_mask={strict_mask}")
hairstyle_process, landmark_processor = _get_models()
from hair_swap_viz import hair_swap_viz
task_id = f"swapviz_{int(time.time()*1000)}"
result, steps = hair_swap_viz(
origin_img=img, hair_id=hair_id,
hairstyle_process=hairstyle_process,
landmark_processor=landmark_processor,
task_id=task_id, is_hr=is_hr, strict_mask=strict_mask)
_, buf = cv2.imencode(".jpg", result, [cv2.IMWRITE_JPEG_QUALITY, 95])
result_b64 = base64.b64encode(buf).decode("utf-8")
print(f"[swap_viz] 完成,总耗时 {time.time()-t0:.1f}s{len(steps)}个步骤")
return jsonify({"state": 0, "result": result_b64, "steps": steps})
except Exception as e:
print(f"[swap_viz] 失败: {e}")
traceback.print_exc()
return jsonify({"state": -1, "msg": f"换发型失败: {e}"}), 500
@app.route("/api/swap_debug", methods=["POST"])
def api_swap_debug():
"""换发型调试接口(全参数可调 + 每步可视化)。
入参 JSON:
img: 原图 base64
hair_id: 发型ID
# 流程开关
cut_bang: bool, 是否减刘海圆(默认 true)
strict_mask: bool, 严格按mask贴回(默认 false
seamless_blend: bool, 泊松融合消除接缝(默认 true,仅 strict_mask 时生效)
# 尺寸/对齐
is_hr: bool, 高清模式(默认 true
dilate_kernel: [x,y], mask膨胀核(默认 [6,18]
# SD 推理(仅 denoising 可调)
denoising_strength: float 0~1, 重绘强度(默认 0.6
# 贴回/融合
blend_dilate: [x,y], strict贴回mask膨胀(默认 [5,5]
seamless_dilate: [x,y], 泊松融合mask膨胀(默认 [9,9])
返回:
{state, result, steps, params}
"""
t0 = time.time()
try:
d = request.json
img_b64 = d.get("img", "")
hair_id = d.get("hair_id", "")
if not img_b64 or not hair_id:
return jsonify({"state": -1, "msg": "img 和 hair_id 不能为空"}), 400
img = _b64_to_ndarray(img_b64, color=True)
if img is None:
return jsonify({"state": -1, "msg": "img 解析失败"}), 400
# 解析参数(带默认值 + 类型转换)
def g(k, default): return d.get(k, default)
dk = g("dilate_kernel", [6, 18])
bd = g("blend_dilate", [5, 5])
sd = g("seamless_dilate", [9, 9])
print(f"[swap_debug] hair_id={hair_id}, cut_bang={g('cut_bang',True)}, "
f"strict_mask={g('strict_mask',False)}, seamless_blend={g('seamless_blend',True)}, "
f"is_hr={g('is_hr',True)}, denoising={g('denoising_strength',0.6)}")
hairstyle_process, landmark_processor = _get_models()
from hair_swap_debug import hair_swap_debug
task_id = f"swapdbg_{int(time.time()*1000)}"
result, steps, params = hair_swap_debug(
origin_img=img, hair_id=hair_id,
hairstyle_process=hairstyle_process,
landmark_processor=landmark_processor,
task_id=task_id,
cut_bang=bool(g("cut_bang", True)),
strict_mask=bool(g("strict_mask", False)),
seamless_blend=bool(g("seamless_blend", True)),
is_hr=bool(g("is_hr", True)),
dilate_kernel=(int(dk[0]), int(dk[1])),
denoising_strength=float(g("denoising_strength", 0.6)),
blend_dilate=(int(bd[0]), int(bd[1])),
seamless_dilate=(int(sd[0]), int(sd[1])),
)
_, buf = cv2.imencode(".jpg", result, [cv2.IMWRITE_JPEG_QUALITY, 95])
result_b64 = base64.b64encode(buf).decode("utf-8")
print(f"[swap_debug] 完成,总耗时 {time.time()-t0:.1f}s")
return jsonify({"state": 0, "result": result_b64, "steps": steps, "params": params})
except Exception as e:
print(f"[swap_debug] 失败: {e}")
traceback.print_exc()
return jsonify({"state": -1, "msg": f"换发型失败: {e}"}), 500
@app.route("/api/swap_hairline", methods=["POST"])
def api_swap_hairline():
"""发际线带重绘接口(实验性,流程同 swap_debug,仅步骤③改为边界带)。
比 swap_debug 多一个参数:
method: str, 发际线mask方案,可选 boundary_band/mediapipe/landmark_1k/deeplab(默认 mediapipe
band_width: int, 边界带宽度(形态学核大小,默认15,带宽≈2*band_width
其余参数同 swap_debug。
"""
t0 = time.time()
try:
d = request.json
img_b64 = d.get("img", "")
hair_id = d.get("hair_id", "")
if not img_b64 or not hair_id:
return jsonify({"state": -1, "msg": "img 和 hair_id 不能为空"}), 400
img = _b64_to_ndarray(img_b64, color=True)
if img is None:
return jsonify({"state": -1, "msg": "img 解析失败"}), 400
def g(k, default): return d.get(k, default)
dk = g("dilate_kernel", [6, 18])
bd = g("blend_dilate", [5, 5])
sd = g("seamless_dilate", [9, 9])
print(f"[swap_hairline] hair_id={hair_id}, method={g('method','mediapipe')}, "
f"band_width={g('band_width',15)}, denoising={g('denoising_strength',0.6)}")
hairstyle_process, landmark_processor = _get_models()
from hair_swap_hairline import hair_swap_hairline
task_id = f"hairline_{int(time.time()*1000)}"
result, steps, params = hair_swap_hairline(
origin_img=img, hair_id=hair_id,
hairstyle_process=hairstyle_process,
landmark_processor=landmark_processor,
task_id=task_id,
method=str(g("method", "mediapipe")),
strict_mask=bool(g("strict_mask", False)),
seamless_blend=bool(g("seamless_blend", True)),
is_hr=bool(g("is_hr", True)),
dilate_kernel=(int(dk[0]), int(dk[1])),
denoising_strength=float(g("denoising_strength", 0.6)),
blend_dilate=(int(bd[0]), int(bd[1])),
seamless_dilate=(int(sd[0]), int(sd[1])),
band_width=int(g("band_width", 15)),
preview_only=bool(g("preview_only", False)),
height_ratio=float(g("height_ratio", 0.432)),
width_ratio=float(g("width_ratio", 0.144)),
corner_ratio=float(g("corner_ratio", 0.25)),
vertical_offset=float(g("vertical_offset", 0.0)),
refiner_switch_at=float(g("refiner_switch_at", 0.5)),
)
_, buf = cv2.imencode(".jpg", result, [cv2.IMWRITE_JPEG_QUALITY, 95])
result_b64 = base64.b64encode(buf).decode("utf-8")
print(f"[swap_hairline] 完成,总耗时 {time.time()-t0:.1f}s")
return jsonify({"state": 0, "result": result_b64, "steps": steps, "params": params})
except Exception as e:
print(f"[swap_hairline] 失败: {e}")
traceback.print_exc()
return jsonify({"state": -1, "msg": f"发际线带重绘失败: {e}"}), 500
@app.route("/api/swap_manual", methods=["POST"])
def api_swap_manual():
"""手绘mask版换发型接口(重绘区域完全由用户手绘mask决定)。
入参 JSON:
img: 原图 base64
mask: 用户手绘mask base64(白色=重绘区,与img同分辨率)
hair_id: 发型ID
其余参数同 swap_debugstrict_mask/seamless_blend/is_hr/dilate_kernel/
denoising_strength/blend_dilate/seamless_dilate
返回: {state, result, steps, params}
"""
t0 = time.time()
try:
d = request.json
img_b64 = d.get("img", "")
mask_b64 = d.get("mask", "")
hair_id = d.get("hair_id", "")
if not img_b64 or not mask_b64:
return jsonify({"state": -1, "msg": "img 和 mask 不能为空(请先手绘重绘区域)"}), 400
if not hair_id:
return jsonify({"state": -1, "msg": "hair_id 不能为空"}), 400
img = _b64_to_ndarray(img_b64, color=True)
hand_mask = _b64_to_ndarray(mask_b64, color=False)
if img is None:
return jsonify({"state": -1, "msg": "img 解析失败"}), 400
if hand_mask is None:
return jsonify({"state": -1, "msg": "mask 解析失败"}), 400
def g(k, default): return d.get(k, default)
dk = g("dilate_kernel", [6, 18])
bd = g("blend_dilate", [5, 5])
sd = g("seamless_dilate", [9, 9])
print(f"[swap_manual] hair_id={hair_id}, img={img.shape}, mask={hand_mask.shape}, "
f"strict_mask={g('strict_mask',False)}, denoising={g('denoising_strength',0.6)}")
hairstyle_process, landmark_processor = _get_models()
from hair_swap_manual import hair_swap_manual
task_id = f"manual_{int(time.time()*1000)}"
result, steps, params = hair_swap_manual(
origin_img=img, hand_mask=hand_mask, hair_id=hair_id,
hairstyle_process=hairstyle_process,
landmark_processor=landmark_processor,
task_id=task_id,
strict_mask=bool(g("strict_mask", False)),
seamless_blend=bool(g("seamless_blend", True)),
is_hr=bool(g("is_hr", True)),
dilate_kernel=(int(dk[0]), int(dk[1])),
denoising_strength=float(g("denoising_strength", 0.6)),
blend_dilate=(int(bd[0]), int(bd[1])),
seamless_dilate=(int(sd[0]), int(sd[1])),
feather_px=int(g("feather_px", 0)),
enhance=bool(g("enhance", False)),
enhance_denoising=float(g("enhance_denoising", 0.35)),
)
_, buf = cv2.imencode(".jpg", result, [cv2.IMWRITE_JPEG_QUALITY, 95])
result_b64 = base64.b64encode(buf).decode("utf-8")
print(f"[swap_manual] 完成,总耗时 {time.time()-t0:.1f}s")
return jsonify({"state": 0, "result": result_b64, "steps": steps, "params": params})
except Exception as e:
print(f"[swap_manual] 失败: {e}")
traceback.print_exc()
return jsonify({"state": -1, "msg": f"手绘mask换发型失败: {e}"}), 500
def _b64_to_ndarray(b64_str, color=True):
if "," in b64_str and b64_str.startswith("data:"):
b64_str = b64_str.split(",", 1)[1]
data = base64.b64decode(b64_str)
flag = cv2.IMREAD_COLOR if color else cv2.IMREAD_GRAYSCALE
return cv2.imdecode(np.frombuffer(data, np.uint8), flag)
@app.route("/api/hairstyles")
def api_hairstyles():
"""返回可用发型列表(含性别,用于前端下拉框分组)"""
try:
from common.logger import config
hairstyle_dir = config.get('default', 'hairstyleDir')
train_dir = config.get('default', 'train_dir')
upload_dir = config.get('default', 'upload_train_dir')
from hair_grow_swap import list_hairstyles
styles = list_hairstyles(hairstyle_dir, train_dir, upload_dir)
boy = [s for s in styles if s["gender"] == "boy"]
girl = [s for s in styles if s["gender"] == "girl"]
print(f"[hairstyles] 共 {len(styles)} 个可用发型 (boy={len(boy)}, girl={len(girl)})")
return jsonify({"state": 0, "data": styles, "count": len(styles)})
except Exception as e:
traceback.print_exc()
return jsonify({"state": -1, "msg": str(e)}), 500
@app.route("/preview/<hair_id>")
def preview_img(hair_id):
"""直接返回预览图文件(比 base64 API 快,浏览器可缓存)"""
effect_path = os.path.join(BASE_DIR, "static", "previews", f"{hair_id}.jpg")
if os.path.exists(effect_path):
return send_from_directory(os.path.join(BASE_DIR, "static", "previews"), f"{hair_id}.jpg")
# 回退到 ref_rgb
try:
from common.logger import config
hairstyle_dir = config.get('default', 'hairstyleDir')
fallback = os.path.join(hairstyle_dir, hair_id, "ref_rgb_8uc3_768.png")
if os.path.exists(fallback):
return send_file_or_404(fallback)
except Exception:
pass
return ("", 404)
@app.route("/train_src/<hair_id>")
def train_src_img(hair_id):
"""返回发型的训练原图(hair_type_images/<hair_id>.jpg/.png)。
用于测试页展示发型真实样子,而非套在标准脸上的效果图。
"""
src_dir = "/home/xsl/change_hair/hair_type_images"
for ext in (".jpg", ".jpeg", ".png"):
path = os.path.join(src_dir, hair_id + ext)
if os.path.exists(path):
return send_from_directory(src_dir, hair_id + ext)
return ("", 404)
@app.route("/api/hairstyle_preview/<hair_id>")
def api_hairstyle_preview(hair_id):
"""返回某发型的预览图。
优先用生成的效果图 static/previews/<hair_id>.jpg(发型套在标准脸上的样子),
没有则回退到 first## 原始上传图。
"""
try:
# 1. 优先:生成的效果图(发型套在 boy/girl 标准脸上)
preview = None
effect_path = os.path.join(BASE_DIR, "static", "previews", f"{hair_id}.jpg")
if os.path.exists(effect_path):
preview = effect_path
else:
# 2. 回退:first## 原始上传图
from common.logger import config
upload_dir = config.get('default', 'upload_train_dir')
save_dir = os.path.join(upload_dir, hair_id)
if os.path.isdir(save_dir):
for name in os.listdir(save_dir):
if "first##" in name:
preview = os.path.join(save_dir, name)
break
# 3. 再回退:ref_rgb
if not preview or not os.path.exists(preview):
hairstyle_dir = config.get('default', 'hairstyleDir')
fallback = os.path.join(hairstyle_dir, hair_id, "ref_rgb_8uc3_768.png")
if os.path.exists(fallback):
preview = fallback
if not preview:
return jsonify({"state": -1, "msg": "无预览图"}), 404
img = cv2.imread(preview)
img = cv2.resize(img, (256, 256), interpolation=cv2.INTER_AREA)
_, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 85])
b64 = base64.b64encode(buf).decode()
return jsonify({"state": 0, "preview": b64})
except Exception as e:
return jsonify({"state": -1, "msg": str(e)}), 500
@app.route("/api/grow", methods=["POST"])
def api_grow():
"""生发接口(走换发型工作流)
入参 JSON:
img: 原图 base64
mask: 手绘遮罩 base64(白=生发区,与img同分辨率)
hair_id: 选择的发型ID(必填)
is_hr: 是否高清 "true"/"false",默认 "true"
"""
t0 = time.time()
try:
data = request.json
img_b64 = data.get("img", "")
mask_b64 = data.get("mask", "")
hair_id = data.get("hair_id", "")
is_hr = str(data.get("is_hr", "true")).lower() == "true"
if not img_b64 or not mask_b64:
return jsonify({"state": -1, "msg": "img 和 mask 不能为空"}), 400
if not hair_id:
return jsonify({"state": -1, "msg": "hair_id 不能为空(请先选择发型)"}), 400
img = _b64_to_ndarray(img_b64, color=True)
mask = _b64_to_ndarray(mask_b64, color=False)
if img is None:
return jsonify({"state": -1, "msg": "img 解析失败"}), 400
if mask is None:
return jsonify({"state": -1, "msg": "mask 解析失败"}), 400
print(f"[grow] img={img.shape}, mask={mask.shape}, hair_id={hair_id}, is_hr={is_hr}")
# 加载模型(首次慢)
hairstyle_process, landmark_processor = _get_models()
# 调用生发(走换发型工作流)
from hair_grow_swap import hair_grow_swap
task_id = f"grow_{int(time.time()*1000)}"
result = hair_grow_swap(
origin_img=img, hand_mask=mask, hair_id=hair_id,
hairstyle_process=hairstyle_process,
landmark_processor=landmark_processor,
task_id=task_id, is_hr=is_hr)
_, buf = cv2.imencode(".jpg", result, [cv2.IMWRITE_JPEG_QUALITY, 95])
result_b64 = base64.b64encode(buf).decode("utf-8")
print(f"[grow] 完成,总耗时 {time.time()-t0:.1f}s")
return jsonify({"state": 0, "result": result_b64})
except Exception as e:
print(f"[grow] 失败: {e}")
traceback.print_exc()
return jsonify({"state": -1, "msg": f"生发失败: {e}"}), 500
if __name__ == "__main__":
print(f"生发服务启动,端口 {PORT}")
print(f"测试页面: http://0.0.0.0:{PORT}")
print(f"注意:首次请求会加载换发型模型(约30-60秒)")
server = pywsgi.WSGIServer(("0.0.0.0", PORT), app)
server.serve_forever()
+379
View File
@@ -0,0 +1,379 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>生发调试台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; font-size: 14px; }
.header { background: #16213e; padding: 16px 28px; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 19px; font-weight: 600; }
.header p { font-size: 12px; color: #888; margin-top: 3px; }
.header .links a { color: #4ecca3; font-size: 12px; text-decoration: none; margin-left: 12px; }
.container { display: flex; gap: 18px; padding: 18px; max-width: 1800px; margin: 0 auto; }
/* 左侧参数面板 */
.panel { width: 340px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 16px; height: fit-content; max-height: 92vh; overflow-y: auto; }
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.panel h3 { font-size: 13px; color: #4ecca3; margin: 14px 0 8px; text-transform: uppercase; letter-spacing: 1px; padding-bottom: 5px; border-bottom: 1px solid #0f3460; }
.panel h3:first-child { margin-top: 0; }
.field { margin-bottom: 12px; }
.field-label { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #bbb; margin-bottom: 5px; }
.field-label .val { color: #4ecca3; font-family: monospace; font-size: 11px; }
.field-label .tip { color: #666; font-size: 10px; margin-left: 4px; cursor: help; }
.field input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: #0f3460; border-radius: 2px; outline: none; }
.field input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #4ecca3; cursor: pointer; }
.field input[type="number"] { width: 100%; padding: 5px 8px; background: #0d1b3e; border: 1px solid #0f3460; border-radius: 4px; color: #eee; font-size: 12px; }
.field select { width: 100%; padding: 5px 8px; background: #0d1b3e; border: 1px solid #0f3460; border-radius: 4px; color: #eee; font-size: 12px; }
.switch { display: flex; align-items: center; justify-content: space-between; padding: 7px 0; font-size: 13px; color: #ccc; cursor: pointer; }
.switch:hover { color: #eee; }
.toggle { position: relative; width: 36px; height: 20px; background: #0f3460; border-radius: 10px; transition: .2s; flex-shrink: 0; }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #888; border-radius: 50%; transition: .2s; }
.switch.on .toggle { background: #4ecca3; }
.switch.on .toggle::after { left: 18px; background: #16213e; }
.pair { display: flex; gap: 8px; }
.pair .field { flex: 1; }
.hint { font-size: 11px; color: #666; line-height: 1.5; margin-top: 8px; padding: 8px 10px; background: #0d1b3e; border-radius: 5px; }
.hint b { color: #aaa; }
.readonly-info { font-size: 11px; color: #777; padding: 8px 10px; background: #0d1b3e; border-radius: 5px; line-height: 1.6; }
.readonly-info code { color: #aaa; background: #16213e; padding: 1px 5px; border-radius: 3px; }
.btn { display: block; width: 100%; padding: 10px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: .15s; }
.btn-primary { background: #4ecca3; color: #16213e; font-weight: 600; font-size: 15px; padding: 12px; margin-top: 8px; }
.btn-primary:hover { background: #6ee0bd; }
.btn-primary:disabled { background: #555; color: #999; cursor: not-allowed; }
.btn-ghost { background: #0f3460; color: #eee; margin-top: 6px; }
.btn-ghost:hover { background: #1a4a80; }
/* 中间发型选择 */
.mid-panel { width: 200px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 14px; height: fit-content; max-height: 92vh; overflow-y: auto; }
.mid-panel h3 { font-size: 12px; color: #4ecca3; margin-bottom: 10px; }
.hairstyle-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.hairstyle-item { position: relative; cursor: pointer; border-radius: 5px; overflow: hidden; border: 2px solid transparent; aspect-ratio: 1; background: #0d1b3e; }
.hairstyle-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hairstyle-item.ready { border-color: #2a5a4a; }
.hairstyle-item.selected { border-color: #4ecca3; }
.hairstyle-item.selected::after { content: '✓'; position: absolute; top: 1px; right: 3px; color: #4ecca3; font-weight: bold; text-shadow: 0 0 3px #000; font-size: 11px; }
.hairstyle-item .name { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); color: #fff; font-size: 10px; padding: 8px 2px 2px; text-align: center; }
.hairstyle-item.pending { opacity: 0.4; cursor: not-allowed; }
/* 右侧工作区 */
.workspace { flex: 1; min-width: 0; }
.canvas-wrap { background: #0d0d1a; border-radius: 10px; padding: 18px; text-align: center; min-height: 280px; display: flex; align-items: center; justify-content: center; }
.upload-zone { width: 100%; max-width: 460px; border: 2px dashed #0f3460; border-radius: 10px; padding: 40px 20px; text-align: center; cursor: pointer; transition: .2s; }
.upload-zone:hover { border-color: #4ecca3; background: rgba(78,204,163,.05); }
.upload-zone svg { width: 42px; height: 42px; fill: #4ecca3; margin-bottom: 10px; }
.upload-zone p { color: #888; font-size: 13px; }
.upload-zone p.highlight { color: #4ecca3; }
#preview-img { display: none; max-width: 100%; max-height: 460px; border-radius: 6px; }
#status { text-align: center; padding: 24px; color: #4ecca3; display: none; }
.spinner { display: inline-block; width: 18px; height: 18px; border: 3px solid #0f3460; border-top-color: #4ecca3; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 8px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
#error-msg { color: #e74c3c; padding: 16px; display: none; font-size: 13px; background: rgba(231,76,60,.1); border-radius: 6px; margin-top: 12px; }
/* 步骤画廊 */
.result-top { margin-top: 16px; background: #16213e; border-radius: 10px; padding: 16px; display: none; }
.result-top.show { display: block; }
.result-top h3 { font-size: 14px; color: #4ecca3; margin-bottom: 12px; }
.result-pair { display: flex; gap: 16px; flex-wrap: wrap; }
.result-card { flex: 1; min-width: 240px; }
.result-card h4 { font-size: 12px; color: #aaa; margin-bottom: 6px; text-align: center; }
.result-card img { width: 100%; border-radius: 6px; display: block; }
.params-used { margin-top: 12px; padding: 10px 12px; background: #0d1b3e; border-radius: 6px; font-size: 11px; color: #888; line-height: 1.7; font-family: monospace; }
.steps-section { margin-top: 16px; }
.step-item { background: #16213e; border-radius: 10px; padding: 16px; margin-bottom: 14px; }
.step-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.step-title { font-size: 15px; font-weight: 600; color: #4ecca3; }
.step-time { font-size: 11px; color: #666; font-family: monospace; }
.step-desc { font-size: 12px; color: #999; margin-bottom: 12px; line-height: 1.6; }
.step-images { display: flex; gap: 12px; flex-wrap: wrap; }
.step-img-card { flex: 1; min-width: 180px; max-width: 280px; }
.step-img-card .lbl { font-size: 11px; color: #888; margin-bottom: 4px; text-align: center; }
.step-img-card img { width: 100%; border-radius: 6px; display: block; border: 1px solid #0f3460; cursor: zoom-in; transition: .15s; }
.step-img-card img:hover { border-color: #4ecca3; }
#lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; }
#lightbox img { max-width: 92%; max-height: 92%; border-radius: 8px; }
#lightbox.show { display: flex; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>🔬 生发调试台(逐步可视化 + 全参数)</h1>
<p>调参数 → 看每一步中间产物如何变化。绿色框=可编辑参数,灰色=webui端固定</p>
</div>
<div class="links">
<a href="/test_new">→ 简易测试页</a>
<a href="/swap">→ 全部发型页</a>
</div>
</div>
<div class="container">
<!-- 左侧:参数面板 -->
<div class="panel">
<h3>① 流程开关</h3>
<div class="switch on" id="sw-cut_bang" onclick="toggleSwitch('cut_bang', this)">
<span>减刘海圆 <span class="tip" title="从原头发mask中扣除眼睛周围的圆形区域,避免重绘眼睛"></span></span>
<div class="toggle"></div>
</div>
<div class="switch" id="sw-strict_mask" onclick="toggleSwitch('strict_mask', this)">
<span>严格mask贴回 <span class="tip" title="只在mask区域覆盖,mask外保留原图(否则整个裁剪框覆盖)"></span></span>
<div class="toggle"></div>
</div>
<div class="switch on" id="sw-seamless_blend" onclick="toggleSwitch('seamless_blend', this)">
<span>泊松融合消接缝 <span class="tip" title="严格mask模式下用seamlessClone消除边缘色差(仅strict_mask开启时生效)"></span></span>
<div class="toggle"></div>
</div>
<h3>② 尺寸 / 对齐</h3>
<div class="switch on" id="sw-is_hr" onclick="toggleSwitch('is_hr', this)">
<span>高清模式 <span class="tip" title="1152x1536 vs 576x768,影响显存和速度"></span></span>
<div class="toggle"></div>
</div>
<div class="pair">
<div class="field">
<div class="field-label">dilate_kernel <span class="val" id="v-dk0">6</span></div>
<input type="number" id="dk0" value="6" min="1" max="40" oninput="syncNum('dk0')">
</div>
<div class="field">
<div class="field-label">mask膨胀核(x,y) <span class="val" id="v-dk1">18</span></div>
<input type="number" id="dk1" value="18" min="1" max="40" oninput="syncNum('dk1')">
</div>
</div>
<h3>③ SD 推理参数</h3>
<div class="field">
<div class="field-label">denoising_strength <span class="val" id="v-den">0.60</span><span class="tip" title="重绘强度。越高越偏离原图,发丝细节越多但可能失真"></span></div>
<input type="range" id="den" min="0.1" max="1.0" step="0.05" value="0.6" oninput="syncRange('den',2)">
</div>
<div class="readonly-info">
<b style="color:#888">webui端固定(不可调):</b><br>
<code>cfg_scale=7</code> <code>steps=20</code><br>
<code>sampler=DPM++ 2M Karras</code><br>
<code>mask_blur=11</code> <code>seed=123456789</code>
</div>
<h3>④ 贴回 / 融合</h3>
<div class="pair">
<div class="field">
<div class="field-label">blend_dilate <span class="val" id="v-bd0">5</span></div>
<input type="number" id="bd0" value="5" min="1" max="40" oninput="syncNum('bd0')">
</div>
<div class="field">
<div class="field-label">(strict贴回) <span class="val" id="v-bd1">5</span></div>
<input type="number" id="bd1" value="5" min="1" max="40" oninput="syncNum('bd1')">
</div>
</div>
<div class="pair">
<div class="field">
<div class="field-label">seamless_dilate <span class="val" id="v-sd0">9</span></div>
<input type="number" id="sd0" value="9" min="1" max="40" oninput="syncNum('sd0')">
</div>
<div class="field">
<div class="field-label">(泊松融合) <span class="val" id="v-sd1">9</span></div>
<input type="number" id="sd1" value="9" min="1" max="40" oninput="syncNum('sd1')">
</div>
</div>
<button class="btn btn-primary" id="btn-run" disabled>▶ 执行生发</button>
<button class="btn btn-ghost" id="btn-reset" onclick="resetParams()">↺ 恢复默认参数</button>
<div class="hint">
<b>使用说明</b><br>
1. 选发型(中间)→ 上传人像(右侧)<br>
2. 调整左侧参数<br>
3. 点"执行",看7步中间产物<br>
4. 改参数重跑,对比效果差异
</div>
</div>
<!-- 中间:发型选择 -->
<div class="mid-panel">
<h3>选择发型</h3>
<div class="hairstyle-grid" id="hairstyle-grid"><div style="color:#666;font-size:11px">加载中...</div></div>
<div style="font-size:11px;color:#4ecca3;margin-top:8px;text-align:center" id="sel-info">未选择</div>
<button class="btn btn-ghost" style="margin-top:8px;font-size:12px" onclick="loadStatus()">↻ 刷新</button>
</div>
<!-- 右侧:工作区 -->
<div class="workspace">
<div class="canvas-wrap">
<div class="upload-zone" id="upload-zone">
<svg viewBox="0 0 24 24"><path d="M19 13v6H5v-6H3v8h18v-8zM6 9l1.41 1.41L11 6.83V18h2V6.83l3.59 3.58L18 9l-6-6z"/></svg>
<p class="highlight">点击上传人像</p>
<p>正面清晰照效果最佳</p>
</div>
<input type="file" id="file-input" accept="image/*" style="display:none">
<img id="preview-img">
</div>
<div id="status"><span class="spinner"></span><span id="status-text">执行中,约40-90秒...</span></div>
<div id="error-msg"></div>
<div class="result-top" id="result-top">
<h3>最终对比</h3>
<div class="result-pair">
<div class="result-card"><h4>原图</h4><img id="r-orig"></div>
<div class="result-card"><h4>生发结果</h4><img id="r-new"></div>
</div>
<div class="params-used" id="params-used"></div>
</div>
<div class="steps-section" id="steps-section"></div>
</div>
</div>
<div id="lightbox" onclick="this.classList.remove('show')"><img id="lightbox-img"></div>
<script>
(function(){
const $=id=>document.getElementById(id);
const HAIRSTYLES = [
{face:"圆",name:"圆-心形"},{face:"圆",name:"圆-椭圆"},{face:"圆",name:"圆-波浪"},{face:"圆",name:"圆-直线"},{face:"圆",name:"圆-花瓣"},
{face:"心形",name:"心形-心形"},{face:"心形",name:"心形-椭圆"},{face:"心形",name:"心形-波浪"},{face:"心形",name:"心形-直线"},{face:"心形",name:"心形-花瓣"},
{face:"方脸",name:"方脸-心形"},{face:"方脸",name:"方脸-椭圆"},{face:"方脸",name:"方脸-波浪"},{face:"方脸",name:"方脸-直线"},{face:"方脸",name:"方脸-花瓣"},
{face:"椭圆",name:"椭圆-心形"},{face:"椭圆",name:"椭圆-椭圆"},{face:"椭圆",name:"椭圆-波浪"},{face:"椭圆",name:"椭圆-直线"},{face:"椭圆",name:"椭圆-花瓣"},
{face:"菱形",name:"菱形-心"},{face:"菱形",name:"菱形-椭圆"},{face:"菱形",name:"菱形-波浪"},{face:"菱形",name:"菱形-直线"},{face:"菱形",name:"菱形-花瓣"},
{face:"长",name:"长-心形"},{face:"长",name:"长 -椭圆"},{face:"长",name:"长 -波浪"},{face:"长",name:"长 -直线"},{face:"长",name:"长 -花瓣"}
];
const defaults = {cut_bang:true,strict_mask:false,seamless_blend:true,is_hr:true,
dk0:6,dk1:18,den:0.6,bd0:5,bd1:5,sd0:9,sd1:9};
let readySet=new Set(), selectedHair=null, imgB64=null;
async function loadStatus(){
try{
const r=await fetch("/api/hairstyles");const d=await r.json();
if(d.state!==0)throw new Error(d.msg);
readySet=new Set(d.data.map(x=>x.hair_id));
}catch(e){readySet=new Set();}
renderGrid();
}
function renderGrid(){
const g=$("hairstyle-grid");g.innerHTML="";
HAIRSTYLES.forEach(h=>{
const ready=readySet.has(h.name);
const it=document.createElement("div");
it.className="hairstyle-item"+(ready?" ready":" pending")+(h.name===selectedHair?" selected":"");
it.innerHTML=`<img loading="lazy" src="/train_src/${encodeURIComponent(h.name)}" onerror="this.style.objectFit='contain';this.src='data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22><text x=%2250%22 y=%2255%22 text-anchor=%22middle%22 fill=%22%23666%22 font-size=%2210%22>训练中</text></svg>'"><div class="name">${h.name}</div>`;
if(ready) it.onclick=()=>{
selectedHair=h.name;
$("sel-info").textContent="已选: "+h.name;
renderGrid(); updateBtn();
};
g.appendChild(it);
});
}
// 参数控件
window.toggleSwitch=(key,el)=>{
el.classList.toggle("on");
updateBtn();
};
// range 滑块绑定显示
["den"].forEach(id=>{
$(id).addEventListener("input",e=>$("v-"+id).textContent=parseFloat(e.target.value).toFixed(2));
});
window.syncNum=(id)=>{ $("v-"+id).textContent=$(id).value; };
window.resetParams=()=>{
Object.keys(defaults).forEach(k=>{
if(typeof defaults[k]==="boolean"){
const el=$("sw-"+k); el.classList.toggle("on",defaults[k]);
}else if(k==="den"){ $("den").value=defaults[k]; $("v-den").textContent=defaults[k].toFixed(2);
}else{ $(k).value=defaults[k]; $("v-"+k).textContent=defaults[k]; }
});
};
function collectParams(){
return {
cut_bang:$("sw-cut_bang").classList.contains("on"),
strict_mask:$("sw-strict_mask").classList.contains("on"),
seamless_blend:$("sw-seamless_blend").classList.contains("on"),
is_hr:$("sw-is_hr").classList.contains("on"),
dilate_kernel:[parseInt($("dk0").value),parseInt($("dk1").value)],
denoising_strength:parseFloat($("den").value),
blend_dilate:[parseInt($("bd0").value),parseInt($("bd1").value)],
seamless_dilate:[parseInt($("sd0").value),parseInt($("sd1").value)],
};
}
// 上传
$("upload-zone").onclick=()=>$("file-input").click();
$("file-input").onchange=e=>{ if(e.target.files[0]) loadImg(e.target.files[0]); };
$("upload-zone").ondragover=e=>{e.preventDefault();$("upload-zone").style.borderColor="#4ecca3";};
$("upload-zone").ondragleave=()=>$("upload-zone").style.borderColor="#0f3460";
$("upload-zone").ondrop=e=>{e.preventDefault();if(e.dataTransfer.files[0])loadImg(e.dataTransfer.files[0]);};
function loadImg(file){
if(!file.type.startsWith("image/")){alert("请上传图片");return;}
const rd=new FileReader();
rd.onload=e=>{
imgB64=e.target.result;
$("preview-img").src=imgB64;
$("preview-img").style.display="block";
$("upload-zone").style.display="none";
updateBtn();
};
rd.readAsDataURL(file);
}
function updateBtn(){ $("btn-run").disabled=!(selectedHair&&imgB64); }
// 执行
$("btn-run").onclick=async()=>{
if(!selectedHair||!imgB64) return;
const p=collectParams();
$("btn-run").disabled=true;
$("status").style.display="block";
$("status-text").textContent="执行中(粗推理→mask→warpAffine→SD→贴回),约40-90秒...";
$("error-msg").style.display="none";
$("result-top").classList.remove("show");
$("steps-section").innerHTML="";
try{
const resp=await fetch("/api/swap_debug",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify(Object.assign({img:imgB64,hair_id:selectedHair},p))
});
const data=await resp.json();
$("status").style.display="none";
if(data.state===0){
const url="data:image/jpeg;base64,"+data.result;
$("r-orig").src=imgB64;$("r-new").src=url;
// 参数回显
let ph=""; Object.keys(data.params).forEach(k=>{
let v=data.params[k]; if(Array.isArray(v))v="["+v.join(",")+"]";
if(typeof v==="boolean")v=v?"开":"关";
ph+=k+" = "+v+" ";
});
$("params-used").textContent="本次参数: "+ph;
$("result-top").classList.add("show");
// 步骤画廊
const sc=$("steps-section");
data.steps.forEach(step=>{
const div=document.createElement("div");div.className="step-item";
let imgs='<div class="step-images">';
step.images.forEach(im=>{imgs+='<div class="step-img-card"><div class="lbl">'+im.label+'</div><img src="data:image/jpeg;base64,'+im.b64+'" data-full="data:image/jpeg;base64,'+im.b64+'"></div>';});
imgs+='</div>';
div.innerHTML='<div class="step-head"><div class="step-title">'+step.title+'</div></div><div class="step-desc">'+step.desc+'</div>'+imgs;
sc.appendChild(div);
});
sc.querySelectorAll("img").forEach(img=>img.onclick=()=>showLightbox(img.dataset.full));
$("result-top").scrollIntoView({behavior:"smooth"});
}else{
$("error-msg").textContent="❌ "+(data.msg||"失败");
$("error-msg").style.display="block";
}
}catch(err){
$("status").style.display="none";
$("error-msg").textContent="❌ 请求失败: "+err.message;
$("error-msg").style.display="block";
}
updateBtn();
};
function showLightbox(src){$("lightbox-img").src=src;$("lightbox").classList.add("show");}
loadStatus();
})();
</script>
</body>
</html>
+434
View File
@@ -0,0 +1,434 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发际线带重绘实验</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; font-size: 14px; }
.header { background: #16213e; padding: 16px 28px; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 19px; font-weight: 600; }
.header p { font-size: 12px; color: #888; margin-top: 3px; }
.header .links a { color: #4ecca3; font-size: 12px; text-decoration: none; margin-left: 12px; }
.container { display: flex; gap: 18px; padding: 18px; max-width: 1800px; margin: 0 auto; }
/* 左侧参数面板 */
.panel { width: 340px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 16px; height: fit-content; max-height: 92vh; overflow-y: auto; }
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.panel h3 { font-size: 13px; color: #4ecca3; margin: 14px 0 8px; text-transform: uppercase; letter-spacing: 1px; padding-bottom: 5px; border-bottom: 1px solid #0f3460; }
.panel h3:first-child { margin-top: 0; }
.field { margin-bottom: 12px; }
.field-label { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #bbb; margin-bottom: 5px; }
.field-label .val { color: #4ecca3; font-family: monospace; font-size: 11px; }
.field-label .tip { color: #666; font-size: 10px; margin-left: 4px; cursor: help; }
.field input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: #0f3460; border-radius: 2px; outline: none; }
.field input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #4ecca3; cursor: pointer; }
.field input[type="number"] { width: 100%; padding: 5px 8px; background: #0d1b3e; border: 1px solid #0f3460; border-radius: 4px; color: #eee; font-size: 12px; }
.field select { width: 100%; padding: 5px 8px; background: #0d1b3e; border: 1px solid #0f3460; border-radius: 4px; color: #eee; font-size: 12px; }
.switch { display: flex; align-items: center; justify-content: space-between; padding: 7px 0; font-size: 13px; color: #ccc; cursor: pointer; }
.switch:hover { color: #eee; }
.toggle { position: relative; width: 36px; height: 20px; background: #0f3460; border-radius: 10px; transition: .2s; flex-shrink: 0; }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #888; border-radius: 50%; transition: .2s; }
.switch.on .toggle { background: #4ecca3; }
.switch.on .toggle::after { left: 18px; background: #16213e; }
.pair { display: flex; gap: 8px; }
.pair .field { flex: 1; }
.hint { font-size: 11px; color: #666; line-height: 1.5; margin-top: 8px; padding: 8px 10px; background: #0d1b3e; border-radius: 5px; }
.hint b { color: #aaa; }
.readonly-info { font-size: 11px; color: #777; padding: 8px 10px; background: #0d1b3e; border-radius: 5px; line-height: 1.6; }
.readonly-info code { color: #aaa; background: #16213e; padding: 1px 5px; border-radius: 3px; }
.btn { display: block; width: 100%; padding: 10px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: .15s; }
.btn-primary { background: #4ecca3; color: #16213e; font-weight: 600; font-size: 15px; padding: 12px; margin-top: 8px; }
.btn-primary:hover { background: #6ee0bd; }
.btn-primary:disabled { background: #555; color: #999; cursor: not-allowed; }
.btn-ghost { background: #0f3460; color: #eee; margin-top: 6px; }
.btn-ghost:hover { background: #1a4a80; }
/* 中间发型选择 */
.mid-panel { width: 200px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 14px; height: fit-content; max-height: 92vh; overflow-y: auto; }
.mid-panel h3 { font-size: 12px; color: #4ecca3; margin-bottom: 10px; }
.hairstyle-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.hairstyle-item { position: relative; cursor: pointer; border-radius: 5px; overflow: hidden; border: 2px solid transparent; aspect-ratio: 1; background: #0d1b3e; }
.hairstyle-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hairstyle-item.ready { border-color: #2a5a4a; }
.hairstyle-item.selected { border-color: #4ecca3; }
.hairstyle-item.selected::after { content: '✓'; position: absolute; top: 1px; right: 3px; color: #4ecca3; font-weight: bold; text-shadow: 0 0 3px #000; font-size: 11px; }
.hairstyle-item .name { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); color: #fff; font-size: 10px; padding: 8px 2px 2px; text-align: center; }
.hairstyle-item.pending { opacity: 0.4; cursor: not-allowed; }
/* 右侧工作区 */
.workspace { flex: 1; min-width: 0; }
.canvas-wrap { background: #0d0d1a; border-radius: 10px; padding: 18px; text-align: center; min-height: 280px; display: flex; align-items: center; justify-content: center; }
.upload-zone { width: 100%; max-width: 460px; border: 2px dashed #0f3460; border-radius: 10px; padding: 40px 20px; text-align: center; cursor: pointer; transition: .2s; }
.upload-zone:hover { border-color: #4ecca3; background: rgba(78,204,163,.05); }
.upload-zone svg { width: 42px; height: 42px; fill: #4ecca3; margin-bottom: 10px; }
.upload-zone p { color: #888; font-size: 13px; }
.upload-zone p.highlight { color: #4ecca3; }
#preview-img { display: none; max-width: 100%; max-height: 460px; border-radius: 6px; }
#status { text-align: center; padding: 24px; color: #4ecca3; display: none; }
.spinner { display: inline-block; width: 18px; height: 18px; border: 3px solid #0f3460; border-top-color: #4ecca3; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 8px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
#error-msg { color: #e74c3c; padding: 16px; display: none; font-size: 13px; background: rgba(231,76,60,.1); border-radius: 6px; margin-top: 12px; }
/* 步骤画廊 */
.result-top { margin-top: 16px; background: #16213e; border-radius: 10px; padding: 16px; display: none; }
.result-top.show { display: block; }
.result-top h3 { font-size: 14px; color: #4ecca3; margin-bottom: 12px; }
.result-pair { display: flex; gap: 16px; flex-wrap: wrap; }
.result-card { flex: 1; min-width: 240px; }
.result-card h4 { font-size: 12px; color: #aaa; margin-bottom: 6px; text-align: center; }
.result-card img { width: 100%; border-radius: 6px; display: block; }
.params-used { margin-top: 12px; padding: 10px 12px; background: #0d1b3e; border-radius: 6px; font-size: 11px; color: #888; line-height: 1.7; font-family: monospace; }
.steps-section { margin-top: 16px; }
.step-item { background: #16213e; border-radius: 10px; padding: 16px; margin-bottom: 14px; }
.step-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.step-title { font-size: 15px; font-weight: 600; color: #4ecca3; }
.step-time { font-size: 11px; color: #666; font-family: monospace; }
.step-desc { font-size: 12px; color: #999; margin-bottom: 12px; line-height: 1.6; }
.step-images { display: flex; gap: 12px; flex-wrap: wrap; }
.step-img-card { flex: 1; min-width: 180px; max-width: 280px; }
.step-img-card .lbl { font-size: 11px; color: #888; margin-bottom: 4px; text-align: center; }
.step-img-card img { width: 100%; border-radius: 6px; display: block; border: 1px solid #0f3460; cursor: zoom-in; transition: .15s; }
.step-img-card img:hover { border-color: #4ecca3; }
#lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; }
#lightbox img { max-width: 92%; max-height: 92%; border-radius: 8px; }
#lightbox.show { display: flex; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>🔬 发际线带重绘实验(只重绘发际线,主体保留原图)</h1>
<p>实验性:重绘区域只取发际线边界带,观察"只改发际线、保留原发型主体"的效果</p>
</div>
<div class="links">
<a href="/test_new">→ 简易测试页</a>
<a href="/swap">→ 全部发型页</a>
</div>
</div>
<div class="container">
<!-- 左侧:参数面板 -->
<div class="panel">
<h3>① 流程开关</h3>
<div class="switch" id="sw-strict_mask" onclick="toggleSwitch('strict_mask', this)">
<span>严格mask贴回 <span class="tip" title="只在mask区域覆盖,mask外保留原图(否则整个裁剪框覆盖)"></span></span>
<div class="toggle"></div>
</div>
<div class="switch on" id="sw-seamless_blend" onclick="toggleSwitch('seamless_blend', this)">
<span>泊松融合消接缝 <span class="tip" title="严格mask模式下用seamlessClone消除边缘色差(仅strict_mask开启时生效)"></span></span>
<div class="toggle"></div>
</div>
<h3>② 尺寸 / 对齐</h3>
<div class="switch on" id="sw-is_hr" onclick="toggleSwitch('is_hr', this)">
<span>高清模式 <span class="tip" title="1152x1536 vs 576x768,影响显存和速度"></span></span>
<div class="toggle"></div>
</div>
<div class="pair">
<div class="field">
<div class="field-label">dilate_kernel <span class="val" id="v-dk0">6</span></div>
<input type="number" id="dk0" value="6" min="1" max="40" oninput="syncNum('dk0')">
</div>
<div class="field">
<div class="field-label">mask膨胀核(x,y) <span class="val" id="v-dk1">18</span></div>
<input type="number" id="dk1" value="18" min="1" max="40" oninput="syncNum('dk1')">
</div>
</div>
<h3>③ 贴回 / 融合</h3>
<div class="pair">
<div class="field">
<div class="field-label">blend_dilate <span class="val" id="v-bd0">5</span></div>
<input type="number" id="bd0" value="5" min="1" max="40" oninput="syncNum('bd0')">
</div>
<div class="field">
<div class="field-label">(strict贴回) <span class="val" id="v-bd1">5</span></div>
<input type="number" id="bd1" value="5" min="1" max="40" oninput="syncNum('bd1')">
</div>
</div>
<div class="pair">
<div class="field">
<div class="field-label">seamless_dilate <span class="val" id="v-sd0">9</span></div>
<input type="number" id="sd0" value="9" min="1" max="40" oninput="syncNum('sd0')">
</div>
<div class="field">
<div class="field-label">(泊松融合) <span class="val" id="v-sd1">9</span></div>
<input type="number" id="sd1" value="9" min="1" max="40" oninput="syncNum('sd1')">
</div>
</div>
<h3 style="color:#e9b949">★ 发际线mask参数(landmark_1k 圆角矩形)</h3>
<div class="field">
<div class="field-label">band_width <span class="val" id="v-bw">15</span><span class="tip" title="膨胀核宽度(像素)。越大重绘区域越宽"></span></div>
<input type="range" id="bw" min="3" max="50" step="1" value="15" oninput="syncRangeInt('bw')">
</div>
<div class="hint" style="font-size:10px;color:#e9b949;margin:6px 0 4px">▼ 圆角矩形形状</div>
<div class="field">
<div class="field-label">height_ratio <span class="val" id="v-hr">0.43</span><span class="tip" title="高度缩放比(相对原额头高度)。越小上下越窄"></span></div>
<input type="range" id="hr" min="0.1" max="1.0" step="0.02" value="0.43" oninput="syncRangeFloat('hr',2)">
</div>
<div class="field">
<div class="field-label">width_ratio <span class="val" id="v-wr">0.14</span><span class="tip" title="左右各扩展比(相对眉宽)。越大左右越宽"></span></div>
<input type="range" id="wr" min="0" max="0.6" step="0.02" value="0.14" oninput="syncRangeFloat('wr',2)">
</div>
<div class="field">
<div class="field-label">corner_ratio <span class="val" id="v-cr">0.25</span><span class="tip" title="圆角半径比(相对短边)。越大圆角越大"></span></div>
<input type="range" id="cr" min="0" max="0.5" step="0.05" value="0.25" oninput="syncRangeFloat('cr',2)">
</div>
<div class="field">
<div class="field-label">vertical_offset <span class="val" id="v-vo">0.00</span><span class="tip" title="上下平移比(相对额头高度)。正值下移,负值上移"></span></div>
<input type="range" id="vo" min="-0.5" max="0.5" step="0.02" value="0" oninput="syncRangeFloat('vo',2)">
</div>
<div class="hint" style="font-size:10px;color:#4ecca3;margin:6px 0 4px">▼ SD 推理(仅执行换发型时生效)</div>
<div class="field">
<div class="field-label">denoising <span class="val" id="v-den">0.60</span><span class="tip" title="重绘强度。越高越偏离原图"></span></div>
<input type="range" id="den" min="0.1" max="1.0" step="0.05" value="0.6" oninput="syncRangeFloat('den',2)">
</div>
<div class="field">
<div class="field-label">refiner_switch_at <span class="val" id="v-rsa">0.50</span><span class="tip" title="refiner切换点(仅高清模式)。0=不用refiner0.5=中途切换,1=全程refiner"></span></div>
<input type="range" id="rsa" min="0" max="1.0" step="0.05" value="0.5" oninput="syncRangeFloat('rsa',2)">
</div>
<div class="switch on" id="sw-preview_only" onclick="toggleSwitch('preview_only', this)">
<span>仅验证mask <span class="tip" title="开=只生成发际线mask(几秒),不做换发型重绘。关=执行完整换发型(40-90秒)"></span></span>
<div class="toggle"></div>
</div>
<div class="hint" style="border-left:3px solid #e9b949">
<b style="color:#e9b949">说明:</b><br>
步骤③用 <b>landmark_1k 关键点</b>定位额头,生成<b>圆角矩形</b>重绘区。头发主体保留原图,LoRA 仅在发际线区生效。<br>
看「★ 叠加用户原图」直观判断重绘区位置。
</div>
<button class="btn btn-primary" id="btn-run" disabled>▶ 执行换发型</button>
<button class="btn btn-ghost" id="btn-reset" onclick="resetParams()">↺ 恢复默认参数</button>
<div class="hint">
<b>使用说明</b><br>
1. 选发型(中间)→ 上传人像(右侧)<br>
2. 调整左侧参数<br>
3. 点"执行",看7步中间产物<br>
4. 改参数重跑,对比效果差异
</div>
</div>
<!-- 中间:发型选择 -->
<div class="mid-panel">
<h3>选择发型</h3>
<div class="hairstyle-grid" id="hairstyle-grid"><div style="color:#666;font-size:11px">加载中...</div></div>
<div style="font-size:11px;color:#4ecca3;margin-top:8px;text-align:center" id="sel-info">未选择</div>
<button class="btn btn-ghost" style="margin-top:8px;font-size:12px" onclick="loadStatus()">↻ 刷新</button>
</div>
<!-- 右侧:工作区 -->
<div class="workspace">
<div class="canvas-wrap">
<div class="upload-zone" id="upload-zone">
<svg viewBox="0 0 24 24"><path d="M19 13v6H5v-6H3v8h18v-8zM6 9l1.41 1.41L11 6.83V18h2V6.83l3.59 3.58L18 9l-6-6z"/></svg>
<p class="highlight">点击上传人像</p>
<p>正面清晰照效果最佳</p>
</div>
<input type="file" id="file-input" accept="image/*" style="display:none">
<img id="preview-img">
</div>
<div id="status"><span class="spinner"></span><span id="status-text">执行中,约40-90秒...</span></div>
<div id="error-msg"></div>
<div class="result-top" id="result-top">
<h3>最终对比</h3>
<div class="result-pair">
<div class="result-card"><h4>原图</h4><img id="r-orig"></div>
<div class="result-card"><h4>换发型结果</h4><img id="r-new"></div>
</div>
<div class="params-used" id="params-used"></div>
</div>
<div class="steps-section" id="steps-section"></div>
</div>
</div>
<div id="lightbox" onclick="this.classList.remove('show')"><img id="lightbox-img"></div>
<script>
(function(){
const $=id=>document.getElementById(id);
const HAIRSTYLES = [
{face:"圆",name:"圆-心形"},{face:"圆",name:"圆-椭圆"},{face:"圆",name:"圆-波浪"},{face:"圆",name:"圆-直线"},{face:"圆",name:"圆-花瓣"},
{face:"心形",name:"心形-心形"},{face:"心形",name:"心形-椭圆"},{face:"心形",name:"心形-波浪"},{face:"心形",name:"心形-直线"},{face:"心形",name:"心形-花瓣"},
{face:"方脸",name:"方脸-心形"},{face:"方脸",name:"方脸-椭圆"},{face:"方脸",name:"方脸-波浪"},{face:"方脸",name:"方脸-直线"},{face:"方脸",name:"方脸-花瓣"},
{face:"椭圆",name:"椭圆-心形"},{face:"椭圆",name:"椭圆-椭圆"},{face:"椭圆",name:"椭圆-波浪"},{face:"椭圆",name:"椭圆-直线"},{face:"椭圆",name:"椭圆-花瓣"},
{face:"菱形",name:"菱形-心"},{face:"菱形",name:"菱形-椭圆"},{face:"菱形",name:"菱形-波浪"},{face:"菱形",name:"菱形-直线"},{face:"菱形",name:"菱形-花瓣"},
{face:"长",name:"长-心形"},{face:"长",name:"长 -椭圆"},{face:"长",name:"长 -波浪"},{face:"长",name:"长 -直线"},{face:"长",name:"长 -花瓣"}
];
const defaults = {method:"landmark_1k",strict_mask:false,seamless_blend:true,is_hr:true,
dk0:6,dk1:18,bd0:5,bd1:5,sd0:9,sd1:9,bw:15,preview_only:true,
hr:0.43,wr:0.14,cr:0.25,vo:0,den:0.6,rsa:0.5};
let readySet=new Set(), selectedHair=null, imgB64=null;
async function loadStatus(){
try{
const r=await fetch("/api/hairstyles");const d=await r.json();
if(d.state!==0)throw new Error(d.msg);
readySet=new Set(d.data.map(x=>x.hair_id));
}catch(e){readySet=new Set();}
renderGrid();
}
function renderGrid(){
const g=$("hairstyle-grid");g.innerHTML="";
HAIRSTYLES.forEach(h=>{
const ready=readySet.has(h.name);
const it=document.createElement("div");
it.className="hairstyle-item"+(ready?" ready":" pending")+(h.name===selectedHair?" selected":"");
it.innerHTML=`<img loading="lazy" src="/train_src/${encodeURIComponent(h.name)}" onerror="this.style.objectFit='contain';this.src='data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22><text x=%2250%22 y=%2255%22 text-anchor=%22middle%22 fill=%22%23666%22 font-size=%2210%22>训练中</text></svg>'"><div class="name">${h.name}</div>`;
if(ready) it.onclick=()=>{
selectedHair=h.name;
$("sel-info").textContent="已选: "+h.name;
renderGrid(); updateBtn();
};
g.appendChild(it);
});
}
// 参数控件
window.toggleSwitch=(key,el)=>{
el.classList.toggle("on");
if(key==="preview_only") updateRunBtn();
updateBtn();
};
function updateRunBtn(){
const po=$("sw-preview_only").classList.contains("on");
$("btn-run").innerHTML = po ? "▶ 仅生成mask(快速预览)" : "▶ 执行完整换发型(40-90秒)";
}
// range 滑块绑定显示
["den"].forEach(id=>{
$(id).addEventListener("input",e=>$("v-"+id).textContent=parseFloat(e.target.value).toFixed(2));
});
// band_width 滑块(整数,HTML 里 oninput 直接调用全局函数)
window.syncRangeInt=(id)=>{ $("v-"+id).textContent=$(id).value; };
// 浮点滑块(HTML oninput 调用,联动显示)
window.syncRangeFloat=(id,fix)=>{ $("v-"+id).textContent=parseFloat($(id).value).toFixed(fix); };
window.syncNum=(id)=>{ $("v-"+id).textContent=$(id).value; };
window.resetParams=()=>{
Object.keys(defaults).forEach(k=>{
if(typeof defaults[k]==="boolean"){
const el=$("sw-"+k); el.classList.toggle("on",defaults[k]);
}else if(k==="method"){
// method 固定 landmark_1k,无 UI
}else{
const v=defaults[k];
$(k).value=v;
const lbl=$("v-"+k);
if(lbl){
// 浮点显示2位小数,整数原样
lbl.textContent = (typeof v==="number" && !Number.isInteger(v)) ? v.toFixed(2) : v;
}
}
});
updateRunBtn();
};
function collectParams(){
return {
method:"landmark_1k",
strict_mask:$("sw-strict_mask").classList.contains("on"),
seamless_blend:$("sw-seamless_blend").classList.contains("on"),
is_hr:$("sw-is_hr").classList.contains("on"),
dilate_kernel:[parseInt($("dk0").value),parseInt($("dk1").value)],
denoising_strength:parseFloat($("den").value),
blend_dilate:[parseInt($("bd0").value),parseInt($("bd1").value)],
seamless_dilate:[parseInt($("sd0").value),parseInt($("sd1").value)],
band_width:parseInt($("bw").value),
preview_only:$("sw-preview_only").classList.contains("on"),
height_ratio:parseFloat($("hr").value),
width_ratio:parseFloat($("wr").value),
corner_ratio:parseFloat($("cr").value),
vertical_offset:parseFloat($("vo").value),
refiner_switch_at:parseFloat($("rsa").value),
};
}
// 上传
$("upload-zone").onclick=()=>$("file-input").click();
$("file-input").onchange=e=>{ if(e.target.files[0]) loadImg(e.target.files[0]); };
$("upload-zone").ondragover=e=>{e.preventDefault();$("upload-zone").style.borderColor="#4ecca3";};
$("upload-zone").ondragleave=()=>$("upload-zone").style.borderColor="#0f3460";
$("upload-zone").ondrop=e=>{e.preventDefault();if(e.dataTransfer.files[0])loadImg(e.dataTransfer.files[0]);};
function loadImg(file){
if(!file.type.startsWith("image/")){alert("请上传图片");return;}
const rd=new FileReader();
rd.onload=e=>{
imgB64=e.target.result;
$("preview-img").src=imgB64;
$("preview-img").style.display="block";
$("upload-zone").style.display="none";
updateBtn();
};
rd.readAsDataURL(file);
}
function updateBtn(){ $("btn-run").disabled=!(selectedHair&&imgB64); }
// 执行
$("btn-run").onclick=async()=>{
if(!selectedHair||!imgB64) return;
const p=collectParams();
$("btn-run").disabled=true;
$("status").style.display="block";
$("status-text").textContent = p.preview_only
? "生成发际线mask中(粗推理+方案),约5-15秒..."
: "执行完整换发型中(粗推理→mask→warpAffine→SD→贴回),约40-90秒...";
$("error-msg").style.display="none";
$("result-top").classList.remove("show");
$("steps-section").innerHTML="";
try{
const resp=await fetch("/api/swap_hairline",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify(Object.assign({img:imgB64,hair_id:selectedHair},p))
});
const data=await resp.json();
$("status").style.display="none";
if(data.state===0){
const url="data:image/jpeg;base64,"+data.result;
$("r-orig").src=imgB64;$("r-new").src=url;
// 参数回显
let ph=""; Object.keys(data.params).forEach(k=>{
let v=data.params[k]; if(Array.isArray(v))v="["+v.join(",")+"]";
if(typeof v==="boolean")v=v?"开":"关";
ph+=k+" = "+v+" ";
});
$("params-used").textContent="本次参数: "+ph;
$("result-top").classList.add("show");
// 步骤画廊
const sc=$("steps-section");
data.steps.forEach(step=>{
const div=document.createElement("div");div.className="step-item";
let imgs='<div class="step-images">';
step.images.forEach(im=>{imgs+='<div class="step-img-card"><div class="lbl">'+im.label+'</div><img src="data:image/jpeg;base64,'+im.b64+'" data-full="data:image/jpeg;base64,'+im.b64+'"></div>';});
imgs+='</div>';
div.innerHTML='<div class="step-head"><div class="step-title">'+step.title+'</div></div><div class="step-desc">'+step.desc+'</div>'+imgs;
sc.appendChild(div);
});
sc.querySelectorAll("img").forEach(img=>img.onclick=()=>showLightbox(img.dataset.full));
$("result-top").scrollIntoView({behavior:"smooth"});
}else{
$("error-msg").textContent="❌ "+(data.msg||"失败");
$("error-msg").style.display="block";
}
}catch(err){
$("status").style.display="none";
$("error-msg").textContent="❌ 请求失败: "+err.message;
$("error-msg").style.display="block";
}
updateBtn();
};
function showLightbox(src){$("lightbox-img").src=src;$("lightbox").classList.add("show");}
updateRunBtn();
loadStatus();
})();
</script>
</body>
</html>
+242
View File
@@ -0,0 +1,242 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>区域生发测试</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
.header { background: #16213e; padding: 18px 28px; border-bottom: 1px solid #0f3460; }
.header h1 { font-size: 20px; font-weight: 600; }
.header p { font-size: 13px; color: #888; margin-top: 4px; }
.container { display: flex; gap: 20px; padding: 20px; max-width: 1700px; margin: 0 auto; }
.panel { width: 280px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 18px; height: fit-content; max-height: 90vh; overflow-y: auto; }
.panel h3 { font-size: 14px; color: #4ecca3; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px; }
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.hairstyle-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.hairstyle-tab { flex: 1; padding: 6px; text-align: center; background: #0f3460; border: none; border-radius: 4px; color: #aaa; cursor: pointer; font-size: 12px; }
.hairstyle-tab.active { background: #4ecca3; color: #16213e; font-weight: 600; }
.search-box { width: 100%; padding: 6px 10px; background: #0d1b3e; border: 1px solid #0f3460; border-radius: 4px; color: #eee; font-size: 12px; margin-bottom: 10px; }
.hairstyle-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; max-height: 280px; overflow-y: auto; }
.hairstyle-item { position: relative; cursor: pointer; border-radius: 4px; overflow: hidden; border: 2px solid transparent; aspect-ratio: 1; background: #0d1b3e; }
.hairstyle-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hairstyle-item.selected { border-color: #4ecca3; }
.hairstyle-item.selected::after { content: '✓'; position: absolute; top: 2px; right: 4px; color: #4ecca3; font-weight: bold; text-shadow: 0 0 3px #000; }
.selected-hairstyle-info { font-size: 11px; color: #888; margin-top: 8px; text-align: center; word-break: break-all; }
.tool-group { margin-bottom: 16px; }
.tool-group label { display: block; font-size: 13px; color: #aaa; margin-bottom: 6px; }
.btn { display: block; width: 100%; padding: 9px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; margin-bottom: 8px; transition: .15s; }
.btn-tool { background: #0f3460; color: #eee; }
.btn-tool:hover { background: #1a4a7a; }
.btn-tool.active { background: #4ecca3; color: #16213e; font-weight: 600; }
.btn-primary { background: #4ecca3; color: #16213e; font-weight: 600; font-size: 15px; padding: 12px; }
.btn-primary:hover { background: #6ee0bd; }
.btn-primary:disabled { background: #555; color: #999; cursor: not-allowed; }
.btn-danger { background: #c0392b; color: #fff; font-size: 12px; }
.btn-danger:hover { background: #e74c3c; }
.hint { font-size: 12px; color: #666; line-height: 1.6; margin-top: 14px; padding: 10px; background: #0d1b3e; border-radius: 6px; }
.workspace { flex: 1; min-width: 0; }
.canvas-wrap { position: relative; background: #0d0d1a; border-radius: 10px; padding: 20px; text-align: center; min-height: 400px; display: flex; align-items: center; justify-content: center; }
.upload-zone { width: 100%; max-width: 500px; border: 2px dashed #0f3460; border-radius: 10px; padding: 50px 20px; text-align: center; cursor: pointer; transition: .2s; }
.upload-zone:hover, .upload-zone.dragover { border-color: #4ecca3; background: rgba(78,204,163,.05); }
.upload-zone svg { width: 48px; height: 48px; fill: #4ecca3; margin-bottom: 12px; }
.upload-zone p { color: #888; font-size: 14px; }
.upload-zone p.highlight { color: #4ecca3; margin-bottom: 4px; }
#canvas-container { position: relative; display: inline-block; max-width: 100%; }
#img-canvas, #mask-canvas { display: block; max-width: 100%; border-radius: 6px; }
#mask-canvas { position: absolute; top: 0; left: 0; cursor: crosshair; touch-action: none; }
#mask-canvas.eraser { cursor: cell; }
.result-section { margin-top: 20px; background: #16213e; border-radius: 10px; padding: 18px; display: none; }
.result-section.show { display: block; }
.result-section h3 { font-size: 14px; color: #4ecca3; margin-bottom: 14px; }
.result-grid { display: flex; gap: 16px; flex-wrap: wrap; }
.result-card { flex: 1; min-width: 250px; }
.result-card h4 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-align: center; }
.result-card img { width: 100%; border-radius: 6px; display: block; }
.download-btn { display: inline-block; margin-top: 8px; padding: 6px 14px; background: #0f3460; color: #eee; border-radius: 4px; font-size: 12px; text-decoration: none; }
#status { text-align: center; padding: 30px; color: #4ecca3; font-size: 15px; display: none; }
.spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #0f3460; border-top-color: #4ecca3; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 8px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
#error-msg { color: #e74c3c; text-align: center; padding: 20px; display: none; font-size: 14px; }
.loading-hairstyles { text-align: center; padding: 20px; color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="header">
<h1>🟢 区域生发(换发型工作流)</h1>
<p>选发型 → 上传人像 → 在缺发区涂抹遮罩 → 生成(走 LoRA + 换发型链路)</p>
</div>
<div class="container">
<div class="panel">
<h3>① 选择发型</h3>
<div class="hairstyle-tabs">
<button class="hairstyle-tab active" data-gender="all">全部</button>
<button class="hairstyle-tab" data-gender="girl">女款</button>
<button class="hairstyle-tab" data-gender="boy">男款</button>
</div>
<input type="text" class="search-box" id="search" placeholder="搜索发型ID...">
<div class="hairstyle-grid" id="hairstyle-grid"><div class="loading-hairstyles">加载发型中...</div></div>
<div class="selected-hairstyle-info" id="selected-info">未选择发型</div>
<h3 style="margin-top:20px">② 涂抹遮罩</h3>
<div class="tool-group">
<button class="btn btn-tool active" id="btn-brush">🖌️ 画笔</button>
<button class="btn btn-tool" id="btn-eraser">🧽 橡皮</button>
</div>
<div class="tool-group">
<label>画笔粗细 <span style="float:right;color:#4ecca3" id="size-val">25px</span></label>
<input type="range" id="brush-size" min="5" max="60" value="25" style="width:100%">
</div>
<button class="btn btn-danger" id="btn-clear">🗑️ 清空遮罩</button>
<h3 style="margin-top:20px">③ 生成</h3>
<div class="tool-group">
<label><input type="checkbox" id="is-hr" checked> 高清模式 (较慢)</label>
</div>
<button class="btn btn-primary" id="btn-generate" disabled>✨ 生成生发</button>
<div class="hint">
<b>使用说明</b><br>
1. 左侧选择一个发型<br>
2. 上传一张人头像照片<br>
3. 用画笔在头发稀少/发际线区域涂抹(红色=生发区)<br>
4. 点"生成",等待30-90秒<br>
工作流:换发型粗推理 + LoRA + mask并集(原头发∪新发型∪手绘)
</div>
</div>
<div class="workspace">
<div class="canvas-wrap" id="canvas-wrap">
<div class="upload-zone" id="upload-zone">
<svg viewBox="0 0 24 24"><path d="M19 13v6H5v-6H3v8h18v-8zM6 9l1.41 1.41L11 6.83V18h2V6.83l3.59 3.58L18 9l-6-6z"/></svg>
<p class="highlight">点击或拖拽上传图片</p>
<p>建议人脸清晰正面照</p>
</div>
<input type="file" id="file-input" accept="image/*" style="display:none">
<div id="canvas-container" style="display:none">
<canvas id="img-canvas"></canvas>
<canvas id="mask-canvas"></canvas>
</div>
</div>
<div id="status"><span class="spinner"></span><span id="status-text">正在生成,请稍候...</span></div>
<div id="error-msg"></div>
<div class="result-section" id="result-section">
<h3>对比结果</h3>
<div class="result-grid">
<div class="result-card"><h4>原图</h4><img id="result-orig" alt="原图"></div>
<div class="result-card"><h4>生发结果</h4><img id="result-new" alt="结果"><a class="download-btn" id="download-link" download="hairgrow_result.jpg">⬇️ 下载结果</a></div>
</div>
</div>
</div>
</div>
<script>
(function(){
const $=id=>document.getElementById(id);
const uploadZone=$("upload-zone"),fileInput=$("file-input"),canvasContainer=$("canvas-container");
const imgCanvas=$("img-canvas"),maskCanvas=$("mask-canvas");
const imgCtx=imgCanvas.getContext("2d"),maskCtx=maskCanvas.getContext("2d",{willReadFrequently:true});
let mode="brush",brushSize=25,drawing=false,lastX=0,lastY=0;
let selectedHairId=null,allHairstyles=[],imgB64Orig=null;
async function loadHairstyles(){
try{
const r=await fetch("/api/hairstyles");const d=await r.json();
if(d.state!==0)throw new Error(d.msg);
allHairstyles=d.data;renderHairstyles("all");
}catch(e){$("hairstyle-grid").innerHTML='<div class="loading-hairstyles">加载失败: '+e.message+'</div>';}
}
function renderHairstyles(gf){
const s=$("search").value.trim().toLowerCase();
const f=allHairstyles.filter(x=>{if(gf!=="all"&&x.gender!==gf)return false;if(s&&!x.hair_id.toLowerCase().includes(s))return false;return true;});
const g=$("hairstyle-grid");
if(f.length===0){g.innerHTML='<div class="loading-hairstyles">无匹配发型</div>';return;}
g.innerHTML="";
f.slice(0,60).forEach(x=>{
const it=document.createElement("div");
it.className="hairstyle-item"+(x.hair_id===selectedHairId?" selected":"");
it.dataset.hairId=x.hair_id;
const im=document.createElement("img");im.loading="lazy";im.src="/preview/"+x.hair_id;
im.onerror=()=>{im.style.visibility="hidden";};
it.appendChild(im);it.onclick=()=>selectHairstyle(x.hair_id);g.appendChild(it);
});
if(f.length>60){const m=document.createElement("div");m.className="loading-hairstyles";m.style.gridColumn="1/-1";m.textContent="还有 "+(f.length-60)+" 个,请搜索";g.appendChild(m);}
}
function selectHairstyle(id){
selectedHairId=id;
document.querySelectorAll(".hairstyle-item").forEach(el=>el.classList.toggle("selected",el.dataset.hairId===id));
$("selected-info").textContent="已选: "+id;updateGenerateBtn();
}
document.querySelectorAll(".hairstyle-tab").forEach(t=>{t.onclick=()=>{document.querySelectorAll(".hairstyle-tab").forEach(x=>x.classList.remove("active"));t.classList.add("active");renderHairstyles(t.dataset.gender);};});
$("search").addEventListener("input",()=>{const a=document.querySelector(".hairstyle-tab.active");renderHairstyles(a?a.dataset.gender:"all");});
loadHairstyles();
$("brush-size").addEventListener("input",e=>{brushSize=+e.target.value;$("size-val").textContent=brushSize+"px";});
$("btn-brush").addEventListener("click",()=>{mode="brush";$("btn-brush").classList.add("active");$("btn-eraser").classList.remove("active");maskCanvas.classList.remove("eraser");});
$("btn-eraser").addEventListener("click",()=>{mode="eraser";$("btn-eraser").classList.add("active");$("btn-brush").classList.remove("active");maskCanvas.classList.add("eraser");});
$("btn-clear").addEventListener("click",()=>{maskCtx.globalCompositeOperation="source-over";maskCtx.clearRect(0,0,maskCanvas.width,maskCanvas.height);});
uploadZone.addEventListener("click",()=>fileInput.click());
uploadZone.addEventListener("dragover",e=>{e.preventDefault();uploadZone.classList.add("dragover");});
uploadZone.addEventListener("dragleave",()=>uploadZone.classList.remove("dragover"));
uploadZone.addEventListener("drop",e=>{e.preventDefault();uploadZone.classList.remove("dragover");if(e.dataTransfer.files[0])loadImage(e.dataTransfer.files[0]);});
fileInput.addEventListener("change",e=>{if(e.target.files[0])loadImage(e.target.files[0]);});
function loadImage(file){
if(!file.type.startsWith("image/")){alert("请上传图片文件");return;}
const rd=new FileReader();
rd.onload=e=>{
const im=new Image();
im.onload=()=>{
let w=im.width,h=im.height;const MAX=1024;
if(w>MAX||h>MAX){const r=Math.min(MAX/w,MAX/h);w=Math.round(w*r);h=Math.round(h*r);}
w=w-(w%8);h=h-(h%8);
imgCanvas.width=maskCanvas.width=w;imgCanvas.height=maskCanvas.height=h;
imgCtx.drawImage(im,0,0,w,h);imgB64Orig=imgCanvas.toDataURL("image/jpeg",0.95);
maskCtx.clearRect(0,0,w,h);
uploadZone.style.display="none";canvasContainer.style.display="inline-block";
updateGenerateBtn();$("result-section").classList.remove("show");$("error-msg").style.display="none";
};im.src=e.target.result;
};rd.readAsDataURL(file);
}
function updateGenerateBtn(){$("btn-generate").disabled=!(selectedHairId&&imgB64Orig);}
function getPos(e){const r=maskCanvas.getBoundingClientRect();const sx=maskCanvas.width/r.width,sy=maskCanvas.height/r.height;const p=e.touches?e.touches[0]:e;return{x:(p.clientX-r.left)*sx,y:(p.clientY-r.top)*sy};}
function startDraw(e){e.preventDefault();drawing=true;const p=getPos(e);lastX=p.x;lastY=p.y;drawDot(p.x,p.y);}
function draw(e){if(!drawing)return;e.preventDefault();const p=getPos(e);drawLine(lastX,lastY,p.x,p.y);lastX=p.x;lastY=p.y;}
function endDraw(){drawing=false;}
function drawDot(x,y){if(mode==="brush"){maskCtx.globalCompositeOperation="source-over";maskCtx.fillStyle="rgba(255,50,50,0.45)";}else{maskCtx.globalCompositeOperation="destination-out";maskCtx.fillStyle="rgba(0,0,0,1)";}maskCtx.beginPath();maskCtx.arc(x,y,brushSize/2,0,Math.PI*2);maskCtx.fill();}
function drawLine(x1,y1,x2,y2){if(mode==="brush"){maskCtx.globalCompositeOperation="source-over";maskCtx.strokeStyle="rgba(255,50,50,0.45)";}else{maskCtx.globalCompositeOperation="destination-out";maskCtx.strokeStyle="rgba(0,0,0,1)";}maskCtx.beginPath();maskCtx.moveTo(x1,y1);maskCtx.lineTo(x2,y2);maskCtx.lineWidth=brushSize;maskCtx.lineCap="round";maskCtx.lineJoin="round";maskCtx.stroke();}
maskCanvas.addEventListener("mousedown",startDraw);maskCanvas.addEventListener("mousemove",draw);
maskCanvas.addEventListener("mouseup",endDraw);maskCanvas.addEventListener("mouseleave",endDraw);
maskCanvas.addEventListener("touchstart",startDraw,{passive:false});maskCanvas.addEventListener("touchmove",draw,{passive:false});
maskCanvas.addEventListener("touchend",endDraw);
$("btn-generate").addEventListener("click",async()=>{
const md=maskCtx.getImageData(0,0,maskCanvas.width,maskCanvas.height);
let hasMask=false;for(let i=3;i<md.data.length;i+=4){if(md.data[i]>30){hasMask=true;break;}}
if(!hasMask){alert("请先用画笔涂抹需要生发的区域");return;}
const isHr=$("is-hr").checked;
const tmp=document.createElement("canvas");tmp.width=maskCanvas.width;tmp.height=maskCanvas.height;
const tmpCtx=tmp.getContext("2d");
const md2=maskCtx.getImageData(0,0,maskCanvas.width,maskCanvas.height);
const out=tmpCtx.createImageData(tmp.width,tmp.height);
for(let i=0;i<md2.data.length;i+=4){const v=md2.data[i+3]>30?255:0;out.data[i]=v;out.data[i+1]=v;out.data[i+2]=v;out.data[i+3]=255;}
tmpCtx.putImageData(out,0,0);
const maskB64=tmp.toDataURL("image/png");
$("btn-generate").disabled=true;$("status").style.display="block";
$("status-text").textContent="正在生成(换发型工作流+LoRA),约30-90秒...";
$("error-msg").style.display="none";$("result-section").classList.remove("show");
try{
const resp=await fetch("/api/grow",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({img:imgB64Orig,mask:maskB64,hair_id:selectedHairId,is_hr:String(isHr)})});
const data=await resp.json();$("status").style.display="none";
if(data.state===0){
const url="data:image/jpeg;base64,"+data.result;
$("result-orig").src=imgB64Orig;$("result-new").src=url;$("download-link").href=url;
$("result-section").classList.add("show");$("result-section").scrollIntoView({behavior:"smooth"});
maskCtx.clearRect(0,0,maskCanvas.width,maskCanvas.height);
}else{$("error-msg").textContent="❌ "+(data.msg||"生成失败");$("error-msg").style.display="block";}
}catch(err){$("status").style.display="none";$("error-msg").textContent="❌ 请求失败: "+err.message;$("error-msg").style.display="block";}
updateGenerateBtn();
});
})();
</script>
</body>
</html>
+427
View File
@@ -0,0 +1,427 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手绘mask换发型</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; font-size: 14px; }
.header { background: #16213e; padding: 16px 28px; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 19px; font-weight: 600; }
.header p { font-size: 12px; color: #888; margin-top: 3px; }
.header .links a { color: #4ecca3; font-size: 12px; text-decoration: none; margin-left: 12px; }
.container { display: flex; gap: 18px; padding: 18px; max-width: 1800px; margin: 0 auto; }
/* 左侧:工具+参数 */
.panel { width: 290px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 16px; height: fit-content; max-height: 92vh; overflow-y: auto; }
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.panel h3 { font-size: 13px; color: #4ecca3; margin: 14px 0 8px; text-transform: uppercase; letter-spacing: 1px; padding-bottom: 5px; border-bottom: 1px solid #0f3460; }
.panel h3:first-child { margin-top: 0; }
.hint { font-size: 11px; color: #888; line-height: 1.6; margin: 6px 0 10px; }
/* 画笔工具 */
.tool-row { display: flex; gap: 6px; margin-bottom: 10px; }
.btn-tool { flex: 1; padding: 8px; background: #0f3460; color: #ccc; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; }
.btn-tool.active { background: #4ecca3; color: #16213e; font-weight: 600; }
.btn-tool:hover { background: #1a4a7a; }
.btn-tool.active:hover { background: #6ee0bd; }
.btn-danger { width: 100%; padding: 7px; background: #c0392b; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; margin-bottom: 10px; }
.btn-danger:hover { background: #e74c3c; }
.field { margin-bottom: 12px; }
.field-label { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #bbb; margin-bottom: 5px; }
.field-label .val { color: #4ecca3; font-family: monospace; font-size: 11px; }
.field input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: #0f3460; border-radius: 2px; outline: none; }
.field input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #4ecca3; cursor: pointer; }
.switch { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; font-size: 13px; color: #ccc; cursor: pointer; }
.switch:hover { color: #eee; }
.toggle { position: relative; width: 36px; height: 20px; background: #0f3460; border-radius: 10px; transition: .2s; flex-shrink: 0; }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #888; border-radius: 50%; transition: .2s; }
.switch.on .toggle { background: #4ecca3; }
.switch.on .toggle::after { left: 18px; background: #16213e; }
.pair { display: flex; gap: 8px; }
.pair .field { flex: 1; }
.readonly-info { font-size: 11px; color: #777; padding: 8px 10px; background: #0d1b3e; border-radius: 5px; line-height: 1.6; }
.readonly-info code { color: #aaa; background: #16213e; padding: 1px 5px; border-radius: 3px; }
.btn { display: block; width: 100%; padding: 10px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: .15s; }
.btn-primary { background: #4ecca3; color: #16213e; font-weight: 600; font-size: 15px; padding: 12px; margin-top: 8px; }
.btn-primary:hover { background: #6ee0bd; }
.btn-primary:disabled { background: #555; color: #999; cursor: not-allowed; }
/* 中间发型选择 */
.mid-panel { width: 200px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 14px; height: fit-content; max-height: 92vh; overflow-y: auto; }
.mid-panel h3 { font-size: 12px; color: #4ecca3; margin-bottom: 10px; }
.hairstyle-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.hairstyle-item { position: relative; cursor: pointer; border-radius: 5px; overflow: hidden; border: 2px solid transparent; aspect-ratio: 1; background: #0d1b3e; }
.hairstyle-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hairstyle-item.ready { border-color: #2a5a4a; }
.hairstyle-item.selected { border-color: #4ecca3; }
.hairstyle-item.selected::after { content: '✓'; position: absolute; top: 1px; right: 3px; color: #4ecca3; font-weight: bold; text-shadow: 0 0 3px #000; font-size: 11px; }
.hairstyle-item .name { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); color: #fff; font-size: 10px; padding: 8px 2px 2px; text-align: center; }
.hairstyle-item.pending { opacity: 0.4; cursor: not-allowed; }
/* 右侧画板+结果 */
.workspace { flex: 1; min-width: 0; }
.canvas-wrap { background: #0d0d1a; border-radius: 10px; padding: 18px; text-align: center; min-height: 280px; display: flex; align-items: center; justify-content: center; }
.upload-zone { width: 100%; max-width: 600px; border: 2px dashed #0f3460; border-radius: 10px; padding: 40px 20px; text-align: center; cursor: pointer; transition: .2s; }
.upload-zone:hover { border-color: #4ecca3; background: rgba(78,204,163,.05); }
.upload-zone svg { width: 42px; height: 42px; fill: #4ecca3; margin-bottom: 10px; }
.upload-zone p { color: #888; font-size: 13px; }
.upload-zone p.highlight { color: #4ecca3; }
#canvas-container { position: relative; display: inline-block; max-width: 100%; }
#img-canvas, #mask-canvas { display: block; max-width: 100%; border-radius: 6px; }
#mask-canvas { position: absolute; top: 0; left: 0; cursor: crosshair; touch-action: none; }
#mask-canvas.eraser { cursor: cell; }
.canvas-tip { font-size: 11px; color: #666; margin-top: 8px; }
#status { text-align: center; padding: 24px; color: #4ecca3; display: none; }
.spinner { display: inline-block; width: 18px; height: 18px; border: 3px solid #0f3460; border-top-color: #4ecca3; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 8px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
#error-msg { color: #e74c3c; padding: 16px; display: none; font-size: 13px; background: rgba(231,76,60,.1); border-radius: 6px; margin-top: 12px; }
.result-top { margin-top: 16px; background: #16213e; border-radius: 10px; padding: 16px; display: none; }
.result-top.show { display: block; }
.result-top h3 { font-size: 14px; color: #4ecca3; margin-bottom: 12px; }
.result-pair { display: flex; gap: 16px; flex-wrap: wrap; }
.result-card { flex: 1; min-width: 240px; }
.result-card h4 { font-size: 12px; color: #aaa; margin-bottom: 6px; text-align: center; }
.result-card img { width: 100%; border-radius: 6px; display: block; }
.params-used { margin-top: 12px; padding: 10px 12px; background: #0d1b3e; border-radius: 6px; font-size: 11px; color: #888; line-height: 1.7; font-family: monospace; }
.steps-section { margin-top: 16px; }
.step-item { background: #16213e; border-radius: 10px; padding: 16px; margin-bottom: 14px; }
.step-title { font-size: 15px; font-weight: 600; color: #4ecca3; margin-bottom: 4px; }
.step-desc { font-size: 12px; color: #999; margin-bottom: 12px; line-height: 1.6; }
.step-images { display: flex; gap: 12px; flex-wrap: wrap; }
.step-img-card { flex: 1; min-width: 180px; max-width: 280px; }
.step-img-card .lbl { font-size: 11px; color: #888; margin-bottom: 4px; text-align: center; }
.step-img-card img { width: 100%; border-radius: 6px; display: block; border: 1px solid #0f3460; cursor: zoom-in; transition: .15s; }
.step-img-card img:hover { border-color: #4ecca3; }
#lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; }
#lightbox img { max-width: 92%; max-height: 92%; border-radius: 8px; }
#lightbox.show { display: flex; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>🖌️ 手绘mask换发型(重绘区由你画)</h1>
<p>上传人像 → 用画笔涂出要重绘的区域 → 选发型 → 生成。SD 只在你涂的区域重绘</p>
</div>
<div class="links">
<a href="/debug">→ 全自动调试台</a>
<a href="/hairline">→ 发际线带实验</a>
<a href="/test_new">→ 简易测试</a>
</div>
</div>
<div class="container">
<!-- 左侧:画笔工具 + 参数 -->
<div class="panel">
<h3>① 画笔工具</h3>
<div class="hint">在右侧图片上涂抹需要重绘的区域(白色=重绘,黑色=保留原图)</div>
<div class="tool-row">
<button class="btn-tool active" id="tool-brush">🖌️ 画笔</button>
<button class="btn-tool" id="tool-eraser">🧹 橡皮</button>
</div>
<div class="field">
<div class="field-label">画笔大小 <span class="val" id="size-val">25px</span></div>
<input type="range" id="brush-size" min="5" max="80" value="25">
</div>
<button class="btn-danger" id="btn-clear">✕ 清除全部涂抹</button>
<h3>② 参数</h3>
<div class="switch on" id="sw-is_hr" onclick="toggleSwitch('is_hr', this)">
<span>高清模式 (1152x1536)</span>
<div class="toggle"></div>
</div>
<div class="switch" id="sw-strict_mask" onclick="toggleSwitch('strict_mask', this)">
<span>严格mask贴回 <span style="color:#666;font-size:10px">(mask外保留原图)</span></span>
<div class="toggle"></div>
</div>
<div class="switch on" id="sw-seamless_blend" onclick="toggleSwitch('seamless_blend', this)">
<span>泊松融合消接缝 <span style="color:#e9b949;font-size:10px">⚠需同时开「严格mask」</span></span>
<div class="toggle"></div>
</div>
<div class="field">
<div class="field-label">denoising_strength <span class="val" id="v-den">0.60</span></div>
<input type="range" id="den" min="0.1" max="1.0" step="0.05" value="0.6" oninput="syncFloat('den')">
</div>
<div class="pair">
<div class="field">
<div class="field-label">dilate_kernel <span class="val" id="v-dk0">6</span></div>
<input type="number" id="dk0" value="6" min="1" max="40" oninput="$('v-dk0').textContent=this.value">
</div>
<div class="field">
<div class="field-label">(膨胀) <span class="val" id="v-dk1">18</span></div>
<input type="number" id="dk1" value="18" min="1" max="40" oninput="$('v-dk1').textContent=this.value">
</div>
</div>
<div class="field">
<div class="field-label">feather_px <span class="val" id="v-fp">0</span><span class="tip" title="严格mask贴回的边缘羽化像素。0=硬边缘,>0=高斯模糊边缘平滑过渡(消除贴回接缝)"></span></div>
<input type="range" id="fp" min="0" max="60" step="1" value="0" oninput="syncInt('fp')">
</div>
<div class="readonly-info">
<b style="color:#888">webui端固定:</b><br>
<code>cfg=7</code> <code>steps=20</code> <code>sampler=DPM++ 2M Karras</code>
</div>
<h3 style="color:#4ecca3;margin-top:14px">③ enhance 二次增强(可选)</h3>
<div class="switch" id="sw-enhance" onclick="toggleSwitch('enhance', this)">
<span>开启enhance <span class="tip" title="换发型后对结果再做一次低强度SD重绘,让发丝更清晰(+15-20秒)"></span></span>
<div class="toggle"></div>
</div>
<div class="field">
<div class="field-label">enhance_denoising <span class="val" id="v-ed">0.35</span><span class="tip" title="enhance重绘强度。越低越保留原图,越高发丝变化越大"></span></div>
<input type="range" id="ed" min="0.1" max="0.8" step="0.05" value="0.35" oninput="syncFloat('ed')">
</div>
<button class="btn btn-primary" id="btn-run" disabled>▶ 生成(手绘区换发型)</button>
</div>
<!-- 中间发型选择 -->
<div class="mid-panel">
<h3>③ 选发型</h3>
<div class="hairstyle-grid" id="hairstyle-grid"><div style="color:#666;font-size:11px">加载中...</div></div>
<div style="font-size:11px;color:#4ecca3;margin-top:8px;text-align:center" id="sel-info">未选择</div>
<button class="btn-tool" style="margin-top:8px;width:100%" onclick="loadStatus()">↻ 刷新</button>
</div>
<!-- 右侧画板+结果 -->
<div class="workspace">
<div class="canvas-wrap">
<div class="upload-zone" id="upload-zone">
<svg viewBox="0 0 24 24"><path d="M19 13v6H5v-6H3v8h18v-8zM6 9l1.41 1.41L11 6.83V18h2V6.83l3.59 3.58L18 9l-6-6z"/></svg>
<p class="highlight">点击上传人像</p>
<p>正面清晰照效果最佳</p>
</div>
<div id="canvas-container" style="display:none">
<canvas id="img-canvas"></canvas>
<canvas id="mask-canvas"></canvas>
</div>
<input type="file" id="file-input" accept="image/*" style="display:none">
</div>
<div class="canvas-tip" id="canvas-tip" style="display:none;text-align:center">用左侧画笔在图片上涂抹要重绘的区域</div>
<div id="status"><span class="spinner"></span><span id="status-text">生成中,约40-90秒...</span></div>
<div id="error-msg"></div>
<div class="result-top" id="result-top">
<h3>最终对比</h3>
<div class="result-pair">
<div class="result-card"><h4>原图</h4><img id="r-orig"></div>
<div class="result-card"><h4>换发型结果(仅手绘区)</h4><img id="r-new"></div>
</div>
<div class="params-used" id="params-used"></div>
</div>
<div class="steps-section" id="steps-section"></div>
</div>
</div>
<div id="lightbox" onclick="this.classList.remove('show')"><img id="lightbox-img"></div>
<script>
(function(){
const $=id=>document.getElementById(id);
const HAIRSTYLES = [
{face:"圆",name:"圆-心形"},{face:"圆",name:"圆-椭圆"},{face:"圆",name:"圆-波浪"},{face:"圆",name:"圆-直线"},{face:"圆",name:"圆-花瓣"},
{face:"心形",name:"心形-心形"},{face:"心形",name:"心形-椭圆"},{face:"心形",name:"心形-波浪"},{face:"心形",name:"心形-直线"},{face:"心形",name:"心形-花瓣"},
{face:"方脸",name:"方脸-心形"},{face:"方脸",name:"方脸-椭圆"},{face:"方脸",name:"方脸-波浪"},{face:"方脸",name:"方脸-直线"},{face:"方脸",name:"方脸-花瓣"},
{face:"椭圆",name:"椭圆-心形"},{face:"椭圆",name:"椭圆-椭圆"},{face:"椭圆",name:"椭圆-波浪"},{face:"椭圆",name:"椭圆-直线"},{face:"椭圆",name:"椭圆-花瓣"},
{face:"菱形",name:"菱形-心"},{face:"菱形",name:"菱形-椭圆"},{face:"菱形",name:"菱形-波浪"},{face:"菱形",name:"菱形-直线"},{face:"菱形",name:"菱形-花瓣"},
{face:"长",name:"长-心形"},{face:"长",name:"长 -椭圆"},{face:"长",name:"长 -波浪"},{face:"长",name:"长 -直线"},{face:"长",name:"长 -花瓣"}
];
let readySet=new Set(), selectedHair=null, imgB64Orig=null;
// 画板状态
const imgCanvas=$("img-canvas"),maskCanvas=$("mask-canvas");
const imgCtx=imgCanvas.getContext("2d"),maskCtx=maskCanvas.getContext("2d",{willReadFrequently:true});
let mode="brush",brushSize=25,drawing=false,lastX=0,lastY=0;
async function loadStatus(){
try{
const r=await fetch("/api/hairstyles");const d=await r.json();
if(d.state!==0)throw new Error(d.msg);
readySet=new Set(d.data.map(x=>x.hair_id));
}catch(e){readySet=new Set();}
renderGrid();
}
function renderGrid(){
const g=$("hairstyle-grid");g.innerHTML="";
HAIRSTYLES.forEach(h=>{
const ready=readySet.has(h.name);
const it=document.createElement("div");
it.className="hairstyle-item"+(ready?" ready":" pending")+(h.name===selectedHair?" selected":"");
it.innerHTML=`<img loading="lazy" src="/train_src/${encodeURIComponent(h.name)}" onerror="this.style.objectFit='contain';this.src='data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22><text x=%2250%22 y=%2255%22 text-anchor=%22middle%22 fill=%22%23666%22 font-size=%2210%22>训练中</text></svg>'"><div class="name">${h.name}</div>`;
if(ready) it.onclick=()=>{
selectedHair=h.name;
$("sel-info").textContent="已选: "+h.name;
renderGrid(); updateBtn();
};
g.appendChild(it);
});
}
window.toggleSwitch=(key,el)=>{ el.classList.toggle("on"); };
// 浮点滑块联动显示
window.syncFloat=(id)=>{ $("v-"+id).textContent=parseFloat($(id).value).toFixed(2); };
// 整数滑块联动显示
window.syncInt=(id)=>{ $("v-"+id).textContent=$(id).value; };
// 上传图片
$("upload-zone").onclick=()=>$("file-input").click();
$("file-input").onchange=e=>{ if(e.target.files[0]) loadImage(e.target.files[0]); };
function loadImage(file){
if(!file.type.startsWith("image/")){alert("请上传图片");return;}
const rd=new FileReader();
rd.onload=e=>{
const im=new Image();
im.onload=()=>{
// 限制最大尺寸,避免太大
let w=im.width,h=im.height;
const MAX=1024;
if(Math.max(w,h)>MAX){const sc=MAX/Math.max(w,h);w=Math.round(w*sc);h=Math.round(h*sc);}
imgCanvas.width=w;imgCanvas.height=h;
maskCanvas.width=w;maskCanvas.height=h;
imgCtx.drawImage(im,0,0,w,h);
maskCtx.clearRect(0,0,w,h);
imgB64Orig=imgCanvas.toDataURL("image/jpeg",0.95);
$("canvas-container").style.display="inline-block";
$("upload-zone").style.display="none";
$("canvas-tip").style.display="block";
updateBtn();
};
im.src=e.target.result;
};
rd.readAsDataURL(file);
}
// 画笔工具切换
$("tool-brush").onclick=()=>{mode="brush";$("tool-brush").classList.add("active");$("tool-eraser").classList.remove("active");maskCanvas.classList.remove("eraser");};
$("tool-eraser").onclick=()=>{mode="eraser";$("tool-eraser").classList.add("active");$("tool-brush").classList.remove("active");maskCanvas.classList.add("eraser");};
$("brush-size").oninput=e=>{brushSize=+e.target.value;$("size-val").textContent=brushSize+"px";};
$("btn-clear").onclick=()=>{maskCtx.globalCompositeOperation="source-over";maskCtx.clearRect(0,0,maskCanvas.width,maskCanvas.height);};
// 绘制
function getPos(e){
const r=maskCanvas.getBoundingClientRect();
const sc=maskCanvas.width/r.width;
const cx=(e.touches?e.touches[0].clientX:e.clientX)-r.left;
const cy=(e.touches?e.touches[0].clientY:e.clientY)-r.top;
return [cx*sc,cy*sc];
}
function startDraw(e){e.preventDefault();drawing=true;const[x,y]=getPos(e);lastX=x;lastY=y;drawDot(x,y);}
function moveDraw(e){if(!drawing)return;e.preventDefault();const[x,y]=getPos(e);drawLine(lastX,lastY,x,y);lastX=x;lastY=y;}
function endDraw(){drawing=false;}
function drawDot(x,y){
if(mode==="brush"){maskCtx.globalCompositeOperation="source-over";maskCtx.fillStyle="rgba(255,60,80,0.5)";}
else{maskCtx.globalCompositeOperation="destination-out";maskCtx.fillStyle="rgba(0,0,0,1)";}
maskCtx.beginPath();maskCtx.arc(x,y,brushSize/2,0,Math.PI*2);maskCtx.fill();
}
function drawLine(x1,y1,x2,y2){
if(mode==="brush"){maskCtx.globalCompositeOperation="source-over";maskCtx.strokeStyle="rgba(255,60,80,0.5)";}
else{maskCtx.globalCompositeOperation="destination-out";maskCtx.strokeStyle="rgba(0,0,0,1)";}
maskCtx.beginPath();maskCtx.moveTo(x1,y1);maskCtx.lineTo(x2,y2);
maskCtx.lineWidth=brushSize;maskCtx.lineCap="round";maskCtx.lineJoin="round";maskCtx.stroke();
}
maskCanvas.addEventListener("mousedown",startDraw);
maskCanvas.addEventListener("mousemove",moveDraw);
window.addEventListener("mouseup",endDraw);
maskCanvas.addEventListener("touchstart",startDraw,{passive:false});
maskCanvas.addEventListener("touchmove",moveDraw,{passive:false});
maskCanvas.addEventListener("touchend",endDraw);
function updateBtn(){ $("btn-run").disabled=!(selectedHair&&imgB64Orig); }
// 提取mask为黑白二值PNG base64
function extractMaskB64(){
const tmp=document.createElement("canvas");
tmp.width=maskCanvas.width;tmp.height=maskCanvas.height;
const tc=tmp.getContext("2d");
const md=maskCtx.getImageData(0,0,maskCanvas.width,maskCanvas.height);
const out=tc.createImageData(tmp.width,tmp.height);
for(let i=0;i<md.data.length;i+=4){
const v=md.data[i+3]>30?255:0; // 用alpha判断是否涂抹过
out.data[i]=v;out.data[i+1]=v;out.data[i+2]=v;out.data[i+3]=255;
}
tc.putImageData(out,0,0);
return tmp.toDataURL("image/png");
}
// 执行
$("btn-run").onclick=async()=>{
if(!selectedHair||!imgB64Orig) return;
// 检查是否涂抹了mask
const md=maskCtx.getImageData(0,0,maskCanvas.width,maskCanvas.height);
let hasMask=false;
for(let i=3;i<md.data.length;i+=4){if(md.data[i]>30){hasMask=true;break;}}
if(!hasMask){alert("请先用画笔涂抹需要重绘的区域");return;}
const maskB64=extractMaskB64();
const params={
strict_mask:$("sw-strict_mask").classList.contains("on"),
seamless_blend:$("sw-seamless_blend").classList.contains("on"),
is_hr:$("sw-is_hr").classList.contains("on"),
dilate_kernel:[parseInt($("dk0").value),parseInt($("dk1").value)],
feather_px:parseInt($("fp").value),
denoising_strength:parseFloat($("den").value),
enhance:$("sw-enhance").classList.contains("on"),
enhance_denoising:parseFloat($("ed").value),
};
$("btn-run").disabled=true;
$("status").style.display="block";
$("status-text").textContent = params.enhance
? "生成中(换发型 + enhance增强),约60-110秒..."
: "生成中(粗推理→手绘mask→warpAffine→SD→贴回),约40-90秒...";
$("error-msg").style.display="none";
$("result-top").classList.remove("show");
$("steps-section").innerHTML="";
try{
const resp=await fetch("/api/swap_manual",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify(Object.assign({img:imgB64Orig,mask:maskB64,hair_id:selectedHair},params))
});
const data=await resp.json();
$("status").style.display="none";
if(data.state===0){
const url="data:image/jpeg;base64,"+data.result;
$("r-orig").src=imgB64Orig;$("r-new").src=url;
let ph="";Object.keys(data.params).forEach(k=>{
let v=data.params[k];if(Array.isArray(v))v="["+v.join(",")+"]";
if(typeof v==="boolean")v=v?"开":"关";
ph+=k+" = "+v+" ";
});
$("params-used").textContent="本次参数: "+ph;
$("result-top").classList.add("show");
// 步骤画廊
const sc=$("steps-section");
data.steps.forEach(step=>{
const div=document.createElement("div");div.className="step-item";
let imgs='<div class="step-images">';
step.images.forEach(im=>{imgs+='<div class="step-img-card"><div class="lbl">'+im.label+'</div><img src="data:image/jpeg;base64,'+im.b64+'" data-full="data:image/jpeg;base64,'+im.b64+'"></div>';});
imgs+='</div>';
div.innerHTML='<div class="step-title">'+step.title+'</div><div class="step-desc">'+step.desc+'</div>'+imgs;
sc.appendChild(div);
});
sc.querySelectorAll("img").forEach(img=>img.onclick=()=>showLightbox(img.dataset.full));
$("result-top").scrollIntoView({behavior:"smooth"});
}else{
$("error-msg").textContent="❌ "+(data.msg||"失败");
$("error-msg").style.display="block";
}
}catch(err){
$("status").style.display="none";
$("error-msg").textContent="❌ 请求失败: "+err.message;
$("error-msg").style.display="block";
}
updateBtn();
};
function showLightbox(src){$("lightbox-img").src=src;$("lightbox").classList.add("show");}
loadStatus();
})();
</script>
</body>
</html>
+210
View File
@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>换发型测试</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
.header { background: #16213e; padding: 18px 28px; border-bottom: 1px solid #0f3460; }
.header h1 { font-size: 20px; font-weight: 600; }
.header p { font-size: 13px; color: #888; margin-top: 4px; }
.container { display: flex; gap: 20px; padding: 20px; max-width: 1700px; margin: 0 auto; }
.panel { width: 280px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 18px; height: fit-content; max-height: 90vh; overflow-y: auto; }
.panel h3 { font-size: 14px; color: #4ecca3; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px; }
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.hairstyle-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.hairstyle-tab { flex: 1; padding: 6px; text-align: center; background: #0f3460; border: none; border-radius: 4px; color: #aaa; cursor: pointer; font-size: 12px; }
.hairstyle-tab.active { background: #4ecca3; color: #16213e; font-weight: 600; }
.search-box { width: 100%; padding: 6px 10px; background: #0d1b3e; border: 1px solid #0f3460; border-radius: 4px; color: #eee; font-size: 12px; margin-bottom: 10px; }
.hairstyle-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; max-height: 420px; overflow-y: auto; }
.hairstyle-item { position: relative; cursor: pointer; border-radius: 4px; overflow: hidden; border: 2px solid transparent; aspect-ratio: 1; background: #0d1b3e; }
.hairstyle-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hairstyle-item.selected { border-color: #4ecca3; }
.hairstyle-item.selected::after { content: '✓'; position: absolute; top: 2px; right: 4px; color: #4ecca3; font-weight: bold; text-shadow: 0 0 3px #000; }
.selected-hairstyle-info { font-size: 11px; color: #888; margin-top: 8px; text-align: center; word-break: break-all; }
.tool-group { margin-bottom: 16px; }
.tool-group label { display: block; font-size: 13px; color: #aaa; margin-bottom: 6px; }
.btn { display: block; width: 100%; padding: 9px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; margin-bottom: 8px; transition: .15s; }
.btn-primary { background: #4ecca3; color: #16213e; font-weight: 600; font-size: 15px; padding: 12px; }
.btn-primary:hover { background: #6ee0bd; }
.btn-primary:disabled { background: #555; color: #999; cursor: not-allowed; }
.hint { font-size: 12px; color: #666; line-height: 1.6; margin-top: 14px; padding: 10px; background: #0d1b3e; border-radius: 6px; }
.workspace { flex: 1; min-width: 0; }
.canvas-wrap { position: relative; background: #0d0d1a; border-radius: 10px; padding: 20px; text-align: center; min-height: 400px; display: flex; align-items: center; justify-content: center; }
.upload-zone { width: 100%; max-width: 500px; border: 2px dashed #0f3460; border-radius: 10px; padding: 50px 20px; text-align: center; cursor: pointer; transition: .2s; }
.upload-zone:hover, .upload-zone.dragover { border-color: #4ecca3; background: rgba(78,204,163,.05); }
.upload-zone svg { width: 48px; height: 48px; fill: #4ecca3; margin-bottom: 12px; }
.upload-zone p { color: #888; font-size: 14px; }
.upload-zone p.highlight { color: #4ecca3; margin-bottom: 4px; }
#preview-img { display: none; max-width: 100%; max-height: 500px; border-radius: 6px; }
.result-section { margin-top: 20px; background: #16213e; border-radius: 10px; padding: 18px; display: none; }
.result-section.show { display: block; }
.result-section h3 { font-size: 14px; color: #4ecca3; margin-bottom: 14px; }
.result-grid { display: flex; gap: 16px; flex-wrap: wrap; }
.result-card { flex: 1; min-width: 250px; }
.result-card h4 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-align: center; }
.result-card img { width: 100%; border-radius: 6px; display: block; }
.download-btn { display: inline-block; margin-top: 8px; padding: 6px 14px; background: #0f3460; color: #eee; border-radius: 4px; font-size: 12px; text-decoration: none; }
/* 步骤画廊 */
.steps-section { margin-top: 20px; background: #16213e; border-radius: 10px; padding: 18px; display: none; }
.steps-section.show { display: block; }
.steps-section h3 { font-size: 14px; color: #4ecca3; margin-bottom: 6px; }
.steps-section .sub { font-size: 12px; color: #666; margin-bottom: 16px; }
.step-item { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #0f3460; }
.step-item:last-child { border-bottom: none; }
.step-title { font-size: 15px; font-weight: 600; color: #4ecca3; margin-bottom: 4px; }
.step-desc { font-size: 12px; color: #999; margin-bottom: 12px; line-height: 1.5; }
.step-images { display: flex; gap: 12px; flex-wrap: wrap; }
.step-img-card { flex: 1; min-width: 180px; max-width: 280px; }
.step-img-card .lbl { font-size: 11px; color: #888; margin-bottom: 4px; text-align: center; }
.step-img-card img { width: 100%; border-radius: 6px; display: block; border: 1px solid #0f3460; cursor: zoom-in; }
.step-img-card img:hover { border-color: #4ecca3; }
#status { text-align: center; padding: 30px; color: #4ecca3; font-size: 15px; display: none; }
.spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #0f3460; border-top-color: #4ecca3; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 8px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
#error-msg { color: #e74c3c; text-align: center; padding: 20px; display: none; font-size: 14px; }
.loading-hairstyles { text-align: center; padding: 20px; color: #666; font-size: 12px; }
.link-row { margin-top: 12px; text-align: center; }
.link-row a { color: #4ecca3; font-size: 12px; text-decoration: none; }
/* 图片放大查看 */
#lightbox { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,.9); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; }
#lightbox img { max-width: 90%; max-height: 90%; border-radius: 8px; }
#lightbox.show { display: flex; }
</style>
</head>
<body>
<div class="header">
<h1>💇 换发型测试(过程可视化)</h1>
<p>选发型 → 上传人像 → 生成(展示换发型每个步骤的中间产物)</p>
</div>
<div class="container">
<div class="panel">
<h3>① 选择发型</h3>
<div class="hairstyle-tabs">
<button class="hairstyle-tab active" data-gender="all">全部</button>
<button class="hairstyle-tab" data-gender="girl">女款</button>
<button class="hairstyle-tab" data-gender="boy">男款</button>
</div>
<input type="text" class="search-box" id="search" placeholder="搜索发型ID...">
<div class="hairstyle-grid" id="hairstyle-grid"><div class="loading-hairstyles">加载发型中...</div></div>
<div class="selected-hairstyle-info" id="selected-info">未选择发型</div>
<h3 style="margin-top:20px">② 上传人像</h3>
<div class="hint" style="margin-top:0">点击右侧上传区选择人头像照片</div>
<h3 style="margin-top:20px">③ 生成</h3>
<div class="tool-group">
<label><input type="checkbox" id="is-hr"> 高清模式 (较慢)</label>
<label><input type="checkbox" id="strict-mask"> 严格按mask贴回 (mask外不变)</label>
</div>
<button class="btn btn-primary" id="btn-generate" disabled>✨ 换发型</button>
<div class="hint">
<b>说明</b><br>
换发型后会展示6个步骤:粗推理→生成mask→warpAffine→SD推理→贴回原图
</div>
<div class="link-row"><a href="/test_new">→ 去新发型测试页(30款)</a></div>
<div class="link-row"><a href="/">→ 去生发测试页</a></div>
</div>
<div class="workspace">
<div class="canvas-wrap" id="canvas-wrap">
<div class="upload-zone" id="upload-zone">
<svg viewBox="0 0 24 24"><path d="M19 13v6H5v-6H3v8h18v-8zM6 9l1.41 1.41L11 6.83V18h2V6.83l3.59 3.58L18 9l-6-6z"/></svg>
<p class="highlight">点击或拖拽上传人像</p>
<p>建议人脸清晰正面照</p>
</div>
<input type="file" id="file-input" accept="image/*" style="display:none">
<img id="preview-img">
</div>
<div id="status"><span class="spinner"></span><span id="status-text">正在换发型(含可视化),约60秒...</span></div>
<div id="error-msg"></div>
<div class="result-section" id="result-section">
<h3>对比结果</h3>
<div class="result-grid">
<div class="result-card"><h4>原图</h4><img id="result-orig"></div>
<div class="result-card"><h4>换发型结果</h4><img id="result-new"><a class="download-btn" id="download-link" download="swaphair_result.jpg">⬇️ 下载结果</a></div>
</div>
</div>
<div class="steps-section" id="steps-section">
<h3>🔍 换发型过程(逐步可视化)</h3>
<div class="sub">每个步骤用到的图片和说明,点击图片可放大查看</div>
<div id="steps-container"></div>
</div>
</div>
</div>
<div id="lightbox" onclick="this.classList.remove('show')"><img id="lightbox-img"></div>
<script>
(function(){
const $=id=>document.getElementById(id);
const uploadZone=$("upload-zone"),fileInput=$("file-input"),previewImg=$("preview-img");
let selectedHairId=null,allHairstyles=[],imgB64Orig=null;
async function loadHairstyles(){
try{const r=await fetch("/api/hairstyles");const d=await r.json();
if(d.state!==0)throw new Error(d.msg);allHairstyles=d.data;renderHairstyles("all");
}catch(e){$("hairstyle-grid").innerHTML='<div class="loading-hairstyles">加载失败: '+e.message+'</div>';}
}
function renderHairstyles(gf){
const s=$("search").value.trim().toLowerCase();
const f=allHairstyles.filter(x=>{if(gf!=="all"&&x.gender!==gf)return false;if(s&&!x.hair_id.toLowerCase().includes(s))return false;return true;});
const g=$("hairstyle-grid");if(f.length===0){g.innerHTML='<div class="loading-hairstyles">无匹配发型</div>';return;}
g.innerHTML="";f.slice(0,90).forEach(x=>{
const it=document.createElement("div");it.className="hairstyle-item"+(x.hair_id===selectedHairId?" selected":"");it.dataset.hairId=x.hair_id;
const im=document.createElement("img");im.loading="lazy";im.src="/preview/"+x.hair_id;im.onerror=()=>{im.style.visibility="hidden";};
it.appendChild(im);it.onclick=()=>selectHairstyle(x.hair_id);g.appendChild(it);
});
if(f.length>90){const m=document.createElement("div");m.className="loading-hairstyles";m.style.gridColumn="1/-1";m.textContent="还有 "+(f.length-90)+" 个,请搜索";g.appendChild(m);}
}
function selectHairstyle(id){selectedHairId=id;document.querySelectorAll(".hairstyle-item").forEach(el=>el.classList.toggle("selected",el.dataset.hairId===id));$("selected-info").textContent="已选: "+id;updateBtn();}
document.querySelectorAll(".hairstyle-tab").forEach(t=>{t.onclick=()=>{document.querySelectorAll(".hairstyle-tab").forEach(x=>x.classList.remove("active"));t.classList.add("active");renderHairstyles(t.dataset.gender);};});
$("search").addEventListener("input",()=>{const a=document.querySelector(".hairstyle-tab.active");renderHairstyles(a?a.dataset.gender:"all");});
loadHairstyles();
uploadZone.addEventListener("click",()=>fileInput.click());
uploadZone.addEventListener("dragover",e=>{e.preventDefault();uploadZone.classList.add("dragover");});
uploadZone.addEventListener("dragleave",()=>uploadZone.classList.remove("dragover"));
uploadZone.addEventListener("drop",e=>{e.preventDefault();uploadZone.classList.remove("dragover");if(e.dataTransfer.files[0])loadImage(e.dataTransfer.files[0]);});
fileInput.addEventListener("change",e=>{if(e.target.files[0])loadImage(e.target.files[0]);});
function loadImage(file){if(!file.type.startsWith("image/")){alert("请上传图片文件");return;}
const rd=new FileReader();rd.onload=e=>{imgB64Orig=e.target.result;previewImg.src=imgB64Orig;previewImg.style.display="block";uploadZone.style.display="none";updateBtn();$("result-section").classList.remove("show");$("steps-section").classList.remove("show");$("error-msg").style.display="none";};rd.readAsDataURL(file);}
function updateBtn(){$("btn-generate").disabled=!(selectedHairId&&imgB64Orig);}
function showLightbox(src){$("lightbox-img").src=src;$("lightbox").classList.add("show");}
$("btn-generate").addEventListener("click",async()=>{
const isHr=$("is-hr").checked;
$("btn-generate").disabled=true;$("status").style.display="block";
$("status-text").textContent="正在换发型(含过程可视化),约60秒...";
$("error-msg").style.display="none";$("result-section").classList.remove("show");$("steps-section").classList.remove("show");
try{
const resp=await fetch("/api/swap_viz",{method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({img:imgB64Orig,hair_id:selectedHairId,is_hr:String(isHr),strict_mask:$("strict-mask").checked})});
const data=await resp.json();$("status").style.display="none";
if(data.state===0){
const url="data:image/jpeg;base64,"+data.result;
$("result-orig").src=imgB64Orig;$("result-new").src=url;$("download-link").href=url;
$("result-section").classList.add("show");
// 渲染步骤画廊
if(data.steps&&data.steps.length){
const c=$("steps-container");c.innerHTML="";
data.steps.forEach(step=>{
const div=document.createElement("div");div.className="step-item";
let imgsHtml='<div class="step-images">';
step.images.forEach(im=>{imgsHtml+='<div class="step-img-card"><div class="lbl">'+im.label+'</div><img src="data:image/jpeg;base64,'+im.b64+'" data-full="data:image/jpeg;base64,'+im.b64+'"></div>';});
imgsHtml+='</div>';
div.innerHTML='<div class="step-title">'+step.title+'</div><div class="step-desc">'+step.desc+'</div>'+imgsHtml;
c.appendChild(div);
});
// 点击放大
c.querySelectorAll("img").forEach(img=>img.onclick=()=>showLightbox(img.dataset.full));
$("steps-section").classList.add("show");
}
$("result-section").scrollIntoView({behavior:"smooth"});
}else{$("error-msg").textContent="❌ "+(data.msg||"换发型失败");$("error-msg").style.display="block";}
}catch(err){$("status").style.display="none";$("error-msg").textContent="❌ 请求失败: "+err.message;$("error-msg").style.display="block";}
updateBtn();
});
})();
</script>
</body>
</html>
+298
View File
@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新发型效果测试</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
.header { background: #16213e; padding: 18px 28px; border-bottom: 1px solid #0f3460; }
.header h1 { font-size: 20px; font-weight: 600; }
.header p { font-size: 13px; color: #888; margin-top: 4px; }
.container { display: flex; gap: 20px; padding: 20px; max-width: 1700px; margin: 0 auto; }
.panel { width: 320px; flex-shrink: 0; background: #16213e; border-radius: 10px; padding: 18px; height: fit-content; max-height: 90vh; overflow-y: auto; }
.panel h3 { font-size: 14px; color: #4ecca3; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px; }
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.face-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.face-tab { padding: 5px 12px; background: #0f3460; border: none; border-radius: 4px; color: #aaa; cursor: pointer; font-size: 12px; }
.face-tab.active { background: #4ecca3; color: #16213e; font-weight: 600; }
.status-row { font-size: 12px; color: #888; margin-bottom: 10px; padding: 6px 10px; background: #0d1b3e; border-radius: 4px; }
.status-row b { color: #4ecca3; }
.hairstyle-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; max-height: 500px; overflow-y: auto; }
.hairstyle-item { position: relative; cursor: pointer; border-radius: 6px; overflow: hidden; border: 2px solid transparent; aspect-ratio: 1; background: #0d1b3e; }
.hairstyle-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hairstyle-item.ready { border-color: #2a5a4a; }
.hairstyle-item.selected { border-color: #4ecca3; }
.hairstyle-item.selected::after { content: '✓'; position: absolute; top: 2px; right: 4px; color: #4ecca3; font-weight: bold; text-shadow: 0 0 3px #000; }
.hairstyle-item .name { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); color: #fff; font-size: 11px; padding: 12px 4px 3px; text-align: center; }
.hairstyle-item .badge { position: absolute; top: 3px; left: 3px; background: #4ecca3; color: #16213e; font-size: 9px; padding: 1px 5px; border-radius: 3px; font-weight: 600; }
.hairstyle-item .badge.training { background: #e9b949; }
.hairstyle-item .badge.pending { background: #555; color: #aaa; }
.hairstyle-item.pending { opacity: 0.45; cursor: not-allowed; }
.selected-hairstyle-info { font-size: 12px; color: #4ecca3; margin-top: 10px; text-align: center; padding: 6px; background: #0d1b3e; border-radius: 4px; }
.tool-group { margin-bottom: 16px; }
.tool-group label { display: block; font-size: 13px; color: #aaa; margin-bottom: 6px; cursor: pointer; }
.tool-group label input { margin-right: 6px; }
.btn { display: block; width: 100%; padding: 9px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; margin-bottom: 8px; transition: .15s; }
.btn-primary { background: #4ecca3; color: #16213e; font-weight: 600; font-size: 15px; padding: 12px; }
.btn-primary:hover { background: #6ee0bd; }
.btn-primary:disabled { background: #555; color: #999; cursor: not-allowed; }
.btn-ghost { background: #0f3460; color: #eee; }
.btn-ghost:hover { background: #1a4a80; }
.hint { font-size: 12px; color: #666; line-height: 1.6; margin-top: 14px; padding: 10px; background: #0d1b3e; border-radius: 6px; }
.workspace { flex: 1; min-width: 0; }
.canvas-wrap { position: relative; background: #0d0d1a; border-radius: 10px; padding: 20px; text-align: center; min-height: 320px; display: flex; align-items: center; justify-content: center; }
.upload-zone { width: 100%; max-width: 500px; border: 2px dashed #0f3460; border-radius: 10px; padding: 50px 20px; text-align: center; cursor: pointer; transition: .2s; }
.upload-zone:hover, .upload-zone.dragover { border-color: #4ecca3; background: rgba(78,204,163,.05); }
.upload-zone svg { width: 48px; height: 48px; fill: #4ecca3; margin-bottom: 12px; }
.upload-zone p { color: #888; font-size: 14px; }
.upload-zone p.highlight { color: #4ecca3; margin-bottom: 4px; }
#preview-img { display: none; max-width: 100%; max-height: 500px; border-radius: 6px; }
.result-section { margin-top: 20px; background: #16213e; border-radius: 10px; padding: 18px; display: none; }
.result-section.show { display: block; }
.result-section h3 { font-size: 14px; color: #4ecca3; margin-bottom: 14px; }
.result-grid { display: flex; gap: 16px; flex-wrap: wrap; }
.result-card { flex: 1; min-width: 250px; }
.result-card h4 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-align: center; }
.result-card img { width: 100%; border-radius: 6px; display: block; }
.download-btn { display: inline-block; margin-top: 8px; padding: 6px 14px; background: #0f3460; color: #eee; border-radius: 4px; font-size: 12px; text-decoration: none; }
.steps-section { margin-top: 20px; background: #16213e; border-radius: 10px; padding: 18px; display: none; }
.steps-section.show { display: block; }
.steps-section h3 { font-size: 14px; color: #4ecca3; margin-bottom: 6px; }
.steps-section .sub { font-size: 12px; color: #666; margin-bottom: 16px; }
.step-item { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #0f3460; }
.step-item:last-child { border-bottom: none; }
.step-title { font-size: 15px; font-weight: 600; color: #4ecca3; margin-bottom: 4px; }
.step-desc { font-size: 12px; color: #999; margin-bottom: 12px; line-height: 1.5; }
.step-images { display: flex; gap: 12px; flex-wrap: wrap; }
.step-img-card { flex: 1; min-width: 180px; max-width: 280px; }
.step-img-card .lbl { font-size: 11px; color: #888; margin-bottom: 4px; text-align: center; }
.step-img-card img { width: 100%; border-radius: 6px; display: block; border: 1px solid #0f3460; cursor: zoom-in; }
.step-img-card img:hover { border-color: #4ecca3; }
#status { text-align: center; padding: 30px; color: #4ecca3; font-size: 15px; display: none; }
.spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #0f3460; border-top-color: #4ecca3; border-radius: 50%; animation: spin .8s linear infinite; margin-right: 8px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
#error-msg { color: #e74c3c; text-align: center; padding: 20px; display: none; font-size: 14px; }
.loading-hairstyles { text-align: center; padding: 20px; color: #666; font-size: 12px; }
.link-row { margin-top: 12px; text-align: center; }
.link-row a { color: #4ecca3; font-size: 12px; text-decoration: none; }
#lightbox { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,.9); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; }
#lightbox img { max-width: 90%; max-height: 90%; border-radius: 8px; }
#lightbox.show { display: flex; }
</style>
</head>
<body>
<div class="header">
<h1>💇 新发型效果测试(30款 · 按脸型适配)</h1>
<p>选择发型 → 上传人像 → 生成对比。绿色徽章=可测试,黄色=训练中,灰色=待训练</p>
</div>
<div class="container">
<div class="panel">
<h3>① 选择发型</h3>
<div class="status-row" id="status-row">加载中...</div>
<div class="face-tabs" id="face-tabs"></div>
<div class="hairstyle-grid" id="hairstyle-grid"><div class="loading-hairstyles">加载中...</div></div>
<div class="selected-hairstyle-info" id="selected-info">未选择发型</div>
<h3 style="margin-top:20px">② 上传人像</h3>
<div class="hint" style="margin-top:0">点击右侧上传区选择一张人头像照片(正面、清晰)</div>
<h3 style="margin-top:20px">③ 生成</h3>
<div class="tool-group">
<label><input type="checkbox" id="is-hr"> 高清模式(更清晰但更慢)</label>
<label><input type="checkbox" id="strict-mask"> 严格按mask贴回(mask外不变)</label>
</div>
<button class="btn btn-primary" id="btn-generate" disabled>✨ 换发型</button>
<button class="btn btn-ghost" id="btn-refresh">↻ 刷新训练状态</button>
<div class="hint">
<b>说明</b><br>
• 只能选择「可测试」状态的发型<br>
• 换发型约需 30-60 秒<br>
• 会展示换发型的中间步骤便于评估效果
</div>
<div class="link-row"><a href="/swap">→ 去全部发型测试页</a></div>
</div>
<div class="workspace">
<div class="canvas-wrap" id="canvas-wrap">
<div class="upload-zone" id="upload-zone">
<svg viewBox="0 0 24 24"><path d="M19 13v6H5v-6H3v8h18v-8zM6 9l1.41 1.41L11 6.83V18h2V6.83l3.59 3.58L18 9l-6-6z"/></svg>
<p class="highlight">点击或拖拽上传人像</p>
<p>建议人脸清晰正面照</p>
</div>
<input type="file" id="file-input" accept="image/*" style="display:none">
<img id="preview-img">
</div>
<div id="status"><span class="spinner"></span><span id="status-text">正在换发型(含可视化),约60秒...</span></div>
<div id="error-msg"></div>
<div class="result-section" id="result-section">
<h3>对比结果</h3>
<div class="result-grid">
<div class="result-card"><h4>原图</h4><img id="result-orig"></div>
<div class="result-card"><h4>换发型结果</h4><img id="result-new"><a class="download-btn" id="download-link" download="swaphair_result.jpg">⬇️ 下载结果</a></div>
</div>
</div>
<div class="steps-section" id="steps-section">
<h3>🔍 换发型过程(逐步可视化)</h3>
<div class="sub">每个步骤用到的图片和说明,点击图片可放大查看</div>
<div id="steps-container"></div>
</div>
</div>
</div>
<div id="lightbox" onclick="this.classList.remove('show')"><img id="lightbox-img"></div>
<script>
(function(){
const $=id=>document.getElementById(id);
// 30 个新发型 ID(按脸型分组)
const HAIRSTYLES = [
// 圆脸
{face:"圆",name:"圆-心形"},{face:"圆",name:"圆-椭圆"},{face:"圆",name:"圆-波浪"},{face:"圆",name:"圆-直线"},{face:"圆",name:"圆-花瓣"},
// 心形脸
{face:"心形",name:"心形-心形"},{face:"心形",name:"心形-椭圆"},{face:"心形",name:"心形-波浪"},{face:"心形",name:"心形-直线"},{face:"心形",name:"心形-花瓣"},
// 方脸
{face:"方脸",name:"方脸-心形"},{face:"方脸",name:"方脸-椭圆"},{face:"方脸",name:"方脸-波浪"},{face:"方脸",name:"方脸-直线"},{face:"方脸",name:"方脸-花瓣"},
// 椭圆脸
{face:"椭圆",name:"椭圆-心形"},{face:"椭圆",name:"椭圆-椭圆"},{face:"椭圆",name:"椭圆-波浪"},{face:"椭圆",name:"椭圆-直线"},{face:"椭圆",name:"椭圆-花瓣"},
// 菱形脸
{face:"菱形",name:"菱形-心"},{face:"菱形",name:"菱形-椭圆"},{face:"菱形",name:"菱形-波浪"},{face:"菱形",name:"菱形-直线"},{face:"菱形",name:"菱形-花瓣"},
// 长脸
{face:"长",name:"长-心形"},{face:"长",name:"长 -椭圆"},{face:"长",name:"长 -波浪"},{face:"长",name:"长 -直线"},{face:"长",name:"长 -花瓣"}
];
const uploadZone=$("upload-zone"),fileInput=$("file-input"),previewImg=$("preview-img");
let selectedHairId=null, readySet=new Set(), imgB64Orig=null;
async function loadStatus(){
try{
const r=await fetch("/api/hairstyles");
const d=await r.json();
if(d.state!==0) throw new Error(d.msg||"加载失败");
readySet=new Set(d.data.map(x=>x.hair_id));
}catch(e){
$("status-row").innerHTML='<span style="color:#e74c3c">加载可用发型失败: '+e.message+'</span>';
}
renderStatus();
renderTabs();
renderGrid("全部");
}
function renderStatus(){
const ready=HAIRSTYLES.filter(h=>readySet.has(h.name)).length;
$("status-row").innerHTML='共 30 款发型|<b style="color:#4ecca3">可测试 '+ready+'</b>|训练中/待训练 '+(30-ready);
}
function renderTabs(){
const faces=["全部",...new Set(HAIRSTYLES.map(h=>h.face))];
$("face-tabs").innerHTML=faces.map((f,i)=>
`<button class="face-tab${i===0?' active':''}" data-face="${f}">${f}</button>`).join('');
$("face-tabs").querySelectorAll(".face-tab").forEach(t=>{
t.onclick=()=>{
$("face-tabs").querySelectorAll(".face-tab").forEach(x=>x.classList.remove("active"));
t.classList.add("active");
renderGrid(t.dataset.face);
};
});
}
function renderGrid(faceFilter){
let list=HAIRSTYLES;
if(faceFilter!=="全部") list=list.filter(h=>h.face===faceFilter);
const g=$("hairstyle-grid");
if(list.length===0){ g.innerHTML='<div class="loading-hairstyles">无发型</div>'; return; }
g.innerHTML="";
list.forEach(h=>{
const ready=readySet.has(h.name);
const it=document.createElement("div");
it.className="hairstyle-item"+(ready?" ready":" pending");
if(h.name===selectedHairId) it.classList.add("selected");
const badge=ready
? '<span class="badge">可测试</span>'
: '<span class="badge training">训练中</span>';
it.innerHTML=badge+
`<img loading="lazy" src="/train_src/${encodeURIComponent(h.name)}" onerror="this.style.background='#0d1b3e';this.style.objectFit='contain';this.src='data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22120%22 height=%22120%22><text x=%2260%22 y=%2265%22 text-anchor=%22middle%22 fill=%22%23666%22 font-size=%2212%22>训练中</text></svg>'">`+
`<div class="name">${h.name}</div>`;
if(ready){
it.onclick=()=>{
selectedHairId=h.name;
$("hairstyle-grid").querySelectorAll(".hairstyle-item").forEach(el=>el.classList.remove("selected"));
it.classList.add("selected");
$("selected-info").textContent="已选: "+h.name;
updateBtn();
};
}
g.appendChild(it);
});
}
uploadZone.addEventListener("click",()=>fileInput.click());
uploadZone.addEventListener("dragover",e=>{e.preventDefault();uploadZone.classList.add("dragover");});
uploadZone.addEventListener("dragleave",()=>uploadZone.classList.remove("dragover"));
uploadZone.addEventListener("drop",e=>{e.preventDefault();uploadZone.classList.remove("dragover");if(e.dataTransfer.files[0])loadImage(e.dataTransfer.files[0]);});
fileInput.addEventListener("change",e=>{if(e.target.files[0])loadImage(e.target.files[0]);});
function loadImage(file){
if(!file.type.startsWith("image/")){alert("请上传图片文件");return;}
const rd=new FileReader();
rd.onload=e=>{
imgB64Orig=e.target.result;
previewImg.src=imgB64Orig;previewImg.style.display="block";
uploadZone.style.display="none";updateBtn();
$("result-section").classList.remove("show");
$("steps-section").classList.remove("show");
$("error-msg").style.display="none";
};
rd.readAsDataURL(file);
}
function updateBtn(){ $("btn-generate").disabled=!(selectedHairId&&imgB64Orig&&readySet.has(selectedHairId)); }
function showLightbox(src){ $("lightbox-img").src=src; $("lightbox").classList.add("show"); }
$("btn-refresh").onclick=()=>{ loadStatus(); };
$("btn-generate").addEventListener("click",async()=>{
if(!selectedHairId||!imgB64Orig) return;
const isHr=$("is-hr").checked;
$("btn-generate").disabled=true;$("status").style.display="block";
$("status-text").textContent="正在换发型(含过程可视化),约30-60秒...";
$("error-msg").style.display="none";
$("result-section").classList.remove("show");$("steps-section").classList.remove("show");
try{
const resp=await fetch("/api/swap_viz",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({img:imgB64Orig,hair_id:selectedHairId,is_hr:String(isHr),strict_mask:$("strict-mask").checked})
});
const data=await resp.json();
$("status").style.display="none";
if(data.state===0){
const url="data:image/jpeg;base64,"+data.result;
$("result-orig").src=imgB64Orig;$("result-new").src=url;$("download-link").href=url;
$("result-section").classList.add("show");
if(data.steps&&data.steps.length){
const c=$("steps-container");c.innerHTML="";
data.steps.forEach(step=>{
const div=document.createElement("div");div.className="step-item";
let imgsHtml='<div class="step-images">';
step.images.forEach(im=>{imgsHtml+='<div class="step-img-card"><div class="lbl">'+im.label+'</div><img src="data:image/jpeg;base64,'+im.b64+'" data-full="data:image/jpeg;base64,'+im.b64+'"></div>';});
imgsHtml+='</div>';
div.innerHTML='<div class="step-title">'+step.title+'</div><div class="step-desc">'+step.desc+'</div>'+imgsHtml;
c.appendChild(div);
});
c.querySelectorAll("img").forEach(img=>img.onclick=()=>showLightbox(img.dataset.full));
$("steps-section").classList.add("show");
}
$("result-section").scrollIntoView({behavior:"smooth"});
}else{
$("error-msg").textContent="❌ "+(data.msg||"换发型失败");
$("error-msg").style.display="block";
}
}catch(err){
$("status").style.display="none";
$("error-msg").textContent="❌ 请求失败: "+err.message;
$("error-msg").style.display="block";
}
updateBtn();
});
loadStatus();
})();
</script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""自动准备 LoRA 训练数据
把 train_images/ 里的原始人像图处理成 kohya LoRA 训练格式:
原始图 → 人脸检测+1k关键点 → Generator_Matte头发抠图 → 白底合成 + 居中裁剪 → 打标签
用法:
cd /home/xsl/change_hair/project/hair_service_sd
/home/xsl/miniconda3/envs/my_hair/bin/python /home/xsl/change_hair/prepare_train_data.py \
--input /home/xsl/change_hair/train_images \
--hair-id new_hairstyle_001 \
--gender boy
"""
import os
import sys
import ssl
import uuid
import argparse
# 禁用 SSL 验证(解决 resnet18 权重缓存的 SSL 证书问题)
ssl._create_default_https_context = ssl._create_unverified_context
# 必须在 hair_service_sd 目录运行(依赖其模块)
HAIR_SERVICE_DIR = "/home/xsl/change_hair/project/hair_service_sd"
os.chdir(HAIR_SERVICE_DIR)
sys.path.insert(0, HAIR_SERVICE_DIR)
os.environ.setdefault("HF_HUB_OFFLINE", "1")
import cv2
import numpy as np
import torch
# Monkey-patch torch.load: 部分第三方代码用旧式 map_location lambda 触发 legacy_load,
# 但权重文件是新版 zip 格式,导致 UnpicklingError。统一用 map_location='cpu'。
_orig_torch_load = torch.load
def _patched_torch_load(*args, **kwargs):
if 'map_location' in kwargs and callable(kwargs['map_location']) and not isinstance(kwargs['map_location'], str):
kwargs['map_location'] = 'cpu'
return _orig_torch_load(*args, **kwargs)
torch.load = _patched_torch_load
from models.detector import RetinaFaceDetector
from models.MomocvFaceAlignment1K import MomocvFaceAlignment1K
from utils.landmark_processor import get_max_rect
# 用 core/process_modules 的 Generator_Matte(从 weights/ 加载真实权重,
# 而非 hair_matting/ 目录的 LFS 指针文件)
from core.process_modules import Generator_Matte
# kohya 标准训练标签(白底版,和已有发型完全一致)
CAPTION = "titor hairstyle, easyphoto, faceless, no human, white background, simple background, "
# 训练图分辨率列表(和 prepare_single_hairstyle_v2 一致)
RESOLUTIONS = [512, 768, 1024, 1280, 1536]
def process_one(img_path, detector, aligner, matte_gen, out_dir, idx):
"""处理单张原始图:检测→抠图→白底合成→多分辨率保存"""
img = cv2.imread(img_path)
if img is None:
print(f" [{idx}] ✗ 读取失败: {img_path}")
return 0
h, w = img.shape[:2]
# 1. 人脸检测
dets, landms = detector.forward(img)
if len(dets) == 0:
print(f" [{idx}] ✗ 未检测到人脸: {os.path.basename(img_path)}")
return 0
max_idx = get_max_rect(dets)
det = dets[max_idx]
# 2. 1k 关键点
landmarks_1k = aligner.stable_forward(img.copy(), [det])
if landmarks_1k is None or len(landmarks_1k) == 0:
print(f" [{idx}] ✗ 关键点检测失败: {os.path.basename(img_path)}")
return 0
pt1k = landmarks_1k[0]
# 3. 头发抠图(matte_inference 返回 pred_fg, pred, image_resize
with torch.no_grad():
pred_fg, pred, image_resize = matte_gen.matte_inference(img, pt1k)
# pred 是 alpha mask,已 resize 回原图尺寸
alpha = pred
h, w = img.shape[:2]
if alpha.shape != (h, w):
alpha = cv2.resize(alpha, (w, h))
# 4. 白底合成(公式同 hairstyle_model.py:3666-3669
alpha_f = alpha.astype(np.float32) / 255.0
white_bg = np.ones_like(img, dtype=np.float32) * 255.0
img_f = img.astype(np.float32)
composite = (img_f * alpha_f[:, :, None] + white_bg * (1 - alpha_f[:, :, None]))
composite = np.clip(composite, 0, 255).astype(np.uint8)
# 5. 居中裁剪到正方形(以人脸为中心)
pts = pt1k[:312] # 前312点是脸部外轮廓
cx, cy = int(pts[:, 0].mean()), int(pts[:, 1].mean())
# 正方形边长 = 图像短边(保证不裁掉头发)
side = min(h, w)
x1 = max(0, cx - side // 2)
y1 = max(0, cy - side // 2)
x2 = min(w, x1 + side)
y2 = min(h, y1 + side)
# 调整确保正方形
actual_side = min(x2 - x1, y2 - y1)
x2, y2 = x1 + actual_side, y1 + actual_side
composite_square = composite[y1:y2, x1:x2]
if composite_square.shape[0] < 100:
# fallback: 直接用整图短边
side = min(h, w)
composite_square = composite[:side, :side]
# 6. 多分辨率保存 + 同名标签
count = 0
for res in RESOLUTIONS:
if composite_square.shape[0] >= res // 2: # 只生成 >= res/2 的(避免过度放大)
if composite_square.shape[0] >= res:
resized = cv2.resize(composite_square, (res, res), interpolation=cv2.INTER_AREA)
else:
resized = cv2.resize(composite_square, (res, res), interpolation=cv2.INTER_LANCZOS4)
file_uuid = str(uuid.uuid4())
png_path = os.path.join(out_dir, f"{file_uuid}.png")
txt_path = os.path.join(out_dir, f"{file_uuid}.txt")
cv2.imwrite(png_path, resized)
with open(txt_path, "w") as f:
f.write(CAPTION)
count += 1
print(f" [{idx}] ✓ {os.path.basename(img_path)}{count}张训练图")
return count
def main():
parser = argparse.ArgumentParser(description="准备 LoRA 训练数据")
parser.add_argument("--input", required=True, help="原始图目录")
parser.add_argument("--hair-id", required=True, help="新发型ID")
parser.add_argument("--gender", default="boy", choices=["boy", "girl"], help="性别")
args = parser.parse_args()
from common.logger import config
train_dir = config.get('default', 'train_dir')
out_base = os.path.join(train_dir, args.hair_id, "images", "1_hairstyle")
os.makedirs(out_base, exist_ok=True)
print(f"=== 准备训练数据 ===")
print(f"输入: {args.input}")
print(f"发型ID: {args.hair_id}, 性别: {args.gender}")
print(f"输出: {out_base}")
# 加载模型
print("加载模型(RetinaFace + 1k关键点 + Generator_Matte...")
gpu_id = 0
detector = RetinaFaceDetector(gpu_id=gpu_id)
aligner = MomocvFaceAlignment1K(gpu_id=gpu_id)
matte_gen = Generator_Matte(gpu=True, device_id=gpu_id)
print("模型加载完成")
# 处理每张图
import glob
imgs = sorted(glob.glob(os.path.join(args.input, "*.jpg")) +
glob.glob(os.path.join(args.input, "*.png")) +
glob.glob(os.path.join(args.input, "*.jpeg")))
print(f"{len(imgs)} 张原始图")
total = 0
for i, img_path in enumerate(imgs):
total += process_one(img_path, detector, aligner, matte_gen, out_base, i + 1)
print(f"\n=== 完成: {len(imgs)}张原始图 → {total}张训练图 ===")
print(f"训练数据目录: {out_base}")
print(f"\n下一步训练命令:")
print(f"curl -X POST http://127.0.0.1:32678/api/hair/train \\")
print(f' -H "Content-Type: application/json" \\')
print(f' -d \'{{"task_id":"train_{args.hair_id}","hair_id":"{args.hair_id}",'
f'"hair_material_dir":"{os.path.join(train_dir, args.hair_id)}",'
f'"is_tj":"1","device_id":"0","webui_addr":"http://0.0.0.0:32678/"}}\'')
if __name__ == "__main__":
main()
+128
View File
@@ -0,0 +1,128 @@
# 大文件与模型权重说明
> 本仓库只托管代码和配置。模型权重、训练数据等大文件(共约 **200G**)已排除,需另行准备。
> 后续会上传网盘,地址见文末。
---
## 被排除的大目录清单
| 目录 | 大小 | 内容 | 如何恢复 |
|------|------|------|---------|
| `data/` | 123G | 341 个发型的训练素材 + LoRA 权重 | 网盘下载后放到 `data/train_material/` |
| `project/onediff/` | 52G | Stable Diffusion webui + SD模型 + 228个LoRA + ControlNet | 见下方「onediff 恢复」 |
| `project/kohya_ss_home/` | 13G | kohya_ss LoRA 训练框架(第三方) | 见下方「kohya 恢复」 |
| `project/hair_service_sd/weights/` | 9.9G | 人脸检测/关键点/抠图/分割等核心模型权重 | 网盘下载 |
| `project/data/` | 5.4G | 运行时材质数据(ref_hairstyle/hair_template_material 等) | 训练完成后自动生成 |
| `hair_type_images/` | 53M | 本次 30 张发型训练原图 | 网盘下载 |
---
## 1. `project/hair_service_sd/weights/` 核心模型(9.9G
主服务推理必需。从网盘下载后解压到本目录。包含:
| 文件 | 大小 | 用途 |
|------|------|------|
| `Resnet50_Final.pth` | 109M | RetinaFace 人脸检测 |
| `face_alignment_1k.pth` | 49M | 1000点人脸关键点 |
| `deeplabv3_hair512_360_0520_wl.pth` | 233M | 头发分割 DeepLabV3+ (3分类) |
| `gca-dist-fg-0430-latest_model.pth` | 303M | GCA 头发抠图 |
| `20210927_01.pth` / `faceseg_20210927_01.pth` | 各229M | 人脸分割 U2NET |
| `master_hair_v8_onlyhair_nowarp_all_0709.pt` | 1.1G | SPADE 发型迁移(粗推理) |
| `hair_fusion_0427.pt` | 779M | 发型融合 |
| `ycj_hairstyle_gan_*.pt` / `ycj_haircolor_gan_*.pt` | 各~500M | 发型/发色 GAN |
| `gfpgan_hairline_512_inversion_ratio.pt` | 308M | 人脸增强 GFPGAN |
| `face_enhance_0630.pt` | 284M | 人脸增强 |
| `MMCVFaceRecognitionServer.pth` | 174M | 人脸识别 |
| `kpnts_detect_lyq_20200720.pth` | 255M | 关键点检测 |
| `human_seg_deeplabv3_288_384_sigmoid_msc.pth` | 134M | 人体分割 |
| `ori_hair_checkpoint_7660_0611.pt` | ~ | BaldSeg 光头分割 |
| `mediapipe/face_landmarker.task` | 3.6M | MediaPipe 478点关键点(发际线检测用) |
| 其余 `*.pth/*.pt` | — | 各功能模块权重 |
---
## 2. `project/onediff/` SD webui52G,第三方)
换发型 SD 推理必需。这是 [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 的部署。
**恢复方式(二选一)**
- **A. 网盘下载**:直接下载整个 onediff 目录解压(最快)
- **B. 重新部署**
```bash
cd project
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git onediff/stable-diffusion-webui
# 然后下载以下模型放到对应目录:
# models/Stable-diffusion/v1-5-pruned-emaonly.safetensors (4G) — SD底模
# models/Lora/*.safetensors — 各发型LoRA(由训练生成)
# models/ControlNet/control_v11p_sd15_canny.pth — ControlNet(可选)
```
**关键配置**
- SD 底模:`v1-5-pruned-emaonly.safetensors`(原项目用 majicmix,本机替换为 v1-5
- LoRA:训练完成的发型会自动生成到 `models/Lora/`
---
## 3. `project/kohya_ss_home/` 训练框架(13G,第三方)
LoRA 训练用。[kohya-ss/sd-scripts](https://github.com/kohya-ss/sd-scripts) 的部署。
**恢复方式**
```bash
cd project
git clone https://github.com/bmaltais/kohya_ss.git kohya_ss_home/kohya_ss
# 安装依赖(用 kohya conda 环境)
/home/xsl/miniconda3/envs/kohya/bin/pip install -r kohya_ss_home/kohya_ss/requirements.txt
```
训练入口:`kohya_ss_home/kohya_ss/train_network.py`
---
## 4. `data/train_material/` 训练数据(123G
341 个发型的 LoRA 训练素材和权重。每个发型一个目录:
```
data/train_material/<hair_id>/
├── images/1_hairstyle/*.png # 训练图(数据增强后约20张)
└── model/
└── hairstyle_hd_lora.safetensors # 训练产出的LoRA权重(~150M)
```
**恢复方式**
- **网盘下载**:下载后解压到 `data/train_material/`
- **重新训练**:用 `batch_train_hairstyles.py` 重新训练需要的发型(每个约30分钟)
---
## 5. `project/data/` 运行时材质(5.4G
换发型功能运行时需要的预生成材质。**训练完成发型后会自动生成**,无需手动准备:
- `ref_hairstyle/<hair_id>/` — 每个发型的参考材质(粗推理生成)
- `hair_template_material/<hair_id>/` — 模板材质(抠图+关键点)
- `upload_train_imgs/<hair_id>/` — 上传的训练原图
---
## 训练原图
`hair_type_images/`30张发型原图,按 `<脸型>-<发型>.jpg` 命名)是本次新增发型的训练源。可从网盘下载,或自行准备同格式图片后用 `batch_train_hairstyles.py` 训练。
---
## 网盘地址
(待补充)—— 大文件上传网盘后,把分享链接填到这里。
---
## 快速检查清单(部署后)
部署完成后,确认以下文件存在:
- [ ] `project/hair_service_sd/weights/Resnet50_Final.pth`(人脸检测)
- [ ] `project/hair_service_sd/weights/face_alignment_1k.pth`(关键点)
- [ ] `project/hair_service_sd/weights/master_hair_v8_onlyhair_nowarp_all_0709.pt`(发型迁移)
- [ ] `project/onediff/stable-diffusion-webui/models/Stable-diffusion/v1-5-pruned-emaonly.safetensors`SD底模)
- [ ] `data/train_material/<hair_id>/model/hairstyle_hd_lora.safetensors`(至少一个发型的LoRA
+3
View File
@@ -0,0 +1,3 @@
*.pth filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text
*.ttc filter=lfs diff=lfs merge=lfs -text
+194
View File
@@ -0,0 +1,194 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the enitre vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
+28
View File
@@ -0,0 +1,28 @@
换发算法服务代码
相关配置:config/configure.ini \
模型:请存放于weights目录下 \
服务启动:执行 gunicorn server:app -c gunicorn_config.py \
测试相关代码请写在 unit_test.py里
建议环境:
python3.7pytorch1.8,cu11.1,torchvision10.0 \
gunicorn需要在/usr/bin/gunicorn需要在里修改python地址
run run_copy_cost.py
需要启动的服务:
环境变量设置
export CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
1. 换发算法服务
cd /root/project/hair_service_sd
/usr/local/miniconda3/envs/condiff-train-hair/bin/python run_copy_cost_colorb64.py
2. webUI服务推理 + onediff
cd /root/project/onediff/stable-diffusion-webui
/usr/local/miniconda3/envs/onediff/bin/python python webui.py --api --listen --xformers --port 57860
./webui.sh --api --listen --disable-safe-unpickle --port 9038
3. photo_service
cd /root/project/photo_service
/usr/local/miniconda3/envs/py310/bin/python lora_train_service_1.py
@@ -0,0 +1,75 @@
import numpy as np
import torch
from torch import nn
def get_norm(norm, out_channels=None):
"""
Args:
norm (str or callable):
Returns:
nn.Module or None: the normalization layer
"""
if isinstance(norm, str):
if len(norm) == 0:
return None
norm = {
"BN": nn.BatchNorm2d,
"IN": nn.InstanceNorm2d,
"GN": lambda channels: nn.GroupNorm(32, channels),
"nnSyncBN": nn.SyncBatchNorm, # keep for debugging
}[norm]
if out_channels is not None:
return norm(out_channels)
else:
return norm
class Conv2d(torch.nn.Conv2d):
"""
A wrapper around :class:`torch.nn.Conv2d` to support zero-size tensor and more features.
"""
def __init__(self, *args, **kwargs):
"""
Extra keyword arguments supported in addition to those in `torch.nn.Conv2d`:
Args:
norm (nn.Module, optional): a normalization layer
activation (callable(Tensor) -> Tensor): a callable activation function
It assumes that norm layer is used before activation.
"""
norm = kwargs.pop("norm", None)
activation = kwargs.pop("activation", None)
super().__init__(*args, **kwargs)
self.norm = norm
self.activation = activation
def forward(self, x):
x = super().forward(x)
if self.norm is not None:
x = self.norm(x)
if self.activation is not None:
x = self.activation(x)
return x
class Backbone(nn.Module):
"""
Abstract base class for network backbones.
"""
def __init__(self):
"""
The `__init__` method of any subclass can specify its own set of arguments.
"""
super().__init__()
def forward(self):
"""
Subclasses must override this method, but adhere to the same return type.
Returns:
dict[str: Tensor]: mapping from feature name (e.g., "res2") to tensor
"""
pass
@@ -0,0 +1,268 @@
import math
import utils.weight_init as weight_init
import torch
import torch.nn.functional as F
from torch import nn
from bodyseg.backbone.backbone import Backbone, get_norm, Conv2d
from bodyseg.backbone.resnet import build_resnet_backbone
class FPN(Backbone):
"""
This module implements Feature Pyramid Network.
It creates pyramid features built on top of some input feature maps.
"""
def __init__(
self, bottom_up, in_features, out_channels, norm="", top_block=None, fuse_type="sum"
):
"""
Args:
bottom_up (Backbone): module representing the bottom up subnetwork.
Must be a subclass of :class:`Backbone`. The multi-scale feature
maps generated by the bottom up network, and listed in `in_features`,
are used to generate FPN levels.
in_features (list[str]): names of the input feature maps coming
from the backbone to which FPN is attached. For example, if the
backbone produces ["res2", "res3", "res4"], any *contiguous* sublist
of these may be used; order must be from high to low resolution.
out_channels (int): number of channels in the output feature maps.
norm (str): the normalization to use.
top_block (nn.Module or None): if provided, an extra operation will
be performed on the output of the last (smallest resolution)
FPN output, and the result will extend the result list. The top_block
further downsamples the feature map. It must have an attribute
"num_levels", meaning the number of extra FPN levels added by
this block, and "in_feature", which is a string representing
its input feature (e.g., p5).
fuse_type (str): types for fusing the top down features and the lateral
ones. It can be "sum" (default), which sums up element-wise; or "avg",
which takes the element-wise mean of the two.
"""
super(FPN, self).__init__()
assert isinstance(bottom_up, Backbone)
# Feature map strides and channels from the bottom up network (e.g. ResNet)
in_strides = [bottom_up._out_feature_strides[f] for f in in_features]
in_channels = [bottom_up._out_feature_channels[f] for f in in_features]
_assert_strides_are_log2_contiguous(in_strides)
lateral_convs = []
output_convs = []
use_bias = norm == ""
for idx, in_channels in enumerate(in_channels):
lateral_norm = get_norm(norm, out_channels)
output_norm = get_norm(norm, out_channels)
lateral_conv = Conv2d(
in_channels, out_channels, kernel_size=1, bias=use_bias, norm=lateral_norm
)
output_conv = Conv2d(
out_channels,
out_channels,
kernel_size=3,
stride=1,
padding=1,
bias=use_bias,
norm=output_norm,
)
weight_init.c2_xavier_fill(lateral_conv)
weight_init.c2_xavier_fill(output_conv)
stage = int(math.log2(in_strides[idx]))
lateral_convs.append(lateral_conv)
output_convs.append(output_conv)
# Place convs into top-down order (from low to high resolution)
# to make the top-down computation in forward clearer.
self.lateral_convs = nn.ModuleList(lateral_convs[::-1])
self.output_convs = nn.ModuleList(output_convs[::-1])
self.top_block = top_block
self.in_features = in_features
self.bottom_up = bottom_up
# Return feature names are "p<stage>", like ["p2", "p3", ..., "p6"]
self._out_feature_strides = {"p{}".format(int(math.log2(s))): s for s in in_strides}
# top block output feature maps.
if self.top_block is not None:
for s in range(stage, stage + self.top_block.num_levels):
self._out_feature_strides["p{}".format(s + 1)] = 2 ** (s + 1)
self._out_features = list(self._out_feature_strides.keys())
self._out_feature_channels = {k: out_channels for k in self._out_features}
assert fuse_type in {"avg", "sum"}
self._fuse_type = fuse_type
def forward(self, x):
"""
Args:
input (dict[str: Tensor]): mapping feature map name (e.g., "res5") to
feature map tensor for each feature level in high to low resolution order.
Returns:
dict[str: Tensor]:
mapping from feature map name to FPN feature map tensor
in high to low resolution order. Returned feature names follow the FPN
paper convention: "p<stage>", where stage has stride = 2 ** stage e.g.,
["p2", "p3", ..., "p6"].
"""
# Reverse feature maps into top-down order (from low to high resolution)
bottom_up_features = self.bottom_up(x)
x = [bottom_up_features[f] for f in self.in_features[::-1]]
results = []
prev_features = self.lateral_convs[0](x[0])
results.append(self.output_convs[0](prev_features))
for features, lateral_conv, output_conv in zip(
x[1:], self.lateral_convs[1:], self.output_convs[1:]
):
top_down_features = F.interpolate(prev_features, scale_factor=2, mode="nearest")
lateral_features = lateral_conv(features)
prev_features = lateral_features + top_down_features
if self._fuse_type == "avg":
prev_features /= 2
results.insert(0, output_conv(prev_features))
if self.top_block is not None:
top_block_in_feature = bottom_up_features.get(self.top_block.in_feature, None)
if top_block_in_feature is None:
top_block_in_feature = results[self._out_features.index(self.top_block.in_feature)]
results.extend(self.top_block(top_block_in_feature))
assert len(self._out_features) == len(results)
return dict(zip(self._out_features, results))
def _assert_strides_are_log2_contiguous(strides):
"""
Assert that each stride is 2x times its preceding stride, i.e. "contiguous in log2".
"""
for i, stride in enumerate(strides[1:], 1):
assert stride == 2 * strides[i - 1], "Strides {} {} are not log2 contiguous".format(
stride, strides[i - 1]
)
class LastLevelMaxPool(nn.Module):
"""
This module is used in the original FPN to generate a downsampled
P6 feature from P5.
"""
def __init__(self):
super().__init__()
self.num_levels = 1
self.in_feature = "p5"
def forward(self, x):
return [F.max_pool2d(x, kernel_size=1, stride=2, padding=0)]
class LastLevelP6P7(nn.Module):
"""
This module is used in RetinaNet to generate extra layers, P6 and P7 from
C5 feature.
"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.num_levels = 2
self.in_feature = "res5"
self.p6 = nn.Conv2d(in_channels, out_channels, 3, 2, 1)
self.p7 = nn.Conv2d(out_channels, out_channels, 3, 2, 1)
for module in [self.p6, self.p7]:
weight_init.c2_xavier_fill(module)
def forward(self, c5):
p6 = self.p6(c5)
p7 = self.p7(F.relu(p6))
return [p6, p7]
def build_resnet_fpn_backbone(in_channels=3):
"""
Args:
cfg: a detectron2 CfgNode
Returns:
backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`.
"""
bottom_up = build_resnet_backbone(in_channels)
in_features = ["res2", "res3", "res4"]
out_channels = 256
backbone = FPN(
bottom_up=bottom_up,
in_features=in_features,
out_channels=out_channels,
norm="BN",
# top_block=LastLevelMaxPool(),
top_block=None,
fuse_type="sum",
)
return backbone
def build_retinanet_resnet_fpn_backbone(cfg, in_channels=3):
"""
Args:
cfg: a detectron2 CfgNode
Returns:
backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`.
"""
bottom_up = build_resnet_backbone(cfg, in_channels)
in_features = cfg.MODEL.FPN.IN_FEATURES
out_channels = cfg.MODEL.FPN.OUT_CHANNELS
in_channels_p6p7 = bottom_up._out_feature_channels["res5"]
backbone = FPN(
bottom_up=bottom_up,
in_features=in_features,
out_channels=out_channels,
norm=cfg.MODEL.FPN.NORM,
top_block=LastLevelP6P7(in_channels_p6p7, out_channels),
fuse_type=cfg.MODEL.FPN.FUSE_TYPE,
)
return backbone
if __name__ == "__main__":
import argparse
from config.default import get_cfg
def setup(args):
"""
Create configs and perform basic setups.
"""
cfg = get_cfg()
cfg.merge_from_file(args.cfg)
cfg.merge_from_list(args.opts)
cfg.freeze()
return cfg
parser = argparse.ArgumentParser(description='Train ImageNet network')
# general
parser.add_argument('--cfg',
help='experiment configure file name',
required=True,
type=str)
parser.add_argument('opts',
help="Modify config options using the command-line",
default=None,
nargs=argparse.REMAINDER)
args = parser.parse_args()
cfg = setup(args)
print(cfg)
model = build_resnet_fpn_backbone(cfg, 3)
# model = build_retinanet_resnet_fpn_backbone(cfg, 3)
print(model)
# model = torch.nn.DataParallel(model, list(range(2))).cuda()
dummy_input = torch.randn(4, 3, 512, 512)
out = model(dummy_input)
for k, v in out.items():
print(k, v.shape)
# torch.onnx.export(model, dummy_input, "tmp.onnx", verbose=True,
# input_names=['input'],
# output_names=['output'])
pass
@@ -0,0 +1,298 @@
import numpy as np
import utils.weight_init as weight_init
import torch
import torch.nn.functional as F
from torch import nn
from bodyseg.backbone.backbone import Backbone, get_norm, Conv2d
class BasicStem(nn.Module):
def __init__(self, in_channels=3, out_channels=64, norm="BN"):
"""
Args:
norm (str or callable): a callable that takes the number of
channels and return a `nn.Module`, or a pre-defined string
(one of {"FrozenBN", "BN", "GN"}).
"""
super().__init__()
self.conv1 = Conv2d(
in_channels,
out_channels,
kernel_size=7,
stride=2,
padding=3,
bias=False,
norm=get_norm(norm, out_channels),
)
weight_init.c2_msra_fill(self.conv1)
def forward(self, x):
x = self.conv1(x)
x = F.relu_(x)
x = F.max_pool2d(x, kernel_size=3, stride=2, padding=1)
return x
@property
def out_channels(self):
return self.conv1.out_channels
@property
def stride(self):
return 4 # = stride 2 conv -> stride 2 max pool
class ResNetBlockBase(nn.Module):
def __init__(self, in_channels, out_channels, stride):
"""
The `__init__` method of any subclass should also contain these arguments.
Args:
in_channels (int):
out_channels (int):
stride (int):
"""
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.stride = stride
class BottleneckBlock(ResNetBlockBase):
def __init__(
self,
in_channels,
out_channels,
*,
bottleneck_channels,
stride=1,
num_groups=1,
norm="BN",
stride_in_1x1=False,
dilation=1,
):
"""
Args:
norm (str or callable): a callable that takes the number of
channels and return a `nn.Module`, or a pre-defined string
(one of {"FrozenBN", "BN", "GN"}).
stride_in_1x1 (bool): when stride==2, whether to put stride in the
first 1x1 convolution or the bottleneck 3x3 convolution.
"""
super().__init__(in_channels, out_channels, stride)
if in_channels != out_channels:
self.shortcut = Conv2d(
in_channels,
out_channels,
kernel_size=1,
stride=stride,
bias=False,
norm=get_norm(norm, out_channels),
)
else:
self.shortcut = None
# The original MSRA ResNet models have stride in the first 1x1 conv
# The subsequent fb.torch.resnet and Caffe2 ResNe[X]t implementations have
# stride in the 3x3 conv
stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride)
self.conv1 = Conv2d(
in_channels,
bottleneck_channels,
kernel_size=1,
stride=stride_1x1,
bias=False,
norm=get_norm(norm, bottleneck_channels),
)
self.conv2 = Conv2d(
bottleneck_channels,
bottleneck_channels,
kernel_size=3,
stride=stride_3x3,
padding=1 * dilation,
bias=False,
groups=num_groups,
dilation=dilation,
norm=get_norm(norm, bottleneck_channels),
)
self.conv3 = Conv2d(
bottleneck_channels,
out_channels,
kernel_size=1,
bias=False,
norm=get_norm(norm, out_channels),
)
for layer in [self.conv1, self.conv2, self.conv3, self.shortcut]:
if layer is not None: # shortcut can be None
weight_init.c2_msra_fill(layer)
# Zero-initialize the last normalization in each residual branch,
# so that at the beginning, the residual branch starts with zeros,
# and each residual block behaves like an identity.
# See Sec 5.1 in "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour":
# "For BN layers, the learnable scaling coefficient γ is initialized
# to be 1, except for each residual block's last BN
# where γ is initialized to be 0."
# nn.init.constant_(self.conv3.norm.weight, 0)
# TODO this somehow hurts performance when training GN models from scratch.
# Add it as an option when we need to use this code to train a backbone.
def forward(self, x):
out = self.conv1(x)
out = F.relu_(out)
out = self.conv2(out)
out = F.relu_(out)
out = self.conv3(out)
if self.shortcut is not None:
shortcut = self.shortcut(x)
else:
shortcut = x
out += shortcut
out = F.relu_(out)
return out
def make_stage(block_class, num_blocks, first_stride, **kwargs):
"""
Create a resnet stage by creating many blocks.
Args:
block_class (class): a subclass of ResNetBlockBase
num_blocks (int):
first_stride (int): the stride of the first block. The other blocks will have stride=1.
A `stride` argument will be passed to the block constructor.
kwargs: other arguments passed to the block constructor.
Returns:
list[nn.Module]: a list of block module.
"""
blocks = []
for i in range(num_blocks):
blocks.append(block_class(stride=first_stride if i == 0 else 1, **kwargs))
kwargs["in_channels"] = kwargs["out_channels"]
return blocks
class ResNet(Backbone):
def __init__(self, stem, stages, num_classes=None, out_features=None):
"""
Args:
stem (nn.Module): a stem module
stages (list[list[ResNetBlock]]): several (typically 4) stages,
each contains multiple :class:`ResNetBlockBase`.
num_classes (None or int): if None, will not perform classification.
out_features (list[str]): name of the layers whose outputs should
be returned in forward. Can be anything in "stem", "linear", or "res2" ...
If None, will return the output of the last layer.
"""
super(ResNet, self).__init__()
self.stem = stem
self.num_classes = num_classes
current_stride = self.stem.stride
self._out_feature_strides = {"stem": current_stride}
self._out_feature_channels = {"stem": self.stem.out_channels}
self.stages = []
self.names = []
for i, blocks in enumerate(stages):
for block in blocks:
assert isinstance(block, ResNetBlockBase), block
curr_channels = block.out_channels
stage = nn.Sequential(*blocks)
name = "res" + str(i + 2)
self.stages.append(stage)
self.names.append(name)
self._out_feature_strides[name] = current_stride = int(
current_stride * np.prod([k.stride for k in blocks])
)
self._out_feature_channels[name] = blocks[-1].out_channels
self.stages = nn.ModuleList(self.stages)
if num_classes is not None:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.linear = nn.Linear(curr_channels, num_classes)
# Sec 5.1 in "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour":
# "The 1000-way fully-connected layer is initialized by
# drawing weights from a zero-mean Gaussian with standard deviation of 0.01."
nn.init.normal_(self.linear.weight, stddev=0.01)
name = "linear"
if out_features is None:
out_features = [name]
self._out_features = out_features
assert len(self._out_features)
for out_feature in self._out_features:
assert out_feature in self.names, "Available children: {}".format(", ".join(self.names))
def forward(self, x):
outputs = {}
x = self.stem(x)
if "stem" in self._out_features:
outputs["stem"] = x
for ix, stage in enumerate(self.stages):
name = self.names[ix]
x = stage(x)
if name in self._out_features:
outputs[name] = x
if self.num_classes is not None:
x = self.avgpool(x)
x = self.linear(x)
if "linear" in self._out_features:
outputs["linear"] = x
return outputs
def build_resnet_backbone(in_channels=3):
norm = "BN"
stem = BasicStem(
in_channels=in_channels,
out_channels=64,
norm=norm,
)
# fmt: off
out_features = ["res2", "res3", "res4"]
depth = 101
num_groups = 1
bottleneck_channels = 64
in_channels = 64
out_channels = 256
stride_in_1x1 = True
res5_dilation = 1
# fmt: on
assert res5_dilation in {1, 2}, "res5_dilation cannot be {}.".format(res5_dilation)
num_blocks_per_stage = {50: [3, 4, 6, 3], 101: [3, 4, 23, 3], 152: [3, 8, 36, 3]}[depth]
stages = []
# Avoid creating variables without gradients
# It consumes extra memory and may cause allreduce to fail
out_stage_idx = [{"res2": 2, "res3": 3, "res4": 4, "res5": 5}[f] for f in out_features]
max_stage_idx = max(out_stage_idx)
for idx, stage_idx in enumerate(range(2, max_stage_idx + 1)):
dilation = res5_dilation if stage_idx == 5 else 1
first_stride = 1 if idx == 0 or (stage_idx == 5 and dilation == 2) else 2
stage_kargs = dict()
stage_kargs.update({
"num_blocks": num_blocks_per_stage[idx],
"first_stride": first_stride,
"in_channels": in_channels,
"bottleneck_channels": bottleneck_channels,
"out_channels": out_channels,
"num_groups": num_groups,
"norm": norm,
"stride_in_1x1": stride_in_1x1,
"dilation": dilation,
})
stage_kargs["block_class"] = BottleneckBlock
blocks = make_stage(**stage_kargs)
in_channels = out_channels
out_channels *= 2
bottleneck_channels *= 2
stages.append(blocks)
return ResNet(stem, stages, out_features=out_features)
@@ -0,0 +1,244 @@
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.model_zoo as model_zoo
from bodyseg.backbone.backbone import Backbone
def fixed_padding(inputs, kernel_size, dilation):
kernel_size_effective = kernel_size + (kernel_size - 1) * (dilation - 1)
pad_total = kernel_size_effective - 1
pad_beg = pad_total // 2
pad_end = pad_total - pad_beg
padded_inputs = F.pad(inputs, (pad_beg, pad_end, pad_beg, pad_end))
return padded_inputs
class SeparableConv2d(nn.Module):
def __init__(self, inplanes, planes, kernel_size=3, stride=1, dilation=1, bias=False, BatchNorm=None):
super(SeparableConv2d, self).__init__()
self.conv1 = nn.Conv2d(inplanes, inplanes, kernel_size, stride, 0, dilation,
groups=inplanes, bias=bias)
self.bn = BatchNorm(inplanes)
self.pointwise = nn.Conv2d(inplanes, planes, 1, 1, 0, 1, 1, bias=bias)
def forward(self, x):
x = fixed_padding(x, self.conv1.kernel_size[0], dilation=self.conv1.dilation[0])
x = self.conv1(x)
x = self.bn(x)
x = self.pointwise(x)
return x
class Block(nn.Module):
def __init__(self, inplanes, planes, reps, stride=1, dilation=1, BatchNorm=None,
start_with_relu=True, grow_first=True, is_last=False):
super(Block, self).__init__()
if planes != inplanes or stride != 1:
self.skip = nn.Conv2d(inplanes, planes, 1, stride=stride, bias=False)
self.skipbn = BatchNorm(planes)
else:
self.skip = None
self.relu = nn.ReLU(inplace=True)
rep = []
filters = inplanes
if grow_first:
rep.append(self.relu)
rep.append(SeparableConv2d(inplanes, planes, 3, 1, dilation, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
filters = planes
for i in range(reps - 1):
rep.append(self.relu)
rep.append(SeparableConv2d(filters, filters, 3, 1, dilation, BatchNorm=BatchNorm))
rep.append(BatchNorm(filters))
if not grow_first:
rep.append(self.relu)
rep.append(SeparableConv2d(inplanes, planes, 3, 1, dilation, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
if stride != 1:
rep.append(self.relu)
rep.append(SeparableConv2d(planes, planes, 3, 2, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
if stride == 1 and is_last:
rep.append(self.relu)
rep.append(SeparableConv2d(planes, planes, 3, 1, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
if not start_with_relu:
rep = rep[1:]
self.rep = nn.Sequential(*rep)
def forward(self, inp):
x = self.rep(inp)
if self.skip is not None:
skip = self.skip(inp)
skip = self.skipbn(skip)
else:
skip = inp
x = x + skip
return x
class AlignedXception(Backbone):
"""
Modified Alighed Xception
"""
def __init__(self, output_stride, BatchNorm):
super(AlignedXception, self).__init__()
if output_stride == 16:
entry_block3_stride = 2
middle_block_dilation = 1
exit_block_dilations = (1, 2)
elif output_stride == 8:
entry_block3_stride = 1
middle_block_dilation = 2
exit_block_dilations = (2, 4)
else:
raise NotImplementedError
# Entry flow
self.conv1 = nn.Conv2d(3, 32, 3, stride=2, padding=1, bias=False)
self.bn1 = BatchNorm(32)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(32, 64, 3, stride=1, padding=1, bias=False)
self.bn2 = BatchNorm(64)
self.block1 = Block(64, 128, reps=2, stride=2, BatchNorm=BatchNorm, start_with_relu=False)
self.block2 = Block(128, 256, reps=2, stride=2, BatchNorm=BatchNorm, start_with_relu=False,
grow_first=True)
self.block3 = Block(256, 728, reps=2, stride=entry_block3_stride, BatchNorm=BatchNorm,
start_with_relu=True, grow_first=True, is_last=True)
# Middle flow
self.block4 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block5 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block6 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block7 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block8 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block9 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block10 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block11 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block12 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block13 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block14 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block15 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block16 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block17 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block18 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block19 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
# Exit flow
self.block20 = Block(728, 1024, reps=2, stride=1, dilation=exit_block_dilations[0],
BatchNorm=BatchNorm, start_with_relu=True, grow_first=False, is_last=True)
self.conv3 = SeparableConv2d(1024, 1536, 3, stride=1, dilation=exit_block_dilations[1], BatchNorm=BatchNorm)
self.bn3 = BatchNorm(1536)
self.conv4 = SeparableConv2d(1536, 1536, 3, stride=1, dilation=exit_block_dilations[1], BatchNorm=BatchNorm)
self.bn4 = BatchNorm(1536)
self.conv5 = SeparableConv2d(1536, 2048, 3, stride=1, dilation=exit_block_dilations[1], BatchNorm=BatchNorm)
self.bn5 = BatchNorm(2048)
# Init weights
self._init_weight()
def forward(self, x):
# Entry flow
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.conv2(x)
x = self.bn2(x)
x = self.relu(x)
x = self.block1(x)
# add relu here
x = self.relu(x)
low_level_feat = x
x = self.block2(x)
x = self.block3(x)
# Middle flow
x = self.block4(x)
x = self.block5(x)
x = self.block6(x)
x = self.block7(x)
x = self.block8(x)
x = self.block9(x)
x = self.block10(x)
x = self.block11(x)
x = self.block12(x)
x = self.block13(x)
x = self.block14(x)
x = self.block15(x)
x = self.block16(x)
x = self.block17(x)
x = self.block18(x)
x = self.block19(x)
# Exit flow
x = self.block20(x)
x = self.relu(x)
x = self.conv3(x)
x = self.bn3(x)
x = self.relu(x)
x = self.conv4(x)
x = self.bn4(x)
x = self.relu(x)
x = self.conv5(x)
x = self.bn5(x)
x = self.relu(x)
return x, low_level_feat
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.SyncBatchNorm):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
if __name__ == "__main__":
import torch
model = AlignedXception(BatchNorm=nn.BatchNorm2d, output_stride=16)
input = torch.rand(1, 3, 512, 512)
output, low_level_feat = model(input)
print(output.size())
print(low_level_feat.size())
@@ -0,0 +1,330 @@
import sys
# sys.path.append("/Users/momo/human_seg_train")
# print(sys.path)
import torch
import torch.nn as nn
import torch.nn.functional as F
from bodyseg.backbone.backbone import get_norm
class ConvBNReLU(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1, norm_layer=None):
padding = (kernel_size - 1) // 2
if norm_layer is None:
norm_layer = nn.BatchNorm2d
super(ConvBNReLU, self).__init__(
nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False),
norm_layer(out_planes),
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio, norm_layer=None):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
if norm_layer is None:
norm_layer = nn.BatchNorm2d
hidden_dim = int(round(inp * expand_ratio))
self.use_res_connect = self.stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# pw
layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1, norm_layer=norm_layer))
layers.extend([
# dw
ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim, norm_layer=norm_layer),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
norm_layer(oup),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class UpSampleBlock(nn.Module):
def __init__(self, in_channels, out_channels, expand_ratio=6):
super(UpSampleBlock, self).__init__()
self.refine = InvertedResidual(in_channels, out_channels, 1, expand_ratio)
def forward(self, x0, x1):
x = torch.cat([x0, x1], dim=1)
x = self.refine(x)
return x
class BodySegNet_32_thin_sigmod(nn.Module):
def __init__(self, input_channels=3, class_nums=1, output_onnx=False):
super(BodySegNet_32_thin_sigmod, self).__init__()
self.class_nums = class_nums
self.output_onnx = output_onnx
self.stage_1 = nn.Sequential(
ConvBNReLU(input_channels, 16, kernel_size=3, stride=2)
)
self.stage_2 = nn.Sequential(
ConvBNReLU(16, 16, kernel_size=3, stride=2, groups=16),
ConvBNReLU(16, 16, kernel_size=1, stride=1),
)
self.stage_3 = nn.Sequential(
InvertedResidual(16, 24, stride=2, expand_ratio=6),
InvertedResidual(24, 24, stride=1, expand_ratio=6),
InvertedResidual(24, 24, stride=1, expand_ratio=6),
)
self.stage_4 = nn.Sequential(
InvertedResidual(24, 32, stride=2, expand_ratio=6),
InvertedResidual(32, 32, stride=1, expand_ratio=6),
InvertedResidual(32, 32, stride=1, expand_ratio=6),
InvertedResidual(32, 32, stride=1, expand_ratio=6),
)
self.stage_5 = nn.Sequential(
InvertedResidual(32, 48, stride=2, expand_ratio=6),
InvertedResidual(48, 48, stride=1, expand_ratio=6),
InvertedResidual(48, 48, stride=1, expand_ratio=6),
InvertedResidual(48, 48, stride=1, expand_ratio=6)
)
self.up_to_4 = UpSampleBlock(48 + 32, 16)
self.up_to_3 = UpSampleBlock(16 + 24, 16)
self.up_to_2 = UpSampleBlock(16 + 16, 16)
self.up_to_1 = UpSampleBlock(16 + 16, 16)
self.last_layer = nn.Sequential(
ConvBNReLU(16, 16, kernel_size=1, stride=1),
nn.Conv2d(16, self.class_nums, kernel_size=1, stride=1)
)
self._initialize_weights()
def forward(self, x):
feature_S = []
x1 = self.stage_1(x)
x2 = self.stage_2(x1)
x3 = self.stage_3(x2)
x4 = self.stage_4(x3)
feature = self.stage_5(x4)
feature = F.interpolate(feature, size=x4.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_4(x4, feature)
feature = F.interpolate(feature, size=x3.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_3(x3, feature)
feature = F.interpolate(feature, size=x2.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_2(x2, feature)
feature = F.interpolate(feature, size=x1.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_1(x1, feature)
feature_S.append(feature)
feature = self.last_layer(feature)
feature_S.append(feature)
output = F.interpolate(feature, size=x.size()[2:], mode='bilinear', align_corners=True)
# output = torch.sigmoid(output)
if self.output_onnx:
output = torch.argmax(output, dim=1).to(torch.float32)
return output, feature_S
def _initialize_weights(self):
for name, m in self.named_modules():
if isinstance(m, nn.Conv2d):
if 'first' in name:
nn.init.normal_(m.weight, 0, 0.01)
else:
nn.init.normal_(m.weight, 0, 1.0 / m.weight.shape[1])
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0.0001)
nn.init.constant_(m.running_mean, 0)
elif isinstance(m, nn.BatchNorm1d):
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0.0001)
nn.init.constant_(m.running_mean, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
class _ASPPModule(nn.Module):
def __init__(self, inplanes, planes, kernel_size, padding, dilation, BatchNorm):
super(_ASPPModule, self).__init__()
self.atrous_conv = nn.Conv2d(inplanes, planes, kernel_size=kernel_size,
stride=1, padding=padding, dilation=dilation, bias=False)
self.bn = BatchNorm(planes)
self.relu = nn.ReLU()
self._init_weight()
def forward(self, x):
x = self.atrous_conv(x)
x = self.bn(x)
return self.relu(x)
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight)
class ASPP(nn.Module):
def __init__(self, backbone, output_stride, BatchNorm):
super(ASPP, self).__init__()
if backbone == 'drn':
inplanes = 512
elif backbone == 'mobilenet':
inplanes = 320
elif backbone == 'resnet_fpn':
inplanes = 256
else:
inplanes = 2048
if output_stride == 16:
dilations = [1, 6, 12, 18]
elif output_stride == 8:
dilations = [1, 12, 24, 36]
else:
raise NotImplementedError
self.aspp1 = _ASPPModule(inplanes, 256, 1, padding=0, dilation=dilations[0], BatchNorm=BatchNorm)
self.aspp2 = _ASPPModule(inplanes, 256, 3, padding=dilations[1], dilation=dilations[1], BatchNorm=BatchNorm)
self.aspp3 = _ASPPModule(inplanes, 256, 3, padding=dilations[2], dilation=dilations[2], BatchNorm=BatchNorm)
self.aspp4 = _ASPPModule(inplanes, 256, 3, padding=dilations[3], dilation=dilations[3], BatchNorm=BatchNorm)
self.global_avg_pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)),
nn.Conv2d(inplanes, 256, 1, stride=1, bias=False),
BatchNorm(256),
nn.ReLU())
self.conv1 = nn.Conv2d(1280, 256, 1, bias=False)
self.bn1 = BatchNorm(256)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
self._init_weight()
def forward(self, x):
x1 = self.aspp1(x)
x2 = self.aspp2(x)
x3 = self.aspp3(x)
x4 = self.aspp4(x)
x5 = self.global_avg_pool(x)
x5 = F.interpolate(x5, size=x4.size()[2:], mode='bilinear', align_corners=True)
# x5 = F.interpolate(x5, size=x4.size()[2:], mode='nearest')
x = torch.cat((x1, x2, x3, x4, x5), dim=1)
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
return self.dropout(x)
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight)
class Decoder(nn.Module):
def __init__(self,num_classes, backbone, BatchNorm):
super(Decoder, self).__init__()
if backbone == 'resnet_fpn' or backbone == 'drn':
low_level_inplanes = 256
elif backbone == 'xception':
low_level_inplanes = 128
elif backbone == 'mobilenet':
low_level_inplanes = 24
else:
raise NotImplementedError
self.conv1 = nn.Conv2d(low_level_inplanes, 16, 1, bias=False)
self.bn1 = BatchNorm(16)
self.relu = nn.ReLU()
self.last_conv = nn.Sequential(nn.Conv2d(304, 256, kernel_size=3, stride=1, padding=1, bias=False),
BatchNorm(256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1, bias=False),
BatchNorm(256),
nn.ReLU(),
nn.Dropout(0.1),
nn.Conv2d(256, num_classes, kernel_size=1, stride=1))
self.up_to_1 = UpSampleBlock(16 + 16, 16)
self.last_layer = nn.Sequential(
ConvBNReLU(16, 16, kernel_size=1, stride=1),
nn.Conv2d(16, 1, kernel_size=1, stride=1)
)
self._init_weight()
# x(1,256,8,6) low(1,256,32,24) -》 x(1,1,32,24)
def forward(self, x, low_level_feat):
feature_T = []
# deeplab part
#(1,256,32,24) -> (1,16,64,48)
low_level_feat = self.conv1(low_level_feat)
low_level_feat = self.bn1(low_level_feat)
low_level_feat = self.relu(low_level_feat)
# x(1, 256, 8, 6)-> (1,16,32,24)
x = F.interpolate(x, size=low_level_feat.size()[2:], mode='bilinear', align_corners=True)
low_level_feat = F.interpolate(low_level_feat,
size=[low_level_feat.size()[2] * 2, low_level_feat.size()[3] * 2],
mode='bilinear', align_corners=True)
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
# bodyseg part
feature = F.interpolate(x, size=[x.size()[2] * 2,x.size()[3] * 2], mode='bilinear', align_corners=True)
# 输入up_to_1 x(low)(1,16,64,48),上采样2倍后的feature(1,16,64,48)
feature = self.up_to_1(low_level_feat, feature)
feature_T.append(feature)
# (1,1,64,48)
feature = self.last_layer(feature)
feature_T.append(feature)
# (1,1,128,96)
output = F.interpolate(feature, size=[128,96], mode='bilinear', align_corners=True)
# output = torch.sigmoid(output)
return output, feature_T
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight)
def build_backbone(backbone, output_stride, BatchNorm, input_channel=3):
from bodyseg.backbone.fpn import build_resnet_fpn_backbone
from bodyseg.backbone.xception import AlignedXception
return build_resnet_fpn_backbone(input_channel)
def build_aspp(backbone, output_stride, BatchNorm):
return ASPP(backbone, output_stride, BatchNorm)
def build_decoder(num_classes, backbone, BatchNorm):
return Decoder(num_classes, backbone, BatchNorm)
class DeepLab(nn.Module):
def __init__(self, input_channel=3, class_num=1):
super(DeepLab, self).__init__()
BatchNorm = get_norm("BN")
self.backbone = build_backbone("resnet_fpn", 16, BatchNorm, input_channel=input_channel)
self.aspp = build_aspp("resnet_fpn", 16, BatchNorm)
self.decoder = build_decoder(class_num, "resnet_fpn", BatchNorm)
def forward(self, input):
# input(1,3,128,96)
#output: "p2"(1,256,32,24), "p3"(1,256,16,12), "p4"(1,256,8,6)
output = self.backbone(input)
# "p4"(1,256,8,6) "p2"(1,256,32,24)
x, low_level_feat = output['p4'], output['p2']
# x(1,256,8,6)-》(1,256,8,6)
x = self.aspp(x)
# x(1,256,8,6) low(1,256,32,24) -》 x(1,1,32,24)
x, feature_T = self.decoder(x, low_level_feat)
# x(1,1,128,96)
x = F.interpolate(x, size=input.size()[2:], mode='bilinear', align_corners=True)
# x = F.interpolate(x, size=input.size()[2:], mode='nearest')
return x, feature_T
+98
View File
@@ -0,0 +1,98 @@
import time
import cv2
from hairstyle_model_infer import HairStyle_Model_Infer
from utils import enhance_hair
import numpy as np
from utils import landmark_processor
hairstyle_process = HairStyle_Model_Infer(gpu=True, use_enhance=True)
def resize_pre_webui(input_img, mask_img, out_h=None, out_w=None):
# 获取头发处理的局部区域图像
box_info = hairstyle_process.get_body_info(input_img)
dst_size = (576, 768)
if out_h is not None and out_w is not None:
dst_size = (out_w, out_h)
box_w, box_h = box_info[2] - box_info[0], box_info[3] - box_info[1]
scale = min(dst_size[1] / box_h, dst_size[0] / box_w)
rotate_center = [(box_info[2] + box_info[0]) * 0.5, (box_info[3] + box_info[1]) * 0.5]
M = cv2.getRotationMatrix2D(rotate_center, 0, scale)
M[:, 2] += np.float32([dst_size[0] * 0.5, dst_size[1] * 0.5]) - np.float32(rotate_center)
input_result = landmark_processor.high_quality_warpAffine(input_img, M, dst_size)
crop_matting = cv2.warpAffine(mask_img, M, dst_size)
mask_result = (crop_matting > 10).astype(np.float32)
mask_dilate = cv2.dilate(mask_result, np.ones((3, 9), np.uint8))
mask_dilate = np.clip(mask_dilate * 255, 0, 255).astype(np.uint8)
# cv2.imshow("input_result", input_result)
# cv2.imwrite("input_result.png", input_result)
# cv2.imshow("mask_dilate", mask_dilate)
# cv2.imwrite("mask_dilate.png", mask_dilate)
# cv2.waitKey(0)
return input_result, mask_dilate, M
def process_infer(user_img_path, target_color, color_dir, dst_path, ratio=1.0):
# 读取用户原始图片
origin_img = cv2.imread(user_img_path)
# resize用户原始图片,防止图片过大
user_scale = 1920 / max(origin_img.shape[0], origin_img.shape[1])
if user_scale < 1.0:
# 缩小图像应该使用INTER_AREA插值
origin_img = cv2.resize(origin_img, (0, 0), fx=user_scale, fy=user_scale, interpolation=cv2.INTER_AREA)
# 首先完成第一阶段的换发色
s1 = time.time()
hair_color_changed_img, hair_mask, status = hairstyle_process.infer_haircolor_v4(origin_img, target_hair_color=target_color, haircolor_dir=color_dir)
print("----------------------------infer_haircolor_v4", time.time()-s1)
hair_color_changed_img = (hair_color_changed_img * ratio + origin_img * (1 - ratio)).astype(np.uint8)
# 通过webui给照片加上标签
s2 = time.time()
# prompt = enhance_hair.webui_tag_by_clip(hair_color_changed_img)
prompt = ""
print("----------------------------webui_tag_by_clip", time.time() - s2)
# 处理图像尺寸
s3 = time.time()
crop_img, crop_mask, M = resize_pre_webui(hair_color_changed_img, hair_mask, 1024, 768)
print(crop_img.shape)
print(crop_mask.shape)
print("----------------------------resize_pre_webui", time.time() - s3)
# 通过webui对照片进行增强
s4 = time.time()
# cv2.imwrite('/home/student/Downloads/12121.jpg',crop_img)
enhanced_img = enhance_hair.webui_img2img(crop_img, crop_mask, prompt=prompt)
print("----------------------------webui_img2img", time.time() - s4)
# 写回原图
origin_img_final = origin_img.copy()
# restore the sd_result_small to origin_img
M_inv = cv2.invertAffineTransform(M)
cv2.warpAffine(enhanced_img, M_inv, (origin_img_final.shape[1], origin_img_final.shape[0]),
dst=origin_img_final,
borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.INTER_LANCZOS4)
# cv2.imshow("origin_img_final", origin_img_final)
# cv2.waitKey(0)
cv2.imwrite(dst_path, origin_img_final)
if __name__ == '__main__':
user_img_path = '/home/student/Downloads/base_color_v2.jpg'
# target_hair_color = [28, 55, 1] # RGB
# target_hair_color = [76, 55, 36] # RGB
# target_hair_color = [75, 1, 0] # RGB
target_hair_color = [247,235,213] # RGB
color_dir = "/home/data/hair/data/ref_color/247_235_213"
dst = "/home/data/hair/data/tmp/4.png"
process_infer(user_img_path=user_img_path, target_color=target_hair_color, color_dir=color_dir, dst_path=dst)
+140
View File
@@ -0,0 +1,140 @@
#coding:utf-8
import traceback
from uuid import uuid4
import imghdr
import torch
from gevent import monkey
monkey.patch_all()
import base64
import os
import random
import shutil
import time
import json
import os.path as osp
import urllib.request
import hashlib
import cv2
from datetime import datetime
import glob
import numpy as np
from core.hairstyle_model import HairStyle_Model
from hairstyle_model_infer import HairStyle_Model_Infer
from prepare_ref_hairstyle_data import prepare_single, prepare_single_color
from utils import enhance_hair
import configparser
from common.logger import config
from change_color import process_infer, resize_pre_webui
hairstyle_process = HairStyle_Model(gpu=True,use_enhance=True)
user_img_save_dir = config.get('default', 'userDir')
user_img_tmp_dir = config.get('default', 'tmp_dir')
user_img_res_dir = config.get('default', 'res_dir')
ref_user_dir = config.get('default', 'ref_user_dir')
train_save_dir = config.get('default', 'train_dir')
hair_template_material_dir = config.get('default', 'hair_template_material_dir')
ref_color_dir = config.get('default', 'ref_color')
ref_color_imgs_dir = config.get('default', 'ref_color_img')
train_upload_dir = config.get('default', 'upload_train_dir')
def download_img(img_url, userId=None, isfix=False, ismask=False):
try:
img_name = img_url.split("/")[-1]
tmp_dir = osp.join(user_img_tmp_dir, img_name)
# if osp.exists(tmp_dir):
# os.remove(tmp_dir)
print(img_url)
download_success = False
for i in range(3):
hairstyle_process.oss2.download_img(img_url, tmp_dir)
if osp.exists(tmp_dir) and osp.getsize(tmp_dir) > 0:
download_success = True
break
if download_success:
img_type = imghdr.what(tmp_dir)
new_tmp_dir = tmp_dir[:tmp_dir.rfind(".")+1] + img_type
shutil.move(tmp_dir, new_tmp_dir)
print("save path", new_tmp_dir)
else:
return None, None
return new_tmp_dir, None
except Exception as e:
print(e)
return None,None
def change_hair_colorv3():
datanow = datetime.now()
time_convert = datanow.strftime("%Y%m%d%H")
taskid = ''.join(str(random.choice(range(10))) for _ in range(6))
taskid = str(time_convert) + str(taskid)
start_time0 = time.time()
try:
img_url = "https://ydapp-1317132355.cos.ap-beijing.myqcloud.com/hair_mz/images/hairstyle/b8e75994-ec6c-4309-ac98-d9687ca5dfbf/ad28024a-2a98-4768-b1d3-846950f4dc57.jpg"
userId = "17519"
color = ''
rgb = [103, 103, 103]
ratio = 0.9
img_path, _ = download_img(img_url, userId)
print("---------------download img:", time.time()-start_time0)
start_time1 = time.time()
# gen material
color_name = img_path[img_path.rfind("/") + 1:]
print("color_name", color_name)
new_color_ref_img_path = os.path.join(ref_color_imgs_dir, color_name)
print("new_color_ref_img_path: ", new_color_ref_img_path)
shutil.copy(img_path, new_color_ref_img_path)
color_id = ""
for color_item in rgb:
color_id += str(color_item) + "_"
if color_id[-1] == "_":
color_id = color_id[:-1]
ref_color_save_dir = os.path.join(ref_color_dir, color_id)
if not os.path.exists(ref_color_save_dir):
os.mkdir(ref_color_save_dir)
print("!!!gen color material:", ref_color_save_dir)
prepare_single_color(new_color_ref_img_path, ref_color_save_dir)
req_id = str(uuid4())
res_path = os.path.join(user_img_res_dir, req_id + ".png")
print("---------------gen material:", time.time() - start_time1)
start_time2 = time.time()
process_infer(img_path, rgb, ref_color_save_dir, res_path, ratio)
print("res path: ", res_path)
print("---------------process_infer:", time.time() - start_time2)
start_time3 = time.time()
if os.path.exists(res_path):
ret_url = hairstyle_process.oss2.upload_file(res_path,
"hair_mz/images/hairstyle/{}/{}".format(color,
req_id + '.jpg'))
print("---------------upload img:", time.time() - start_time3)
except Exception as e:
print(e)
if __name__ == '__main__':
change_hair_colorv3()
@@ -0,0 +1,46 @@
import requests
import json
callback_hairstyle_url = "https://puton.meidaojia.com/api/cloth/callBack"
def callback_color(color_id, cover_img, success):
url = "http://172.21.0.3:8080/ydapp/system/platform/complete_color_model"
payload = json.dumps({
"success": success,
"colorId": color_id,
"coverImg": cover_img
})
headers = {
'X-MZ-API-TOKEN': '98bcf37c942a2240ba2d907c96ed1137',
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
return response
# print(response.text)
def callback_hairstyle(req_id, state, message, clothId):
url = callback_hairstyle_url
payload = json.dumps({
"taskId": req_id,
"status": state,
"clothId": clothId,
"msg": message
})
status = -1
print(f"url:{url},complete_hair_model payload:{payload}")
headers = {
'X-MZ-API-TOKEN': '98bcf37c942a2240ba2d907c96ed1137',
'Content-Type': 'application/json'
}
try:
response = requests.request("POST", url, headers=headers, data=payload)
print("response:", response.text)
status = 0
except Exception as e:
print(e)
return response.text, status
@@ -0,0 +1,27 @@
import pynvml
threshold = 0.9
def get_gpu(need_gpu_id):
used = get_gpu_threshold(need_gpu_id)
#小于一定的
if used > threshold:
need_use = get_use_gpu()
return need_use[0]
else:
return need_gpu_id
def get_use_gpu():
use=[]
for index in range(pynvml.nvmlDeviceGetCount()):
used = get_gpu_threshold(index)
if used > threshold:
use.append(index)
return use
def get_gpu_count():
return pynvml.nvmlDeviceGetCount()
def get_gpu_threshold(index):
handle = pynvml.nvmlDeviceGetHandleByIndex(index)
meminfo = pynvml.nvmlDeviceGetMemoryInfo(handle)
used = meminfo.used / meminfo.total
return used
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/python
# coding=utf-8
import os
from logging.handlers import TimedRotatingFileHandler
import logging
import configparser
config = configparser.ConfigParser() # 创建对象
config.read("config/configure.ini", encoding="utf-8") # 读取配置文件,如果配置文件不存在则创建
"""
自定义日志处理
"""
class LogFactory(object):
@staticmethod
def getLogger(log_name,log_level=None):
if log_level is None:
log_level = LogFactory.getLogLevel(getLevel())
logger = logging.getLogger(log_name)
path = getPath()
isExists=os.path.exists(path)
if not isExists:
os.makedirs(path)
log_file = os.path.join(path, '{}.log'.format(log_name))
if len(logger.handlers) <= 0:
handler = TimedRotatingFileHandler(log_file, when='D', interval=1)
else:
handler = logger.handlers[0]
formatter = logging.Formatter("%(asctime)s-%(thread)d-%(filename)s-%(levelname)s-%(message)s", "%Y-%m-%d %H:%M:%S")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(log_level)
return logger
@staticmethod
def getLogLevel(log_level):
log_level = getattr(logging, log_level.upper(), None)
if log_level is None:
raise Exception("No such log level.")
return log_level
def getPath():
import sys
process_order = sys.argv[1] if len(sys.argv) > 1 else None
logpath = config.get('logger', "logpath")
logpath = logpath.rstrip("/")
if process_order is None:
return logpath
return logpath
def getLevel():
return config.get('logger', "level")
@@ -0,0 +1,35 @@
[default]
modelDir = weights
hairstyleDir = /home/xsl/change_hair/project/data/ref_hairstyle
haircolorDir= /home/xsl/change_hair/project/data/ref_haircolor
userDir=/home/xsl/change_hair/project/data/userImage
tmp_dir=/home/xsl/change_hair/project/data/tmp
res_dir=/home/xsl/change_hair/project/data/res_dir
userInfo_dir=/home/xsl/change_hair/project/data/user_info
baseColor_ID=HDR10_443322
Port = 11023
refer_dir = /home/xsl/change_hair/project/data/ref_online
ref_user_dir = /home/xsl/change_hair/project/data/ref_user_imgs
train_dir = /home/xsl/change_hair/data/train_material
refImgDir=/home/xsl/change_hair/project/data/refImage
hair_template_material_dir=/home/xsl/change_hair/project/data/hair_template_material
ref_color=/home/xsl/change_hair/project/data/ref_color
ref_color_img=/home/xsl/change_hair/project/data/ref_color_imgs
upload_train_dir=/home/xsl/change_hair/project/data/upload_train_imgs
;version=local
version=online
[fix]
strength=1
[logger]
logpath = /home/xsl/change_hair/project/logs
level=INFO
[timelogger]
name=watch-time
[errorlogger]
level=ERROR
name=w-error
+116
View File
@@ -0,0 +1,116 @@
import os
import shutil
from pathlib import Path
def copy_files_by_id(id_list_file, source_dirs, output_dir):
"""
根据ID列表拷贝文件到新目录保持目录结构
参数:
id_list_file: 包含文件ID列表的文本文件
source_dirs: 源目录列表 [hair_template_material, ref_hairstyle, train_material, upload_train_imgs]
output_dir: 输出目录
"""
# 读取ID列表
with open(id_list_file, 'r') as f:
id_list = [line.strip() for line in f.readlines() if line.strip()]
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
# 遍历所有源目录
for src_dir in source_dirs:
if not os.path.exists(src_dir):
print(f"警告: 源目录不存在 {src_dir}")
continue
# 遍历源目录下的所有文件
for root, _, files in os.walk(src_dir):
for file in files:
# 检查文件名是否包含ID列表中的任一ID
if any(file_id in file for file_id in id_list):
src_path = os.path.join(root, file)
# 计算相对路径
rel_path = os.path.relpath(root, src_dir)
dst_dir = os.path.join(output_dir, os.path.basename(src_dir), rel_path)
# 创建目标目录并拷贝文件
os.makedirs(dst_dir, exist_ok=True)
dst_path = os.path.join(dst_dir, file)
shutil.copy2(src_path, dst_path)
print(f"已拷贝: {src_path} -> {dst_path}")
if __name__ == "__main__":
# import argparse
# parser = argparse.ArgumentParser(description='根据ID列表拷贝文件')
# parser.add_argument('id_list', help='包含文件ID列表的文本文件路径')
# parser.add_argument('output_dir', help='输出目录路径')
# parser.add_argument('--source_dirs', nargs='+',
# default=['hair_template_material', 'ref_hairstyle', 'train_material', 'upload_train_imgs'],
# help='源目录列表')
# args = parser.parse_args()
id_list = [1939351512506802177,
1939351392453238785,
1939351319849836545,
1939351238526476289,
1939351219396255745,
1939339435616608258,
1939330299344560130,
1938848782956732417,
1938848730150445058,
1938848709308948482,
1938841049238970370,
1938492830059438081,
1938492790033195009,
1938490058547240961,
1938489994072399874,
1938489915521474561,
1938489840707674114,
1938489739088076801,
1938489674713899010,
1938489620162781185]
output_dir = '/home/data/hair/data/hairStyle_addin/06300845'
hair_template_material = '/home/data/hair/data/hair_template_material'
ref_hairstyle = '/home/data/hair/data/ref_hairstyle'
train_material = '/mnt/database2/online-server/hair-online-tj-v2/train_material'
upload_train_imgs = '/home/data/hair/data/upload_train_imgs'
copy_files_by_id(id_list, source_dirs, args.output_dir)
def copy_files_by_id(id_list, hair_template_material, ref_hairstyle, train_material, upload_train_imgs, output_dir):
"""
根据ID列表从指定目录拷贝文件到输出目录
:param id_list: 文件ID列表(字符串格式)
:param hair_template_material: 发型模板材质目录
:param ref_hairstyle: 参考发型目录
:param train_material: 训练材质目录
:param upload_train_imgs: 上传训练图片目录
:param output_dir: 输出目录
"""
# 将ID转换为字符串格式
id_list = [str(id) for id in id_list]
# 创建源目录列表
source_dirs = [hair_template_material, ref_hairstyle, train_material, upload_train_imgs]
for src_dir in source_dirs:
if not os.path.exists(src_dir):
print(f"警告: 源目录不存在 {src_dir}")
continue
# 遍历源目录下的所有文件
for root, _, files in os.walk(src_dir):
for file in files:
# 检查文件名是否包含ID列表中的任一ID
if any(file_id in file for file_id in id_list):
src_path = os.path.join(root, file)
# 计算相对路径
rel_path = os.path.relpath(root, src_dir)
dst_dir = os.path.join(output_dir, os.path.basename(src_dir), rel_path)
# 创建目标目录并拷贝文件
os.makedirs(dst_dir, exist_ok=True)
dst_path = os.path.join(dst_dir, file)
shutil.copy2(src_path, dst_path)
print(f"已拷贝: {src_path} -> {dst_path}")
@@ -0,0 +1,196 @@
import torch
import torch.nn as nn
import math
import os
import cv2
import numpy as np
from core.utils import landmark_processor
import glob
# from models.BigResNetStable import MomocvFaceAlignment
def op_name(op_name, m):
m.op_name = op_name
return m
class Flatten(nn.Module):
def __init__(self, axis=1):
super(Flatten, self).__init__()
self.axis = axis
def forward(self, x):
assert self.axis == 1
x = x.reshape(x.shape[0], -1)
return x
def flatten(name, axis=1):
return op_name(name, Flatten(axis))
def conv_relu(name, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1):
return nn.Sequential(
op_name(name, nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, True)),
op_name(name.replace('conv', 'relu'), nn.LeakyReLU(inplace=False, negative_slope=5e-11)),
)
def conv(name, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1):
return op_name(name, nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, True))
def bn_conv_relu(name, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, momentum = 0.9, track_running_stats=True):
return nn.Sequential(
op_name(name + '_bn1', nn.BatchNorm2d(in_channels, momentum=momentum, eps=2e-5, track_running_stats=track_running_stats)),
op_name(name + '_conv1', nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, True)),
op_name(name + '_relu', nn.LeakyReLU(inplace=False, negative_slope=5e-11)),
)
def bn(name, in_channels, momentum = 0.9, track_running_stats=True):
return op_name(name, nn.BatchNorm2d(in_channels, momentum=momentum, eps=2e-5, track_running_stats=track_running_stats))
class BasicBlock(nn.Module):
def __init__(self, stage, unit, inplanes, outplanes, stride):
super(BasicBlock, self).__init__()
self.bn = bn('stage{}_unit{}_bn1'.format(stage, unit), inplanes)
self.conv1 = conv_relu('stage{}_unit{}_conv1'.format(stage, unit), inplanes, outplanes, kernel_size=3, stride=1, padding=1)
self.conv2 = conv('stage{}_unit{}_conv2'.format(stage, unit), outplanes, outplanes, kernel_size=3, stride=stride, padding=1)
if inplanes != outplanes or stride != 1:
self.conv_sc = conv('stage{}_unit{}_conv1sc'.format(stage, unit), inplanes, outplanes, kernel_size=1, stride=stride, padding=0)
self.inplanes = inplanes
self.outplanes = outplanes
self.stride = stride
def forward(self, x):
residule = x
out = self.bn(x)
out = self.conv1(out)
out = self.conv2(out)
if self.inplanes != self.outplanes or self.stride != 1:
residule = self.conv_sc(residule)
ret = out + residule
return ret
class ResnetBlock(nn.Module):
def __init__(self, stage, inplanes, outplanes, stride=2, n_blocks=1):
super(ResnetBlock, self).__init__()
self.conv = []
for m in range(n_blocks):
if m == 0:
self.conv.append(BasicBlock(stage, m + 1, inplanes, outplanes, stride))
else:
self.conv.append(BasicBlock(stage, m + 1, outplanes, outplanes, 1))
self.conv = nn.Sequential(*self.conv)
def forward(self, x):
ret = self.conv(x)
return ret
class FaceRecognitionServer(nn.Module):
def __init__(self):
super(FaceRecognitionServer, self).__init__()
ch_num = [64, 64, 128, 256, 512]
# ch_num = [64, 64]
strides = [2, 2, 2, 2]
n_blocks = [3, 4, 14, 3]
op_list = [conv_relu('conv0', 3, 64, 3, 1, 1)]
op_list += [ResnetBlock(i + 1, ch_num[i], ch_num[i + 1], strides[i], n_blocks[i]) for i in range(len(ch_num) - 1)]
op_list += [bn('bn1', 512)]
op_list += [flatten('flatten', 1)]
op_list += [op_name('pre_fc1', nn.Linear(25088, 512))]
self.features = nn.Sequential(*op_list)
model_path = os.path.dirname(os.path.split(os.path.realpath(__file__))[0])
weights = torch.load('weights/MMCVFaceRecognitionServer.pth',
map_location=lambda storage, loc: storage)
self.load_state_dict(weights)
self.eval()
def forward(self, x):
features = self.features(x)
return features
class MomocvFaceRecognitionServer(object):
def __init__(self, gpu_id=None):
self.gpu_id = gpu_id
self.device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
self.face_recognition_net = FaceRecognitionServer()
# self.mmcv = MomocvFaceAlignment()
# self.model_path, _ = os.path.split(os.path.realpath(__file__))
# weights = torch.load(os.path.join(self.model_path, 'MMCVFaceRecognitionServer.pth'),
# map_location=lambda storage, loc: storage)
# self.face_recognition_net.load_state_dict(weights)
self.face_recognition_net.to(self.device)
# self.face_recognition_net.eval()
def forward(self, images, landmarks):
dst_size = 112
with torch.no_grad():
input_numpy = np.zeros((len(images), 3, dst_size, dst_size), dtype=np.float32)
assert len(images) == len(landmarks)
for ix, img in enumerate(images):
landmark = landmarks[ix]
# get angel
# M = landmark_processor.get_transform_mat_full_face(landmark, 576, scale=1, offset=(0, 0.3))
# pt1k_crop = landmark_processor.transform_points(landmark, M)
# crop_face = cv2.warpAffine(img, M, (576, 768),
# flags=cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_REFLECT)
# t0 = time.time()
# landmarks87, poselayer, tracking_probe, occlusion_probe = self.mmcv.detect(crop_face, [pt1k_crop])
# # print('time:', time.time() - t0)
# if tracking_probe[0] < 0.5:
# continue
mat = landmark_processor.get_transform_mat_for_face_recognition(landmark, dst_size)[0:2]
tmp = cv2.warpAffine(img, mat, (dst_size, dst_size))
input_numpy[ix, :, :, :] = (tmp.transpose((2, 0, 1)).astype(np.float32) - 127.5) / 128
# cv2.imshow('tmp', tmp)
# cv2.waitKey()
in_tensor = torch.from_numpy(input_numpy)
in_tensor = in_tensor.to(self.device)
features = self.face_recognition_net(in_tensor)
features = features.cpu().numpy()
norm_factor = np.sqrt(np.sum(features ** 2, axis=1))
norm_factor = norm_factor.reshape(-1, 1)
features /= norm_factor
return features
def cos_sim(self, a, b):
a_norm = np.linalg.norm(a)
b_norm = np.linalg.norm(b)
cos = np.dot(a, b) / (a_norm * b_norm)
return cos
if __name__ == '__main__':
all_jpegs = glob.glob(r'D:\data\deepface_example\data\02b59e75ce91bda300ff827a85a74687d633cdfb5d830e7d3855e31b75201fa8\*.jpg')
for s_filename_path in all_jpegs:
img = cv2.imread(s_filename_path)
# cv2.imshow('img', img)
# cv2.waitKey()
# dflpng = DATAIMG(str(s_filename_path), print_on_no_embedded_data=True)
# if dflpng is None:
# print('ERROR')
#
# landmarks = dflpng.get_landmarks_mmcv_137()
#
# mmcv = MomocvFaceRecognitionServer()
# features = mmcv.forward([img, img], [landmarks, landmarks])
# print(features)
# print('conansherry')
# fullyconnected1 = fullyconnected1[0]
# fullyconnected1 = (np.reshape(fullyconnected1, (2, 87)).transpose((1, 0))).astype(np.int32)
# occlusion_probe = occlusion_probe[0]
# for ix, pt in enumerate(fullyconnected1):
# cv2.circle(img, (int(pt[0]), int(pt[1])), 1, (0, 255, 0) if occlusion_probe[ix] > 0.1 else (0, 0, 255), 2)
# # cv2.putText(img, str(ix), (int(pt[0]), int(pt[1])), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 255, 0), 1)
# cv2.imshow('img', img)
# cv2.waitKey()
Binary file not shown.
@@ -0,0 +1,75 @@
import numpy as np
import torch
from torch import nn
def get_norm(norm, out_channels=None):
"""
Args:
norm (str or callable):
Returns:
nn.Module or None: the normalization layer
"""
if isinstance(norm, str):
if len(norm) == 0:
return None
norm = {
"BN": nn.BatchNorm2d,
"IN": nn.InstanceNorm2d,
"GN": lambda channels: nn.GroupNorm(32, channels),
"nnSyncBN": nn.SyncBatchNorm, # keep for debugging
}[norm]
if out_channels is not None:
return norm(out_channels)
else:
return norm
class Conv2d(torch.nn.Conv2d):
"""
A wrapper around :class:`torch.nn.Conv2d` to support zero-size tensor and more features.
"""
def __init__(self, *args, **kwargs):
"""
Extra keyword arguments supported in addition to those in `torch.nn.Conv2d`:
Args:
norm (nn.Module, optional): a normalization layer
activation (callable(Tensor) -> Tensor): a callable activation function
It assumes that norm layer is used before activation.
"""
norm = kwargs.pop("norm", None)
activation = kwargs.pop("activation", None)
super().__init__(*args, **kwargs)
self.norm = norm
self.activation = activation
def forward(self, x):
x = super().forward(x)
if self.norm is not None:
x = self.norm(x)
if self.activation is not None:
x = self.activation(x)
return x
class Backbone(nn.Module):
"""
Abstract base class for network backbones.
"""
def __init__(self):
"""
The `__init__` method of any subclass can specify its own set of arguments.
"""
super().__init__()
def forward(self):
"""
Subclasses must override this method, but adhere to the same return type.
Returns:
dict[str: Tensor]: mapping from feature name (e.g., "res2") to tensor
"""
pass
@@ -0,0 +1,268 @@
import math
import core.utils.weight_init as weight_init
import torch
import torch.nn.functional as F
from torch import nn
from core.bodyseg.backbone.backbone import Backbone, get_norm, Conv2d
from core.bodyseg.backbone.resnet import build_resnet_backbone
class FPN(Backbone):
"""
This module implements Feature Pyramid Network.
It creates pyramid features built on top of some input feature maps.
"""
def __init__(
self, bottom_up, in_features, out_channels, norm="", top_block=None, fuse_type="sum"
):
"""
Args:
bottom_up (Backbone): module representing the bottom up subnetwork.
Must be a subclass of :class:`Backbone`. The multi-scale feature
maps generated by the bottom up network, and listed in `in_features`,
are used to generate FPN levels.
in_features (list[str]): names of the input feature maps coming
from the backbone to which FPN is attached. For example, if the
backbone produces ["res2", "res3", "res4"], any *contiguous* sublist
of these may be used; order must be from high to low resolution.
out_channels (int): number of channels in the output feature maps.
norm (str): the normalization to use.
top_block (nn.Module or None): if provided, an extra operation will
be performed on the output of the last (smallest resolution)
FPN output, and the result will extend the result list. The top_block
further downsamples the feature map. It must have an attribute
"num_levels", meaning the number of extra FPN levels added by
this block, and "in_feature", which is a string representing
its input feature (e.g., p5).
fuse_type (str): types for fusing the top down features and the lateral
ones. It can be "sum" (default), which sums up element-wise; or "avg",
which takes the element-wise mean of the two.
"""
super(FPN, self).__init__()
assert isinstance(bottom_up, Backbone)
# Feature map strides and channels from the bottom up network (e.g. ResNet)
in_strides = [bottom_up._out_feature_strides[f] for f in in_features]
in_channels = [bottom_up._out_feature_channels[f] for f in in_features]
_assert_strides_are_log2_contiguous(in_strides)
lateral_convs = []
output_convs = []
use_bias = norm == ""
for idx, in_channels in enumerate(in_channels):
lateral_norm = get_norm(norm, out_channels)
output_norm = get_norm(norm, out_channels)
lateral_conv = Conv2d(
in_channels, out_channels, kernel_size=1, bias=use_bias, norm=lateral_norm
)
output_conv = Conv2d(
out_channels,
out_channels,
kernel_size=3,
stride=1,
padding=1,
bias=use_bias,
norm=output_norm,
)
weight_init.c2_xavier_fill(lateral_conv)
weight_init.c2_xavier_fill(output_conv)
stage = int(math.log2(in_strides[idx]))
lateral_convs.append(lateral_conv)
output_convs.append(output_conv)
# Place convs into top-down order (from low to high resolution)
# to make the top-down computation in forward clearer.
self.lateral_convs = nn.ModuleList(lateral_convs[::-1])
self.output_convs = nn.ModuleList(output_convs[::-1])
self.top_block = top_block
self.in_features = in_features
self.bottom_up = bottom_up
# Return feature names are "p<stage>", like ["p2", "p3", ..., "p6"]
self._out_feature_strides = {"p{}".format(int(math.log2(s))): s for s in in_strides}
# top block output feature maps.
if self.top_block is not None:
for s in range(stage, stage + self.top_block.num_levels):
self._out_feature_strides["p{}".format(s + 1)] = 2 ** (s + 1)
self._out_features = list(self._out_feature_strides.keys())
self._out_feature_channels = {k: out_channels for k in self._out_features}
assert fuse_type in {"avg", "sum"}
self._fuse_type = fuse_type
def forward(self, x):
"""
Args:
input (dict[str: Tensor]): mapping feature map name (e.g., "res5") to
feature map tensor for each feature level in high to low resolution order.
Returns:
dict[str: Tensor]:
mapping from feature map name to FPN feature map tensor
in high to low resolution order. Returned feature names follow the FPN
paper convention: "p<stage>", where stage has stride = 2 ** stage e.g.,
["p2", "p3", ..., "p6"].
"""
# Reverse feature maps into top-down order (from low to high resolution)
bottom_up_features = self.bottom_up(x)
x = [bottom_up_features[f] for f in self.in_features[::-1]]
results = []
prev_features = self.lateral_convs[0](x[0])
results.append(self.output_convs[0](prev_features))
for features, lateral_conv, output_conv in zip(
x[1:], self.lateral_convs[1:], self.output_convs[1:]
):
top_down_features = F.interpolate(prev_features, scale_factor=2, mode="nearest")
lateral_features = lateral_conv(features)
prev_features = lateral_features + top_down_features
if self._fuse_type == "avg":
prev_features /= 2
results.insert(0, output_conv(prev_features))
if self.top_block is not None:
top_block_in_feature = bottom_up_features.get(self.top_block.in_feature, None)
if top_block_in_feature is None:
top_block_in_feature = results[self._out_features.index(self.top_block.in_feature)]
results.extend(self.top_block(top_block_in_feature))
assert len(self._out_features) == len(results)
return dict(zip(self._out_features, results))
def _assert_strides_are_log2_contiguous(strides):
"""
Assert that each stride is 2x times its preceding stride, i.e. "contiguous in log2".
"""
for i, stride in enumerate(strides[1:], 1):
assert stride == 2 * strides[i - 1], "Strides {} {} are not log2 contiguous".format(
stride, strides[i - 1]
)
class LastLevelMaxPool(nn.Module):
"""
This module is used in the original FPN to generate a downsampled
P6 feature from P5.
"""
def __init__(self):
super().__init__()
self.num_levels = 1
self.in_feature = "p5"
def forward(self, x):
return [F.max_pool2d(x, kernel_size=1, stride=2, padding=0)]
class LastLevelP6P7(nn.Module):
"""
This module is used in RetinaNet to generate extra layers, P6 and P7 from
C5 feature.
"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.num_levels = 2
self.in_feature = "res5"
self.p6 = nn.Conv2d(in_channels, out_channels, 3, 2, 1)
self.p7 = nn.Conv2d(out_channels, out_channels, 3, 2, 1)
for module in [self.p6, self.p7]:
weight_init.c2_xavier_fill(module)
def forward(self, c5):
p6 = self.p6(c5)
p7 = self.p7(F.relu(p6))
return [p6, p7]
def build_resnet_fpn_backbone(in_channels=3):
"""
Args:
cfg: a detectron2 CfgNode
Returns:
backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`.
"""
bottom_up = build_resnet_backbone(in_channels)
in_features = ["res2", "res3", "res4"]
out_channels = 256
backbone = FPN(
bottom_up=bottom_up,
in_features=in_features,
out_channels=out_channels,
norm="BN",
# top_block=LastLevelMaxPool(),
top_block=None,
fuse_type="sum",
)
return backbone
def build_retinanet_resnet_fpn_backbone(cfg, in_channels=3):
"""
Args:
cfg: a detectron2 CfgNode
Returns:
backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`.
"""
bottom_up = build_resnet_backbone(cfg, in_channels)
in_features = cfg.MODEL.FPN.IN_FEATURES
out_channels = cfg.MODEL.FPN.OUT_CHANNELS
in_channels_p6p7 = bottom_up._out_feature_channels["res5"]
backbone = FPN(
bottom_up=bottom_up,
in_features=in_features,
out_channels=out_channels,
norm=cfg.MODEL.FPN.NORM,
top_block=LastLevelP6P7(in_channels_p6p7, out_channels),
fuse_type=cfg.MODEL.FPN.FUSE_TYPE,
)
return backbone
if __name__ == "__main__":
import argparse
from config.default import get_cfg
def setup(args):
"""
Create configs and perform basic setups.
"""
cfg = get_cfg()
cfg.merge_from_file(args.cfg)
cfg.merge_from_list(args.opts)
cfg.freeze()
return cfg
parser = argparse.ArgumentParser(description='Train ImageNet network')
# general
parser.add_argument('--cfg',
help='experiment configure file name',
required=True,
type=str)
parser.add_argument('opts',
help="Modify config options using the command-line",
default=None,
nargs=argparse.REMAINDER)
args = parser.parse_args()
cfg = setup(args)
print(cfg)
model = build_resnet_fpn_backbone(cfg, 3)
# model = build_retinanet_resnet_fpn_backbone(cfg, 3)
print(model)
# model = torch.nn.DataParallel(model, list(range(2))).cuda()
dummy_input = torch.randn(4, 3, 512, 512)
out = model(dummy_input)
for k, v in out.items():
print(k, v.shape)
# torch.onnx.export(model, dummy_input, "tmp.onnx", verbose=True,
# input_names=['input'],
# output_names=['output'])
pass
@@ -0,0 +1,298 @@
import numpy as np
import core.utils.weight_init as weight_init
import torch
import torch.nn.functional as F
from torch import nn
from core.bodyseg.backbone.backbone import Backbone, get_norm, Conv2d
class BasicStem(nn.Module):
def __init__(self, in_channels=3, out_channels=64, norm="BN"):
"""
Args:
norm (str or callable): a callable that takes the number of
channels and return a `nn.Module`, or a pre-defined string
(one of {"FrozenBN", "BN", "GN"}).
"""
super().__init__()
self.conv1 = Conv2d(
in_channels,
out_channels,
kernel_size=7,
stride=2,
padding=3,
bias=False,
norm=get_norm(norm, out_channels),
)
weight_init.c2_msra_fill(self.conv1)
def forward(self, x):
x = self.conv1(x)
x = F.relu_(x)
x = F.max_pool2d(x, kernel_size=3, stride=2, padding=1)
return x
@property
def out_channels(self):
return self.conv1.out_channels
@property
def stride(self):
return 4 # = stride 2 conv -> stride 2 max pool
class ResNetBlockBase(nn.Module):
def __init__(self, in_channels, out_channels, stride):
"""
The `__init__` method of any subclass should also contain these arguments.
Args:
in_channels (int):
out_channels (int):
stride (int):
"""
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.stride = stride
class BottleneckBlock(ResNetBlockBase):
def __init__(
self,
in_channels,
out_channels,
*,
bottleneck_channels,
stride=1,
num_groups=1,
norm="BN",
stride_in_1x1=False,
dilation=1,
):
"""
Args:
norm (str or callable): a callable that takes the number of
channels and return a `nn.Module`, or a pre-defined string
(one of {"FrozenBN", "BN", "GN"}).
stride_in_1x1 (bool): when stride==2, whether to put stride in the
first 1x1 convolution or the bottleneck 3x3 convolution.
"""
super().__init__(in_channels, out_channels, stride)
if in_channels != out_channels:
self.shortcut = Conv2d(
in_channels,
out_channels,
kernel_size=1,
stride=stride,
bias=False,
norm=get_norm(norm, out_channels),
)
else:
self.shortcut = None
# The original MSRA ResNet models have stride in the first 1x1 conv
# The subsequent fb.torch.resnet and Caffe2 ResNe[X]t implementations have
# stride in the 3x3 conv
stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride)
self.conv1 = Conv2d(
in_channels,
bottleneck_channels,
kernel_size=1,
stride=stride_1x1,
bias=False,
norm=get_norm(norm, bottleneck_channels),
)
self.conv2 = Conv2d(
bottleneck_channels,
bottleneck_channels,
kernel_size=3,
stride=stride_3x3,
padding=1 * dilation,
bias=False,
groups=num_groups,
dilation=dilation,
norm=get_norm(norm, bottleneck_channels),
)
self.conv3 = Conv2d(
bottleneck_channels,
out_channels,
kernel_size=1,
bias=False,
norm=get_norm(norm, out_channels),
)
for layer in [self.conv1, self.conv2, self.conv3, self.shortcut]:
if layer is not None: # shortcut can be None
weight_init.c2_msra_fill(layer)
# Zero-initialize the last normalization in each residual branch,
# so that at the beginning, the residual branch starts with zeros,
# and each residual block behaves like an identity.
# See Sec 5.1 in "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour":
# "For BN layers, the learnable scaling coefficient γ is initialized
# to be 1, except for each residual block's last BN
# where γ is initialized to be 0."
# nn.init.constant_(self.conv3.norm.weight, 0)
# TODO this somehow hurts performance when training GN models from scratch.
# Add it as an option when we need to use this code to train a backbone.
def forward(self, x):
out = self.conv1(x)
out = F.relu_(out)
out = self.conv2(out)
out = F.relu_(out)
out = self.conv3(out)
if self.shortcut is not None:
shortcut = self.shortcut(x)
else:
shortcut = x
out += shortcut
out = F.relu_(out)
return out
def make_stage(block_class, num_blocks, first_stride, **kwargs):
"""
Create a resnet stage by creating many blocks.
Args:
block_class (class): a subclass of ResNetBlockBase
num_blocks (int):
first_stride (int): the stride of the first block. The other blocks will have stride=1.
A `stride` argument will be passed to the block constructor.
kwargs: other arguments passed to the block constructor.
Returns:
list[nn.Module]: a list of block module.
"""
blocks = []
for i in range(num_blocks):
blocks.append(block_class(stride=first_stride if i == 0 else 1, **kwargs))
kwargs["in_channels"] = kwargs["out_channels"]
return blocks
class ResNet(Backbone):
def __init__(self, stem, stages, num_classes=None, out_features=None):
"""
Args:
stem (nn.Module): a stem module
stages (list[list[ResNetBlock]]): several (typically 4) stages,
each contains multiple :class:`ResNetBlockBase`.
num_classes (None or int): if None, will not perform classification.
out_features (list[str]): name of the layers whose outputs should
be returned in forward. Can be anything in "stem", "linear", or "res2" ...
If None, will return the output of the last layer.
"""
super(ResNet, self).__init__()
self.stem = stem
self.num_classes = num_classes
current_stride = self.stem.stride
self._out_feature_strides = {"stem": current_stride}
self._out_feature_channels = {"stem": self.stem.out_channels}
self.stages = []
self.names = []
for i, blocks in enumerate(stages):
for block in blocks:
assert isinstance(block, ResNetBlockBase), block
curr_channels = block.out_channels
stage = nn.Sequential(*blocks)
name = "res" + str(i + 2)
self.stages.append(stage)
self.names.append(name)
self._out_feature_strides[name] = current_stride = int(
current_stride * np.prod([k.stride for k in blocks])
)
self._out_feature_channels[name] = blocks[-1].out_channels
self.stages = nn.ModuleList(self.stages)
if num_classes is not None:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.linear = nn.Linear(curr_channels, num_classes)
# Sec 5.1 in "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour":
# "The 1000-way fully-connected layer is initialized by
# drawing weights from a zero-mean Gaussian with standard deviation of 0.01."
nn.init.normal_(self.linear.weight, stddev=0.01)
name = "linear"
if out_features is None:
out_features = [name]
self._out_features = out_features
assert len(self._out_features)
for out_feature in self._out_features:
assert out_feature in self.names, "Available children: {}".format(", ".join(self.names))
def forward(self, x):
outputs = {}
x = self.stem(x)
if "stem" in self._out_features:
outputs["stem"] = x
for ix, stage in enumerate(self.stages):
name = self.names[ix]
x = stage(x)
if name in self._out_features:
outputs[name] = x
if self.num_classes is not None:
x = self.avgpool(x)
x = self.linear(x)
if "linear" in self._out_features:
outputs["linear"] = x
return outputs
def build_resnet_backbone(in_channels=3):
norm = "BN"
stem = BasicStem(
in_channels=in_channels,
out_channels=64,
norm=norm,
)
# fmt: off
out_features = ["res2", "res3", "res4"]
depth = 101
num_groups = 1
bottleneck_channels = 64
in_channels = 64
out_channels = 256
stride_in_1x1 = True
res5_dilation = 1
# fmt: on
assert res5_dilation in {1, 2}, "res5_dilation cannot be {}.".format(res5_dilation)
num_blocks_per_stage = {50: [3, 4, 6, 3], 101: [3, 4, 23, 3], 152: [3, 8, 36, 3]}[depth]
stages = []
# Avoid creating variables without gradients
# It consumes extra memory and may cause allreduce to fail
out_stage_idx = [{"res2": 2, "res3": 3, "res4": 4, "res5": 5}[f] for f in out_features]
max_stage_idx = max(out_stage_idx)
for idx, stage_idx in enumerate(range(2, max_stage_idx + 1)):
dilation = res5_dilation if stage_idx == 5 else 1
first_stride = 1 if idx == 0 or (stage_idx == 5 and dilation == 2) else 2
stage_kargs = dict()
stage_kargs.update({
"num_blocks": num_blocks_per_stage[idx],
"first_stride": first_stride,
"in_channels": in_channels,
"bottleneck_channels": bottleneck_channels,
"out_channels": out_channels,
"num_groups": num_groups,
"norm": norm,
"stride_in_1x1": stride_in_1x1,
"dilation": dilation,
})
stage_kargs["block_class"] = BottleneckBlock
blocks = make_stage(**stage_kargs)
in_channels = out_channels
out_channels *= 2
bottleneck_channels *= 2
stages.append(blocks)
return ResNet(stem, stages, out_features=out_features)
@@ -0,0 +1,244 @@
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.model_zoo as model_zoo
from core.bodyseg.backbone.backbone import Backbone
def fixed_padding(inputs, kernel_size, dilation):
kernel_size_effective = kernel_size + (kernel_size - 1) * (dilation - 1)
pad_total = kernel_size_effective - 1
pad_beg = pad_total // 2
pad_end = pad_total - pad_beg
padded_inputs = F.pad(inputs, (pad_beg, pad_end, pad_beg, pad_end))
return padded_inputs
class SeparableConv2d(nn.Module):
def __init__(self, inplanes, planes, kernel_size=3, stride=1, dilation=1, bias=False, BatchNorm=None):
super(SeparableConv2d, self).__init__()
self.conv1 = nn.Conv2d(inplanes, inplanes, kernel_size, stride, 0, dilation,
groups=inplanes, bias=bias)
self.bn = BatchNorm(inplanes)
self.pointwise = nn.Conv2d(inplanes, planes, 1, 1, 0, 1, 1, bias=bias)
def forward(self, x):
x = fixed_padding(x, self.conv1.kernel_size[0], dilation=self.conv1.dilation[0])
x = self.conv1(x)
x = self.bn(x)
x = self.pointwise(x)
return x
class Block(nn.Module):
def __init__(self, inplanes, planes, reps, stride=1, dilation=1, BatchNorm=None,
start_with_relu=True, grow_first=True, is_last=False):
super(Block, self).__init__()
if planes != inplanes or stride != 1:
self.skip = nn.Conv2d(inplanes, planes, 1, stride=stride, bias=False)
self.skipbn = BatchNorm(planes)
else:
self.skip = None
self.relu = nn.ReLU(inplace=True)
rep = []
filters = inplanes
if grow_first:
rep.append(self.relu)
rep.append(SeparableConv2d(inplanes, planes, 3, 1, dilation, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
filters = planes
for i in range(reps - 1):
rep.append(self.relu)
rep.append(SeparableConv2d(filters, filters, 3, 1, dilation, BatchNorm=BatchNorm))
rep.append(BatchNorm(filters))
if not grow_first:
rep.append(self.relu)
rep.append(SeparableConv2d(inplanes, planes, 3, 1, dilation, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
if stride != 1:
rep.append(self.relu)
rep.append(SeparableConv2d(planes, planes, 3, 2, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
if stride == 1 and is_last:
rep.append(self.relu)
rep.append(SeparableConv2d(planes, planes, 3, 1, BatchNorm=BatchNorm))
rep.append(BatchNorm(planes))
if not start_with_relu:
rep = rep[1:]
self.rep = nn.Sequential(*rep)
def forward(self, inp):
x = self.rep(inp)
if self.skip is not None:
skip = self.skip(inp)
skip = self.skipbn(skip)
else:
skip = inp
x = x + skip
return x
class AlignedXception(Backbone):
"""
Modified Alighed Xception
"""
def __init__(self, output_stride, BatchNorm):
super(AlignedXception, self).__init__()
if output_stride == 16:
entry_block3_stride = 2
middle_block_dilation = 1
exit_block_dilations = (1, 2)
elif output_stride == 8:
entry_block3_stride = 1
middle_block_dilation = 2
exit_block_dilations = (2, 4)
else:
raise NotImplementedError
# Entry flow
self.conv1 = nn.Conv2d(3, 32, 3, stride=2, padding=1, bias=False)
self.bn1 = BatchNorm(32)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(32, 64, 3, stride=1, padding=1, bias=False)
self.bn2 = BatchNorm(64)
self.block1 = Block(64, 128, reps=2, stride=2, BatchNorm=BatchNorm, start_with_relu=False)
self.block2 = Block(128, 256, reps=2, stride=2, BatchNorm=BatchNorm, start_with_relu=False,
grow_first=True)
self.block3 = Block(256, 728, reps=2, stride=entry_block3_stride, BatchNorm=BatchNorm,
start_with_relu=True, grow_first=True, is_last=True)
# Middle flow
self.block4 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block5 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block6 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block7 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block8 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block9 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block10 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block11 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block12 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block13 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block14 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block15 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block16 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block17 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block18 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
self.block19 = Block(728, 728, reps=3, stride=1, dilation=middle_block_dilation,
BatchNorm=BatchNorm, start_with_relu=True, grow_first=True)
# Exit flow
self.block20 = Block(728, 1024, reps=2, stride=1, dilation=exit_block_dilations[0],
BatchNorm=BatchNorm, start_with_relu=True, grow_first=False, is_last=True)
self.conv3 = SeparableConv2d(1024, 1536, 3, stride=1, dilation=exit_block_dilations[1], BatchNorm=BatchNorm)
self.bn3 = BatchNorm(1536)
self.conv4 = SeparableConv2d(1536, 1536, 3, stride=1, dilation=exit_block_dilations[1], BatchNorm=BatchNorm)
self.bn4 = BatchNorm(1536)
self.conv5 = SeparableConv2d(1536, 2048, 3, stride=1, dilation=exit_block_dilations[1], BatchNorm=BatchNorm)
self.bn5 = BatchNorm(2048)
# Init weights
self._init_weight()
def forward(self, x):
# Entry flow
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.conv2(x)
x = self.bn2(x)
x = self.relu(x)
x = self.block1(x)
# add relu here
x = self.relu(x)
low_level_feat = x
x = self.block2(x)
x = self.block3(x)
# Middle flow
x = self.block4(x)
x = self.block5(x)
x = self.block6(x)
x = self.block7(x)
x = self.block8(x)
x = self.block9(x)
x = self.block10(x)
x = self.block11(x)
x = self.block12(x)
x = self.block13(x)
x = self.block14(x)
x = self.block15(x)
x = self.block16(x)
x = self.block17(x)
x = self.block18(x)
x = self.block19(x)
# Exit flow
x = self.block20(x)
x = self.relu(x)
x = self.conv3(x)
x = self.bn3(x)
x = self.relu(x)
x = self.conv4(x)
x = self.bn4(x)
x = self.relu(x)
x = self.conv5(x)
x = self.bn5(x)
x = self.relu(x)
return x, low_level_feat
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.SyncBatchNorm):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
if __name__ == "__main__":
import torch
model = AlignedXception(BatchNorm=nn.BatchNorm2d, output_stride=16)
input = torch.rand(1, 3, 512, 512)
output, low_level_feat = model(input)
print(output.size())
print(low_level_feat.size())
@@ -0,0 +1,330 @@
import sys
# sys.path.append("/Users/momo/human_seg_train")
# print(sys.path)
import torch
import torch.nn as nn
import torch.nn.functional as F
from core.bodyseg.backbone.backbone import get_norm
class ConvBNReLU(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1, norm_layer=None):
padding = (kernel_size - 1) // 2
if norm_layer is None:
norm_layer = nn.BatchNorm2d
super(ConvBNReLU, self).__init__(
nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False),
norm_layer(out_planes),
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio, norm_layer=None):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
if norm_layer is None:
norm_layer = nn.BatchNorm2d
hidden_dim = int(round(inp * expand_ratio))
self.use_res_connect = self.stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# pw
layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1, norm_layer=norm_layer))
layers.extend([
# dw
ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim, norm_layer=norm_layer),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
norm_layer(oup),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class UpSampleBlock(nn.Module):
def __init__(self, in_channels, out_channels, expand_ratio=6):
super(UpSampleBlock, self).__init__()
self.refine = InvertedResidual(in_channels, out_channels, 1, expand_ratio)
def forward(self, x0, x1):
x = torch.cat([x0, x1], dim=1)
x = self.refine(x)
return x
class BodySegNet_32_thin_sigmod(nn.Module):
def __init__(self, input_channels=3, class_nums=1, output_onnx=False):
super(BodySegNet_32_thin_sigmod, self).__init__()
self.class_nums = class_nums
self.output_onnx = output_onnx
self.stage_1 = nn.Sequential(
ConvBNReLU(input_channels, 16, kernel_size=3, stride=2)
)
self.stage_2 = nn.Sequential(
ConvBNReLU(16, 16, kernel_size=3, stride=2, groups=16),
ConvBNReLU(16, 16, kernel_size=1, stride=1),
)
self.stage_3 = nn.Sequential(
InvertedResidual(16, 24, stride=2, expand_ratio=6),
InvertedResidual(24, 24, stride=1, expand_ratio=6),
InvertedResidual(24, 24, stride=1, expand_ratio=6),
)
self.stage_4 = nn.Sequential(
InvertedResidual(24, 32, stride=2, expand_ratio=6),
InvertedResidual(32, 32, stride=1, expand_ratio=6),
InvertedResidual(32, 32, stride=1, expand_ratio=6),
InvertedResidual(32, 32, stride=1, expand_ratio=6),
)
self.stage_5 = nn.Sequential(
InvertedResidual(32, 48, stride=2, expand_ratio=6),
InvertedResidual(48, 48, stride=1, expand_ratio=6),
InvertedResidual(48, 48, stride=1, expand_ratio=6),
InvertedResidual(48, 48, stride=1, expand_ratio=6)
)
self.up_to_4 = UpSampleBlock(48 + 32, 16)
self.up_to_3 = UpSampleBlock(16 + 24, 16)
self.up_to_2 = UpSampleBlock(16 + 16, 16)
self.up_to_1 = UpSampleBlock(16 + 16, 16)
self.last_layer = nn.Sequential(
ConvBNReLU(16, 16, kernel_size=1, stride=1),
nn.Conv2d(16, self.class_nums, kernel_size=1, stride=1)
)
self._initialize_weights()
def forward(self, x):
feature_S = []
x1 = self.stage_1(x)
x2 = self.stage_2(x1)
x3 = self.stage_3(x2)
x4 = self.stage_4(x3)
feature = self.stage_5(x4)
feature = F.interpolate(feature, size=x4.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_4(x4, feature)
feature = F.interpolate(feature, size=x3.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_3(x3, feature)
feature = F.interpolate(feature, size=x2.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_2(x2, feature)
feature = F.interpolate(feature, size=x1.size()[2:], mode='bilinear', align_corners=True)
feature = self.up_to_1(x1, feature)
feature_S.append(feature)
feature = self.last_layer(feature)
feature_S.append(feature)
output = F.interpolate(feature, size=x.size()[2:], mode='bilinear', align_corners=True)
# output = torch.sigmoid(output)
if self.output_onnx:
output = torch.argmax(output, dim=1).to(torch.float32)
return output, feature_S
def _initialize_weights(self):
for name, m in self.named_modules():
if isinstance(m, nn.Conv2d):
if 'first' in name:
nn.init.normal_(m.weight, 0, 0.01)
else:
nn.init.normal_(m.weight, 0, 1.0 / m.weight.shape[1])
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0.0001)
nn.init.constant_(m.running_mean, 0)
elif isinstance(m, nn.BatchNorm1d):
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0.0001)
nn.init.constant_(m.running_mean, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
class _ASPPModule(nn.Module):
def __init__(self, inplanes, planes, kernel_size, padding, dilation, BatchNorm):
super(_ASPPModule, self).__init__()
self.atrous_conv = nn.Conv2d(inplanes, planes, kernel_size=kernel_size,
stride=1, padding=padding, dilation=dilation, bias=False)
self.bn = BatchNorm(planes)
self.relu = nn.ReLU()
self._init_weight()
def forward(self, x):
x = self.atrous_conv(x)
x = self.bn(x)
return self.relu(x)
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight)
class ASPP(nn.Module):
def __init__(self, backbone, output_stride, BatchNorm):
super(ASPP, self).__init__()
if backbone == 'drn':
inplanes = 512
elif backbone == 'mobilenet':
inplanes = 320
elif backbone == 'resnet_fpn':
inplanes = 256
else:
inplanes = 2048
if output_stride == 16:
dilations = [1, 6, 12, 18]
elif output_stride == 8:
dilations = [1, 12, 24, 36]
else:
raise NotImplementedError
self.aspp1 = _ASPPModule(inplanes, 256, 1, padding=0, dilation=dilations[0], BatchNorm=BatchNorm)
self.aspp2 = _ASPPModule(inplanes, 256, 3, padding=dilations[1], dilation=dilations[1], BatchNorm=BatchNorm)
self.aspp3 = _ASPPModule(inplanes, 256, 3, padding=dilations[2], dilation=dilations[2], BatchNorm=BatchNorm)
self.aspp4 = _ASPPModule(inplanes, 256, 3, padding=dilations[3], dilation=dilations[3], BatchNorm=BatchNorm)
self.global_avg_pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)),
nn.Conv2d(inplanes, 256, 1, stride=1, bias=False),
BatchNorm(256),
nn.ReLU())
self.conv1 = nn.Conv2d(1280, 256, 1, bias=False)
self.bn1 = BatchNorm(256)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
self._init_weight()
def forward(self, x):
x1 = self.aspp1(x)
x2 = self.aspp2(x)
x3 = self.aspp3(x)
x4 = self.aspp4(x)
x5 = self.global_avg_pool(x)
x5 = F.interpolate(x5, size=x4.size()[2:], mode='bilinear', align_corners=True)
# x5 = F.interpolate(x5, size=x4.size()[2:], mode='nearest')
x = torch.cat((x1, x2, x3, x4, x5), dim=1)
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
return self.dropout(x)
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight)
class Decoder(nn.Module):
def __init__(self,num_classes, backbone, BatchNorm):
super(Decoder, self).__init__()
if backbone == 'resnet_fpn' or backbone == 'drn':
low_level_inplanes = 256
elif backbone == 'xception':
low_level_inplanes = 128
elif backbone == 'mobilenet':
low_level_inplanes = 24
else:
raise NotImplementedError
self.conv1 = nn.Conv2d(low_level_inplanes, 16, 1, bias=False)
self.bn1 = BatchNorm(16)
self.relu = nn.ReLU()
self.last_conv = nn.Sequential(nn.Conv2d(304, 256, kernel_size=3, stride=1, padding=1, bias=False),
BatchNorm(256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1, bias=False),
BatchNorm(256),
nn.ReLU(),
nn.Dropout(0.1),
nn.Conv2d(256, num_classes, kernel_size=1, stride=1))
self.up_to_1 = UpSampleBlock(16 + 16, 16)
self.last_layer = nn.Sequential(
ConvBNReLU(16, 16, kernel_size=1, stride=1),
nn.Conv2d(16, 1, kernel_size=1, stride=1)
)
self._init_weight()
# x(1,256,8,6) low(1,256,32,24) -》 x(1,1,32,24)
def forward(self, x, low_level_feat):
feature_T = []
# deeplab part
#(1,256,32,24) -> (1,16,64,48)
low_level_feat = self.conv1(low_level_feat)
low_level_feat = self.bn1(low_level_feat)
low_level_feat = self.relu(low_level_feat)
# x(1, 256, 8, 6)-> (1,16,32,24)
x = F.interpolate(x, size=low_level_feat.size()[2:], mode='bilinear', align_corners=True)
low_level_feat = F.interpolate(low_level_feat,
size=[low_level_feat.size()[2] * 2, low_level_feat.size()[3] * 2],
mode='bilinear', align_corners=True)
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
# bodyseg part
feature = F.interpolate(x, size=[x.size()[2] * 2,x.size()[3] * 2], mode='bilinear', align_corners=True)
# 输入up_to_1 x(low)(1,16,64,48),上采样2倍后的feature(1,16,64,48)
feature = self.up_to_1(low_level_feat, feature)
feature_T.append(feature)
# (1,1,64,48)
feature = self.last_layer(feature)
feature_T.append(feature)
# (1,1,128,96)
output = F.interpolate(feature, size=[128,96], mode='bilinear', align_corners=True)
# output = torch.sigmoid(output)
return output, feature_T
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight)
def build_backbone(backbone, output_stride, BatchNorm, input_channel=3):
from core.bodyseg.backbone.fpn import build_resnet_fpn_backbone
from core.bodyseg.backbone.xception import AlignedXception
return build_resnet_fpn_backbone(input_channel)
def build_aspp(backbone, output_stride, BatchNorm):
return ASPP(backbone, output_stride, BatchNorm)
def build_decoder(num_classes, backbone, BatchNorm):
return Decoder(num_classes, backbone, BatchNorm)
class DeepLab(nn.Module):
def __init__(self, input_channel=3, class_num=1):
super(DeepLab, self).__init__()
BatchNorm = get_norm("BN")
self.backbone = build_backbone("resnet_fpn", 16, BatchNorm, input_channel=input_channel)
self.aspp = build_aspp("resnet_fpn", 16, BatchNorm)
self.decoder = build_decoder(class_num, "resnet_fpn", BatchNorm)
def forward(self, input):
# input(1,3,128,96)
#output: "p2"(1,256,32,24), "p3"(1,256,16,12), "p4"(1,256,8,6)
output = self.backbone(input)
# "p4"(1,256,8,6) "p2"(1,256,32,24)
x, low_level_feat = output['p4'], output['p2']
# x(1,256,8,6)-》(1,256,8,6)
x = self.aspp(x)
# x(1,256,8,6) low(1,256,32,24) -》 x(1,1,32,24)
x, feature_T = self.decoder(x, low_level_feat)
# x(1,1,128,96)
x = F.interpolate(x, size=input.size()[2:], mode='bilinear', align_corners=True)
# x = F.interpolate(x, size=input.size()[2:], mode='nearest')
return x, feature_T
@@ -0,0 +1,59 @@
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
import sys
import os
import os.path as osp
import time
import logging
from common.logger import config as confccc
class COS_object():
def __init__(self):
# 正常情况日志级别使用 INFO,需要定位时可以修改为 DEBUG,此时 SDK 会打印和服务端的通信信息
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
# 1. 设置用户属性, 包括 secret_id, secret_key, region等。从环境变量读取,避免硬编码密钥。
secret_id = os.getenv('COS_SECRET_ID', '<your-cos-secret-id>')
secret_key = os.getenv('COS_SECRET_KEY', '<your-cos-secret-key>')
self.region = os.getenv('COS_REGION', '<your-cos-region>') # 例: ap-beijing
token = None # 如果使用永久密钥不需要填入 token,如果使用临时密钥需要填入,临时密钥生成和使用指引参见 https://cloud.tencent.com/document/product/436/14048
self.scheme = 'https' # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
self.BucketName= os.getenv('COS_BUCKET', '<your-cos-bucket>')
for param in (secret_id, secret_key, self.region, self.BucketName):
assert '<' not in param, '请设置环境变量 COS_SECRET_ID / COS_SECRET_KEY / COS_REGION / COS_BUCKET'
config = CosConfig(Region=self.region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=self.scheme)
self.client = CosS3Client(config)
def upload_file(self, file, target_name):
t0 = time.time()
with open(file, 'rb') as fp:
response = self.client.put_object(
Bucket=self.BucketName, # Bucket 由 BucketName-APPID 组成
Body=fp,
Key=target_name,
StorageClass='STANDARD',
ContentType='image/jpeg;image/jpg;image/png;image/gif'
)
ret_url = self.scheme + '://' + self.BucketName + '.cos.' + self.region + '.myqcloud.com/' + target_name
print('time costs: {}'.format(time.time() - t0), ret_url)
return ret_url
def download_img(self, img_url, tmp_dir):
# user_img_save_dir = confccc.get('default', 'userDir')
# user_img_tmp_dir = confccc.get('default', 'tmp_dir')
fileName = img_url.split('.com/')[-1]
print(fileName)
response = self.client.get_object(
Bucket=self.BucketName,
Key=fileName,
)
response['Body'].get_stream_to_file(tmp_dir)
if __name__=='__main__':
t0 = time.time()
cos = COS_object()
cos.upload_file('/home/chinatszrn/Pictures/vaffflue12.png', 'hair_mz/images/vaffflue12.png')
# cos.download_img('https://ydapp-1317132355.cos.ap-beijing.myqcloud.com/hair_mz/images/hairstyle/fb7d7a58-3228-40f2-84a6-337088ee31e2/2023031623425988.jpg', 'a0')
print(time.time() - t0)
@@ -0,0 +1,2 @@
from . import mesh
from . import morphable_model
@@ -0,0 +1,7 @@
# from .cython import mesh_core_cython
from . import io
from . import vis
from . import transform
from . import light
from . import render
@@ -0,0 +1 @@
*.cpython-36m-x86_64-linux-gnu.so
@@ -0,0 +1,375 @@
/*
functions that can not be optimazed by vertorization in python.
1. rasterization.(need process each triangle)
2. normal of each vertex.(use one-ring, need process each vertex)
3. write obj(seems that it can be verctorized? anyway, writing it in c++ is simple, so also add function here. --> however, why writting in c++ is still slow?)
Author: Yao Feng
Mail: yaofeng1995@gmail.com
*/
#include "mesh_core.h"
/* Judge whether the point is in the triangle
Method:
http://blackpawn.com/texts/pointinpoly/
Args:
point: [x, y]
tri_points: three vertices(2d points) of a triangle. 2 coords x 3 vertices
Returns:
bool: true for in triangle
*/
bool isPointInTri(point p, point p0, point p1, point p2)
{
// vectors
point v0, v1, v2;
v0 = p2 - p0;
v1 = p1 - p0;
v2 = p - p0;
// dot products
float dot00 = v0.dot(v0); //v0.x * v0.x + v0.y * v0.y //np.dot(v0.T, v0)
float dot01 = v0.dot(v1); //v0.x * v1.x + v0.y * v1.y //np.dot(v0.T, v1)
float dot02 = v0.dot(v2); //v0.x * v2.x + v0.y * v2.y //np.dot(v0.T, v2)
float dot11 = v1.dot(v1); //v1.x * v1.x + v1.y * v1.y //np.dot(v1.T, v1)
float dot12 = v1.dot(v2); //v1.x * v2.x + v1.y * v2.y//np.dot(v1.T, v2)
// barycentric coordinates
float inverDeno;
if(dot00*dot11 - dot01*dot01 == 0)
inverDeno = 0;
else
inverDeno = 1/(dot00*dot11 - dot01*dot01);
float u = (dot11*dot02 - dot01*dot12)*inverDeno;
float v = (dot00*dot12 - dot01*dot02)*inverDeno;
// check if point in triangle
return (u >= 0) && (v >= 0) && (u + v < 1);
}
void get_point_weight(float* weight, point p, point p0, point p1, point p2)
{
// vectors
point v0, v1, v2;
v0 = p2 - p0;
v1 = p1 - p0;
v2 = p - p0;
// dot products
float dot00 = v0.dot(v0); //v0.x * v0.x + v0.y * v0.y //np.dot(v0.T, v0)
float dot01 = v0.dot(v1); //v0.x * v1.x + v0.y * v1.y //np.dot(v0.T, v1)
float dot02 = v0.dot(v2); //v0.x * v2.x + v0.y * v2.y //np.dot(v0.T, v2)
float dot11 = v1.dot(v1); //v1.x * v1.x + v1.y * v1.y //np.dot(v1.T, v1)
float dot12 = v1.dot(v2); //v1.x * v2.x + v1.y * v2.y//np.dot(v1.T, v2)
// barycentric coordinates
float inverDeno;
if(dot00*dot11 - dot01*dot01 == 0)
inverDeno = 0;
else
inverDeno = 1/(dot00*dot11 - dot01*dot01);
float u = (dot11*dot02 - dot01*dot12)*inverDeno;
float v = (dot00*dot12 - dot01*dot02)*inverDeno;
// weight
weight[0] = 1 - u - v;
weight[1] = v;
weight[2] = u;
}
void _get_normal_core(
float* normal, float* tri_normal, int* triangles,
int ntri)
{
int i, j;
int tri_p0_ind, tri_p1_ind, tri_p2_ind;
for(i = 0; i < ntri; i++)
{
tri_p0_ind = triangles[3*i];
tri_p1_ind = triangles[3*i + 1];
tri_p2_ind = triangles[3*i + 2];
for(j = 0; j < 3; j++)
{
normal[3*tri_p0_ind + j] = normal[3*tri_p0_ind + j] + tri_normal[3*i + j];
normal[3*tri_p1_ind + j] = normal[3*tri_p1_ind + j] + tri_normal[3*i + j];
normal[3*tri_p2_ind + j] = normal[3*tri_p2_ind + j] + tri_normal[3*i + j];
}
}
}
void _rasterize_triangles_core(
float* vertices, int* triangles,
float* depth_buffer, int* triangle_buffer, float* barycentric_weight,
int nver, int ntri,
int h, int w)
{
int i;
int x, y, k;
int tri_p0_ind, tri_p1_ind, tri_p2_ind;
point p0, p1, p2, p;
int x_min, x_max, y_min, y_max;
float p_depth, p0_depth, p1_depth, p2_depth;
float weight[3];
for(i = 0; i < ntri; i++)
{
tri_p0_ind = triangles[3*i];
tri_p1_ind = triangles[3*i + 1];
tri_p2_ind = triangles[3*i + 2];
p0.x = vertices[3*tri_p0_ind]; p0.y = vertices[3*tri_p0_ind + 1]; p0_depth = vertices[3*tri_p0_ind + 2];
p1.x = vertices[3*tri_p1_ind]; p1.y = vertices[3*tri_p1_ind + 1]; p1_depth = vertices[3*tri_p1_ind + 2];
p2.x = vertices[3*tri_p2_ind]; p2.y = vertices[3*tri_p2_ind + 1]; p2_depth = vertices[3*tri_p2_ind + 2];
x_min = max((int)ceil(min(p0.x, min(p1.x, p2.x))), 0);
x_max = min((int)floor(max(p0.x, max(p1.x, p2.x))), w - 1);
y_min = max((int)ceil(min(p0.y, min(p1.y, p2.y))), 0);
y_max = min((int)floor(max(p0.y, max(p1.y, p2.y))), h - 1);
if(x_max < x_min || y_max < y_min)
{
continue;
}
for(y = y_min; y <= y_max; y++) //h
{
for(x = x_min; x <= x_max; x++) //w
{
p.x = x; p.y = y;
if(p.x < 2 || p.x > w - 3 || p.y < 2 || p.y > h - 3 || isPointInTri(p, p0, p1, p2))
{
get_point_weight(weight, p, p0, p1, p2);
p_depth = weight[0]*p0_depth + weight[1]*p1_depth + weight[2]*p2_depth;
if((p_depth > depth_buffer[y*w + x]))
{
depth_buffer[y*w + x] = p_depth;
triangle_buffer[y*w + x] = i;
for(k = 0; k < 3; k++)
{
barycentric_weight[y*w*3 + x*3 + k] = weight[k];
}
}
}
}
}
}
}
void _render_colors_core(
float* image, float* vertices, int* triangles,
float* colors,
float* depth_buffer,
int nver, int ntri,
int h, int w, int c)
{
int i;
int x, y, k;
int tri_p0_ind, tri_p1_ind, tri_p2_ind;
point p0, p1, p2, p;
int x_min, x_max, y_min, y_max;
float p_depth, p0_depth, p1_depth, p2_depth;
float p_color, p0_color, p1_color, p2_color;
float weight[3];
for(i = 0; i < ntri; i++)
{
tri_p0_ind = triangles[3*i];
tri_p1_ind = triangles[3*i + 1];
tri_p2_ind = triangles[3*i + 2];
p0.x = vertices[3*tri_p0_ind]; p0.y = vertices[3*tri_p0_ind + 1]; p0_depth = vertices[3*tri_p0_ind + 2];
p1.x = vertices[3*tri_p1_ind]; p1.y = vertices[3*tri_p1_ind + 1]; p1_depth = vertices[3*tri_p1_ind + 2];
p2.x = vertices[3*tri_p2_ind]; p2.y = vertices[3*tri_p2_ind + 1]; p2_depth = vertices[3*tri_p2_ind + 2];
x_min = max((int)ceil(min(p0.x, min(p1.x, p2.x))), 0);
x_max = min((int)floor(max(p0.x, max(p1.x, p2.x))), w - 1);
y_min = max((int)ceil(min(p0.y, min(p1.y, p2.y))), 0);
y_max = min((int)floor(max(p0.y, max(p1.y, p2.y))), h - 1);
if(x_max < x_min || y_max < y_min)
{
continue;
}
for(y = y_min; y <= y_max; y++) //h
{
for(x = x_min; x <= x_max; x++) //w
{
p.x = x; p.y = y;
if(p.x < 2 || p.x > w - 3 || p.y < 2 || p.y > h - 3 || isPointInTri(p, p0, p1, p2))
{
get_point_weight(weight, p, p0, p1, p2);
p_depth = weight[0]*p0_depth + weight[1]*p1_depth + weight[2]*p2_depth;
if((p_depth > depth_buffer[y*w + x]))
{
for(k = 0; k < c; k++) // c
{
p0_color = colors[c*tri_p0_ind + k];
p1_color = colors[c*tri_p1_ind + k];
p2_color = colors[c*tri_p2_ind + k];
p_color = weight[0]*p0_color + weight[1]*p1_color + weight[2]*p2_color;
image[y*w*c + x*c + k] = p_color;
}
depth_buffer[y*w + x] = p_depth;
}
}
}
}
}
}
void _render_texture_core(
float* image, float* vertices, int* triangles,
float* texture, float* tex_coords, int* tex_triangles,
float* depth_buffer,
int nver, int tex_nver, int ntri,
int h, int w, int c,
int tex_h, int tex_w, int tex_c,
int mapping_type)
{
int i;
int x, y, k;
int tri_p0_ind, tri_p1_ind, tri_p2_ind;
int tex_tri_p0_ind, tex_tri_p1_ind, tex_tri_p2_ind;
point p0, p1, p2, p;
point tex_p0, tex_p1, tex_p2, tex_p;
int x_min, x_max, y_min, y_max;
float weight[3];
float p_depth, p0_depth, p1_depth, p2_depth;
float xd, yd;
float ul, ur, dl, dr;
for(i = 0; i < ntri; i++)
{
// mesh
tri_p0_ind = triangles[3*i];
tri_p1_ind = triangles[3*i + 1];
tri_p2_ind = triangles[3*i + 2];
p0.x = vertices[3*tri_p0_ind]; p0.y = vertices[3*tri_p0_ind + 1]; p0_depth = vertices[3*tri_p0_ind + 2];
p1.x = vertices[3*tri_p1_ind]; p1.y = vertices[3*tri_p1_ind + 1]; p1_depth = vertices[3*tri_p1_ind + 2];
p2.x = vertices[3*tri_p2_ind]; p2.y = vertices[3*tri_p2_ind + 1]; p2_depth = vertices[3*tri_p2_ind + 2];
// texture
tex_tri_p0_ind = tex_triangles[3*i];
tex_tri_p1_ind = tex_triangles[3*i + 1];
tex_tri_p2_ind = tex_triangles[3*i + 2];
tex_p0.x = tex_coords[3*tex_tri_p0_ind]; tex_p0.y = tex_coords[3*tri_p0_ind + 1];
tex_p1.x = tex_coords[3*tex_tri_p1_ind]; tex_p1.y = tex_coords[3*tri_p1_ind + 1];
tex_p2.x = tex_coords[3*tex_tri_p2_ind]; tex_p2.y = tex_coords[3*tri_p2_ind + 1];
x_min = max((int)ceil(min(p0.x, min(p1.x, p2.x))), 0);
x_max = min((int)floor(max(p0.x, max(p1.x, p2.x))), w - 1);
y_min = max((int)ceil(min(p0.y, min(p1.y, p2.y))), 0);
y_max = min((int)floor(max(p0.y, max(p1.y, p2.y))), h - 1);
if(x_max < x_min || y_max < y_min)
{
continue;
}
for(y = y_min; y <= y_max; y++) //h
{
for(x = x_min; x <= x_max; x++) //w
{
p.x = x; p.y = y;
if(p.x < 2 || p.x > w - 3 || p.y < 2 || p.y > h - 3 || isPointInTri(p, p0, p1, p2))
{
get_point_weight(weight, p, p0, p1, p2);
p_depth = weight[0]*p0_depth + weight[1]*p1_depth + weight[2]*p2_depth;
if((p_depth > depth_buffer[y*w + x]))
{
// -- color from texture
// cal weight in mesh tri
get_point_weight(weight, p, p0, p1, p2);
// cal coord in texture
tex_p = tex_p0*weight[0] + tex_p1*weight[1] + tex_p2*weight[2];
tex_p.x = max(min(tex_p.x, float(tex_w - 1)), float(0));
tex_p.y = max(min(tex_p.y, float(tex_h - 1)), float(0));
yd = tex_p.y - floor(tex_p.y);
xd = tex_p.x - floor(tex_p.x);
for(k = 0; k < c; k++)
{
if(mapping_type==0)// nearest
{
image[y*w*c + x*c + k] = texture[int(round(tex_p.y))*tex_w*tex_c + int(round(tex_p.x))*tex_c + k];
}
else//bilinear interp
{
ul = texture[(int)floor(tex_p.y)*tex_w*tex_c + (int)floor(tex_p.x)*tex_c + k];
ur = texture[(int)floor(tex_p.y)*tex_w*tex_c + (int)ceil(tex_p.x)*tex_c + k];
dl = texture[(int)ceil(tex_p.y)*tex_w*tex_c + (int)floor(tex_p.x)*tex_c + k];
dr = texture[(int)ceil(tex_p.y)*tex_w*tex_c + (int)ceil(tex_p.x)*tex_c + k];
image[y*w*c + x*c + k] = ul*(1-xd)*(1-yd) + ur*xd*(1-yd) + dl*(1-xd)*yd + dr*xd*yd;
}
}
depth_buffer[y*w + x] = p_depth;
}
}
}
}
}
}
// ------------------------------------------------- write
// obj write
// Ref: https://github.com/patrikhuber/eos/blob/master/include/eos/core/Mesh.hpp
void _write_obj_with_colors_texture(string filename, string mtl_name,
float* vertices, int* triangles, float* colors, float* uv_coords,
int nver, int ntri, int ntexver)
{
int i;
ofstream obj_file(filename.c_str());
// first line of the obj file: the mtl name
obj_file << "mtllib " << mtl_name << endl;
// write vertices
for (i = 0; i < nver; ++i)
{
obj_file << "v " << vertices[3*i] << " " << vertices[3*i + 1] << " " << vertices[3*i + 2] << colors[3*i] << " " << colors[3*i + 1] << " " << colors[3*i + 2] << endl;
}
// write uv coordinates
for (i = 0; i < ntexver; ++i)
{
//obj_file << "vt " << uv_coords[2*i] << " " << (1 - uv_coords[2*i + 1]) << endl;
obj_file << "vt " << uv_coords[2*i] << " " << uv_coords[2*i + 1] << endl;
}
obj_file << "usemtl FaceTexture" << endl;
// write triangles
for (i = 0; i < ntri; ++i)
{
// obj_file << "f " << triangles[3*i] << "/" << triangles[3*i] << " " << triangles[3*i + 1] << "/" << triangles[3*i + 1] << " " << triangles[3*i + 2] << "/" << triangles[3*i + 2] << endl;
obj_file << "f " << triangles[3*i + 2] << "/" << triangles[3*i + 2] << " " << triangles[3*i + 1] << "/" << triangles[3*i + 1] << " " << triangles[3*i] << "/" << triangles[3*i] << endl;
}
}
@@ -0,0 +1,83 @@
#ifndef MESH_CORE_HPP_
#define MESH_CORE_HPP_
#include <stdio.h>
#include <cmath>
#include <algorithm>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
class point
{
public:
float x;
float y;
float dot(point p)
{
return this->x * p.x + this->y * p.y;
}
point operator-(const point& p)
{
point np;
np.x = this->x - p.x;
np.y = this->y - p.y;
return np;
}
point operator+(const point& p)
{
point np;
np.x = this->x + p.x;
np.y = this->y + p.y;
return np;
}
point operator*(float s)
{
point np;
np.x = s * this->x;
np.y = s * this->y;
return np;
}
};
bool isPointInTri(point p, point p0, point p1, point p2, int h, int w);
void get_point_weight(float* weight, point p, point p0, point p1, point p2);
void _get_normal_core(
float* normal, float* tri_normal, int* triangles,
int ntri);
void _rasterize_triangles_core(
float* vertices, int* triangles,
float* depth_buffer, int* triangle_buffer, float* barycentric_weight,
int nver, int ntri,
int h, int w);
void _render_colors_core(
float* image, float* vertices, int* triangles,
float* colors,
float* depth_buffer,
int nver, int ntri,
int h, int w, int c);
void _render_texture_core(
float* image, float* vertices, int* triangles,
float* texture, float* tex_coords, int* tex_triangles,
float* depth_buffer,
int nver, int tex_nver, int ntri,
int h, int w, int c,
int tex_h, int tex_w, int tex_c,
int mapping_type);
void _write_obj_with_colors_texture(string filename, string mtl_name,
float* vertices, int* triangles, float* colors, float* uv_coords,
int nver, int ntri, int ntexver);
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
'''
python setup.py build_ext -i
to compile
'''
# setup.py
from distutils.core import setup
from setuptools import Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import numpy
setup(
name = 'mesh_core_cython',
cmdclass={'build_ext': build_ext},
ext_modules=[Extension("mesh_core_cython",
sources=["mesh_core_cython.pyx", "mesh_core.cpp"],
language='c++',
include_dirs=[numpy.get_include()])],
)
@@ -0,0 +1,142 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
import os
# from skimage import io
from time import time
# from .cython import mesh_core_cython
## TODO
## TODO: c++ version
def read_obj(obj_name):
''' read mesh
'''
return 0
# ------------------------- write
def write_asc(path, vertices):
'''
Args:
vertices: shape = (nver, 3)
'''
if path.split('.')[-1] == 'asc':
np.savetxt(path, vertices)
else:
np.savetxt(path + '.asc', vertices)
def write_obj_with_colors(obj_name, vertices, triangles, colors):
''' Save 3D face model with texture represented by colors.
Args:
obj_name: str
vertices: shape = (nver, 3)
triangles: shape = (ntri, 3)
colors: shape = (nver, 3)
'''
triangles = triangles.copy()
triangles += 1 # meshlab start with 1
if obj_name.split('.')[-1] != 'obj':
obj_name = obj_name + '.obj'
# write obj
with open(obj_name, 'w') as f:
# write vertices & colors
for i in range(vertices.shape[0]):
# s = 'v {} {} {} \n'.format(vertices[0,i], vertices[1,i], vertices[2,i])
s = 'v {} {} {} {} {} {}\n'.format(vertices[i, 0], vertices[i, 1], vertices[i, 2], colors[i, 0], colors[i, 1], colors[i, 2])
f.write(s)
# write f: ver ind/ uv ind
[k, ntri] = triangles.shape
for i in range(triangles.shape[0]):
# s = 'f {} {} {}\n'.format(triangles[i, 0], triangles[i, 1], triangles[i, 2])
s = 'f {} {} {}\n'.format(triangles[i, 2], triangles[i, 1], triangles[i, 0])
f.write(s)
## TODO: c++ version
def write_obj_with_texture(obj_name, vertices, triangles, texture, uv_coords):
''' Save 3D face model with texture represented by texture map.
Ref: https://github.com/patrikhuber/eos/blob/bd00155ebae4b1a13b08bf5a991694d682abbada/include/eos/core/Mesh.hpp
Args:
obj_name: str
vertices: shape = (nver, 3)
triangles: shape = (ntri, 3)
texture: shape = (256,256,3)
uv_coords: shape = (nver, 3) max value<=1
'''
if obj_name.split('.')[-1] != 'obj':
obj_name = obj_name + '.obj'
mtl_name = obj_name.replace('.obj', '.mtl')
texture_name = obj_name.replace('.obj', '_texture.png')
triangles = triangles.copy()
triangles += 1 # mesh lab start with 1
# write obj
with open(obj_name, 'w') as f:
# first line: write mtlib(material library)
s = "mtllib {}\n".format(os.path.abspath(mtl_name))
f.write(s)
# write vertices
for i in range(vertices.shape[0]):
s = 'v {} {} {}\n'.format(vertices[i, 0], vertices[i, 1], vertices[i, 2])
f.write(s)
# write uv coords
for i in range(uv_coords.shape[0]):
s = 'vt {} {}\n'.format(uv_coords[i,0], 1 - uv_coords[i,1])
f.write(s)
f.write("usemtl FaceTexture\n")
# write f: ver ind/ uv ind
for i in range(triangles.shape[0]):
s = 'f {}/{} {}/{} {}/{}\n'.format(triangles[i,2], triangles[i,2], triangles[i,1], triangles[i,1], triangles[i,0], triangles[i,0])
f.write(s)
# write mtl
with open(mtl_name, 'w') as f:
f.write("newmtl FaceTexture\n")
s = 'map_Kd {}\n'.format(os.path.abspath(texture_name)) # map to image
f.write(s)
# write texture as png
imsave(texture_name, texture)
# c++ version
def write_obj_with_colors_texture(obj_name, vertices, triangles, colors, texture, uv_coords):
''' Save 3D face model with texture.
Ref: https://github.com/patrikhuber/eos/blob/bd00155ebae4b1a13b08bf5a991694d682abbada/include/eos/core/Mesh.hpp
Args:
obj_name: str
vertices: shape = (nver, 3)
triangles: shape = (ntri, 3)
colors: shape = (nver, 3)
texture: shape = (256,256,3)
uv_coords: shape = (nver, 3) max value<=1
'''
if obj_name.split('.')[-1] != 'obj':
obj_name = obj_name + '.obj'
mtl_name = obj_name.replace('.obj', '.mtl')
texture_name = obj_name.replace('.obj', '_texture.png')
triangles = triangles.copy()
triangles += 1 # mesh lab start with 1
# write obj
vertices, colors, uv_coords = vertices.astype(np.float32).copy(), colors.astype(np.float32).copy(), uv_coords.astype(np.float32).copy()
mesh_core_cython.write_obj_with_colors_texture_core(str.encode(obj_name), str.encode(os.path.abspath(mtl_name)), vertices, triangles, colors, uv_coords, vertices.shape[0], triangles.shape[0], uv_coords.shape[0])
# write mtl
with open(mtl_name, 'w') as f:
f.write("newmtl FaceTexture\n")
s = 'map_Kd {}\n'.format(os.path.abspath(texture_name)) # map to image
f.write(s)
# write texture as png
# io.imsave(texture_name, texture)
@@ -0,0 +1,213 @@
'''
Functions about lighting mesh(changing colors/texture of mesh).
1. add light to colors/texture (shade each vertex)
2. fit light according to colors/texture & image.
'''
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
# from .cython import mesh_core_cython
def get_normal(vertices, triangles):
''' calculate normal direction in each vertex
Args:
vertices: [nver, 3]
triangles: [ntri, 3]
Returns:
normal: [nver, 3]
'''
pt0 = vertices[triangles[:, 0], :] # [ntri, 3]
pt1 = vertices[triangles[:, 1], :] # [ntri, 3]
pt2 = vertices[triangles[:, 2], :] # [ntri, 3]
tri_normal = np.cross(pt0 - pt1, pt0 - pt2) # [ntri, 3]. normal of each triangle
normal = np.zeros_like(vertices, dtype = np.float32).copy() # [nver, 3]
# for i in range(triangles.shape[0]):
# normal[triangles[i, 0], :] = normal[triangles[i, 0], :] + tri_normal[i, :]
# normal[triangles[i, 1], :] = normal[triangles[i, 1], :] + tri_normal[i, :]
# normal[triangles[i, 2], :] = normal[triangles[i, 2], :] + tri_normal[i, :]
mesh_core_cython.get_normal_core(normal, tri_normal.astype(np.float32).copy(), triangles.copy(), triangles.shape[0])
# normalize to unit length
mag = np.sum(normal**2, 1) # [nver]
zero_ind = (mag == 0)
mag[zero_ind] = 1
normal[zero_ind, 0] = np.ones((np.sum(zero_ind)))
normal = normal/np.sqrt(mag[:,np.newaxis])
return normal
# TODO: test
def add_light_sh(vertices, triangles, colors, sh_coeff):
'''
In 3d face, usually assume:
1. The surface of face is Lambertian(reflect only the low frequencies of lighting)
2. Lighting can be an arbitrary combination of point sources
--> can be expressed in terms of spherical harmonics(omit the lighting coefficients)
I = albedo * (sh(n) x sh_coeff)
albedo: n x 1
sh_coeff: 9 x 1
Y(n) = (1, n_x, n_y, n_z, n_xn_y, n_xn_z, n_yn_z, n_x^2 - n_y^2, 3n_z^2 - 1)': n x 9
# Y(n) = (1, n_x, n_y, n_z)': n x 4
Args:
vertices: [nver, 3]
triangles: [ntri, 3]
colors: [nver, 3] albedo
sh_coeff: [9, 1] spherical harmonics coefficients
Returns:
lit_colors: [nver, 3]
'''
assert vertices.shape[0] == colors.shape[0]
nver = vertices.shape[0]
normal = get_normal(vertices, triangles) # [nver, 3]
sh = np.array((np.ones(nver), n[:,0], n[:,1], n[:,2], n[:,0]*n[:,1], n[:,0]*n[:,2], n[:,1]*n[:,2], n[:,0]**2 - n[:,1]**2, 3*(n[:,2]**2) - 1)) # [nver, 9]
ref = sh.dot(sh_coeff) #[nver, 1]
lit_colors = colors*ref
return lit_colors
def add_light(vertices, triangles, colors, light_positions = 0, light_intensities = 0):
''' Gouraud shading. add point lights.
In 3d face, usually assume:
1. The surface of face is Lambertian(reflect only the low frequencies of lighting)
2. Lighting can be an arbitrary combination of point sources
3. No specular (unless skin is oil, 23333)
Ref: https://cs184.eecs.berkeley.edu/lecture/pipeline
Args:
vertices: [nver, 3]
triangles: [ntri, 3]
light_positions: [nlight, 3]
light_intensities: [nlight, 3]
Returns:
lit_colors: [nver, 3]
'''
nver = vertices.shape[0]
normals = get_normal(vertices, triangles) # [nver, 3]
# ambient
# La = ka*Ia
# diffuse
# Ld = kd*(I/r^2)max(0, nxl)
direction_to_lights = vertices[np.newaxis, :, :] - light_positions[:, np.newaxis, :] # [nlight, nver, 3]
direction_to_lights_n = np.sqrt(np.sum(direction_to_lights**2, axis = 2)) # [nlight, nver]
direction_to_lights = direction_to_lights/direction_to_lights_n[:, :, np.newaxis]
normals_dot_lights = normals[np.newaxis, :, :]*direction_to_lights # [nlight, nver, 3]
normals_dot_lights = np.sum(normals_dot_lights, axis = 2) # [nlight, nver]
diffuse_output = colors[np.newaxis, :, :]*normals_dot_lights[:, :, np.newaxis]*light_intensities[:, np.newaxis, :]
diffuse_output = np.sum(diffuse_output, axis = 0) # [nver, 3]
# specular
# h = (v + l)/(|v + l|) bisector
# Ls = ks*(I/r^2)max(0, nxh)^p
# increasing p narrows the reflectionlob
lit_colors = diffuse_output # only diffuse part here.
lit_colors = np.minimum(np.maximum(lit_colors, 0), 1)
return lit_colors
## TODO. estimate light(sh coeff)
## -------------------------------- estimate. can not use now.
def fit_light(image, vertices, colors, triangles, vis_ind, lamb = 10, max_iter = 3):
[h, w, c] = image.shape
# surface normal
norm = get_normal(vertices, triangles)
nver = vertices.shape[1]
# vertices --> corresponding image pixel
pt2d = vertices[:2, :]
pt2d[0,:] = np.minimum(np.maximum(pt2d[0,:], 0), w - 1)
pt2d[1,:] = np.minimum(np.maximum(pt2d[1,:], 0), h - 1)
pt2d = np.round(pt2d).astype(np.int32) # 2 x nver
image_pixel = image[pt2d[1,:], pt2d[0,:], :] # nver x 3
image_pixel = image_pixel.T # 3 x nver
# vertices --> corresponding mean texture pixel with illumination
# Spherical Harmonic Basis
harmonic_dim = 9
nx = norm[0,:];
ny = norm[1,:];
nz = norm[2,:];
harmonic = np.zeros((nver, harmonic_dim))
pi = np.pi
harmonic[:,0] = np.sqrt(1/(4*pi)) * np.ones((nver,));
harmonic[:,1] = np.sqrt(3/(4*pi)) * nx;
harmonic[:,2] = np.sqrt(3/(4*pi)) * ny;
harmonic[:,3] = np.sqrt(3/(4*pi)) * nz;
harmonic[:,4] = 1/2. * np.sqrt(3/(4*pi)) * (2*nz**2 - nx**2 - ny**2);
harmonic[:,5] = 3 * np.sqrt(5/(12*pi)) * (ny*nz);
harmonic[:,6] = 3 * np.sqrt(5/(12*pi)) * (nx*nz);
harmonic[:,7] = 3 * np.sqrt(5/(12*pi)) * (nx*ny);
harmonic[:,8] = 3/2. * np.sqrt(5/(12*pi)) * (nx*nx - ny*ny);
'''
I' = sum(albedo * lj * hj) j = 0:9 (albedo = tex)
set A = albedo*h (n x 9)
alpha = lj (9 x 1)
Y = I (n x 1)
Y' = A.dot(alpha)
opt function:
||Y - A*alpha|| + lambda*(alpha'*alpha)
result:
A'*(Y - A*alpha) + lambda*alpha = 0
==>
(A'*A*alpha - lambda)*alpha = A'*Y
left: 9 x 9
right: 9 x 1
'''
n_vis_ind = len(vis_ind)
n = n_vis_ind*c
Y = np.zeros((n, 1))
A = np.zeros((n, 9))
light = np.zeros((3, 1))
for k in range(c):
Y[k*n_vis_ind:(k+1)*n_vis_ind, :] = image_pixel[k, vis_ind][:, np.newaxis]
A[k*n_vis_ind:(k+1)*n_vis_ind, :] = texture[k, vis_ind][:, np.newaxis] * harmonic[vis_ind, :]
Ac = texture[k, vis_ind][:, np.newaxis]
Yc = image_pixel[k, vis_ind][:, np.newaxis]
light[k] = (Ac.T.dot(Yc))/(Ac.T.dot(Ac))
for i in range(max_iter):
Yc = Y.copy()
for k in range(c):
Yc[k*n_vis_ind:(k+1)*n_vis_ind, :] /= light[k]
# update alpha
equation_left = np.dot(A.T, A) + lamb*np.eye(harmonic_dim); # why + ?
equation_right = np.dot(A.T, Yc)
alpha = np.dot(np.linalg.inv(equation_left), equation_right)
# update light
for k in range(c):
Ac = A[k*n_vis_ind:(k+1)*n_vis_ind, :].dot(alpha)
Yc = Y[k*n_vis_ind:(k+1)*n_vis_ind, :]
light[k] = (Ac.T.dot(Yc))/(Ac.T.dot(Ac))
appearance = np.zeros_like(texture)
for k in range(c):
tmp = np.dot(harmonic*texture[k, :][:, np.newaxis], alpha*light[k])
appearance[k,:] = tmp.T
appearance = np.minimum(np.maximum(appearance, 0), 1)
return appearance
@@ -0,0 +1,135 @@
'''
functions about rendering mesh(from 3d obj to 2d image).
only use rasterization render here.
Note that:
1. Generally, render func includes camera, light, raterize. Here no camera and light(I write these in other files)
2. Generally, the input vertices are normalized to [-1,1] and cetered on [0, 0]. (in world space)
Here, the vertices are using image coords, which centers on [w/2, h/2] with the y-axis pointing to oppisite direction.
Means: render here only conducts interpolation.(I just want to make the input flexible)
Author: Yao Feng
Mail: yaofeng1995@gmail.com
'''
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
from time import time
# from .cython import mesh_core_cython
def rasterize_triangles(vertices, triangles, h, w):
'''
Args:
vertices: [nver, 3]
triangles: [ntri, 3]
h: height
w: width
Returns:
depth_buffer: [h, w] saves the depth, here, the bigger the z, the fronter the point.
triangle_buffer: [h, w] saves the tri id(-1 for no triangle).
barycentric_weight: [h, w, 3] saves corresponding barycentric weight.
# Each triangle has 3 vertices & Each vertex has 3 coordinates x, y, z.
# h, w is the size of rendering
'''
# initial
depth_buffer = np.zeros([h, w]) - 999999. #set the initial z to the farest position
triangle_buffer = np.zeros([h, w], dtype = np.int32) - 1 # if tri id = -1, the pixel has no triangle correspondance
barycentric_weight = np.zeros([h, w, 3], dtype = np.float32) #
vertices = vertices.astype(np.float32).copy()
triangles = triangles.astype(np.int32).copy()
mesh_core_cython.rasterize_triangles_core(
vertices, triangles,
depth_buffer, triangle_buffer, barycentric_weight,
vertices.shape[0], triangles.shape[0],
h, w)
def render_colors(vertices, triangles, colors, h, w, c = 3, BG = None):
''' render mesh with colors
Args:
vertices: [nver, 3]
triangles: [ntri, 3]
colors: [nver, 3]
h: height
w: width
c: channel
BG: background image
Returns:
image: [h, w, c]. rendered image./rendering.
'''
# initial
if BG is None:
image = np.zeros((h, w, c), dtype = np.float32)
else:
assert BG.shape[0] == h and BG.shape[1] == w and BG.shape[2] == c
image = BG
depth_buffer = np.zeros([h, w], dtype = np.float32, order = 'C') - 999999.
# change orders. --> C-contiguous order(column major)
vertices = vertices.astype(np.float32).copy()
triangles = triangles.astype(np.int32).copy()
colors = colors.astype(np.float32).copy()
###
st = time()
mesh_core_cython.render_colors_core(
image, vertices, triangles,
colors,
depth_buffer,
vertices.shape[0], triangles.shape[0],
h, w, c)
return image
def render_texture(vertices, triangles, texture, tex_coords, tex_triangles, h, w, c = 3, mapping_type = 'nearest', BG = None):
''' render mesh with texture map
Args:
vertices: [3, nver]
triangles: [3, ntri]
texture: [tex_h, tex_w, 3]
tex_coords: [ntexcoords, 3]
tex_triangles: [ntri, 3]
h: height of rendering
w: width of rendering
c: channel
mapping_type: 'bilinear' or 'nearest'
'''
# initial
if BG is None:
image = np.zeros((h, w, c), dtype = np.float32)
else:
assert BG.shape[0] == h and BG.shape[1] == w and BG.shape[2] == c
image = BG.astype(np.float32)
depth_buffer = np.zeros([h, w], dtype = np.float32, order = 'C') - 999999.
tex_h, tex_w, tex_c = texture.shape
if mapping_type == 'nearest':
mt = int(0)
elif mapping_type == 'bilinear':
mt = int(1)
else:
mt = int(0)
# -> C order
vertices = vertices.astype(np.float32).copy()
triangles = triangles.astype(np.int32).copy()
texture = texture.astype(np.float32).copy()
tex_coords = tex_coords.astype(np.float32).copy()
tex_triangles = tex_triangles.astype(np.int32).copy()
mesh_core_cython.render_texture_core(
image, vertices, triangles,
texture, tex_coords, tex_triangles,
depth_buffer,
vertices.shape[0], tex_coords.shape[0], triangles.shape[0],
h, w, c,
tex_h, tex_w, tex_c,
mt)
return image
@@ -0,0 +1,383 @@
'''
Functions about transforming mesh(changing the position: modify vertices).
1. forward: transform(transform, camera, project).
2. backward: estimate transform matrix from correspondences.
Author: Yao Feng
Mail: yaofeng1995@gmail.com
'''
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
import math
from math import cos, sin
def angle2matrix(angles):
''' get rotation matrix from three rotation angles(degree). right-handed.
Args:
angles: [3,]. x, y, z angles
x: pitch. positive for looking down.
y: yaw. positive for looking left.
z: roll. positive for tilting head right.
Returns:
R: [3, 3]. rotation matrix.
'''
x, y, z = np.deg2rad(angles[0]), np.deg2rad(angles[1]), np.deg2rad(angles[2])
# x
Rx=np.array([[1, 0, 0],
[0, cos(x), -sin(x)],
[0, sin(x), cos(x)]])
# y
Ry=np.array([[ cos(y), 0, sin(y)],
[ 0, 1, 0],
[-sin(y), 0, cos(y)]])
# z
Rz=np.array([[cos(z), -sin(z), 0],
[sin(z), cos(z), 0],
[ 0, 0, 1]])
R=Rz.dot(Ry.dot(Rx))
return R.astype(np.float32)
def angle2matrix_3ddfa(angles):
''' get rotation matrix from three rotation angles(radian). The same as in 3DDFA.
Args:
angles: [3,]. x, y, z angles
x: pitch.
y: yaw.
z: roll.
Returns:
R: 3x3. rotation matrix.
'''
# x, y, z = np.deg2rad(angles[0]), np.deg2rad(angles[1]), np.deg2rad(angles[2])
x, y, z = angles[0], angles[1], angles[2]
# x
Rx=np.array([[1, 0, 0],
[0, cos(x), sin(x)],
[0, -sin(x), cos(x)]])
# y
Ry=np.array([[ cos(y), 0, -sin(y)],
[ 0, 1, 0],
[sin(y), 0, cos(y)]])
# z
Rz=np.array([[cos(z), sin(z), 0],
[-sin(z), cos(z), 0],
[ 0, 0, 1]])
R = Rx.dot(Ry).dot(Rz)
return R.astype(np.float32)
## ------------------------------------------ 1. transform(transform, project, camera).
## ---------- 3d-3d transform. Transform obj in world space
def rotate(vertices, angles):
''' rotate vertices.
X_new = R.dot(X). X: 3 x 1
Args:
vertices: [nver, 3].
rx, ry, rz: degree angles
rx: pitch. positive for looking down
ry: yaw. positive for looking left
rz: roll. positive for tilting head right
Returns:
rotated vertices: [nver, 3]
'''
R = angle2matrix(angles)
rotated_vertices = vertices.dot(R.T)
return rotated_vertices
def similarity_transform(vertices, s, R, t3d):
''' similarity transform. dof = 7.
3D: s*R.dot(X) + t
Homo: M = [[sR, t],[0^T, 1]]. M.dot(X)
Args:(float32)
vertices: [nver, 3].
s: [1,]. scale factor.
R: [3,3]. rotation matrix.
t3d: [3,]. 3d translation vector.
Returns:
transformed vertices: [nver, 3]
'''
t3d = np.squeeze(np.array(t3d, dtype = np.float32))
transformed_vertices = s * vertices.dot(R.T) + t3d[np.newaxis, :]
return transformed_vertices
## -------------- Camera. from world space to camera space
# Ref: https://cs184.eecs.berkeley.edu/lecture/transforms-2
def normalize(x):
epsilon = 1e-12
norm = np.sqrt(np.sum(x**2, axis = 0))
norm = np.maximum(norm, epsilon)
return x/norm
def lookat_camera(vertices, eye, at = None, up = None):
""" 'look at' transformation: from world space to camera space
standard camera space:
camera located at the origin.
looking down negative z-axis.
vertical vector is y-axis.
Xcam = R(X - C)
Homo: [[R, -RC], [0, 1]]
Args:
vertices: [nver, 3]
eye: [3,] the XYZ world space position of the camera.
at: [3,] a position along the center of the camera's gaze.
up: [3,] up direction
Returns:
transformed_vertices: [nver, 3]
"""
if at is None:
at = np.array([0, 0, 0], np.float32)
if up is None:
up = np.array([0, 1, 0], np.float32)
eye = np.array(eye).astype(np.float32)
at = np.array(at).astype(np.float32)
z_aixs = -normalize(at - eye) # look forward
x_aixs = normalize(np.cross(up, z_aixs)) # look right
y_axis = np.cross(z_aixs, x_aixs) # look up
R = np.stack((x_aixs, y_axis, z_aixs))#, axis = 0) # 3 x 3
transformed_vertices = vertices - eye # translation
transformed_vertices = transformed_vertices.dot(R.T) # rotation
return transformed_vertices
## --------- 3d-2d project. from camera space to image plane
# generally, image plane only keeps x,y channels, here reserve z channel for calculating z-buffer.
def orthographic_project(vertices):
''' scaled orthographic projection(just delete z)
assumes: variations in depth over the object is small relative to the mean distance from camera to object
x -> x*f/z, y -> x*f/z, z -> f.
for point i,j. zi~=zj. so just delete z
** often used in face
Homo: P = [[1,0,0,0], [0,1,0,0], [0,0,1,0]]
Args:
vertices: [nver, 3]
Returns:
projected_vertices: [nver, 3] if isKeepZ=True. [nver, 2] if isKeepZ=False.
'''
return vertices.copy()
def perspective_project(vertices, fovy, aspect_ratio = 1., near = 0.1, far = 1000.):
''' perspective projection.
Args:
vertices: [nver, 3]
fovy: vertical angular field of view. degree.
aspect_ratio : width / height of field of view
near : depth of near clipping plane
far : depth of far clipping plane
Returns:
projected_vertices: [nver, 3]
'''
fovy = np.deg2rad(fovy)
top = near*np.tan(fovy)
bottom = -top
right = top*aspect_ratio
left = -right
#-- homo
P = np.array([[near/right, 0, 0, 0],
[0, near/top, 0, 0],
[0, 0, -(far+near)/(far-near), -2*far*near/(far-near)],
[0, 0, -1, 0]])
vertices_homo = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) # [nver, 4]
projected_vertices = vertices_homo.dot(P.T)
projected_vertices = projected_vertices/projected_vertices[:,3:]
projected_vertices = projected_vertices[:,:3]
projected_vertices[:,2] = -projected_vertices[:,2]
#-- non homo. only fovy
# projected_vertices = vertices.copy()
# projected_vertices[:,0] = -(near/right)*vertices[:,0]/vertices[:,2]
# projected_vertices[:,1] = -(near/top)*vertices[:,1]/vertices[:,2]
return projected_vertices
def to_image(vertices, h, w, is_perspective = False):
''' change vertices to image coord system
3d system: XYZ, center(0, 0, 0)
2d image: x(u), y(v). center(w/2, h/2), flip y-axis.
Args:
vertices: [nver, 3]
h: height of the rendering
w : width of the rendering
Returns:
projected_vertices: [nver, 3]
'''
image_vertices = vertices.copy()
if is_perspective:
# if perspective, the projected vertices are normalized to [-1, 1]. so change it to image size first.
image_vertices[:,0] = image_vertices[:,0]*w/2
image_vertices[:,1] = image_vertices[:,1]*h/2
# move to center of image
image_vertices[:,0] = image_vertices[:,0] + w/2
image_vertices[:,1] = image_vertices[:,1] + h/2
# flip vertices along y-axis.
image_vertices[:,1] = h - image_vertices[:,1] - 1
return image_vertices
#### -------------------------------------------2. estimate transform matrix from correspondences.
def estimate_affine_matrix_3d23d(X, Y):
''' Using least-squares solution
Args:
X: [n, 3]. 3d points(fixed)
Y: [n, 3]. corresponding 3d points(moving). Y = PX
Returns:
P_Affine: (3, 4). Affine camera matrix (the third row is [0, 0, 0, 1]).
'''
X_homo = np.hstack((X, np.ones([X.shape[1],1]))) #n x 4
P = np.linalg.lstsq(X_homo, Y)[0].T # Affine matrix. 3 x 4
return P
def estimate_affine_matrix_3d22d(X, x):
''' Using Golden Standard Algorithm for estimating an affine camera
matrix P from world to image correspondences.
See Alg.7.2. in MVGCV
Code Ref: https://github.com/patrikhuber/eos/blob/master/include/eos/fitting/affine_camera_estimation.hpp
x_homo = X_homo.dot(P_Affine)
Args:
X: [n, 3]. corresponding 3d points(fixed)
x: [n, 2]. n>=4. 2d points(moving). x = PX
Returns:
P_Affine: [3, 4]. Affine camera matrix
'''
X = X.T; x = x.T
assert(x.shape[1] == X.shape[1])
n = x.shape[1]
assert(n >= 4)
#--- 1. normalization
# 2d points
mean = np.mean(x, 1) # (2,)
x = x - np.tile(mean[:, np.newaxis], [1, n])
average_norm = np.mean(np.sqrt(np.sum(x**2, 0)))
scale = np.sqrt(2) / average_norm
x = scale * x
T = np.zeros((3,3), dtype = np.float32)
T[0, 0] = T[1, 1] = scale
T[:2, 2] = -mean*scale
T[2, 2] = 1
# 3d points
X_homo = np.vstack((X, np.ones((1, n))))
mean = np.mean(X, 1) # (3,)
X = X - np.tile(mean[:, np.newaxis], [1, n])
m = X_homo[:3,:] - X
average_norm = np.mean(np.sqrt(np.sum(X**2, 0)))
scale = np.sqrt(3) / average_norm
X = scale * X
U = np.zeros((4,4), dtype = np.float32)
U[0, 0] = U[1, 1] = U[2, 2] = scale
U[:3, 3] = -mean*scale
U[3, 3] = 1
# --- 2. equations
A = np.zeros((n*2, 8), dtype = np.float32);
X_homo = np.vstack((X, np.ones((1, n)))).T
A[:n, :4] = X_homo
A[n:, 4:] = X_homo
b = np.reshape(x, [-1, 1])
# --- 3. solution
p_8 = np.linalg.pinv(A).dot(b)
P = np.zeros((3, 4), dtype = np.float32)
P[0, :] = p_8[:4, 0]
P[1, :] = p_8[4:, 0]
P[-1, -1] = 1
# --- 4. denormalization
P_Affine = np.linalg.inv(T).dot(P.dot(U))
return P_Affine
def P2sRt(P):
''' decompositing camera matrix P
Args:
P: (3, 4). Affine Camera Matrix.
Returns:
s: scale factor.
R: (3, 3). rotation matrix.
t: (3,). translation.
'''
t = P[:, 3]
R1 = P[0:1, :3]
R2 = P[1:2, :3]
s = (np.linalg.norm(R1) + np.linalg.norm(R2))/2.0
r1 = R1/np.linalg.norm(R1)
r2 = R2/np.linalg.norm(R2)
r3 = np.cross(r1, r2)
R = np.concatenate((r1, r2, r3), 0)
return s, R, t
#Ref: https://www.learnopencv.com/rotation-matrix-to-euler-angles/
def isRotationMatrix(R):
''' checks if a matrix is a valid rotation matrix(whether orthogonal or not)
'''
Rt = np.transpose(R)
shouldBeIdentity = np.dot(Rt, R)
I = np.identity(3, dtype = R.dtype)
n = np.linalg.norm(I - shouldBeIdentity)
return n < 1e-6
def matrix2angle(R):
''' get three Euler angles from Rotation Matrix
Args:
R: (3,3). rotation matrix
Returns:
x: pitch
y: yaw
z: roll
'''
assert(isRotationMatrix)
sy = math.sqrt(R[0,0] * R[0,0] + R[1,0] * R[1,0])
singular = sy < 1e-6
if not singular :
x = math.atan2(R[2,1] , R[2,2])
y = math.atan2(-R[2,0], sy)
z = math.atan2(R[1,0], R[0,0])
else :
x = math.atan2(-R[1,2], R[1,1])
y = math.atan2(-R[2,0], sy)
z = 0
# rx, ry, rz = np.rad2deg(x), np.rad2deg(y), np.rad2deg(z)
rx, ry, rz = x*180/np.pi, y*180/np.pi, z*180/np.pi
return rx, ry, rz
# def matrix2angle(R):
# ''' compute three Euler angles from a Rotation Matrix. Ref: http://www.gregslabaugh.net/publications/euler.pdf
# Args:
# R: (3,3). rotation matrix
# Returns:
# x: yaw
# y: pitch
# z: roll
# '''
# # assert(isRotationMatrix(R))
# if R[2,0] !=1 or R[2,0] != -1:
# x = math.asin(R[2,0])
# y = math.atan2(R[2,1]/cos(x), R[2,2]/cos(x))
# z = math.atan2(R[1,0]/cos(x), R[0,0]/cos(x))
# else:# Gimbal lock
# z = 0 #can be anything
# if R[2,0] == -1:
# x = np.pi/2
# y = z + math.atan2(R[0,1], R[0,2])
# else:
# x = -np.pi/2
# y = -z + math.atan2(-R[0,1], -R[0,2])
# return x, y, z
@@ -0,0 +1,24 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
# from skimage import measure
from mpl_toolkits.mplot3d import Axes3D
def plot_mesh(vertices, triangles, subplot = [1,1,1], title = 'mesh', el = 90, az = -90, lwdt=.1, dist = 6, color = "grey"):
'''
plot the mesh
Args:
vertices: [nver, 3]
triangles: [ntri, 3]
'''
ax = plt.subplot(subplot[0], subplot[1], subplot[2], projection = '3d')
ax.plot_trisurf(vertices[:, 0], vertices[:, 1], vertices[:, 2], triangles = triangles, lw = lwdt, color = color, alpha = 1)
ax.axis("off")
ax.view_init(elev = el, azim = az)
ax.dist = dist
plt.title(title)
### -------------- Todo: use vtk to visualize mesh? or visvis? or VisPy?
@@ -0,0 +1,7 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from .. import mesh
from .morphabel_model import MorphabelModel
from . import load
@@ -0,0 +1,270 @@
'''
Estimating parameters about vertices: shape para, exp para, pose para(s, R, t)
'''
import numpy as np
''' TODO: a clear document.
Given: image_points, 3D Model, Camera Matrix(s, R, t2d)
Estimate: shape parameters, expression parameters
Inference:
projected_vertices = s*P*R(mu + shape + exp) + t2d --> image_points
s*P*R*shape + s*P*R(mu + exp) + t2d --> image_poitns
# Define:
X = vertices
x_hat = projected_vertices
x = image_points
A = s*P*R
b = s*P*R(mu + exp) + t2d
==>
x_hat = A*shape + b (2 x n)
A*shape (2 x n)
shape = reshape(shapePC * sp) (3 x n)
shapePC*sp : (3n x 1)
* flatten:
x_hat_flatten = A*shape + b_flatten (2n x 1)
A*shape (2n x 1)
--> A*shapePC (2n x 199) sp: 199 x 1
# Define:
pc_2d = A* reshape(shapePC)
pc_2d_flatten = flatten(pc_2d) (2n x 199)
=====>
x_hat_flatten = pc_2d_flatten * sp + b_flatten ---> x_flatten (2n x 1)
Goals:
(ignore flatten, pc_2d-->pc)
min E = || x_hat - x || + lambda*sum(sp/sigma)^2
= || pc * sp + b - x || + lambda*sum(sp/sigma)^2
Solve:
d(E)/d(sp) = 0
2 * pc' * (pc * sp + b - x) + 2 * lambda * sp / (sigma' * sigma) = 0
Get:
(pc' * pc + lambda / (sigma'* sigma)) * sp = pc' * (x - b)
'''
def estimate_shape(x, shapeMU, shapePC, shapeEV, expression, s, R, t2d, lamb = 3000):
'''
Args:
x: (2, n). image points (to be fitted)
shapeMU: (3n, 1)
shapePC: (3n, n_sp)
shapeEV: (n_sp, 1)
expression: (3, n)
s: scale
R: (3, 3). rotation matrix
t2d: (2,). 2d translation
lambda: regulation coefficient
Returns:
shape_para: (n_sp, 1) shape parameters(coefficients)
'''
x = x.copy()
assert(shapeMU.shape[0] == shapePC.shape[0])
assert(shapeMU.shape[0] == x.shape[1]*3)
dof = shapePC.shape[1]
n = x.shape[1]
sigma = shapeEV
t2d = np.array(t2d)
P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)
A = s*P.dot(R)
# --- calc pc
pc_3d = np.resize(shapePC.T, [dof, n, 3]) # 199 x n x 3
pc_3d = np.reshape(pc_3d, [dof*n, 3])
pc_2d = pc_3d.dot(A.T.copy()) # 199 x n x 2
pc = np.reshape(pc_2d, [dof, -1]).T # 2n x 199
# --- calc b
# shapeMU
mu_3d = np.resize(shapeMU, [n, 3]).T # 3 x n
# expression
exp_3d = expression
#
b = A.dot(mu_3d + exp_3d) + np.tile(t2d[:, np.newaxis], [1, n]) # 2 x n
b = np.reshape(b.T, [-1, 1]) # 2n x 1
# --- solve
equation_left = np.dot(pc.T, pc) + lamb * np.diagflat(1/sigma**2)
x = np.reshape(x.T, [-1, 1])
equation_right = np.dot(pc.T, x - b)
shape_para = np.dot(np.linalg.inv(equation_left), equation_right)
return shape_para
def estimate_expression(x, shapeMU, expPC, expEV, shape, s, R, t2d, lamb = 2000):
'''
Args:
x: (2, n). image points (to be fitted)
shapeMU: (3n, 1)
expPC: (3n, n_ep)
expEV: (n_ep, 1)
shape: (3, n)
s: scale
R: (3, 3). rotation matrix
t2d: (2,). 2d translation
lambda: regulation coefficient
Returns:
exp_para: (n_ep, 1) shape parameters(coefficients)
'''
x = x.copy()
assert(shapeMU.shape[0] == expPC.shape[0])
assert(shapeMU.shape[0] == x.shape[1]*3)
dof = expPC.shape[1]
n = x.shape[1]
sigma = expEV
t2d = np.array(t2d)
P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)
A = s*P.dot(R)
# --- calc pc
pc_3d = np.resize(expPC.T, [dof, n, 3])
pc_3d = np.reshape(pc_3d, [dof*n, 3])
pc_2d = pc_3d.dot(A.T)
pc = np.reshape(pc_2d, [dof, -1]).T # 2n x 29
# --- calc b
# shapeMU
mu_3d = np.resize(shapeMU, [n, 3]).T # 3 x n
# expression
shape_3d = shape
#
b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n]) # 2 x n
b = np.reshape(b.T, [-1, 1]) # 2n x 1
# --- solve
equation_left = np.dot(pc.T, pc) + lamb * np.diagflat(1/sigma**2)
x = np.reshape(x.T, [-1, 1])
equation_right = np.dot(pc.T, x - b)
exp_para = np.dot(np.linalg.inv(equation_left), equation_right)
return exp_para
# ---------------- fit
def fit_points(x, X_ind, model, n_sp, n_ep, max_iter = 4):
'''
Args:
x: (n, 2) image points
X_ind: (n,) corresponding Model vertex indices
model: 3DMM
max_iter: iteration
Returns:
sp: (n_sp, 1). shape parameters
ep: (n_ep, 1). exp parameters
s, R, t
'''
x = x.copy().T
#-- init
sp = np.zeros((n_sp, 1), dtype = np.float32)
ep = np.zeros((n_ep, 1), dtype = np.float32)
#-------------------- estimate
X_ind_all = np.tile(X_ind[np.newaxis, :], [3, 1])*3
X_ind_all[1, :] += 1
X_ind_all[2, :] += 2
valid_ind = X_ind_all.flatten('F')
shapeMU = model['shapeMU'][valid_ind, :]
shapePC = model['shapePC'][valid_ind, :n_sp]
expPC = model['expPC'][valid_ind, :n_ep]
for i in range(max_iter):
X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)
X = np.reshape(X, [int(len(X)/3), 3]).T
#----- estimate pose
P = Face.face3d.mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)
s, R, t = Face.face3d.mesh.transform.P2sRt(P)
rx, ry, rz = Face.face3d.mesh.transform.matrix2angle(R)
# print('Iter:{}; estimated pose: s {}, rx {}, ry {}, rz {}, t1 {}, t2 {}'.format(i, s, rx, ry, rz, t[0], t[1]))
#----- estimate shape
# expression
shape = shapePC.dot(sp)
shape = np.reshape(shape, [int(len(shape)/3), 3]).T
ep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 20)
# shape
expression = expPC.dot(ep)
expression = np.reshape(expression, [int(len(expression)/3), 3]).T
sp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 40)
return sp, ep, s, R, t
# ---------------- fitting process
def fit_points_for_show(x, X_ind, model, n_sp, n_ep, max_iter = 4):
'''
Args:
x: (n, 2) image points
X_ind: (n,) corresponding Model vertex indices
model: 3DMM
max_iter: iteration
Returns:
sp: (n_sp, 1). shape parameters
ep: (n_ep, 1). exp parameters
s, R, t
'''
x = x.copy().T
#-- init
sp = np.zeros((n_sp, 1), dtype = np.float32)
ep = np.zeros((n_ep, 1), dtype = np.float32)
#-------------------- estimate
X_ind_all = np.tile(X_ind[np.newaxis, :], [3, 1])*3
X_ind_all[1, :] += 1
X_ind_all[2, :] += 2
valid_ind = X_ind_all.flatten('F')
shapeMU = model['shapeMU'][valid_ind, :]
shapePC = model['shapePC'][valid_ind, :n_sp]
expPC = model['expPC'][valid_ind, :n_ep]
s = 4e-04
R = Face.face3d.mesh.transform.angle2matrix([0, 0, 0])
t = [0, 0, 0]
lsp = []; lep = []; ls = []; lR = []; lt = []
for i in range(max_iter):
X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)
X = np.reshape(X, [int(len(X)/3), 3]).T
lsp.append(sp); lep.append(ep); ls.append(s), lR.append(R), lt.append(t)
#----- estimate pose
P = Face.face3d.mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)
s, R, t = Face.face3d.mesh.transform.P2sRt(P)
lsp.append(sp); lep.append(ep); ls.append(s), lR.append(R), lt.append(t)
#----- estimate shape
# expression
shape = shapePC.dot(sp)
shape = np.reshape(shape, [int(len(shape)/3), 3]).T
ep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 20)
lsp.append(sp); lep.append(ep); ls.append(s), lR.append(R), lt.append(t)
# shape
expression = expPC.dot(ep)
expression = np.reshape(expression, [int(len(expression)/3), 3]).T
sp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 40)
# print('ls', ls)
# print('lR', lR)
return np.array(lsp), np.array(lep), np.array(ls), np.array(lR), np.array(lt)
@@ -0,0 +1,110 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
import scipy.io as sio
### --------------------------------- load BFM data
def load_BFM(model_path):
''' load BFM 3DMM model
Args:
model_path: path to BFM model.
Returns:
model: (nver = 53215, ntri = 105840). nver: number of vertices. ntri: number of triangles.
'shapeMU': [3*nver, 1]
'shapePC': [3*nver, 199]
'shapeEV': [199, 1]
'expMU': [3*nver, 1]
'expPC': [3*nver, 29]
'expEV': [29, 1]
'texMU': [3*nver, 1]
'texPC': [3*nver, 199]
'texEV': [199, 1]
'tri': [ntri, 3] (start from 1, should sub 1 in python and c++)
'tri_mouth': [114, 3] (start from 1, as a supplement to mouth triangles)
'kpt_ind': [68,] (start from 1)
PS:
You can change codes according to your own saved data.
Just make sure the model has corresponding attributes.
'''
C = sio.loadmat(model_path)
model = C['model']
model = model[0,0]
# change dtype from double(np.float64) to np.float32,
# since big matrix process(espetially matrix dot) is too slow in python.
model['shapeMU'] = (model['shapeMU'] + model['expMU']).astype(np.float32)
model['shapePC'] = model['shapePC'].astype(np.float32)
model['shapeEV'] = model['shapeEV'].astype(np.float32)
model['expEV'] = model['expEV'].astype(np.float32)
model['expPC'] = model['expPC'].astype(np.float32)
# matlab start with 1. change to 0 in python.
model['tri'] = model['tri'].T.copy(order = 'C').astype(np.int32) - 1
model['tri_mouth'] = model['tri_mouth'].T.copy(order = 'C').astype(np.int32) - 1
# kpt ind
model['kpt_ind'] = (np.squeeze(model['kpt_ind']) - 1).astype(np.int32)
return model
def load_BFM_info(path = 'BFM_info.mat'):
''' load 3DMM model extra information
Args:
path: path to BFM info.
Returns:
model_info:
'symlist': 2 x 26720
'symlist_tri': 2 x 52937
'segbin': 4 x n (0: nose, 1: eye, 2: mouth, 3: cheek)
'segbin_tri': 4 x ntri
'face_contour': 1 x 28
'face_contour_line': 1 x 512
'face_contour_front': 1 x 28
'face_contour_front_line': 1 x 512
'nose_hole': 1 x 142
'nose_hole_right': 1 x 71
'nose_hole_left': 1 x 71
'parallel': 17 x 1 cell
'parallel_face_contour': 28 x 1 cell
'uv_coords': n x 2
'''
C = sio.loadmat(path)
model_info = C['model_info']
model_info = model_info[0,0]
return model_info
def load_uv_coords(path = 'BFM_UV.mat'):
''' load uv coords of BFM
Args:
path: path to data.
Returns:
uv_coords: [nver, 2]. range: 0-1
'''
C = sio.loadmat(path)
uv_coords = C['UV'].copy(order = 'C')
return uv_coords
def load_pncc_code(path = 'pncc_code.mat'):
''' load pncc code of BFM
PNCC code: Defined in 'Face Alignment Across Large Poses: A 3D Solution Xiangyu'
download at http://www.cbsr.ia.ac.cn/users/xiangyuzhu/projects/3DDFA/main.htm.
Args:
path: path to data.
Returns:
pncc_code: [nver, 3]
'''
C = sio.loadmat(path)
pncc_code = C['vertex_code'].T
return pncc_code
##
def get_organ_ind(model_info):
''' get nose, eye, mouth index
'''
valid_bin = model_info['segbin'].astype(bool)
organ_ind = np.nonzero(valid_bin[0,:])[0]
for i in range(1, valid_bin.shape[0] - 1):
organ_ind = np.union1d(organ_ind, np.nonzero(valid_bin[i,:])[0])
return organ_ind.astype(np.int32)
@@ -0,0 +1,141 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
from . import fit
from . import load
class MorphabelModel(object):
"""docstring for MorphabelModel
model: nver: number of vertices. ntri: number of triangles. *: must have. ~: can generate ones array for place holder.
'shapeMU': [3*nver, 1]. *
'shapePC': [3*nver, n_shape_para]. *
'shapeEV': [n_shape_para, 1]. ~
'expMU': [3*nver, 1]. ~
'expPC': [3*nver, n_exp_para]. ~
'expEV': [n_exp_para, 1]. ~
'texMU': [3*nver, 1]. ~
'texPC': [3*nver, n_tex_para]. ~
'texEV': [n_tex_para, 1]. ~
'tri': [ntri, 3] (start from 1, should sub 1 in python and c++). *
'tri_mouth': [114, 3] (start from 1, as a supplement to mouth triangles). ~
'kpt_ind': [68,] (start from 1). ~
"""
def __init__(self, model_path, model_type = 'BFM'):
super( MorphabelModel, self).__init__()
if model_type=='BFM':
self.model = load.load_BFM(model_path)
else:
print('sorry, not support other 3DMM model now')
exit()
# fixed attributes
self.nver = self.model['shapePC'].shape[0]/3
self.ntri = self.model['tri'].shape[0]
self.n_shape_para = self.model['shapePC'].shape[1]
self.n_exp_para = self.model['expPC'].shape[1]
self.n_tex_para = self.model['texMU'].shape[1]
self.kpt_ind = self.model['kpt_ind']
self.triangles = self.model['tri']
self.full_triangles = np.vstack((self.model['tri'], self.model['tri_mouth']))
# ------------------------------------- shape: represented with mesh(vertices & triangles(fixed))
def get_shape_para(self, type = 'random'):
if type == 'zero':
sp = np.random.zeros((self.n_shape_para, 1))
elif type == 'random':
sp = np.random.rand(self.n_shape_para, 1)*1e04
return sp
def get_exp_para(self, type = 'random'):
if type == 'zero':
ep = np.zeros((self.n_exp_para, 1))
elif type == 'random':
ep = -1.5 + 3*np.random.random([self.n_exp_para, 1])
ep[6:, 0] = 0
return ep
def generate_vertices(self, shape_para, exp_para):
'''
Args:
shape_para: (n_shape_para, 1)
exp_para: (n_exp_para, 1)
Returns:
vertices: (nver, 3)
'''
vertices = self.model['shapeMU'] + self.model['shapePC'].dot(shape_para) + self.model['expPC'].dot(exp_para)
vertices = np.reshape(vertices, [int(3), int(len(vertices)/3)], 'F').T
return vertices
# -------------------------------------- texture: here represented with rgb value(colors) in vertices.
def get_tex_para(self, type = 'random'):
if type == 'zero':
tp = np.zeros((self.n_tex_para, 1))
elif type == 'random':
tp = np.random.rand(self.n_tex_para, 1)
return tp
def generate_colors(self, tex_para):
'''
Args:
tex_para: (n_tex_para, 1)
Returns:
colors: (nver, 3)
'''
colors = self.model['texMU'] + self.model['texPC'].dot(tex_para*self.model['texEV'])
colors = np.reshape(colors, [int(3), int(len(colors)/3)], 'F').T/255.
return colors
# ------------------------------------------- transformation
# ------------- transform
def rotate(self, vertices, angles):
''' rotate face
Args:
vertices: [nver, 3]
angles: [3] x, y, z rotation angle(degree)
x: pitch. positive for looking down
y: yaw. positive for looking left
z: roll. positive for tilting head right
Returns:
vertices: rotated vertices
'''
return Face.face3d.mesh.transform.rotate(vertices, angles)
def transform(self, vertices, s, angles, t3d):
R = Face.face3d.mesh.transform.angle2matrix(angles)
return Face.face3d.mesh.transform.similarity_transform(vertices, s, R, t3d)
def transform_3ddfa(self, vertices, s, angles, t3d): # only used for processing 300W_LP data
R = Face.face3d.mesh.transform.angle2matrix_3ddfa(angles)
return Face.face3d.mesh.transform.similarity_transform(vertices, s, R, t3d)
# --------------------------------------------------- fitting
def fit(self, x, X_ind, max_iter = 4, isShow = False):
''' fit 3dmm & pose parameters
Args:
x: (n, 2) image points
X_ind: (n,) corresponding Model vertex indices
max_iter: iteration
isShow: whether to reserve middle results for show
Returns:
fitted_sp: (n_sp, 1). shape parameters
fitted_ep: (n_ep, 1). exp parameters
s, angles, t
'''
if isShow:
fitted_sp, fitted_ep, s, R, t = fit.fit_points_for_show(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
angles = np.zeros((R.shape[0], 3))
for i in range(R.shape[0]):
angles[i] = Face.face3d.mesh.transform.matrix2angle(R[i])
else:
fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
angles = Face.face3d.mesh.transform.matrix2angle(R)
return fitted_sp, fitted_ep, s, angles, t
@@ -0,0 +1,96 @@
import os
import cv2
import glob
import numpy as np
from core.utils import landmark_processor
from core.face_enhance.face_gan_pt import FaceGAN
from time import time
class FaceEnhancement(object):
def __init__(self, size=512, gpu_id=None):
self.facegan = FaceGAN(size, gpu_id)
self.size = size
self.threshold = 0.9
# the mask for pasting restored faces back
self.mask = np.zeros((512, 512), np.float32)
cv2.rectangle(self.mask, (26, 26), (486, 486), (1, 1, 1), -1, cv2.LINE_AA)
self.mask = cv2.GaussianBlur(self.mask, (101, 101), 11)
self.mask = cv2.GaussianBlur(self.mask, (101, 101), 11)
self.kernel = np.array((
[0.0625, 0.125, 0.0625],
[0.125, 0.25, 0.125],
[0.0625, 0.125, 0.0625]), dtype="float32")
def process(self, img, landmarks1k):
assert len(landmarks1k) == 1000
image_to_face_mat = landmark_processor.get_transform_mat_face_restore(landmarks1k, self.size)
tfm_inv = cv2.invertAffineTransform(image_to_face_mat)
height, width = img.shape[:2]
full_mask = np.zeros((height, width), dtype=np.float32)
full_img = np.zeros(img.shape, dtype=np.uint8)
fh, fw = (landmarks1k[0][1]-landmarks1k[154][1]), (landmarks1k[95][0]-landmarks1k[215][0])
of = cv2.warpAffine(img, image_to_face_mat, (self.size, self.size), flags=cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_CONSTANT, borderValue=[0, 0, 0])
# enhance the face
t0 = time()
ef = self.facegan.process(of)
# print('facegan process costs:', time() - t0)
tmp_mask = self.mask
tmp_mask = cv2.resize(tmp_mask, ef.shape[:2])
tmp_mask = cv2.warpAffine(tmp_mask, tfm_inv, (width, height), flags=3)
if min(fh, fw)<100: # gaussian filter for small faces
ef = cv2.filter2D(ef, -1, self.kernel)
# tmp_img = cv2.warpAffine(ef, tfm_inv, (width, height), flags=3)
tmp_img = cv2.warpAffine(ef, tfm_inv, (width, height), dst=img.copy(), borderMode=cv2.BORDER_TRANSPARENT)
# cv2.imshow("tmp_img: ", tmp_img)
mask = tmp_mask - full_mask
full_mask[np.where(mask>0)] = tmp_mask[np.where(mask>0)]
full_img[np.where(mask>0)] = tmp_img[np.where(mask>0)]
full_mask = full_mask[:, :, np.newaxis]
img = cv2.convertScaleAbs(img*(1-full_mask) + full_img*full_mask)
return img
if __name__=='__main__':
indir = '/media/DATA_4T/zao_data/tencent_con_0701/ref_c/res_contrast_blur13_notps_yunfu_res'
outdir = '/media/DATA_4T/zao_data/tencent_con_0701/ref_c/res_contrast_blur13_notps_yunfu_res_outs2'
os.makedirs(outdir, exist_ok=True)
faceenhancer = FaceEnhancement(base_dir="/", size=512, model="GPEN-512", channel_multiplier=2)
files = sorted(glob.glob(os.path.join(indir, '*.*g')))
for n, file in enumerate(files[:]):
filename = os.path.basename(file)
txtname = file.replace(".jpg", "_landmark1k.txt")
im = cv2.imread(file, cv2.IMREAD_COLOR) # BGR
print(txtname)
landmark = np.loadtxt(txtname)
if not isinstance(im, np.ndarray): print(filename, 'error'); continue
start = time()
img = faceenhancer.process(im, landmark)
end = time()
print("Time cost: {:.4f}".format(end - start))
cv2.imwrite(os.path.join(outdir, '.'.join(filename.split('.')[:-1])+'_2.jpg'), img)
@@ -0,0 +1,64 @@
'''
@paper: GAN Prior Embedded Network for Blind Face Restoration in the Wild (CVPR2021)
@author: yangxy (yangtao9009@gmail.com)
'''
import torch
import os
import cv2
import numpy as np
class FaceGAN(object):
def __init__(self, size=512, gpu_id=0):
# self.mfile = os.path.join(base_dir, model+'.pth')
self.n_mlp = 8
self.resolution = size
self.device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
self.load_model()
def load_model(self):
self.face_gan_model = os.path.join('weights', "face_enhance_0630.pt")
self.model = torch.jit.load(self.face_gan_model, torch.device('cpu')).to(self.device)
# self.model = torch.jit.load(self.face_gan_model).to(self.device)
# self.model = torch.load(self.face_gan_model,map_location=self.device)
self.model.eval()
def process_o(self, img):
img = cv2.resize(img, (self.resolution, self.resolution))
img_t = self.img2tensor(img)
with torch.no_grad():
out, __ = self.model(img_t)
out = self.tensor2img(out)
return out
def process(self, img):
img = cv2.resize(img, (self.resolution, self.resolution))
img_t = self.img2tensor(img)
with torch.no_grad():
out = self.forward(img_t)
out = self.tensor2img(out)
return out
def forward(self, img_t):
with torch.no_grad():
out = self.model(img_t)
return out
def img2tensor(self, img):
img_t = (torch.from_numpy(img).to(self.device)/255. - 0.5) / 0.5
img_t = img_t.permute(2, 0, 1).unsqueeze(0).flip(1) # BGR->RGB
return img_t
def tensor2img(self, image_tensor, pmax=255.0, imtype=np.uint8):
image_tensor = image_tensor * 0.5 + 0.5
image_tensor = image_tensor.squeeze(0).permute(1, 2, 0).flip(2) # RGB->BGR
image_numpy = np.clip(image_tensor.float().cpu().numpy(), 0, 1) * pmax
return image_numpy.astype(imtype)
@@ -0,0 +1,102 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @File : setup.py
# @Time : 2020/1/15
# @Author : yangchaojie (yangchaojie@immomo.com)
import os
import sys
import shutil
import numpy
import tempfile
from setuptools import setup
from setuptools.extension import Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import platform
def get_root_path(root):
if os.path.dirname(root) in ['', '.']:
return os.path.basename(root)
else:
return get_root_path(os.path.dirname(root))
def copy_file(src, dest):
if os.path.exists(dest):
return
if not os.path.exists(os.path.dirname(dest)):
os.makedirs(os.path.dirname(dest))
if os.path.isdir(src):
shutil.copytree(src, dest)
else:
shutil.copyfile(src, dest)
def touch_init_file():
init_file_name = os.path.join(tempfile.mkdtemp(), '__init__.py')
with open(init_file_name, 'w'):
pass
return init_file_name
def compose_extensions(root='.'):
for file_ in os.listdir(root):
abs_file = os.path.join(root, file_)
if os.path.isfile(abs_file):
if abs_file.endswith('.py'):
extensions.append(Extension(get_root_path(abs_file) + '.*', [abs_file]))
elif abs_file.endswith('.c') or abs_file.endswith('.pyc'):
continue
else:
copy_file(abs_file, os.path.join(build_root_dir, abs_file))
if abs_file.endswith('__init__.py'):
copy_file(init_file, os.path.join(build_root_dir, abs_file))
else:
if os.path.basename(abs_file) in ignore_folders :
continue
if os.path.basename(abs_file) in conf_folders:
copy_file(abs_file, os.path.join(build_root_dir, abs_file))
compose_extensions(abs_file)
# if __name__ == '__main__':
build_root_dir = 'build/lib.' + platform.system().lower() + '-' + platform.machine() + '-' + str(
sys.version_info.major) + '.' + str(sys.version_info.minor)
print(build_root_dir)
extensions = []
ignore_folders = ['build', 'new_ref_zao_color_0818', 'ref_hair_online_0703_local', 'ref_分好类别', '.git']
conf_folders = ['conf']
init_file = touch_init_file()
print(init_file)
compose_extensions()
os.remove(init_file)
setup(
name='moxie_hairstyle',
version='1.0',
ext_modules=cythonize(
extensions,
nthreads=16,
compiler_directives=dict(always_allow_keywords=True),
include_path=[numpy.get_include()]),
cmdclass=dict(build_ext=build_ext))
# python setup.py build_ext
@@ -0,0 +1,186 @@
import cv2
class SingleFaceQualityInfo():
def __int__(self, gpu_id):
print('Model 3DDFA init success')
def getSpotProportion(self, grayImage):
Proportion = 0
hist = cv2.calcHist([grayImage], [0], None, [256], [0, 256])
for i in range(253, 256):
Proportion = Proportion + hist[i, 0]
return Proportion
def GetSpotRatio(self, wrap_img, wrap_face_rect):
scale = 0.9
x = max(0, int(wrap_face_rect[0]))
y = max(0, int(wrap_face_rect[1]))
w = max(0, int(wrap_face_rect[2]-wrap_face_rect[0]))
h = max(0, int(wrap_face_rect[3]-wrap_face_rect[1]))
height, width, _ = wrap_img.shape
if (x + w) > width:
w = width - x
if (y + h) > height:
h = height - y
p_cen = (x + w/2, y+h/2)
new_w = w*scale
new_h = h*scale
na_box = [int(p_cen[0]-new_w/2), int(p_cen[1]) - int(new_h/2), int(new_w), int(new_h)]
imgCropGray = cv2.cvtColor(wrap_img[na_box[1]:na_box[1] + na_box[3], na_box[0]:na_box[0] + na_box[2]],cv2.COLOR_RGB2GRAY)
Spot_Ratio = self.getSpotProportion(imgCropGray) / (imgCropGray.shape[0] * imgCropGray.shape[1])
return Spot_Ratio
def BoxClarityAndBrightValue(self, wrap_img, wrap_face_rect):
x = max(0, int(wrap_face_rect[0]))
y = max(0, int(wrap_face_rect[1]))
w = max(0, int(wrap_face_rect[2]-wrap_face_rect[0]))
h = max(0, int(wrap_face_rect[3]-wrap_face_rect[1]))
# cv2.imshow('face', wrap_img[y:y+h,x:x+w])
# cv2.waitKey()
height, width, _ = wrap_img.shape
if (x + w) > width:
w = width - x
if (y + h) > height:
h = height - y
box_crop = [int(x), int(y), int(w), int(h)]
imgCropGray = cv2.cvtColor(wrap_img[box_crop[1]:box_crop[1] + box_crop[3], box_crop[0]:box_crop[0] + box_crop[2]],cv2.COLOR_RGB2GRAY)
left_ = [0,0,int(w/2.0),int(h)]
right_ = [int(w/2.0),0,int(w/2.0),int(h)]
imgR = imgCropGray[right_[1]:right_[1] + right_[3], right_[0]:right_[0] + right_[2]]
imgL = imgCropGray[left_[1]:left_[1] + left_[3], left_[0]:left_[0] + left_[2]]
mean_R, var_R = cv2.meanStdDev(imgR)
mean_L, var_L = cv2.meanStdDev(imgL)
bV_R = mean_R[0]
bV_L = mean_L[0]
imageCropSobel = cv2.Laplacian(imgCropGray, cv2.CV_64F, 3)
mean, var = cv2.meanStdDev(imageCropSobel)
clarityValue = var[0]
result = [bV_R, bV_L, clarityValue]
return result
def qualityTest(self, wrap_img, wrap_face_rect, single_eulers):
single_face_quality_info = {
'quality_score': -1,
'face_quality_flag': -1,
'SpotRatio': 0,
'Brightness': 0,
'Clarity': 0,
'SpotScore': 0,
'BrightnessScore': 0,
'ClarityScore': 0
}
if wrap_img is None:
single_face_quality_info['face_quality_flag'] = 0
qp_img_size = wrap_img.shape[1]
Spot_Ratio = self.GetSpotRatio(wrap_img, wrap_face_rect)
result = self.BoxClarityAndBrightValue(wrap_img, wrap_face_rect)
bv_R, bv_L, clarityValue = result
single_face_quality_info['SpotRatio'] = Spot_Ratio
single_face_quality_info['Brightness'] = min(bv_L, bv_R)
single_face_quality_info['Clarity'] = clarityValue
if qp_img_size == 100:
clarityValue_thresholdDown = 35
clarityValue_thresholdUp = 300
else:
clarityValue_thresholdDown = 24
clarityValue_thresholdUp = 300
Bright_area_thre = 0.06
brightnessValue_thresholddown = 40
brightnessValue_thresholdUp = 230
whitespot = (Spot_Ratio > Bright_area_thre)
brightness =(bv_R > brightnessValue_thresholddown and bv_R < brightnessValue_thresholdUp) and (bv_L > brightnessValue_thresholddown and bv_L < brightnessValue_thresholdUp)
clarity = clarityValue < clarityValue_thresholdUp and clarityValue > clarityValue_thresholdDown
if whitespot:
single_face_quality_info['face_qality_flag'] = 2
if brightness:
single_face_quality_info['face_qality_flag'] = 4
if not clarity:
single_face_quality_info['face_qality_flag'] = 3
if brightness and clarity and not whitespot:
single_face_quality_info['face_qality_flag'] = 1
if single_face_quality_info['face_qality_flag'] == 1:
PerfectSpotRatio = 0.02
if qp_img_size == 100:
PerfectClarity = 130
else:
PerfectClarity = 60
PerfectBrightness_down = 90
PerfectBrightness_up = 200
PerfectBrightness_standard = PerfectBrightness_down - brightnessValue_thresholddown
PerfectClarity_standard = PerfectClarity - clarityValue_thresholdDown
PerfectSpot_standard = Bright_area_thre - PerfectSpotRatio
if single_face_quality_info['SpotRatio'] <= PerfectSpotRatio:
single_face_quality_info['SpotScore'] = 1
else:
single_face_quality_info['SpotRatio'] = 1 - (single_face_quality_info['SpotRatio'] - PerfectSpotRatio) / PerfectSpot_standard
if single_face_quality_info['Clarity'] >= PerfectClarity:
single_face_quality_info['ClarityScore'] = 1
else:
single_face_quality_info['ClarityScore'] = 1 - (PerfectClarity - single_face_quality_info['Clarity']) / PerfectClarity_standard
if single_face_quality_info['Brightness'] >= PerfectBrightness_down and single_face_quality_info['Brightness']<= PerfectBrightness_up:
single_face_quality_info['BrightnessScore'] = 1
elif single_face_quality_info['Brightness'] < PerfectBrightness_down:
single_face_quality_info['BrightnessScore'] = 1 - (PerfectBrightness_down - single_face_quality_info['Brightness']) / PerfectBrightness_standard
elif single_face_quality_info['Brightness'] > PerfectBrightness_up:
single_face_quality_info['BrightnessScore'] = 1 - (single_face_quality_info['Brightness'] - PerfectBrightness_up)/PerfectBrightness_standard
else:
print("Wrong Brightness :%f",single_face_quality_info['Brightness'])
else:
single_face_quality_info['ClarityScore'] = 0
single_face_quality_info['BrightnessScore'] = 0
single_face_quality_info['SpotScore'] = 0
if len(single_eulers) > 2:
euler_pitch_perfect = 10
euler_yaw_perfect = 10
euler_pitch_max = 25
euler_yaw_max = 25
euler_roll_max = 30
image_ClarityScore = float(single_face_quality_info['ClarityScore'])
image_BrightnessScore = single_face_quality_info['BrightnessScore']
image_SpotScore = single_face_quality_info['SpotScore']
euler_pitch, euler_yaw, euler_roll = single_eulers
if abs(euler_pitch) > euler_pitch_perfect and abs(euler_pitch) < euler_pitch_max and single_face_quality_info['face_qality_flag'] == 1:
image_PitchScore = 1 - (abs(euler_pitch) - euler_pitch_perfect) / (euler_pitch_max - euler_pitch_perfect)
elif abs(euler_pitch) < euler_pitch_perfect and single_face_quality_info['face_qality_flag'] == 1:
image_PitchScore = 1.0
else:
image_PitchScore = 0
if abs(euler_yaw) > euler_yaw_perfect and abs(euler_yaw) < euler_yaw_max and single_face_quality_info['face_qality_flag']==1:
image_YawScore = 1 - (abs(euler_yaw) - euler_yaw_perfect) / (euler_yaw_max - euler_yaw_perfect)
elif abs(euler_yaw) < euler_yaw_perfect and single_face_quality_info['face_qality_flag']==1:
image_YawScore = 1.0
else:
image_YawScore = 0
if image_PitchScore < 0.3 and image_YawScore < 0.3:
image_spot_weight = 0
image_clarity_weight = 0.1
image_brightness_weight = 0.1
euler_pitch_weight = 0.4
euler_yaw_weight = 0.4
else:
image_spot_weight = 0.05
image_clarity_weight = 0.35
image_brightness_weight = 0.1
euler_pitch_weight = 0.25
euler_yaw_weight = 0.25
if abs(euler_pitch) < euler_pitch_max and abs(euler_yaw) < euler_yaw_max and abs(euler_roll) < euler_roll_max and single_face_quality_info['face_qality_flag'] == 1:
single_face_quality_info['quality_score'] = (image_spot_weight*image_SpotScore + image_clarity_weight * image_ClarityScore + image_brightness_weight * image_BrightnessScore + euler_pitch_weight * image_PitchScore + euler_yaw_weight* image_YawScore)
else:
single_face_quality_info['quality_score'] = 0
return single_face_quality_info
@@ -0,0 +1,75 @@
import os
import pickle
import torch
from core.faceseg.u2net import U2NET
from core.utils import landmark_processor
import cv2
import numpy as np
class FaceSeg:
def __init__(self, gpu_id = 0):
model = U2NET(in_ch=4, out_ch=1)
weights = torch.load('weights/faceseg_20210927_01.pth', map_location='cpu')
model_dict = model.state_dict()
pretrained_dict = {}
for ix, (k, v) in enumerate(model_dict.items()):
if k in weights and weights[k].data.shape == v.data.shape:
pretrained_dict[k] = weights[k]
else:
print('ignore {}'.format(k))
model_dict.update(pretrained_dict)
model.load_state_dict(model_dict)
print('update success')
model.cuda(gpu_id)
model.eval()
self.model = model
self.last_mask = None
self.output_img_size = 320
self.gpu_id = gpu_id
def inference(self, frame, pt1k, video_mode = False):
image_to_face_mat = landmark_processor.get_transform_mat_full_face(pt1k, self.output_img_size)
face_image = cv2.warpAffine(frame, image_to_face_mat, (self.output_img_size, self.output_img_size), flags=cv2.INTER_LANCZOS4)
if face_image.dtype == np.uint8: face_image = face_image.astype(np.float32) / 255
if video_mode and self.last_mask is not None:
last_small_mask = cv2.warpAffine(self.last_mask, image_to_face_mat,
(self.output_img_size, self.output_img_size), flags=cv2.INTER_LANCZOS4)[:,:,np.newaxis]
input_img = np.concatenate([face_image, last_small_mask], axis=2)
else:
zero_mask = np.zeros((face_image.shape[1], face_image.shape[0], 1), dtype=np.float32)
input_img = np.concatenate([face_image, zero_mask], axis=2)
face_image_tensor = input_img.transpose((2, 0, 1))[np.newaxis]
face_image_tensor = torch.from_numpy(face_image_tensor).cuda(self.gpu_id)
with torch.no_grad():
mask = self.model.test(face_image_tensor)
mask = mask[0].detach().cpu().numpy().transpose((1, 2, 0))
origin_mask = cv2.warpAffine(mask, image_to_face_mat, (frame.shape[1], frame.shape[0]),
flags=cv2.WARP_INVERSE_MAP|cv2.INTER_LANCZOS4)[:, :, np.newaxis]
if video_mode: self.last_mask = origin_mask.copy()
return origin_mask
if __name__ == '__main__':
face_segmentor = FaceSeg(gpu_id=0)
testdata_dir = "/home/yangchaojie/Desktop/faceswap_hd/datasets/origin/8171"
for picname in os.listdir(testdata_dir):
img_path = os.path.join(testdata_dir, picname)
pkl_path = img_path[:-4]+".pkl"
if not picname.endswith(".jpg"):
continue
if not os.path.exists(pkl_path):
continue
img = cv2.imread(img_path)
with open(pkl_path, "rb") as fp:
info = pickle.load(fp)
pt1k = info["pt1k"]
face_seg_mask = face_segmentor.inference(img, pt1k, video_mode=False)
# cv2.imshow("face_seg_mask", face_seg_mask)
# cv2.imshow("img", img)
# cv2.waitKey()
+299
View File
@@ -0,0 +1,299 @@
from __future__ import division
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.nn.init as init
import torch.utils.model_zoo as model_zoo
from torchvision import models
# general libs
import cv2
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
import math
import time
import tqdm
import os
import argparse
import copy
import sys
from utils.helpers import *
class ResBlock(nn.Module):
def __init__(self, backbone, indim, outdim=None, stride=1):
super(ResBlock, self).__init__()
self.backbone = backbone
if outdim == None:
outdim = indim
if indim == outdim and stride == 1:
self.downsample = None
else:
self.downsample = nn.Conv2d(indim, outdim, kernel_size=3, padding=1, stride=stride)
self.conv1 = nn.Conv2d(indim, outdim, kernel_size=3, padding=1, stride=stride)
self.conv2 = nn.Conv2d(outdim, outdim, kernel_size=3, padding=1)
def forward(self, x):
if self.backbone == 'resnest101':
r = self.conv1(F.relu(x, inplace=True))
r = self.conv2(F.relu(r, inplace=True))
else:
r = self.conv1(F.relu(x))
r = self.conv2(F.relu(r))
if self.downsample is not None:
x = self.downsample(x)
return x + r
class Encoder_M(nn.Module):
def __init__(self, backbone):
super(Encoder_M, self).__init__()
if backbone == 'resnest101':
self.conv1_m = nn.Conv2d(1, 128, kernel_size=7, stride=2, padding=3, bias=False)
self.conv1_o = nn.Conv2d(1, 128, kernel_size=7, stride=2, padding=3, bias=False)
else:
self.conv1_m = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.conv1_o = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
if backbone == 'resnet50':
resnet = models.resnet50(pretrained=True)
elif backbone == 'resnet18':
resnet = models.resnet18(pretrained=True)
self.conv1 = resnet.conv1
self.bn1 = resnet.bn1
self.relu = resnet.relu # 1/2, 64
self.maxpool = resnet.maxpool
self.res2 = resnet.layer1 # 1/4, 256
self.res3 = resnet.layer2 # 1/8, 512
self.res4 = resnet.layer3 # 1/8, 1024
self.register_buffer('mean', torch.FloatTensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
self.register_buffer('std', torch.FloatTensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
def forward(self, in_f, in_m, in_o):
f = (in_f - self.mean) / self.std
m = torch.unsqueeze(in_m, dim=1).float() # add channel dim
o = torch.unsqueeze(in_o, dim=1).float() # add channel dim
x = self.conv1(f) + self.conv1_m(m) + self.conv1_o(o)
x = self.bn1(x)
c1 = self.relu(x) # 1/2, 64
x = self.maxpool(c1) # 1/4, 64
r2 = self.res2(x) # 1/4, 256
r3 = self.res3(r2) # 1/8, 512
r4 = self.res4(r3) # 1/8, 1024
return r4, r3, r2, c1, f
class Encoder_Q(nn.Module):
def __init__(self, backbone):
super(Encoder_Q, self).__init__()
if backbone == 'resnet50':
resnet = models.resnet50(pretrained=True)
elif backbone == 'resnet18':
resnet = models.resnet18(pretrained=True)
self.conv1 = resnet.conv1
self.bn1 = resnet.bn1
self.relu = resnet.relu # 1/2, 64
self.maxpool = resnet.maxpool
self.res2 = resnet.layer1 # 1/4, 256
self.res3 = resnet.layer2 # 1/8, 512
self.res4 = resnet.layer3 # 1/8, 1024
self.register_buffer('mean', torch.FloatTensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
self.register_buffer('std', torch.FloatTensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
def forward(self, in_f):
f = (in_f - self.mean) / self.std
x = self.conv1(f)
x = self.bn1(x)
c1 = self.relu(x) # 1/2, 64
x = self.maxpool(c1) # 1/4, 64
r2 = self.res2(x) # 1/4, 256
r3 = self.res3(r2) # 1/8, 512
r4 = self.res4(r3) # 1/8, 1024
return r4, r3, r2, c1, f
class Refine(nn.Module):
def __init__(self, backbone, inplanes, planes, scale_factor=2):
super(Refine, self).__init__()
self.convFS = nn.Conv2d(inplanes, planes, kernel_size=(3, 3), padding=(1, 1), stride=1)
self.ResFS = ResBlock(backbone, planes, planes)
self.ResMM = ResBlock(backbone, planes, planes)
self.scale_factor = scale_factor
def forward(self, f, pm):
s = self.ResFS(self.convFS(f))
m = s + F.interpolate(pm, scale_factor=self.scale_factor, mode='bilinear', align_corners=False)
m = self.ResMM(m)
return m
class Decoder(nn.Module):
def __init__(self, mdim, scale_rate, backbone):
super(Decoder, self).__init__()
self.backbone = backbone
if backbone == 'resnest101':
self.convFM = nn.Conv2d(256, mdim, kernel_size=(3, 3), padding=(1, 1), stride=1)
else:
self.convFM = nn.Conv2d(1024 // scale_rate, mdim, kernel_size=(3, 3), padding=(1, 1), stride=1)
self.ResMM = ResBlock(backbone, mdim, mdim)
self.RF3 = Refine(backbone, 512 // scale_rate, mdim) # 1/8 -> 1/4
self.RF2 = Refine(backbone, 256 // scale_rate, mdim) # 1/4 -> 1
self.pred2 = nn.Conv2d(mdim, 2, kernel_size=(3, 3), padding=(1, 1), stride=1)
def forward(self, r4, r3, r2):
m4 = self.ResMM(self.convFM(r4))
m3 = self.RF3(r3, m4) # out: 1/8, 256
m2 = self.RF2(r2, m3) # out: 1/4, 256
if self.backbone == 'resnest101':
p2 = self.pred2(F.relu(m2, inplace=True))
else:
p2 = self.pred2(F.relu(m2))
p = F.interpolate(p2, scale_factor=4, mode='bilinear', align_corners=False)
return p # , p2, p3, p4
class Memory(nn.Module):
def __init__(self):
super(Memory, self).__init__()
def forward(self, m_in, m_out, q_in, q_out): # m_in: o,c,t,h,w
B, D_e, T, H, W = m_in.size()
_, D_o, _, _, _ = m_out.size()
mi = m_in.view(B, D_e, T * H * W)
mi = torch.transpose(mi, 1, 2) # b, THW, emb
qi = q_in.view(B, D_e, H * W) # b, emb, HW
p = torch.bmm(mi, qi) # b, THW, HW
p = p / math.sqrt(D_e)
p = F.softmax(p, dim=1) # b, THW, HW
mo = m_out.view(B, D_o, T * H * W)
mem = torch.bmm(mo, p) # Weighted-sum B, D_o, HW
mem = mem.view(B, D_o, H, W)
mem_out = torch.cat([mem, q_out], dim=1)
return mem_out, p
class KeyValue(nn.Module):
# Not using location
def __init__(self, indim, keydim, valdim):
super(KeyValue, self).__init__()
self.Key = nn.Conv2d(indim, keydim, kernel_size=(3, 3), padding=(1, 1), stride=1)
self.Value = nn.Conv2d(indim, valdim, kernel_size=(3, 3), padding=(1, 1), stride=1)
def forward(self, x):
return self.Key(x), self.Value(x)
class STM(nn.Module):
def __init__(self, backbone='resnet50'):
super(STM, self).__init__()
self.backbone = backbone
assert backbone == 'resnet50' or backbone == 'resnet18' or backbone == 'resnest101'
scale_rate = (1 if (backbone == 'resnet50' or backbone == 'resnest101') else 4)
self.Encoder_M = Encoder_M(backbone)
self.Encoder_Q = Encoder_Q(backbone)
self.KV_M_r4 = KeyValue(1024 // scale_rate, keydim=128 // scale_rate, valdim=512 // scale_rate)
self.KV_Q_r4 = KeyValue(1024 // scale_rate, keydim=128 // scale_rate, valdim=512 // scale_rate)
self.Memory = Memory()
self.Decoder = Decoder(256, scale_rate, backbone)
def Pad_memory(self, mems, num_objects, K):
pad_mems = []
for mem in mems:
pad_mem = ToCuda(torch.zeros(1, K, mem.size()[1], 1, mem.size()[2], mem.size()[3]))
pad_mem[0, 1:num_objects + 1, :, 0] = mem
pad_mems.append(pad_mem)
return pad_mems
def memorize(self, frame, masks, num_objects):
# memorize a frame
num_objects = num_objects[0].item()
_, K, H, W = masks.shape # B = 1
(frame, masks), pad = pad_divide_by([frame, masks], 16, (frame.size()[2], frame.size()[3]))
# make batch arg list
B_list = {'f': [], 'm': [], 'o': []}
for o in range(1, num_objects + 1): # 1 - no
B_list['f'].append(frame)
B_list['m'].append(masks[:, o])
B_list['o'].append((torch.sum(masks[:, 1:o], dim=1) + \
torch.sum(masks[:, o + 1:num_objects + 1], dim=1)).clamp(0, 1))
# make Batch
B_ = {}
for arg in B_list.keys():
B_[arg] = torch.cat(B_list[arg], dim=0)
r4, _, _, _, _ = self.Encoder_M(B_['f'], B_['m'], B_['o'])
k4, v4 = self.KV_M_r4(r4) # num_objects, 128 and 512, H/16, W/16
k4, v4 = self.Pad_memory([k4, v4], num_objects=num_objects, K=K)
return k4, v4
def Soft_aggregation(self, ps, K):
num_objects, H, W = ps.shape
em = ToCuda(torch.zeros(1, K, H, W))
em[0, 0] = torch.prod(1 - ps, dim=0) # bg prob
em[0, 1:num_objects + 1] = ps # obj prob
em = torch.clamp(em, 1e-7, 1 - 1e-7)
logit = torch.log((em / (1 - em)))
return logit
def segment(self, frame, keys, values, num_objects):
num_objects = num_objects[0].item()
_, K, keydim, T, H, W = keys.shape # B = 1
# pad
[frame], pad = pad_divide_by([frame], 16, (frame.size()[2], frame.size()[3]))
r4, r3, r2, _, _ = self.Encoder_Q(frame)
k4, v4 = self.KV_Q_r4(r4) # 1, dim, H/16, W/16
# expand to --- no, c, h, w
k4e, v4e = k4.expand(num_objects, -1, -1, -1), v4.expand(num_objects, -1, -1, -1)
r3e, r2e = r3.expand(num_objects, -1, -1, -1), r2.expand(num_objects, -1, -1, -1)
# memory select kv:(1, K, C, T, H, W)
m4, viz = self.Memory(keys[0, 1:num_objects + 1], values[0, 1:num_objects + 1], k4e, v4e)
logits = self.Decoder(m4, r3e, r2e)
ps = F.softmax(logits, dim=1)[:, 1] # no, h, w
# ps = indipendant possibility to belong to each object
logit = self.Soft_aggregation(ps, K) # 1, K, H, W
if pad[2] + pad[3] > 0:
logit = logit[:, :, pad[2]:-pad[3], :]
if pad[0] + pad[1] > 0:
logit = logit[:, :, :, pad[0]:-pad[1]]
return logit
def forward(self, *args, **kwargs):
if args[1].dim() > 4: # keys
return self.segment(*args, **kwargs)
else:
return self.memorize(*args, **kwargs)
+185
View File
@@ -0,0 +1,185 @@
import torch
import torch.nn.functional as F
from torch import nn
import numpy as np
class SequenceConv(nn.ModuleList):
"""Sequence conv module.
Args:
in_channels (int): input tensor channel.
out_channels (int): output tensor channel.
kernel_size (int): convolution kernel size.
sequence_num (int): sequence length.
conv_cfg (dict): convolution config dictionary.
norm_cfg (dict): normalization config dictionary.
act_cfg (dict): activation config dictionary.
"""
def __init__(self, in_channels, out_channels, kernel_size, sequence_num):
super(SequenceConv, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = kernel_size
self.sequence_num = sequence_num
for _ in range(sequence_num):
self.append(
nn.Sequential(
nn.Conv2d(self.in_channels, self.out_channels, self.kernel_size, 1, self.kernel_size // 2, bias=False),
nn.BatchNorm2d(self.out_channels),
nn.ReLU()
)
)
def forward(self, sequence_imgs):
"""
Args:
sequence_imgs (Tensor): TxBxCxHxW
Returns:
sequence conv output: TxBxCxHxW
"""
sequence_outs = []
assert sequence_imgs.shape[0] == self.sequence_num
for i, sequence_conv in enumerate(self):
sequence_out = sequence_conv(sequence_imgs[i, ...])
sequence_out = sequence_out.unsqueeze(0)
sequence_outs.append(sequence_out)
sequence_outs = torch.cat(sequence_outs, dim=0) # TxBxCxHxW
return sequence_outs
class MemoryModule(nn.Module):
"""Memory read module.
Args:
"""
def __init__(self,
matmul_norm=False):
super(MemoryModule, self).__init__()
self.matmul_norm = matmul_norm
def forward(self, memory_keys, memory_values, query_key, query_value):
"""
Memory Module forward.
Args:
memory_keys (Tensor): memory keys tensor, shape: TxBxCxHxW
memory_values (Tensor): memory values tensor, shape: TxBxCxHxW
query_key (Tensor): query keys tensor, shape: BxCxHxW
query_value (Tensor): query values tensor, shape: BxCxHxW
Returns:
Concat query and memory tensor.
"""
sequence_num, batch_size, key_channels, height, width = memory_keys.shape
_, _, value_channels, _, _ = memory_values.shape
assert query_key.shape[1] == key_channels and query_value.shape[1] == value_channels
memory_keys = memory_keys.permute(1, 2, 0, 3, 4).contiguous() # BxCxTxHxW
memory_keys = memory_keys.view(batch_size, key_channels, sequence_num * height * width) # BxCxT*H*W
query_key = query_key.view(batch_size, key_channels, height * width).permute(0, 2, 1).contiguous() # BxH*WxCk
key_attention = torch.bmm(query_key, memory_keys) # BxH*WxT*H*W
if self.matmul_norm:
key_attention = (key_channels ** -.5) * key_attention
key_attention = F.softmax(key_attention, dim=-1) # BxH*WxT*H*W
memory_values = memory_values.permute(1, 2, 0, 3, 4).contiguous() # BxCxTxHxW
memory_values = memory_values.view(batch_size, value_channels, sequence_num * height * width)
memory_values = memory_values.permute(0, 2, 1).contiguous() # BxT*H*WxC
memory = torch.bmm(key_attention, memory_values) # BxH*WxC
memory = memory.permute(0, 2, 1).contiguous() # BxCxH*W
memory = memory.view(batch_size, value_channels, height, width) # BxCxHxW
query_memory = torch.cat([query_value, memory], dim=1)
return query_memory
#
# class TMAHead(nn.Module):
# """TMAHead decoder for video semantic segmentation."""
#
# def __init__(self, sequence_num, key_channels, value_channels, num_classes=2, dropout_ratio=0):
# super(TMAHead, self).__init__()
#
# self.sequence_num = sequence_num
# self.memory_key_conv = nn.Sequential(
# SequenceConv(self.in_channels, key_channels, 1, sequence_num),
# SequenceConv(key_channels, key_channels, 3, sequence_num)
# )
# self.memory_value_conv = nn.Sequential(
# SequenceConv(self.in_channels, value_channels, 1, sequence_num),
# SequenceConv(value_channels, value_channels, 3, sequence_num)
# )
# self.query_key_conv = nn.Sequential(
# nn.Sequential(
# nn.Conv2d(self.in_channels, key_channels, 1, 1, 0, bias=False),
# nn.BatchNorm2d(key_channels),
# nn.ReLU()
# ),
# nn.Sequential(
# nn.Conv2d(key_channels, key_channels, 3, 1, 1, bias=False),
# nn.BatchNorm2d(key_channels),
# nn.ReLU()
# ),
# )
#
# self.query_value_conv = nn.Sequential(
# nn.Sequential(
# nn.Conv2d(self.in_channels, value_channels, 1, 1, 0, bias=False),
# nn.BatchNorm2d(value_channels),
# nn.ReLU()
# ),
# nn.Sequential(
# nn.Conv2d(value_channels, value_channels, 3, 1, 1, bias=False),
# nn.BatchNorm2d(value_channels),
# nn.ReLU()
# ),
# )
# self.memory_module = MemoryModule(matmul_norm=False)
# self.bottleneck = nn.Sequential(
# nn.Conv2d(value_channels * 2, self.channels, 3, 1, 1, bias=False),
# nn.BatchNorm2d(value_channels),
# nn.ReLU()
# )
#
# self.conv_seg = nn.Conv2d(self.channels, num_classes, kernel_size=1)
# if dropout_ratio > 0:
# self.dropout = nn.Dropout2d(dropout_ratio)
# else:
# self.dropout = None
#
# def cls_seg(self, feat):
# """Classify each pixel."""
# if self.dropout is not None:
# feat = self.dropout(feat)
# output = self.conv_seg(feat)
# return output
#
# def forward(self, inputs, sequence_imgs):
# """
# Forward fuction.
# Args:
# inputs (list[Tensor]): backbone multi-level outputs.
# sequence_imgs (list[Tensor]): len(sequence_imgs) is equal to batch_size,
# each element is a Tensor with shape of TxCxHxW.
#
# Returns:
# decoder logits.
# """
# x = inputs
# sequence_imgs = [y.unsqueeze(0) for y in sequence_imgs] # T, BxCxHxW
# sequence_imgs = torch.cat(sequence_imgs, dim=0) # TxBxCxHxW
# sequence_num, batch_size, channels, height, width = sequence_imgs.shape
#
# assert sequence_num == self.sequence_num
# memory_keys = self.memory_key_conv(sequence_imgs)
# memory_values = self.memory_value_conv(sequence_imgs)
# query_key = self.query_key_conv(x) # BxCxHxW
# query_value = self.query_value_conv(x) # BxCxHxW
#
# # memory read
# output = self.memory_module(memory_keys, memory_values, query_key, query_value)
# output = self.bottleneck(output)
# output = self.cls_seg(output)
#
# return output
@@ -0,0 +1,624 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class REBNCONV(nn.Module):
def __init__(self,in_ch=3,out_ch=3,dirate=1):
super(REBNCONV,self).__init__()
self.conv_s1 = nn.Conv2d(in_ch,out_ch,3,padding=1*dirate,dilation=1*dirate)
self.bn_s1 = nn.BatchNorm2d(out_ch)
self.relu_s1 = nn.ReLU(inplace=True)
def forward(self,x):
hx = x
xout = self.relu_s1(self.bn_s1(self.conv_s1(hx)))
return xout
## upsample tensor 'src' to have the same spatial size with tensor 'tar'
def _upsample_like(src,tar):
src = F.upsample(src,size=tar.shape[2:],mode='bilinear')
return src
### RSU-7 ###
class RSU7(nn.Module):#UNet07DRES(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU7,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool5 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv7 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv6d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx = self.pool3(hx3)
hx4 = self.rebnconv4(hx)
hx = self.pool4(hx4)
hx5 = self.rebnconv5(hx)
hx = self.pool5(hx5)
hx6 = self.rebnconv6(hx)
hx7 = self.rebnconv7(hx6)
hx6d = self.rebnconv6d(torch.cat((hx7,hx6),1))
hx6dup = _upsample_like(hx6d,hx5)
hx5d = self.rebnconv5d(torch.cat((hx6dup,hx5),1))
hx5dup = _upsample_like(hx5d,hx4)
hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-6 ###
class RSU6(nn.Module):#UNet06DRES(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU6,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx = self.pool3(hx3)
hx4 = self.rebnconv4(hx)
hx = self.pool4(hx4)
hx5 = self.rebnconv5(hx)
hx6 = self.rebnconv6(hx5)
hx5d = self.rebnconv5d(torch.cat((hx6,hx5),1))
hx5dup = _upsample_like(hx5d,hx4)
hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-5 ###
class RSU5(nn.Module):#UNet05DRES(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU5,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx = self.pool3(hx3)
hx4 = self.rebnconv4(hx)
hx5 = self.rebnconv5(hx4)
hx4d = self.rebnconv4d(torch.cat((hx5,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-4 ###
class RSU4(nn.Module):#UNet04DRES(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU4,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx4 = self.rebnconv4(hx3)
hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-4F ###
class RSU4F(nn.Module):#UNet04FRES(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU4F,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=4)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=8)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=4)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=2)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx2 = self.rebnconv2(hx1)
hx3 = self.rebnconv3(hx2)
hx4 = self.rebnconv4(hx3)
hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1))
hx2d = self.rebnconv2d(torch.cat((hx3d,hx2),1))
hx1d = self.rebnconv1d(torch.cat((hx2d,hx1),1))
return hx1d + hxin
from core.faceseg.tma import SequenceConv, MemoryModule
##### U^2-Net ####
class U2NET(nn.Module):
def __init__(self, in_ch=3, out_ch=1):
super(U2NET, self).__init__()
self.stage1 = RSU7(in_ch,32,64)
self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage2 = RSU6(64,32,128)
self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage3 = RSU5(128,64,256)
self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage4 = RSU4(256,128,512)
self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage5 = RSU4F(512,256,512)
self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage6 = RSU4F(512,256,512)
# decoder
self.stage5d = RSU4F(1024,256,512)
self.stage4d = RSU4(1024,128,256)
self.stage3d = RSU5(512,64,128)
self.stage2d = RSU6(256,32,64)
self.stage1d = RSU7(128,16,64)
self.side1 = nn.Conv2d(64,out_ch,3,padding=1)
self.side2 = nn.Conv2d(64,out_ch,3,padding=1)
self.side3 = nn.Conv2d(128,out_ch,3,padding=1)
self.side4 = nn.Conv2d(256,out_ch,3,padding=1)
self.side5 = nn.Conv2d(512,out_ch,3,padding=1)
self.side6 = nn.Conv2d(512,out_ch,3,padding=1)
self.outconv = nn.Conv2d(6*out_ch,out_ch,1)
self.in_channels = 512
key_channels = 128
value_channels = 512
self.sequence_num = sequence_num = 2
self.memory_key_conv = nn.Sequential(
SequenceConv(self.in_channels, key_channels, 1, sequence_num),
SequenceConv(key_channels, key_channels, 3, sequence_num)
)
self.memory_value_conv = nn.Sequential(
SequenceConv(self.in_channels, value_channels, 1, sequence_num),
SequenceConv(value_channels, value_channels, 3, sequence_num)
)
self.query_key_conv = nn.Sequential(
nn.Sequential(
nn.Conv2d(self.in_channels, key_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(key_channels),
nn.ReLU()
),
nn.Sequential(
nn.Conv2d(key_channels, key_channels, 3, 1, 1, bias=False),
nn.BatchNorm2d(key_channels),
nn.ReLU()
),
)
self.query_value_conv = nn.Sequential(
nn.Sequential(
nn.Conv2d(self.in_channels, value_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(value_channels),
nn.ReLU()
),
nn.Sequential(
nn.Conv2d(value_channels, value_channels, 3, 1, 1, bias=False),
nn.BatchNorm2d(value_channels),
nn.ReLU()
),
)
self.memory_module = MemoryModule(matmul_norm=False)
self.bottleneck = nn.Sequential(
nn.Conv2d(value_channels * 2, self.in_channels, 3, 1, 1, bias=False),
nn.BatchNorm2d(value_channels),
nn.ReLU()
)
self.is_train = True
def extract_feature(self, x):
hx = x
# stage 1
hx1 = self.stage1(hx)
hx = self.pool12(hx1)
# stage 2
hx2 = self.stage2(hx)
hx = self.pool23(hx2)
# stage 3
hx3 = self.stage3(hx)
hx = self.pool34(hx3)
# stage 4
hx4 = self.stage4(hx)
hx = self.pool45(hx4)
# stage 5
hx5 = self.stage5(hx)
hx = self.pool56(hx5)
# stage 6
hx6 = self.stage6(hx)
return hx1, hx2, hx3, hx4, hx5, hx6
def decoder(self, hx1, hx2, hx3, hx4, hx5, hx6):
hx6up = _upsample_like(hx6, hx5)
# -------------------- decoder --------------------
hx5d = self.stage5d(torch.cat((hx6up, hx5), 1))
hx5dup = _upsample_like(hx5d, hx4)
hx4d = self.stage4d(torch.cat((hx5dup, hx4), 1))
hx4dup = _upsample_like(hx4d, hx3)
hx3d = self.stage3d(torch.cat((hx4dup, hx3), 1))
hx3dup = _upsample_like(hx3d, hx2)
hx2d = self.stage2d(torch.cat((hx3dup, hx2), 1))
hx2dup = _upsample_like(hx2d, hx1)
hx1d = self.stage1d(torch.cat((hx2dup, hx1), 1))
return hx1d, hx2d, hx3d, hx4d, hx5d
def side_output(self, hx1d, hx2d, hx3d, hx4d, hx5d, hx6):
# side output
d1 = self.side1(hx1d)
d2 = self.side2(hx2d)
d2 = _upsample_like(d2, d1)
d3 = self.side3(hx3d)
d3 = _upsample_like(d3, d1)
d4 = self.side4(hx4d)
d4 = _upsample_like(d4, d1)
d5 = self.side5(hx5d)
d5 = _upsample_like(d5, d1)
d6 = self.side6(hx6)
d6 = _upsample_like(d6, d1)
d0 = self.outconv(torch.cat((d1, d2, d3, d4, d5, d6), 1))
return F.sigmoid(d0), F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6)
def forward(self, x, memory_sequence=None):
if self.is_train:
hx1, hx2, hx3, hx4, hx5, hx6 = self.extract_feature(x)
if memory_sequence is None:
memory_hx6 = [hx6 for _ in range(self.sequence_num)]
else:
memory_hx6 = []
for single_memory in memory_sequence:
_, _, _, _, _, hx6 = self.extract_feature(single_memory)
memory_hx6.append(hx6)
memory_hx6 = [mhx6.unsqueeze(0) for mhx6 in memory_hx6] # T, BxCxHxW
memory_hx6 = torch.cat(memory_hx6, dim=0)
memory_keys = self.memory_key_conv(memory_hx6)
memory_values = self.memory_value_conv(memory_hx6)
query_key = self.query_key_conv(hx6)
query_value = self.query_value_conv(hx6)
merge_hx6 = self.memory_module(memory_keys, memory_values, query_key, query_value)
merge_hx6 = self.bottleneck(merge_hx6)
hx1d, hx2d, hx3d, hx4d, hx5d = self.decoder(hx1, hx2, hx3, hx4, hx5, merge_hx6)
return self.side_output(hx1d, hx2d, hx3d, hx4d, hx5d, hx6)
else:
return self.test(x)
def test(self, x):
with torch.no_grad():
hx1, hx2, hx3, hx4, hx5, hx6 = self.extract_feature(x)
memory_hx6 = [hx6 for _ in range(self.sequence_num)]
memory_hx6 = [mhx6.unsqueeze(0) for mhx6 in memory_hx6] # T, BxCxHxW
memory_hx6 = torch.cat(memory_hx6, dim=0)
memory_keys = self.memory_key_conv(memory_hx6)
memory_values = self.memory_value_conv(memory_hx6)
query_key = self.query_key_conv(hx6)
query_value = self.query_value_conv(hx6)
merge_hx6 = self.memory_module(memory_keys, memory_values, query_key, query_value)
merge_hx6 = self.bottleneck(merge_hx6)
hx1d, hx2d, hx3d, hx4d, hx5d = self.decoder(hx1, hx2, hx3, hx4, hx5, merge_hx6)
mask, _, _, _, _, _, _ = self.side_output(hx1d, hx2d, hx3d, hx4d, hx5d, hx6)
return mask
#
# ### U^2-Net small ###
# class U2NETP(nn.Module):
#
# def __init__(self,in_ch=3,out_ch=1):
# super(U2NETP,self).__init__()
#
# self.stage1 = RSU7(in_ch,16,64)
# self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
#
# self.stage2 = RSU6(64,16,64)
# self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
#
# self.stage3 = RSU5(64,16,64)
# self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
#
# self.stage4 = RSU4(64,16,64)
# self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
#
# self.stage5 = RSU4F(64,16,64)
# self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
#
# self.stage6 = RSU4F(64,16,64)
#
# # decoder
# self.stage5d = RSU4F(128,16,64)
# self.stage4d = RSU4(128,16,64)
# self.stage3d = RSU5(128,16,64)
# self.stage2d = RSU6(128,16,64)
# self.stage1d = RSU7(128,16,64)
#
# self.side1 = nn.Conv2d(64,out_ch,3,padding=1)
# self.side2 = nn.Conv2d(64,out_ch,3,padding=1)
# self.side3 = nn.Conv2d(64,out_ch,3,padding=1)
# self.side4 = nn.Conv2d(64,out_ch,3,padding=1)
# self.side5 = nn.Conv2d(64,out_ch,3,padding=1)
# self.side6 = nn.Conv2d(64,out_ch,3,padding=1)
#
# self.outconv = nn.Conv2d(6*out_ch,out_ch,1)
#
# def forward(self,x):
#
# hx = x
#
# #stage 1
# hx1 = self.stage1(hx)
# hx = self.pool12(hx1)
#
# #stage 2
# hx2 = self.stage2(hx)
# hx = self.pool23(hx2)
#
# #stage 3
# hx3 = self.stage3(hx)
# hx = self.pool34(hx3)
#
# #stage 4
# hx4 = self.stage4(hx)
# hx = self.pool45(hx4)
#
# #stage 5
# hx5 = self.stage5(hx)
# hx = self.pool56(hx5)
#
# #stage 6
# hx6 = self.stage6(hx)
# hx6up = _upsample_like(hx6,hx5)
#
# #decoder
# hx5d = self.stage5d(torch.cat((hx6up,hx5),1))
# hx5dup = _upsample_like(hx5d,hx4)
#
# hx4d = self.stage4d(torch.cat((hx5dup,hx4),1))
# hx4dup = _upsample_like(hx4d,hx3)
#
# hx3d = self.stage3d(torch.cat((hx4dup,hx3),1))
# hx3dup = _upsample_like(hx3d,hx2)
#
# hx2d = self.stage2d(torch.cat((hx3dup,hx2),1))
# hx2dup = _upsample_like(hx2d,hx1)
#
# hx1d = self.stage1d(torch.cat((hx2dup,hx1),1))
#
#
# #side output
# d1 = self.side1(hx1d)
#
# d2 = self.side2(hx2d)
# d2 = _upsample_like(d2,d1)
#
# d3 = self.side3(hx3d)
# d3 = _upsample_like(d3,d1)
#
# d4 = self.side4(hx4d)
# d4 = _upsample_like(d4,d1)
#
# d5 = self.side5(hx5d)
# d5 = _upsample_like(d5,d1)
#
# d6 = self.side6(hx6)
# d6 = _upsample_like(d6,d1)
#
# d0 = self.outconv(torch.cat((d1,d2,d3,d4,d5,d6),1))
#
# return F.sigmoid(d0), F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6)
@@ -0,0 +1,210 @@
import cv2
import numpy as np
import torch
import json
import os
import os.path as osp
from common.logger import LogFactory
from core.process_modules import Get_Landmark, Process_Data, localtranslationwarpfastwithstrength,\
Generator_Hair, chinClass, Generator_Fusion_Res, Change_Hair_Color, GenderClassifyProcessor, BodySeg, \
localtranslationwarpfastwithstrength_v2, localtranslationwarpfastwithstrength_v2_soft, updateEndPosition
from core.face_enhance.face_enhancement import FaceEnhancement
from datetime import datetime
import random
from core.utils import landmark_processor
from core.cos_module import COS_object as OSS_object
from core.faceseg.face_seg import FaceSeg
from common.logger import config
class Prepare_Ref_HairColor_Data(object):
def __init__(self, gpu, device_id):
if gpu and torch.cuda.is_available():
self.device = torch.device("cuda:%d" % device_id if device_id >= 0 else "cpu")
self.cuda = True
else:
self.device = torch.device("cpu")
self.cuda = False
self.process_data = Process_Data(gpu, device_id)
self.get_landmark = Get_Landmark(gpu_id=device_id)
self.color_output_size = 768
def get_prepare_ref_haircolor_768_data(self, ref_rgb_8uc3_orisize):
ref_landmark_1k2_f_orisize, _, _ = self.get_landmark.forward(ref_rgb_8uc3_orisize)
ref_matte_fg_8uc3_orisize, ref_matte_pred_8uc1_orisize, _ = self.process_data.generator_matte.matte_inference(ref_rgb_8uc3_orisize, ref_landmark_1k2_f_orisize)
# 光头分割
ref_baldseg_8uc3_orisize = self.process_data.generator_baldseg.forward(ref_rgb_8uc3_orisize, ref_matte_pred_8uc1_orisize,
ref_landmark_1k2_f_orisize)
color_hair_M = landmark_processor.get_transform_mat_hair_ratio_v1(ref_landmark_1k2_f_orisize, self.color_output_size, ratio=0.35, h_offset=0.45)
ref_matte_pred_8uc3_orisize = np.repeat(ref_matte_pred_8uc1_orisize[:, :, np.newaxis], 3, axis=2)
# show_concat = np.concatenate((ref_rgb_8uc3_orisize, ref_matte_pred_8uc3_orisize, ref_baldseg_8uc3_orisize), axis=1)
# show_concat = cv2.resize(show_concat, (0, 0), fx=0.5, fy=0.5)
# cv2.imshow("show_concat", show_concat)
# cv2.waitKey()
ref_rgb_8uc3_768 = cv2.warpAffine(ref_rgb_8uc3_orisize, color_hair_M, (self.color_output_size, self.color_output_size))
ref_matte_pred_8uc3_768 = cv2.warpAffine(ref_matte_pred_8uc3_orisize, color_hair_M, (self.color_output_size, self.color_output_size))
ref_baldseg_8uc3_768 = cv2.warpAffine(ref_baldseg_8uc3_orisize, color_hair_M, (self.color_output_size, self.color_output_size), flags=cv2.INTER_NEAREST)
ref_landmark_1k2_f_768 = landmark_processor.transform_points(ref_landmark_1k2_f_orisize, color_hair_M)
return ref_rgb_8uc3_768, ref_matte_pred_8uc3_768, ref_baldseg_8uc3_768, ref_landmark_1k2_f_768
class Prepare_Ref_HairStyle_Data(object):
def __init__(self, gpu, device_id):
if gpu and torch.cuda.is_available():
self.device = torch.device("cuda:%d" % device_id if device_id >= 0 else "cpu")
self.cuda = True
else:
self.device = torch.device("cpu")
self.cuda = False
self.process_data = Process_Data(gpu, device_id)
self.get_landmark = Get_Landmark(gpu_id=device_id)
self.gender_classify = GenderClassifyProcessor(gpu_id=device_id)
def get_prepare_ref_768_color_data(self, ref_rgb_8uc3_orisize):
ref_landmark_1k2_f_orisize = self.get_landmark.forward(ref_rgb_8uc3_orisize)
ref_rgb_8uc3_color_768, ref_matting_8uc3_color_768, ref_baldseg_8uc3_color_768, ref_landmark_f1k2_color_768 = \
self.process_data.get_prepare_ref_768_bald_data(ref_rgb_8uc3_orisize, ref_landmark_1k2_f_orisize)
return ref_rgb_8uc3_color_768, ref_matting_8uc3_color_768, ref_baldseg_8uc3_color_768, ref_landmark_f1k2_color_768
def get_prepare_ref_768_color_data_landmark1k(self, ref_rgb_8uc3_orisize,ref_landmark_1k2_f_orisize):
# ref_landmark_1k2_f_orisize = self.get_landmark.inference(ref_rgb_8uc3_orisize)
ref_rgb_8uc3_color_768, ref_matting_8uc3_color_768, ref_baldseg_8uc3_color_768, ref_landmark_f1k2_color_768 = \
self.process_data.get_prepare_ref_768_bald_data(ref_rgb_8uc3_orisize, ref_landmark_1k2_f_orisize)
return ref_rgb_8uc3_color_768, ref_matting_8uc3_color_768, ref_baldseg_8uc3_color_768, ref_landmark_f1k2_color_768
def calculate_hair_ratio_after_align(self, hair_mask, origin_landmark1k, img_size=768):
image_to_face_mat = landmark_processor.get_transform_mat_hair_ratio_v1(origin_landmark1k, 768, ratio=0.35, h_offset=0.32)
hair_mask_align = cv2.warpAffine(hair_mask, image_to_face_mat, (img_size, img_size))
hair_rect = cv2.boundingRect(hair_mask_align[:, :, :1])
hair_mask_ratio = hair_rect[2] * hair_rect[3] / (img_size * img_size)
return hair_mask_ratio
def check_female_hair_ratio(self, origin_img_8uc3, landmark_1k2_f_orisize):
ref_matte_fg_8uc3_orisize, ref_matte_pred_8uc1_orisize, _ = self.process_data.generator_matte.matte_inference(origin_img_8uc3, landmark_1k2_f_orisize)
ref_matte_pred_8uc3_orisize = np.repeat(ref_matte_pred_8uc1_orisize[:, :, np.newaxis], 3, axis=2)
hairstyle_M = self.process_data.get_hair_M_girl_v1(landmark_1k2_f_orisize)
ref_rgb_8uc3_768 = cv2.warpAffine(ref_matte_pred_8uc3_orisize, hairstyle_M, (768, 768))
# cv2.imshow("ref_rgb_8uc3_768", ref_rgb_8uc3_768)
# cv2.waitKey()
edge_width = 5
if (ref_rgb_8uc3_768[-edge_width:, :, :]).max() > 0 or (ref_rgb_8uc3_768[:, -edge_width:, :]).max() > 0 or (ref_rgb_8uc3_768[:, :edge_width, :]).max() > 0:
return False
else:
return True
def get_prepare_ref_768_data(self, ref_rgb_8uc3_orisize):
ref_landmark_1k2_f_orisize, _, _ = self.get_landmark.forward(ref_rgb_8uc3_orisize)
# for i in range(1000):
# cv2.circle(ref_rgb_8uc3_orisize, (int(ref_landmark_1k2_f_orisize[i][0]),int(ref_landmark_1k2_f_orisize[i][1])), 1,(255, 0,0), 1)
# cv2.imshow('ffff', ref_rgb_8uc3_orisize)
# cv2.waitKey()
# ref_matte_fg_8uc3_orisize, ref_matte_pred_8uc1_orisize = self.process_data.generator_matte.matte_inference(
# ref_rgb_8uc3_orisize, ref_landmark_1k2_f_orisize)
ref_landmark_137kpts_f_orisize = landmark_processor.pts_1k_to_137(ref_landmark_1k2_f_orisize)
gender_res = self.gender_classify.forward(ref_rgb_8uc3_orisize, ref_landmark_137kpts_f_orisize)
# hair_ratio = self.calculate_hair_ratio_after_align(ref_rgb_8uc3_orisize, ref_landmark_1k2_f_orisize)
# if hair_ratio > 0.3:
# ratio = 2
# else:
# if gender_res:
# ratio = 1
# else:
# ratio = 0
if not gender_res:
ratio = 0
else:
check_res = self.check_female_hair_ratio(ref_rgb_8uc3_orisize, ref_landmark_137kpts_f_orisize)
if check_res:
ratio = 1
else:
ratio = 2
if gender_res:
gender = "girl"
else:
gender = "boy"
# show_concat = np.concatenate((ref_rgb_8uc3_orisize, ref_matte_fg_8uc3_orisize), axis=1)
# resize_ratio = 1024. / max(show_concat.shape[:2])
# show_concat = cv2.resize(show_concat, (0, 0), fx=resize_ratio, fy=resize_ratio)
# print("gender_res: ", gender_res, " hair_ratio: ", hair_ratio)
# cv2.imshow("show_concat", show_concat)
# cv2.imshow("ref_matte_pred_8uc1_orisize", ref_matte_pred_8uc1_orisize)
# cv2.waitKey()
ref_rgb_8uc3_768, ref_matting_fg_8uc3_768, ref_matting_8uc3_768, ref_baldseg_8uc3_768, ref_landmark_f1k2_768 = \
self.process_data.get_prepare_ref_768_data(ref_rgb_8uc3_orisize, ref_landmark_1k2_f_orisize, ratio)
# cv2.imshow("ref_rgb_8uc3_768", ref_rgb_8uc3_768)
# cv2.waitKey()
return ref_rgb_8uc3_768, ref_matting_fg_8uc3_768, ref_matting_8uc3_768, ref_baldseg_8uc3_768, ref_landmark_f1k2_768, gender, ratio
def Generator_reftensor(self, ref_rgb_8uc3_768, ref_matting_8uc3_768, ref_baldseg_8uc3_768,
ref_landmark_f1k2_768):
"""
input
图像尺寸基于 人脸 512 图像均为3通道
ref_rgb_8uc3_512: 参考图 size 512 uint8 0-255
ref_matting_8uc3_512参考图 matting alpha uint8 0-255
ref_baldseg_8uc3_512: 参考图 光头分割 uint8 0-255
ref_landmark_f1k2_512 参考图 关键点 1k*2 float32
output
input_another_pose_hair_image: 参考图 条件图 float32 0-255
"""
# 8 ********************** another pose hair_image **********************
another_pose_image = ref_rgb_8uc3_768.copy()
another_nohair_pose_mask = ref_baldseg_8uc3_768.copy()
# cv2.imshow("another_nohair_pose_mask", another_nohair_pose_mask)
# cv2.waitKey()
another_nohair_pose_mask[(np.abs(another_nohair_pose_mask - [0, 0, 255]) < 50).all(axis=2)] = [0, 255, 255]
another_nohair_pose_mask[(another_nohair_pose_mask == [0, 0, 0]).all(axis=2)] = [255, 255, 255]
another_pose_pts137 = landmark_processor.pts_1k_to_137(ref_landmark_f1k2_768).astype(np.int32)
cv2.fillPoly(another_nohair_pose_mask,
np.concatenate((another_pose_pts137[47:35:-1], another_pose_pts137[56:64],
[another_pose_pts137[48], another_pose_pts137[22]]))[
np.newaxis, :, :], (255, 255, 0))
cv2.fillPoly(another_nohair_pose_mask,
np.concatenate((another_pose_pts137[22:37], another_pose_pts137[56:47:-1]))[np.newaxis, :, :],
(255, 0, 128))
cv2.fillPoly(another_nohair_pose_mask, another_pose_pts137[88:104][np.newaxis, :, :], (255, 0, 255)) # eye
cv2.fillPoly(another_nohair_pose_mask, another_pose_pts137[105:121][np.newaxis, :, :], (255, 0, 255)) # eye
# Label nose
cv2.fillPoly(another_nohair_pose_mask, another_pose_pts137[64:79][np.newaxis, :, :], (255, 255, 255))
# Label eyebrow
cv2.fillPoly(another_nohair_pose_mask, another_pose_pts137[121:129][np.newaxis, :, :], (255, 128, 0))
cv2.fillPoly(another_nohair_pose_mask, another_pose_pts137[129:137][np.newaxis, :, :], (255, 128, 0))
another_pose_hair_alpha = ref_matting_8uc3_768.copy() / 255.
another_pose_hair_image = (another_pose_image * another_pose_hair_alpha + another_nohair_pose_mask * (
1 - another_pose_hair_alpha)).astype(np.uint8)
input_another_pose_hair_image = another_pose_hair_image.astype(np.float32) / 255
return input_another_pose_hair_image
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,208 @@
import os
import cv2
import argparse
import numpy as np
import torch
from torch.nn import functional as F
import utils
from core.matting import networks
from utils.data_preprocess import *
from time import time
def single_inference(model, image_dict, device, return_offset=False):
with torch.no_grad():
image, trimap = image_dict['image'], image_dict['trimap']
alpha_shape = image_dict['alpha_shape']
image = image.to(device)
trimap = trimap.to(device)
matte_start = time()
alpha_pred, info_dict = model(image, trimap)
matte_end = time()
# print("matte time cost : {:.4f}".format(matte_end-matte_start))
trimap_argmax = trimap.argmax(dim=1, keepdim=True)
alpha_pred[trimap_argmax == 2] = 1
alpha_pred[trimap_argmax == 0] = 0
h, w = alpha_shape
test_pred = alpha_pred[0, 0, ...].detach().cpu().numpy() * 255
test_pred = test_pred.astype(np.uint8)
test_pred = test_pred[32:h+32, 32:w+32]
if return_offset:
short_side = h if h < w else w
ratio = 512 / short_side
offset_1 = utils.flow_to_image(info_dict['offset_1'][0][0, ...].data.cpu().numpy()).astype(np.uint8)
# write softmax_scale to offset image
scale = info_dict['offset_1'][1].cpu()
offset_1 = cv2.resize(offset_1, (int(w * ratio), int(h * ratio)), interpolation=cv2.INTER_NEAREST)
text = 'unknown: {:.2f}, known: {:.2f}'.format(scale[-1, 0].item(), scale[-1,1].item())
offset_1 = cv2.putText(offset_1, text, (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, 0, thickness=2)
offset_2 = utils.flow_to_image(info_dict['offset_2'][0][0, ...].data.cpu().numpy()).astype(np.uint8)
# write softmax_scale to offset image
scale = info_dict['offset_2'][1].cpu()
offset_2 = cv2.resize(offset_2, (int(w * ratio), int(h * ratio)), interpolation=cv2.INTER_NEAREST)
text = 'unknown: {:.2f}, known: {:.2f}'.format(scale[-1, 0].item(), scale[-1,1].item())
offset_2 = cv2.putText(offset_2, text, (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, 0, thickness=2)
return test_pred, (offset_1, offset_2)
else:
return test_pred, None
def generator_tensor_dict(image, trimap):
sample = {'image': image, 'trimap': trimap, 'alpha_shape': trimap.shape}
# reshape
h, w = sample["alpha_shape"]
if h % 32 == 0 and w % 32 == 0:
padded_image = np.pad(sample['image'], ((32, 32), (32, 32), (0, 0)), mode="reflect")
padded_trimap = np.pad(sample['trimap'], ((32, 32), (32, 32)), mode="reflect")
sample['image'] = padded_image
sample['trimap'] = padded_trimap
else:
target_h = 32 * ((h - 1) // 32 + 1)
target_w = 32 * ((w - 1) // 32 + 1)
pad_h = target_h - h
pad_w = target_w - w
padded_image = np.pad(sample['image'], ((32, pad_h+32), (32, pad_w+32), (0, 0)), mode="reflect")
padded_trimap = np.pad(sample['trimap'], ((32, pad_h+32), (32, pad_w+32)), mode="reflect")
sample['image'] = padded_image
sample['trimap'] = padded_trimap
# ImageNet mean & std
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
# convert GBR images to RGB
image, trimap = sample['image'][:, :, ::-1], sample['trimap']
# swap color axis
image = image.transpose((2, 0, 1)).astype(np.float32)
trimap[trimap < 85] = 0
trimap[trimap >= 170] = 2
trimap[trimap >= 85] = 1
# normalize image
image /= 255.
# to tensor
sample['image'], sample['trimap'] = torch.from_numpy(image), torch.from_numpy(trimap).to(torch.long)
sample['image'] = sample['image'].sub_(mean).div_(std)
# if CONFIG.model.trimap_channel == 3:
sample['trimap'] = F.one_hot(sample['trimap'], num_classes=3).permute(2, 0, 1).float()
# elif CONFIG.model.trimap_channel == 1:
# sample['trimap'] = sample['trimap'][None, ...].float()
# else:
# raise NotImplementedError("CONFIG.model.trimap_channel can only be 3 or 1")
# add first channel
sample['image'], sample['trimap'] = sample['image'][None, ...], sample['trimap'][None, ...]
return sample
if __name__ == '__main__':
print('Torch Version: ', torch.__version__)
parser = argparse.ArgumentParser()
parser.add_argument('--checkpoint', type=str, default='/home/liyang/project/matting/GCA-Matting/checkpoints/gca-dist-align-truth-1101/best_model.pth',
help="path of checkpoint")
parser.add_argument('--image-dir', type=str, default='/home/liyang/project/matting/合格/origin', help="input image dir")
parser.add_argument('--trimap-dir', type=str, default='/home/liyang/project/matting/合格/trimap', help="input trimap dir")
parser.add_argument('--output', type=str, default='/home/liyang/project/matting/合格/pred6', help="output dir")
# Parse configuration
args = parser.parse_args()
# # Check if toml config file is loaded
# if CONFIG.is_default:
# raise ValueError("No .toml config loaded.")
args.output = os.path.join(args.output, args.checkpoint.split('/')[-1])
utils.make_dir(args.output)
# build model
model = networks.get_generator(encoder="resnet_gca_encoder_29", decoder="res_gca_decoder_22", num_class=1)
model.cuda()
print("model: ", model)
# load checkpoint
checkpoint = torch.load(args.checkpoint)
model.load_state_dict(utils.remove_prefix_state_dict(checkpoint['state_dict']), strict=True)
# inference
model = model.eval()
for image_name in os.listdir(args.image_dir):
if not is_image_file(image_name):
continue
# assume image and trimap have the same file name
img_basename, ext = os.path.splitext(image_name)
image_path = os.path.join(args.image_dir, image_name)
trimap_path = os.path.join(args.trimap_dir, image_name.replace(ext, "_alpha.png"))
# trimap_path = os.path.join(args.trimap_dir, image_name)
print('Image: ', image_path, ' Tirmap: ', trimap_path)
# read images
img_basename, img_ext = os.path.splitext(image_name)
# img_pt_path = image_path.replace(img_ext, "_landmark1k.txt")
# img_landmark1k = np.loadtxt(img_pt_path)
image = cv2.imread(image_path)
trimap = cv2.imread(trimap_path, 0)
ori_h, ori_w, _ = image.shape
tri_h, tri_w = trimap.shape
if image.shape[1] != trimap.shape[1] or image.shape[0] != trimap.shape[0]:
image = cv2.resize(image, (trimap.shape[1], trimap.shape[0]), interpolation=cv2.INTER_CUBIC)
# img_landmark1k = landmark_processor.resize_points(img_landmark1k, ori_w, ori_h,
# tri_w, tri_h)
# hair_mat = landmark_processor.get_transform_mat_hair(img_landmark1k, 640, ratio=0.3, w_ratio=0.5,
# h_ratio=0.4)
# hair_img_landmark = landmark_processor.transform_points(img_landmark1k, hair_mat)
#
# image = cv2.warpAffine(image, hair_mat, (640, 640), flags=cv2.INTER_CUBIC)
# trimap = cv2.warpAffine(trimap, hair_mat, (640, 640), flags=cv2.INTER_CUBIC)
if tri_h > 1920 or tri_w > 1920:
if tri_h > 1920:
new_tri_h = 1920
new_tri_w = int(tri_w * 1920 / tri_h)
else:
new_tri_w = 1920
new_tri_h = int(tri_h * 1920 / tri_w)
image_resize = cv2.resize(image, (new_tri_w, new_tri_h), interpolation=cv2.INTER_CUBIC)
trimap_resize = cv2.resize(trimap, (new_tri_w, new_tri_h), interpolation=cv2.INTER_NEAREST)
else:
image_resize = image
trimap_resize = trimap
# image_dict = generator_tensor_dict(image, trimap)
image_dict = generator_tensor_dict(image_resize, trimap_resize)
pred, offset = single_inference(model, image_dict)
# torch.cuda.empty_cache()
pred = cv2.resize(pred, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_CUBIC)
# pred = cv2.warpAffine(pred, cv2.invertAffineTransform(hair_mat), (tri_w, tri_h), flags=cv2.INTER_CUBIC)
# offset[0] = cv2.resize(offset[0], (image.shape[1], image.shape[0]), interpolation=cv2.INTER_NEAREST)
# offset[1] = cv2.resize(offset[1], (image.shape[1], image.shape[0]), interpolation=cv2.INTER_NEAREST)
# cv2.imshow("image_resize", image_resize)
# cv2.imshow("trimap_resize", trimap_resize)
# cv2.imshow("pred", pred)
# cv2.waitKey()
cv2.imwrite(os.path.join(args.output, image_name.replace(img_ext, "_noalign.png")), pred)
# if offset is not None:
# cv2.imwrite(os.path.join(args.output, os.path.splitext(image_name)[0]+'_offset1.png'), offset[0])
# cv2.imwrite(os.path.join(args.output, os.path.splitext(image_name)[0]+'_offset2.png'), offset[1])
@@ -0,0 +1,200 @@
import os
import cv2
import argparse
import numpy as np
import torch
from torch.nn import functional as F
import utils
from core.matting import networks
from utils.data_preprocess import *
from time import time
def single_inference(model, image_dict, device, return_offset=False):
with torch.no_grad():
image, trimap = image_dict['image'], image_dict['trimap']
alpha_shape = image_dict['alpha_shape']
image = image.to(device)
trimap = trimap.to(device)
matte_start = time()
alpha_pred, info_dict = model(image, trimap)
fg_pred = alpha_pred[:, :-1, :, :]
alpha_pred = alpha_pred[:, -1, :, :].unsqueeze(1)
matte_end = time()
# print("matte time cost : {:.4f}".format(matte_end-matte_start))
trimap_argmax = trimap.argmax(dim=1, keepdim=True)
alpha_pred[trimap_argmax == 2] = 1
alpha_pred[trimap_argmax == 0] = 0
h, w = alpha_shape
test_fg_pred = fg_pred[0].detach().cpu().numpy().transpose(1, 2, 0)[:, :, ::-1] * 255
test_fg_pred = test_fg_pred.astype(np.uint8)
test_fg_pred = test_fg_pred[32:h+32, 32:w+32]
test_pred = alpha_pred[0, 0, ...].detach().cpu().numpy() * 255
test_pred = test_pred.astype(np.uint8)
test_pred = test_pred[32:h+32, 32:w+32]
# cv2.imshow('test_fg_pred', test_fg_pred)
# cv2.imshow('test_pred', test_pred)
# cv2.waitKey()
if return_offset:
short_side = h if h < w else w
ratio = 512 / short_side
offset_1 = utils.flow_to_image(info_dict['offset_1'][0][0, ...].data.cpu().numpy()).astype(np.uint8)
# write softmax_scale to offset image
scale = info_dict['offset_1'][1].cpu()
offset_1 = cv2.resize(offset_1, (int(w * ratio), int(h * ratio)), interpolation=cv2.INTER_NEAREST)
text = 'unknown: {:.2f}, known: {:.2f}'.format(scale[-1, 0].item(), scale[-1,1].item())
offset_1 = cv2.putText(offset_1, text, (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, 0, thickness=2)
offset_2 = utils.flow_to_image(info_dict['offset_2'][0][0, ...].data.cpu().numpy()).astype(np.uint8)
# write softmax_scale to offset image
scale = info_dict['offset_2'][1].cpu()
offset_2 = cv2.resize(offset_2, (int(w * ratio), int(h * ratio)), interpolation=cv2.INTER_NEAREST)
text = 'unknown: {:.2f}, known: {:.2f}'.format(scale[-1, 0].item(), scale[-1,1].item())
offset_2 = cv2.putText(offset_2, text, (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, 0, thickness=2)
return test_fg_pred, test_pred, (offset_1, offset_2)
else:
return test_fg_pred, test_pred, None
def generator_tensor_dict(image, trimap):
sample = {'image': image, 'trimap': trimap, 'alpha_shape': trimap.shape}
# reshape
h, w = sample["alpha_shape"]
if h % 32 == 0 and w % 32 == 0:
padded_image = np.pad(sample['image'], ((32, 32), (32, 32), (0, 0)), mode="reflect")
padded_trimap = np.pad(sample['trimap'], ((32, 32), (32, 32)), mode="reflect")
sample['image'] = padded_image
sample['trimap'] = padded_trimap
else:
target_h = 32 * ((h - 1) // 32 + 1)
target_w = 32 * ((w - 1) // 32 + 1)
pad_h = target_h - h
pad_w = target_w - w
padded_image = np.pad(sample['image'], ((32, pad_h+32), (32, pad_w+32), (0, 0)), mode="reflect")
padded_trimap = np.pad(sample['trimap'], ((32, pad_h+32), (32, pad_w+32)), mode="reflect")
sample['image'] = padded_image
sample['trimap'] = padded_trimap
# ImageNet mean & std
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
# convert GBR images to RGB
image, trimap = sample['image'][:, :, ::-1], sample['trimap']
# swap color axis
image = image.transpose((2, 0, 1)).astype(np.float32)
trimap[trimap < 85] = 0
trimap[trimap >= 170] = 2
trimap[trimap >= 85] = 1
# normalize image
image /= 255.
# to tensor
sample['image'], sample['trimap'] = torch.from_numpy(image), torch.from_numpy(trimap).to(torch.long)
sample['image'] = sample['image'].sub_(mean).div_(std)
sample['trimap'] = F.one_hot(sample['trimap'], num_classes=3).permute(2, 0, 1).float()
# add first channel
sample['image'], sample['trimap'] = sample['image'][None, ...], sample['trimap'][None, ...]
return sample
if __name__ == '__main__':
print('Torch Version: ', torch.__version__)
parser = argparse.ArgumentParser()
parser.add_argument('--checkpoint', type=str, default='/home/liyang/project/matting/GCA-Matting/checkpoints/gca-dist-align-truth-1101/best_model.pth',
help="path of checkpoint")
parser.add_argument('--image-dir', type=str, default='/home/liyang/project/matting/合格/origin', help="input image dir")
parser.add_argument('--trimap-dir', type=str, default='/home/liyang/project/matting/合格/trimap', help="input trimap dir")
parser.add_argument('--output', type=str, default='/home/liyang/project/matting/合格/pred6', help="output dir")
# Parse configuration
args = parser.parse_args()
# # Check if toml config file is loaded
# if CONFIG.is_default:
# raise ValueError("No .toml config loaded.")
args.output = os.path.join(args.output, args.checkpoint.split('/')[-1])
utils.make_dir(args.output)
# build model
model = networks.get_generator(encoder=CONFIG.model.arch.encoder, decoder=CONFIG.model.arch.decoder)
model.cuda()
print("model: ", model)
# load checkpoint
checkpoint = torch.load(args.checkpoint)
model.load_state_dict(utils.remove_prefix_state_dict(checkpoint['state_dict']), strict=True)
# inference
model = model.eval()
export_onnx_file = "test.onnx"
torch.onnx.export(model, x, export_onnx_file, opset_version=10, do_constant_folding=True, input_names=["image", "trimap"], # 输入名
output_names=["fg_pred", "alpha_pred", "None"])
for image_name in os.listdir(args.image_dir):
if not is_image_file(image_name):
continue
# assume image and trimap have the same file name
img_basename, ext = os.path.splitext(image_name)
image_path = os.path.join(args.image_dir, image_name)
trimap_path = os.path.join(args.trimap_dir, image_name.replace(ext, "_alpha.png"))
# trimap_path = os.path.join(args.trimap_dir, image_name)
print('Image: ', image_path, ' Tirmap: ', trimap_path)
# read images
img_basename, img_ext = os.path.splitext(image_name)
image = cv2.imread(image_path)
trimap = cv2.imread(trimap_path, 0)
ori_h, ori_w, _ = image.shape
tri_h, tri_w = trimap.shape
if image.shape[1] != trimap.shape[1] or image.shape[0] != trimap.shape[0]:
image = cv2.resize(image, (trimap.shape[1], trimap.shape[0]), interpolation=cv2.INTER_CUBIC)
if tri_h > 1920 or tri_w > 1920:
if tri_h > 1920:
new_tri_h = 1920
new_tri_w = int(tri_w * 1920 / tri_h)
else:
new_tri_w = 1920
new_tri_h = int(tri_h * 1920 / tri_w)
image_resize = cv2.resize(image, (new_tri_w, new_tri_h), interpolation=cv2.INTER_CUBIC)
trimap_resize = cv2.resize(trimap, (new_tri_w, new_tri_h), interpolation=cv2.INTER_NEAREST)
else:
image_resize = image
trimap_resize = trimap
# image_dict = generator_tensor_dict(image, trimap)
image_dict = generator_tensor_dict(image_resize, trimap_resize)
fg_pred, pred, offset = single_inference(model, image_dict)
# torch.cuda.empty_cache()
fg_pred = cv2.resize(fg_pred, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_CUBIC)
pred = cv2.resize(pred, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_CUBIC)
cv2.imwrite(os.path.join(args.output, image_name.replace(img_ext, "_noalign.png")), pred)
cv2.imwrite(os.path.join(args.output, image_name.replace(img_ext, "_fg_p.png")), fg_pred)
@@ -0,0 +1 @@
from .generators import *
@@ -0,0 +1,28 @@
from .resnet_dec import ResNet_D_Dec, BasicBlock
from .res_shortcut_dec import ResShortCut_D_Dec
from .res_gca_dec import ResGuidedCxtAtten_Dec
__all__ = ['res_shortcut_decoder_22', 'res_gca_decoder_22']
def _res_shortcut_D_dec(block, layers, **kwargs):
model = ResShortCut_D_Dec(block, layers, **kwargs)
return model
def _res_gca_D_dec(block, layers, num_class, **kwargs):
model = ResGuidedCxtAtten_Dec(block, layers, num_class, **kwargs)
return model
def res_shortcut_decoder_22(**kwargs):
"""Constructs a resnet_encoder_14 model.
"""
return _res_shortcut_D_dec(BasicBlock, [2, 3, 3, 2], **kwargs)
def res_gca_decoder_22(num_class=1, **kwargs):
"""Constructs a resnet_encoder_14 model.
"""
return _res_gca_D_dec(BasicBlock, [2, 3, 3, 2], num_class, **kwargs)
@@ -0,0 +1,28 @@
from core.matting.networks.ops import GuidedCxtAtten
from core.matting.networks.decoders.res_shortcut_dec import ResShortCut_D_Dec
class ResGuidedCxtAtten_Dec(ResShortCut_D_Dec):
def __init__(self, block, layers, num_class=1, norm_layer=None, large_kernel=False):
super(ResGuidedCxtAtten_Dec, self).__init__(block, layers, num_class, norm_layer, large_kernel)
self.gca = GuidedCxtAtten(128, 128)
self.num_class = num_class
def forward(self, x, mid_fea):
fea1, fea2, fea3, fea4, fea5 = mid_fea['shortcut']
im = mid_fea['image_fea']
x = self.layer1(x) + fea5 # N x 256 x 32 x 32
x = self.layer2(x) + fea4 # N x 128 x 64 x 64
x, offset = self.gca(im, x, mid_fea['unknown']) # contextual attention
x = self.layer3(x) + fea3 # N x 64 x 128 x 128
x = self.layer4(x) + fea2 # N x 32 x 256 x 256
x = self.conv1(x)
x = self.bn1(x)
x = self.leaky_relu(x) + fea1
x = self.conv2(x)
alpha = (self.tanh(x) + 1.0) / 2.0
return alpha, {'offset_1': mid_fea['offset_1'], 'offset_2': offset}
@@ -0,0 +1,24 @@
from core.matting.networks.decoders.resnet_dec import ResNet_D_Dec
class ResShortCut_D_Dec(ResNet_D_Dec):
def __init__(self, block, layers, num_class=1, norm_layer=None, large_kernel=False, late_downsample=False):
super(ResShortCut_D_Dec, self).__init__(block, layers, num_class, norm_layer, large_kernel,
late_downsample=late_downsample)
def forward(self, x, mid_fea):
fea1, fea2, fea3, fea4, fea5 = mid_fea['shortcut']
x = self.layer1(x) + fea5
x = self.layer2(x) + fea4
x = self.layer3(x) + fea3
x = self.layer4(x) + fea2
x = self.conv1(x)
x = self.bn1(x)
x = self.leaky_relu(x) + fea1
x = self.conv2(x)
alpha = (self.tanh(x) + 1.0) / 2.0
return alpha, None
@@ -0,0 +1,142 @@
import logging
import torch.nn as nn
from core.matting.networks.ops import SpectralNorm
def conv5x5(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""5x5 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=5, stride=stride,
padding=2, groups=groups, bias=False, dilation=dilation)
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, upsample=None, norm_layer=None, large_kernel=False):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self.stride = stride
conv = conv5x5 if large_kernel else conv3x3
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
if self.stride > 1:
self.conv1 = SpectralNorm(nn.ConvTranspose2d(inplanes, inplanes, kernel_size=4, stride=2, padding=1, bias=False))
else:
self.conv1 = SpectralNorm(conv(inplanes, inplanes))
self.bn1 = norm_layer(inplanes)
self.activation = nn.LeakyReLU(0.2, inplace=True)
self.conv2 = SpectralNorm(conv(inplanes, planes))
self.bn2 = norm_layer(planes)
self.upsample = upsample
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.activation(out)
out = self.conv2(out)
out = self.bn2(out)
if self.upsample is not None:
identity = self.upsample(x)
out += identity
out = self.activation(out)
return out
class ResNet_D_Dec(nn.Module):
def __init__(self, block, layers, num_class=1, norm_layer=None, large_kernel=False, late_downsample=False):
super(ResNet_D_Dec, self).__init__()
self.logger = logging.getLogger("Logger")
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.large_kernel = large_kernel
self.kernel_size = 5 if self.large_kernel else 3
self.inplanes = 512 if layers[0] > 0 else 256
self.late_downsample = late_downsample
self.midplanes = 64 if late_downsample else 32
self.conv1 = SpectralNorm(nn.ConvTranspose2d(self.midplanes, 32, kernel_size=4, stride=2, padding=1, bias=False))
self.bn1 = norm_layer(32)
self.leaky_relu = nn.LeakyReLU(0.2, inplace=True)
self.conv2 = nn.Conv2d(32, num_class, kernel_size=self.kernel_size, stride=1, padding=self.kernel_size//2)
self.upsample = nn.UpsamplingNearest2d(scale_factor=2)
self.tanh = nn.Tanh()
self.layer1 = self._make_layer(block, 256, layers[0], stride=2)
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 64, layers[2], stride=2)
self.layer4 = self._make_layer(block, self.midplanes, layers[3], stride=2)
for m in self.modules():
if isinstance(m, nn.Conv2d):
if hasattr(m, "weight_bar"):
nn.init.xavier_uniform_(m.weight_bar)
else:
nn.init.xavier_uniform_(m.weight)
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
for m in self.modules():
if isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
self.logger.debug(self)
def _make_layer(self, block, planes, blocks, stride=1):
if blocks == 0:
return nn.Sequential(nn.Identity())
norm_layer = self._norm_layer
upsample = None
if stride != 1:
upsample = nn.Sequential(
nn.UpsamplingNearest2d(scale_factor=2),
SpectralNorm(conv1x1(self.inplanes, planes * block.expansion)),
norm_layer(planes * block.expansion),
)
elif self.inplanes != planes * block.expansion:
upsample = nn.Sequential(
SpectralNorm(conv1x1(self.inplanes, planes * block.expansion)),
norm_layer(planes * block.expansion),
)
layers = [block(self.inplanes, planes, stride, upsample, norm_layer, self.large_kernel)]
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, norm_layer=norm_layer, large_kernel=self.large_kernel))
return nn.Sequential(*layers)
def forward(self, x, mid_fea):
x = self.layer1(x) # N x 256 x 32 x 32
x = self.layer2(x) # N x 128 x 64 x 64
x = self.layer3(x) # N x 64 x 128 x 128
x = self.layer4(x) # N x 32 x 256 x 256
x = self.conv1(x)
x = self.bn1(x)
x = self.leaky_relu(x)
x = self.conv2(x)
alpha = (self.tanh(x) + 1.0) / 2.0
return alpha, None
@@ -0,0 +1,39 @@
import logging
from .resnet_enc import ResNet_D, BasicBlock
from .res_shortcut_enc import ResShortCut_D
from .res_gca_enc import ResGuidedCxtAtten
__all__ = ['res_shortcut_encoder_29', 'resnet_gca_encoder_29']
def _res_shortcut_D(block, layers, **kwargs):
model = ResShortCut_D(block, layers, **kwargs)
return model
def _res_gca_D(block, layers, **kwargs):
model = ResGuidedCxtAtten(block, layers, **kwargs)
return model
def resnet_gca_encoder_29(**kwargs):
"""Constructs a resnet_encoder_29 model.
"""
return _res_gca_D(BasicBlock, [3, 4, 4, 2], **kwargs)
def res_shortcut_encoder_29(**kwargs):
"""Constructs a resnet_encoder_25 model.
"""
return _res_shortcut_D(BasicBlock, [3, 4, 4, 2], **kwargs)
if __name__ == "__main__":
import torch
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%m-%d %H:%M:%S')
resnet_encoder = res_shortcut_encoder_29()
x = torch.randn(4,6,512,512)
z = resnet_encoder(x)
print(z[0].shape)
@@ -0,0 +1,97 @@
import torch.nn as nn
import torch.nn.functional as F
# from utils import CONFIG
from core.matting.networks.encoders.resnet_enc import ResNet_D
from core.matting.networks.ops import GuidedCxtAtten, SpectralNorm
class ResGuidedCxtAtten(ResNet_D):
def __init__(self, block, layers, norm_layer=None, late_downsample=False):
super(ResGuidedCxtAtten, self).__init__(block, layers, norm_layer, late_downsample=late_downsample)
first_inplane = 3 + 3
self.shortcut_inplane = [first_inplane, self.midplanes, 64, 128, 256]
self.shortcut_plane = [32, self.midplanes, 64, 128, 256]
self.shortcut = nn.ModuleList()
for stage, inplane in enumerate(self.shortcut_inplane):
self.shortcut.append(self._make_shortcut(inplane, self.shortcut_plane[stage]))
self.guidance_head = nn.Sequential(
nn.ReflectionPad2d(1),
SpectralNorm(nn.Conv2d(3, 16, kernel_size=3, padding=0, stride=2, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(16),
nn.ReflectionPad2d(1),
SpectralNorm(nn.Conv2d(16, 32, kernel_size=3, padding=0, stride=2, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(32),
nn.ReflectionPad2d(1),
SpectralNorm(nn.Conv2d(32, 128, kernel_size=3, padding=0, stride=2, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(128)
)
self.gca = GuidedCxtAtten(128, 128)
# initialize guidance head
for layers in range(len(self.guidance_head)):
m = self.guidance_head[layers]
if isinstance(m, nn.Conv2d):
if hasattr(m, "weight_bar"):
nn.init.xavier_uniform_(m.weight_bar)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_shortcut(self, inplane, planes):
return nn.Sequential(
SpectralNorm(nn.Conv2d(inplane, planes, kernel_size=3, padding=1, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(planes),
SpectralNorm(nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(planes)
)
def forward(self, x):
out = self.conv1(x)
out = self.bn1(out)
out = self.activation(out)
out = self.conv2(out)
out = self.bn2(out)
x1 = self.activation(out) # N x 32 x 256 x 256
out = self.conv3(x1)
out = self.bn3(out)
out = self.activation(out)
im_fea = self.guidance_head(x[:, :3, ...]) # downsample origin image and extract features
# if CONFIG.model.trimap_channel == 3:
unknown = F.interpolate(x[:, 4:5, ...], scale_factor=1/8, mode='nearest')
# else:
# unknown = F.interpolate(x[:,3:,...].eq(1.).float(), scale_factor=1/8, mode='nearest')
x2 = self.layer1(out) # N x 64 x 128 x 128
x3= self.layer2(x2) # N x 128 x 64 x 64
x3, offset = self.gca(im_fea, x3, unknown) # contextual attention
x4 = self.layer3(x3) # N x 256 x 32 x 32
out = self.layer_bottleneck(x4) # N x 512 x 16 x 16
fea1 = self.shortcut[0](x) # input image and trimap
fea2 = self.shortcut[1](x1)
fea3 = self.shortcut[2](x2)
fea4 = self.shortcut[3](x3)
fea5 = self.shortcut[4](x4)
return out, {'shortcut': (fea1, fea2, fea3, fea4, fea5),
'image_fea': im_fea,
'unknown': unknown,
'offset_1': offset}
if __name__ == "__main__":
from core.matting.networks.encoders.resnet_enc import BasicBlock
m = ResGuidedCxtAtten(BasicBlock, [3, 4, 4, 2])
for m in m.modules():
print(m)
@@ -0,0 +1,51 @@
import torch.nn as nn
# from utils import CONFIG
from core.matting.networks.encoders.resnet_enc import ResNet_D
from core.matting.networks.ops import SpectralNorm
class ResShortCut_D(ResNet_D):
def __init__(self, block, layers, norm_layer=None, late_downsample=False):
super(ResShortCut_D, self).__init__(block, layers, norm_layer, late_downsample=late_downsample)
first_inplane = 3 + 3
self.shortcut_inplane = [first_inplane, self.midplanes, 64, 128, 256]
self.shortcut_plane = [32, self.midplanes, 64, 128, 256]
self.shortcut = nn.ModuleList()
for stage, inplane in enumerate(self.shortcut_inplane):
self.shortcut.append(self._make_shortcut(inplane, self.shortcut_plane[stage]))
def _make_shortcut(self, inplane, planes):
return nn.Sequential(
SpectralNorm(nn.Conv2d(inplane, planes, kernel_size=3, padding=1, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(planes),
SpectralNorm(nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)),
nn.ReLU(inplace=True),
self._norm_layer(planes)
)
def forward(self, x):
out = self.conv1(x)
out = self.bn1(out)
out = self.activation(out)
out = self.conv2(out)
out = self.bn2(out)
x1 = self.activation(out) # N x 32 x 256 x 256
out = self.conv3(x1)
out = self.bn3(out)
out = self.activation(out)
x2 = self.layer1(out) # N x 64 x 128 x 128
x3= self.layer2(x2) # N x 128 x 64 x 64
x4 = self.layer3(x3) # N x 256 x 32 x 32
out = self.layer_bottleneck(x4) # N x 512 x 16 x 16
fea1 = self.shortcut[0](x) # input image and trimap
fea2 = self.shortcut[1](x1)
fea3 = self.shortcut[2](x2)
fea4 = self.shortcut[3](x3)
fea5 = self.shortcut[4](x4)
return out, {'shortcut':(fea1, fea2, fea3, fea4, fea5), 'image':x[:,:3,...]}
@@ -0,0 +1,150 @@
import logging
import torch.nn as nn
from core.matting.networks.ops import SpectralNorm
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = SpectralNorm(conv3x3(inplanes, planes, stride))
self.bn1 = norm_layer(planes)
self.activation = nn.ReLU(inplace=True)
self.conv2 = SpectralNorm(conv3x3(planes, planes))
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.activation(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.activation(out)
return out
class ResNet_D(nn.Module):
"""
Implement and pre-train on ImageNet with the tricks from
https://arxiv.org/abs/1812.01187
without the mix-up part.
"""
def __init__(self, block, layers, norm_layer=None, late_downsample=False):
super(ResNet_D, self).__init__()
self.logger = logging.getLogger("Logger")
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.inplanes = 64
self.late_downsample = late_downsample
self.midplanes = 64 if late_downsample else 32
self.start_stride = [1, 2, 1, 2] if late_downsample else [2, 1, 2, 1]
self.conv1 = SpectralNorm(nn.Conv2d(3 + 3, 32, kernel_size=3,
stride=self.start_stride[0], padding=1, bias=False))
self.conv2 = SpectralNorm(nn.Conv2d(32, self.midplanes, kernel_size=3, stride=self.start_stride[1], padding=1,
bias=False))
self.conv3 = SpectralNorm(nn.Conv2d(self.midplanes, self.inplanes, kernel_size=3, stride=self.start_stride[2],
padding=1, bias=False))
self.bn1 = norm_layer(32)
self.bn2 = norm_layer(self.midplanes)
self.bn3 = norm_layer(self.inplanes)
self.activation = nn.ReLU(inplace=True)
self.layer1 = self._make_layer(block, 64, layers[0], stride=self.start_stride[3])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer_bottleneck = self._make_layer(block, 512, layers[3], stride=2)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.xavier_uniform_(m.weight_bar)
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
for m in self.modules():
if isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
self.logger.debug("encoder conv1 weight shape: {}".format(str(self.conv1.module.weight_bar.data.shape)))
self.conv1.module.weight_bar.data[:,3:,:,:] = 0
self.logger.debug(self)
def _make_layer(self, block, planes, blocks, stride=1):
if blocks == 0:
return nn.Sequential(nn.Identity())
norm_layer = self._norm_layer
downsample = None
if stride != 1:
downsample = nn.Sequential(
nn.AvgPool2d(2, stride),
SpectralNorm(conv1x1(self.inplanes, planes * block.expansion)),
norm_layer(planes * block.expansion),
)
elif self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
SpectralNorm(conv1x1(self.inplanes, planes * block.expansion, stride)),
norm_layer(planes * block.expansion),
)
layers = [block(self.inplanes, planes, stride, downsample, norm_layer)]
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, norm_layer=norm_layer))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.activation(x)
x = self.conv2(x)
x = self.bn2(x)
x1 = self.activation(x) # N x 32 x 256 x 256
x = self.conv3(x1)
x = self.bn3(x)
x2 = self.activation(x) # N x 64 x 128 x 128
x3 = self.layer1(x2) # N x 64 x 128 x 128
x4 = self.layer2(x3) # N x 128 x 64 x 64
x5 = self.layer3(x4) # N x 256 x 32 x 32
x = self.layer_bottleneck(x5) # N x 512 x 16 x 16
return x, (x1, x2, x3, x4, x5)
if __name__ == "__main__":
m = ResNet_D(BasicBlock, [3, 4, 4, 2])
for m in m.modules():
print(m._get_name())
@@ -0,0 +1,60 @@
import torch
import torch.nn as nn
# from utils import CONFIG
from core.matting.networks import decoders, encoders
class Generator(nn.Module):
def __init__(self, encoder, decoder, num_class=1):
super(Generator, self).__init__()
if encoder not in encoders.__all__:
raise NotImplementedError("Unknown Encoder {}".format(encoder))
self.encoder = encoders.__dict__[encoder]()
if decoder not in decoders.__all__:
raise NotImplementedError("Unknown Decoder {}".format(decoder))
self.decoder = decoders.__dict__[decoder](num_class)
def forward(self, image, trimap):
inp = torch.cat((image, trimap), dim=1)
embedding, mid_fea = self.encoder(inp)
alpha, info_dict = self.decoder(embedding, mid_fea)
return alpha, info_dict
def get_generator(encoder, decoder, num_class=1):
generator = Generator(encoder=encoder, decoder=decoder, num_class=num_class)
return generator
if __name__=="__main__":
import time
# generator = get_generator(encoder=CONFIG.model.arch.encoder, decoder=CONFIG.model.arch.decoder).cuda().train()
batch_size = 12
# generator.eval()
n_eval = 10
# pre run the model
# with torch.no_grad():
# for i in range(2):
# x = torch.rand(batch_size, 3, 512, 512, device=device)
# y = torch.rand(batch_size, 3, 512, 512, device=device)
# z = generator(x,y)
# test without GPU IO
# x = torch.zeros(batch_size, 3, 512, 512, device=device)
# y = torch.zeros(batch_size, 1, 512, 512, device=device)
x = torch.randn(batch_size, 3, 512, 512)
y = torch.randn(batch_size, 3, 512, 512)
t = time.time()
# with torch.no_grad():
# for i in range(n_eval):
# a = generator(x.cuda(),y.cuda())
# torch.cuda.synchronize()
# print(generator.__class__.__name__, 'With IO \t', f'{(time.time() - t)/n_eval/batch_size:.5f} s')
# print(generator.__class__.__name__, 'FPS \t\t', f'{1/((time.time() - t)/n_eval/batch_size):.5f} s')
# for n, p in generator.named_parameters():
# print(n)
@@ -0,0 +1,256 @@
import torch
from torch import nn
from torch.nn import Parameter
from torch.autograd import Variable
from torch.nn import functional as F
def l2normalize(v, eps=1e-12):
return v / (v.norm() + eps)
class SpectralNorm(nn.Module):
"""
Based on https://github.com/heykeetae/Self-Attention-GAN/blob/master/spectral.py
and add _noupdate_u_v() for evaluation
"""
def __init__(self, module, name='weight', power_iterations=1):
super(SpectralNorm, self).__init__()
self.module = module
self.name = name
self.power_iterations = power_iterations
if not self._made_params():
self._make_params()
def _update_u_v(self):
u = getattr(self.module, self.name + "_u")
v = getattr(self.module, self.name + "_v")
w = getattr(self.module, self.name + "_bar")
height = w.data.shape[0]
for _ in range(self.power_iterations):
v.data = l2normalize(torch.mv(torch.t(w.view(height,-1).data), u.data))
u.data = l2normalize(torch.mv(w.view(height,-1).data, v.data))
sigma = u.dot(w.view(height, -1).mv(v))
setattr(self.module, self.name, w / sigma.expand_as(w))
def _noupdate_u_v(self):
u = getattr(self.module, self.name + "_u")
v = getattr(self.module, self.name + "_v")
w = getattr(self.module, self.name + "_bar")
height = w.data.shape[0]
sigma = u.dot(w.view(height, -1).mv(v))
setattr(self.module, self.name, w / sigma.expand_as(w))
def _made_params(self):
try:
u = getattr(self.module, self.name + "_u")
v = getattr(self.module, self.name + "_v")
w = getattr(self.module, self.name + "_bar")
return True
except AttributeError:
return False
def _make_params(self):
w = getattr(self.module, self.name)
height = w.data.shape[0]
width = w.view(height, -1).data.shape[1]
u = Parameter(w.data.new(height).normal_(0, 1), requires_grad=False)
v = Parameter(w.data.new(width).normal_(0, 1), requires_grad=False)
u.data = l2normalize(u.data)
v.data = l2normalize(v.data)
w_bar = Parameter(w.data)
del self.module._parameters[self.name]
self.module.register_parameter(self.name + "_u", u)
self.module.register_parameter(self.name + "_v", v)
self.module.register_parameter(self.name + "_bar", w_bar)
def forward(self, *args):
# if torch.is_grad_enabled() and self.module.training:
if self.module.training:
self._update_u_v()
else:
self._noupdate_u_v()
return self.module.forward(*args)
class GuidedCxtAtten(nn.Module):
# based on https://github.com/nbei/Deep-Flow-Guided-Video-Inpainting/blob/a6fe298fec502bfd9cbc64eb01e39f78a3262a59/models/DeepFill_Models/ops.py#L210
def __init__(self, out_channels, guidance_channels, rate=2):
super(GuidedCxtAtten, self).__init__()
self.rate = rate
self.padding = nn.ReflectionPad2d(1)
self.up_sample = nn.Upsample(scale_factor=self.rate, mode='nearest')
self.guidance_conv = nn.Conv2d(in_channels=guidance_channels, out_channels=guidance_channels//2,
kernel_size=1, stride=1, padding=0)
self.W = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=out_channels,
kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_channels)
)
nn.init.xavier_uniform_(self.guidance_conv.weight)
nn.init.constant_(self.guidance_conv.bias, 0)
nn.init.xavier_uniform_(self.W[0].weight)
nn.init.constant_(self.W[1].weight, 1e-3)
nn.init.constant_(self.W[1].bias, 0)
def forward(self, f, alpha, unknown=None, ksize=3, stride=1, fuse_k=3, softmax_scale=1., training=True):
f = self.guidance_conv(f)
# get shapes
raw_int_fs = list(f.size()) # N x 64 x 64 x 64
raw_int_alpha = list(alpha.size()) # N x 128 x 64 x 64
# extract patches from background with stride and rate
kernel = 2*self.rate
alpha_w = self.extract_patches(alpha, kernel=kernel, stride=self.rate)
alpha_w = alpha_w.permute(0, 2, 3, 4, 5, 1)
alpha_w = alpha_w.contiguous().view(raw_int_alpha[0], raw_int_alpha[2] // self.rate, raw_int_alpha[3] // self.rate, -1)
alpha_w = alpha_w.contiguous().view(raw_int_alpha[0], -1, kernel, kernel, raw_int_alpha[1])
alpha_w = alpha_w.permute(0, 1, 4, 2, 3)
f = F.interpolate(f, scale_factor=1/self.rate, mode='nearest')
fs = f.size() # B x 64 x 32 x 32
f_groups = torch.split(f, 1, dim=0) # Split tensors by batch dimension; tuple is returned
# from b(B*H*W*C) to w(b*k*k*c*h*w)
int_fs = list(fs)
w = self.extract_patches(f)
w = w.permute(0, 2, 3, 4, 5, 1)
w = w.contiguous().view(raw_int_fs[0], raw_int_fs[2] // self.rate, raw_int_fs[3] // self.rate, -1)
w = w.contiguous().view(raw_int_fs[0], -1, ksize, ksize, raw_int_fs[1])
w = w.permute(0, 1, 4, 2, 3)
# process mask
if unknown is not None:
unknown = unknown.clone()
unknown = F.interpolate(unknown, scale_factor=1/self.rate, mode='nearest')
assert unknown.size(2) == f.size(2), "mask should have same size as f at dim 2,3"
unknown_mean = unknown.mean(dim=[2,3])
known_mean = 1 - unknown_mean
unknown_scale = torch.clamp(torch.sqrt(unknown_mean / known_mean), 0.1, 10).to(alpha)
known_scale = torch.clamp(torch.sqrt(known_mean / unknown_mean), 0.1, 10).to(alpha)
softmax_scale = torch.cat([unknown_scale, known_scale], dim=1)
else:
unknown = torch.ones([fs[0], 1, fs[2], fs[3]]).to(alpha)
softmax_scale = torch.FloatTensor([softmax_scale, softmax_scale]).view(1,2).repeat(fs[0],1).to(alpha)
m = self.extract_patches(unknown)
m = m.permute(0, 2, 3, 4, 5, 1)
m = m.contiguous().view(raw_int_fs[0], raw_int_fs[2]//self.rate, raw_int_fs[3]//self.rate, -1)
m = m.contiguous().view(raw_int_fs[0], -1, ksize, ksize)
m = self.reduce_mean(m) # smoothing, maybe
# mask out the
mm = m.gt(0.).float() # (N, 32*32, 1, 1)
# the correlation with itself should be 0
self_mask = F.one_hot(torch.arange(fs[2] * fs[3]).view(fs[2], fs[3]).contiguous().to(alpha).long(),
num_classes=int_fs[2] * int_fs[3])
self_mask = self_mask.permute(2, 0, 1).view(1, fs[2] * fs[3], fs[2], fs[3]).float() * (-1e4)
w_groups = torch.split(w, 1, dim=0) # Split tensors by batch dimension; tuple is returned
alpha_w_groups = torch.split(alpha_w, 1, dim=0) # Split tensors by batch dimension; tuple is returned
mm_groups = torch.split(mm, 1, dim=0)
scale_group = torch.split(softmax_scale, 1, dim=0)
y = []
offsets = []
k = fuse_k
y_test = []
for xi, wi, alpha_wi, mmi, scale in zip(f_groups, w_groups, alpha_w_groups, mm_groups, scale_group):
# conv for compare
wi = wi[0]
escape_NaN = Variable(torch.FloatTensor([1e-4])).to(alpha)
wi_normed = wi / torch.max(self.l2_norm(wi), escape_NaN)
xi = F.pad(xi, (1,1,1,1), mode='reflect')
yi = F.conv2d(xi, wi_normed, stride=1, padding=0) # yi => (B=1, C=32*32, H=32, W=32)
y_test.append(yi)
# conv implementation for fuse scores to encourage large patches
yi = yi.permute(0, 2, 3, 1)
yi = yi.contiguous().view(1, fs[2], fs[3], fs[2] * fs[3])
yi = yi.permute(0, 3, 1, 2) # (B=1, C=32*32, H=32, W=32)
# softmax to match
# scale the correlation with predicted scale factor for known and unknown area
yi = yi * (scale[0,0] * mmi.gt(0.).float() + scale[0,1] * mmi.le(0.).float()) # mmi => (1, 32*32, 1, 1)
# mask itself, self-mask only applied to unknown area
yi = yi + self_mask * mmi # self_mask: (1, 32*32, 32, 32)
# for small input inference
yi = F.softmax(yi, dim=1)
_, offset = torch.max(yi, dim=1) # argmax; index
offset = torch.stack([offset // fs[3], offset % fs[3]], dim=1)
wi_center = alpha_wi[0]
if self.rate == 1:
left = (kernel) // 2
right = (kernel - 1) // 2
yi = F.pad(yi, (left, right, left, right), mode='reflect')
wi_center = wi_center.permute(1, 0, 2, 3)
yi = F.conv2d(yi, wi_center, padding=0) / 4. # (B=1, C=128, H=64, W=64)
else:
yi = F.conv_transpose2d(yi, wi_center, stride=self.rate, padding=1) / 4. # (B=1, C=128, H=64, W=64)
y.append(yi)
offsets.append(offset)
y = torch.cat(y, dim=0) # back to the mini-batch
y.contiguous().view(raw_int_alpha)
offsets = torch.cat(offsets, dim=0)
offsets = offsets.view([int_fs[0]] + [2] + int_fs[2:])
# # case1: visualize optical flow: minus current position
# h_add = Variable(torch.arange(0,float(fs[2]))).to(alpha).view([1, 1, fs[2], 1])
# h_add = h_add.expand(fs[0], 1, fs[2], fs[3])
# w_add = Variable(torch.arange(0,float(fs[3]))).to(alpha).view([1, 1, 1, fs[3]])
# w_add = w_add.expand(fs[0], 1, fs[2], fs[3])
#
# offsets = offsets - torch.cat([h_add, w_add], dim=1).long()
# case2: visualize absolute position
offsets = offsets - torch.Tensor([fs[2]//2, fs[3]//2]).view(1,2,1,1).to(alpha).long()
y = self.W(y) + alpha
return y, (offsets, softmax_scale)
@staticmethod
def extract_patches(x, kernel=3, stride=1):
left =(kernel - stride + 1) // 2
right =(kernel - stride) // 2
x = F.pad(x, (left, right, left, right), mode='reflect')
all_patches = x.unfold(2, kernel, stride).unfold(3, kernel, stride)
return all_patches
@staticmethod
def reduce_mean(x):
for i in range(4):
if i <= 1:
continue
x = torch.mean(x, dim=i, keepdim=True)
return x
@staticmethod
def l2_norm(x):
def reduce_sum(x):
for i in range(4):
if i == 0:
continue
x = torch.sum(x, dim=i, keepdim=True)
return x
x = x**2
x = reduce_sum(x)
return torch.sqrt(x)
@@ -0,0 +1,102 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @File : setup.py
# @Time : 2020/1/15
# @Author : yangchaojie (yangchaojie@immomo.com)
import os
import sys
import shutil
import numpy
import tempfile
from setuptools import setup
from setuptools.extension import Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import platform
def get_root_path(root):
if os.path.dirname(root) in ['', '.']:
return os.path.basename(root)
else:
return get_root_path(os.path.dirname(root))
def copy_file(src, dest):
if os.path.exists(dest):
return
if not os.path.exists(os.path.dirname(dest)):
os.makedirs(os.path.dirname(dest))
if os.path.isdir(src):
shutil.copytree(src, dest)
else:
shutil.copyfile(src, dest)
def touch_init_file():
init_file_name = os.path.join(tempfile.mkdtemp(), '__init__.py')
with open(init_file_name, 'w'):
pass
return init_file_name
def compose_extensions(root='.'):
for file_ in os.listdir(root):
abs_file = os.path.join(root, file_)
if os.path.isfile(abs_file):
if abs_file.endswith('.py'):
extensions.append(Extension(get_root_path(abs_file) + '.*', [abs_file]))
elif abs_file.endswith('.c') or abs_file.endswith('.pyc'):
continue
else:
copy_file(abs_file, os.path.join(build_root_dir, abs_file))
if abs_file.endswith('__init__.py'):
copy_file(init_file, os.path.join(build_root_dir, abs_file))
else:
if os.path.basename(abs_file) in ignore_folders :
continue
if os.path.basename(abs_file) in conf_folders:
copy_file(abs_file, os.path.join(build_root_dir, abs_file))
compose_extensions(abs_file)
# if __name__ == '__main__':
build_root_dir = 'build/lib.' + platform.system().lower() + '-' + platform.machine() + '-' + str(
sys.version_info.major) + '.' + str(sys.version_info.minor)
print(build_root_dir)
extensions = []
ignore_folders = ['build', 'new_ref_zao_color_0818', 'ref_hair_online_0703_local', 'ref_分好类别', '.git']
conf_folders = ['conf']
init_file = touch_init_file()
print(init_file)
compose_extensions()
os.remove(init_file)
setup(
name='moxie_hairstyle',
version='1.0',
ext_modules=cythonize(
extensions,
nthreads=16,
compiler_directives=dict(always_allow_keywords=True),
include_path=[numpy.get_include()]),
cmdclass=dict(build_ext=build_ext))
# python setup.py build_ext
@@ -0,0 +1,319 @@
import torch.nn as nn
import torch.utils.model_zoo as model_zoo
import torch
import os
import cv2
import numpy as np
from core.utils import utils_3ddfa,params_3ddfa,landmark_processor
__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152']
model_urls = {
'resnet18': 'http://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'http://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'http://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'http://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'http://download.pytorch.org/models/resnet152-b121ed2d.pth',
}
def conv3x3(in_planes, out_planes, stride=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = conv1x1(inplanes, planes)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = conv3x3(planes, planes, stride)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = conv1x1(planes, planes * self.expansion)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, no_branch=False, no_activate=False, zero_init_residual=False):
super(ResNet, self).__init__()
self.inplanes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
if no_activate:
if no_branch:
self.fc = nn.Sequential(*[nn.Linear(512 * block.expansion, num_classes)])
else:
self.fc_key = nn.Sequential(*[nn.Linear(256 * block.expansion, 45 * 2)])
self.fc_ctrl = nn.Sequential(*[nn.Linear(256 * block.expansion, 48 * 2)])
else:
if no_branch:
self.fc = nn.Sequential(*[nn.Linear(512 * block.expansion, num_classes), nn.Tanh()])
else:
self.fc_key = nn.Sequential(*[nn.Linear(256 * block.expansion, 45 * 2), nn.Tanh()])
self.fc_ctrl = nn.Sequential(*[nn.Linear(256 * block.expansion, 48 * 2), nn.Tanh()])
self.no_branch = no_branch
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.no_branch:
key = self.avgpool(x)
key = key.view(key.size(0), -1)
key = self.fc(key)
return key
else:
key, ctrl = torch.chunk(x, 2, dim=1)
key = self.avgpool(key)
key = key.view(key.size(0), -1)
key = self.fc_key(key)
ctrl = self.avgpool(ctrl)
ctrl = ctrl.view(ctrl.size(0), -1)
ctrl = self.fc_ctrl(ctrl)
return key, ctrl
def resnet18(pretrained=False, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']), strict=False)
return model
def resnet34(pretrained=False, **kwargs):
"""Constructs a ResNet-34 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [3, 4, 6, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))
return model
def resnet50(pretrained=False, **kwargs):
"""Constructs a ResNet-50 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
return model
def resnet101(pretrained=False, **kwargs):
"""Constructs a ResNet-101 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))
return model
def resnet152(pretrained=False, **kwargs):
"""Constructs a ResNet-152 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 8, 36, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))
return model
class Model_3DDFA(nn.Module):
def __init__(self, gpu_id=None):
super(Model_3DDFA, self).__init__()
self.init_status = False
self.face_alignment_net = resnet18(pretrained=True, num_classes=76, no_branch=True, no_activate=True)
self.model_path = 'weights'
# model_path, _ = os.path.split(os.path.realpath(__file__))
weights = torch.load(os.path.join(self.model_path, 'face_3ddfa.pth'), map_location=lambda storage, loc: storage)
self.load_state_dict(weights)
self.eval()
self.device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
self.to(self.device)
self.init_status = True
def forward(self, imgs):
pred_pose_shape_exp = self.face_alignment_net(imgs)
return pred_pose_shape_exp
def running(self):
return self.init_status
def forward_np(self, imgs):
pred_pose_shape_exp = self.face_alignment_net(imgs)
return pred_pose_shape_exp.detach().cpu().numpy()
def detect(self, images, landmarks):
dst_size = 256
with torch.no_grad():
input_numpy = np.zeros((len(images), 3, dst_size, dst_size), dtype=np.float32)
assert len(images) == len(landmarks)
all_mat = []
all_res = []
all_height = []
for ix, img in enumerate(images):
landmark = landmarks[ix]
mat = landmark_processor.get_transform_mat_full_face(landmark, dst_size)
all_mat.append(mat)
all_height.append(img.shape[0])
tmp = cv2.warpAffine(img, mat, (dst_size, dst_size))
input_numpy[ix, :, :, :] = tmp.transpose((2, 0, 1)).astype(np.float32) / 255
# cv2.imshow('3ddfa_', tmp)
# cv2.waitKey()
in_tensor = torch.from_numpy(input_numpy)
in_tensor = in_tensor.to(self.device)
params = self.face_alignment_net(in_tensor)
params = params.cpu().numpy()
for ix, param in enumerate(params):
param[0] = param[0] / params_3ddfa.SCALE_F
param[1:4] = param[1:4] / params_3ddfa.SCALE_ROTATE
param[4:6] = param[4:6] / params_3ddfa.SCALE_OFFSET
param[6:56] = (param[6:56] / params_3ddfa.SCALE_SHAPE)
param[56:] = (param[56:] / params_3ddfa.SCALE_EXP)
new_param = utils_3ddfa.transform_params(param, cv2.invertAffineTransform(all_mat[ix]), all_height[ix],
dst_size)
all_res.append(new_param)
return all_res
@@ -0,0 +1,67 @@
import os
import torch
import torch.nn.parallel
import numpy as np
from core.utils import landmark_processor
import cv2
modelRoot = "weights"
label_map = [
[0, 0, 0],
[255, 0, 0],
[0, 0, 255],
[0, 255, 0],
[0, 255, 255],
# [0, 255, 0]
]
class Generator_BaldSeg_5c(object):
def __init__(self, gpu_flag, gpu_id):
if not gpu_flag:
self.device = torch.device("cpu")
else:
self.device = torch.device('cuda:{0}'.format(gpu_id))
# load seg model
self.model_dir = modelRoot
self.pre_trained_model = os.path.join('weights', "ori_hair_checkpoint_7660_0611.pt")
self.net = torch.jit.load(self.pre_trained_model, map_location=self.device).to(self.device)
self.net.eval()
self.output_img_size = 512
self.img_ratio = 0.4
def label_to_mask(self, label_np):
label_np = label_np.astype(np.int32)[:, :, np.newaxis]
mask = np.zeros((label_np.shape[0], label_np.shape[1], 3), dtype=np.uint8)
for id, color in enumerate(label_map):
index = (label_np == id).all(axis=2)
mask[index] = color
return mask
def forward(self, image, alpha, landmarks1k):
image_to_face_mat = landmark_processor.get_transform_mat_full_face_ratio_deeplab(landmarks1k, self.output_img_size, self.img_ratio)
img_ = (image * (1 - alpha[:, :, np.newaxis].astype(np.float32) / 255)).astype(np.uint8)
img_cuted = cv2.warpAffine(img_, image_to_face_mat, (self.output_img_size, self.output_img_size), flags=cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_CONSTANT, borderValue=[255, 255, 255])
inv_image_to_face_mat = cv2.invertAffineTransform(image_to_face_mat)
img = (img_cuted.astype(np.float32) / 255).transpose((2, 0, 1))
img = np.expand_dims(img, axis=0)
img = torch.from_numpy(img).to(self.device) #cuda(self.gpu_id)
with torch.no_grad():
output = self.net(img)
pred = output.detach().cpu().numpy().squeeze().astype(np.float32) #torch.max(output[:1], 1)[1].detach().cpu().numpy().squeeze().astype(np.float32)
mask = self.label_to_mask(pred)
# cv2.imshow("img_cuted: ", img_cuted)
# cv2.imshow("mask: ", mask)
# cv2.waitKey()
black_img = np.zeros(image.shape).astype(np.uint8)
cv2.warpAffine(mask, inv_image_to_face_mat, (image.shape[1], image.shape[0]),
dst=black_img, flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_TRANSPARENT)
return black_img
@@ -0,0 +1,441 @@
import torch.nn as nn
import torch.utils.model_zoo as model_zoo
import torch
import numpy as np
import os
from core.utils import landmark_processor
from core.utils.umeyama import umeyama
import cv2
modelRoot = "weights"
__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152']
model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}
def conv3x3(in_planes, out_planes, stride=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = conv1x1(inplanes, planes)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = conv3x3(planes, planes, stride)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = conv1x1(planes, planes * self.expansion)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, is_1k=False, zero_init_residual=False):
super(ResNet, self).__init__()
self.inplanes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
if is_1k:
self.fc = nn.Sequential(*[nn.Linear(512 * block.expansion, num_classes), nn.Tanh()])
else:
self.fc_key = nn.Sequential(*[nn.Linear(256 * block.expansion, 45 * 2), nn.Tanh()])
self.fc_ctrl = nn.Sequential(*[nn.Linear(256 * block.expansion, 48 * 2), nn.Tanh()])
self.is_1k = is_1k
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.is_1k:
key = self.avgpool(x)
key = key.view(key.size(0), -1)
key = self.fc(key)
return key
else:
key, ctrl = torch.chunk(x, 2, dim=1)
key = self.avgpool(key)
key = key.view(key.size(0), -1)
key = self.fc_key(key)
ctrl = self.avgpool(ctrl)
ctrl = ctrl.view(ctrl.size(0), -1)
ctrl = self.fc_ctrl(ctrl)
return key, ctrl
def resnet18(pretrained=False, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']), strict=False)
return model
def resnet34(pretrained=False, **kwargs):
"""Constructs a ResNet-34 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [3, 4, 6, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))
return model
def resnet50(pretrained=False, **kwargs):
"""Constructs a ResNet-50 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
return model
def resnet101(pretrained=False, **kwargs):
"""Constructs a ResNet-101 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))
return model
def resnet152(pretrained=False, **kwargs):
"""Constructs a ResNet-152 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 8, 36, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))
return model
class Model1k(nn.Module):
def __init__(self, gpu_id=None):
super(Model1k, self).__init__()
self.device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
self.face_alignment_net = resnet18(pretrained=False, num_classes=1000 * 2, is_1k=True)
self.model_path, _ = os.path.split(os.path.realpath(__file__))
# weights = torch.load(os.path.join(self.model_path, 'face_alignment_1k.pth'), map_location=lambda storage, loc: storage)
weights = torch.load(os.path.join(modelRoot, 'face_alignment_1k.pth'), map_location=lambda storage, loc: storage)
self.load_state_dict(weights)
self.to(self.device)
self.eval()
def forward(self, imgs):
pred_key_pts = self.face_alignment_net(imgs)
pred_key_pts = pred_key_pts + 0.5
return pred_key_pts
class MomocvFaceAlignment1K(object):
def __init__(self, gpu_id=None):
self.gpu_id = gpu_id
self.device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
self.face_alignment_net = Model1k(gpu_id)
self.trackingFaceRects = []
print('conansherry MomocvFaceAlignment1K')
def forward(self, img_tensor):
fullyconnected1 = self.face_alignment_net(img_tensor).detach().cpu().numpy()
return fullyconnected1
def detect(self, img, landmarks):
dst_size = 256
landmarks_res = []
with torch.no_grad():
input_numpy = np.zeros((len(landmarks), 3, dst_size, dst_size), dtype=np.float32)
all_mat = []
for ix, landmark in enumerate(landmarks):
M = landmark_processor.get_transform_mat_full_face(landmark, dst_size)
all_mat.append(M)
tmp = cv2.warpAffine(img, M, (dst_size, dst_size))
# cv2.imshow('inp', tmp)
# cv2.waitKey()
input_numpy[ix, :, :, :] = tmp.transpose((2, 0, 1)).astype(np.float32) / 255
in_tensor = torch.from_numpy(input_numpy)
in_tensor = in_tensor.to(self.device)
fullyconnected1 = self.face_alignment_net(in_tensor).detach().cpu().numpy()
for ix, pts in enumerate(fullyconnected1):
orig_pts = (np.reshape(pts, (2, 1000)).transpose((1, 0)) * dst_size)
orig_pts = landmark_processor.transform_points(orig_pts, all_mat[ix], invert=True)
landmarks_res.append(orig_pts)
return landmarks_res
def detect_single_face(self, img):
dst_size = 256
with torch.no_grad():
crop_img = img[104:img.shape[0] - 104, 104:img.shape[1] - 104, :]
tmp = cv2.resize(crop_img, (dst_size, dst_size))
input_numpy = np.zeros((1, 3, dst_size, dst_size), dtype=np.float32)
input_numpy[0, :, :, :] = tmp.transpose((2, 0, 1)).astype(np.float32) / 255
in_tensor = torch.from_numpy(input_numpy)
in_tensor = in_tensor.to(self.device)
fullyconnected1 = self.face_alignment_net(in_tensor).detach().cpu().numpy()
orig_pts = (np.reshape(fullyconnected1[0], (2, 1000)).transpose((1, 0)) * crop_img.shape[0])
orig_pts[:, 0] += 104
orig_pts[:, 1] += 104
return orig_pts
def detect_single_face_old(self, img):
dst_size = 256
with torch.no_grad():
tmp = cv2.resize(img, (dst_size, dst_size))
input_numpy = np.zeros((1, 3, dst_size, dst_size), dtype=np.float32)
input_numpy[0, :, :, :] = tmp.transpose((2, 0, 1)).astype(np.float32) / 255
in_tensor = torch.from_numpy(input_numpy)
in_tensor = in_tensor.to(self.device)
fullyconnected1 = self.face_alignment_net(in_tensor).detach().cpu().numpy()
orig_pts = (np.reshape(fullyconnected1[0], (2, 1000)).transpose((1, 0)) * img.shape[0])
return orig_pts
def detect_according_5pts(self, img, pts5):
dst_size = 256
with torch.no_grad():
input_numpy = np.zeros((1, 3, dst_size, dst_size), dtype=np.float32)
eye_dis = 0.34
mouth_dis = 0.34
g_Average_5point_180 = np.array([
eye_dis, 0.3,
1 - eye_dis, 0.37,
0.5, 0.6,
mouth_dis, 0.63,
1 - mouth_dis, 0.7
])
# print(g_Average_5point_180)
# left_eye = np.array([pts5[0], pts5[5]])
# right_eye = np.array([pts5[1], pts5[6]])
# nose = np.array([pts5[2], pts5[7]])
# left_mouth = np.array([pts5[3], pts5[8]])
# right_mouth = np.array([pts5[4], pts5[9]])
left_eye = np.array([pts5[0], pts5[1]])
right_eye = np.array([pts5[2], pts5[3]])
nose = np.array([pts5[4], pts5[5]])
left_mouth = np.array([pts5[6], pts5[7]])
right_mouth = np.array([pts5[8], pts5[9]])
pts5_src = np.vstack((left_eye, right_eye,
nose,
left_mouth, right_mouth))
pts5_src = np.array(pts5_src).astype(np.int32)
pts5_dst = g_Average_5point_180.reshape((5, -1)) * dst_size
# print('pts5_src: ', pts5_src)
# print('pts5_dst: ', pts5_dst)
# mat = cv2.estimateAffinePartial2D(pts5_src, pts5_dst, False)[0]
mat = umeyama(pts5_src, pts5_dst, True)[0:2]
print('mat: ', mat)
tmp = cv2.warpAffine(img, mat, (dst_size, dst_size))
# tmp2 = cv2.warpAffine(img, mat2, (dst_size, dst_size))
# cv2.imshow("tmp2", tmp2)
# cv2.imshow("tmp", tmp)
# cv2.waitKey()
input_numpy[0, :, :, :] = tmp.transpose((2, 0, 1)).astype(np.float32) / 255
in_tensor = torch.from_numpy(input_numpy)
in_tensor = in_tensor.to(self.device)
fullyconnected1 = self.face_alignment_net(in_tensor).detach().cpu().numpy()
orig_pts = (np.reshape(fullyconnected1[0], (2, 1000)).transpose((1, 0)) * dst_size)
orig_pts = landmark_processor.transform_points(orig_pts, mat, invert=True)
return orig_pts
def stable_forward(self, image, detected_faces, reset=False):
if reset is True:
self.trackingFaceRects = []
if len(self.trackingFaceRects) == 0:
for face_rect in detected_faces:
new_tracking_rect = [face_rect, True, [0, 0], 0, None]
self.trackingFaceRects.append(new_tracking_rect)
with torch.no_grad():
landmarks = []
for ix, tracking_face_rect in enumerate(self.trackingFaceRects):
if tracking_face_rect[1] == True:
d = tracking_face_rect[0]
src_center = np.array([d[2] - (d[2] - d[0]) / 2.0, d[3] - (d[3] - d[1]) / 2.0])
rotate_degree = tracking_face_rect[3]
scale = 256 * 0.6 / min(d[2] - d[0], d[3] - d[1])
dst_center = np.array([0.5, 0.5]) * 256
offset = dst_center - src_center
M = cv2.getRotationMatrix2D((src_center[0], src_center[1]), rotate_degree, scale)
M[:, 2] += offset
else:
rotate_degree = 0
M = landmark_processor.get_transform_mat_mmcv_bigger(tracking_face_rect[4], 256)
inp = cv2.warpAffine(image, M, (256, 256))
# cv2.imshow('inp_{}'.format(ix), inp)
# cv2.waitKey()
orig_inp = inp
inp = inp.transpose((2, 0, 1)).astype(np.float32)
inp = inp[np.newaxis, :, :, :] / 255
in_tensor = torch.from_numpy(inp)
in_tensor = in_tensor.cuda(0)
fullyconnected1 = self.forward(in_tensor)
fullyconnected1 = fullyconnected1[0]
orig_pts = (np.reshape(fullyconnected1, (2, 1000)).transpose((1, 0))) * 256
t2 = cv2.getTickCount()
orig_pts = landmark_processor.transform_points(orig_pts, M, invert=True)
# orig_pts = orig_pts.transpose((1, 0))
fullyconnected1 = orig_pts
# update tracking infos
tracking_face_rect[1] = False
tracking_face_rect[2] = None
tracking_face_rect[3] = rotate_degree
tracking_face_rect[4] = fullyconnected1
# fullyconnected1 = landmark_processor.pts_1k_to_137(fullyconnected1)
# eye_landmark = self.detect_eye(image, fullyconnected1)
# fullyconnected1[87:104] = eye_landmark[0]
# fullyconnected1[104:121] = eye_landmark[1]
landmarks.append(fullyconnected1)
return landmarks
@@ -0,0 +1,133 @@
import numpy as np
import cv2
def nms(boxes, overlap_threshold=0.5, mode='union'):
""" Pure Python NMS baseline. """
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
scores = boxes[:, 4]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
if mode is 'min':
ovr = inter / np.minimum(areas[i], areas[order[1:]])
else:
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= overlap_threshold)[0]
order = order[inds + 1]
return keep
def convert_to_square(bboxes):
"""
Convert bounding boxes to a square form.
"""
square_bboxes = np.zeros_like(bboxes)
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
h = y2 - y1 + 1.0
w = x2 - x1 + 1.0
max_side = np.maximum(h, w)
square_bboxes[:, 0] = x1 + w*0.5 - max_side*0.5
square_bboxes[:, 1] = y1 + h*0.5 - max_side*0.5
square_bboxes[:, 2] = square_bboxes[:, 0] + max_side - 1.0
square_bboxes[:, 3] = square_bboxes[:, 1] + max_side - 1.0
return square_bboxes
def calibrate_box(bboxes, offsets):
"""Transform bounding boxes to be more like true bounding boxes.
'offsets' is one of the outputs of the nets.
"""
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
w = x2 - x1 + 1.0
h = y2 - y1 + 1.0
w = np.expand_dims(w, 1)
h = np.expand_dims(h, 1)
translation = np.hstack([w, h, w, h])*offsets
bboxes[:, 0:4] = bboxes[:, 0:4] + translation
return bboxes
def get_image_boxes(bounding_boxes, img, size=24):
"""Cut out boxes from the image.
"""
num_boxes = len(bounding_boxes)
(height, width, _) = img.shape
[dy, edy, dx, edx, y, ey, x, ex, w, h] = correct_bboxes(bounding_boxes, width, height)
img_boxes = np.zeros((num_boxes, 3, size, size), 'float32')
for i in range(num_boxes):
img_box = np.zeros((h[i], w[i], 3), 'uint8')
img_array = np.asarray(img, 'uint8')
img_box[dy[i]:(edy[i] + 1), dx[i]:(edx[i] + 1), :] =\
img_array[y[i]:(ey[i] + 1), x[i]:(ex[i] + 1), :]
img_box = cv2.resize(img_box, (size, size))
img_box = np.asarray(img_box, 'float32')
img_boxes[i, :, :, :] = _preprocess(img_box)
return img_boxes
def correct_bboxes(bboxes, width, height):
"""Crop boxes that are too big and get coordinates
with respect to cutouts.
"""
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
w, h = x2 - x1 + 1.0, y2 - y1 + 1.0
num_boxes = bboxes.shape[0]
x, y, ex, ey = x1, y1, x2, y2
dx, dy = np.zeros((num_boxes,)), np.zeros((num_boxes,))
edx, edy = w.copy() - 1.0, h.copy() - 1.0
ind = np.where(ex > width - 1.0)[0]
edx[ind] = w[ind] + width - 2.0 - ex[ind]
ex[ind] = width - 1.0
ind = np.where(ey > height - 1.0)[0]
edy[ind] = h[ind] + height - 2.0 - ey[ind]
ey[ind] = height - 1.0
ind = np.where(x < 0.0)[0]
dx[ind] = 0.0 - x[ind]
x[ind] = 0.0
ind = np.where(y < 0.0)[0]
dy[ind] = 0.0 - y[ind]
y[ind] = 0.0
return_list = [dy, edy, dx, edx, y, ey, x, ex, w, h]
return_list = [i.astype('int32') for i in return_list]
return return_list
def _preprocess(img):
"""Preprocessing step before feeding the network.
"""
img = img.transpose((2, 0, 1))
img = np.expand_dims(img, 0)
img = (img - 127.5)*0.0078125
return img
@@ -0,0 +1,42 @@
# config.py
cfg_mnet = {
'name': 'mobilenet0.25',
'min_sizes': [[16, 32], [64, 128], [256, 512]],
'steps': [8, 16, 32],
'variance': [0.1, 0.2],
'clip': False,
'loc_weight': 2.0,
'gpu_train': True,
'batch_size': 32,
'ngpu': 1,
'epoch': 250,
'decay1': 190,
'decay2': 220,
'image_size': 640,
'pretrain': True,
'return_layers': {'stage1': 1, 'stage2': 2, 'stage3': 3},
'in_channel': 32,
'out_channel': 64
}
cfg_re50 = {
'name': 'Resnet50',
'min_sizes': [[16, 32], [64, 128], [256, 512]],
'steps': [8, 16, 32],
'variance': [0.1, 0.2],
'clip': False,
'loc_weight': 2.0,
'gpu_train': True,
'batch_size': 24,
'ngpu': 4,
'epoch': 100,
'decay1': 70,
'decay2': 90,
'image_size': 840,
'pretrain': True,
'return_layers': {'layer2': 1, 'layer3': 2, 'layer4': 3},
'in_channel': 256,
'out_channel': 256
}
@@ -0,0 +1,259 @@
import math
import numpy as np
from .box_utils import nms, calibrate_box, get_image_boxes, convert_to_square, _preprocess
import torch
import cv2
from .nms.py_cpu_nms import py_cpu_nms
from core.utils import box_utils_Retina
from .layers.functions.prior_box import PriorBox
from .config import cfg_re50
from .retinaface import RetinaFace
from mtcnn.model import PNet, RNet, ONet
gpu_id = 0
device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
pnet, rnet, onet= PNet(), RNet(), ONet()
pnet.to(device)
rnet.to(device)
onet.to(device)
onet.eval()
class RetinaFaceDetector(object):
def __init__(self, gpu_id=None):
self.gpu_id = gpu_id
self.cfg = cfg_re50
self.device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
model = RetinaFace(cfg=self.cfg, phase='test')
model = self.load_model(model, 'weights/Resnet50_Final.pth', True)
model.eval()
self.net = model.to(self.device)
self.resize = 1
self.confidence_threshold = 0.02
self.top_k = 5000
self.nms_threshold = 0.4
self.keep_top_k = 750
def remove_prefix(self, state_dict, prefix):
# print('remove prefix \'{}\''.format(prefix))
f = lambda x: x.split(prefix, 1)[-1] if x.startswith(prefix) else x
return {f(key): value for key, value in state_dict.items()}
def check_keys(self, model, pretrained_state_dict):
ckpt_keys = set(pretrained_state_dict.keys())
model_keys = set(model.state_dict().keys())
used_pretrained_keys = model_keys & ckpt_keys
# unused_pretrained_keys = ckpt_keys - model_keys
# missing_keys = model_keys - ckpt_keys
assert len(used_pretrained_keys) > 0, 'load NONE from pretrained checkpoint'
return True
def load_model(self, model, pretrained_path, load_to_cpu):
# print('Loading pretrained model from {}'.format(pretrained_path))
if load_to_cpu:
pretrained_dict = torch.load(pretrained_path, map_location=lambda storage, loc: storage)
else:
device = torch.cuda.current_device()
pretrained_dict = torch.load(pretrained_path, map_location=lambda storage, loc: storage.cuda(device))
if "state_dict" in pretrained_dict.keys():
pretrained_dict = self.remove_prefix(pretrained_dict['state_dict'], 'module.')
else:
pretrained_dict = self.remove_prefix(pretrained_dict, 'module.')
self.check_keys(model, pretrained_dict)
model.load_state_dict(pretrained_dict, strict=False)
return model
def forward(self, img_raw, min_face_size=50):
img_scale = 640 / max(img_raw.shape[0], img_raw.shape[1])
img = cv2.resize(img_raw, (0, 0), fx=img_scale, fy=img_scale)
img = np.float32(img)
im_height, im_width, _ = img.shape
scale = torch.Tensor([img.shape[1], img.shape[0], img.shape[1], img.shape[0]])
img -= (104, 117, 123)
img = img.transpose(2, 0, 1)
img = torch.from_numpy(img).unsqueeze(0)
img = img.to(self.device)
scale = scale.to(self.device)
loc, conf, landms = self.net(img) # forward pass
priorbox = PriorBox(self.cfg, image_size=(im_height, im_width))
priors = priorbox.forward()
priors = priors.to(self.device)
prior_data = priors.data
boxes = box_utils_Retina.decode(loc.data.squeeze(0), prior_data, self.cfg['variance'])
boxes = boxes * scale / self.resize
boxes = boxes.cpu().numpy()
scores = conf.squeeze(0).data.cpu().numpy()[:, 1]
landms = box_utils_Retina.decode_landm(landms.data.squeeze(0), prior_data, self.cfg['variance'])
scale1 = torch.Tensor([img.shape[3], img.shape[2], img.shape[3], img.shape[2],
img.shape[3], img.shape[2], img.shape[3], img.shape[2],
img.shape[3], img.shape[2]])
scale1 = scale1.to(self.device)
landms = landms * scale1 / self.resize
landms = landms.cpu().numpy()
# ignore low scores
inds = np.where(scores > self.confidence_threshold)[0]
boxes = boxes[inds]
landms = landms[inds]
scores = scores[inds]
# keep top-K before NMS
order = scores.argsort()[::-1][:self.top_k]
boxes = boxes[order]
landms = landms[order]
scores = scores[order]
# do NMS
dets = np.hstack((boxes, scores[:, np.newaxis])).astype(np.float32, copy=False)
keep = py_cpu_nms(dets, self.nms_threshold, min_face_size = min_face_size * img_scale)
# keep = nms(dets, args.nms_threshold,force_cpu=args.cpu)
dets = dets[keep, :]
landms = landms[keep]
# keep top-K faster NMS
dets = dets[:self.keep_top_k, :]
landms = landms[:self.keep_top_k, :]
dets[:, :4] = dets[:, :4] / img_scale
landms /= img_scale
return dets, landms
def forward_v2(self, image, min_face_size=20.0, thresholds=[0.6, 0.7, 0.8], nms_thresholds=[0.7, 0.7, 0.7]):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
(height, width, _) = image.shape
min_length = min(height, width)
min_detection_size = 12
factor = 0.707 # sqrt(0.5)
scales = []
m = min_detection_size / min_face_size
min_length *= m
factor_count = 0
while min_length > min_detection_size:
scales.append(m * factor ** factor_count)
min_length *= factor
factor_count += 1
# STAGE 1
bounding_boxes = []
for s in scales: # run P-Net on different scales
boxes = run_first_stage(image, pnet, scale=s, threshold=thresholds[0], gpu_id=self.gpu_id)
bounding_boxes.append(boxes)
bounding_boxes = [i for i in bounding_boxes if i is not None]
if len(bounding_boxes) == 0:
return [], []
bounding_boxes = np.vstack(bounding_boxes)
keep = nms(bounding_boxes[:, 0:5], nms_thresholds[0])
bounding_boxes = bounding_boxes[keep]
bounding_boxes = calibrate_box(bounding_boxes[:, 0:5], bounding_boxes[:, 5:])
bounding_boxes = convert_to_square(bounding_boxes)
bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
# STAGE 2
img_boxes = get_image_boxes(bounding_boxes, image, size=24)
img_boxes = torch.from_numpy(img_boxes)
img_boxes = img_boxes.to(self.device)
output = rnet(img_boxes)
offsets = output[0].to('cpu').data.numpy() # shape [n_boxes, 4]
probs = output[1].to('cpu').data.numpy() # shape [n_boxes, 2]
keep = np.where(probs[:, 1] > thresholds[1])[0]
bounding_boxes = bounding_boxes[keep]
bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
offsets = offsets[keep]
keep = nms(bounding_boxes, nms_thresholds[1])
bounding_boxes = bounding_boxes[keep]
bounding_boxes = calibrate_box(bounding_boxes, offsets[keep])
bounding_boxes = convert_to_square(bounding_boxes)
bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
# STAGE 3
img_boxes = get_image_boxes(bounding_boxes, image, size=48)
if len(img_boxes) == 0:
return [], []
img_boxes = torch.from_numpy(img_boxes)
img_boxes = img_boxes.to(self.device)
output = onet(img_boxes)
landmarks = output[0].to('cpu').data.numpy() # shape [n_boxes, 10]
offsets = output[1].to('cpu').data.numpy() # shape [n_boxes, 4]
probs = output[2].to('cpu').data.numpy() # shape [n_boxes, 2]
keep = np.where(probs[:, 1] > thresholds[2])[0]
bounding_boxes = bounding_boxes[keep]
bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
offsets = offsets[keep]
landmarks = landmarks[keep]
# compute landmark points
width = bounding_boxes[:, 2] - bounding_boxes[:, 0] + 1.0
height = bounding_boxes[:, 3] - bounding_boxes[:, 1] + 1.0
xmin, ymin = bounding_boxes[:, 0], bounding_boxes[:, 1]
landmarks[:, 0:5] = np.expand_dims(xmin, 1) + np.expand_dims(width, 1) * landmarks[:, 0:5]
landmarks[:, 5:10] = np.expand_dims(ymin, 1) + np.expand_dims(height, 1) * landmarks[:, 5:10]
bounding_boxes = calibrate_box(bounding_boxes, offsets)
keep = nms(bounding_boxes, nms_thresholds[2], mode='min')
bounding_boxes = bounding_boxes[keep]
landmarks = landmarks[keep]
return bounding_boxes, landmarks
def run_first_stage(image, net, scale, threshold, gpu_id=0):
"""
Run P-Net, generate bounding boxes, and do NMS.
"""
device = torch.device('cuda:{}'.format(gpu_id) if gpu_id is not None else 'cpu')
(height, width, _) = image.shape
sw, sh = math.ceil(width * scale), math.ceil(height * scale)
img = cv2.resize(image, (sw, sh))
# img = image.resize((sw, sh), Image.BILINEAR)
img = np.asarray(img, 'float32')
img = torch.from_numpy(_preprocess(img))
img = img.to(device)
output = net(img)
probs = output[1].to('cpu').data.numpy()[0, 1, :, :]
offsets = output[0].to('cpu').data.numpy()
boxes = _generate_bboxes(probs, offsets, scale, threshold)
if len(boxes) == 0:
return None
keep = nms(boxes[:, 0:5], overlap_threshold=0.5)
return boxes[keep]
def _generate_bboxes(probs, offsets, scale, threshold):
"""
Generate bounding boxes at places where there is probably a face.
"""
stride = 2
cell_size = 12
inds = np.where(probs > threshold)
if inds[0].size == 0:
return np.array([])
tx1, ty1, tx2, ty2 = [offsets[0, i, inds[0], inds[1]] for i in range(4)]
offsets = np.array([tx1, ty1, tx2, ty2])
score = probs[inds[0], inds[1]]
# P-Net is applied to scaled images, so we need to rescale bounding boxes back
bounding_boxes = np.vstack([
np.round((stride * inds[1] + 1.0) / scale),
np.round((stride * inds[0] + 1.0) / scale),
np.round((stride * inds[1] + 1.0 + cell_size) / scale),
np.round((stride * inds[0] + 1.0 + cell_size) / scale),
score, offsets
])
return bounding_boxes.T

Some files were not shown because too many files have changed in this diff Show More