初始化换发型项目:3个微服务代码 + 部署脚本

包含:
- hair_service_sd: 换发型/换发色算法服务 (端口 8801)
- photo_service: LoRA 训练调度服务 (端口 32678)
- stable-diffusion-webui: SD WebUI 推理服务 (端口 57860)
- kohya_ss_home: 训练环境代码
- meidaojia: 监控测试脚本
- setup.sh: 一键部署脚本 (conda环境恢复 + 配置生成 + 完整性检查)
- start_all_services.sh: 启动3个服务
- configure.ini.template: 路径模板化 (BASE_DIR自动推导)
- conda_envs/py310.yml: py310 环境定义

大文件 (weights/, models/, data/, conda_envs/*.tar.gz 等) 通过 .gitignore 排除,
由网盘单独上传。
This commit is contained in:
colomi
2026-07-11 18:11:49 +08:00
commit 0eb61f3e60
628 changed files with 120882 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# Python
__pycache__/
*.pyc
*.pyo
*.egg-info/
.eggs/
# IDE
.idea/
.vscode/
# Logs
*.log
nohup.out
/logs/
# 大文件目录(网盘上传)— 注意 /data/ 用前导斜杠仅匹配顶层
hair_service_sd/weights/
stable-diffusion-webui/models/
stable-diffusion-webui/extensions/sd-webui-controlnet/
stable-diffusion-webui/repositories/
kohya_ss_home/.local/
kohya_ss_home/.cache/
kohya_ss_home/kohya_ss/
/data/
conda_envs/*.tar.gz
docker/
# 运行时生成(setup.sh 生成)
hair_service_sd/config/configure.ini
*.pid
# 注意:hair_service_sd/data/ (19M 小静态文件) 不排除,需 git 管理
# 注意:conda_envs/py310.yml 不排除,需 git 管理
# 注意:kohya_ss_home/start_docker.sh 不排除,需 git 管理
+330
View File
@@ -0,0 +1,330 @@
# 换发型项目迁移收尾计划
## 背景
项目从 `/home/szlc/project` 迁移到 `/home/szlc/change_hair_3090`。前序工作已完成代码复制和大部分路径修改。本计划覆盖剩余的收尾工作。
## 当前状态
### 已完成
- ✅ 代码复制:hair_service_sd (代码+weights 9.9G)、photo_service (1.9M)、stable-diffusion-webui (代码+extensions 4.4G+repositories 225M)、meidaojia (124K)、kohya_ss_home (kohya_ss 439M + .local 7.6G + .cache 3.3M)、data (4.9G)
- ✅ configure.ini.template 创建 + configure.ini 生成
- ✅ lora_train_service_1.py 修改(BASE_DIR + Docker 命令路径修正)
- ✅ watch_delete.py 修改(从 config 读取路径)
- ✅ gunicorn_config.py 修改(注释 chdir
- ✅ config.json 修改(删除 onediff 配置,改 sd_model_checkpoint
- ✅ git initmain 分支,无提交)
### 未完成(本计划覆盖)
1.**stable-diffusion-webui/models/ 目录未复制**41G,含 228 个 LoRA 33G + v1-5-pruned 4G + 辅助模型)
2.**hairstyle_model_infer.py:1096 未注释**(活跃代码,换发色会崩溃)
3.**.gitignore 未创建**
4.**setup.sh 未创建**
5.**start_all_services.sh 未创建**
6.**README.md 未创建**
7.**conda 环境未导出**my_hair, sdwebui, py310
8.**git commit 未执行**
### 用户决策
- Docker 镜像 `chinatszrn/ubuntu:kohya_ss` **不需要导出**(用户表示当前训练已不用 Docker)
- majicmixRealistic_v7.safetensors 用户会从网上下载
- onediff 不迁移
## 服务启动方式(从 .bash 脚本确认)
| 服务 | 端口 | conda 环境 | 启动命令 |
|------|------|-----------|---------|
| hair_service_sd | 8801 | my_hair | `python run_copy_cost_colorb64.py` |
| stable-diffusion-webui | 57860 | sdwebui | `python webui.py --api --listen --xformers --port 57860` |
| photo_service | 32678 | py310 | `python lora_train_service_1.py` |
---
## 执行步骤
### 步骤 1:复制 models/ 目录(41G
源:`/home/szlc/project/onediff/stable-diffusion-webui/models/`
目标:`/home/szlc/change_hair_3090/stable-diffusion-webui/models/`
```bash
rsync -av --progress /home/szlc/project/onediff/stable-diffusion-webui/models/ /home/szlc/change_hair_3090/stable-diffusion-webui/models/
```
包含:
- Lora/ (33G, 228 个 LoRA 文件)
- Stable-diffusion/ (4G, v1-5-pruned-emaonly.safetensors)
- ESRGAN/ (1.2G)、BLIP/ (855M)、torch_deepdanbooru/ (615M)、roop/ (529M)、GFPGAN/ (519M)、Codeformer/ (360M)、RealESRGAN/ (64M) 等
注意:majicmixRealistic_v7.safetensors 不在源目录中,用户会单独下载。
### 步骤 2:注释 hairstyle_model_infer.py:1096
文件:`hair_service_sd/hairstyle_model_infer.py`
第 1096 行:
```python
# 修改前
cv2.imwrite('/home/student/Desktop/tmp_color/need/face_base2.png', user_res_8uc3_orisize2)
# 修改后
# cv2.imwrite('/home/student/Desktop/tmp_color/need/face_base2.png', user_res_8uc3_orisize2) # 调试用,路径不存在会导致崩溃
```
### 步骤 3:创建 .gitignore
文件:`/home/szlc/change_hair_3090/.gitignore`
```gitignore
# Python
__pycache__/
*.pyc
*.pyo
*.egg-info/
.eggs/
# IDE
.idea/
.vscode/
# Logs
*.log
nohup.out
/logs/
# 大文件目录(网盘上传)— 注意 /data/ 用前导斜杠仅匹配顶层
hair_service_sd/weights/
stable-diffusion-webui/models/
stable-diffusion-webui/extensions/sd-webui-controlnet/
stable-diffusion-webui/repositories/
kohya_ss_home/.local/
kohya_ss_home/.cache/
kohya_ss_home/kohya_ss/
/data/
conda_envs/*.tar.gz
docker/
# 运行时生成(setup.sh 生成)
hair_service_sd/config/configure.ini
*.pid
# 注意:hair_service_sd/data/ (19M 小静态文件) 不排除,需 git 管理
# 注意:conda_envs/py310.yml 不排除,需 git 管理
# 注意:kohya_ss_home/start_docker.sh 不排除,需 git 管理
```
### 步骤 4:创建 setup.sh
文件:`/home/szlc/change_hair_3090/setup.sh`
功能:
1. 检查前提条件(conda, git, nvidia-docker
2. 从 template 生成 configure.inised 替换 `__BASE_DIR__`
3. 用 conda-pack 恢复 my_hair 和 sdwebui 环境
4. 用 yml 创建 py310 环境
5. 检查模型/数据目录完整性
6. 检查 majicmixRealistic_v7.safetensors 是否存在
7. 创建运行时目录(tmp, res_dir, userImage 等)
```bash
#!/bin/bash
set -e
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
CONDA_BASE="${CONDA_BASE:-/home/szlc/miniconda3}"
# 初始化 conda(脚本中需要 source 才能用 conda activate
if [ -f "$CONDA_BASE/etc/profile.d/conda.sh" ]; then
source "$CONDA_BASE/etc/profile.d/conda.sh"
else
echo "WARNING: 未找到 conda.sh,请确认 CONDA_BASE=$CONDA_BASE 正确"
fi
echo "=== 换发型项目部署脚本 ==="
echo "BASE_DIR: $BASE_DIR"
# 1. 检查前提条件
echo "[1/7] 检查前提条件..."
command -v git >/dev/null || { echo "ERROR: git 未安装"; exit 1; }
command -v conda >/dev/null || { echo "ERROR: conda 未安装"; exit 1; }
nvidia-smi >/dev/null 2>&1 || { echo "ERROR: NVIDIA 驱动未安装"; exit 1; }
# 2. 生成 configure.ini
echo "[2/7] 生成 configure.ini..."
sed "s|__BASE_DIR__|$BASE_DIR|g" "$BASE_DIR/hair_service_sd/config/configure.ini.template" > "$BASE_DIR/hair_service_sd/config/configure.ini"
echo " configure.ini 已生成"
# 3. 恢复 conda 环境(conda-pack
echo "[3/7] 恢复 conda 环境..."
for env_name in my_hair sdwebui; do
if [ -f "$BASE_DIR/conda_envs/${env_name}.tar.gz" ]; then
echo " 恢复 $env_name ..."
rm -rf "$CONDA_BASE/envs/$env_name"
mkdir -p "$CONDA_BASE/envs/$env_name"
tar -xzf "$BASE_DIR/conda_envs/${env_name}.tar.gz" -C "$CONDA_BASE/envs/$env_name"
conda activate "$env_name"
conda-unpack # 修复打包后的硬编码路径
conda deactivate
echo "$env_name 已恢复"
else
echo " ✗ conda_envs/${env_name}.tar.gz 不存在,请从网盘下载"
fi
done
# 4. 创建 py310 环境(从 yml
echo "[4/7] 创建 py310 环境..."
if [ -f "$BASE_DIR/conda_envs/py310.yml" ]; then
conda env create -f "$BASE_DIR/conda_envs/py310.yml" -n py310 2>/dev/null || echo " py310 环境已存在,跳过"
echo " ✓ py310 已就绪"
else
echo " ✗ conda_envs/py310.yml 不存在"
fi
# 5. 检查模型/数据目录
echo "[5/7] 检查模型和数据目录..."
for dir in hair_service_sd/weights stable-diffusion-webui/models/Lora stable-diffusion-webui/models/Stable-diffusion stable-diffusion-webui/extensions/sd-webui-controlnet stable-diffusion-webui/repositories kohya_ss_home/.local kohya_ss_home/kohya_ss data/ref_hairstyle; do
if [ -d "$BASE_DIR/$dir" ]; then
echo "$dir"
else
echo "$dir 缺失,请从网盘下载"
fi
done
# 6. 检查训练底模
echo "[6/7] 检查 majicmixRealistic_v7..."
if [ -f "$BASE_DIR/stable-diffusion-webui/models/Stable-diffusion/majicmixRealistic_v7.safetensors" ]; then
echo " ✓ majicmixRealistic_v7.safetensors 已存在"
else
echo " ✗ majicmixRealistic_v7.safetensors 缺失,请从网上下载并放置到 stable-diffusion-webui/models/Stable-diffusion/"
fi
# 7. 创建运行时目录
echo "[7/7] 创建运行时目录..."
mkdir -p "$BASE_DIR/logs"
mkdir -p "$BASE_DIR/data/tmp" "$BASE_DIR/data/res_dir" "$BASE_DIR/data/userImage" "$BASE_DIR/data/user_info"
mkdir -p "$BASE_DIR/kohya_ss_home/train_material"
echo ""
echo "=== 部署完成 ==="
echo "启动服务: ./start_all_services.sh"
```
### 步骤 5:创建 start_all_services.sh
文件:`/home/szlc/change_hair_3090/start_all_services.sh`
```bash
#!/bin/bash
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
CONDA_BASE="${CONDA_BASE:-/home/szlc/miniconda3}"
# 1. hair_service_sd (端口 8801)
cd "$BASE_DIR/hair_service_sd"
nohup "$CONDA_BASE/envs/my_hair/bin/python" run_copy_cost_colorb64.py > "$BASE_DIR/logs/hair_service.log" 2>&1 &
echo "hair_service_sd 已启动 (PID: $!)"
# 2. stable-diffusion-webui (端口 57860)
cd "$BASE_DIR/stable-diffusion-webui"
nohup "$CONDA_BASE/envs/sdwebui/bin/python" webui.py --api --listen --xformers --port 57860 > "$BASE_DIR/logs/webui.log" 2>&1 &
echo "stable-diffusion-webui 已启动 (PID: $!)"
# 3. photo_service (端口 32678)
cd "$BASE_DIR/photo_service"
nohup "$CONDA_BASE/envs/py310/bin/python" lora_train_service_1.py > "$BASE_DIR/logs/photo_service.log" 2>&1 &
echo "photo_service 已启动 (PID: $!)"
echo "所有服务已在后台启动,日志在 $BASE_DIR/logs/"
```
### 步骤 6:创建 README.md
文件:`/home/szlc/change_hair_3090/README.md`
内容包含:
- 项目简介(3 个微服务)
- 目录结构
- 前提条件(Ubuntu 22.04, 3090 GPU, conda, git
- 部署步骤(git clone → 下载网盘文件 → ./setup.sh → ./start_all_services.sh
- 网盘文件清单(models/, weights/, data/, conda_envs/, etc.
- 服务端口说明
- 常见问题
### 步骤 7:导出 conda 环境
```bash
# 安装 conda-pack
conda install -n base -c conda-forge conda-pack -y
# 导出 my_hair (10G → ~5G 压缩)
conda pack -n my_hair -o /home/szlc/change_hair_3090/conda_envs/my_hair.tar.gz --force
# 导出 sdwebui (7.3G → ~4G 压缩)
conda pack -n sdwebui -o /home/szlc/change_hair_3090/conda_envs/sdwebui.tar.gz --force
# 导出 py310 (yml)
conda env export -n py310 --no-builds > /home/szlc/change_hair_3090/conda_envs/py310.yml
```
注意:conda-pack 可能需要较长时间(每个环境 5-10 分钟),建议后台执行。
### 步骤 8Git 提交
```bash
cd /home/szlc/change_hair_3090
git add .
# 验证大文件未被追踪
git status --short | grep -E "\.(safetensors|pth|pt|tar\.gz|tar)$" # 应无输出
git commit -m "初始化换发型项目:3个微服务代码 + 部署脚本"
```
---
## 验证步骤
### 路径一致性验证
```bash
# 检查无残留旧路径(注释行除外)
grep -rn "/home/szlc/project\|/gz-fs\|/mnt/nas_hdd\|/root/project" /home/szlc/change_hair_3090/ --include="*.py" --include="*.ini" --include="*.sh" | grep -v "^.*:#"
# 应无输出或仅匹配注释行
```
### Git 追踪验证
```bash
# 大文件不应被追踪
git status --short | grep -E "weights/|models/.*safetensors|\.tar\.gz|\.tar$"
# 应无输出
```
### 目录完整性验证
```bash
# 关键目录存在
for d in hair_service_sd/weights stable-diffusion-webui/models/Lora stable-diffusion-webui/models/Stable-diffusion stable-diffusion-webui/extensions/sd-webui-controlnet kohya_ss_home/.local data/ref_hairstyle; do
[ -d "$d" ] && echo "$d" || echo "$d 缺失"
done
```
---
## 网盘上传清单
| 路径 | 大小 | 说明 |
|------|------|------|
| hair_service_sd/weights/ | 10G | 换发算法模型 |
| stable-diffusion-webui/models/ | 41G | SD模型+LoRA+辅助模型 |
| stable-diffusion-webui/extensions/sd-webui-controlnet/ | 4.4G | ControlNet扩展 |
| stable-diffusion-webui/repositories/ | 225M | SD依赖仓库 |
| kohya_ss_home/.local/ | 7.6G | 训练pip包 |
| kohya_ss_home/.cache/ | 3.3M | CLIP缓存 |
| kohya_ss_home/kohya_ss/ | 439M | 训练代码 |
| data/ | 4.9G | 业务数据 |
| conda_envs/my_hair.tar.gz | ~5G | conda环境 |
| conda_envs/sdwebui.tar.gz | ~4G | conda环境 |
| **合计** | **约 78G** | |
---
## 关键风险
1. **hairstyle_model_infer.py:1096** — 活跃代码路径中的 `cv2.imwrite('/home/student/Desktop/...')`,不注释会导致换发色功能崩溃
2. **majicmixRealistic_v7.safetensors** — 训练底模不在源目录中,用户需从网上下载
3. **conda-pack glibc 兼容性** — 源机器和目标机器需同为 Ubuntu 且 glibc 版本兼容
4. **Docker 训练** — 当前代码仍含 Docker 命令,用户表示不再需要 Docker 训练,代码可能需要后续修改
@@ -0,0 +1,272 @@
# 换发型项目迁移到 3090 部署计划
## Context
本机 `/home/szlc/project` 下有一个换发型/换发色/训练发型的 AI 项目,包含 3 个微服务协同工作。目标是将所有代码和依赖迁移到 `/home/szlc/change_hair_3090`,代码用 git 管理,模型/数据/环境等大文件放在同一目录但 .gitignore 排除(单独用网盘上传)。最终实现:在新 3090 Ubuntu 机器上 `git clone` + 下载网盘文件 + 运行 `setup.sh` 即可完成部署。
**用户决策**:包含全部 228 个 LoRA33G);Docker sudo 密码 asdfasdf;从网上下载 majicmixRealistic_v7;不迁移 onediff。
---
## 目标目录结构
```
/home/szlc/change_hair_3090/ ← Git 仓库根
├── .gitignore
├── README.md ← 部署文档 [git]
├── setup.sh ← 一键部署脚本 [git]
├── start_all_services.sh ← 启动3个服务 [git]
├── hair_service_sd/ ← 换发算法服务 [git: 代码]
│ ├── config/configure.ini.template ← 配置模板(含 __BASE_DIR__ 占位符)[git]
│ ├── config/configure.ini ← setup.sh 生成 [gitignore]
│ ├── core/ models/ utils/ common/ ← 代码 [git]
│ ├── *.py ← 代码 [git]
│ ├── data/ ← 小静态文件 9M [git]
│ └── weights/ ← 10G 模型 [gitignore/网盘]
├── photo_service/ ← 训练服务 [git: 代码 4M]
│ ├── lora_train_service_1.py ← 需修改路径 [git]
│ ├── webui_im2im.py *.py utils/ ← [git]
│ └── conda_envs/py310.yml ← py310 环境定义 [git]
├── stable-diffusion-webui/ ← SD WebUI [git: 代码 ~280M]
│ ├── webui.py modules/ javascript/ ← 代码 [git]
│ ├── config.json ← 清理onediff配置 [git]
│ ├── extensions/sd-webui-controlnet/ ← 4.4G(annotator+models) [gitignore/网盘]
│ ├── repositories/ ← 225M [gitignore/网盘]
│ └── models/ ← 49G [gitignore/网盘]
│ ├── Lora/ ← 33G 228个LoRA
│ ├── Stable-diffusion/ ← v1-5 + majicmixRealistic_v7
│ └── ESRGAN/ GFPGAN/ BLIP/ ...
├── kohya_ss_home/ ← 训练环境 [gitignore/网盘]
│ ├── kohya_ss/ ← 训练代码 427M
│ ├── .local/ ← pip包 7.6G
│ ├── .cache/clip/ ← CLIP缓存 1.6M
│ ├── .cache/huggingface/ ← 1.8M
│ └── start_docker.sh ← [单独提取到git]
├── data/ ← 业务数据 4.9G [gitignore/网盘]
│ ├── ref_hairstyle/ ref_color/ ref_online/ ...
├── conda_envs/ ← [gitignore/网盘]
│ ├── my_hair.tar.gz ← conda-pack ~5G
│ └── sdwebui.tar.gz ← conda-pack ~4G
├── docker/ ← [gitignore/网盘]
│ └── kohya_ss_image.tar ← Docker镜像
├── logs/ ← [gitignore]
└── meidaojia/ ← 监控测试脚本 [git]
```
---
## 路径迁移策略(核心)
### 策略:BASE_DIR 自动推导 + configure.ini 模板
所有服务通过脚本自身位置自动推导 `BASE_DIR`,不依赖环境变量,clone 到任意目录均可工作。
### 1. configure.ini → 模板化
创建 `hair_service_sd/config/configure.ini.template`[git] 管理),所有 `/home/szlc/project/data/...` 替换为 `__BASE_DIR__/data/...`
关键变更:
- 所有数据路径:`/home/szlc/project/data/...``__BASE_DIR__/data/...`
- `train_dir``/data/train_material``__BASE_DIR__/kohya_ss_home/train_material`(必须在 kohya_ss_home 下,Docker 容器才能访问)
- `logpath``/home/szlc/project/logs``__BASE_DIR__/logs`
- `configure.ini` 本身加入 .gitignore,由 setup.sh 用 sed 替换 `__BASE_DIR__` 生成
### 2. lora_train_service_1.py → BASE_DIR 自动推导
**第 38-43 行**online 模式路径):
```python
# 修改前
kohya_ss_home_dir = '/root/project/kohya_ss_home'
webui_lora_dir = '/gz-fs/Lora'
# 修改后
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
kohya_ss_home_dir = os.path.join(BASE_DIR, 'kohya_ss_home')
webui_lora_dir = os.path.join(BASE_DIR, 'stable-diffusion-webui', 'models', 'Lora')
```
### 3. Docker 训练命令(第 301-315 行)→ 路径映射修正
关键变更:
- `--gpus "device=1"``--gpus "device=0"`(新机器单 GPU
- 删除 `-v /mnt:/mnt`NAS 挂载不再需要)
- 新增 `-v {sd_models_dir}:/mnt/sd_models`(挂载训练底模到容器)
- `--pretrained_model_name_or_path``/mnt/sd_models/majicmixRealistic_v7.safetensors`
- `--train_data_dir``--output_dir`:主机路径转换为容器路径(`kohya_ss_home_dir``/home/chinatszrn`
```python
sd_models_dir = os.path.join(BASE_DIR, 'stable-diffusion-webui', 'models', 'Stable-diffusion')
container_images_dir = images_dir.replace(kohya_ss_home_dir, '/home/chinatszrn')
container_model_dir = model_dir.replace(kohya_ss_home_dir, '/home/chinatszrn')
```
### 4. 其他硬编码路径修复
| 文件 | 行号 | 修改 |
|------|------|------|
| `hairstyle_model_infer.py` | 1096 | 注释掉 `cv2.imwrite('/home/student/Desktop/...')`(**活跃代码路径,不注释会崩溃**) |
| `watch_delete.py` | 8 | 从 configure.ini 读取 userDir 而非硬编码 |
| `config.json` (webui) | - | 删除 `onediff_compiler_caches_path``onediff_compiler_backend` 两行 |
| `gunicorn_config.py` | 21 | 注释掉 `chdir`(未使用) |
| webui extensions | - | 删除失效的 onediff_sd_webui_extensions 符号链接 |
### 5. 安全提醒
`upload_oss.py` 第 8-9 行包含硬编码的阿里云 OSS AccessKey/Secret(作为 `os.getenv` 默认值)。这些密钥会随代码进入 git。如仓库非私有,应改为只从环境变量读取。
---
## Conda 环境迁移
| 环境 | 大小 | 策略 |
|------|------|------|
| my_hair | 10G/289包 | conda-pack 打包为 tar.gz(约 5G 压缩) |
| sdwebui | 7.3G/193包 | conda-pack 打包为 tar.gz(约 4G 压缩) |
| py310 | 701M/32包 | yml 导出(包少,重建可靠) |
setup.sh 中用 `conda pack` 恢复 tar.gz 环境,用 `conda env create -f` 恢复 py310。
---
## 执行步骤
### 阶段 1:创建目录结构 + 复制代码
1. 创建目标目录结构
2. `rsync` 复制 hair_service_sd 代码(排除 weights, __pycache__, .git, *.log, nohup.out
3. `rsync` 复制 photo_service 代码(排除 __pycache__, .idea, *.log, 大图片)
4. `rsync` 复制 stable-diffusion-webui 代码(排除 models/, extensions/sd-webui-controlnet/, repositories/, .git, cache/, outputs/
5. `rsync` 复制 meidaojia(排除 .git, nohup.out, 大图片)
6. 复制 kohya_ss_home/start_docker.sh 到 git 管理
### 阶段 2:复制资源(网盘文件)
7. 复制 hair_service_sd/weights/10G
8. 复制 hair_service_sd/data/9M 静态文件)
9. 复制 stable-diffusion-webui/models/49G,含 33G LoRA
10. 复制 stable-diffusion-webui/extensions/sd-webui-controlnet/4.4G
11. 复制 stable-diffusion-webui/repositories/225M
12. 复制 data/ 业务数据(4.9G
13. 复制 kohya_ss_home/kohya_ss 代码 + .local 7.6G + .cache/clip + .cache/huggingface,排除 pip/torch 缓存)
### 阶段 3:路径修改
14. 创建 `configure.ini.template`
15. 修改 `lora_train_service_1.py`BASE_DIR + Docker 命令)
16. 注释 `hairstyle_model_infer.py:1096`
17. 修改 `watch_delete.py`
18. 清理 `config.json`(删除 onediff 配置)
19. 注释 `gunicorn_config.py:21`
20. 创建 `start_all_services.sh`
21. 创建 `setup.sh`
22. 创建 `README.md`
23. 创建 `.gitignore`
### 阶段 4:导出环境和镜像
24. `conda install -n base -c conda-forge conda-pack`
25. `conda pack -n my_hair -o conda_envs/my_hair.tar.gz`
26. `conda pack -n sdwebui -o conda_envs/sdwebui.tar.gz`
27. `conda env export -n py310 --no-builds > conda_envs/py310.yml`
28. `sudo docker save chinatszrn/ubuntu:kohya_ss -o docker/kohya_ss_image.tar`
### 阶段 5Git 提交
29. `git add .` + 验证 .gitignore 生效(大文件不被追踪)
30. `git commit`
---
## setup.sh 部署脚本功能
新机器上执行 `./setup.sh` 完成:
1. 检查前提条件(git, conda, docker, nvidia-docker
2. 用 sed 从 template 生成 configure.ini
3. 用 conda-pack 恢复 my_hair 和 sdwebui 环境
4. 用 yml 创建 py310 环境
5. `docker load` 导入 kohya_ss 镜像
6. 检查所有模型/数据目录是否存在(报告缺失项)
7. 检查 majicmixRealistic_v7.safetensors 是否已下载
8. 创建运行时目录(tmp, res_dir, userImage 等)
---
## 验证方案
### 环境验证
```bash
# Conda 环境
/home/szlc/miniconda3/envs/my_hair/bin/python -c "import torch; print(torch.cuda.is_available())"
/home/szlc/miniconda3/envs/sdwebui/bin/python -c "import torch; print(torch.cuda.is_available())"
# Docker
sudo docker run --rm --gpus all chinatszrn/ubuntu:kohya_ss nvidia-smi
```
### 服务启动验证
```bash
# 1. hair_service_sd(端口 8801
cd /home/szlc/change_hair_3090/hair_service_sd
/home/szlc/miniconda3/envs/my_hair/bin/python run_copy_cost_colorb64.py
# 预期:看到 "[HAIR_INIT] All init done." 且端口 8801 可访问
# 2. webui(端口 57860
cd /home/szlc/change_hair_3090/stable-diffusion-webui
/home/szlc/miniconda3/envs/sdwebui/bin/python webui.py --api --listen --xformers --port 57860
# 预期:curl http://127.0.0.1:57860/sdapi/v1/options 返回 JSON
# 3. photo_service(端口 32678
cd /home/szlc/change_hair_3090/photo_service
/home/szlc/miniconda3/envs/py310/bin/python lora_train_service_1.py
# 预期:服务启动无报错
```
### 路径一致性验证
```bash
# 无残留旧路径
grep -r "/home/szlc/project" /home/szlc/change_hair_3090/ --include="*.py" --include="*.ini" --include="*.sh"
grep -r "/gz-fs\|/mnt/nas_hdd\|/root/project" /home/szlc/change_hair_3090/ --include="*.py" --include="*.ini"
# 以上应无输出(注释行除外)
```
---
## .gitignore 中的例外
以下文件虽在大文件目录中,但需 git 管理(在 .gitignore 中用 `!` 添加例外):
- `!conda_envs/py310.yml` — py310 环境定义(小文本文件)
- `!kohya_ss_home/start_docker.sh` — Docker 启动脚本(小文件)
---
## 关键风险
1. **hairstyle_model_infer.py:1096** — 活跃代码路径中的 `cv2.imwrite('/home/student/Desktop/...')`,不注释会导致换发色功能崩溃
2. **Docker GPU 设备号** — 原代码 `--gpus "device=1"`,新机器需改为 `device=0`
3. **train_dir 路径变更** — 原 `/data/train_material`121G)不迁移,新路径在 `kohya_ss_home/train_material/`
4. **OSS 密钥** — upload_oss.py 含硬编码密钥,会进入 git
5. **majicmixRealistic_v7** — 需用户手动下载放置到 `stable-diffusion-webui/models/Stable-diffusion/`
6. **conda-pack glibc 兼容性** — 源机器和目标机器需同为 Ubuntu 且 glibc 版本兼容
---
## 网盘需上传的文件清单
| 路径 | 大小 | 说明 |
|------|------|------|
| hair_service_sd/weights/ | 10G | 换发算法模型 |
| stable-diffusion-webui/models/ | 49G | SD模型+LoRA+辅助模型 |
| stable-diffusion-webui/extensions/sd-webui-controlnet/ | 4.4G | ControlNet扩展 |
| stable-diffusion-webui/repositories/ | 225M | SD依赖仓库 |
| kohya_ss_home/ | ~8G | 训练环境(代码+.local+clip缓存) |
| data/ | 4.9G | 业务数据 |
| conda_envs/my_hair.tar.gz | ~5G | conda环境 |
| conda_envs/sdwebui.tar.gz | ~4G | conda环境 |
| docker/kohya_ss_image.tar | ?G | Docker镜像 |
| **合计** | **约 85G+** | |
+123
View File
@@ -0,0 +1,123 @@
# 换发型项目
AI 换发型/换发色/训练发型服务,基于 Stable Diffusion + ControlNet + LoRA。
## 项目结构
```
change_hair_3090/
├── hair_service_sd/ # 换发算法服务 (端口 8801)
├── photo_service/ # LoRA 训练服务 (端口 32678)
├── stable-diffusion-webui/ # SD WebUI 推理服务 (端口 57860)
├── kohya_ss_home/ # kohya_ss 训练环境
├── data/ # 业务数据(发型参考图、用户图片等)
├── conda_envs/ # conda 环境打包文件
├── setup.sh # 一键部署脚本
├── start_all_services.sh # 启动所有服务
└── README.md
```
## 前提条件
- Ubuntu 22.04
- NVIDIA 3090 GPU + 驱动
- conda (Miniconda/Anaconda)
- git
## 部署步骤
### 1. Clone 代码
```bash
git clone <repo_url> ~/change_hair_3090
cd ~/change_hair_3090
```
### 2. 下载网盘文件
将网盘上的以下目录/文件下载到项目根目录(与代码合并):
| 路径 | 大小 | 说明 |
|------|------|------|
| hair_service_sd/weights/ | 10G | 换发算法模型 |
| stable-diffusion-webui/models/ | 41G | SD模型 + LoRA + 辅助模型 |
| stable-diffusion-webui/extensions/sd-webui-controlnet/ | 4.4G | ControlNet 扩展 |
| stable-diffusion-webui/repositories/ | 225M | SD 依赖仓库 |
| kohya_ss_home/.local/ | 7.6G | 训练 pip 包 |
| kohya_ss_home/.cache/ | 3.3M | CLIP 缓存 |
| kohya_ss_home/kohya_ss/ | 439M | 训练代码 |
| data/ | 4.9G | 业务数据 |
| conda_envs/my_hair.tar.gz | ~5G | conda 环境 |
| conda_envs/sdwebui.tar.gz | ~4G | conda 环境 |
### 3. 下载训练底模
从网上下载 `majicmixRealistic_v7.safetensors`,放置到:
```
stable-diffusion-webui/models/Stable-diffusion/majicmixRealistic_v7.safetensors
```
### 4. 运行部署脚本
```bash
chmod +x setup.sh start_all_services.sh
./setup.sh
```
setup.sh 会:
1. 生成 configure.ini(自动替换路径)
2. 恢复 conda 环境(my_hair, sdwebui, py310
3. 检查模型和数据目录完整性
4. 创建运行时目录
### 5. 启动服务
```bash
./start_all_services.sh
```
## 服务说明
| 服务 | 端口 | conda 环境 | 说明 |
|------|------|-----------|------|
| hair_service_sd | 8801 | my_hair | 换发型/换发色核心算法 |
| stable-diffusion-webui | 57860 | sdwebui | SD 图生图推理 |
| photo_service | 32678 | py310 | LoRA 训练调度 |
## 查看日志
```bash
tail -f logs/hair_service.log
tail -f logs/webui.log
tail -f logs/photo_service.log
```
## 停止服务
```bash
# 按端口查找并停止
kill $(lsof -t -i:8801)
kill $(lsof -t -i:57860)
kill $(lsof -t -i:32678)
```
## 常见问题
### conda 环境恢复失败
确保 `CONDA_BASE` 环境变量指向你的 conda 安装路径(默认 `/home/szlc/miniconda3`):
```bash
export CONDA_BASE=/your/conda/path
./setup.sh
```
### GPU 不可用
检查 NVIDIA 驱动和 CUDA
```bash
nvidia-smi
```
### WebUI 启动慢
首次启动需要加载模型(约 30-60 秒),请耐心等待。查看 `logs/webui.log` 确认状态。
### 路径问题
所有路径通过 `BASE_DIR` 自动推导,`configure.ini``setup.sh` 自动生成。如需手动修改配置,编辑 `hair_service_sd/config/configure.ini.template` 后重新运行 `setup.sh`
+55
View File
@@ -0,0 +1,55 @@
name: py310
channels:
- defaults
dependencies:
- _libgcc_mutex=0.1
- _openmp_mutex=5.1
- bzip2=1.0.8
- ca-certificates=2024.7.2
- ld_impl_linux-64=2.38
- libffi=3.4.4
- libgcc-ng=11.2.0
- libgomp=11.2.0
- libstdcxx-ng=11.2.0
- libuuid=1.41.5
- ncurses=6.4
- openssl=3.0.14
- pip=24.0
- python=3.10.14
- readline=8.2
- setuptools=69.5.1
- sqlite=3.45.3
- tk=8.6.14
- tqdm=4.66.4
- tzdata=2024a
- wheel=0.43.0
- xz=5.4.6
- zlib=1.2.13
- pip:
- blinker==1.8.2
- certifi==2024.7.4
- charset-normalizer==3.3.2
- click==8.1.7
- flask==3.0.3
- gevent==24.2.1
- greenlet==3.0.3
- idna==3.7
- imageio==2.34.2
- itsdangerous==2.2.0
- jinja2==3.1.4
- lazy-loader==0.4
- markupsafe==2.1.5
- networkx==3.3
- numpy==2.0.0
- opencv-python==4.10.0.84
- packaging==24.1
- pillow==10.4.0
- requests==2.32.3
- scikit-image==0.24.0
- scipy==1.14.0
- tifffile==2024.7.2
- urllib3==2.2.2
- werkzeug==3.0.3
- zope-event==5.0
- zope-interface==6.4.post2
prefix: /home/szlc/miniconda3/envs/py310
+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
+268
View File
@@ -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
+298
View File
@@ -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())
+330
View File
@@ -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()
View File
+46
View File
@@ -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
+27
View File
@@ -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")
View File
View File
@@ -0,0 +1,34 @@
[default]
modelDir = weights
hairstyleDir = __BASE_DIR__/data/ref_hairstyle
haircolorDir= __BASE_DIR__/data/ref_haircolor
userDir=__BASE_DIR__/data/userImage
tmp_dir=__BASE_DIR__/data/tmp
res_dir=__BASE_DIR__/data/res_dir
userInfo_dir=__BASE_DIR__/data/user_info
baseColor_ID=HDR10_443322
Port = 11023
refer_dir = __BASE_DIR__/data/ref_online
ref_user_dir = __BASE_DIR__/data/ref_user_imgs
train_dir = __BASE_DIR__/kohya_ss_home/train_material
refImgDir=__BASE_DIR__/data/refImage
hair_template_material_dir=__BASE_DIR__/data/hair_template_material
ref_color=__BASE_DIR__/data/ref_color
ref_color_img=__BASE_DIR__/data/ref_color_imgs
upload_train_dir=__BASE_DIR__/data/upload_train_imgs
;version=local
version=online
[fix]
strength=1
[logger]
logpath = __BASE_DIR__/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.
View File
@@ -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
+57
View File
@@ -0,0 +1,57 @@
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
import sys
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等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成
secret_id = 'AKIDAIZ3MTLyNXRaWLvGAsLaZhgbzQjqkyGN' # 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
secret_key = '73kEZgjRxIsGQTM4oaFjCXGp9IH7xgIm' # 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
self.region = 'ap-beijing' # 替换为用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket
# COS 支持的所有 region 列表参见https://cloud.tencent.com/document/product/436/6224
token = None # 如果使用永久密钥不需要填入 token,如果使用临时密钥需要填入,临时密钥生成和使用指引参见 https://cloud.tencent.com/document/product/436/14048
self.scheme = 'https' # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
self.BucketName= 'ydapp-1317132355'
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)
+2
View File
@@ -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()])],
)
+142
View File
@@ -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)
+213
View File
@@ -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
+135
View File
@@ -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
+24
View File
@@ -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)
+102
View File
@@ -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
+186
View File
@@ -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
+75
View File
@@ -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
+624
View File
@@ -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)
+210
View File
@@ -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)
+102
View File
@@ -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
+133
View File
@@ -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
+42
View File
@@ -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
}
+259
View File
@@ -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
@@ -0,0 +1,2 @@
from .functions import *
from .modules import *
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
from .wider_face import WiderFaceDetection, detection_collate
from .data_augment import *
from .config import *
@@ -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,237 @@
import cv2
import numpy as np
import random
from core.utils.box_utils_Retina import matrix_iof
def _crop(image, boxes, labels, landm, img_dim):
height, width, _ = image.shape
pad_image_flag = True
for _ in range(250):
"""
if random.uniform(0, 1) <= 0.2:
scale = 1.0
else:
scale = random.uniform(0.3, 1.0)
"""
PRE_SCALES = [0.3, 0.45, 0.6, 0.8, 1.0]
scale = random.choice(PRE_SCALES)
short_side = min(width, height)
w = int(scale * short_side)
h = w
if width == w:
l = 0
else:
l = random.randrange(width - w)
if height == h:
t = 0
else:
t = random.randrange(height - h)
roi = np.array((l, t, l + w, t + h))
value = matrix_iof(boxes, roi[np.newaxis])
flag = (value >= 1)
if not flag.any():
continue
centers = (boxes[:, :2] + boxes[:, 2:]) / 2
mask_a = np.logical_and(roi[:2] < centers, centers < roi[2:]).all(axis=1)
boxes_t = boxes[mask_a].copy()
labels_t = labels[mask_a].copy()
landms_t = landm[mask_a].copy()
landms_t = landms_t.reshape([-1, 5, 2])
if boxes_t.shape[0] == 0:
continue
image_t = image[roi[1]:roi[3], roi[0]:roi[2]]
boxes_t[:, :2] = np.maximum(boxes_t[:, :2], roi[:2])
boxes_t[:, :2] -= roi[:2]
boxes_t[:, 2:] = np.minimum(boxes_t[:, 2:], roi[2:])
boxes_t[:, 2:] -= roi[:2]
# landm
landms_t[:, :, :2] = landms_t[:, :, :2] - roi[:2]
landms_t[:, :, :2] = np.maximum(landms_t[:, :, :2], np.array([0, 0]))
landms_t[:, :, :2] = np.minimum(landms_t[:, :, :2], roi[2:] - roi[:2])
landms_t = landms_t.reshape([-1, 10])
# make sure that the cropped image contains at least one face > 16 pixel at training image scale
b_w_t = (boxes_t[:, 2] - boxes_t[:, 0] + 1) / w * img_dim
b_h_t = (boxes_t[:, 3] - boxes_t[:, 1] + 1) / h * img_dim
mask_b = np.minimum(b_w_t, b_h_t) > 0.0
boxes_t = boxes_t[mask_b]
labels_t = labels_t[mask_b]
landms_t = landms_t[mask_b]
if boxes_t.shape[0] == 0:
continue
pad_image_flag = False
return image_t, boxes_t, labels_t, landms_t, pad_image_flag
return image, boxes, labels, landm, pad_image_flag
def _distort(image):
def _convert(image, alpha=1, beta=0):
tmp = image.astype(float) * alpha + beta
tmp[tmp < 0] = 0
tmp[tmp > 255] = 255
image[:] = tmp
image = image.copy()
if random.randrange(2):
#brightness distortion
if random.randrange(2):
_convert(image, beta=random.uniform(-32, 32))
#contrast distortion
if random.randrange(2):
_convert(image, alpha=random.uniform(0.5, 1.5))
image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
#saturation distortion
if random.randrange(2):
_convert(image[:, :, 1], alpha=random.uniform(0.5, 1.5))
#hue distortion
if random.randrange(2):
tmp = image[:, :, 0].astype(int) + random.randint(-18, 18)
tmp %= 180
image[:, :, 0] = tmp
image = cv2.cvtColor(image, cv2.COLOR_HSV2BGR)
else:
#brightness distortion
if random.randrange(2):
_convert(image, beta=random.uniform(-32, 32))
image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
#saturation distortion
if random.randrange(2):
_convert(image[:, :, 1], alpha=random.uniform(0.5, 1.5))
#hue distortion
if random.randrange(2):
tmp = image[:, :, 0].astype(int) + random.randint(-18, 18)
tmp %= 180
image[:, :, 0] = tmp
image = cv2.cvtColor(image, cv2.COLOR_HSV2BGR)
#contrast distortion
if random.randrange(2):
_convert(image, alpha=random.uniform(0.5, 1.5))
return image
def _expand(image, boxes, fill, p):
if random.randrange(2):
return image, boxes
height, width, depth = image.shape
scale = random.uniform(1, p)
w = int(scale * width)
h = int(scale * height)
left = random.randint(0, w - width)
top = random.randint(0, h - height)
boxes_t = boxes.copy()
boxes_t[:, :2] += (left, top)
boxes_t[:, 2:] += (left, top)
expand_image = np.empty(
(h, w, depth),
dtype=image.dtype)
expand_image[:, :] = fill
expand_image[top:top + height, left:left + width] = image
image = expand_image
return image, boxes_t
def _mirror(image, boxes, landms):
_, width, _ = image.shape
if random.randrange(2):
image = image[:, ::-1]
boxes = boxes.copy()
boxes[:, 0::2] = width - boxes[:, 2::-2]
# landm
landms = landms.copy()
landms = landms.reshape([-1, 5, 2])
landms[:, :, 0] = width - landms[:, :, 0]
tmp = landms[:, 1, :].copy()
landms[:, 1, :] = landms[:, 0, :]
landms[:, 0, :] = tmp
tmp1 = landms[:, 4, :].copy()
landms[:, 4, :] = landms[:, 3, :]
landms[:, 3, :] = tmp1
landms = landms.reshape([-1, 10])
return image, boxes, landms
def _pad_to_square(image, rgb_mean, pad_image_flag):
if not pad_image_flag:
return image
height, width, _ = image.shape
long_side = max(width, height)
image_t = np.empty((long_side, long_side, 3), dtype=image.dtype)
image_t[:, :] = rgb_mean
image_t[0:0 + height, 0:0 + width] = image
return image_t
def _resize_subtract_mean(image, insize, rgb_mean):
interp_methods = [cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_NEAREST, cv2.INTER_LANCZOS4]
interp_method = interp_methods[random.randrange(5)]
image = cv2.resize(image, (insize, insize), interpolation=interp_method)
image = image.astype(np.float32)
image -= rgb_mean
return image.transpose(2, 0, 1)
class preproc(object):
def __init__(self, img_dim, rgb_means):
self.img_dim = img_dim
self.rgb_means = rgb_means
def __call__(self, image, targets):
assert targets.shape[0] > 0, "this image does not have gt"
boxes = targets[:, :4].copy()
labels = targets[:, -1].copy()
landm = targets[:, 4:-1].copy()
image_t, boxes_t, labels_t, landm_t, pad_image_flag = _crop(image, boxes, labels, landm, self.img_dim)
image_t = _distort(image_t)
image_t = _pad_to_square(image_t,self.rgb_means, pad_image_flag)
image_t, boxes_t, landm_t = _mirror(image_t, boxes_t, landm_t)
height, width, _ = image_t.shape
image_t = _resize_subtract_mean(image_t, self.img_dim, self.rgb_means)
boxes_t[:, 0::2] /= width
boxes_t[:, 1::2] /= height
landm_t[:, 0::2] /= width
landm_t[:, 1::2] /= height
labels_t = np.expand_dims(labels_t, 1)
targets_t = np.hstack((boxes_t, landm_t, labels_t))
return image_t, targets_t
@@ -0,0 +1,101 @@
import os
import os.path
import sys
import torch
import torch.utils.data as data
import cv2
import numpy as np
class WiderFaceDetection(data.Dataset):
def __init__(self, txt_path, preproc=None):
self.preproc = preproc
self.imgs_path = []
self.words = []
f = open(txt_path,'r')
lines = f.readlines()
isFirst = True
labels = []
for line in lines:
line = line.rstrip()
if line.startswith('#'):
if isFirst is True:
isFirst = False
else:
labels_copy = labels.copy()
self.words.append(labels_copy)
labels.clear()
path = line[2:]
path = txt_path.replace('label.txt','images/') + path
self.imgs_path.append(path)
else:
line = line.split(' ')
label = [float(x) for x in line]
labels.append(label)
self.words.append(labels)
def __len__(self):
return len(self.imgs_path)
def __getitem__(self, index):
img = cv2.imread(self.imgs_path[index])
height, width, _ = img.shape
labels = self.words[index]
annotations = np.zeros((0, 15))
if len(labels) == 0:
return annotations
for idx, label in enumerate(labels):
annotation = np.zeros((1, 15))
# bbox
annotation[0, 0] = label[0] # x1
annotation[0, 1] = label[1] # y1
annotation[0, 2] = label[0] + label[2] # x2
annotation[0, 3] = label[1] + label[3] # y2
# landmarks
annotation[0, 4] = label[4] # l0_x
annotation[0, 5] = label[5] # l0_y
annotation[0, 6] = label[7] # l1_x
annotation[0, 7] = label[8] # l1_y
annotation[0, 8] = label[10] # l2_x
annotation[0, 9] = label[11] # l2_y
annotation[0, 10] = label[13] # l3_x
annotation[0, 11] = label[14] # l3_y
annotation[0, 12] = label[16] # l4_x
annotation[0, 13] = label[17] # l4_y
if (annotation[0, 4]<0):
annotation[0, 14] = -1
else:
annotation[0, 14] = 1
annotations = np.append(annotations, annotation, axis=0)
target = np.array(annotations)
if self.preproc is not None:
img, target = self.preproc(img, target)
return torch.from_numpy(img), target
def detection_collate(batch):
"""Custom collate fn for dealing with batches of images that have a different
number of associated object annotations (bounding boxes).
Arguments:
batch: (tuple) A tuple of tensor images and lists of annotations
Return:
A tuple containing:
1) (tensor) batch of images stacked on their 0 dim
2) (list of tensors) annotations for a given image are stacked on 0 dim
"""
targets = []
imgs = []
for _, sample in enumerate(batch):
for _, tup in enumerate(sample):
if torch.is_tensor(tup):
imgs.append(tup)
elif isinstance(tup, type(np.empty(0))):
annos = torch.from_numpy(tup).float()
targets.append(annos)
return (torch.stack(imgs, 0), targets)
@@ -0,0 +1,34 @@
import torch
from itertools import product as product
import numpy as np
from math import ceil
class PriorBox(object):
def __init__(self, cfg, image_size=None, phase='train'):
super(PriorBox, self).__init__()
self.min_sizes = cfg['min_sizes']
self.steps = cfg['steps']
self.clip = cfg['clip']
self.image_size = image_size
self.feature_maps = [[ceil(self.image_size[0]/step), ceil(self.image_size[1]/step)] for step in self.steps]
self.name = "s"
def forward(self):
anchors = []
for k, f in enumerate(self.feature_maps):
min_sizes = self.min_sizes[k]
for i, j in product(range(f[0]), range(f[1])):
for min_size in min_sizes:
s_kx = min_size / self.image_size[1]
s_ky = min_size / self.image_size[0]
dense_cx = [x * self.steps[k] / self.image_size[1] for x in [j + 0.5]]
dense_cy = [y * self.steps[k] / self.image_size[0] for y in [i + 0.5]]
for cy, cx in product(dense_cy, dense_cx):
anchors += [cx, cy, s_kx, s_ky]
# back to torch land
output = torch.Tensor(anchors).view(-1, 4)
if self.clip:
output.clamp_(max=1, min=0)
return output
@@ -0,0 +1,3 @@
from .multibox_loss import MultiBoxLoss
__all__ = ['MultiBoxLoss']
@@ -0,0 +1,125 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
from core.utils.box_utils_Retina import match, log_sum_exp
from core.models.layers.data import cfg_mnet
GPU = cfg_mnet['gpu_train']
class MultiBoxLoss(nn.Module):
"""SSD Weighted Loss Function
Compute Targets:
1) Produce Confidence Target Indices by matching ground truth boxes
with (default) 'priorboxes' that have jaccard index > threshold parameter
(default threshold: 0.5).
2) Produce localization target by 'encoding' variance into offsets of ground
truth boxes and their matched 'priorboxes'.
3) Hard negative mining to filter the excessive number of negative examples
that comes with using a large number of default bounding boxes.
(default negative:positive ratio 3:1)
Objective Loss:
L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N
Where, Lconf is the CrossEntropy Loss and Lloc is the SmoothL1 Loss
weighted by α which is set to 1 by cross val.
Args:
c: class confidences,
l: predicted boxes,
g: ground truth boxes
N: number of matched default boxes
See: https://arxiv.org/pdf/1512.02325.pdf for more details.
"""
def __init__(self, num_classes, overlap_thresh, prior_for_matching, bkg_label, neg_mining, neg_pos, neg_overlap, encode_target):
super(MultiBoxLoss, self).__init__()
self.num_classes = num_classes
self.threshold = overlap_thresh
self.background_label = bkg_label
self.encode_target = encode_target
self.use_prior_for_matching = prior_for_matching
self.do_neg_mining = neg_mining
self.negpos_ratio = neg_pos
self.neg_overlap = neg_overlap
self.variance = [0.1, 0.2]
def forward(self, predictions, priors, targets):
"""Multibox Loss
Args:
predictions (tuple): A tuple containing loc preds, conf preds,
and prior boxes from SSD net.
conf shape: torch.size(batch_size,num_priors,num_classes)
loc shape: torch.size(batch_size,num_priors,4)
priors shape: torch.size(num_priors,4)
ground_truth (tensor): Ground truth boxes and labels for a batch,
shape: [batch_size,num_objs,5] (last idx is the label).
"""
loc_data, conf_data, landm_data = predictions
priors = priors
num = loc_data.size(0)
num_priors = (priors.size(0))
# match priors (default boxes) and ground truth boxes
loc_t = torch.Tensor(num, num_priors, 4)
landm_t = torch.Tensor(num, num_priors, 10)
conf_t = torch.LongTensor(num, num_priors)
for idx in range(num):
truths = targets[idx][:, :4].data
labels = targets[idx][:, -1].data
landms = targets[idx][:, 4:14].data
defaults = priors.data
match(self.threshold, truths, defaults, self.variance, labels, landms, loc_t, conf_t, landm_t, idx)
if GPU:
loc_t = loc_t.cuda()
conf_t = conf_t.cuda()
landm_t = landm_t.cuda()
zeros = torch.tensor(0).cuda()
# landm Loss (Smooth L1)
# Shape: [batch,num_priors,10]
pos1 = conf_t > zeros
num_pos_landm = pos1.long().sum(1, keepdim=True)
N1 = max(num_pos_landm.data.sum().float(), 1)
pos_idx1 = pos1.unsqueeze(pos1.dim()).expand_as(landm_data)
landm_p = landm_data[pos_idx1].view(-1, 10)
landm_t = landm_t[pos_idx1].view(-1, 10)
loss_landm = F.smooth_l1_loss(landm_p, landm_t, reduction='sum')
pos = conf_t != zeros
conf_t[pos] = 1
# Localization Loss (Smooth L1)
# Shape: [batch,num_priors,4]
pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
loc_p = loc_data[pos_idx].view(-1, 4)
loc_t = loc_t[pos_idx].view(-1, 4)
loss_l = F.smooth_l1_loss(loc_p, loc_t, reduction='sum')
# Compute max conf across batch for hard negative mining
batch_conf = conf_data.view(-1, self.num_classes)
loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
# Hard Negative Mining
loss_c[pos.view(-1, 1)] = 0 # filter out pos boxes for now
loss_c = loss_c.view(num, -1)
_, loss_idx = loss_c.sort(1, descending=True)
_, idx_rank = loss_idx.sort(1)
num_pos = pos.long().sum(1, keepdim=True)
num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
neg = idx_rank < num_neg.expand_as(idx_rank)
# Confidence Loss Including Positive and Negative Examples
pos_idx = pos.unsqueeze(2).expand_as(conf_data)
neg_idx = neg.unsqueeze(2).expand_as(conf_data)
conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1,self.num_classes)
targets_weighted = conf_t[(pos+neg).gt(0)]
loss_c = F.cross_entropy(conf_p, targets_weighted, reduction='sum')
# Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N
N = max(num_pos.data.sum().float(), 1)
loss_l /= N
loss_c /= N
loss_landm /= N1
return loss_l, loss_c, loss_landm
+137
View File
@@ -0,0 +1,137 @@
import time
import torch
import torch.nn as nn
# import torchvision.models._utils as _utils
# import torchvision.models as models
import torch.nn.functional as F
# from torch.autograd import Variable
def conv_bn(inp, oup, stride = 1, leaky = 0):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
nn.LeakyReLU(negative_slope=leaky, inplace=True)
)
def conv_bn_no_relu(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
)
def conv_bn1X1(inp, oup, stride, leaky=0):
return nn.Sequential(
nn.Conv2d(inp, oup, 1, stride, padding=0, bias=False),
nn.BatchNorm2d(oup),
nn.LeakyReLU(negative_slope=leaky, inplace=True)
)
def conv_dw(inp, oup, stride, leaky=0.1):
return nn.Sequential(
nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),
nn.BatchNorm2d(inp),
nn.LeakyReLU(negative_slope= leaky,inplace=True),
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
nn.LeakyReLU(negative_slope= leaky,inplace=True),
)
class SSH(nn.Module):
def __init__(self, in_channel, out_channel):
super(SSH, self).__init__()
assert out_channel % 4 == 0
leaky = 0
if (out_channel <= 64):
leaky = 0.1
self.conv3X3 = conv_bn_no_relu(in_channel, out_channel//2, stride=1)
self.conv5X5_1 = conv_bn(in_channel, out_channel//4, stride=1, leaky = leaky)
self.conv5X5_2 = conv_bn_no_relu(out_channel//4, out_channel//4, stride=1)
self.conv7X7_2 = conv_bn(out_channel//4, out_channel//4, stride=1, leaky = leaky)
self.conv7x7_3 = conv_bn_no_relu(out_channel//4, out_channel//4, stride=1)
def forward(self, input):
conv3X3 = self.conv3X3(input)
conv5X5_1 = self.conv5X5_1(input)
conv5X5 = self.conv5X5_2(conv5X5_1)
conv7X7_2 = self.conv7X7_2(conv5X5_1)
conv7X7 = self.conv7x7_3(conv7X7_2)
out = torch.cat([conv3X3, conv5X5, conv7X7], dim=1)
out = F.relu(out)
return out
class FPN(nn.Module):
def __init__(self,in_channels_list,out_channels):
super(FPN,self).__init__()
leaky = 0
if (out_channels <= 64):
leaky = 0.1
self.output1 = conv_bn1X1(in_channels_list[0], out_channels, stride = 1, leaky = leaky)
self.output2 = conv_bn1X1(in_channels_list[1], out_channels, stride = 1, leaky = leaky)
self.output3 = conv_bn1X1(in_channels_list[2], out_channels, stride = 1, leaky = leaky)
self.merge1 = conv_bn(out_channels, out_channels, leaky = leaky)
self.merge2 = conv_bn(out_channels, out_channels, leaky = leaky)
def forward(self, input):
# names = list(input.keys())
input = list(input.values())
output1 = self.output1(input[0])
output2 = self.output2(input[1])
output3 = self.output3(input[2])
up3 = F.interpolate(output3, size=[output2.size(2), output2.size(3)], mode="nearest")
output2 = output2 + up3
output2 = self.merge2(output2)
up2 = F.interpolate(output2, size=[output1.size(2), output1.size(3)], mode="nearest")
output1 = output1 + up2
output1 = self.merge1(output1)
out = [output1, output2, output3]
return out
class MobileNetV1(nn.Module):
def __init__(self):
super(MobileNetV1, self).__init__()
self.stage1 = nn.Sequential(
conv_bn(3, 8, 2, leaky = 0.1), # 3
conv_dw(8, 16, 1), # 7
conv_dw(16, 32, 2), # 11
conv_dw(32, 32, 1), # 19
conv_dw(32, 64, 2), # 27
conv_dw(64, 64, 1), # 43
)
self.stage2 = nn.Sequential(
conv_dw(64, 128, 2), # 43 + 16 = 59
conv_dw(128, 128, 1), # 59 + 32 = 91
conv_dw(128, 128, 1), # 91 + 32 = 123
conv_dw(128, 128, 1), # 123 + 32 = 155
conv_dw(128, 128, 1), # 155 + 32 = 187
conv_dw(128, 128, 1), # 187 + 32 = 219
)
self.stage3 = nn.Sequential(
conv_dw(128, 256, 2), # 219 +3 2 = 241
conv_dw(256, 256, 1), # 241 + 64 = 301
)
self.avg = nn.AdaptiveAvgPool2d((1,1))
self.fc = nn.Linear(256, 1000)
def forward(self, x):
x = self.stage1(x)
x = self.stage2(x)
x = self.stage3(x)
x = self.avg(x)
# x = self.model(x)
x = x.view(-1, 256)
x = self.fc(x)
return x
@@ -0,0 +1,46 @@
# --------------------------------------------------------
# Fast R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick
# --------------------------------------------------------
import numpy as np
def py_cpu_nms(dets, thresh, min_face_size = 50):
"""Pure Python NMS baseline."""
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 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
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= thresh)[0]
order = order[inds + 1]
#filter_small_faces
filter_list = []
for idx in keep:
w = np.abs(dets[idx, 2] - dets[idx, 0])
h = np.abs(dets[idx, 3] - dets[idx, 1])
if max(w, h) < min_face_size: continue
filter_list.append(idx)
return filter_list
+342
View File
@@ -0,0 +1,342 @@
import torch
import torch.nn as nn
__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
'wide_resnet50_2', 'wide_resnet101_2']
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',
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}
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, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = 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.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):
# Bottleneck in torchvision places the stride for downsampling at 3x3 convolution(self.conv2)
# while original implementation places the stride at the first 1x1 convolution(self.conv1)
# according to "Deep residual learning for image recognition"https://arxiv.org/abs/1512.03385.
# This variant is also known as ResNet V1.5 and improves accuracy according to
# https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch.
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(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, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=2, padding=3, bias=False) ### ycj
self.bn1 = norm_layer(self.inplanes)
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,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
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.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
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, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))
return nn.Sequential(*layers)
def _forward_impl(self, x):
# See note [TorchScript super()]
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)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
def forward(self, x):
return self._forward_impl(x)
def _resnet(arch, block, layers, pretrained, progress, n_class, **kwargs):
model = ResNet(block, layers, num_classes=n_class, **kwargs)
# if pretrained:
# state_dict = load_state_dict_from_url(model_urls[arch], progress=progress)
# model.load_state_dict(state_dict)
return model
def resnet18(n_class=1000, pretrained=False, progress=True, **kwargs):
r"""ResNet-18 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress, n_class,
**kwargs)
def resnet34(pretrained=False, progress=True, **kwargs):
r"""ResNet-34 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)
def resnet50(n_class=1000, pretrained=False, progress=True, **kwargs):
r"""ResNet-50 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress, n_class, **kwargs)
def resnet101(n_class=1000, pretrained=False, progress=True, **kwargs):
r"""ResNet-101 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress, n_class, **kwargs)
def resnet152(n_class=1000, pretrained=False, progress=True, **kwargs):
r"""ResNet-152 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress, n_class,
**kwargs)
def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-50 32x4d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)
def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-101 32x8d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)
def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-50-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_
The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)
def wide_resnet101_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-101-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_
The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)
if __name__ == '__main__':
net = resnet18(n_class=33)
input_size = (1, 3, 224, 224)
x = torch.randn(input_size)
out = net(x)
print(out.shape)
+127
View File
@@ -0,0 +1,127 @@
import torch
import torch.nn as nn
# import torchvision.models.detection.backbone_utils as backbone_utils
import torchvision.models._utils as _utils
import torch.nn.functional as F
# from collections import OrderedDict
from core.models.net import MobileNetV1 as MobileNetV1
from core.models.net import FPN as FPN
from core.models.net import SSH as SSH
class ClassHead(nn.Module):
def __init__(self,inchannels=512,num_anchors=3):
super(ClassHead,self).__init__()
self.num_anchors = num_anchors
self.conv1x1 = nn.Conv2d(inchannels,self.num_anchors*2,kernel_size=(1,1),stride=1,padding=0)
def forward(self,x):
out = self.conv1x1(x)
out = out.permute(0,2,3,1).contiguous()
return out.view(out.shape[0], -1, 2)
class BboxHead(nn.Module):
def __init__(self,inchannels=512,num_anchors=3):
super(BboxHead,self).__init__()
self.conv1x1 = nn.Conv2d(inchannels,num_anchors*4,kernel_size=(1,1),stride=1,padding=0)
def forward(self,x):
out = self.conv1x1(x)
out = out.permute(0,2,3,1).contiguous()
return out.view(out.shape[0], -1, 4)
class LandmarkHead(nn.Module):
def __init__(self,inchannels=512,num_anchors=3):
super(LandmarkHead,self).__init__()
self.conv1x1 = nn.Conv2d(inchannels,num_anchors*10,kernel_size=(1,1),stride=1,padding=0)
def forward(self,x):
out = self.conv1x1(x)
out = out.permute(0,2,3,1).contiguous()
return out.view(out.shape[0], -1, 10)
class RetinaFace(nn.Module):
def __init__(self, cfg = None, phase = 'train'):
"""
:param cfg: Network related settings.
:param phase: train or test.
"""
super(RetinaFace,self).__init__()
self.phase = phase
backbone = None
if cfg['name'] == 'mobilenet0.25':
backbone = MobileNetV1()
if cfg['pretrain']:
checkpoint = torch.load("./weights/mobilenetV1X0.25_pretrain.tar", map_location=torch.device('cpu'))
from collections import OrderedDict
new_state_dict = OrderedDict()
for k, v in checkpoint['state_dict'].items():
name = k[7:] # remove module.
new_state_dict[name] = v
# load params
backbone.load_state_dict(new_state_dict)
elif cfg['name'] == 'Resnet50':
import torchvision.models as models
backbone = models.resnet50(pretrained=cfg['pretrain'])
self.body = _utils.IntermediateLayerGetter(backbone, cfg['return_layers'])
in_channels_stage2 = cfg['in_channel']
in_channels_list = [
in_channels_stage2 * 2,
in_channels_stage2 * 4,
in_channels_stage2 * 8,
]
out_channels = cfg['out_channel']
self.fpn = FPN(in_channels_list,out_channels)
self.ssh1 = SSH(out_channels, out_channels)
self.ssh2 = SSH(out_channels, out_channels)
self.ssh3 = SSH(out_channels, out_channels)
self.ClassHead = self._make_class_head(fpn_num=3, inchannels=cfg['out_channel'])
self.BboxHead = self._make_bbox_head(fpn_num=3, inchannels=cfg['out_channel'])
self.LandmarkHead = self._make_landmark_head(fpn_num=3, inchannels=cfg['out_channel'])
def _make_class_head(self,fpn_num=3,inchannels=64,anchor_num=2):
classhead = nn.ModuleList()
for i in range(fpn_num):
classhead.append(ClassHead(inchannels,anchor_num))
return classhead
def _make_bbox_head(self,fpn_num=3,inchannels=64,anchor_num=2):
bboxhead = nn.ModuleList()
for i in range(fpn_num):
bboxhead.append(BboxHead(inchannels,anchor_num))
return bboxhead
def _make_landmark_head(self,fpn_num=3,inchannels=64,anchor_num=2):
landmarkhead = nn.ModuleList()
for i in range(fpn_num):
landmarkhead.append(LandmarkHead(inchannels,anchor_num))
return landmarkhead
def forward(self,inputs):
out = self.body(inputs)
# FPN
fpn = self.fpn(out)
# SSH
feature1 = self.ssh1(fpn[0])
feature2 = self.ssh2(fpn[1])
feature3 = self.ssh3(fpn[2])
features = [feature1, feature2, feature3]
bbox_regressions = torch.cat([self.BboxHead[i](feature) for i, feature in enumerate(features)], dim=1)
classifications = torch.cat([self.ClassHead[i](feature) for i, feature in enumerate(features)],dim=1)
ldm_regressions = torch.cat([self.LandmarkHead[i](feature) for i, feature in enumerate(features)], dim=1)
if self.phase == 'train':
output = (bbox_regressions, classifications, ldm_regressions)
else:
output = (bbox_regressions, F.softmax(classifications, dim=-1), ldm_regressions)
return output
+1
View File
@@ -0,0 +1 @@
+133
View File
@@ -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
+242
View File
@@ -0,0 +1,242 @@
import math
import numpy as np
import torch
from .model import PNet, RNet, ONet
from .box_utils import nms, calibrate_box, get_image_boxes, convert_to_square, _preprocess
import torch
import cv2
def detect_faces(image, min_face_size=20.0, thresholds=[0.6, 0.7, 0.8],
nms_thresholds=[0.7, 0.7, 0.7], 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()
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=gpu_id)
bounding_boxes.append(boxes)
bounding_boxes = [i for i in bounding_boxes if i is not None]
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(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(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
class MTCNNFaceDetector(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.pnet, self.rnet, self.onet = PNet(), RNet(), ONet()
self.pnet.to(self.device)
self.rnet.to(self.device)
self.onet.to(self.device)
self.onet.eval()
def forward(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, self.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 = self.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 = self.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
+242
View File
@@ -0,0 +1,242 @@
import math
import numpy as np
import torch
from .model import PNet, RNet, ONet
from .box_utils import nms, calibrate_box, get_image_boxes, convert_to_square, _preprocess
import torch
import cv2
def detect_faces(image, min_face_size=20.0, thresholds=[0.6, 0.7, 0.8],
nms_thresholds=[0.7, 0.7, 0.7], 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()
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=gpu_id)
bounding_boxes.append(boxes)
bounding_boxes = [i for i in bounding_boxes if i is not None]
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(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(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
class MTCNNFaceDetector(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.pnet, self.rnet, self.onet = PNet(), RNet(), ONet()
self.pnet.to(self.device)
self.rnet.to(self.device)
self.onet.to(self.device)
self.onet.eval()
def forward(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, self.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 = self.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 = self.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