2 Commits
Author SHA1 Message Date
flavioyandClaude Opus 4.6 ec94f2e04b 添加PyInstaller macOS打包支持:资源路径处理、multiprocessing兼容、配置文件外置
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:08:37 +08:00
flavioyandClaude Opus 4.6 2d1d3fe37b 移除ProPainter模式:删除代码、模型、UI设置和翻译
ProPainter算法占用341MB模型文件且需要大量显存,移除以精简项目。
- 删除 backend/inpaint/propainter_inpaint.py 和 video/ 模块
- 删除 backend/models/propainter/ 模型目录
- 移除 InpaintMode.PROPAINTER 枚举和 propainterMaxLoadNum 配置
- 移除高级设置页面的 ProPainter 设置组
- 清理所有7种语言的翻译文件
- 更新 README 和 .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:20:29 +08:00
62 changed files with 27 additions and 8839 deletions
-1
View File
@@ -369,7 +369,6 @@ test_*.mp4
test*_no_sub*.mp4
/test/coods/
/local_test/
/backend/models/propainter/ProPainter.pth
/backend/models/big-lama/big-lama.pt
/test/debug/
/backend/tools/train/release_model/
+1 -2
View File
@@ -73,7 +73,7 @@ options:
Output video file path (optional)
--subtitle-area-coords YMIN YMAX XMIN XMAX, -c YMIN YMAX XMIN XMAX
Subtitle area coordinates (ymin ymax xmin xmax). Can be specified multiple times for multiple areas.
--inpaint-mode {sttn-auto,sttn-det,lama,propainter,opencv}
--inpaint-mode {sttn-auto,sttn-det,lama,opencv}
Inpaint mode, default is sttn-auto
```
## 演示
@@ -234,7 +234,6 @@ STTN_SKIP_DETECTION = True # 跳过字幕检测,跳过后可能会导致要去
> - InpaintMode.STTN 算法:对于真人视频效果较好,速度快,可以跳过字幕检测
> - InpaintMode.LAMA 算法:对于图片效果最好,对动画类视频效果好,速度一般,不可以跳过字幕检测
> - InpaintMode.PROPAINTER 算法: 需要消耗大量显存,速度较慢,对运动非常剧烈的视频效果较好
- 使用STTN算法
+1 -2
View File
@@ -73,7 +73,7 @@ options:
Output video file path (optional)
--subtitle-area-coords YMIN YMAX XMIN XMAX, -c YMIN YMAX XMIN XMAX
Subtitle area coordinates (ymin ymax xmin xmax). Can be specified multiple times for multiple areas.
--inpaint-mode {sttn-auto,sttn-det,lama,propainter,opencv}
--inpaint-mode {sttn-auto,sttn-det,lama,opencv}
Inpaint mode, default is sttn-auto
```
## Demonstration
@@ -234,7 +234,6 @@ Modify the values in backend/config.py and try different removal algorithms. Her
> - InpaintMode.STTN algorithm: Good for live-action videos and fast in speed, capable of skipping subtitle detection
> - InpaintMode.LAMA algorithm: Best for images and effective for animated videos, moderate speed, unable to skip subtitle detection
> - InpaintMode.PROPAINTER algorithm: Consumes a significant amount of VRAM, slower in speed, works better for videos with very intense movement
- Using the STTN algorithm
+8 -8
View File
@@ -1,5 +1,6 @@
import os
import sys
from pathlib import Path
from qfluentwidgets import (qconfig, ConfigItem, QConfig, OptionsValidator, BoolValidator, OptionsConfigItem,
EnumSerializer, RangeValidator, RangeConfigItem, ConfigValidator)
@@ -47,7 +48,6 @@ class Config(QConfig):
- InpaintMode.STTN_AUTO 智能擦除版
- InpaintMode.STTN_DET 带字幕检测版, 无智能擦除
- InpaintMode.LAMA 算法:对于动画类视频效果好,速度一般,不可以跳过字幕检测
- InpaintMode.PROPAINTER 算法: 需要消耗大量显存,速度较慢,对运动非常剧烈的视频效果较好
"""
# 【设置inpaint算法】
inpaintMode = OptionsConfigItem("Main", "InpaintMode", InpaintMode.STTN_AUTO, OptionsValidator(InpaintMode), EnumSerializer(InpaintMode))
@@ -92,12 +92,6 @@ class Config(QConfig):
# 设置STTN算法最大同时处理的帧数量
sttnMaxLoadNum = RangeConfigItem("Sttn", "MaxLoadNum", 50, RangeValidator(1, 300))
getSttnMaxLoadNum = lambda self: max(self.sttnMaxLoadNum.value, self.sttnNeighborStride.value * self.sttnReferenceLength.value)
# 以下参数仅适用PROPAINTER算法时,才生效
# 【根据自己的GPU显存大小设置】最大同时处理的图片数量,设置越大处理效果越好,但是要求显存越高
# 1280x720p视频设置80需要25G显存,设置50需要19G显存
# 720x480p视频设置80需要8G显存,设置50需要7G显存
propainterMaxLoadNum = RangeConfigItem("ProPainter", "MaxLoadNum", 70, RangeValidator(1, 300))
# 是否使用硬件加速
hardwareAcceleration = ConfigItem("Main", "HardwareAcceleration", HARDWARD_ACCELERATION_OPTION, BoolValidator())
@@ -108,7 +102,13 @@ class Config(QConfig):
# 视频保存目录
saveDirectory = ConfigItem("Main", "SaveDirectory", "", ConfigValidator())
CONFIG_FILE = 'config/config.json'
# PyInstaller 打包后配置文件写入 .app bundle 同级目录(避免修改签名)
if getattr(sys, 'frozen', False):
# sys.executable -> Contents/MacOS/VideoSubtitleRemover, 向上3级到 .app 所在目录
_app_dir = os.path.dirname(os.path.dirname(os.path.dirname(sys.executable)))
CONFIG_FILE = os.path.join(_app_dir, 'config', 'config.json')
else:
CONFIG_FILE = 'config/config.json'
config = Config()
qconfig.load(CONFIG_FILE, config)
-447
View File
@@ -1,447 +0,0 @@
# -*- coding: utf-8 -*-
import os
import gc
import cv2
import numpy as np
import scipy.ndimage
from PIL import Image
from typing import List
import torch
import torchvision
from backend import config
from backend.inpaint.video.model.modules.flow_comp_raft import RAFT_bi
from backend.inpaint.video.model.recurrent_flow_completion import RecurrentFlowCompleteNet
from backend.inpaint.video.model.propainter import InpaintGenerator
from backend.inpaint.video.core.utils import to_tensors
from backend.inpaint.video.model.misc import get_device
from backend.tools.inpaint_tools import get_inpaint_area_by_mask
import warnings
warnings.filterwarnings("ignore")
def binary_mask(mask, th=0.1):
mask[mask > th] = 1
mask[mask <= th] = 0
return mask
# read frame-wise masks
def read_mask(mpath, length, size, flow_mask_dilates=8, mask_dilates=5):
masks_img = []
masks_dilated = []
flow_masks = []
# 如果传入的直接为numpy array
if isinstance(mpath, np.ndarray):
if mpath.ndim == 3 and mpath.shape[2] == 1:
mpath = mpath.squeeze(2) # 从 (H,W,1) 转为 (H,W)
elif mpath.ndim == 3 and mpath.shape[2] == 3:
# 如果是彩色图像,转为灰度
mpath = cv2.cvtColor(mpath, cv2.COLOR_BGR2GRAY)
masks_img = [Image.fromarray(mpath)]
# input single img path
else:
if isinstance(mpath, str):
if mpath.endswith(('jpg', 'jpeg', 'png', 'JPG', 'JPEG', 'PNG')):
masks_img = [Image.open(mpath)]
else:
mnames = sorted(os.listdir(mpath))
for mp in mnames:
masks_img.append(Image.open(os.path.join(mpath, mp)))
for mask_img in masks_img:
mask_img = np.array(mask_img.convert('L'))
# Dilate 8 pixel so that all known pixel is trustworthy
if flow_mask_dilates > 0:
flow_mask_img = scipy.ndimage.binary_dilation(mask_img, iterations=flow_mask_dilates).astype(np.uint8)
else:
flow_mask_img = binary_mask(mask_img).astype(np.uint8)
# Close the small holes inside the foreground objects
# flow_mask_img = cv2.morphologyEx(flow_mask_img, cv2.MORPH_CLOSE, np.ones((21, 21),np.uint8)).astype(bool)
# flow_mask_img = scipy.ndimage.binary_fill_holes(flow_mask_img).astype(np.uint8)
flow_masks.append(Image.fromarray(flow_mask_img * 255))
if mask_dilates > 0:
mask_img = scipy.ndimage.binary_dilation(mask_img, iterations=mask_dilates).astype(np.uint8)
else:
mask_img = binary_mask(mask_img).astype(np.uint8)
masks_dilated.append(Image.fromarray(mask_img * 255))
if len(masks_img) == 1:
flow_masks = flow_masks * length
masks_dilated = masks_dilated * length
return flow_masks, masks_dilated
def extrapolation(video_ori, scale):
"""Prepares the data for video outpainting.
"""
nFrame = len(video_ori)
imgW, imgH = video_ori[0].size
# Defines new FOV.
imgH_extr = int(scale[0] * imgH)
imgW_extr = int(scale[1] * imgW)
imgH_extr = imgH_extr - imgH_extr % 8
imgW_extr = imgW_extr - imgW_extr % 8
H_start = int((imgH_extr - imgH) / 2)
W_start = int((imgW_extr - imgW) / 2)
# Extrapolates the FOV for video.
frames = []
for v in video_ori:
frame = np.zeros((imgH_extr, imgW_extr, 3), dtype=np.uint8)
frame[H_start: H_start + imgH, W_start: W_start + imgW, :] = v
frames.append(Image.fromarray(frame))
# Generates the mask for missing region.
masks_dilated = []
flow_masks = []
dilate_h = 4 if H_start > 10 else 0
dilate_w = 4 if W_start > 10 else 0
mask = np.ones(((imgH_extr, imgW_extr)), dtype=np.uint8)
mask[H_start + dilate_h: H_start + imgH - dilate_h,
W_start + dilate_w: W_start + imgW - dilate_w] = 0
flow_masks.append(Image.fromarray(mask * 255))
mask[H_start: H_start + imgH, W_start: W_start + imgW] = 0
masks_dilated.append(Image.fromarray(mask * 255))
flow_masks = flow_masks * nFrame
masks_dilated = masks_dilated * nFrame
return frames, flow_masks, masks_dilated, (imgW_extr, imgH_extr)
def get_ref_index(mid_neighbor_id, neighbor_ids, length, ref_stride=10, ref_num=-1):
ref_index = []
if ref_num == -1:
for i in range(0, length, ref_stride):
if i not in neighbor_ids:
ref_index.append(i)
else:
start_idx = max(0, mid_neighbor_id - ref_stride * (ref_num // 2))
end_idx = min(length, mid_neighbor_id + ref_stride * (ref_num // 2))
for i in range(start_idx, end_idx, ref_stride):
if i not in neighbor_ids:
if len(ref_index) > ref_num:
break
ref_index.append(i)
return ref_index
class PropainterInpaint:
def __init__(self, device, model_dir, sub_video_length=80, use_fp16=True):
self.device = device
self.model_dir = model_dir
self.use_fp16 = use_fp16
self.use_half = True if self.use_fp16 else False
if self.device == torch.device('cpu'):
self.use_half = False
# Length of sub-video for long video inference.
self.sub_video_length = sub_video_length
# Length of local neighboring frames.'
self.neighbor_length = 10
# Mask dilation for video and flow masking
self.mask_dilation = 4
# Stride of global reference frames
self.ref_stride = 10
# Iterations for RAFT inference
self.raft_iter = 20
# Stride of global reference frames
self.ref_stride = 10
# 设置raft模型
self.fix_raft = self.init_raft_model()
# 设置fix_flow模型
self.fix_flow_complete = self.init_fix_flow_model()
# 设置inpaint模型
self.model = self.init_inpaint_model()
def init_raft_model(self):
# set up RAFT and flow competition model
return RAFT_bi(os.path.join(self.model_dir, 'raft-things.pth'), self.device)
def init_fix_flow_model(self):
fix_flow_complete_model = RecurrentFlowCompleteNet(
os.path.join(self.model_dir, 'recurrent_flow_completion.pth'))
for p in fix_flow_complete_model.parameters():
p.requires_grad = False
if self.use_half:
fix_flow_complete_model = fix_flow_complete_model.half()
fix_flow_complete_model.to(self.device)
fix_flow_complete_model.eval()
return fix_flow_complete_model
def init_inpaint_model(self):
# set up ProPainter model
model = InpaintGenerator(model_path=os.path.join(self.model_dir, 'ProPainter.pth'))
if self.use_half:
model = model.half()
model = model.to(self.device).eval()
return model
def inpaint(self, frames, mask):
if isinstance(frames[0], np.ndarray):
frames = [Image.fromarray(cv2.cvtColor(f, cv2.COLOR_BGR2RGB)) for f in frames]
size = frames[0].size
frames_len = len(frames)
flow_masks, masks_dilated = read_mask(mask, frames_len, size,
flow_mask_dilates=self.mask_dilation,
mask_dilates=self.mask_dilation)
w, h = size
# for saving the masked frames or video
masked_frame_for_save = []
for i in range(len(frames)):
mask_ = np.expand_dims(np.array(masks_dilated[i]), 2).repeat(3, axis=2) / 255.
img = np.array(frames[i])
green = np.zeros([h, w, 3])
green[:, :, 1] = 255
alpha = 0.6
# alpha = 1.0
fuse_img = (1 - alpha) * img + alpha * green
fuse_img = mask_ * fuse_img + (1 - mask_) * img
masked_frame_for_save.append(fuse_img.astype(np.uint8))
frames_inp = [np.array(f).astype(np.uint8) for f in frames]
frames = to_tensors()(frames).unsqueeze(0) * 2 - 1
flow_masks = to_tensors()(flow_masks).unsqueeze(0)
masks_dilated = to_tensors()(masks_dilated).unsqueeze(0)
frames, flow_masks, masks_dilated = frames.to(self.device), flow_masks.to(self.device), masks_dilated.to(
self.device)
video_length = frames.size(1)
with torch.no_grad():
# ---- compute flow ----
if frames.size(-1) <= 640:
short_clip_len = 12
elif frames.size(-1) <= 720:
short_clip_len = 8
elif frames.size(-1) <= 1280:
short_clip_len = 4
else:
short_clip_len = 2
# use fp32 for RAFT
if frames.size(1) > short_clip_len:
gt_flows_f_list, gt_flows_b_list = [], []
for f in range(0, video_length, short_clip_len):
end_f = min(video_length, f + short_clip_len)
if f == 0:
flows_f, flows_b = self.fix_raft(frames[:, f:end_f], iters=self.raft_iter)
else:
flows_f, flows_b = self.fix_raft(frames[:, f - 1:end_f], iters=self.raft_iter)
gt_flows_f_list.append(flows_f)
gt_flows_b_list.append(flows_b)
torch.cuda.empty_cache()
gt_flows_f = torch.cat(gt_flows_f_list, dim=1)
gt_flows_b = torch.cat(gt_flows_b_list, dim=1)
gt_flows_bi = (gt_flows_f, gt_flows_b)
else:
gt_flows_bi = self.fix_raft(frames, iters=self.raft_iter)
torch.cuda.empty_cache()
if self.use_half:
frames, flow_masks, masks_dilated = frames.half(), flow_masks.half(), masks_dilated.half()
gt_flows_bi = (gt_flows_bi[0].half(), gt_flows_bi[1].half())
# ---- complete flow ----
flow_length = gt_flows_bi[0].size(1)
if flow_length > self.sub_video_length:
pred_flows_f, pred_flows_b = [], []
pad_len = 5
for f in range(0, flow_length, self.sub_video_length):
s_f = max(0, f - pad_len)
e_f = min(flow_length, f + self.sub_video_length + pad_len)
pad_len_s = max(0, f) - s_f
pad_len_e = e_f - min(flow_length, f + self.sub_video_length)
pred_flows_bi_sub, _ = self.fix_flow_complete.forward_bidirect_flow(
(gt_flows_bi[0][:, s_f:e_f], gt_flows_bi[1][:, s_f:e_f]),
flow_masks[:, s_f:e_f + 1])
pred_flows_bi_sub = self.fix_flow_complete.combine_flow(
(gt_flows_bi[0][:, s_f:e_f], gt_flows_bi[1][:, s_f:e_f]),
pred_flows_bi_sub,
flow_masks[:, s_f:e_f + 1])
pred_flows_f.append(pred_flows_bi_sub[0][:, pad_len_s:e_f - s_f - pad_len_e])
pred_flows_b.append(pred_flows_bi_sub[1][:, pad_len_s:e_f - s_f - pad_len_e])
torch.cuda.empty_cache()
pred_flows_f = torch.cat(pred_flows_f, dim=1)
pred_flows_b = torch.cat(pred_flows_b, dim=1)
pred_flows_bi = (pred_flows_f, pred_flows_b)
else:
pred_flows_bi, _ = self.fix_flow_complete.forward_bidirect_flow(gt_flows_bi, flow_masks)
pred_flows_bi = self.fix_flow_complete.combine_flow(gt_flows_bi, pred_flows_bi, flow_masks)
torch.cuda.empty_cache()
# ---- image propagation ----
masked_frames = frames * (1 - masks_dilated)
# ensure a minimum of 100 frames for image propagation
subvideo_length_img_prop = min(100, self.sub_video_length)
if video_length > subvideo_length_img_prop:
updated_frames, updated_masks = [], []
pad_len = 10
for f in range(0, video_length, subvideo_length_img_prop):
s_f = max(0, f - pad_len)
e_f = min(video_length, f + subvideo_length_img_prop + pad_len)
pad_len_s = max(0, f) - s_f
pad_len_e = e_f - min(video_length, f + subvideo_length_img_prop)
b, t, _, _, _ = masks_dilated[:, s_f:e_f].size()
pred_flows_bi_sub = (pred_flows_bi[0][:, s_f:e_f - 1], pred_flows_bi[1][:, s_f:e_f - 1])
prop_imgs_sub, updated_local_masks_sub = self.model.img_propagation(masked_frames[:, s_f:e_f],
pred_flows_bi_sub,
masks_dilated[:, s_f:e_f],
'nearest')
updated_frames_sub = frames[:, s_f:e_f] * (1 - masks_dilated[:, s_f:e_f]) + prop_imgs_sub.view(b, t, 3, h, w) * masks_dilated[:, s_f:e_f]
updated_masks_sub = updated_local_masks_sub.view(b, t, 1, h, w)
updated_frames.append(updated_frames_sub[:, pad_len_s:e_f - s_f - pad_len_e])
updated_masks.append(updated_masks_sub[:, pad_len_s:e_f - s_f - pad_len_e])
torch.cuda.empty_cache()
updated_frames = torch.cat(updated_frames, dim=1)
updated_masks = torch.cat(updated_masks, dim=1)
else:
b, t, _, _, _ = masks_dilated.size()
prop_imgs, updated_local_masks = self.model.img_propagation(masked_frames, pred_flows_bi, masks_dilated,
'nearest')
updated_frames = frames * (1 - masks_dilated) + prop_imgs.view(b, t, 3, h, w) * masks_dilated
updated_masks = updated_local_masks.view(b, t, 1, h, w)
torch.cuda.empty_cache()
ori_frames = frames_inp
comp_frames = [None] * video_length
neighbor_stride = self.neighbor_length // 2
if video_length > self.sub_video_length:
ref_num = self.sub_video_length // self.ref_stride
else:
ref_num = -1
# ---- feature propagation + transformer ----
for f in range(0, video_length, neighbor_stride):
neighbor_ids = [
i for i in range(max(0, f - neighbor_stride),
min(video_length, f + neighbor_stride + 1))
]
ref_ids = get_ref_index(f, neighbor_ids, video_length, self.ref_stride, ref_num)
selected_imgs = updated_frames[:, neighbor_ids + ref_ids, :, :, :]
selected_masks = masks_dilated[:, neighbor_ids + ref_ids, :, :, :]
selected_update_masks = updated_masks[:, neighbor_ids + ref_ids, :, :, :]
selected_pred_flows_bi = (
pred_flows_bi[0][:, neighbor_ids[:-1], :, :, :], pred_flows_bi[1][:, neighbor_ids[:-1], :, :, :])
with torch.no_grad():
# 1.0 indicates mask
l_t = len(neighbor_ids)
pred_img = self.model(selected_imgs, selected_pred_flows_bi, selected_masks, selected_update_masks, l_t)
pred_img = pred_img.view(-1, 3, h, w)
pred_img = (pred_img + 1) / 2
pred_img = pred_img.cpu().permute(0, 2, 3, 1).numpy() * 255
binary_masks = masks_dilated[0, neighbor_ids, :, :, :].cpu().permute(
0, 2, 3, 1).numpy().astype(np.uint8)
for i in range(len(neighbor_ids)):
idx = neighbor_ids[i]
img = np.array(pred_img[i]).astype(np.uint8) * binary_masks[i] \
+ ori_frames[idx] * (1 - binary_masks[i])
if comp_frames[idx] is None:
comp_frames[idx] = img
else:
comp_frames[idx] = comp_frames[idx].astype(np.float32) * 0.5 + img.astype(np.float32) * 0.5
comp_frames[idx] = comp_frames[idx].astype(np.uint8)
torch.cuda.empty_cache()
# save videos frame
comp_frames = [cv2.cvtColor(i, cv2.COLOR_RGB2BGR) for i in comp_frames]
return comp_frames
def __call__(self, input_frames: List[np.ndarray], input_mask: np.ndarray):
"""
:param input_frames: 原视频帧
:param input_mask: 字幕区域mask
"""
mask = input_mask[:, :, None]
H_ori, W_ori = mask.shape[:2]
H_ori = int(H_ori + 0.5)
W_ori = int(W_ori + 0.5)
# 确定去字幕的垂直高度部分
split_h = int(W_ori * 3 / 16)
inpaint_area = get_inpaint_area_by_mask(W_ori, H_ori, split_h, mask, multiple=8)
# 初始化帧存储变量
# 高分辨率帧存储列表
frames_hr = [f.copy() for f in input_frames]
frames_scaled = {} # 存放缩放后帧的字典
masks_scaled = {} # 存放缩放后遮罩的字典
comps = {} # 存放补全后帧的字典
# 存储最终的视频帧
inpainted_frames = []
for k in range(len(inpaint_area)):
frames_scaled[k] = [] # 为每个去除部分初始化一个列表
masks_scaled[k] = [] # 为每个去除部分初始化一个列表
# 读取并缩放帧
for j in range(len(frames_hr)):
image = frames_hr[j]
# 对每个去除部分进行切割和缩放
for k in range(len(inpaint_area)):
image_crop = image[inpaint_area[k][0]:inpaint_area[k][1], inpaint_area[k][2]:inpaint_area[k][3], :] # 切割
mask_crop = mask[inpaint_area[k][0]:inpaint_area[k][1], inpaint_area[k][2]:inpaint_area[k][3], :] # 切割
frames_scaled[k].append(image_crop) # 将缩放后的帧添加到对应列表
masks_scaled[k].append(mask_crop) # 将缩放后的遮罩添加到对应列表
# 处理每一个去除部分
for k in range(len(inpaint_area)):
# 调用inpaint函数进行处理
comps[k] = self.inpaint(frames_scaled[k], masks_scaled[k][0])
del frames_scaled[k], masks_scaled[k]
gc.collect()
# 如果存在去除部分
if inpaint_area:
for j in range(len(frames_hr)):
frame = frames_hr[j] # 取出原始帧
# 对于模式中的每一个段落
for k in range(len(inpaint_area)):
comp = comps[k][j] # 获取补全后的帧
# 实现遮罩区域内的图像融合
frame[inpaint_area[k][0]:inpaint_area[k][1], inpaint_area[k][2]:inpaint_area[k][3], :] = comp
# 将最终帧添加到列表
inpainted_frames.append(frame)
# print(f'processing frame, {len(frames_hr) - j} left')
else:
inpainted_frames = frames_hr
return inpainted_frames
def read_frames(v_path):
video_cap = cv2.VideoCapture(v_path)
video_frames = []
while True:
ret, frame = video_cap.read()
if not ret:
break
video_frames.append(frame)
video_frames = [Image.fromarray(f) for f in video_frames]
return video_frames
if __name__ == '__main__':
# PropainterInpaint
propainter_inpaint = PropainterInpaint(get_device(), ModelConfig().PROPAINTER_MODEL_DIR, sub_video_length=80)
frames = read_frames('/home/yao/Documents/Project/video-subtitle-remover/local_test/test1.mp4')
mask = cv2.imread('/home/yao/Documents/Project/video-subtitle-remover/local_test/test1_mask.png')
inpainted_frames = propainter_inpaint.inpaint(frames, mask)
save_root = '/home/yao/Documents/Project/video-subtitle-remover/local_test/'
video_out_path = os.path.join(save_root, 'inpaint_out.mp4')
print("size: ", inpainted_frames[0].shape)
video_writer = cv2.VideoWriter(video_out_path, cv2.VideoWriter_fourcc(*'mp4v'), 24, (640, 360))
for comp_frame in inpainted_frames:
video_writer.write(comp_frame)
video_writer.release()
print(f'\nAll results are saved in {save_root}')
-232
View File
@@ -1,232 +0,0 @@
import os
import json
import random
import cv2
from PIL import Image
import numpy as np
import torch
import torchvision.transforms as transforms
from utils.file_client import FileClient
from utils.img_util import imfrombytes
from utils.flow_util import resize_flow, flowread
from core.utils import (create_random_shape_with_random_motion, Stack,
ToTorchFormatTensor, GroupRandomHorizontalFlip,GroupRandomHorizontalFlowFlip)
class TrainDataset(torch.utils.data.Dataset):
def __init__(self, args: dict):
self.args = args
self.video_root = args['video_root']
self.flow_root = args['flow_root']
self.num_local_frames = args['num_local_frames']
self.num_ref_frames = args['num_ref_frames']
self.size = self.w, self.h = (args['w'], args['h'])
self.load_flow = args['load_flow']
if self.load_flow:
assert os.path.exists(self.flow_root)
json_path = os.path.join('./datasets', args['name'], 'train.json')
with open(json_path, 'r') as f:
self.video_train_dict = json.load(f)
self.video_names = sorted(list(self.video_train_dict.keys()))
# self.video_names = sorted(os.listdir(self.video_root))
self.video_dict = {}
self.frame_dict = {}
for v in self.video_names:
frame_list = sorted(os.listdir(os.path.join(self.video_root, v)))
v_len = len(frame_list)
if v_len > self.num_local_frames + self.num_ref_frames:
self.video_dict[v] = v_len
self.frame_dict[v] = frame_list
self.video_names = list(self.video_dict.keys()) # update names
self._to_tensors = transforms.Compose([
Stack(),
ToTorchFormatTensor(),
])
self.file_client = FileClient('disk')
def __len__(self):
return len(self.video_names)
def _sample_index(self, length, sample_length, num_ref_frame=3):
complete_idx_set = list(range(length))
pivot = random.randint(0, length - sample_length)
local_idx = complete_idx_set[pivot:pivot + sample_length]
remain_idx = list(set(complete_idx_set) - set(local_idx))
ref_index = sorted(random.sample(remain_idx, num_ref_frame))
return local_idx + ref_index
def __getitem__(self, index):
video_name = self.video_names[index]
# create masks
all_masks = create_random_shape_with_random_motion(
self.video_dict[video_name], imageHeight=self.h, imageWidth=self.w)
# create sample index
selected_index = self._sample_index(self.video_dict[video_name],
self.num_local_frames,
self.num_ref_frames)
# read video frames
frames = []
masks = []
flows_f, flows_b = [], []
for idx in selected_index:
frame_list = self.frame_dict[video_name]
img_path = os.path.join(self.video_root, video_name, frame_list[idx])
img_bytes = self.file_client.get(img_path, 'img')
img = imfrombytes(img_bytes, float32=False)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, self.size, interpolation=cv2.INTER_LINEAR)
img = Image.fromarray(img)
frames.append(img)
masks.append(all_masks[idx])
if len(frames) <= self.num_local_frames-1 and self.load_flow:
current_n = frame_list[idx][:-4]
next_n = frame_list[idx+1][:-4]
flow_f_path = os.path.join(self.flow_root, video_name, f'{current_n}_{next_n}_f.flo')
flow_b_path = os.path.join(self.flow_root, video_name, f'{next_n}_{current_n}_b.flo')
flow_f = flowread(flow_f_path, quantize=False)
flow_b = flowread(flow_b_path, quantize=False)
flow_f = resize_flow(flow_f, self.h, self.w)
flow_b = resize_flow(flow_b, self.h, self.w)
flows_f.append(flow_f)
flows_b.append(flow_b)
if len(frames) == self.num_local_frames: # random reverse
if random.random() < 0.5:
frames.reverse()
masks.reverse()
if self.load_flow:
flows_f.reverse()
flows_b.reverse()
flows_ = flows_f
flows_f = flows_b
flows_b = flows_
if self.load_flow:
frames, flows_f, flows_b = GroupRandomHorizontalFlowFlip()(frames, flows_f, flows_b)
else:
frames = GroupRandomHorizontalFlip()(frames)
# normalizate, to tensors
frame_tensors = self._to_tensors(frames) * 2.0 - 1.0
mask_tensors = self._to_tensors(masks)
if self.load_flow:
flows_f = np.stack(flows_f, axis=-1) # H W 2 T-1
flows_b = np.stack(flows_b, axis=-1)
flows_f = torch.from_numpy(flows_f).permute(3, 2, 0, 1).contiguous().float()
flows_b = torch.from_numpy(flows_b).permute(3, 2, 0, 1).contiguous().float()
# img [-1,1] mask [0,1]
if self.load_flow:
return frame_tensors, mask_tensors, flows_f, flows_b, video_name
else:
return frame_tensors, mask_tensors, 'None', 'None', video_name
class TestDataset(torch.utils.data.Dataset):
def __init__(self, args):
self.args = args
self.size = self.w, self.h = args['size']
self.video_root = args['video_root']
self.mask_root = args['mask_root']
self.flow_root = args['flow_root']
self.load_flow = args['load_flow']
if self.load_flow:
assert os.path.exists(self.flow_root)
self.video_names = sorted(os.listdir(self.mask_root))
self.video_dict = {}
self.frame_dict = {}
for v in self.video_names:
frame_list = sorted(os.listdir(os.path.join(self.video_root, v)))
v_len = len(frame_list)
self.video_dict[v] = v_len
self.frame_dict[v] = frame_list
self._to_tensors = transforms.Compose([
Stack(),
ToTorchFormatTensor(),
])
self.file_client = FileClient('disk')
def __len__(self):
return len(self.video_names)
def __getitem__(self, index):
video_name = self.video_names[index]
selected_index = list(range(self.video_dict[video_name]))
# read video frames
frames = []
masks = []
flows_f, flows_b = [], []
for idx in selected_index:
frame_list = self.frame_dict[video_name]
frame_path = os.path.join(self.video_root, video_name, frame_list[idx])
img_bytes = self.file_client.get(frame_path, 'input')
img = imfrombytes(img_bytes, float32=False)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, self.size, interpolation=cv2.INTER_LINEAR)
img = Image.fromarray(img)
frames.append(img)
mask_path = os.path.join(self.mask_root, video_name, str(idx).zfill(5) + '.png')
mask = Image.open(mask_path).resize(self.size, Image.NEAREST).convert('L')
# origin: 0 indicates missing. now: 1 indicates missing
mask = np.asarray(mask)
m = np.array(mask > 0).astype(np.uint8)
m = cv2.dilate(m,
cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)),
iterations=4)
mask = Image.fromarray(m * 255)
masks.append(mask)
if len(frames) <= len(selected_index)-1 and self.load_flow:
current_n = frame_list[idx][:-4]
next_n = frame_list[idx+1][:-4]
flow_f_path = os.path.join(self.flow_root, video_name, f'{current_n}_{next_n}_f.flo')
flow_b_path = os.path.join(self.flow_root, video_name, f'{next_n}_{current_n}_b.flo')
flow_f = flowread(flow_f_path, quantize=False)
flow_b = flowread(flow_b_path, quantize=False)
flow_f = resize_flow(flow_f, self.h, self.w)
flow_b = resize_flow(flow_b, self.h, self.w)
flows_f.append(flow_f)
flows_b.append(flow_b)
# normalizate, to tensors
frames_PIL = [np.array(f).astype(np.uint8) for f in frames]
frame_tensors = self._to_tensors(frames) * 2.0 - 1.0
mask_tensors = self._to_tensors(masks)
if self.load_flow:
flows_f = np.stack(flows_f, axis=-1) # H W 2 T-1
flows_b = np.stack(flows_b, axis=-1)
flows_f = torch.from_numpy(flows_f).permute(3, 2, 0, 1).contiguous().float()
flows_b = torch.from_numpy(flows_b).permute(3, 2, 0, 1).contiguous().float()
if self.load_flow:
return frame_tensors, mask_tensors, flows_f, flows_b, video_name, frames_PIL
else:
return frame_tensors, mask_tensors, 'None', 'None', video_name
-47
View File
@@ -1,47 +0,0 @@
import os
import torch
def get_world_size():
"""Find OMPI world size without calling mpi functions
:rtype: int
"""
if os.environ.get('PMI_SIZE') is not None:
return int(os.environ.get('PMI_SIZE') or 1)
elif os.environ.get('OMPI_COMM_WORLD_SIZE') is not None:
return int(os.environ.get('OMPI_COMM_WORLD_SIZE') or 1)
else:
return torch.cuda.device_count()
def get_global_rank():
"""Find OMPI world rank without calling mpi functions
:rtype: int
"""
if os.environ.get('PMI_RANK') is not None:
return int(os.environ.get('PMI_RANK') or 0)
elif os.environ.get('OMPI_COMM_WORLD_RANK') is not None:
return int(os.environ.get('OMPI_COMM_WORLD_RANK') or 0)
else:
return 0
def get_local_rank():
"""Find OMPI local rank without calling mpi functions
:rtype: int
"""
if os.environ.get('MPI_LOCALRANKID') is not None:
return int(os.environ.get('MPI_LOCALRANKID') or 0)
elif os.environ.get('OMPI_COMM_WORLD_LOCAL_RANK') is not None:
return int(os.environ.get('OMPI_COMM_WORLD_LOCAL_RANK') or 0)
else:
return 0
def get_master_ip():
if os.environ.get('AZ_BATCH_MASTER_NODE') is not None:
return os.environ.get('AZ_BATCH_MASTER_NODE').split(':')[0]
elif os.environ.get('AZ_BATCHAI_MPI_MASTER_NODE') is not None:
return os.environ.get('AZ_BATCHAI_MPI_MASTER_NODE')
else:
return "127.0.0.1"
-180
View File
@@ -1,180 +0,0 @@
import torch
import torch.nn as nn
import lpips
from model.vgg_arch import VGGFeatureExtractor
class PerceptualLoss(nn.Module):
"""Perceptual loss with commonly used style loss.
Args:
layer_weights (dict): The weight for each layer of vgg feature.
Here is an example: {'conv5_4': 1.}, which means the conv5_4
feature layer (before relu5_4) will be extracted with weight
1.0 in calculting losses.
vgg_type (str): The type of vgg network used as feature extractor.
Default: 'vgg19'.
use_input_norm (bool): If True, normalize the input image in vgg.
Default: True.
range_norm (bool): If True, norm images with range [-1, 1] to [0, 1].
Default: False.
perceptual_weight (float): If `perceptual_weight > 0`, the perceptual
loss will be calculated and the loss will multiplied by the
weight. Default: 1.0.
style_weight (float): If `style_weight > 0`, the style loss will be
calculated and the loss will multiplied by the weight.
Default: 0.
criterion (str): Criterion used for perceptual loss. Default: 'l1'.
"""
def __init__(self,
layer_weights,
vgg_type='vgg19',
use_input_norm=True,
range_norm=False,
perceptual_weight=1.0,
style_weight=0.,
criterion='l1'):
super(PerceptualLoss, self).__init__()
self.perceptual_weight = perceptual_weight
self.style_weight = style_weight
self.layer_weights = layer_weights
self.vgg = VGGFeatureExtractor(
layer_name_list=list(layer_weights.keys()),
vgg_type=vgg_type,
use_input_norm=use_input_norm,
range_norm=range_norm)
self.criterion_type = criterion
if self.criterion_type == 'l1':
self.criterion = torch.nn.L1Loss()
elif self.criterion_type == 'l2':
self.criterion = torch.nn.L2loss()
elif self.criterion_type == 'mse':
self.criterion = torch.nn.MSELoss(reduction='mean')
elif self.criterion_type == 'fro':
self.criterion = None
else:
raise NotImplementedError(f'{criterion} criterion has not been supported.')
def forward(self, x, gt):
"""Forward function.
Args:
x (Tensor): Input tensor with shape (n, c, h, w).
gt (Tensor): Ground-truth tensor with shape (n, c, h, w).
Returns:
Tensor: Forward results.
"""
# extract vgg features
x_features = self.vgg(x)
gt_features = self.vgg(gt.detach())
# calculate perceptual loss
if self.perceptual_weight > 0:
percep_loss = 0
for k in x_features.keys():
if self.criterion_type == 'fro':
percep_loss += torch.norm(x_features[k] - gt_features[k], p='fro') * self.layer_weights[k]
else:
percep_loss += self.criterion(x_features[k], gt_features[k]) * self.layer_weights[k]
percep_loss *= self.perceptual_weight
else:
percep_loss = None
# calculate style loss
if self.style_weight > 0:
style_loss = 0
for k in x_features.keys():
if self.criterion_type == 'fro':
style_loss += torch.norm(
self._gram_mat(x_features[k]) - self._gram_mat(gt_features[k]), p='fro') * self.layer_weights[k]
else:
style_loss += self.criterion(self._gram_mat(x_features[k]), self._gram_mat(
gt_features[k])) * self.layer_weights[k]
style_loss *= self.style_weight
else:
style_loss = None
return percep_loss, style_loss
def _gram_mat(self, x):
"""Calculate Gram matrix.
Args:
x (torch.Tensor): Tensor with shape of (n, c, h, w).
Returns:
torch.Tensor: Gram matrix.
"""
n, c, h, w = x.size()
features = x.view(n, c, w * h)
features_t = features.transpose(1, 2)
gram = features.bmm(features_t) / (c * h * w)
return gram
class LPIPSLoss(nn.Module):
def __init__(self,
loss_weight=1.0,
use_input_norm=True,
range_norm=False,):
super(LPIPSLoss, self).__init__()
self.perceptual = lpips.LPIPS(net="vgg", spatial=False).eval()
self.loss_weight = loss_weight
self.use_input_norm = use_input_norm
self.range_norm = range_norm
if self.use_input_norm:
# the mean is for image with range [0, 1]
self.register_buffer('mean', torch.Tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
# the std is for image with range [0, 1]
self.register_buffer('std', torch.Tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
def forward(self, pred, target):
if self.range_norm:
pred = (pred + 1) / 2
target = (target + 1) / 2
if self.use_input_norm:
pred = (pred - self.mean) / self.std
target = (target - self.mean) / self.std
lpips_loss = self.perceptual(target.contiguous(), pred.contiguous())
return self.loss_weight * lpips_loss.mean(), None
class AdversarialLoss(nn.Module):
r"""
Adversarial loss
https://arxiv.org/abs/1711.10337
"""
def __init__(self,
type='nsgan',
target_real_label=1.0,
target_fake_label=0.0):
r"""
type = nsgan | lsgan | hinge
"""
super(AdversarialLoss, self).__init__()
self.type = type
self.register_buffer('real_label', torch.tensor(target_real_label))
self.register_buffer('fake_label', torch.tensor(target_fake_label))
if type == 'nsgan':
self.criterion = nn.BCELoss()
elif type == 'lsgan':
self.criterion = nn.MSELoss()
elif type == 'hinge':
self.criterion = nn.ReLU()
def __call__(self, outputs, is_real, is_disc=None):
if self.type == 'hinge':
if is_disc:
if is_real:
outputs = -outputs
return self.criterion(1 + outputs).mean()
else:
return (-outputs).mean()
else:
labels = (self.real_label
if is_real else self.fake_label).expand_as(outputs)
loss = self.criterion(outputs, labels)
return loss
-112
View File
@@ -1,112 +0,0 @@
"""
LR scheduler from BasicSR https://github.com/xinntao/BasicSR
"""
import math
from collections import Counter
from torch.optim.lr_scheduler import _LRScheduler
class MultiStepRestartLR(_LRScheduler):
""" MultiStep with restarts learning rate scheme.
Args:
optimizer (torch.nn.optimizer): Torch optimizer.
milestones (list): Iterations that will decrease learning rate.
gamma (float): Decrease ratio. Default: 0.1.
restarts (list): Restart iterations. Default: [0].
restart_weights (list): Restart weights at each restart iteration.
Default: [1].
last_epoch (int): Used in _LRScheduler. Default: -1.
"""
def __init__(self,
optimizer,
milestones,
gamma=0.1,
restarts=(0, ),
restart_weights=(1, ),
last_epoch=-1):
self.milestones = Counter(milestones)
self.gamma = gamma
self.restarts = restarts
self.restart_weights = restart_weights
assert len(self.restarts) == len(
self.restart_weights), 'restarts and their weights do not match.'
super(MultiStepRestartLR, self).__init__(optimizer, last_epoch)
def get_lr(self):
if self.last_epoch in self.restarts:
weight = self.restart_weights[self.restarts.index(self.last_epoch)]
return [
group['initial_lr'] * weight
for group in self.optimizer.param_groups
]
if self.last_epoch not in self.milestones:
return [group['lr'] for group in self.optimizer.param_groups]
return [
group['lr'] * self.gamma**self.milestones[self.last_epoch]
for group in self.optimizer.param_groups
]
def get_position_from_periods(iteration, cumulative_period):
"""Get the position from a period list.
It will return the index of the right-closest number in the period list.
For example, the cumulative_period = [100, 200, 300, 400],
if iteration == 50, return 0;
if iteration == 210, return 2;
if iteration == 300, return 2.
Args:
iteration (int): Current iteration.
cumulative_period (list[int]): Cumulative period list.
Returns:
int: The position of the right-closest number in the period list.
"""
for i, period in enumerate(cumulative_period):
if iteration <= period:
return i
class CosineAnnealingRestartLR(_LRScheduler):
""" Cosine annealing with restarts learning rate scheme.
An example of config:
periods = [10, 10, 10, 10]
restart_weights = [1, 0.5, 0.5, 0.5]
eta_min=1e-7
It has four cycles, each has 10 iterations. At 10th, 20th, 30th, the
scheduler will restart with the weights in restart_weights.
Args:
optimizer (torch.nn.optimizer): Torch optimizer.
periods (list): Period for each cosine anneling cycle.
restart_weights (list): Restart weights at each restart iteration.
Default: [1].
eta_min (float): The mimimum lr. Default: 0.
last_epoch (int): Used in _LRScheduler. Default: -1.
"""
def __init__(self,
optimizer,
periods,
restart_weights=(1, ),
eta_min=1e-7,
last_epoch=-1):
self.periods = periods
self.restart_weights = restart_weights
self.eta_min = eta_min
assert (len(self.periods) == len(self.restart_weights)
), 'periods and restart_weights should have the same length.'
self.cumulative_period = [
sum(self.periods[0:i + 1]) for i in range(0, len(self.periods))
]
super(CosineAnnealingRestartLR, self).__init__(optimizer, last_epoch)
def get_lr(self):
idx = get_position_from_periods(self.last_epoch,
self.cumulative_period)
current_weight = self.restart_weights[idx]
nearest_restart = 0 if idx == 0 else self.cumulative_period[idx - 1]
current_period = self.periods[idx]
return [
self.eta_min + current_weight * 0.5 * (base_lr - self.eta_min) *
(1 + math.cos(math.pi * (
(self.last_epoch - nearest_restart) / current_period)))
for base_lr in self.base_lrs
]
-569
View File
@@ -1,569 +0,0 @@
import numpy as np
from skimage import measure
from scipy import linalg
import torch
import torch.nn as nn
import torch.nn.functional as F
from core.utils import to_tensors
def calculate_epe(flow1, flow2):
"""Calculate End point errors."""
epe = torch.sum((flow1 - flow2)**2, dim=1).sqrt()
epe = epe.view(-1)
return epe.mean().item()
def calculate_psnr(img1, img2):
"""Calculate PSNR (Peak Signal-to-Noise Ratio).
Ref: https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
Args:
img1 (ndarray): Images with range [0, 255].
img2 (ndarray): Images with range [0, 255].
Returns:
float: psnr result.
"""
assert img1.shape == img2.shape, \
(f'Image shapes are differnet: {img1.shape}, {img2.shape}.')
mse = np.mean((img1 - img2)**2)
if mse == 0:
return float('inf')
return 20. * np.log10(255. / np.sqrt(mse))
def calc_psnr_and_ssim(img1, img2):
"""Calculate PSNR and SSIM for images.
img1: ndarray, range [0, 255]
img2: ndarray, range [0, 255]
"""
img1 = img1.astype(np.float64)
img2 = img2.astype(np.float64)
psnr = calculate_psnr(img1, img2)
ssim = measure.compare_ssim(img1,
img2,
data_range=255,
multichannel=True,
win_size=65)
return psnr, ssim
###########################
# I3D models
###########################
def init_i3d_model(i3d_model_path):
print(f"[Loading I3D model from {i3d_model_path} for FID score ..]")
i3d_model = InceptionI3d(400, in_channels=3, final_endpoint='Logits')
i3d_model.load_state_dict(torch.load(i3d_model_path))
i3d_model.to(torch.device('cuda:0'))
return i3d_model
def calculate_i3d_activations(video1, video2, i3d_model, device):
"""Calculate VFID metric.
video1: list[PIL.Image]
video2: list[PIL.Image]
"""
video1 = to_tensors()(video1).unsqueeze(0).to(device)
video2 = to_tensors()(video2).unsqueeze(0).to(device)
video1_activations = get_i3d_activations(
video1, i3d_model).cpu().numpy().flatten()
video2_activations = get_i3d_activations(
video2, i3d_model).cpu().numpy().flatten()
return video1_activations, video2_activations
def calculate_vfid(real_activations, fake_activations):
"""
Given two distribution of features, compute the FID score between them
Params:
real_activations: list[ndarray]
fake_activations: list[ndarray]
"""
m1 = np.mean(real_activations, axis=0)
m2 = np.mean(fake_activations, axis=0)
s1 = np.cov(real_activations, rowvar=False)
s2 = np.cov(fake_activations, rowvar=False)
return calculate_frechet_distance(m1, s1, m2, s2)
def calculate_frechet_distance(mu1, sigma1, mu2, sigma2, eps=1e-6):
"""Numpy implementation of the Frechet Distance.
The Frechet distance between two multivariate Gaussians X_1 ~ N(mu_1, C_1)
and X_2 ~ N(mu_2, C_2) is
d^2 = ||mu_1 - mu_2||^2 + Tr(C_1 + C_2 - 2*sqrt(C_1*C_2)).
Stable version by Dougal J. Sutherland.
Params:
-- mu1 : Numpy array containing the activations of a layer of the
inception net (like returned by the function 'get_predictions')
for generated samples.
-- mu2 : The sample mean over activations, precalculated on an
representive data set.
-- sigma1: The covariance matrix over activations for generated samples.
-- sigma2: The covariance matrix over activations, precalculated on an
representive data set.
Returns:
-- : The Frechet Distance.
"""
mu1 = np.atleast_1d(mu1)
mu2 = np.atleast_1d(mu2)
sigma1 = np.atleast_2d(sigma1)
sigma2 = np.atleast_2d(sigma2)
assert mu1.shape == mu2.shape, \
'Training and test mean vectors have different lengths'
assert sigma1.shape == sigma2.shape, \
'Training and test covariances have different dimensions'
diff = mu1 - mu2
# Product might be almost singular
covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)
if not np.isfinite(covmean).all():
msg = ('fid calculation produces singular product; '
'adding %s to diagonal of cov estimates') % eps
print(msg)
offset = np.eye(sigma1.shape[0]) * eps
covmean = linalg.sqrtm((sigma1 + offset).dot(sigma2 + offset))
# Numerical error might give slight imaginary component
if np.iscomplexobj(covmean):
if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-3):
m = np.max(np.abs(covmean.imag))
raise ValueError('Imaginary component {}'.format(m))
covmean = covmean.real
tr_covmean = np.trace(covmean)
return (diff.dot(diff) + np.trace(sigma1) + # NOQA
np.trace(sigma2) - 2 * tr_covmean)
def get_i3d_activations(batched_video,
i3d_model,
target_endpoint='Logits',
flatten=True,
grad_enabled=False):
"""
Get features from i3d model and flatten them to 1d feature,
valid target endpoints are defined in InceptionI3d.VALID_ENDPOINTS
VALID_ENDPOINTS = (
'Conv3d_1a_7x7',
'MaxPool3d_2a_3x3',
'Conv3d_2b_1x1',
'Conv3d_2c_3x3',
'MaxPool3d_3a_3x3',
'Mixed_3b',
'Mixed_3c',
'MaxPool3d_4a_3x3',
'Mixed_4b',
'Mixed_4c',
'Mixed_4d',
'Mixed_4e',
'Mixed_4f',
'MaxPool3d_5a_2x2',
'Mixed_5b',
'Mixed_5c',
'Logits',
'Predictions',
)
"""
with torch.set_grad_enabled(grad_enabled):
feat = i3d_model.extract_features(batched_video.transpose(1, 2),
target_endpoint)
if flatten:
feat = feat.view(feat.size(0), -1)
return feat
# This code is from https://github.com/piergiaj/pytorch-i3d/blob/master/pytorch_i3d.py
# I only fix flake8 errors and do some cleaning here
class MaxPool3dSamePadding(nn.MaxPool3d):
def compute_pad(self, dim, s):
if s % self.stride[dim] == 0:
return max(self.kernel_size[dim] - self.stride[dim], 0)
else:
return max(self.kernel_size[dim] - (s % self.stride[dim]), 0)
def forward(self, x):
# compute 'same' padding
(batch, channel, t, h, w) = x.size()
pad_t = self.compute_pad(0, t)
pad_h = self.compute_pad(1, h)
pad_w = self.compute_pad(2, w)
pad_t_f = pad_t // 2
pad_t_b = pad_t - pad_t_f
pad_h_f = pad_h // 2
pad_h_b = pad_h - pad_h_f
pad_w_f = pad_w // 2
pad_w_b = pad_w - pad_w_f
pad = (pad_w_f, pad_w_b, pad_h_f, pad_h_b, pad_t_f, pad_t_b)
x = F.pad(x, pad)
return super(MaxPool3dSamePadding, self).forward(x)
class Unit3D(nn.Module):
def __init__(self,
in_channels,
output_channels,
kernel_shape=(1, 1, 1),
stride=(1, 1, 1),
padding=0,
activation_fn=F.relu,
use_batch_norm=True,
use_bias=False,
name='unit_3d'):
"""Initializes Unit3D module."""
super(Unit3D, self).__init__()
self._output_channels = output_channels
self._kernel_shape = kernel_shape
self._stride = stride
self._use_batch_norm = use_batch_norm
self._activation_fn = activation_fn
self._use_bias = use_bias
self.name = name
self.padding = padding
self.conv3d = nn.Conv3d(
in_channels=in_channels,
out_channels=self._output_channels,
kernel_size=self._kernel_shape,
stride=self._stride,
padding=0, # we always want padding to be 0 here. We will
# dynamically pad based on input size in forward function
bias=self._use_bias)
if self._use_batch_norm:
self.bn = nn.BatchNorm3d(self._output_channels,
eps=0.001,
momentum=0.01)
def compute_pad(self, dim, s):
if s % self._stride[dim] == 0:
return max(self._kernel_shape[dim] - self._stride[dim], 0)
else:
return max(self._kernel_shape[dim] - (s % self._stride[dim]), 0)
def forward(self, x):
# compute 'same' padding
(batch, channel, t, h, w) = x.size()
pad_t = self.compute_pad(0, t)
pad_h = self.compute_pad(1, h)
pad_w = self.compute_pad(2, w)
pad_t_f = pad_t // 2
pad_t_b = pad_t - pad_t_f
pad_h_f = pad_h // 2
pad_h_b = pad_h - pad_h_f
pad_w_f = pad_w // 2
pad_w_b = pad_w - pad_w_f
pad = (pad_w_f, pad_w_b, pad_h_f, pad_h_b, pad_t_f, pad_t_b)
x = F.pad(x, pad)
x = self.conv3d(x)
if self._use_batch_norm:
x = self.bn(x)
if self._activation_fn is not None:
x = self._activation_fn(x)
return x
class InceptionModule(nn.Module):
def __init__(self, in_channels, out_channels, name):
super(InceptionModule, self).__init__()
self.b0 = Unit3D(in_channels=in_channels,
output_channels=out_channels[0],
kernel_shape=[1, 1, 1],
padding=0,
name=name + '/Branch_0/Conv3d_0a_1x1')
self.b1a = Unit3D(in_channels=in_channels,
output_channels=out_channels[1],
kernel_shape=[1, 1, 1],
padding=0,
name=name + '/Branch_1/Conv3d_0a_1x1')
self.b1b = Unit3D(in_channels=out_channels[1],
output_channels=out_channels[2],
kernel_shape=[3, 3, 3],
name=name + '/Branch_1/Conv3d_0b_3x3')
self.b2a = Unit3D(in_channels=in_channels,
output_channels=out_channels[3],
kernel_shape=[1, 1, 1],
padding=0,
name=name + '/Branch_2/Conv3d_0a_1x1')
self.b2b = Unit3D(in_channels=out_channels[3],
output_channels=out_channels[4],
kernel_shape=[3, 3, 3],
name=name + '/Branch_2/Conv3d_0b_3x3')
self.b3a = MaxPool3dSamePadding(kernel_size=[3, 3, 3],
stride=(1, 1, 1),
padding=0)
self.b3b = Unit3D(in_channels=in_channels,
output_channels=out_channels[5],
kernel_shape=[1, 1, 1],
padding=0,
name=name + '/Branch_3/Conv3d_0b_1x1')
self.name = name
def forward(self, x):
b0 = self.b0(x)
b1 = self.b1b(self.b1a(x))
b2 = self.b2b(self.b2a(x))
b3 = self.b3b(self.b3a(x))
return torch.cat([b0, b1, b2, b3], dim=1)
class InceptionI3d(nn.Module):
"""Inception-v1 I3D architecture.
The model is introduced in:
Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset
Joao Carreira, Andrew Zisserman
https://arxiv.org/pdf/1705.07750v1.pdf.
See also the Inception architecture, introduced in:
Going deeper with convolutions
Christian Szegedy, Wei Liu, Yangqing Jia, Pierre Sermanet, Scott Reed,
Dragomir Anguelov, Dumitru Erhan, Vincent Vanhoucke, Andrew Rabinovich.
http://arxiv.org/pdf/1409.4842v1.pdf.
"""
# Endpoints of the model in order. During construction, all the endpoints up
# to a designated `final_endpoint` are returned in a dictionary as the
# second return value.
VALID_ENDPOINTS = (
'Conv3d_1a_7x7',
'MaxPool3d_2a_3x3',
'Conv3d_2b_1x1',
'Conv3d_2c_3x3',
'MaxPool3d_3a_3x3',
'Mixed_3b',
'Mixed_3c',
'MaxPool3d_4a_3x3',
'Mixed_4b',
'Mixed_4c',
'Mixed_4d',
'Mixed_4e',
'Mixed_4f',
'MaxPool3d_5a_2x2',
'Mixed_5b',
'Mixed_5c',
'Logits',
'Predictions',
)
def __init__(self,
num_classes=400,
spatial_squeeze=True,
final_endpoint='Logits',
name='inception_i3d',
in_channels=3,
dropout_keep_prob=0.5):
"""Initializes I3D model instance.
Args:
num_classes: The number of outputs in the logit layer (default 400, which
matches the Kinetics dataset).
spatial_squeeze: Whether to squeeze the spatial dimensions for the logits
before returning (default True).
final_endpoint: The model contains many possible endpoints.
`final_endpoint` specifies the last endpoint for the model to be built
up to. In addition to the output at `final_endpoint`, all the outputs
at endpoints up to `final_endpoint` will also be returned, in a
dictionary. `final_endpoint` must be one of
InceptionI3d.VALID_ENDPOINTS (default 'Logits').
name: A string (optional). The name of this module.
Raises:
ValueError: if `final_endpoint` is not recognized.
"""
if final_endpoint not in self.VALID_ENDPOINTS:
raise ValueError('Unknown final endpoint %s' % final_endpoint)
super(InceptionI3d, self).__init__()
self._num_classes = num_classes
self._spatial_squeeze = spatial_squeeze
self._final_endpoint = final_endpoint
self.logits = None
if self._final_endpoint not in self.VALID_ENDPOINTS:
raise ValueError('Unknown final endpoint %s' %
self._final_endpoint)
self.end_points = {}
end_point = 'Conv3d_1a_7x7'
self.end_points[end_point] = Unit3D(in_channels=in_channels,
output_channels=64,
kernel_shape=[7, 7, 7],
stride=(2, 2, 2),
padding=(3, 3, 3),
name=name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'MaxPool3d_2a_3x3'
self.end_points[end_point] = MaxPool3dSamePadding(
kernel_size=[1, 3, 3], stride=(1, 2, 2), padding=0)
if self._final_endpoint == end_point:
return
end_point = 'Conv3d_2b_1x1'
self.end_points[end_point] = Unit3D(in_channels=64,
output_channels=64,
kernel_shape=[1, 1, 1],
padding=0,
name=name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Conv3d_2c_3x3'
self.end_points[end_point] = Unit3D(in_channels=64,
output_channels=192,
kernel_shape=[3, 3, 3],
padding=1,
name=name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'MaxPool3d_3a_3x3'
self.end_points[end_point] = MaxPool3dSamePadding(
kernel_size=[1, 3, 3], stride=(1, 2, 2), padding=0)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_3b'
self.end_points[end_point] = InceptionModule(192,
[64, 96, 128, 16, 32, 32],
name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_3c'
self.end_points[end_point] = InceptionModule(
256, [128, 128, 192, 32, 96, 64], name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'MaxPool3d_4a_3x3'
self.end_points[end_point] = MaxPool3dSamePadding(
kernel_size=[3, 3, 3], stride=(2, 2, 2), padding=0)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_4b'
self.end_points[end_point] = InceptionModule(
128 + 192 + 96 + 64, [192, 96, 208, 16, 48, 64], name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_4c'
self.end_points[end_point] = InceptionModule(
192 + 208 + 48 + 64, [160, 112, 224, 24, 64, 64], name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_4d'
self.end_points[end_point] = InceptionModule(
160 + 224 + 64 + 64, [128, 128, 256, 24, 64, 64], name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_4e'
self.end_points[end_point] = InceptionModule(
128 + 256 + 64 + 64, [112, 144, 288, 32, 64, 64], name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_4f'
self.end_points[end_point] = InceptionModule(
112 + 288 + 64 + 64, [256, 160, 320, 32, 128, 128],
name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'MaxPool3d_5a_2x2'
self.end_points[end_point] = MaxPool3dSamePadding(
kernel_size=[2, 2, 2], stride=(2, 2, 2), padding=0)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_5b'
self.end_points[end_point] = InceptionModule(
256 + 320 + 128 + 128, [256, 160, 320, 32, 128, 128],
name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Mixed_5c'
self.end_points[end_point] = InceptionModule(
256 + 320 + 128 + 128, [384, 192, 384, 48, 128, 128],
name + end_point)
if self._final_endpoint == end_point:
return
end_point = 'Logits'
self.avg_pool = nn.AvgPool3d(kernel_size=[2, 7, 7], stride=(1, 1, 1))
self.dropout = nn.Dropout(dropout_keep_prob)
self.logits = Unit3D(in_channels=384 + 384 + 128 + 128,
output_channels=self._num_classes,
kernel_shape=[1, 1, 1],
padding=0,
activation_fn=None,
use_batch_norm=False,
use_bias=True,
name='logits')
self.build()
def replace_logits(self, num_classes):
self._num_classes = num_classes
self.logits = Unit3D(in_channels=384 + 384 + 128 + 128,
output_channels=self._num_classes,
kernel_shape=[1, 1, 1],
padding=0,
activation_fn=None,
use_batch_norm=False,
use_bias=True,
name='logits')
def build(self):
for k in self.end_points.keys():
self.add_module(k, self.end_points[k])
def forward(self, x):
for end_point in self.VALID_ENDPOINTS:
if end_point in self.end_points:
x = self._modules[end_point](
x) # use _modules to work with dataparallel
x = self.logits(self.dropout(self.avg_pool(x)))
if self._spatial_squeeze:
logits = x.squeeze(3).squeeze(3)
# logits is batch X time X classes, which is what we want to work with
return logits
def extract_features(self, x, target_endpoint='Logits'):
for end_point in self.VALID_ENDPOINTS:
if end_point in self.end_points:
x = self._modules[end_point](x)
if end_point == target_endpoint:
break
if target_endpoint == 'Logits':
return x.mean(4).mean(3).mean(2)
else:
return x
@@ -1,125 +0,0 @@
import queue as Queue
import threading
import torch
from torch.utils.data import DataLoader
class PrefetchGenerator(threading.Thread):
"""A general prefetch generator.
Ref:
https://stackoverflow.com/questions/7323664/python-generator-pre-fetch
Args:
generator: Python generator.
num_prefetch_queue (int): Number of prefetch queue.
"""
def __init__(self, generator, num_prefetch_queue):
threading.Thread.__init__(self)
self.queue = Queue.Queue(num_prefetch_queue)
self.generator = generator
self.daemon = True
self.start()
def run(self):
for item in self.generator:
self.queue.put(item)
self.queue.put(None)
def __next__(self):
next_item = self.queue.get()
if next_item is None:
raise StopIteration
return next_item
def __iter__(self):
return self
class PrefetchDataLoader(DataLoader):
"""Prefetch version of dataloader.
Ref:
https://github.com/IgorSusmelj/pytorch-styleguide/issues/5#
TODO:
Need to test on single gpu and ddp (multi-gpu). There is a known issue in
ddp.
Args:
num_prefetch_queue (int): Number of prefetch queue.
kwargs (dict): Other arguments for dataloader.
"""
def __init__(self, num_prefetch_queue, **kwargs):
self.num_prefetch_queue = num_prefetch_queue
super(PrefetchDataLoader, self).__init__(**kwargs)
def __iter__(self):
return PrefetchGenerator(super().__iter__(), self.num_prefetch_queue)
class CPUPrefetcher():
"""CPU prefetcher.
Args:
loader: Dataloader.
"""
def __init__(self, loader):
self.ori_loader = loader
self.loader = iter(loader)
def next(self):
try:
return next(self.loader)
except StopIteration:
return None
def reset(self):
self.loader = iter(self.ori_loader)
class CUDAPrefetcher():
"""CUDA prefetcher.
Ref:
https://github.com/NVIDIA/apex/issues/304#
It may consums more GPU memory.
Args:
loader: Dataloader.
opt (dict): Options.
"""
def __init__(self, loader, opt):
self.ori_loader = loader
self.loader = iter(loader)
self.opt = opt
self.stream = torch.cuda.Stream()
self.device = torch.device('cuda' if opt['num_gpu'] != 0 else 'cpu')
self.preload()
def preload(self):
try:
self.batch = next(self.loader) # self.batch is a dict
except StopIteration:
self.batch = None
return None
# put tensors to gpu
with torch.cuda.stream(self.stream):
for k, v in self.batch.items():
if torch.is_tensor(v):
self.batch[k] = self.batch[k].to(device=self.device, non_blocking=True)
def next(self):
torch.cuda.current_stream().wait_stream(self.stream)
batch = self.batch
self.preload()
return batch
def reset(self):
self.loader = iter(self.ori_loader)
self.preload()
-509
View File
@@ -1,509 +0,0 @@
import os
import glob
import logging
import importlib
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
from core.prefetch_dataloader import PrefetchDataLoader, CPUPrefetcher
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
import torchvision
from torch.utils.tensorboard import SummaryWriter
from core.lr_scheduler import MultiStepRestartLR, CosineAnnealingRestartLR
from core.loss import AdversarialLoss, PerceptualLoss, LPIPSLoss
from core.dataset import TrainDataset
from model.modules.flow_comp_raft import RAFT_bi, FlowLoss, EdgeLoss
from model.recurrent_flow_completion import RecurrentFlowCompleteNet
from RAFT.utils.flow_viz_pt import flow_to_image
class Trainer:
def __init__(self, config):
self.config = config
self.epoch = 0
self.iteration = 0
self.num_local_frames = config['train_data_loader']['num_local_frames']
self.num_ref_frames = config['train_data_loader']['num_ref_frames']
# setup data set and data loader
self.train_dataset = TrainDataset(config['train_data_loader'])
self.train_sampler = None
self.train_args = config['trainer']
if config['distributed']:
self.train_sampler = DistributedSampler(
self.train_dataset,
num_replicas=config['world_size'],
rank=config['global_rank'])
dataloader_args = dict(
dataset=self.train_dataset,
batch_size=self.train_args['batch_size'] // config['world_size'],
shuffle=(self.train_sampler is None),
num_workers=self.train_args['num_workers'],
sampler=self.train_sampler,
drop_last=True)
self.train_loader = PrefetchDataLoader(self.train_args['num_prefetch_queue'], **dataloader_args)
self.prefetcher = CPUPrefetcher(self.train_loader)
# set loss functions
self.adversarial_loss = AdversarialLoss(type=self.config['losses']['GAN_LOSS'])
self.adversarial_loss = self.adversarial_loss.to(self.config['device'])
self.l1_loss = nn.L1Loss()
# self.perc_loss = PerceptualLoss(
# layer_weights={'conv3_4': 0.25, 'conv4_4': 0.25, 'conv5_4': 0.5},
# use_input_norm=True,
# range_norm=True,
# criterion='l1'
# ).to(self.config['device'])
if self.config['losses']['perceptual_weight'] > 0:
self.perc_loss = LPIPSLoss(use_input_norm=True, range_norm=True).to(self.config['device'])
# self.flow_comp_loss = FlowCompletionLoss().to(self.config['device'])
# self.flow_comp_loss = FlowCompletionLoss(self.config['device'])
# set raft
self.fix_raft = RAFT_bi(device = self.config['device'])
self.fix_flow_complete = RecurrentFlowCompleteNet('/mnt/lustre/sczhou/VQGANs/CodeMOVI/experiments_model/recurrent_flow_completion_v5_train_flowcomp_v5/gen_760000.pth')
for p in self.fix_flow_complete.parameters():
p.requires_grad = False
self.fix_flow_complete.to(self.config['device'])
self.fix_flow_complete.eval()
# self.flow_loss = FlowLoss()
# setup models including generator and discriminator
net = importlib.import_module('model.' + config['model']['net'])
self.netG = net.InpaintGenerator()
# print(self.netG)
self.netG = self.netG.to(self.config['device'])
if not self.config['model'].get('no_dis', False):
if self.config['model'].get('dis_2d', False):
self.netD = net.Discriminator_2D(
in_channels=3,
use_sigmoid=config['losses']['GAN_LOSS'] != 'hinge')
else:
self.netD = net.Discriminator(
in_channels=3,
use_sigmoid=config['losses']['GAN_LOSS'] != 'hinge')
self.netD = self.netD.to(self.config['device'])
self.interp_mode = self.config['model']['interp_mode']
# setup optimizers and schedulers
self.setup_optimizers()
self.setup_schedulers()
self.load()
if config['distributed']:
self.netG = DDP(self.netG,
device_ids=[self.config['local_rank']],
output_device=self.config['local_rank'],
broadcast_buffers=True,
find_unused_parameters=True)
if not self.config['model']['no_dis']:
self.netD = DDP(self.netD,
device_ids=[self.config['local_rank']],
output_device=self.config['local_rank'],
broadcast_buffers=True,
find_unused_parameters=False)
# set summary writer
self.dis_writer = None
self.gen_writer = None
self.summary = {}
if self.config['global_rank'] == 0 or (not config['distributed']):
if not self.config['model']['no_dis']:
self.dis_writer = SummaryWriter(
os.path.join(config['save_dir'], 'dis'))
self.gen_writer = SummaryWriter(
os.path.join(config['save_dir'], 'gen'))
def setup_optimizers(self):
"""Set up optimizers."""
backbone_params = []
for name, param in self.netG.named_parameters():
if param.requires_grad:
backbone_params.append(param)
else:
print(f'Params {name} will not be optimized.')
optim_params = [
{
'params': backbone_params,
'lr': self.config['trainer']['lr']
},
]
self.optimG = torch.optim.Adam(optim_params,
betas=(self.config['trainer']['beta1'],
self.config['trainer']['beta2']))
if not self.config['model']['no_dis']:
self.optimD = torch.optim.Adam(
self.netD.parameters(),
lr=self.config['trainer']['lr'],
betas=(self.config['trainer']['beta1'],
self.config['trainer']['beta2']))
def setup_schedulers(self):
"""Set up schedulers."""
scheduler_opt = self.config['trainer']['scheduler']
scheduler_type = scheduler_opt.pop('type')
if scheduler_type in ['MultiStepLR', 'MultiStepRestartLR']:
self.scheG = MultiStepRestartLR(
self.optimG,
milestones=scheduler_opt['milestones'],
gamma=scheduler_opt['gamma'])
if not self.config['model']['no_dis']:
self.scheD = MultiStepRestartLR(
self.optimD,
milestones=scheduler_opt['milestones'],
gamma=scheduler_opt['gamma'])
elif scheduler_type == 'CosineAnnealingRestartLR':
self.scheG = CosineAnnealingRestartLR(
self.optimG,
periods=scheduler_opt['periods'],
restart_weights=scheduler_opt['restart_weights'],
eta_min=scheduler_opt['eta_min'])
if not self.config['model']['no_dis']:
self.scheD = CosineAnnealingRestartLR(
self.optimD,
periods=scheduler_opt['periods'],
restart_weights=scheduler_opt['restart_weights'],
eta_min=scheduler_opt['eta_min'])
else:
raise NotImplementedError(
f'Scheduler {scheduler_type} is not implemented yet.')
def update_learning_rate(self):
"""Update learning rate."""
self.scheG.step()
if not self.config['model']['no_dis']:
self.scheD.step()
def get_lr(self):
"""Get current learning rate."""
return self.optimG.param_groups[0]['lr']
def add_summary(self, writer, name, val):
"""Add tensorboard summary."""
if name not in self.summary:
self.summary[name] = 0
self.summary[name] += val
n = self.train_args['log_freq']
if writer is not None and self.iteration % n == 0:
writer.add_scalar(name, self.summary[name] / n, self.iteration)
self.summary[name] = 0
def load(self):
"""Load netG (and netD)."""
# get the latest checkpoint
model_path = self.config['save_dir']
# TODO: add resume name
if os.path.isfile(os.path.join(model_path, 'latest.ckpt')):
latest_epoch = open(os.path.join(model_path, 'latest.ckpt'),
'r').read().splitlines()[-1]
else:
ckpts = [
os.path.basename(i).split('.pth')[0]
for i in glob.glob(os.path.join(model_path, '*.pth'))
]
ckpts.sort()
latest_epoch = ckpts[-1][4:] if len(ckpts) > 0 else None
if latest_epoch is not None:
gen_path = os.path.join(model_path,
f'gen_{int(latest_epoch):06d}.pth')
dis_path = os.path.join(model_path,
f'dis_{int(latest_epoch):06d}.pth')
opt_path = os.path.join(model_path,
f'opt_{int(latest_epoch):06d}.pth')
if self.config['global_rank'] == 0:
print(f'Loading model from {gen_path}...')
dataG = torch.load(gen_path, map_location=self.config['device'])
self.netG.load_state_dict(dataG)
if not self.config['model']['no_dis'] and self.config['model']['load_d']:
dataD = torch.load(dis_path, map_location=self.config['device'])
self.netD.load_state_dict(dataD)
data_opt = torch.load(opt_path, map_location=self.config['device'])
self.optimG.load_state_dict(data_opt['optimG'])
# self.scheG.load_state_dict(data_opt['scheG'])
if not self.config['model']['no_dis'] and self.config['model']['load_d']:
self.optimD.load_state_dict(data_opt['optimD'])
# self.scheD.load_state_dict(data_opt['scheD'])
self.epoch = data_opt['epoch']
self.iteration = data_opt['iteration']
else:
gen_path = self.config['trainer'].get('gen_path', None)
dis_path = self.config['trainer'].get('dis_path', None)
opt_path = self.config['trainer'].get('opt_path', None)
if gen_path is not None:
if self.config['global_rank'] == 0:
print(f'Loading Gen-Net from {gen_path}...')
dataG = torch.load(gen_path, map_location=self.config['device'])
self.netG.load_state_dict(dataG)
if dis_path is not None and not self.config['model']['no_dis'] and self.config['model']['load_d']:
if self.config['global_rank'] == 0:
print(f'Loading Dis-Net from {dis_path}...')
dataD = torch.load(dis_path, map_location=self.config['device'])
self.netD.load_state_dict(dataD)
if opt_path is not None:
data_opt = torch.load(opt_path, map_location=self.config['device'])
self.optimG.load_state_dict(data_opt['optimG'])
self.scheG.load_state_dict(data_opt['scheG'])
if not self.config['model']['no_dis'] and self.config['model']['load_d']:
self.optimD.load_state_dict(data_opt['optimD'])
self.scheD.load_state_dict(data_opt['scheD'])
else:
if self.config['global_rank'] == 0:
print('Warnning: There is no trained model found.'
'An initialized model will be used.')
def save(self, it):
"""Save parameters every eval_epoch"""
if self.config['global_rank'] == 0:
# configure path
gen_path = os.path.join(self.config['save_dir'],
f'gen_{it:06d}.pth')
dis_path = os.path.join(self.config['save_dir'],
f'dis_{it:06d}.pth')
opt_path = os.path.join(self.config['save_dir'],
f'opt_{it:06d}.pth')
print(f'\nsaving model to {gen_path} ...')
# remove .module for saving
if isinstance(self.netG, torch.nn.DataParallel) or isinstance(self.netG, DDP):
netG = self.netG.module
if not self.config['model']['no_dis']:
netD = self.netD.module
else:
netG = self.netG
if not self.config['model']['no_dis']:
netD = self.netD
# save checkpoints
torch.save(netG.state_dict(), gen_path)
if not self.config['model']['no_dis']:
torch.save(netD.state_dict(), dis_path)
torch.save(
{
'epoch': self.epoch,
'iteration': self.iteration,
'optimG': self.optimG.state_dict(),
'optimD': self.optimD.state_dict(),
'scheG': self.scheG.state_dict(),
'scheD': self.scheD.state_dict()
}, opt_path)
else:
torch.save(
{
'epoch': self.epoch,
'iteration': self.iteration,
'optimG': self.optimG.state_dict(),
'scheG': self.scheG.state_dict()
}, opt_path)
latest_path = os.path.join(self.config['save_dir'], 'latest.ckpt')
os.system(f"echo {it:06d} > {latest_path}")
def train(self):
"""training entry"""
pbar = range(int(self.train_args['iterations']))
if self.config['global_rank'] == 0:
pbar = tqdm(pbar,
initial=self.iteration,
dynamic_ncols=True,
smoothing=0.01)
os.makedirs('logs', exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(filename)s[line:%(lineno)d]"
"%(levelname)s %(message)s",
datefmt="%a, %d %b %Y %H:%M:%S",
filename=f"logs/{self.config['save_dir'].split('/')[-1]}.log",
filemode='w')
while True:
self.epoch += 1
self.prefetcher.reset()
if self.config['distributed']:
self.train_sampler.set_epoch(self.epoch)
self._train_epoch(pbar)
if self.iteration > self.train_args['iterations']:
break
print('\nEnd training....')
def _train_epoch(self, pbar):
"""Process input and calculate loss every training epoch"""
device = self.config['device']
train_data = self.prefetcher.next()
while train_data is not None:
self.iteration += 1
frames, masks, flows_f, flows_b, _ = train_data
frames, masks = frames.to(device), masks.to(device).float()
l_t = self.num_local_frames
b, t, c, h, w = frames.size()
gt_local_frames = frames[:, :l_t, ...]
local_masks = masks[:, :l_t, ...].contiguous()
masked_frames = frames * (1 - masks)
masked_local_frames = masked_frames[:, :l_t, ...]
# get gt optical flow
if flows_f[0] == 'None' or flows_b[0] == 'None':
gt_flows_bi = self.fix_raft(gt_local_frames)
else:
gt_flows_bi = (flows_f.to(device), flows_b.to(device))
# ---- complete flow ----
pred_flows_bi, _ = self.fix_flow_complete.forward_bidirect_flow(gt_flows_bi, local_masks)
pred_flows_bi = self.fix_flow_complete.combine_flow(gt_flows_bi, pred_flows_bi, local_masks)
# pred_flows_bi = gt_flows_bi
# ---- image propagation ----
prop_imgs, updated_local_masks = self.netG.module.img_propagation(masked_local_frames, pred_flows_bi, local_masks, interpolation=self.interp_mode)
updated_masks = masks.clone()
updated_masks[:, :l_t, ...] = updated_local_masks.view(b, l_t, 1, h, w)
updated_frames = masked_frames.clone()
prop_local_frames = gt_local_frames * (1-local_masks) + prop_imgs.view(b, l_t, 3, h, w) * local_masks # merge
updated_frames[:, :l_t, ...] = prop_local_frames
# ---- feature propagation + Transformer ----
pred_imgs = self.netG(updated_frames, pred_flows_bi, masks, updated_masks, l_t)
pred_imgs = pred_imgs.view(b, -1, c, h, w)
# get the local frames
pred_local_frames = pred_imgs[:, :l_t, ...]
comp_local_frames = gt_local_frames * (1. - local_masks) + pred_local_frames * local_masks
comp_imgs = frames * (1. - masks) + pred_imgs * masks
gen_loss = 0
dis_loss = 0
# optimize net_g
if not self.config['model']['no_dis']:
for p in self.netD.parameters():
p.requires_grad = False
self.optimG.zero_grad()
# generator l1 loss
hole_loss = self.l1_loss(pred_imgs * masks, frames * masks)
hole_loss = hole_loss / torch.mean(masks) * self.config['losses']['hole_weight']
gen_loss += hole_loss
self.add_summary(self.gen_writer, 'loss/hole_loss', hole_loss.item())
valid_loss = self.l1_loss(pred_imgs * (1 - masks), frames * (1 - masks))
valid_loss = valid_loss / torch.mean(1-masks) * self.config['losses']['valid_weight']
gen_loss += valid_loss
self.add_summary(self.gen_writer, 'loss/valid_loss', valid_loss.item())
# perceptual loss
if self.config['losses']['perceptual_weight'] > 0:
perc_loss = self.perc_loss(pred_imgs.view(-1,3,h,w), frames.view(-1,3,h,w))[0] * self.config['losses']['perceptual_weight']
gen_loss += perc_loss
self.add_summary(self.gen_writer, 'loss/perc_loss', perc_loss.item())
# gan loss
if not self.config['model']['no_dis']:
# generator adversarial loss
gen_clip = self.netD(comp_imgs)
gan_loss = self.adversarial_loss(gen_clip, True, False)
gan_loss = gan_loss * self.config['losses']['adversarial_weight']
gen_loss += gan_loss
self.add_summary(self.gen_writer, 'loss/gan_loss', gan_loss.item())
gen_loss.backward()
self.optimG.step()
if not self.config['model']['no_dis']:
# optimize net_d
for p in self.netD.parameters():
p.requires_grad = True
self.optimD.zero_grad()
# discriminator adversarial loss
real_clip = self.netD(frames)
fake_clip = self.netD(comp_imgs.detach())
dis_real_loss = self.adversarial_loss(real_clip, True, True)
dis_fake_loss = self.adversarial_loss(fake_clip, False, True)
dis_loss += (dis_real_loss + dis_fake_loss) / 2
self.add_summary(self.dis_writer, 'loss/dis_vid_real', dis_real_loss.item())
self.add_summary(self.dis_writer, 'loss/dis_vid_fake', dis_fake_loss.item())
dis_loss.backward()
self.optimD.step()
self.update_learning_rate()
# write image to tensorboard
if self.iteration % 200 == 0:
# img to cpu
t = 0
gt_local_frames_cpu = ((gt_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
masked_local_frames = ((masked_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
prop_local_frames_cpu = ((prop_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
pred_local_frames_cpu = ((pred_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
img_results = torch.cat([masked_local_frames[0][t], gt_local_frames_cpu[0][t],
prop_local_frames_cpu[0][t], pred_local_frames_cpu[0][t]], 1)
img_results = torchvision.utils.make_grid(img_results, nrow=1, normalize=True)
if self.gen_writer is not None:
self.gen_writer.add_image(f'img/img:inp-gt-res-{t}', img_results, self.iteration)
t = 5
if masked_local_frames.shape[1] > 5:
img_results = torch.cat([masked_local_frames[0][t], gt_local_frames_cpu[0][t],
prop_local_frames_cpu[0][t], pred_local_frames_cpu[0][t]], 1)
img_results = torchvision.utils.make_grid(img_results, nrow=1, normalize=True)
if self.gen_writer is not None:
self.gen_writer.add_image(f'img/img:inp-gt-res-{t}', img_results, self.iteration)
# flow to cpu
gt_flows_forward_cpu = flow_to_image(gt_flows_bi[0][0]).cpu()
masked_flows_forward_cpu = (gt_flows_forward_cpu[0] * (1-local_masks[0][0].cpu())).to(gt_flows_forward_cpu)
pred_flows_forward_cpu = flow_to_image(pred_flows_bi[0][0]).cpu()
flow_results = torch.cat([gt_flows_forward_cpu[0], masked_flows_forward_cpu, pred_flows_forward_cpu[0]], 1)
if self.gen_writer is not None:
self.gen_writer.add_image('img/flow:gt-pred', flow_results, self.iteration)
# console logs
if self.config['global_rank'] == 0:
pbar.update(1)
if not self.config['model']['no_dis']:
pbar.set_description((f"d: {dis_loss.item():.3f}; "
f"hole: {hole_loss.item():.3f}; "
f"valid: {valid_loss.item():.3f}"))
else:
pbar.set_description((f"hole: {hole_loss.item():.3f}; "
f"valid: {valid_loss.item():.3f}"))
if self.iteration % self.train_args['log_freq'] == 0:
if not self.config['model']['no_dis']:
logging.info(f"[Iter {self.iteration}] "
f"d: {dis_loss.item():.4f}; "
f"hole: {hole_loss.item():.4f}; "
f"valid: {valid_loss.item():.4f}")
else:
logging.info(f"[Iter {self.iteration}] "
f"hole: {hole_loss.item():.4f}; "
f"valid: {valid_loss.item():.4f}")
# saving models
if self.iteration % self.train_args['save_freq'] == 0:
self.save(int(self.iteration))
if self.iteration > self.train_args['iterations']:
break
train_data = self.prefetcher.next()
@@ -1,380 +0,0 @@
import os
import glob
import logging
import importlib
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
from core.prefetch_dataloader import PrefetchDataLoader, CPUPrefetcher
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.tensorboard import SummaryWriter
from core.lr_scheduler import MultiStepRestartLR, CosineAnnealingRestartLR
from core.dataset import TrainDataset
from model.modules.flow_comp_raft import RAFT_bi, FlowLoss, EdgeLoss
# from skimage.feature import canny
from model.canny.canny_filter import Canny
from RAFT.utils.flow_viz_pt import flow_to_image
class Trainer:
def __init__(self, config):
self.config = config
self.epoch = 0
self.iteration = 0
self.num_local_frames = config['train_data_loader']['num_local_frames']
self.num_ref_frames = config['train_data_loader']['num_ref_frames']
# setup data set and data loader
self.train_dataset = TrainDataset(config['train_data_loader'])
self.train_sampler = None
self.train_args = config['trainer']
if config['distributed']:
self.train_sampler = DistributedSampler(
self.train_dataset,
num_replicas=config['world_size'],
rank=config['global_rank'])
dataloader_args = dict(
dataset=self.train_dataset,
batch_size=self.train_args['batch_size'] // config['world_size'],
shuffle=(self.train_sampler is None),
num_workers=self.train_args['num_workers'],
sampler=self.train_sampler,
drop_last=True)
self.train_loader = PrefetchDataLoader(self.train_args['num_prefetch_queue'], **dataloader_args)
self.prefetcher = CPUPrefetcher(self.train_loader)
# set raft
self.fix_raft = RAFT_bi(device = self.config['device'])
self.flow_loss = FlowLoss()
self.edge_loss = EdgeLoss()
self.canny = Canny(sigma=(2,2), low_threshold=0.1, high_threshold=0.2)
# setup models including generator and discriminator
net = importlib.import_module('model.' + config['model']['net'])
self.netG = net.RecurrentFlowCompleteNet()
# print(self.netG)
self.netG = self.netG.to(self.config['device'])
# setup optimizers and schedulers
self.setup_optimizers()
self.setup_schedulers()
self.load()
if config['distributed']:
self.netG = DDP(self.netG,
device_ids=[self.config['local_rank']],
output_device=self.config['local_rank'],
broadcast_buffers=True,
find_unused_parameters=True)
# set summary writer
self.dis_writer = None
self.gen_writer = None
self.summary = {}
if self.config['global_rank'] == 0 or (not config['distributed']):
self.gen_writer = SummaryWriter(
os.path.join(config['save_dir'], 'gen'))
def setup_optimizers(self):
"""Set up optimizers."""
backbone_params = []
for name, param in self.netG.named_parameters():
if param.requires_grad:
backbone_params.append(param)
else:
print(f'Params {name} will not be optimized.')
optim_params = [
{
'params': backbone_params,
'lr': self.config['trainer']['lr']
},
]
self.optimG = torch.optim.Adam(optim_params,
betas=(self.config['trainer']['beta1'],
self.config['trainer']['beta2']))
def setup_schedulers(self):
"""Set up schedulers."""
scheduler_opt = self.config['trainer']['scheduler']
scheduler_type = scheduler_opt.pop('type')
if scheduler_type in ['MultiStepLR', 'MultiStepRestartLR']:
self.scheG = MultiStepRestartLR(
self.optimG,
milestones=scheduler_opt['milestones'],
gamma=scheduler_opt['gamma'])
elif scheduler_type == 'CosineAnnealingRestartLR':
self.scheG = CosineAnnealingRestartLR(
self.optimG,
periods=scheduler_opt['periods'],
restart_weights=scheduler_opt['restart_weights'])
else:
raise NotImplementedError(
f'Scheduler {scheduler_type} is not implemented yet.')
def update_learning_rate(self):
"""Update learning rate."""
self.scheG.step()
def get_lr(self):
"""Get current learning rate."""
return self.optimG.param_groups[0]['lr']
def add_summary(self, writer, name, val):
"""Add tensorboard summary."""
if name not in self.summary:
self.summary[name] = 0
self.summary[name] += val
n = self.train_args['log_freq']
if writer is not None and self.iteration % n == 0:
writer.add_scalar(name, self.summary[name] / n, self.iteration)
self.summary[name] = 0
def load(self):
"""Load netG."""
# get the latest checkpoint
model_path = self.config['save_dir']
if os.path.isfile(os.path.join(model_path, 'latest.ckpt')):
latest_epoch = open(os.path.join(model_path, 'latest.ckpt'),
'r').read().splitlines()[-1]
else:
ckpts = [
os.path.basename(i).split('.pth')[0]
for i in glob.glob(os.path.join(model_path, '*.pth'))
]
ckpts.sort()
latest_epoch = ckpts[-1][4:] if len(ckpts) > 0 else None
if latest_epoch is not None:
gen_path = os.path.join(model_path, f'gen_{int(latest_epoch):06d}.pth')
opt_path = os.path.join(model_path,f'opt_{int(latest_epoch):06d}.pth')
if self.config['global_rank'] == 0:
print(f'Loading model from {gen_path}...')
dataG = torch.load(gen_path, map_location=self.config['device'])
self.netG.load_state_dict(dataG)
data_opt = torch.load(opt_path, map_location=self.config['device'])
self.optimG.load_state_dict(data_opt['optimG'])
self.scheG.load_state_dict(data_opt['scheG'])
self.epoch = data_opt['epoch']
self.iteration = data_opt['iteration']
else:
if self.config['global_rank'] == 0:
print('Warnning: There is no trained model found.'
'An initialized model will be used.')
def save(self, it):
"""Save parameters every eval_epoch"""
if self.config['global_rank'] == 0:
# configure path
gen_path = os.path.join(self.config['save_dir'],
f'gen_{it:06d}.pth')
opt_path = os.path.join(self.config['save_dir'],
f'opt_{it:06d}.pth')
print(f'\nsaving model to {gen_path} ...')
# remove .module for saving
if isinstance(self.netG, torch.nn.DataParallel) or isinstance(self.netG, DDP):
netG = self.netG.module
else:
netG = self.netG
# save checkpoints
torch.save(netG.state_dict(), gen_path)
torch.save(
{
'epoch': self.epoch,
'iteration': self.iteration,
'optimG': self.optimG.state_dict(),
'scheG': self.scheG.state_dict()
}, opt_path)
latest_path = os.path.join(self.config['save_dir'], 'latest.ckpt')
os.system(f"echo {it:06d} > {latest_path}")
def train(self):
"""training entry"""
pbar = range(int(self.train_args['iterations']))
if self.config['global_rank'] == 0:
pbar = tqdm(pbar,
initial=self.iteration,
dynamic_ncols=True,
smoothing=0.01)
os.makedirs('logs', exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(filename)s[line:%(lineno)d]"
"%(levelname)s %(message)s",
datefmt="%a, %d %b %Y %H:%M:%S",
filename=f"logs/{self.config['save_dir'].split('/')[-1]}.log",
filemode='w')
while True:
self.epoch += 1
self.prefetcher.reset()
if self.config['distributed']:
self.train_sampler.set_epoch(self.epoch)
self._train_epoch(pbar)
if self.iteration > self.train_args['iterations']:
break
print('\nEnd training....')
# def get_edges(self, flows): # fgvc
# # (b, t, 2, H, W)
# b, t, _, h, w = flows.shape
# flows = flows.view(-1, 2, h, w)
# flows_list = flows.permute(0, 2, 3, 1).cpu().numpy()
# edges = []
# for f in list(flows_list):
# flows_gray = (f[:, :, 0] ** 2 + f[:, :, 1] ** 2) ** 0.5
# if flows_gray.max() < 1:
# flows_gray = flows_gray*0
# else:
# flows_gray = flows_gray / flows_gray.max()
# edge = canny(flows_gray, sigma=2, low_threshold=0.1, high_threshold=0.2) # fgvc
# edge = torch.from_numpy(edge).view(1, 1, h, w).float()
# edges.append(edge)
# edges = torch.stack(edges, dim=0).to(self.config['device'])
# edges = edges.view(b, t, 1, h, w)
# return edges
def get_edges(self, flows):
# (b, t, 2, H, W)
b, t, _, h, w = flows.shape
flows = flows.view(-1, 2, h, w)
flows_gray = (flows[:, 0, None] ** 2 + flows[:, 1, None] ** 2) ** 0.5
if flows_gray.max() < 1:
flows_gray = flows_gray*0
else:
flows_gray = flows_gray / flows_gray.max()
magnitude, edges = self.canny(flows_gray.float())
edges = edges.view(b, t, 1, h, w)
return edges
def _train_epoch(self, pbar):
"""Process input and calculate loss every training epoch"""
device = self.config['device']
train_data = self.prefetcher.next()
while train_data is not None:
self.iteration += 1
frames, masks, flows_f, flows_b, _ = train_data
frames, masks = frames.to(device), masks.to(device)
masks = masks.float()
l_t = self.num_local_frames
b, t, c, h, w = frames.size()
gt_local_frames = frames[:, :l_t, ...]
local_masks = masks[:, :l_t, ...].contiguous()
# get gt optical flow
if flows_f[0] == 'None' or flows_b[0] == 'None':
gt_flows_bi = self.fix_raft(gt_local_frames)
else:
gt_flows_bi = (flows_f.to(device), flows_b.to(device))
# get gt edge
gt_edges_forward = self.get_edges(gt_flows_bi[0])
gt_edges_backward = self.get_edges(gt_flows_bi[1])
gt_edges_bi = [gt_edges_forward, gt_edges_backward]
# complete flow
pred_flows_bi, pred_edges_bi = self.netG.module.forward_bidirect_flow(gt_flows_bi, local_masks)
# optimize net_g
self.optimG.zero_grad()
# compulte flow_loss
flow_loss, warp_loss = self.flow_loss(pred_flows_bi, gt_flows_bi, local_masks, gt_local_frames)
flow_loss = flow_loss * self.config['losses']['flow_weight']
warp_loss = warp_loss * 0.01
self.add_summary(self.gen_writer, 'loss/flow_loss', flow_loss.item())
self.add_summary(self.gen_writer, 'loss/warp_loss', warp_loss.item())
# compute edge loss
edge_loss = self.edge_loss(pred_edges_bi, gt_edges_bi, local_masks)
edge_loss = edge_loss*1.0
self.add_summary(self.gen_writer, 'loss/edge_loss', edge_loss.item())
loss = flow_loss + warp_loss + edge_loss
loss.backward()
self.optimG.step()
self.update_learning_rate()
# write image to tensorboard
# if self.iteration % 200 == 0:
if self.iteration % 200 == 0 and self.gen_writer is not None:
t = 5
# forward to cpu
gt_flows_forward_cpu = flow_to_image(gt_flows_bi[0][0]).cpu()
masked_flows_forward_cpu = (gt_flows_forward_cpu[t] * (1-local_masks[0][t].cpu())).to(gt_flows_forward_cpu)
pred_flows_forward_cpu = flow_to_image(pred_flows_bi[0][0]).cpu()
flow_results = torch.cat([gt_flows_forward_cpu[t], masked_flows_forward_cpu, pred_flows_forward_cpu[t]], 1)
self.gen_writer.add_image('img/flow-f:gt-pred', flow_results, self.iteration)
# backward to cpu
gt_flows_backward_cpu = flow_to_image(gt_flows_bi[1][0]).cpu()
masked_flows_backward_cpu = (gt_flows_backward_cpu[t] * (1-local_masks[0][t+1].cpu())).to(gt_flows_backward_cpu)
pred_flows_backward_cpu = flow_to_image(pred_flows_bi[1][0]).cpu()
flow_results = torch.cat([gt_flows_backward_cpu[t], masked_flows_backward_cpu, pred_flows_backward_cpu[t]], 1)
self.gen_writer.add_image('img/flow-b:gt-pred', flow_results, self.iteration)
# TODO: show edge
# forward
gt_edges_forward_cpu = gt_edges_bi[0][0].cpu()
masked_edges_forward_cpu = (gt_edges_forward_cpu[t] * (1-local_masks[0][t].cpu())).to(gt_edges_forward_cpu)
pred_edges_forward_cpu = pred_edges_bi[0][0].cpu()
edge_results = torch.cat([gt_edges_forward_cpu[t], masked_edges_forward_cpu, pred_edges_forward_cpu[t]], 1)
self.gen_writer.add_image('img/edge-f:gt-pred', edge_results, self.iteration)
# backward
gt_edges_backward_cpu = gt_edges_bi[1][0].cpu()
masked_edges_backward_cpu = (gt_edges_backward_cpu[t] * (1-local_masks[0][t+1].cpu())).to(gt_edges_backward_cpu)
pred_edges_backward_cpu = pred_edges_bi[1][0].cpu()
edge_results = torch.cat([gt_edges_backward_cpu[t], masked_edges_backward_cpu, pred_edges_backward_cpu[t]], 1)
self.gen_writer.add_image('img/edge-b:gt-pred', edge_results, self.iteration)
# console logs
if self.config['global_rank'] == 0:
pbar.update(1)
pbar.set_description((f"flow: {flow_loss.item():.3f}; "
f"warp: {warp_loss.item():.3f}; "
f"edge: {edge_loss.item():.3f}; "
f"lr: {self.get_lr()}"))
if self.iteration % self.train_args['log_freq'] == 0:
logging.info(f"[Iter {self.iteration}] "
f"flow: {flow_loss.item():.4f}; "
f"warp: {warp_loss.item():.4f}")
# saving models
if self.iteration % self.train_args['save_freq'] == 0:
self.save(int(self.iteration))
if self.iteration > self.train_args['iterations']:
break
train_data = self.prefetcher.next()
-371
View File
@@ -1,371 +0,0 @@
import os
import io
import cv2
import random
import numpy as np
from PIL import Image, ImageOps
import zipfile
import math
import torch
import matplotlib
import matplotlib.patches as patches
from matplotlib.path import Path
from matplotlib import pyplot as plt
from torchvision import transforms
# matplotlib.use('agg')
# ###########################################################################
# Directory IO
# ###########################################################################
def read_dirnames_under_root(root_dir):
dirnames = [
name for i, name in enumerate(sorted(os.listdir(root_dir)))
if os.path.isdir(os.path.join(root_dir, name))
]
print(f'Reading directories under {root_dir}, num: {len(dirnames)}')
return dirnames
class TrainZipReader(object):
file_dict = dict()
def __init__(self):
super(TrainZipReader, self).__init__()
@staticmethod
def build_file_dict(path):
file_dict = TrainZipReader.file_dict
if path in file_dict:
return file_dict[path]
else:
file_handle = zipfile.ZipFile(path, 'r')
file_dict[path] = file_handle
return file_dict[path]
@staticmethod
def imread(path, idx):
zfile = TrainZipReader.build_file_dict(path)
filelist = zfile.namelist()
filelist.sort()
data = zfile.read(filelist[idx])
#
im = Image.open(io.BytesIO(data))
return im
class TestZipReader(object):
file_dict = dict()
def __init__(self):
super(TestZipReader, self).__init__()
@staticmethod
def build_file_dict(path):
file_dict = TestZipReader.file_dict
if path in file_dict:
return file_dict[path]
else:
file_handle = zipfile.ZipFile(path, 'r')
file_dict[path] = file_handle
return file_dict[path]
@staticmethod
def imread(path, idx):
zfile = TestZipReader.build_file_dict(path)
filelist = zfile.namelist()
filelist.sort()
data = zfile.read(filelist[idx])
file_bytes = np.asarray(bytearray(data), dtype=np.uint8)
im = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
im = Image.fromarray(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
# im = Image.open(io.BytesIO(data))
return im
# ###########################################################################
# Data augmentation
# ###########################################################################
def to_tensors():
return transforms.Compose([Stack(), ToTorchFormatTensor()])
class GroupRandomHorizontalFlowFlip(object):
"""Randomly horizontally flips the given PIL.Image with a probability of 0.5
"""
def __call__(self, img_group, flowF_group, flowB_group):
v = random.random()
if v < 0.5:
ret_img = [
img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group
]
ret_flowF = [ff[:, ::-1] * [-1.0, 1.0] for ff in flowF_group]
ret_flowB = [fb[:, ::-1] * [-1.0, 1.0] for fb in flowB_group]
return ret_img, ret_flowF, ret_flowB
else:
return img_group, flowF_group, flowB_group
class GroupRandomHorizontalFlip(object):
"""Randomly horizontally flips the given PIL.Image with a probability of 0.5
"""
def __call__(self, img_group, is_flow=False):
v = random.random()
if v < 0.5:
ret = [img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group]
if is_flow:
for i in range(0, len(ret), 2):
# invert flow pixel values when flipping
ret[i] = ImageOps.invert(ret[i])
return ret
else:
return img_group
class Stack(object):
def __init__(self, roll=False):
self.roll = roll
def __call__(self, img_group):
mode = img_group[0].mode
if mode == '1':
img_group = [img.convert('L') for img in img_group]
mode = 'L'
if mode == 'L':
return np.stack([np.expand_dims(x, 2) for x in img_group], axis=2)
elif mode == 'RGB':
if self.roll:
return np.stack([np.array(x)[:, :, ::-1] for x in img_group],
axis=2)
else:
return np.stack(img_group, axis=2)
else:
raise NotImplementedError(f"Image mode {mode}")
class ToTorchFormatTensor(object):
""" Converts a PIL.Image (RGB) or numpy.ndarray (H x W x C) in the range [0, 255]
to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0] """
def __init__(self, div=True):
self.div = div
def __call__(self, pic):
if isinstance(pic, np.ndarray):
# numpy img: [L, C, H, W]
img = torch.from_numpy(pic).permute(2, 3, 0, 1).contiguous()
else:
# handle PIL Image
img = torch.ByteTensor(torch.ByteStorage.from_buffer(
pic.tobytes()))
img = img.view(pic.size[1], pic.size[0], len(pic.mode))
# put it from HWC to CHW format
# yikes, this transpose takes 80% of the loading time/CPU
img = img.transpose(0, 1).transpose(0, 2).contiguous()
img = img.float().div(255) if self.div else img.float()
return img
# ###########################################################################
# Create masks with random shape
# ###########################################################################
def create_random_shape_with_random_motion(video_length,
imageHeight=240,
imageWidth=432):
# get a random shape
height = random.randint(imageHeight // 3, imageHeight - 1)
width = random.randint(imageWidth // 3, imageWidth - 1)
edge_num = random.randint(6, 8)
ratio = random.randint(6, 8) / 10
region = get_random_shape(edge_num=edge_num,
ratio=ratio,
height=height,
width=width)
region_width, region_height = region.size
# get random position
x, y = random.randint(0, imageHeight - region_height), random.randint(
0, imageWidth - region_width)
velocity = get_random_velocity(max_speed=3)
m = Image.fromarray(np.zeros((imageHeight, imageWidth)).astype(np.uint8))
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
masks = [m.convert('L')]
# return fixed masks
if random.uniform(0, 1) > 0.5:
return masks * video_length
# return moving masks
for _ in range(video_length - 1):
x, y, velocity = random_move_control_points(x,
y,
imageHeight,
imageWidth,
velocity,
region.size,
maxLineAcceleration=(3,
0.5),
maxInitSpeed=3)
m = Image.fromarray(
np.zeros((imageHeight, imageWidth)).astype(np.uint8))
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
masks.append(m.convert('L'))
return masks
def create_random_shape_with_random_motion_zoom_rotation(video_length, zoomin=0.9, zoomout=1.1, rotmin=1, rotmax=10, imageHeight=240, imageWidth=432):
# get a random shape
assert zoomin < 1, "Zoom-in parameter must be smaller than 1"
assert zoomout > 1, "Zoom-out parameter must be larger than 1"
assert rotmin < rotmax, "Minimum value of rotation must be smaller than maximun value !"
height = random.randint(imageHeight//3, imageHeight-1)
width = random.randint(imageWidth//3, imageWidth-1)
edge_num = random.randint(6, 8)
ratio = random.randint(6, 8)/10
region = get_random_shape(
edge_num=edge_num, ratio=ratio, height=height, width=width)
region_width, region_height = region.size
# get random position
x, y = random.randint(
0, imageHeight-region_height), random.randint(0, imageWidth-region_width)
velocity = get_random_velocity(max_speed=3)
m = Image.fromarray(np.zeros((imageHeight, imageWidth)).astype(np.uint8))
m.paste(region, (y, x, y+region.size[0], x+region.size[1]))
masks = [m.convert('L')]
# return fixed masks
if random.uniform(0, 1) > 0.5:
return masks*video_length # -> directly copy all the base masks
# return moving masks
for _ in range(video_length-1):
x, y, velocity = random_move_control_points(
x, y, imageHeight, imageWidth, velocity, region.size, maxLineAcceleration=(3, 0.5), maxInitSpeed=3)
m = Image.fromarray(
np.zeros((imageHeight, imageWidth)).astype(np.uint8))
### add by kaidong, to simulate zoon-in, zoom-out and rotation
extra_transform = random.uniform(0, 1)
# zoom in and zoom out
if extra_transform > 0.75:
resize_coefficient = random.uniform(zoomin, zoomout)
region = region.resize((math.ceil(region_width * resize_coefficient), math.ceil(region_height * resize_coefficient)), Image.NEAREST)
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
region_width, region_height = region.size
# rotation
elif extra_transform > 0.5:
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
m = m.rotate(random.randint(rotmin, rotmax))
# region_width, region_height = region.size
### end
else:
m.paste(region, (y, x, y+region.size[0], x+region.size[1]))
masks.append(m.convert('L'))
return masks
def get_random_shape(edge_num=9, ratio=0.7, width=432, height=240):
'''
There is the initial point and 3 points per cubic bezier curve.
Thus, the curve will only pass though n points, which will be the sharp edges.
The other 2 modify the shape of the bezier curve.
edge_num, Number of possibly sharp edges
points_num, number of points in the Path
ratio, (0, 1) magnitude of the perturbation from the unit circle,
'''
points_num = edge_num*3 + 1
angles = np.linspace(0, 2*np.pi, points_num)
codes = np.full(points_num, Path.CURVE4)
codes[0] = Path.MOVETO
# Using this instead of Path.CLOSEPOLY avoids an innecessary straight line
verts = np.stack((np.cos(angles), np.sin(angles))).T * \
(2*ratio*np.random.random(points_num)+1-ratio)[:, None]
verts[-1, :] = verts[0, :]
path = Path(verts, codes)
# draw paths into images
fig = plt.figure()
ax = fig.add_subplot(111)
patch = patches.PathPatch(path, facecolor='black', lw=2)
ax.add_patch(patch)
ax.set_xlim(np.min(verts)*1.1, np.max(verts)*1.1)
ax.set_ylim(np.min(verts)*1.1, np.max(verts)*1.1)
ax.axis('off') # removes the axis to leave only the shape
fig.canvas.draw()
# convert plt images into numpy images
data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
data = data.reshape((fig.canvas.get_width_height()[::-1] + (3,)))
plt.close(fig)
# postprocess
data = cv2.resize(data, (width, height))[:, :, 0]
data = (1 - np.array(data > 0).astype(np.uint8))*255
corrdinates = np.where(data > 0)
xmin, xmax, ymin, ymax = np.min(corrdinates[0]), np.max(
corrdinates[0]), np.min(corrdinates[1]), np.max(corrdinates[1])
region = Image.fromarray(data).crop((ymin, xmin, ymax, xmax))
return region
def random_accelerate(velocity, maxAcceleration, dist='uniform'):
speed, angle = velocity
d_speed, d_angle = maxAcceleration
if dist == 'uniform':
speed += np.random.uniform(-d_speed, d_speed)
angle += np.random.uniform(-d_angle, d_angle)
elif dist == 'guassian':
speed += np.random.normal(0, d_speed / 2)
angle += np.random.normal(0, d_angle / 2)
else:
raise NotImplementedError(
f'Distribution type {dist} is not supported.')
return (speed, angle)
def get_random_velocity(max_speed=3, dist='uniform'):
if dist == 'uniform':
speed = np.random.uniform(max_speed)
elif dist == 'guassian':
speed = np.abs(np.random.normal(0, max_speed / 2))
else:
raise NotImplementedError(
f'Distribution type {dist} is not supported.')
angle = np.random.uniform(0, 2 * np.pi)
return (speed, angle)
def random_move_control_points(X,
Y,
imageHeight,
imageWidth,
lineVelocity,
region_size,
maxLineAcceleration=(3, 0.5),
maxInitSpeed=3):
region_width, region_height = region_size
speed, angle = lineVelocity
X += int(speed * np.cos(angle))
Y += int(speed * np.sin(angle))
lineVelocity = random_accelerate(lineVelocity,
maxLineAcceleration,
dist='guassian')
if ((X > imageHeight - region_height) or (X < 0)
or (Y > imageWidth - region_width) or (Y < 0)):
lineVelocity = get_random_velocity(maxInitSpeed, dist='guassian')
new_X = np.clip(X, 0, imageHeight - region_height)
new_Y = np.clip(Y, 0, imageWidth - region_width)
return new_X, new_Y, lineVelocity
if __name__ == '__main__':
trials = 10
for _ in range(trials):
video_length = 10
# The returned masks are either stationary (50%) or moving (50%)
masks = create_random_shape_with_random_motion(video_length,
imageHeight=240,
imageWidth=432)
for m in masks:
cv2.imshow('mask', np.array(m))
cv2.waitKey(500)
-1
View File
@@ -1 +0,0 @@
@@ -1,256 +0,0 @@
import math
from typing import Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from .gaussian import gaussian_blur2d
from .kernels import get_canny_nms_kernel, get_hysteresis_kernel
from .sobel import spatial_gradient
def rgb_to_grayscale(image, rgb_weights = None):
if len(image.shape) < 3 or image.shape[-3] != 3:
raise ValueError(f"Input size must have a shape of (*, 3, H, W). Got {image.shape}")
if rgb_weights is None:
# 8 bit images
if image.dtype == torch.uint8:
rgb_weights = torch.tensor([76, 150, 29], device=image.device, dtype=torch.uint8)
# floating point images
elif image.dtype in (torch.float16, torch.float32, torch.float64):
rgb_weights = torch.tensor([0.299, 0.587, 0.114], device=image.device, dtype=image.dtype)
else:
raise TypeError(f"Unknown data type: {image.dtype}")
else:
# is tensor that we make sure is in the same device/dtype
rgb_weights = rgb_weights.to(image)
# unpack the color image channels with RGB order
r = image[..., 0:1, :, :]
g = image[..., 1:2, :, :]
b = image[..., 2:3, :, :]
w_r, w_g, w_b = rgb_weights.unbind()
return w_r * r + w_g * g + w_b * b
def canny(
input: torch.Tensor,
low_threshold: float = 0.1,
high_threshold: float = 0.2,
kernel_size: Tuple[int, int] = (5, 5),
sigma: Tuple[float, float] = (1, 1),
hysteresis: bool = True,
eps: float = 1e-6,
) -> Tuple[torch.Tensor, torch.Tensor]:
r"""Find edges of the input image and filters them using the Canny algorithm.
.. image:: _static/img/canny.png
Args:
input: input image tensor with shape :math:`(B,C,H,W)`.
low_threshold: lower threshold for the hysteresis procedure.
high_threshold: upper threshold for the hysteresis procedure.
kernel_size: the size of the kernel for the gaussian blur.
sigma: the standard deviation of the kernel for the gaussian blur.
hysteresis: if True, applies the hysteresis edge tracking.
Otherwise, the edges are divided between weak (0.5) and strong (1) edges.
eps: regularization number to avoid NaN during backprop.
Returns:
- the canny edge magnitudes map, shape of :math:`(B,1,H,W)`.
- the canny edge detection filtered by thresholds and hysteresis, shape of :math:`(B,1,H,W)`.
.. note::
See a working example `here <https://kornia-tutorials.readthedocs.io/en/latest/
canny.html>`__.
Example:
>>> input = torch.rand(5, 3, 4, 4)
>>> magnitude, edges = canny(input) # 5x3x4x4
>>> magnitude.shape
torch.Size([5, 1, 4, 4])
>>> edges.shape
torch.Size([5, 1, 4, 4])
"""
if not isinstance(input, torch.Tensor):
raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
if not len(input.shape) == 4:
raise ValueError(f"Invalid input shape, we expect BxCxHxW. Got: {input.shape}")
if low_threshold > high_threshold:
raise ValueError(
"Invalid input thresholds. low_threshold should be smaller than the high_threshold. Got: {}>{}".format(
low_threshold, high_threshold
)
)
if low_threshold < 0 and low_threshold > 1:
raise ValueError(f"Invalid input threshold. low_threshold should be in range (0,1). Got: {low_threshold}")
if high_threshold < 0 and high_threshold > 1:
raise ValueError(f"Invalid input threshold. high_threshold should be in range (0,1). Got: {high_threshold}")
device: torch.device = input.device
dtype: torch.dtype = input.dtype
# To Grayscale
if input.shape[1] == 3:
input = rgb_to_grayscale(input)
# Gaussian filter
blurred: torch.Tensor = gaussian_blur2d(input, kernel_size, sigma)
# Compute the gradients
gradients: torch.Tensor = spatial_gradient(blurred, normalized=False)
# Unpack the edges
gx: torch.Tensor = gradients[:, :, 0]
gy: torch.Tensor = gradients[:, :, 1]
# Compute gradient magnitude and angle
magnitude: torch.Tensor = torch.sqrt(gx * gx + gy * gy + eps)
angle: torch.Tensor = torch.atan2(gy, gx)
# Radians to Degrees
angle = 180.0 * angle / math.pi
# Round angle to the nearest 45 degree
angle = torch.round(angle / 45) * 45
# Non-maximal suppression
nms_kernels: torch.Tensor = get_canny_nms_kernel(device, dtype)
nms_magnitude: torch.Tensor = F.conv2d(magnitude, nms_kernels, padding=nms_kernels.shape[-1] // 2)
# Get the indices for both directions
positive_idx: torch.Tensor = (angle / 45) % 8
positive_idx = positive_idx.long()
negative_idx: torch.Tensor = ((angle / 45) + 4) % 8
negative_idx = negative_idx.long()
# Apply the non-maximum suppression to the different directions
channel_select_filtered_positive: torch.Tensor = torch.gather(nms_magnitude, 1, positive_idx)
channel_select_filtered_negative: torch.Tensor = torch.gather(nms_magnitude, 1, negative_idx)
channel_select_filtered: torch.Tensor = torch.stack(
[channel_select_filtered_positive, channel_select_filtered_negative], 1
)
is_max: torch.Tensor = channel_select_filtered.min(dim=1)[0] > 0.0
magnitude = magnitude * is_max
# Threshold
edges: torch.Tensor = F.threshold(magnitude, low_threshold, 0.0)
low: torch.Tensor = magnitude > low_threshold
high: torch.Tensor = magnitude > high_threshold
edges = low * 0.5 + high * 0.5
edges = edges.to(dtype)
# Hysteresis
if hysteresis:
edges_old: torch.Tensor = -torch.ones(edges.shape, device=edges.device, dtype=dtype)
hysteresis_kernels: torch.Tensor = get_hysteresis_kernel(device, dtype)
while ((edges_old - edges).abs() != 0).any():
weak: torch.Tensor = (edges == 0.5).float()
strong: torch.Tensor = (edges == 1).float()
hysteresis_magnitude: torch.Tensor = F.conv2d(
edges, hysteresis_kernels, padding=hysteresis_kernels.shape[-1] // 2
)
hysteresis_magnitude = (hysteresis_magnitude == 1).any(1, keepdim=True).to(dtype)
hysteresis_magnitude = hysteresis_magnitude * weak + strong
edges_old = edges.clone()
edges = hysteresis_magnitude + (hysteresis_magnitude == 0) * weak * 0.5
edges = hysteresis_magnitude
return magnitude, edges
class Canny(nn.Module):
r"""Module that finds edges of the input image and filters them using the Canny algorithm.
Args:
input: input image tensor with shape :math:`(B,C,H,W)`.
low_threshold: lower threshold for the hysteresis procedure.
high_threshold: upper threshold for the hysteresis procedure.
kernel_size: the size of the kernel for the gaussian blur.
sigma: the standard deviation of the kernel for the gaussian blur.
hysteresis: if True, applies the hysteresis edge tracking.
Otherwise, the edges are divided between weak (0.5) and strong (1) edges.
eps: regularization number to avoid NaN during backprop.
Returns:
- the canny edge magnitudes map, shape of :math:`(B,1,H,W)`.
- the canny edge detection filtered by thresholds and hysteresis, shape of :math:`(B,1,H,W)`.
Example:
>>> input = torch.rand(5, 3, 4, 4)
>>> magnitude, edges = Canny()(input) # 5x3x4x4
>>> magnitude.shape
torch.Size([5, 1, 4, 4])
>>> edges.shape
torch.Size([5, 1, 4, 4])
"""
def __init__(
self,
low_threshold: float = 0.1,
high_threshold: float = 0.2,
kernel_size: Tuple[int, int] = (5, 5),
sigma: Tuple[float, float] = (1, 1),
hysteresis: bool = True,
eps: float = 1e-6,
) -> None:
super().__init__()
if low_threshold > high_threshold:
raise ValueError(
"Invalid input thresholds. low_threshold should be\
smaller than the high_threshold. Got: {}>{}".format(
low_threshold, high_threshold
)
)
if low_threshold < 0 or low_threshold > 1:
raise ValueError(f"Invalid input threshold. low_threshold should be in range (0,1). Got: {low_threshold}")
if high_threshold < 0 or high_threshold > 1:
raise ValueError(f"Invalid input threshold. high_threshold should be in range (0,1). Got: {high_threshold}")
# Gaussian blur parameters
self.kernel_size = kernel_size
self.sigma = sigma
# Double threshold
self.low_threshold = low_threshold
self.high_threshold = high_threshold
# Hysteresis
self.hysteresis = hysteresis
self.eps: float = eps
def __repr__(self) -> str:
return ''.join(
(
f'{type(self).__name__}(',
', '.join(
f'{name}={getattr(self, name)}' for name in sorted(self.__dict__) if not name.startswith('_')
),
')',
)
)
def forward(self, input: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
return canny(
input, self.low_threshold, self.high_threshold, self.kernel_size, self.sigma, self.hysteresis, self.eps
)
-288
View File
@@ -1,288 +0,0 @@
from typing import List
import torch
import torch.nn.functional as F
from .kernels import normalize_kernel2d
def _compute_padding(kernel_size: List[int]) -> List[int]:
"""Compute padding tuple."""
# 4 or 6 ints: (padding_left, padding_right,padding_top,padding_bottom)
# https://pytorch.org/docs/stable/nn.html#torch.nn.functional.pad
if len(kernel_size) < 2:
raise AssertionError(kernel_size)
computed = [k - 1 for k in kernel_size]
# for even kernels we need to do asymmetric padding :(
out_padding = 2 * len(kernel_size) * [0]
for i in range(len(kernel_size)):
computed_tmp = computed[-(i + 1)]
pad_front = computed_tmp // 2
pad_rear = computed_tmp - pad_front
out_padding[2 * i + 0] = pad_front
out_padding[2 * i + 1] = pad_rear
return out_padding
def filter2d(
input: torch.Tensor,
kernel: torch.Tensor,
border_type: str = 'reflect',
normalized: bool = False,
padding: str = 'same',
) -> torch.Tensor:
r"""Convolve a tensor with a 2d kernel.
The function applies a given kernel to a tensor. The kernel is applied
independently at each depth channel of the tensor. Before applying the
kernel, the function applies padding according to the specified mode so
that the output remains in the same shape.
Args:
input: the input tensor with shape of
:math:`(B, C, H, W)`.
kernel: the kernel to be convolved with the input
tensor. The kernel shape must be :math:`(1, kH, kW)` or :math:`(B, kH, kW)`.
border_type: the padding mode to be applied before convolving.
The expected modes are: ``'constant'``, ``'reflect'``,
``'replicate'`` or ``'circular'``.
normalized: If True, kernel will be L1 normalized.
padding: This defines the type of padding.
2 modes available ``'same'`` or ``'valid'``.
Return:
torch.Tensor: the convolved tensor of same size and numbers of channels
as the input with shape :math:`(B, C, H, W)`.
Example:
>>> input = torch.tensor([[[
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 5., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],]]])
>>> kernel = torch.ones(1, 3, 3)
>>> filter2d(input, kernel, padding='same')
tensor([[[[0., 0., 0., 0., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 0., 0., 0., 0.]]]])
"""
if not isinstance(input, torch.Tensor):
raise TypeError(f"Input input is not torch.Tensor. Got {type(input)}")
if not isinstance(kernel, torch.Tensor):
raise TypeError(f"Input kernel is not torch.Tensor. Got {type(kernel)}")
if not isinstance(border_type, str):
raise TypeError(f"Input border_type is not string. Got {type(border_type)}")
if border_type not in ['constant', 'reflect', 'replicate', 'circular']:
raise ValueError(
f"Invalid border type, we expect 'constant', \
'reflect', 'replicate', 'circular'. Got:{border_type}"
)
if not isinstance(padding, str):
raise TypeError(f"Input padding is not string. Got {type(padding)}")
if padding not in ['valid', 'same']:
raise ValueError(f"Invalid padding mode, we expect 'valid' or 'same'. Got: {padding}")
if not len(input.shape) == 4:
raise ValueError(f"Invalid input shape, we expect BxCxHxW. Got: {input.shape}")
if (not len(kernel.shape) == 3) and not ((kernel.shape[0] == 0) or (kernel.shape[0] == input.shape[0])):
raise ValueError(f"Invalid kernel shape, we expect 1xHxW or BxHxW. Got: {kernel.shape}")
# prepare kernel
b, c, h, w = input.shape
tmp_kernel: torch.Tensor = kernel.unsqueeze(1).to(input)
if normalized:
tmp_kernel = normalize_kernel2d(tmp_kernel)
tmp_kernel = tmp_kernel.expand(-1, c, -1, -1)
height, width = tmp_kernel.shape[-2:]
# pad the input tensor
if padding == 'same':
padding_shape: List[int] = _compute_padding([height, width])
input = F.pad(input, padding_shape, mode=border_type)
# kernel and input tensor reshape to align element-wise or batch-wise params
tmp_kernel = tmp_kernel.reshape(-1, 1, height, width)
input = input.view(-1, tmp_kernel.size(0), input.size(-2), input.size(-1))
# convolve the tensor with the kernel.
output = F.conv2d(input, tmp_kernel, groups=tmp_kernel.size(0), padding=0, stride=1)
if padding == 'same':
out = output.view(b, c, h, w)
else:
out = output.view(b, c, h - height + 1, w - width + 1)
return out
def filter2d_separable(
input: torch.Tensor,
kernel_x: torch.Tensor,
kernel_y: torch.Tensor,
border_type: str = 'reflect',
normalized: bool = False,
padding: str = 'same',
) -> torch.Tensor:
r"""Convolve a tensor with two 1d kernels, in x and y directions.
The function applies a given kernel to a tensor. The kernel is applied
independently at each depth channel of the tensor. Before applying the
kernel, the function applies padding according to the specified mode so
that the output remains in the same shape.
Args:
input: the input tensor with shape of
:math:`(B, C, H, W)`.
kernel_x: the kernel to be convolved with the input
tensor. The kernel shape must be :math:`(1, kW)` or :math:`(B, kW)`.
kernel_y: the kernel to be convolved with the input
tensor. The kernel shape must be :math:`(1, kH)` or :math:`(B, kH)`.
border_type: the padding mode to be applied before convolving.
The expected modes are: ``'constant'``, ``'reflect'``,
``'replicate'`` or ``'circular'``.
normalized: If True, kernel will be L1 normalized.
padding: This defines the type of padding.
2 modes available ``'same'`` or ``'valid'``.
Return:
torch.Tensor: the convolved tensor of same size and numbers of channels
as the input with shape :math:`(B, C, H, W)`.
Example:
>>> input = torch.tensor([[[
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 5., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],]]])
>>> kernel = torch.ones(1, 3)
>>> filter2d_separable(input, kernel, kernel, padding='same')
tensor([[[[0., 0., 0., 0., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 0., 0., 0., 0.]]]])
"""
out_x = filter2d(input, kernel_x.unsqueeze(0), border_type, normalized, padding)
out = filter2d(out_x, kernel_y.unsqueeze(-1), border_type, normalized, padding)
return out
def filter3d(
input: torch.Tensor, kernel: torch.Tensor, border_type: str = 'replicate', normalized: bool = False
) -> torch.Tensor:
r"""Convolve a tensor with a 3d kernel.
The function applies a given kernel to a tensor. The kernel is applied
independently at each depth channel of the tensor. Before applying the
kernel, the function applies padding according to the specified mode so
that the output remains in the same shape.
Args:
input: the input tensor with shape of
:math:`(B, C, D, H, W)`.
kernel: the kernel to be convolved with the input
tensor. The kernel shape must be :math:`(1, kD, kH, kW)` or :math:`(B, kD, kH, kW)`.
border_type: the padding mode to be applied before convolving.
The expected modes are: ``'constant'``,
``'replicate'`` or ``'circular'``.
normalized: If True, kernel will be L1 normalized.
Return:
the convolved tensor of same size and numbers of channels
as the input with shape :math:`(B, C, D, H, W)`.
Example:
>>> input = torch.tensor([[[
... [[0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.]],
... [[0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 5., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.]],
... [[0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.],
... [0., 0., 0., 0., 0.]]
... ]]])
>>> kernel = torch.ones(1, 3, 3, 3)
>>> filter3d(input, kernel)
tensor([[[[[0., 0., 0., 0., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 0., 0., 0., 0.]],
<BLANKLINE>
[[0., 0., 0., 0., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 0., 0., 0., 0.]],
<BLANKLINE>
[[0., 0., 0., 0., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 5., 5., 5., 0.],
[0., 0., 0., 0., 0.]]]]])
"""
if not isinstance(input, torch.Tensor):
raise TypeError(f"Input border_type is not torch.Tensor. Got {type(input)}")
if not isinstance(kernel, torch.Tensor):
raise TypeError(f"Input border_type is not torch.Tensor. Got {type(kernel)}")
if not isinstance(border_type, str):
raise TypeError(f"Input border_type is not string. Got {type(kernel)}")
if not len(input.shape) == 5:
raise ValueError(f"Invalid input shape, we expect BxCxDxHxW. Got: {input.shape}")
if not len(kernel.shape) == 4 and kernel.shape[0] != 1:
raise ValueError(f"Invalid kernel shape, we expect 1xDxHxW. Got: {kernel.shape}")
# prepare kernel
b, c, d, h, w = input.shape
tmp_kernel: torch.Tensor = kernel.unsqueeze(1).to(input)
if normalized:
bk, dk, hk, wk = kernel.shape
tmp_kernel = normalize_kernel2d(tmp_kernel.view(bk, dk, hk * wk)).view_as(tmp_kernel)
tmp_kernel = tmp_kernel.expand(-1, c, -1, -1, -1)
# pad the input tensor
depth, height, width = tmp_kernel.shape[-3:]
padding_shape: List[int] = _compute_padding([depth, height, width])
input_pad: torch.Tensor = F.pad(input, padding_shape, mode=border_type)
# kernel and input tensor reshape to align element-wise or batch-wise params
tmp_kernel = tmp_kernel.reshape(-1, 1, depth, height, width)
input_pad = input_pad.view(-1, tmp_kernel.size(0), input_pad.size(-3), input_pad.size(-2), input_pad.size(-1))
# convolve the tensor with the kernel.
output = F.conv3d(input_pad, tmp_kernel, groups=tmp_kernel.size(0), padding=0, stride=1)
return output.view(b, c, d, h, w)
@@ -1,116 +0,0 @@
from typing import Tuple
import torch
import torch.nn as nn
from .filter import filter2d, filter2d_separable
from .kernels import get_gaussian_kernel1d, get_gaussian_kernel2d
def gaussian_blur2d(
input: torch.Tensor,
kernel_size: Tuple[int, int],
sigma: Tuple[float, float],
border_type: str = 'reflect',
separable: bool = True,
) -> torch.Tensor:
r"""Create an operator that blurs a tensor using a Gaussian filter.
.. image:: _static/img/gaussian_blur2d.png
The operator smooths the given tensor with a gaussian kernel by convolving
it to each channel. It supports batched operation.
Arguments:
input: the input tensor with shape :math:`(B,C,H,W)`.
kernel_size: the size of the kernel.
sigma: the standard deviation of the kernel.
border_type: the padding mode to be applied before convolving.
The expected modes are: ``'constant'``, ``'reflect'``,
``'replicate'`` or ``'circular'``. Default: ``'reflect'``.
separable: run as composition of two 1d-convolutions.
Returns:
the blurred tensor with shape :math:`(B, C, H, W)`.
.. note::
See a working example `here <https://kornia-tutorials.readthedocs.io/en/latest/
gaussian_blur.html>`__.
Examples:
>>> input = torch.rand(2, 4, 5, 5)
>>> output = gaussian_blur2d(input, (3, 3), (1.5, 1.5))
>>> output.shape
torch.Size([2, 4, 5, 5])
"""
if separable:
kernel_x: torch.Tensor = get_gaussian_kernel1d(kernel_size[1], sigma[1])
kernel_y: torch.Tensor = get_gaussian_kernel1d(kernel_size[0], sigma[0])
out = filter2d_separable(input, kernel_x[None], kernel_y[None], border_type)
else:
kernel: torch.Tensor = get_gaussian_kernel2d(kernel_size, sigma)
out = filter2d(input, kernel[None], border_type)
return out
class GaussianBlur2d(nn.Module):
r"""Create an operator that blurs a tensor using a Gaussian filter.
The operator smooths the given tensor with a gaussian kernel by convolving
it to each channel. It supports batched operation.
Arguments:
kernel_size: the size of the kernel.
sigma: the standard deviation of the kernel.
border_type: the padding mode to be applied before convolving.
The expected modes are: ``'constant'``, ``'reflect'``,
``'replicate'`` or ``'circular'``. Default: ``'reflect'``.
separable: run as composition of two 1d-convolutions.
Returns:
the blurred tensor.
Shape:
- Input: :math:`(B, C, H, W)`
- Output: :math:`(B, C, H, W)`
Examples::
>>> input = torch.rand(2, 4, 5, 5)
>>> gauss = GaussianBlur2d((3, 3), (1.5, 1.5))
>>> output = gauss(input) # 2x4x5x5
>>> output.shape
torch.Size([2, 4, 5, 5])
"""
def __init__(
self,
kernel_size: Tuple[int, int],
sigma: Tuple[float, float],
border_type: str = 'reflect',
separable: bool = True,
) -> None:
super().__init__()
self.kernel_size: Tuple[int, int] = kernel_size
self.sigma: Tuple[float, float] = sigma
self.border_type = border_type
self.separable = separable
def __repr__(self) -> str:
return (
self.__class__.__name__
+ '(kernel_size='
+ str(self.kernel_size)
+ ', '
+ 'sigma='
+ str(self.sigma)
+ ', '
+ 'border_type='
+ self.border_type
+ 'separable='
+ str(self.separable)
+ ')'
)
def forward(self, input: torch.Tensor) -> torch.Tensor:
return gaussian_blur2d(input, self.kernel_size, self.sigma, self.border_type, self.separable)
@@ -1,690 +0,0 @@
import math
from math import sqrt
from typing import List, Optional, Tuple
import torch
def normalize_kernel2d(input: torch.Tensor) -> torch.Tensor:
r"""Normalize both derivative and smoothing kernel."""
if len(input.size()) < 2:
raise TypeError(f"input should be at least 2D tensor. Got {input.size()}")
norm: torch.Tensor = input.abs().sum(dim=-1).sum(dim=-1)
return input / (norm.unsqueeze(-1).unsqueeze(-1))
def gaussian(window_size: int, sigma: float) -> torch.Tensor:
device, dtype = None, None
if isinstance(sigma, torch.Tensor):
device, dtype = sigma.device, sigma.dtype
x = torch.arange(window_size, device=device, dtype=dtype) - window_size // 2
if window_size % 2 == 0:
x = x + 0.5
gauss = torch.exp((-x.pow(2.0) / (2 * sigma**2)).float())
return gauss / gauss.sum()
def gaussian_discrete_erf(window_size: int, sigma) -> torch.Tensor:
r"""Discrete Gaussian by interpolating the error function.
Adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py
"""
device = sigma.device if isinstance(sigma, torch.Tensor) else None
sigma = torch.as_tensor(sigma, dtype=torch.float, device=device)
x = torch.arange(window_size).float() - window_size // 2
t = 0.70710678 / torch.abs(sigma)
gauss = 0.5 * ((t * (x + 0.5)).erf() - (t * (x - 0.5)).erf())
gauss = gauss.clamp(min=0)
return gauss / gauss.sum()
def _modified_bessel_0(x: torch.Tensor) -> torch.Tensor:
r"""Adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py
"""
if torch.abs(x) < 3.75:
y = (x / 3.75) * (x / 3.75)
return 1.0 + y * (
3.5156229 + y * (3.0899424 + y * (1.2067492 + y * (0.2659732 + y * (0.360768e-1 + y * 0.45813e-2))))
)
ax = torch.abs(x)
y = 3.75 / ax
ans = 0.916281e-2 + y * (-0.2057706e-1 + y * (0.2635537e-1 + y * (-0.1647633e-1 + y * 0.392377e-2)))
coef = 0.39894228 + y * (0.1328592e-1 + y * (0.225319e-2 + y * (-0.157565e-2 + y * ans)))
return (torch.exp(ax) / torch.sqrt(ax)) * coef
def _modified_bessel_1(x: torch.Tensor) -> torch.Tensor:
r"""adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py
"""
if torch.abs(x) < 3.75:
y = (x / 3.75) * (x / 3.75)
ans = 0.51498869 + y * (0.15084934 + y * (0.2658733e-1 + y * (0.301532e-2 + y * 0.32411e-3)))
return torch.abs(x) * (0.5 + y * (0.87890594 + y * ans))
ax = torch.abs(x)
y = 3.75 / ax
ans = 0.2282967e-1 + y * (-0.2895312e-1 + y * (0.1787654e-1 - y * 0.420059e-2))
ans = 0.39894228 + y * (-0.3988024e-1 + y * (-0.362018e-2 + y * (0.163801e-2 + y * (-0.1031555e-1 + y * ans))))
ans = ans * torch.exp(ax) / torch.sqrt(ax)
return -ans if x < 0.0 else ans
def _modified_bessel_i(n: int, x: torch.Tensor) -> torch.Tensor:
r"""adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py
"""
if n < 2:
raise ValueError("n must be greater than 1.")
if x == 0.0:
return x
device = x.device
tox = 2.0 / torch.abs(x)
ans = torch.tensor(0.0, device=device)
bip = torch.tensor(0.0, device=device)
bi = torch.tensor(1.0, device=device)
m = int(2 * (n + int(sqrt(40.0 * n))))
for j in range(m, 0, -1):
bim = bip + float(j) * tox * bi
bip = bi
bi = bim
if abs(bi) > 1.0e10:
ans = ans * 1.0e-10
bi = bi * 1.0e-10
bip = bip * 1.0e-10
if j == n:
ans = bip
ans = ans * _modified_bessel_0(x) / bi
return -ans if x < 0.0 and (n % 2) == 1 else ans
def gaussian_discrete(window_size, sigma) -> torch.Tensor:
r"""Discrete Gaussian kernel based on the modified Bessel functions.
Adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py
"""
device = sigma.device if isinstance(sigma, torch.Tensor) else None
sigma = torch.as_tensor(sigma, dtype=torch.float, device=device)
sigma2 = sigma * sigma
tail = int(window_size // 2)
out_pos: List[Optional[torch.Tensor]] = [None] * (tail + 1)
out_pos[0] = _modified_bessel_0(sigma2)
out_pos[1] = _modified_bessel_1(sigma2)
for k in range(2, len(out_pos)):
out_pos[k] = _modified_bessel_i(k, sigma2)
out = out_pos[:0:-1]
out.extend(out_pos)
out = torch.stack(out) * torch.exp(sigma2) # type: ignore
return out / out.sum() # type: ignore
def laplacian_1d(window_size) -> torch.Tensor:
r"""One could also use the Laplacian of Gaussian formula to design the filter."""
filter_1d = torch.ones(window_size)
filter_1d[window_size // 2] = 1 - window_size
laplacian_1d: torch.Tensor = filter_1d
return laplacian_1d
def get_box_kernel2d(kernel_size: Tuple[int, int]) -> torch.Tensor:
r"""Utility function that returns a box filter."""
kx: float = float(kernel_size[0])
ky: float = float(kernel_size[1])
scale: torch.Tensor = torch.tensor(1.0) / torch.tensor([kx * ky])
tmp_kernel: torch.Tensor = torch.ones(1, kernel_size[0], kernel_size[1])
return scale.to(tmp_kernel.dtype) * tmp_kernel
def get_binary_kernel2d(window_size: Tuple[int, int]) -> torch.Tensor:
r"""Create a binary kernel to extract the patches.
If the window size is HxW will create a (H*W)xHxW kernel.
"""
window_range: int = window_size[0] * window_size[1]
kernel: torch.Tensor = torch.zeros(window_range, window_range)
for i in range(window_range):
kernel[i, i] += 1.0
return kernel.view(window_range, 1, window_size[0], window_size[1])
def get_sobel_kernel_3x3() -> torch.Tensor:
"""Utility function that returns a sobel kernel of 3x3."""
return torch.tensor([[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]])
def get_sobel_kernel_5x5_2nd_order() -> torch.Tensor:
"""Utility function that returns a 2nd order sobel kernel of 5x5."""
return torch.tensor(
[
[-1.0, 0.0, 2.0, 0.0, -1.0],
[-4.0, 0.0, 8.0, 0.0, -4.0],
[-6.0, 0.0, 12.0, 0.0, -6.0],
[-4.0, 0.0, 8.0, 0.0, -4.0],
[-1.0, 0.0, 2.0, 0.0, -1.0],
]
)
def _get_sobel_kernel_5x5_2nd_order_xy() -> torch.Tensor:
"""Utility function that returns a 2nd order sobel kernel of 5x5."""
return torch.tensor(
[
[-1.0, -2.0, 0.0, 2.0, 1.0],
[-2.0, -4.0, 0.0, 4.0, 2.0],
[0.0, 0.0, 0.0, 0.0, 0.0],
[2.0, 4.0, 0.0, -4.0, -2.0],
[1.0, 2.0, 0.0, -2.0, -1.0],
]
)
def get_diff_kernel_3x3() -> torch.Tensor:
"""Utility function that returns a first order derivative kernel of 3x3."""
return torch.tensor([[-0.0, 0.0, 0.0], [-1.0, 0.0, 1.0], [-0.0, 0.0, 0.0]])
def get_diff_kernel3d(device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
"""Utility function that returns a first order derivative kernel of 3x3x3."""
kernel: torch.Tensor = torch.tensor(
[
[
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [-0.5, 0.0, 0.5], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
],
[
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, -0.5, 0.0], [0.0, 0.0, 0.0], [0.0, 0.5, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
],
[
[[0.0, 0.0, 0.0], [0.0, -0.5, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 0.0]],
],
],
device=device,
dtype=dtype,
)
return kernel.unsqueeze(1)
def get_diff_kernel3d_2nd_order(device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
"""Utility function that returns a first order derivative kernel of 3x3x3."""
kernel: torch.Tensor = torch.tensor(
[
[
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [1.0, -2.0, 1.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
],
[
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 1.0, 0.0], [0.0, -2.0, 0.0], [0.0, 1.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
],
[
[[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, -2.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
],
[
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[1.0, 0.0, -1.0], [0.0, 0.0, 0.0], [-1.0, 0.0, 1.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
],
[
[[0.0, 1.0, 0.0], [0.0, 0.0, 0.0], [0.0, -1.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, -1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
],
[
[[0.0, 0.0, 0.0], [1.0, 0.0, -1.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [-1.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
],
],
device=device,
dtype=dtype,
)
return kernel.unsqueeze(1)
def get_sobel_kernel2d() -> torch.Tensor:
kernel_x: torch.Tensor = get_sobel_kernel_3x3()
kernel_y: torch.Tensor = kernel_x.transpose(0, 1)
return torch.stack([kernel_x, kernel_y])
def get_diff_kernel2d() -> torch.Tensor:
kernel_x: torch.Tensor = get_diff_kernel_3x3()
kernel_y: torch.Tensor = kernel_x.transpose(0, 1)
return torch.stack([kernel_x, kernel_y])
def get_sobel_kernel2d_2nd_order() -> torch.Tensor:
gxx: torch.Tensor = get_sobel_kernel_5x5_2nd_order()
gyy: torch.Tensor = gxx.transpose(0, 1)
gxy: torch.Tensor = _get_sobel_kernel_5x5_2nd_order_xy()
return torch.stack([gxx, gxy, gyy])
def get_diff_kernel2d_2nd_order() -> torch.Tensor:
gxx: torch.Tensor = torch.tensor([[0.0, 0.0, 0.0], [1.0, -2.0, 1.0], [0.0, 0.0, 0.0]])
gyy: torch.Tensor = gxx.transpose(0, 1)
gxy: torch.Tensor = torch.tensor([[-1.0, 0.0, 1.0], [0.0, 0.0, 0.0], [1.0, 0.0, -1.0]])
return torch.stack([gxx, gxy, gyy])
def get_spatial_gradient_kernel2d(mode: str, order: int) -> torch.Tensor:
r"""Function that returns kernel for 1st or 2nd order image gradients, using one of the following operators:
sobel, diff.
"""
if mode not in ['sobel', 'diff']:
raise TypeError(
"mode should be either sobel\
or diff. Got {}".format(
mode
)
)
if order not in [1, 2]:
raise TypeError(
"order should be either 1 or 2\
Got {}".format(
order
)
)
if mode == 'sobel' and order == 1:
kernel: torch.Tensor = get_sobel_kernel2d()
elif mode == 'sobel' and order == 2:
kernel = get_sobel_kernel2d_2nd_order()
elif mode == 'diff' and order == 1:
kernel = get_diff_kernel2d()
elif mode == 'diff' and order == 2:
kernel = get_diff_kernel2d_2nd_order()
else:
raise NotImplementedError("")
return kernel
def get_spatial_gradient_kernel3d(mode: str, order: int, device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
r"""Function that returns kernel for 1st or 2nd order scale pyramid gradients, using one of the following
operators: sobel, diff."""
if mode not in ['sobel', 'diff']:
raise TypeError(
"mode should be either sobel\
or diff. Got {}".format(
mode
)
)
if order not in [1, 2]:
raise TypeError(
"order should be either 1 or 2\
Got {}".format(
order
)
)
if mode == 'sobel':
raise NotImplementedError("Sobel kernel for 3d gradient is not implemented yet")
if mode == 'diff' and order == 1:
kernel = get_diff_kernel3d(device, dtype)
elif mode == 'diff' and order == 2:
kernel = get_diff_kernel3d_2nd_order(device, dtype)
else:
raise NotImplementedError("")
return kernel
def get_gaussian_kernel1d(kernel_size: int, sigma: float, force_even: bool = False) -> torch.Tensor:
r"""Function that returns Gaussian filter coefficients.
Args:
kernel_size: filter size. It should be odd and positive.
sigma: gaussian standard deviation.
force_even: overrides requirement for odd kernel size.
Returns:
1D tensor with gaussian filter coefficients.
Shape:
- Output: :math:`(\text{kernel_size})`
Examples:
>>> get_gaussian_kernel1d(3, 2.5)
tensor([0.3243, 0.3513, 0.3243])
>>> get_gaussian_kernel1d(5, 1.5)
tensor([0.1201, 0.2339, 0.2921, 0.2339, 0.1201])
"""
if not isinstance(kernel_size, int) or ((kernel_size % 2 == 0) and not force_even) or (kernel_size <= 0):
raise TypeError("kernel_size must be an odd positive integer. " "Got {}".format(kernel_size))
window_1d: torch.Tensor = gaussian(kernel_size, sigma)
return window_1d
def get_gaussian_discrete_kernel1d(kernel_size: int, sigma: float, force_even: bool = False) -> torch.Tensor:
r"""Function that returns Gaussian filter coefficients based on the modified Bessel functions. Adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py.
Args:
kernel_size: filter size. It should be odd and positive.
sigma: gaussian standard deviation.
force_even: overrides requirement for odd kernel size.
Returns:
1D tensor with gaussian filter coefficients.
Shape:
- Output: :math:`(\text{kernel_size})`
Examples:
>>> get_gaussian_discrete_kernel1d(3, 2.5)
tensor([0.3235, 0.3531, 0.3235])
>>> get_gaussian_discrete_kernel1d(5, 1.5)
tensor([0.1096, 0.2323, 0.3161, 0.2323, 0.1096])
"""
if not isinstance(kernel_size, int) or ((kernel_size % 2 == 0) and not force_even) or (kernel_size <= 0):
raise TypeError("kernel_size must be an odd positive integer. " "Got {}".format(kernel_size))
window_1d = gaussian_discrete(kernel_size, sigma)
return window_1d
def get_gaussian_erf_kernel1d(kernel_size: int, sigma: float, force_even: bool = False) -> torch.Tensor:
r"""Function that returns Gaussian filter coefficients by interpolating the error function, adapted from:
https://github.com/Project-MONAI/MONAI/blob/master/monai/networks/layers/convutils.py.
Args:
kernel_size: filter size. It should be odd and positive.
sigma: gaussian standard deviation.
force_even: overrides requirement for odd kernel size.
Returns:
1D tensor with gaussian filter coefficients.
Shape:
- Output: :math:`(\text{kernel_size})`
Examples:
>>> get_gaussian_erf_kernel1d(3, 2.5)
tensor([0.3245, 0.3511, 0.3245])
>>> get_gaussian_erf_kernel1d(5, 1.5)
tensor([0.1226, 0.2331, 0.2887, 0.2331, 0.1226])
"""
if not isinstance(kernel_size, int) or ((kernel_size % 2 == 0) and not force_even) or (kernel_size <= 0):
raise TypeError("kernel_size must be an odd positive integer. " "Got {}".format(kernel_size))
window_1d = gaussian_discrete_erf(kernel_size, sigma)
return window_1d
def get_gaussian_kernel2d(
kernel_size: Tuple[int, int], sigma: Tuple[float, float], force_even: bool = False
) -> torch.Tensor:
r"""Function that returns Gaussian filter matrix coefficients.
Args:
kernel_size: filter sizes in the x and y direction.
Sizes should be odd and positive.
sigma: gaussian standard deviation in the x and y
direction.
force_even: overrides requirement for odd kernel size.
Returns:
2D tensor with gaussian filter matrix coefficients.
Shape:
- Output: :math:`(\text{kernel_size}_x, \text{kernel_size}_y)`
Examples:
>>> get_gaussian_kernel2d((3, 3), (1.5, 1.5))
tensor([[0.0947, 0.1183, 0.0947],
[0.1183, 0.1478, 0.1183],
[0.0947, 0.1183, 0.0947]])
>>> get_gaussian_kernel2d((3, 5), (1.5, 1.5))
tensor([[0.0370, 0.0720, 0.0899, 0.0720, 0.0370],
[0.0462, 0.0899, 0.1123, 0.0899, 0.0462],
[0.0370, 0.0720, 0.0899, 0.0720, 0.0370]])
"""
if not isinstance(kernel_size, tuple) or len(kernel_size) != 2:
raise TypeError(f"kernel_size must be a tuple of length two. Got {kernel_size}")
if not isinstance(sigma, tuple) or len(sigma) != 2:
raise TypeError(f"sigma must be a tuple of length two. Got {sigma}")
ksize_x, ksize_y = kernel_size
sigma_x, sigma_y = sigma
kernel_x: torch.Tensor = get_gaussian_kernel1d(ksize_x, sigma_x, force_even)
kernel_y: torch.Tensor = get_gaussian_kernel1d(ksize_y, sigma_y, force_even)
kernel_2d: torch.Tensor = torch.matmul(kernel_x.unsqueeze(-1), kernel_y.unsqueeze(-1).t())
return kernel_2d
def get_laplacian_kernel1d(kernel_size: int) -> torch.Tensor:
r"""Function that returns the coefficients of a 1D Laplacian filter.
Args:
kernel_size: filter size. It should be odd and positive.
Returns:
1D tensor with laplacian filter coefficients.
Shape:
- Output: math:`(\text{kernel_size})`
Examples:
>>> get_laplacian_kernel1d(3)
tensor([ 1., -2., 1.])
>>> get_laplacian_kernel1d(5)
tensor([ 1., 1., -4., 1., 1.])
"""
if not isinstance(kernel_size, int) or kernel_size % 2 == 0 or kernel_size <= 0:
raise TypeError(f"ksize must be an odd positive integer. Got {kernel_size}")
window_1d: torch.Tensor = laplacian_1d(kernel_size)
return window_1d
def get_laplacian_kernel2d(kernel_size: int) -> torch.Tensor:
r"""Function that returns Gaussian filter matrix coefficients.
Args:
kernel_size: filter size should be odd.
Returns:
2D tensor with laplacian filter matrix coefficients.
Shape:
- Output: :math:`(\text{kernel_size}_x, \text{kernel_size}_y)`
Examples:
>>> get_laplacian_kernel2d(3)
tensor([[ 1., 1., 1.],
[ 1., -8., 1.],
[ 1., 1., 1.]])
>>> get_laplacian_kernel2d(5)
tensor([[ 1., 1., 1., 1., 1.],
[ 1., 1., 1., 1., 1.],
[ 1., 1., -24., 1., 1.],
[ 1., 1., 1., 1., 1.],
[ 1., 1., 1., 1., 1.]])
"""
if not isinstance(kernel_size, int) or kernel_size % 2 == 0 or kernel_size <= 0:
raise TypeError(f"ksize must be an odd positive integer. Got {kernel_size}")
kernel = torch.ones((kernel_size, kernel_size))
mid = kernel_size // 2
kernel[mid, mid] = 1 - kernel_size**2
kernel_2d: torch.Tensor = kernel
return kernel_2d
def get_pascal_kernel_2d(kernel_size: int, norm: bool = True) -> torch.Tensor:
"""Generate pascal filter kernel by kernel size.
Args:
kernel_size: height and width of the kernel.
norm: if to normalize the kernel or not. Default: True.
Returns:
kernel shaped as :math:`(kernel_size, kernel_size)`
Examples:
>>> get_pascal_kernel_2d(1)
tensor([[1.]])
>>> get_pascal_kernel_2d(4)
tensor([[0.0156, 0.0469, 0.0469, 0.0156],
[0.0469, 0.1406, 0.1406, 0.0469],
[0.0469, 0.1406, 0.1406, 0.0469],
[0.0156, 0.0469, 0.0469, 0.0156]])
>>> get_pascal_kernel_2d(4, norm=False)
tensor([[1., 3., 3., 1.],
[3., 9., 9., 3.],
[3., 9., 9., 3.],
[1., 3., 3., 1.]])
"""
a = get_pascal_kernel_1d(kernel_size)
filt = a[:, None] * a[None, :]
if norm:
filt = filt / torch.sum(filt)
return filt
def get_pascal_kernel_1d(kernel_size: int, norm: bool = False) -> torch.Tensor:
"""Generate Yang Hui triangle (Pascal's triangle) by a given number.
Args:
kernel_size: height and width of the kernel.
norm: if to normalize the kernel or not. Default: False.
Returns:
kernel shaped as :math:`(kernel_size,)`
Examples:
>>> get_pascal_kernel_1d(1)
tensor([1.])
>>> get_pascal_kernel_1d(2)
tensor([1., 1.])
>>> get_pascal_kernel_1d(3)
tensor([1., 2., 1.])
>>> get_pascal_kernel_1d(4)
tensor([1., 3., 3., 1.])
>>> get_pascal_kernel_1d(5)
tensor([1., 4., 6., 4., 1.])
>>> get_pascal_kernel_1d(6)
tensor([ 1., 5., 10., 10., 5., 1.])
"""
pre: List[float] = []
cur: List[float] = []
for i in range(kernel_size):
cur = [1.0] * (i + 1)
for j in range(1, i // 2 + 1):
value = pre[j - 1] + pre[j]
cur[j] = value
if i != 2 * j:
cur[-j - 1] = value
pre = cur
out = torch.as_tensor(cur)
if norm:
out = out / torch.sum(out)
return out
def get_canny_nms_kernel(device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
"""Utility function that returns 3x3 kernels for the Canny Non-maximal suppression."""
kernel: torch.Tensor = torch.tensor(
[
[[0.0, 0.0, 0.0], [0.0, 1.0, -1.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]],
[[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
[[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
],
device=device,
dtype=dtype,
)
return kernel.unsqueeze(1)
def get_hysteresis_kernel(device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
"""Utility function that returns the 3x3 kernels for the Canny hysteresis."""
kernel: torch.Tensor = torch.tensor(
[
[[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
],
device=device,
dtype=dtype,
)
return kernel.unsqueeze(1)
def get_hanning_kernel1d(kernel_size: int, device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
r"""Returns Hanning (also known as Hann) kernel, used in signal processing and KCF tracker.
.. math:: w(n) = 0.5 - 0.5cos\\left(\\frac{2\\pi{n}}{M-1}\\right)
\\qquad 0 \\leq n \\leq M-1
See further in numpy docs https://numpy.org/doc/stable/reference/generated/numpy.hanning.html
Args:
kernel_size: The size the of the kernel. It should be positive.
Returns:
1D tensor with Hanning filter coefficients.
.. math:: w(n) = 0.5 - 0.5cos\\left(\\frac{2\\pi{n}}{M-1}\\right)
Shape:
- Output: math:`(\text{kernel_size})`
Examples:
>>> get_hanning_kernel1d(4)
tensor([0.0000, 0.7500, 0.7500, 0.0000])
"""
if not isinstance(kernel_size, int) or kernel_size <= 2:
raise TypeError(f"ksize must be an positive integer > 2. Got {kernel_size}")
x: torch.Tensor = torch.arange(kernel_size, device=device, dtype=dtype)
x = 0.5 - 0.5 * torch.cos(2.0 * math.pi * x / float(kernel_size - 1))
return x
def get_hanning_kernel2d(kernel_size: Tuple[int, int], device=torch.device('cpu'), dtype=torch.float) -> torch.Tensor:
r"""Returns 2d Hanning kernel, used in signal processing and KCF tracker.
Args:
kernel_size: The size of the kernel for the filter. It should be positive.
Returns:
2D tensor with Hanning filter coefficients.
.. math:: w(n) = 0.5 - 0.5cos\\left(\\frac{2\\pi{n}}{M-1}\\right)
Shape:
- Output: math:`(\text{kernel_size[0], kernel_size[1]})`
"""
if kernel_size[0] <= 2 or kernel_size[1] <= 2:
raise TypeError(f"ksize must be an tuple of positive integers > 2. Got {kernel_size}")
ky: torch.Tensor = get_hanning_kernel1d(kernel_size[0], device, dtype)[None].T
kx: torch.Tensor = get_hanning_kernel1d(kernel_size[1], device, dtype)[None]
kernel2d = ky @ kx
return kernel2d
-263
View File
@@ -1,263 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from .kernels import get_spatial_gradient_kernel2d, get_spatial_gradient_kernel3d, normalize_kernel2d
def spatial_gradient(input: torch.Tensor, mode: str = 'sobel', order: int = 1, normalized: bool = True) -> torch.Tensor:
r"""Compute the first order image derivative in both x and y using a Sobel operator.
.. image:: _static/img/spatial_gradient.png
Args:
input: input image tensor with shape :math:`(B, C, H, W)`.
mode: derivatives modality, can be: `sobel` or `diff`.
order: the order of the derivatives.
normalized: whether the output is normalized.
Return:
the derivatives of the input feature map. with shape :math:`(B, C, 2, H, W)`.
.. note::
See a working example `here <https://kornia-tutorials.readthedocs.io/en/latest/
filtering_edges.html>`__.
Examples:
>>> input = torch.rand(1, 3, 4, 4)
>>> output = spatial_gradient(input) # 1x3x2x4x4
>>> output.shape
torch.Size([1, 3, 2, 4, 4])
"""
if not isinstance(input, torch.Tensor):
raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
if not len(input.shape) == 4:
raise ValueError(f"Invalid input shape, we expect BxCxHxW. Got: {input.shape}")
# allocate kernel
kernel: torch.Tensor = get_spatial_gradient_kernel2d(mode, order)
if normalized:
kernel = normalize_kernel2d(kernel)
# prepare kernel
b, c, h, w = input.shape
tmp_kernel: torch.Tensor = kernel.to(input).detach()
tmp_kernel = tmp_kernel.unsqueeze(1).unsqueeze(1)
# convolve input tensor with sobel kernel
kernel_flip: torch.Tensor = tmp_kernel.flip(-3)
# Pad with "replicate for spatial dims, but with zeros for channel
spatial_pad = [kernel.size(1) // 2, kernel.size(1) // 2, kernel.size(2) // 2, kernel.size(2) // 2]
out_channels: int = 3 if order == 2 else 2
padded_inp: torch.Tensor = F.pad(input.reshape(b * c, 1, h, w), spatial_pad, 'replicate')[:, :, None]
return F.conv3d(padded_inp, kernel_flip, padding=0).view(b, c, out_channels, h, w)
def spatial_gradient3d(input: torch.Tensor, mode: str = 'diff', order: int = 1) -> torch.Tensor:
r"""Compute the first and second order volume derivative in x, y and d using a diff operator.
Args:
input: input features tensor with shape :math:`(B, C, D, H, W)`.
mode: derivatives modality, can be: `sobel` or `diff`.
order: the order of the derivatives.
Return:
the spatial gradients of the input feature map with shape math:`(B, C, 3, D, H, W)`
or :math:`(B, C, 6, D, H, W)`.
Examples:
>>> input = torch.rand(1, 4, 2, 4, 4)
>>> output = spatial_gradient3d(input)
>>> output.shape
torch.Size([1, 4, 3, 2, 4, 4])
"""
if not isinstance(input, torch.Tensor):
raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
if not len(input.shape) == 5:
raise ValueError(f"Invalid input shape, we expect BxCxDxHxW. Got: {input.shape}")
b, c, d, h, w = input.shape
dev = input.device
dtype = input.dtype
if (mode == 'diff') and (order == 1):
# we go for the special case implementation due to conv3d bad speed
x: torch.Tensor = F.pad(input, 6 * [1], 'replicate')
center = slice(1, -1)
left = slice(0, -2)
right = slice(2, None)
out = torch.empty(b, c, 3, d, h, w, device=dev, dtype=dtype)
out[..., 0, :, :, :] = x[..., center, center, right] - x[..., center, center, left]
out[..., 1, :, :, :] = x[..., center, right, center] - x[..., center, left, center]
out[..., 2, :, :, :] = x[..., right, center, center] - x[..., left, center, center]
out = 0.5 * out
else:
# prepare kernel
# allocate kernel
kernel: torch.Tensor = get_spatial_gradient_kernel3d(mode, order)
tmp_kernel: torch.Tensor = kernel.to(input).detach()
tmp_kernel = tmp_kernel.repeat(c, 1, 1, 1, 1)
# convolve input tensor with grad kernel
kernel_flip: torch.Tensor = tmp_kernel.flip(-3)
# Pad with "replicate for spatial dims, but with zeros for channel
spatial_pad = [
kernel.size(2) // 2,
kernel.size(2) // 2,
kernel.size(3) // 2,
kernel.size(3) // 2,
kernel.size(4) // 2,
kernel.size(4) // 2,
]
out_ch: int = 6 if order == 2 else 3
out = F.conv3d(F.pad(input, spatial_pad, 'replicate'), kernel_flip, padding=0, groups=c).view(
b, c, out_ch, d, h, w
)
return out
def sobel(input: torch.Tensor, normalized: bool = True, eps: float = 1e-6) -> torch.Tensor:
r"""Compute the Sobel operator and returns the magnitude per channel.
.. image:: _static/img/sobel.png
Args:
input: the input image with shape :math:`(B,C,H,W)`.
normalized: if True, L1 norm of the kernel is set to 1.
eps: regularization number to avoid NaN during backprop.
Return:
the sobel edge gradient magnitudes map with shape :math:`(B,C,H,W)`.
.. note::
See a working example `here <https://kornia-tutorials.readthedocs.io/en/latest/
filtering_edges.html>`__.
Example:
>>> input = torch.rand(1, 3, 4, 4)
>>> output = sobel(input) # 1x3x4x4
>>> output.shape
torch.Size([1, 3, 4, 4])
"""
if not isinstance(input, torch.Tensor):
raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
if not len(input.shape) == 4:
raise ValueError(f"Invalid input shape, we expect BxCxHxW. Got: {input.shape}")
# comput the x/y gradients
edges: torch.Tensor = spatial_gradient(input, normalized=normalized)
# unpack the edges
gx: torch.Tensor = edges[:, :, 0]
gy: torch.Tensor = edges[:, :, 1]
# compute gradient maginitude
magnitude: torch.Tensor = torch.sqrt(gx * gx + gy * gy + eps)
return magnitude
class SpatialGradient(nn.Module):
r"""Compute the first order image derivative in both x and y using a Sobel operator.
Args:
mode: derivatives modality, can be: `sobel` or `diff`.
order: the order of the derivatives.
normalized: whether the output is normalized.
Return:
the sobel edges of the input feature map.
Shape:
- Input: :math:`(B, C, H, W)`
- Output: :math:`(B, C, 2, H, W)`
Examples:
>>> input = torch.rand(1, 3, 4, 4)
>>> output = SpatialGradient()(input) # 1x3x2x4x4
"""
def __init__(self, mode: str = 'sobel', order: int = 1, normalized: bool = True) -> None:
super().__init__()
self.normalized: bool = normalized
self.order: int = order
self.mode: str = mode
def __repr__(self) -> str:
return (
self.__class__.__name__ + '('
'order=' + str(self.order) + ', ' + 'normalized=' + str(self.normalized) + ', ' + 'mode=' + self.mode + ')'
)
def forward(self, input: torch.Tensor) -> torch.Tensor:
return spatial_gradient(input, self.mode, self.order, self.normalized)
class SpatialGradient3d(nn.Module):
r"""Compute the first and second order volume derivative in x, y and d using a diff operator.
Args:
mode: derivatives modality, can be: `sobel` or `diff`.
order: the order of the derivatives.
Return:
the spatial gradients of the input feature map.
Shape:
- Input: :math:`(B, C, D, H, W)`. D, H, W are spatial dimensions, gradient is calculated w.r.t to them.
- Output: :math:`(B, C, 3, D, H, W)` or :math:`(B, C, 6, D, H, W)`
Examples:
>>> input = torch.rand(1, 4, 2, 4, 4)
>>> output = SpatialGradient3d()(input)
>>> output.shape
torch.Size([1, 4, 3, 2, 4, 4])
"""
def __init__(self, mode: str = 'diff', order: int = 1) -> None:
super().__init__()
self.order: int = order
self.mode: str = mode
self.kernel = get_spatial_gradient_kernel3d(mode, order)
return
def __repr__(self) -> str:
return self.__class__.__name__ + '(' 'order=' + str(self.order) + ', ' + 'mode=' + self.mode + ')'
def forward(self, input: torch.Tensor) -> torch.Tensor: # type: ignore
return spatial_gradient3d(input, self.mode, self.order)
class Sobel(nn.Module):
r"""Compute the Sobel operator and returns the magnitude per channel.
Args:
normalized: if True, L1 norm of the kernel is set to 1.
eps: regularization number to avoid NaN during backprop.
Return:
the sobel edge gradient magnitudes map.
Shape:
- Input: :math:`(B, C, H, W)`
- Output: :math:`(B, C, H, W)`
Examples:
>>> input = torch.rand(1, 3, 4, 4)
>>> output = Sobel()(input) # 1x3x4x4
"""
def __init__(self, normalized: bool = True, eps: float = 1e-6) -> None:
super().__init__()
self.normalized: bool = normalized
self.eps: float = eps
def __repr__(self) -> str:
return self.__class__.__name__ + '(' 'normalized=' + str(self.normalized) + ')'
def forward(self, input: torch.Tensor) -> torch.Tensor:
return sobel(input, self.normalized, self.eps)
-141
View File
@@ -1,141 +0,0 @@
import os
import re
import random
import time
import torch
import torch.nn as nn
import logging
import numpy as np
from os import path as osp
def constant_init(module, val, bias=0):
if hasattr(module, 'weight') and module.weight is not None:
nn.init.constant_(module.weight, val)
if hasattr(module, 'bias') and module.bias is not None:
nn.init.constant_(module.bias, bias)
initialized_logger = {}
def get_root_logger(logger_name='basicsr', log_level=logging.INFO, log_file=None):
"""Get the root logger.
The logger will be initialized if it has not been initialized. By default a
StreamHandler will be added. If `log_file` is specified, a FileHandler will
also be added.
Args:
logger_name (str): root logger name. Default: 'basicsr'.
log_file (str | None): The log filename. If specified, a FileHandler
will be added to the root logger.
log_level (int): The root logger level. Note that only the process of
rank 0 is affected, while other processes will set the level to
"Error" and be silent most of the time.
Returns:
logging.Logger: The root logger.
"""
logger = logging.getLogger(logger_name)
# if the logger has been initialized, just return it
if logger_name in initialized_logger:
return logger
format_str = '%(asctime)s %(levelname)s: %(message)s'
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(format_str))
logger.addHandler(stream_handler)
logger.propagate = False
if log_file is not None:
logger.setLevel(log_level)
# add file handler
# file_handler = logging.FileHandler(log_file, 'w')
file_handler = logging.FileHandler(log_file, 'a') #Shangchen: keep the previous log
file_handler.setFormatter(logging.Formatter(format_str))
file_handler.setLevel(log_level)
logger.addHandler(file_handler)
initialized_logger[logger_name] = True
return logger
def get_version_numbers(version_str):
# 匹配主要版本号(支持 2.8.0 或 2.8.0.dev20250422+cu128 等格式)
pattern = r"^(\d+)\.(\d+)\.(\d+)"
match = re.match(pattern, version_str)
if match:
return [int(x) for x in match.groups()]
return [0, 0, 0] # 如果无法匹配,返回默认值
# 使用示例
IS_HIGH_VERSION = get_version_numbers(torch.__version__) >= [1, 12, 0]
def gpu_is_available():
if IS_HIGH_VERSION:
if torch.backends.mps.is_available():
return True
return True if torch.cuda.is_available() and torch.backends.cudnn.is_available() else False
def get_device(gpu_id=None):
if gpu_id is None:
gpu_str = ''
elif isinstance(gpu_id, int):
gpu_str = f':{gpu_id}'
else:
raise TypeError('Input should be int value.')
if IS_HIGH_VERSION:
if torch.backends.mps.is_available():
return torch.device('mps'+gpu_str)
return torch.device('cuda'+gpu_str if torch.cuda.is_available() and torch.backends.cudnn.is_available() else 'cpu')
def set_random_seed(seed):
"""Set random seeds."""
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def get_time_str():
return time.strftime('%Y%m%d_%H%M%S', time.localtime())
def scandir(dir_path, suffix=None, recursive=False, full_path=False):
"""Scan a directory to find the interested files.
Args:
dir_path (str): Path of the directory.
suffix (str | tuple(str), optional): File suffix that we are
interested in. Default: None.
recursive (bool, optional): If set to True, recursively scan the
directory. Default: False.
full_path (bool, optional): If set to True, include the dir_path.
Default: False.
Returns:
A generator for all the interested files with relative pathes.
"""
if (suffix is not None) and not isinstance(suffix, (str, tuple)):
raise TypeError('"suffix" must be a string or tuple of strings')
root = dir_path
def _scandir(dir_path, suffix, recursive):
for entry in os.scandir(dir_path):
if not entry.name.startswith('.') and entry.is_file():
if full_path:
return_path = entry.path
else:
return_path = osp.relpath(entry.path, root)
if suffix is None:
yield return_path
elif return_path.endswith(suffix):
yield return_path
else:
if recursive:
yield from _scandir(entry.path, suffix=suffix, recursive=recursive)
else:
continue
return _scandir(dir_path, suffix=suffix, recursive=recursive)
@@ -1,131 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from functools import reduce
class BaseNetwork(nn.Module):
def __init__(self):
super(BaseNetwork, self).__init__()
def print_network(self):
if isinstance(self, list):
self = self[0]
num_params = 0
for param in self.parameters():
num_params += param.numel()
print(
'Network [%s] was created. Total number of parameters: %.1f million. '
'To see the architecture, do print(network).' %
(type(self).__name__, num_params / 1000000))
def init_weights(self, init_type='normal', gain=0.02):
'''
initialize network's weights
init_type: normal | xavier | kaiming | orthogonal
https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/9451e70673400885567d08a9e97ade2524c700d0/models/networks.py#L39
'''
def init_func(m):
classname = m.__class__.__name__
if classname.find('InstanceNorm2d') != -1:
if hasattr(m, 'weight') and m.weight is not None:
nn.init.constant_(m.weight.data, 1.0)
if hasattr(m, 'bias') and m.bias is not None:
nn.init.constant_(m.bias.data, 0.0)
elif hasattr(m, 'weight') and (classname.find('Conv') != -1
or classname.find('Linear') != -1):
if init_type == 'normal':
nn.init.normal_(m.weight.data, 0.0, gain)
elif init_type == 'xavier':
nn.init.xavier_normal_(m.weight.data, gain=gain)
elif init_type == 'xavier_uniform':
nn.init.xavier_uniform_(m.weight.data, gain=1.0)
elif init_type == 'kaiming':
nn.init.kaiming_normal_(m.weight.data, a=0, mode='fan_in')
elif init_type == 'orthogonal':
nn.init.orthogonal_(m.weight.data, gain=gain)
elif init_type == 'none': # uses pytorch's default init method
m.reset_parameters()
else:
raise NotImplementedError(
'initialization method [%s] is not implemented' %
init_type)
if hasattr(m, 'bias') and m.bias is not None:
nn.init.constant_(m.bias.data, 0.0)
self.apply(init_func)
# propagate to children
for m in self.children():
if hasattr(m, 'init_weights'):
m.init_weights(init_type, gain)
class Vec2Feat(nn.Module):
def __init__(self, channel, hidden, kernel_size, stride, padding):
super(Vec2Feat, self).__init__()
self.relu = nn.LeakyReLU(0.2, inplace=True)
c_out = reduce((lambda x, y: x * y), kernel_size) * channel
self.embedding = nn.Linear(hidden, c_out)
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
self.bias_conv = nn.Conv2d(channel,
channel,
kernel_size=3,
stride=1,
padding=1)
def forward(self, x, t, output_size):
b_, _, _, _, c_ = x.shape
x = x.view(b_, -1, c_)
feat = self.embedding(x)
b, _, c = feat.size()
feat = feat.view(b * t, -1, c).permute(0, 2, 1)
feat = F.fold(feat,
output_size=output_size,
kernel_size=self.kernel_size,
stride=self.stride,
padding=self.padding)
feat = self.bias_conv(feat)
return feat
class FusionFeedForward(nn.Module):
def __init__(self, dim, hidden_dim=1960, t2t_params=None):
super(FusionFeedForward, self).__init__()
# We set hidden_dim as a default to 1960
self.fc1 = nn.Sequential(nn.Linear(dim, hidden_dim))
self.fc2 = nn.Sequential(nn.GELU(), nn.Linear(hidden_dim, dim))
assert t2t_params is not None
self.t2t_params = t2t_params
self.kernel_shape = reduce((lambda x, y: x * y), t2t_params['kernel_size']) # 49
def forward(self, x, output_size):
n_vecs = 1
for i, d in enumerate(self.t2t_params['kernel_size']):
n_vecs *= int((output_size[i] + 2 * self.t2t_params['padding'][i] -
(d - 1) - 1) / self.t2t_params['stride'][i] + 1)
x = self.fc1(x)
b, n, c = x.size()
normalizer = x.new_ones(b, n, self.kernel_shape).view(-1, n_vecs, self.kernel_shape).permute(0, 2, 1)
normalizer = F.fold(normalizer,
output_size=output_size,
kernel_size=self.t2t_params['kernel_size'],
padding=self.t2t_params['padding'],
stride=self.t2t_params['stride'])
x = F.fold(x.view(-1, n_vecs, c).permute(0, 2, 1),
output_size=output_size,
kernel_size=self.t2t_params['kernel_size'],
padding=self.t2t_params['padding'],
stride=self.t2t_params['stride'])
x = F.unfold(x / normalizer,
kernel_size=self.t2t_params['kernel_size'],
padding=self.t2t_params['padding'],
stride=self.t2t_params['stride']).permute(
0, 2, 1).contiguous().view(b, n, c)
x = self.fc2(x)
return x
@@ -1,54 +0,0 @@
import torch
import torch.nn as nn
from torch.nn import init as init
from torch.nn.modules.utils import _pair, _single
import math
class ModulatedDeformConv2d(nn.Module):
def __init__(self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
deform_groups=1,
bias=True):
super(ModulatedDeformConv2d, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = _pair(kernel_size)
self.stride = stride
self.padding = padding
self.dilation = dilation
self.groups = groups
self.deform_groups = deform_groups
self.with_bias = bias
# enable compatibility with nn.Conv2d
self.transposed = False
self.output_padding = _single(0)
self.weight = nn.Parameter(torch.Tensor(out_channels, in_channels // groups, *self.kernel_size))
if bias:
self.bias = nn.Parameter(torch.Tensor(out_channels))
else:
self.register_parameter('bias', None)
self.init_weights()
def init_weights(self):
n = self.in_channels
for k in self.kernel_size:
n *= k
stdv = 1. / math.sqrt(n)
self.weight.data.uniform_(-stdv, stdv)
if self.bias is not None:
self.bias.data.zero_()
if hasattr(self, 'conv_offset'):
self.conv_offset.weight.data.zero_()
self.conv_offset.bias.data.zero_()
def forward(self, x, offset, mask):
pass
@@ -1,265 +0,0 @@
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
from backend.inpaint.video.raft import RAFT
from backend.inpaint.video.model.modules.flow_loss_utils import flow_warp, ternary_loss2
def initialize_RAFT(model_path='weights/raft-things.pth', device='cuda'):
"""Initializes the RAFT model.
"""
args = argparse.ArgumentParser()
args.raft_model = model_path
args.small = False
args.mixed_precision = False
args.alternate_corr = False
model = torch.nn.DataParallel(RAFT(args))
model.load_state_dict(torch.load(args.raft_model, map_location='cpu'))
model = model.module
model.to(device)
return model
class RAFT_bi(nn.Module):
"""Flow completion loss"""
def __init__(self, model_path='weights/raft-things.pth', device='cuda'):
super().__init__()
self.fix_raft = initialize_RAFT(model_path, device=device)
for p in self.fix_raft.parameters():
p.requires_grad = False
self.l1_criterion = nn.L1Loss()
self.eval()
def forward(self, gt_local_frames, iters=20):
b, l_t, c, h, w = gt_local_frames.size()
# print(gt_local_frames.shape)
with torch.no_grad():
gtlf_1 = gt_local_frames[:, :-1, :, :, :].reshape(-1, c, h, w)
gtlf_2 = gt_local_frames[:, 1:, :, :, :].reshape(-1, c, h, w)
# print(gtlf_1.shape)
_, gt_flows_forward = self.fix_raft(gtlf_1, gtlf_2, iters=iters, test_mode=True)
_, gt_flows_backward = self.fix_raft(gtlf_2, gtlf_1, iters=iters, test_mode=True)
gt_flows_forward = gt_flows_forward.view(b, l_t-1, 2, h, w)
gt_flows_backward = gt_flows_backward.view(b, l_t-1, 2, h, w)
return gt_flows_forward, gt_flows_backward
##################################################################################
def smoothness_loss(flow, cmask):
delta_u, delta_v, mask = smoothness_deltas(flow)
loss_u = charbonnier_loss(delta_u, cmask)
loss_v = charbonnier_loss(delta_v, cmask)
return loss_u + loss_v
def smoothness_deltas(flow):
"""
flow: [b, c, h, w]
"""
mask_x = create_mask(flow, [[0, 0], [0, 1]])
mask_y = create_mask(flow, [[0, 1], [0, 0]])
mask = torch.cat((mask_x, mask_y), dim=1)
mask = mask.to(flow.device)
filter_x = torch.tensor([[0, 0, 0.], [0, 1, -1], [0, 0, 0]])
filter_y = torch.tensor([[0, 0, 0.], [0, 1, 0], [0, -1, 0]])
weights = torch.ones([2, 1, 3, 3])
weights[0, 0] = filter_x
weights[1, 0] = filter_y
weights = weights.to(flow.device)
flow_u, flow_v = torch.split(flow, split_size_or_sections=1, dim=1)
delta_u = F.conv2d(flow_u, weights, stride=1, padding=1)
delta_v = F.conv2d(flow_v, weights, stride=1, padding=1)
return delta_u, delta_v, mask
def second_order_loss(flow, cmask):
delta_u, delta_v, mask = second_order_deltas(flow)
loss_u = charbonnier_loss(delta_u, cmask)
loss_v = charbonnier_loss(delta_v, cmask)
return loss_u + loss_v
def charbonnier_loss(x, mask=None, truncate=None, alpha=0.45, beta=1.0, epsilon=0.001):
"""
Compute the generalized charbonnier loss of the difference tensor x
All positions where mask == 0 are not taken into account
x: a tensor of shape [b, c, h, w]
mask: a mask of shape [b, mc, h, w], where mask channels must be either 1 or the same as
the number of channels of x. Entries should be 0 or 1
return: loss
"""
b, c, h, w = x.shape
norm = b * c * h * w
error = torch.pow(torch.square(x * beta) + torch.square(torch.tensor(epsilon)), alpha)
if mask is not None:
error = mask * error
if truncate is not None:
error = torch.min(error, truncate)
return torch.sum(error) / norm
def second_order_deltas(flow):
"""
consider the single flow first
flow shape: [b, c, h, w]
"""
# create mask
mask_x = create_mask(flow, [[0, 0], [1, 1]])
mask_y = create_mask(flow, [[1, 1], [0, 0]])
mask_diag = create_mask(flow, [[1, 1], [1, 1]])
mask = torch.cat((mask_x, mask_y, mask_diag, mask_diag), dim=1)
mask = mask.to(flow.device)
filter_x = torch.tensor([[0, 0, 0.], [1, -2, 1], [0, 0, 0]])
filter_y = torch.tensor([[0, 1, 0.], [0, -2, 0], [0, 1, 0]])
filter_diag1 = torch.tensor([[1, 0, 0.], [0, -2, 0], [0, 0, 1]])
filter_diag2 = torch.tensor([[0, 0, 1.], [0, -2, 0], [1, 0, 0]])
weights = torch.ones([4, 1, 3, 3])
weights[0] = filter_x
weights[1] = filter_y
weights[2] = filter_diag1
weights[3] = filter_diag2
weights = weights.to(flow.device)
# split the flow into flow_u and flow_v, conv them with the weights
flow_u, flow_v = torch.split(flow, split_size_or_sections=1, dim=1)
delta_u = F.conv2d(flow_u, weights, stride=1, padding=1)
delta_v = F.conv2d(flow_v, weights, stride=1, padding=1)
return delta_u, delta_v, mask
def create_mask(tensor, paddings):
"""
tensor shape: [b, c, h, w]
paddings: [2 x 2] shape list, the first row indicates up and down paddings
the second row indicates left and right paddings
| |
| x |
| x * x |
| x |
| |
"""
shape = tensor.shape
inner_height = shape[2] - (paddings[0][0] + paddings[0][1])
inner_width = shape[3] - (paddings[1][0] + paddings[1][1])
inner = torch.ones([inner_height, inner_width])
torch_paddings = [paddings[1][0], paddings[1][1], paddings[0][0], paddings[0][1]] # left, right, up and down
mask2d = F.pad(inner, pad=torch_paddings)
mask3d = mask2d.unsqueeze(0).repeat(shape[0], 1, 1)
mask4d = mask3d.unsqueeze(1)
return mask4d.detach()
def ternary_loss(flow_comp, flow_gt, mask, current_frame, shift_frame, scale_factor=1):
if scale_factor != 1:
current_frame = F.interpolate(current_frame, scale_factor=1 / scale_factor, mode='bilinear')
shift_frame = F.interpolate(shift_frame, scale_factor=1 / scale_factor, mode='bilinear')
warped_sc = flow_warp(shift_frame, flow_gt.permute(0, 2, 3, 1))
noc_mask = torch.exp(-50. * torch.sum(torch.abs(current_frame - warped_sc), dim=1).pow(2)).unsqueeze(1)
warped_comp_sc = flow_warp(shift_frame, flow_comp.permute(0, 2, 3, 1))
loss = ternary_loss2(current_frame, warped_comp_sc, noc_mask, mask)
return loss
class FlowLoss(nn.Module):
def __init__(self):
super().__init__()
self.l1_criterion = nn.L1Loss()
def forward(self, pred_flows, gt_flows, masks, frames):
# pred_flows: b t-1 2 h w
loss = 0
warp_loss = 0
h, w = pred_flows[0].shape[-2:]
masks = [masks[:,:-1,...].contiguous(), masks[:, 1:, ...].contiguous()]
frames0 = frames[:,:-1,...]
frames1 = frames[:,1:,...]
current_frames = [frames0, frames1]
next_frames = [frames1, frames0]
for i in range(len(pred_flows)):
# print(pred_flows[i].shape)
combined_flow = pred_flows[i] * masks[i] + gt_flows[i] * (1-masks[i])
l1_loss = self.l1_criterion(pred_flows[i] * masks[i], gt_flows[i] * masks[i]) / torch.mean(masks[i])
l1_loss += self.l1_criterion(pred_flows[i] * (1-masks[i]), gt_flows[i] * (1-masks[i])) / torch.mean((1-masks[i]))
smooth_loss = smoothness_loss(combined_flow.reshape(-1,2,h,w), masks[i].reshape(-1,1,h,w))
smooth_loss2 = second_order_loss(combined_flow.reshape(-1,2,h,w), masks[i].reshape(-1,1,h,w))
warp_loss_i = ternary_loss(combined_flow.reshape(-1,2,h,w), gt_flows[i].reshape(-1,2,h,w),
masks[i].reshape(-1,1,h,w), current_frames[i].reshape(-1,3,h,w), next_frames[i].reshape(-1,3,h,w))
loss += l1_loss + smooth_loss + smooth_loss2
warp_loss += warp_loss_i
return loss, warp_loss
def edgeLoss(preds_edges, edges):
"""
Args:
preds_edges: with shape [b, c, h , w]
edges: with shape [b, c, h, w]
Returns: Edge losses
"""
mask = (edges > 0.5).float()
b, c, h, w = mask.shape
num_pos = torch.sum(mask, dim=[1, 2, 3]).float() # Shape: [b,].
num_neg = c * h * w - num_pos # Shape: [b,].
neg_weights = (num_neg / (num_pos + num_neg)).unsqueeze(1).unsqueeze(2).unsqueeze(3)
pos_weights = (num_pos / (num_pos + num_neg)).unsqueeze(1).unsqueeze(2).unsqueeze(3)
weight = neg_weights * mask + pos_weights * (1 - mask) # weight for debug
losses = F.binary_cross_entropy_with_logits(preds_edges.float(), edges.float(), weight=weight, reduction='none')
loss = torch.mean(losses)
return loss
class EdgeLoss(nn.Module):
def __init__(self):
super().__init__()
def forward(self, pred_edges, gt_edges, masks):
# pred_flows: b t-1 1 h w
loss = 0
h, w = pred_edges[0].shape[-2:]
masks = [masks[:,:-1,...].contiguous(), masks[:, 1:, ...].contiguous()]
for i in range(len(pred_edges)):
# print(f'edges_{i}', torch.sum(gt_edges[i])) # debug
combined_edge = pred_edges[i] * masks[i] + gt_edges[i] * (1-masks[i])
edge_loss = (edgeLoss(pred_edges[i].reshape(-1,1,h,w), gt_edges[i].reshape(-1,1,h,w)) \
+ 5 * edgeLoss(combined_edge.reshape(-1,1,h,w), gt_edges[i].reshape(-1,1,h,w)))
loss += edge_loss
return loss
class FlowSimpleLoss(nn.Module):
def __init__(self):
super().__init__()
self.l1_criterion = nn.L1Loss()
def forward(self, pred_flows, gt_flows):
# pred_flows: b t-1 2 h w
loss = 0
h, w = pred_flows[0].shape[-2:]
h_orig, w_orig = gt_flows[0].shape[-2:]
pred_flows = [f.view(-1, 2, h, w) for f in pred_flows]
gt_flows = [f.view(-1, 2, h_orig, w_orig) for f in gt_flows]
ds_factor = 1.0*h/h_orig
gt_flows = [F.interpolate(f, scale_factor=ds_factor, mode='area') * ds_factor for f in gt_flows]
for i in range(len(pred_flows)):
loss += self.l1_criterion(pred_flows[i], gt_flows[i])
return loss
@@ -1,142 +0,0 @@
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
def flow_warp(x,
flow,
interpolation='bilinear',
padding_mode='zeros',
align_corners=True):
"""Warp an image or a feature map with optical flow.
Args:
x (Tensor): Tensor with size (n, c, h, w).
flow (Tensor): Tensor with size (n, h, w, 2). The last dimension is
a two-channel, denoting the width and height relative offsets.
Note that the values are not normalized to [-1, 1].
interpolation (str): Interpolation mode: 'nearest' or 'bilinear'.
Default: 'bilinear'.
padding_mode (str): Padding mode: 'zeros' or 'border' or 'reflection'.
Default: 'zeros'.
align_corners (bool): Whether align corners. Default: True.
Returns:
Tensor: Warped image or feature map.
"""
if x.size()[-2:] != flow.size()[1:3]:
raise ValueError(f'The spatial sizes of input ({x.size()[-2:]}) and '
f'flow ({flow.size()[1:3]}) are not the same.')
_, _, h, w = x.size()
# create mesh grid
device = flow.device
grid_y, grid_x = torch.meshgrid(torch.arange(0, h, device=device), torch.arange(0, w, device=device))
grid = torch.stack((grid_x, grid_y), 2).type_as(x) # (w, h, 2)
grid.requires_grad = False
grid_flow = grid + flow
# scale grid_flow to [-1,1]
grid_flow_x = 2.0 * grid_flow[:, :, :, 0] / max(w - 1, 1) - 1.0
grid_flow_y = 2.0 * grid_flow[:, :, :, 1] / max(h - 1, 1) - 1.0
grid_flow = torch.stack((grid_flow_x, grid_flow_y), dim=3)
output = F.grid_sample(x,
grid_flow,
mode=interpolation,
padding_mode=padding_mode,
align_corners=align_corners)
return output
# def image_warp(image, flow):
# b, c, h, w = image.size()
# device = image.device
# flow = torch.cat([flow[:, 0:1, :, :] / ((w - 1.0) / 2.0), flow[:, 1:2, :, :] / ((h - 1.0) / 2.0)], dim=1) # normalize to [-1~1](from upper left to lower right
# flow = flow.permute(0, 2, 3, 1) # if you wanna use grid_sample function, the channel(band) shape of show must be in the last dimension
# x = np.linspace(-1, 1, w)
# y = np.linspace(-1, 1, h)
# X, Y = np.meshgrid(x, y)
# grid = torch.cat((torch.from_numpy(X.astype('float32')).unsqueeze(0).unsqueeze(3),
# torch.from_numpy(Y.astype('float32')).unsqueeze(0).unsqueeze(3)), 3).to(device)
# output = torch.nn.functional.grid_sample(image, grid + flow, mode='bilinear', padding_mode='zeros')
# return output
def length_sq(x):
return torch.sum(torch.square(x), dim=1, keepdim=True)
def fbConsistencyCheck(flow_fw, flow_bw, alpha1=0.01, alpha2=0.5):
flow_bw_warped = flow_warp(flow_bw, flow_fw.permute(0, 2, 3, 1)) # wb(wf(x))
flow_fw_warped = flow_warp(flow_fw, flow_bw.permute(0, 2, 3, 1)) # wf(wb(x))
flow_diff_fw = flow_fw + flow_bw_warped # wf + wb(wf(x))
flow_diff_bw = flow_bw + flow_fw_warped # wb + wf(wb(x))
mag_sq_fw = length_sq(flow_fw) + length_sq(flow_bw_warped) # |wf| + |wb(wf(x))|
mag_sq_bw = length_sq(flow_bw) + length_sq(flow_fw_warped) # |wb| + |wf(wb(x))|
occ_thresh_fw = alpha1 * mag_sq_fw + alpha2
occ_thresh_bw = alpha1 * mag_sq_bw + alpha2
fb_occ_fw = (length_sq(flow_diff_fw) > occ_thresh_fw).float()
fb_occ_bw = (length_sq(flow_diff_bw) > occ_thresh_bw).float()
return fb_occ_fw, fb_occ_bw # fb_occ_fw -> frame2 area occluded by frame1, fb_occ_bw -> frame1 area occluded by frame2
def rgb2gray(image):
gray_image = image[:, 0] * 0.299 + image[:, 1] * 0.587 + 0.110 * image[:, 2]
gray_image = gray_image.unsqueeze(1)
return gray_image
def ternary_transform(image, max_distance=1):
device = image.device
patch_size = 2 * max_distance + 1
intensities = rgb2gray(image) * 255
out_channels = patch_size * patch_size
w = np.eye(out_channels).reshape(out_channels, 1, patch_size, patch_size)
weights = torch.from_numpy(w).float().to(device)
patches = F.conv2d(intensities, weights, stride=1, padding=1)
transf = patches - intensities
transf_norm = transf / torch.sqrt(0.81 + torch.square(transf))
return transf_norm
def hamming_distance(t1, t2):
dist = torch.square(t1 - t2)
dist_norm = dist / (0.1 + dist)
dist_sum = torch.sum(dist_norm, dim=1, keepdim=True)
return dist_sum
def create_mask(mask, paddings):
"""
padding: [[top, bottom], [left, right]]
"""
shape = mask.shape
inner_height = shape[2] - (paddings[0][0] + paddings[0][1])
inner_width = shape[3] - (paddings[1][0] + paddings[1][1])
inner = torch.ones([inner_height, inner_width])
mask2d = F.pad(inner, pad=[paddings[1][0], paddings[1][1], paddings[0][0], paddings[0][1]])
mask3d = mask2d.unsqueeze(0)
mask4d = mask3d.unsqueeze(0).repeat(shape[0], 1, 1, 1)
return mask4d.detach()
def ternary_loss2(frame1, warp_frame21, confMask, masks, max_distance=1):
"""
Args:
frame1: torch tensor, with shape [b * t, c, h, w]
warp_frame21: torch tensor, with shape [b * t, c, h, w]
confMask: confidence mask, with shape [b * t, c, h, w]
masks: torch tensor, with shape [b * t, c, h, w]
max_distance: maximum distance.
Returns: ternary loss
"""
t1 = ternary_transform(frame1)
t21 = ternary_transform(warp_frame21)
dist = hamming_distance(t1, t21)
loss = torch.mean(dist * confMask * masks) / torch.mean(masks)
return loss
@@ -1,344 +0,0 @@
import math
from functools import reduce
import torch
import torch.nn as nn
import torch.nn.functional as F
class SoftSplit(nn.Module):
def __init__(self, channel, hidden, kernel_size, stride, padding):
super(SoftSplit, self).__init__()
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
self.t2t = nn.Unfold(kernel_size=kernel_size,
stride=stride,
padding=padding)
c_in = reduce((lambda x, y: x * y), kernel_size) * channel
self.embedding = nn.Linear(c_in, hidden)
def forward(self, x, b, output_size):
f_h = int((output_size[0] + 2 * self.padding[0] -
(self.kernel_size[0] - 1) - 1) / self.stride[0] + 1)
f_w = int((output_size[1] + 2 * self.padding[1] -
(self.kernel_size[1] - 1) - 1) / self.stride[1] + 1)
feat = self.t2t(x)
feat = feat.permute(0, 2, 1)
# feat shape [b*t, num_vec, ks*ks*c]
feat = self.embedding(feat)
# feat shape after embedding [b, t*num_vec, hidden]
feat = feat.view(b, -1, f_h, f_w, feat.size(2))
return feat
class SoftComp(nn.Module):
def __init__(self, channel, hidden, kernel_size, stride, padding):
super(SoftComp, self).__init__()
self.relu = nn.LeakyReLU(0.2, inplace=True)
c_out = reduce((lambda x, y: x * y), kernel_size) * channel
self.embedding = nn.Linear(hidden, c_out)
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
self.bias_conv = nn.Conv2d(channel,
channel,
kernel_size=3,
stride=1,
padding=1)
def forward(self, x, t, output_size):
b_, _, _, _, c_ = x.shape
x = x.view(b_, -1, c_)
feat = self.embedding(x)
b, _, c = feat.size()
feat = feat.view(b * t, -1, c).permute(0, 2, 1)
feat = F.fold(feat,
output_size=output_size,
kernel_size=self.kernel_size,
stride=self.stride,
padding=self.padding)
feat = self.bias_conv(feat)
return feat
class FusionFeedForward(nn.Module):
def __init__(self, dim, hidden_dim=1960, t2t_params=None):
super(FusionFeedForward, self).__init__()
# We set hidden_dim as a default to 1960
self.fc1 = nn.Sequential(nn.Linear(dim, hidden_dim))
self.fc2 = nn.Sequential(nn.GELU(), nn.Linear(hidden_dim, dim))
assert t2t_params is not None
self.t2t_params = t2t_params
self.kernel_shape = reduce((lambda x, y: x * y), t2t_params['kernel_size']) # 49
def forward(self, x, output_size):
n_vecs = 1
for i, d in enumerate(self.t2t_params['kernel_size']):
n_vecs *= int((output_size[i] + 2 * self.t2t_params['padding'][i] -
(d - 1) - 1) / self.t2t_params['stride'][i] + 1)
x = self.fc1(x)
b, n, c = x.size()
normalizer = x.new_ones(b, n, self.kernel_shape).view(-1, n_vecs, self.kernel_shape).permute(0, 2, 1)
normalizer = F.fold(normalizer,
output_size=output_size,
kernel_size=self.t2t_params['kernel_size'],
padding=self.t2t_params['padding'],
stride=self.t2t_params['stride'])
x = F.fold(x.view(-1, n_vecs, c).permute(0, 2, 1),
output_size=output_size,
kernel_size=self.t2t_params['kernel_size'],
padding=self.t2t_params['padding'],
stride=self.t2t_params['stride'])
x = F.unfold(x / normalizer,
kernel_size=self.t2t_params['kernel_size'],
padding=self.t2t_params['padding'],
stride=self.t2t_params['stride']).permute(
0, 2, 1).contiguous().view(b, n, c)
x = self.fc2(x)
return x
def window_partition(x, window_size, n_head):
"""
Args:
x: shape is (B, T, H, W, C)
window_size (tuple[int]): window size
Returns:
windows: (B, num_windows_h, num_windows_w, n_head, T, window_size, window_size, C//n_head)
"""
B, T, H, W, C = x.shape
x = x.view(B, T, H // window_size[0], window_size[0], W // window_size[1], window_size[1], n_head, C//n_head)
windows = x.permute(0, 2, 4, 6, 1, 3, 5, 7).contiguous()
return windows
class SparseWindowAttention(nn.Module):
def __init__(self, dim, n_head, window_size, pool_size=(4,4), qkv_bias=True, attn_drop=0., proj_drop=0.,
pooling_token=True):
super().__init__()
assert dim % n_head == 0
# key, query, value projections for all heads
self.key = nn.Linear(dim, dim, qkv_bias)
self.query = nn.Linear(dim, dim, qkv_bias)
self.value = nn.Linear(dim, dim, qkv_bias)
# regularization
self.attn_drop = nn.Dropout(attn_drop)
self.proj_drop = nn.Dropout(proj_drop)
# output projection
self.proj = nn.Linear(dim, dim)
self.n_head = n_head
self.window_size = window_size
self.pooling_token = pooling_token
if self.pooling_token:
ks, stride = pool_size, pool_size
self.pool_layer = nn.Conv2d(dim, dim, kernel_size=ks, stride=stride, padding=(0, 0), groups=dim)
self.pool_layer.weight.data.fill_(1. / (pool_size[0] * pool_size[1]))
self.pool_layer.bias.data.fill_(0)
# self.expand_size = tuple(i // 2 for i in window_size)
self.expand_size = tuple((i + 1) // 2 for i in window_size)
if any(i > 0 for i in self.expand_size):
# get mask for rolled k and rolled v
mask_tl = torch.ones(self.window_size[0], self.window_size[1])
mask_tl[:-self.expand_size[0], :-self.expand_size[1]] = 0
mask_tr = torch.ones(self.window_size[0], self.window_size[1])
mask_tr[:-self.expand_size[0], self.expand_size[1]:] = 0
mask_bl = torch.ones(self.window_size[0], self.window_size[1])
mask_bl[self.expand_size[0]:, :-self.expand_size[1]] = 0
mask_br = torch.ones(self.window_size[0], self.window_size[1])
mask_br[self.expand_size[0]:, self.expand_size[1]:] = 0
masrool_k = torch.stack((mask_tl, mask_tr, mask_bl, mask_br), 0).flatten(0)
self.register_buffer("valid_ind_rolled", masrool_k.nonzero(as_tuple=False).view(-1))
self.max_pool = nn.MaxPool2d(window_size, window_size, (0, 0))
def forward(self, x, mask=None, T_ind=None, attn_mask=None):
b, t, h, w, c = x.shape # 20 36
w_h, w_w = self.window_size[0], self.window_size[1]
c_head = c // self.n_head
n_wh = math.ceil(h / self.window_size[0])
n_ww = math.ceil(w / self.window_size[1])
new_h = n_wh * self.window_size[0] # 20
new_w = n_ww * self.window_size[1] # 36
pad_r = new_w - w
pad_b = new_h - h
# reverse order
if pad_r > 0 or pad_b > 0:
x = F.pad(x,(0, 0, 0, pad_r, 0, pad_b, 0, 0), mode='constant', value=0)
mask = F.pad(mask,(0, 0, 0, pad_r, 0, pad_b, 0, 0), mode='constant', value=0)
# calculate query, key, values for all heads in batch and move head forward to be the batch dim
q = self.query(x)
k = self.key(x)
v = self.value(x)
win_q = window_partition(q.contiguous(), self.window_size, self.n_head).view(b, n_wh*n_ww, self.n_head, t, w_h*w_w, c_head)
win_k = window_partition(k.contiguous(), self.window_size, self.n_head).view(b, n_wh*n_ww, self.n_head, t, w_h*w_w, c_head)
win_v = window_partition(v.contiguous(), self.window_size, self.n_head).view(b, n_wh*n_ww, self.n_head, t, w_h*w_w, c_head)
# roll_k and roll_v
if any(i > 0 for i in self.expand_size):
(k_tl, v_tl) = map(lambda a: torch.roll(a, shifts=(-self.expand_size[0], -self.expand_size[1]), dims=(2, 3)), (k, v))
(k_tr, v_tr) = map(lambda a: torch.roll(a, shifts=(-self.expand_size[0], self.expand_size[1]), dims=(2, 3)), (k, v))
(k_bl, v_bl) = map(lambda a: torch.roll(a, shifts=(self.expand_size[0], -self.expand_size[1]), dims=(2, 3)), (k, v))
(k_br, v_br) = map(lambda a: torch.roll(a, shifts=(self.expand_size[0], self.expand_size[1]), dims=(2, 3)), (k, v))
(k_tl_windows, k_tr_windows, k_bl_windows, k_br_windows) = map(
lambda a: window_partition(a, self.window_size, self.n_head).view(b, n_wh*n_ww, self.n_head, t, w_h*w_w, c_head),
(k_tl, k_tr, k_bl, k_br))
(v_tl_windows, v_tr_windows, v_bl_windows, v_br_windows) = map(
lambda a: window_partition(a, self.window_size, self.n_head).view(b, n_wh*n_ww, self.n_head, t, w_h*w_w, c_head),
(v_tl, v_tr, v_bl, v_br))
rool_k = torch.cat((k_tl_windows, k_tr_windows, k_bl_windows, k_br_windows), 4).contiguous()
rool_v = torch.cat((v_tl_windows, v_tr_windows, v_bl_windows, v_br_windows), 4).contiguous() # [b, n_wh*n_ww, n_head, t, w_h*w_w, c_head]
# mask out tokens in current window
rool_k = rool_k[:, :, :, :, self.valid_ind_rolled]
rool_v = rool_v[:, :, :, :, self.valid_ind_rolled]
roll_N = rool_k.shape[4]
rool_k = rool_k.view(b, n_wh*n_ww, self.n_head, t, roll_N, c // self.n_head)
rool_v = rool_v.view(b, n_wh*n_ww, self.n_head, t, roll_N, c // self.n_head)
win_k = torch.cat((win_k, rool_k), dim=4)
win_v = torch.cat((win_v, rool_v), dim=4)
else:
win_k = win_k
win_v = win_v
# pool_k and pool_v
if self.pooling_token:
pool_x = self.pool_layer(x.view(b*t, new_h, new_w, c).permute(0,3,1,2))
_, _, p_h, p_w = pool_x.shape
pool_x = pool_x.permute(0,2,3,1).view(b, t, p_h, p_w, c)
# pool_k
pool_k = self.key(pool_x).unsqueeze(1).repeat(1, n_wh*n_ww, 1, 1, 1, 1) # [b, n_wh*n_ww, t, p_h, p_w, c]
pool_k = pool_k.view(b, n_wh*n_ww, t, p_h, p_w, self.n_head, c_head).permute(0,1,5,2,3,4,6)
pool_k = pool_k.contiguous().view(b, n_wh*n_ww, self.n_head, t, p_h*p_w, c_head)
win_k = torch.cat((win_k, pool_k), dim=4)
# pool_v
pool_v = self.value(pool_x).unsqueeze(1).repeat(1, n_wh*n_ww, 1, 1, 1, 1) # [b, n_wh*n_ww, t, p_h, p_w, c]
pool_v = pool_v.view(b, n_wh*n_ww, t, p_h, p_w, self.n_head, c_head).permute(0,1,5,2,3,4,6)
pool_v = pool_v.contiguous().view(b, n_wh*n_ww, self.n_head, t, p_h*p_w, c_head)
win_v = torch.cat((win_v, pool_v), dim=4)
# [b, n_wh*n_ww, n_head, t, w_h*w_w, c_head]
out = torch.zeros_like(win_q)
l_t = mask.size(1)
mask = self.max_pool(mask.view(b * l_t, new_h, new_w))
mask = mask.view(b, l_t, n_wh*n_ww)
mask = torch.sum(mask, dim=1) # [b, n_wh*n_ww]
for i in range(win_q.shape[0]):
### For masked windows
mask_ind_i = mask[i].nonzero(as_tuple=False).view(-1)
# mask out quary in current window
# [b, n_wh*n_ww, n_head, t, w_h*w_w, c_head]
mask_n = len(mask_ind_i)
if mask_n > 0:
win_q_t = win_q[i, mask_ind_i].view(mask_n, self.n_head, t*w_h*w_w, c_head)
win_k_t = win_k[i, mask_ind_i]
win_v_t = win_v[i, mask_ind_i]
# mask out key and value
if T_ind is not None:
# key [n_wh*n_ww, n_head, t, w_h*w_w, c_head]
win_k_t = win_k_t[:, :, T_ind.view(-1)].view(mask_n, self.n_head, -1, c_head)
# value
win_v_t = win_v_t[:, :, T_ind.view(-1)].view(mask_n, self.n_head, -1, c_head)
else:
win_k_t = win_k_t.view(n_wh*n_ww, self.n_head, t*w_h*w_w, c_head)
win_v_t = win_v_t.view(n_wh*n_ww, self.n_head, t*w_h*w_w, c_head)
att_t = (win_q_t @ win_k_t.transpose(-2, -1)) * (1.0 / math.sqrt(win_q_t.size(-1)))
att_t = F.softmax(att_t, dim=-1)
att_t = self.attn_drop(att_t)
y_t = att_t @ win_v_t
out[i, mask_ind_i] = y_t.view(-1, self.n_head, t, w_h*w_w, c_head)
### For unmasked windows
unmask_ind_i = (mask[i] == 0).nonzero(as_tuple=False).view(-1)
# mask out quary in current window
# [b, n_wh*n_ww, n_head, t, w_h*w_w, c_head]
win_q_s = win_q[i, unmask_ind_i]
win_k_s = win_k[i, unmask_ind_i, :, :, :w_h*w_w]
win_v_s = win_v[i, unmask_ind_i, :, :, :w_h*w_w]
att_s = (win_q_s @ win_k_s.transpose(-2, -1)) * (1.0 / math.sqrt(win_q_s.size(-1)))
att_s = F.softmax(att_s, dim=-1)
att_s = self.attn_drop(att_s)
y_s = att_s @ win_v_s
out[i, unmask_ind_i] = y_s
# re-assemble all head outputs side by side
out = out.view(b, n_wh, n_ww, self.n_head, t, w_h, w_w, c_head)
out = out.permute(0, 4, 1, 5, 2, 6, 3, 7).contiguous().view(b, t, new_h, new_w, c)
if pad_r > 0 or pad_b > 0:
out = out[:, :, :h, :w, :]
# output projection
out = self.proj_drop(self.proj(out))
return out
class TemporalSparseTransformer(nn.Module):
def __init__(self, dim, n_head, window_size, pool_size,
norm_layer=nn.LayerNorm, t2t_params=None):
super().__init__()
self.window_size = window_size
self.attention = SparseWindowAttention(dim, n_head, window_size, pool_size)
self.norm1 = norm_layer(dim)
self.norm2 = norm_layer(dim)
self.mlp = FusionFeedForward(dim, t2t_params=t2t_params)
def forward(self, x, fold_x_size, mask=None, T_ind=None):
"""
Args:
x: image tokens, shape [B T H W C]
fold_x_size: fold feature size, shape [60 108]
mask: mask tokens, shape [B T H W 1]
Returns:
out_tokens: shape [B T H W C]
"""
B, T, H, W, C = x.shape # 20 36
shortcut = x
x = self.norm1(x)
att_x = self.attention(x, mask, T_ind)
# FFN
x = shortcut + att_x
y = self.norm2(x)
x = x + self.mlp(y.view(B, T * H * W, C), fold_x_size).view(B, T, H, W, C)
return x
class TemporalSparseTransformerBlock(nn.Module):
def __init__(self, dim, n_head, window_size, pool_size, depths, t2t_params=None):
super().__init__()
blocks = []
for i in range(depths):
blocks.append(
TemporalSparseTransformer(dim, n_head, window_size, pool_size, t2t_params=t2t_params)
)
self.transformer = nn.Sequential(*blocks)
self.depths = depths
def forward(self, x, fold_x_size, l_mask=None, t_dilation=2):
"""
Args:
x: image tokens, shape [B T H W C]
fold_x_size: fold feature size, shape [60 108]
l_mask: local mask tokens, shape [B T H W 1]
Returns:
out_tokens: shape [B T H W C]
"""
assert self.depths % t_dilation == 0, 'wrong t_dilation input.'
T = x.size(1)
T_ind = [torch.arange(i, T, t_dilation) for i in range(t_dilation)] * (self.depths // t_dilation)
for i in range(0, self.depths):
x = self.transformer[i](x, fold_x_size, l_mask, T_ind[i])
return x
@@ -1,288 +0,0 @@
"""
Spectral Normalization from https://arxiv.org/abs/1802.05957
"""
import torch
from torch.nn.functional import normalize
class SpectralNorm(object):
# Invariant before and after each forward call:
# u = normalize(W @ v)
# NB: At initialization, this invariant is not enforced
_version = 1
# At version 1:
# made `W` not a buffer,
# added `v` as a buffer, and
# made eval mode use `W = u @ W_orig @ v` rather than the stored `W`.
def __init__(self, name='weight', n_power_iterations=1, dim=0, eps=1e-12):
self.name = name
self.dim = dim
if n_power_iterations <= 0:
raise ValueError(
'Expected n_power_iterations to be positive, but '
'got n_power_iterations={}'.format(n_power_iterations))
self.n_power_iterations = n_power_iterations
self.eps = eps
def reshape_weight_to_matrix(self, weight):
weight_mat = weight
if self.dim != 0:
# permute dim to front
weight_mat = weight_mat.permute(
self.dim,
*[d for d in range(weight_mat.dim()) if d != self.dim])
height = weight_mat.size(0)
return weight_mat.reshape(height, -1)
def compute_weight(self, module, do_power_iteration):
# NB: If `do_power_iteration` is set, the `u` and `v` vectors are
# updated in power iteration **in-place**. This is very important
# because in `DataParallel` forward, the vectors (being buffers) are
# broadcast from the parallelized module to each module replica,
# which is a new module object created on the fly. And each replica
# runs its own spectral norm power iteration. So simply assigning
# the updated vectors to the module this function runs on will cause
# the update to be lost forever. And the next time the parallelized
# module is replicated, the same randomly initialized vectors are
# broadcast and used!
#
# Therefore, to make the change propagate back, we rely on two
# important behaviors (also enforced via tests):
# 1. `DataParallel` doesn't clone storage if the broadcast tensor
# is already on correct device; and it makes sure that the
# parallelized module is already on `device[0]`.
# 2. If the out tensor in `out=` kwarg has correct shape, it will
# just fill in the values.
# Therefore, since the same power iteration is performed on all
# devices, simply updating the tensors in-place will make sure that
# the module replica on `device[0]` will update the _u vector on the
# parallized module (by shared storage).
#
# However, after we update `u` and `v` in-place, we need to **clone**
# them before using them to normalize the weight. This is to support
# backproping through two forward passes, e.g., the common pattern in
# GAN training: loss = D(real) - D(fake). Otherwise, engine will
# complain that variables needed to do backward for the first forward
# (i.e., the `u` and `v` vectors) are changed in the second forward.
weight = getattr(module, self.name + '_orig')
u = getattr(module, self.name + '_u')
v = getattr(module, self.name + '_v')
weight_mat = self.reshape_weight_to_matrix(weight)
if do_power_iteration:
with torch.no_grad():
for _ in range(self.n_power_iterations):
# Spectral norm of weight equals to `u^T W v`, where `u` and `v`
# are the first left and right singular vectors.
# This power iteration produces approximations of `u` and `v`.
v = normalize(torch.mv(weight_mat.t(), u),
dim=0,
eps=self.eps,
out=v)
u = normalize(torch.mv(weight_mat, v),
dim=0,
eps=self.eps,
out=u)
if self.n_power_iterations > 0:
# See above on why we need to clone
u = u.clone()
v = v.clone()
sigma = torch.dot(u, torch.mv(weight_mat, v))
weight = weight / sigma
return weight
def remove(self, module):
with torch.no_grad():
weight = self.compute_weight(module, do_power_iteration=False)
delattr(module, self.name)
delattr(module, self.name + '_u')
delattr(module, self.name + '_v')
delattr(module, self.name + '_orig')
module.register_parameter(self.name,
torch.nn.Parameter(weight.detach()))
def __call__(self, module, inputs):
setattr(
module, self.name,
self.compute_weight(module, do_power_iteration=module.training))
def _solve_v_and_rescale(self, weight_mat, u, target_sigma):
# Tries to returns a vector `v` s.t. `u = normalize(W @ v)`
# (the invariant at top of this class) and `u @ W @ v = sigma`.
# This uses pinverse in case W^T W is not invertible.
v = torch.chain_matmul(weight_mat.t().mm(weight_mat).pinverse(),
weight_mat.t(), u.unsqueeze(1)).squeeze(1)
return v.mul_(target_sigma / torch.dot(u, torch.mv(weight_mat, v)))
@staticmethod
def apply(module, name, n_power_iterations, dim, eps):
for k, hook in module._forward_pre_hooks.items():
if isinstance(hook, SpectralNorm) and hook.name == name:
raise RuntimeError(
"Cannot register two spectral_norm hooks on "
"the same parameter {}".format(name))
fn = SpectralNorm(name, n_power_iterations, dim, eps)
weight = module._parameters[name]
with torch.no_grad():
weight_mat = fn.reshape_weight_to_matrix(weight)
h, w = weight_mat.size()
# randomly initialize `u` and `v`
u = normalize(weight.new_empty(h).normal_(0, 1), dim=0, eps=fn.eps)
v = normalize(weight.new_empty(w).normal_(0, 1), dim=0, eps=fn.eps)
delattr(module, fn.name)
module.register_parameter(fn.name + "_orig", weight)
# We still need to assign weight back as fn.name because all sorts of
# things may assume that it exists, e.g., when initializing weights.
# However, we can't directly assign as it could be an nn.Parameter and
# gets added as a parameter. Instead, we register weight.data as a plain
# attribute.
setattr(module, fn.name, weight.data)
module.register_buffer(fn.name + "_u", u)
module.register_buffer(fn.name + "_v", v)
module.register_forward_pre_hook(fn)
module._register_state_dict_hook(SpectralNormStateDictHook(fn))
module._register_load_state_dict_pre_hook(
SpectralNormLoadStateDictPreHook(fn))
return fn
# This is a top level class because Py2 pickle doesn't like inner class nor an
# instancemethod.
class SpectralNormLoadStateDictPreHook(object):
# See docstring of SpectralNorm._version on the changes to spectral_norm.
def __init__(self, fn):
self.fn = fn
# For state_dict with version None, (assuming that it has gone through at
# least one training forward), we have
#
# u = normalize(W_orig @ v)
# W = W_orig / sigma, where sigma = u @ W_orig @ v
#
# To compute `v`, we solve `W_orig @ x = u`, and let
# v = x / (u @ W_orig @ x) * (W / W_orig).
def __call__(self, state_dict, prefix, local_metadata, strict,
missing_keys, unexpected_keys, error_msgs):
fn = self.fn
version = local_metadata.get('spectral_norm',
{}).get(fn.name + '.version', None)
if version is None or version < 1:
with torch.no_grad():
weight_orig = state_dict[prefix + fn.name + '_orig']
# weight = state_dict.pop(prefix + fn.name)
# sigma = (weight_orig / weight).mean()
weight_mat = fn.reshape_weight_to_matrix(weight_orig)
u = state_dict[prefix + fn.name + '_u']
# v = fn._solve_v_and_rescale(weight_mat, u, sigma)
# state_dict[prefix + fn.name + '_v'] = v
# This is a top level class because Py2 pickle doesn't like inner class nor an
# instancemethod.
class SpectralNormStateDictHook(object):
# See docstring of SpectralNorm._version on the changes to spectral_norm.
def __init__(self, fn):
self.fn = fn
def __call__(self, module, state_dict, prefix, local_metadata):
if 'spectral_norm' not in local_metadata:
local_metadata['spectral_norm'] = {}
key = self.fn.name + '.version'
if key in local_metadata['spectral_norm']:
raise RuntimeError(
"Unexpected key in metadata['spectral_norm']: {}".format(key))
local_metadata['spectral_norm'][key] = self.fn._version
def spectral_norm(module,
name='weight',
n_power_iterations=1,
eps=1e-12,
dim=None):
r"""Applies spectral normalization to a parameter in the given module.
.. math::
\mathbf{W}_{SN} = \dfrac{\mathbf{W}}{\sigma(\mathbf{W})},
\sigma(\mathbf{W}) = \max_{\mathbf{h}: \mathbf{h} \ne 0} \dfrac{\|\mathbf{W} \mathbf{h}\|_2}{\|\mathbf{h}\|_2}
Spectral normalization stabilizes the training of discriminators (critics)
in Generative Adversarial Networks (GANs) by rescaling the weight tensor
with spectral norm :math:`\sigma` of the weight matrix calculated using
power iteration method. If the dimension of the weight tensor is greater
than 2, it is reshaped to 2D in power iteration method to get spectral
norm. This is implemented via a hook that calculates spectral norm and
rescales weight before every :meth:`~Module.forward` call.
See `Spectral Normalization for Generative Adversarial Networks`_ .
.. _`Spectral Normalization for Generative Adversarial Networks`: https://arxiv.org/abs/1802.05957
Args:
module (nn.Module): containing module
name (str, optional): name of weight parameter
n_power_iterations (int, optional): number of power iterations to
calculate spectral norm
eps (float, optional): epsilon for numerical stability in
calculating norms
dim (int, optional): dimension corresponding to number of outputs,
the default is ``0``, except for modules that are instances of
ConvTranspose{1,2,3}d, when it is ``1``
Returns:
The original module with the spectral norm hook
Example::
>>> m = spectral_norm(nn.Linear(20, 40))
>>> m
Linear(in_features=20, out_features=40, bias=True)
>>> m.weight_u.size()
torch.Size([40])
"""
if dim is None:
if isinstance(module,
(torch.nn.ConvTranspose1d, torch.nn.ConvTranspose2d,
torch.nn.ConvTranspose3d)):
dim = 1
else:
dim = 0
SpectralNorm.apply(module, name, n_power_iterations, dim, eps)
return module
def remove_spectral_norm(module, name='weight'):
r"""Removes the spectral normalization reparameterization from a module.
Args:
module (Module): containing module
name (str, optional): name of weight parameter
Example:
>>> m = spectral_norm(nn.Linear(40, 10))
>>> remove_spectral_norm(m)
"""
for k, hook in module._forward_pre_hooks.items():
if isinstance(hook, SpectralNorm) and hook.name == name:
hook.remove(module)
del module._forward_pre_hooks[k]
return module
raise ValueError("spectral_norm of '{}' not found in {}".format(
name, module))
def use_spectral_norm(module, use_sn=False):
if use_sn:
return spectral_norm(module)
return module
-539
View File
@@ -1,539 +0,0 @@
''' Towards An End-to-End Framework for Video Inpainting
'''
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from einops import rearrange
from backend.inpaint.video.model.modules.base_module import BaseNetwork
from backend.inpaint.video.model.modules.sparse_transformer import TemporalSparseTransformerBlock, SoftSplit, SoftComp
from backend.inpaint.video.model.modules.spectral_norm import spectral_norm as _spectral_norm
from backend.inpaint.video.model.modules.flow_loss_utils import flow_warp
from backend.inpaint.video.model.modules.deformconv import ModulatedDeformConv2d
from .misc import constant_init
def length_sq(x):
return torch.sum(torch.square(x), dim=1, keepdim=True)
def fbConsistencyCheck(flow_fw, flow_bw, alpha1=0.01, alpha2=0.5):
flow_bw_warped = flow_warp(flow_bw, flow_fw.permute(0, 2, 3, 1)) # wb(wf(x))
flow_diff_fw = flow_fw + flow_bw_warped # wf + wb(wf(x))
mag_sq_fw = length_sq(flow_fw) + length_sq(flow_bw_warped) # |wf| + |wb(wf(x))|
occ_thresh_fw = alpha1 * mag_sq_fw + alpha2
# fb_valid_fw = (length_sq(flow_diff_fw) < occ_thresh_fw).float()
fb_valid_fw = (length_sq(flow_diff_fw) < occ_thresh_fw).to(flow_fw)
return fb_valid_fw
class DeformableAlignment(ModulatedDeformConv2d):
"""Second-order deformable alignment module."""
def __init__(self, *args, **kwargs):
# self.max_residue_magnitude = kwargs.pop('max_residue_magnitude', 10)
self.max_residue_magnitude = kwargs.pop('max_residue_magnitude', 3)
super(DeformableAlignment, self).__init__(*args, **kwargs)
self.conv_offset = nn.Sequential(
nn.Conv2d(2 * self.out_channels + 2 + 1 + 2, self.out_channels, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(self.out_channels, self.out_channels, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(self.out_channels, self.out_channels, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(self.out_channels, 27 * self.deform_groups, 3, 1, 1),
)
self.init_offset()
def init_offset(self):
constant_init(self.conv_offset[-1], val=0, bias=0)
def forward(self, x, cond_feat, flow):
out = self.conv_offset(cond_feat)
o1, o2, mask = torch.chunk(out, 3, dim=1)
# offset
offset = self.max_residue_magnitude * torch.tanh(torch.cat((o1, o2), dim=1))
offset = offset + flow.flip(1).repeat(1, offset.size(1) // 2, 1, 1)
# mask
mask = torch.sigmoid(mask)
return torchvision.ops.deform_conv2d(x, offset, self.weight, self.bias,
self.stride, self.padding,
self.dilation, mask)
class BidirectionalPropagation(nn.Module):
def __init__(self, channel, learnable=True):
super(BidirectionalPropagation, self).__init__()
self.deform_align = nn.ModuleDict()
self.backbone = nn.ModuleDict()
self.channel = channel
self.prop_list = ['backward_1', 'forward_1']
self.learnable = learnable
if self.learnable:
for i, module in enumerate(self.prop_list):
self.deform_align[module] = DeformableAlignment(
channel, channel, 3, padding=1, deform_groups=16)
self.backbone[module] = nn.Sequential(
nn.Conv2d(2 * channel + 2, channel, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
nn.Conv2d(channel, channel, 3, 1, 1),
)
self.fuse = nn.Sequential(
nn.Conv2d(2 * channel + 2, channel, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
nn.Conv2d(channel, channel, 3, 1, 1),
)
def binary_mask(self, mask, th=0.1):
mask[mask > th] = 1
mask[mask <= th] = 0
# return mask.float()
return mask.to(mask)
def forward(self, x, flows_forward, flows_backward, mask, interpolation='bilinear'):
"""
x shape : [b, t, c, h, w]
return [b, t, c, h, w]
"""
# For backward warping
# pred_flows_forward for backward feature propagation
# pred_flows_backward for forward feature propagation
b, t, c, h, w = x.shape
feats, masks = {}, {}
feats['input'] = [x[:, i, :, :, :] for i in range(0, t)]
masks['input'] = [mask[:, i, :, :, :] for i in range(0, t)]
prop_list = ['backward_1', 'forward_1']
cache_list = ['input'] + prop_list
for p_i, module_name in enumerate(prop_list):
feats[module_name] = []
masks[module_name] = []
if 'backward' in module_name:
frame_idx = range(0, t)
frame_idx = frame_idx[::-1]
flow_idx = frame_idx
flows_for_prop = flows_forward
flows_for_check = flows_backward
else:
frame_idx = range(0, t)
flow_idx = range(-1, t - 1)
flows_for_prop = flows_backward
flows_for_check = flows_forward
for i, idx in enumerate(frame_idx):
feat_current = feats[cache_list[p_i]][idx]
mask_current = masks[cache_list[p_i]][idx]
if i == 0:
feat_prop = feat_current
mask_prop = mask_current
else:
flow_prop = flows_for_prop[:, flow_idx[i], :, :, :]
flow_check = flows_for_check[:, flow_idx[i], :, :, :]
flow_vaild_mask = fbConsistencyCheck(flow_prop, flow_check)
feat_warped = flow_warp(feat_prop, flow_prop.permute(0, 2, 3, 1), interpolation)
if self.learnable:
cond = torch.cat([feat_current, feat_warped, flow_prop, flow_vaild_mask, mask_current], dim=1)
feat_prop = self.deform_align[module_name](feat_prop, cond, flow_prop)
mask_prop = mask_current
else:
mask_prop_valid = flow_warp(mask_prop, flow_prop.permute(0, 2, 3, 1))
mask_prop_valid = self.binary_mask(mask_prop_valid)
union_vaild_mask = self.binary_mask(mask_current * flow_vaild_mask * (1 - mask_prop_valid))
feat_prop = union_vaild_mask * feat_warped + (1 - union_vaild_mask) * feat_current
# update mask
mask_prop = self.binary_mask(mask_current * (1 - (flow_vaild_mask * (1 - mask_prop_valid))))
# refine
if self.learnable:
feat = torch.cat([feat_current, feat_prop, mask_current], dim=1)
feat_prop = feat_prop + self.backbone[module_name](feat)
# feat_prop = self.backbone[module_name](feat_prop)
feats[module_name].append(feat_prop)
masks[module_name].append(mask_prop)
# end for
if 'backward' in module_name:
feats[module_name] = feats[module_name][::-1]
masks[module_name] = masks[module_name][::-1]
outputs_b = torch.stack(feats['backward_1'], dim=1).view(-1, c, h, w)
outputs_f = torch.stack(feats['forward_1'], dim=1).view(-1, c, h, w)
if self.learnable:
mask_in = mask.view(-1, 2, h, w)
masks_b, masks_f = None, None
outputs = self.fuse(torch.cat([outputs_b, outputs_f, mask_in], dim=1)) + x.view(-1, c, h, w)
else:
masks_b = torch.stack(masks['backward_1'], dim=1)
masks_f = torch.stack(masks['forward_1'], dim=1)
outputs = outputs_f
return outputs_b.view(b, -1, c, h, w), outputs_f.view(b, -1, c, h, w), \
outputs.view(b, -1, c, h, w), masks_f
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
self.group = [1, 2, 4, 8, 1]
self.layers = nn.ModuleList([
nn.Conv2d(5, 64, kernel_size=3, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1, groups=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(640, 512, kernel_size=3, stride=1, padding=1, groups=2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(768, 384, kernel_size=3, stride=1, padding=1, groups=4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(640, 256, kernel_size=3, stride=1, padding=1, groups=8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(512, 128, kernel_size=3, stride=1, padding=1, groups=1),
nn.LeakyReLU(0.2, inplace=True)
])
def forward(self, x):
bt, c, _, _ = x.size()
# h, w = h//4, w//4
out = x
for i, layer in enumerate(self.layers):
if i == 8:
x0 = out
_, _, h, w = x0.size()
if i > 8 and i % 2 == 0:
g = self.group[(i - 8) // 2]
x = x0.view(bt, g, -1, h, w)
o = out.view(bt, g, -1, h, w)
out = torch.cat([x, o], 2).view(bt, -1, h, w)
out = layer(out)
return out
class deconv(nn.Module):
def __init__(self,
input_channel,
output_channel,
kernel_size=3,
padding=0):
super().__init__()
self.conv = nn.Conv2d(input_channel,
output_channel,
kernel_size=kernel_size,
stride=1,
padding=padding)
def forward(self, x):
x = F.interpolate(x,
scale_factor=2,
mode='bilinear',
align_corners=True)
return self.conv(x)
class InpaintGenerator(BaseNetwork):
def __init__(self, init_weights=True, model_path=None):
super(InpaintGenerator, self).__init__()
channel = 128
hidden = 512
# encoder
self.encoder = Encoder()
# decoder
self.decoder = nn.Sequential(
deconv(channel, 128, kernel_size=3, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(128, 64, kernel_size=3, stride=1, padding=1),
nn.LeakyReLU(0.2, inplace=True),
deconv(64, 64, kernel_size=3, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 3, kernel_size=3, stride=1, padding=1))
# soft split and soft composition
kernel_size = (7, 7)
padding = (3, 3)
stride = (3, 3)
t2t_params = {
'kernel_size': kernel_size,
'stride': stride,
'padding': padding
}
self.ss = SoftSplit(channel, hidden, kernel_size, stride, padding)
self.sc = SoftComp(channel, hidden, kernel_size, stride, padding)
self.max_pool = nn.MaxPool2d(kernel_size, stride, padding)
# feature propagation module
self.img_prop_module = BidirectionalPropagation(3, learnable=False)
self.feat_prop_module = BidirectionalPropagation(128, learnable=True)
depths = 8
num_heads = 4
window_size = (5, 9)
pool_size = (4, 4)
self.transformers = TemporalSparseTransformerBlock(dim=hidden,
n_head=num_heads,
window_size=window_size,
pool_size=pool_size,
depths=depths,
t2t_params=t2t_params)
if init_weights:
self.init_weights()
if model_path is not None:
print('Pretrained ProPainter has loaded...')
ckpt = torch.load(model_path, map_location='cpu')
self.load_state_dict(ckpt, strict=True)
# print network parameter number
self.print_network()
def img_propagation(self, masked_frames, completed_flows, masks, interpolation='nearest'):
_, _, prop_frames, updated_masks = self.img_prop_module(masked_frames, completed_flows[0], completed_flows[1],
masks, interpolation)
return prop_frames, updated_masks
def forward(self, masked_frames, completed_flows, masks_in, masks_updated, num_local_frames,
interpolation='bilinear', t_dilation=2):
"""
Args:
masks_in: original mask
masks_updated: updated mask after image propagation
"""
l_t = num_local_frames
b, t, _, ori_h, ori_w = masked_frames.size()
# extracting features
enc_feat = self.encoder(torch.cat([masked_frames.view(b * t, 3, ori_h, ori_w),
masks_in.view(b * t, 1, ori_h, ori_w),
masks_updated.view(b * t, 1, ori_h, ori_w)], dim=1))
_, c, h, w = enc_feat.size()
local_feat = enc_feat.view(b, t, c, h, w)[:, :l_t, ...]
ref_feat = enc_feat.view(b, t, c, h, w)[:, l_t:, ...]
fold_feat_size = (h, w)
ds_flows_f = F.interpolate(completed_flows[0].view(-1, 2, ori_h, ori_w), scale_factor=1 / 4, mode='bilinear',
align_corners=False).view(b, l_t - 1, 2, h, w) / 4.0
ds_flows_b = F.interpolate(completed_flows[1].view(-1, 2, ori_h, ori_w), scale_factor=1 / 4, mode='bilinear',
align_corners=False).view(b, l_t - 1, 2, h, w) / 4.0
ds_mask_in = F.interpolate(masks_in.reshape(-1, 1, ori_h, ori_w), scale_factor=1 / 4, mode='nearest').view(b, t,
1, h,
w)
ds_mask_in_local = ds_mask_in[:, :l_t]
ds_mask_updated_local = F.interpolate(masks_updated[:, :l_t].reshape(-1, 1, ori_h, ori_w), scale_factor=1 / 4,
mode='nearest').view(b, l_t, 1, h, w)
if self.training:
mask_pool_l = self.max_pool(ds_mask_in.view(-1, 1, h, w))
mask_pool_l = mask_pool_l.view(b, t, 1, mask_pool_l.size(-2), mask_pool_l.size(-1))
else:
mask_pool_l = self.max_pool(ds_mask_in_local.view(-1, 1, h, w))
mask_pool_l = mask_pool_l.view(b, l_t, 1, mask_pool_l.size(-2), mask_pool_l.size(-1))
prop_mask_in = torch.cat([ds_mask_in_local, ds_mask_updated_local], dim=2)
_, _, local_feat, _ = self.feat_prop_module(local_feat, ds_flows_f, ds_flows_b, prop_mask_in, interpolation)
enc_feat = torch.cat((local_feat, ref_feat), dim=1)
trans_feat = self.ss(enc_feat.view(-1, c, h, w), b, fold_feat_size)
mask_pool_l = rearrange(mask_pool_l, 'b t c h w -> b t h w c').contiguous()
trans_feat = self.transformers(trans_feat, fold_feat_size, mask_pool_l, t_dilation=t_dilation)
trans_feat = self.sc(trans_feat, t, fold_feat_size)
trans_feat = trans_feat.view(b, t, -1, h, w)
enc_feat = enc_feat + trans_feat
if self.training:
output = self.decoder(enc_feat.view(-1, c, h, w))
output = torch.tanh(output).view(b, t, 3, ori_h, ori_w)
else:
output = self.decoder(enc_feat[:, :l_t].view(-1, c, h, w))
output = torch.tanh(output).view(b, l_t, 3, ori_h, ori_w)
return output
# ######################################################################
# Discriminator for Temporal Patch GAN
# ######################################################################
class Discriminator(BaseNetwork):
def __init__(self,
in_channels=3,
use_sigmoid=False,
use_spectral_norm=True,
init_weights=True):
super(Discriminator, self).__init__()
self.use_sigmoid = use_sigmoid
nf = 32
self.conv = nn.Sequential(
spectral_norm(
nn.Conv3d(in_channels=in_channels,
out_channels=nf * 1,
kernel_size=(3, 5, 5),
stride=(1, 2, 2),
padding=1,
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(64, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 1,
nf * 2,
kernel_size=(3, 5, 5),
stride=(1, 2, 2),
padding=(1, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(128, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 2,
nf * 4,
kernel_size=(3, 5, 5),
stride=(1, 2, 2),
padding=(1, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(256, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 4,
nf * 4,
kernel_size=(3, 5, 5),
stride=(1, 2, 2),
padding=(1, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(256, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 4,
nf * 4,
kernel_size=(3, 5, 5),
stride=(1, 2, 2),
padding=(1, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(256, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv3d(nf * 4,
nf * 4,
kernel_size=(3, 5, 5),
stride=(1, 2, 2),
padding=(1, 2, 2)))
if init_weights:
self.init_weights()
def forward(self, xs):
# T, C, H, W = xs.shape (old)
# B, T, C, H, W (new)
xs_t = torch.transpose(xs, 1, 2)
feat = self.conv(xs_t)
if self.use_sigmoid:
feat = torch.sigmoid(feat)
out = torch.transpose(feat, 1, 2) # B, T, C, H, W
return out
class Discriminator_2D(BaseNetwork):
def __init__(self,
in_channels=3,
use_sigmoid=False,
use_spectral_norm=True,
init_weights=True):
super(Discriminator_2D, self).__init__()
self.use_sigmoid = use_sigmoid
nf = 32
self.conv = nn.Sequential(
spectral_norm(
nn.Conv3d(in_channels=in_channels,
out_channels=nf * 1,
kernel_size=(1, 5, 5),
stride=(1, 2, 2),
padding=(0, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(64, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 1,
nf * 2,
kernel_size=(1, 5, 5),
stride=(1, 2, 2),
padding=(0, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(128, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 2,
nf * 4,
kernel_size=(1, 5, 5),
stride=(1, 2, 2),
padding=(0, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(256, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 4,
nf * 4,
kernel_size=(1, 5, 5),
stride=(1, 2, 2),
padding=(0, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(256, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(
nn.Conv3d(nf * 4,
nf * 4,
kernel_size=(1, 5, 5),
stride=(1, 2, 2),
padding=(0, 2, 2),
bias=not use_spectral_norm), use_spectral_norm),
# nn.InstanceNorm2d(256, track_running_stats=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv3d(nf * 4,
nf * 4,
kernel_size=(1, 5, 5),
stride=(1, 2, 2),
padding=(0, 2, 2)))
if init_weights:
self.init_weights()
def forward(self, xs):
# T, C, H, W = xs.shape (old)
# B, T, C, H, W (new)
xs_t = torch.transpose(xs, 1, 2)
feat = self.conv(xs_t)
if self.use_sigmoid:
feat = torch.sigmoid(feat)
out = torch.transpose(feat, 1, 2) # B, T, C, H, W
return out
def spectral_norm(module, mode=True):
if mode:
return _spectral_norm(module)
return module
@@ -1,348 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from backend.inpaint.video.model.modules.deformconv import ModulatedDeformConv2d
from .misc import constant_init
class SecondOrderDeformableAlignment(ModulatedDeformConv2d):
"""Second-order deformable alignment module."""
def __init__(self, *args, **kwargs):
self.max_residue_magnitude = kwargs.pop('max_residue_magnitude', 5)
super(SecondOrderDeformableAlignment, self).__init__(*args, **kwargs)
self.conv_offset = nn.Sequential(
nn.Conv2d(3 * self.out_channels, self.out_channels, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(self.out_channels, self.out_channels, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(self.out_channels, self.out_channels, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(self.out_channels, 27 * self.deform_groups, 3, 1, 1),
)
self.init_offset()
def init_offset(self):
constant_init(self.conv_offset[-1], val=0, bias=0)
def forward(self, x, extra_feat):
out = self.conv_offset(extra_feat)
o1, o2, mask = torch.chunk(out, 3, dim=1)
# offset
offset = self.max_residue_magnitude * torch.tanh(torch.cat((o1, o2), dim=1))
offset_1, offset_2 = torch.chunk(offset, 2, dim=1)
offset = torch.cat([offset_1, offset_2], dim=1)
# mask
mask = torch.sigmoid(mask)
return torchvision.ops.deform_conv2d(x, offset, self.weight, self.bias,
self.stride, self.padding,
self.dilation, mask)
class BidirectionalPropagation(nn.Module):
def __init__(self, channel):
super(BidirectionalPropagation, self).__init__()
modules = ['backward_', 'forward_']
self.deform_align = nn.ModuleDict()
self.backbone = nn.ModuleDict()
self.channel = channel
for i, module in enumerate(modules):
self.deform_align[module] = SecondOrderDeformableAlignment(
2 * channel, channel, 3, padding=1, deform_groups=16)
self.backbone[module] = nn.Sequential(
nn.Conv2d((2 + i) * channel, channel, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.1, inplace=True),
nn.Conv2d(channel, channel, 3, 1, 1),
)
self.fusion = nn.Conv2d(2 * channel, channel, 1, 1, 0)
def forward(self, x):
"""
x shape : [b, t, c, h, w]
return [b, t, c, h, w]
"""
b, t, c, h, w = x.shape
feats = {}
feats['spatial'] = [x[:, i, :, :, :] for i in range(0, t)]
for module_name in ['backward_', 'forward_']:
feats[module_name] = []
frame_idx = range(0, t)
mapping_idx = list(range(0, len(feats['spatial'])))
mapping_idx += mapping_idx[::-1]
if 'backward' in module_name:
frame_idx = frame_idx[::-1]
feat_prop = x.new_zeros(b, self.channel, h, w)
for i, idx in enumerate(frame_idx):
feat_current = feats['spatial'][mapping_idx[idx]]
if i > 0:
cond_n1 = feat_prop
# initialize second-order features
feat_n2 = torch.zeros_like(feat_prop)
cond_n2 = torch.zeros_like(cond_n1)
if i > 1: # second-order features
feat_n2 = feats[module_name][-2]
cond_n2 = feat_n2
cond = torch.cat([cond_n1, feat_current, cond_n2],
dim=1) # condition information, cond(flow warped 1st/2nd feature)
feat_prop = torch.cat([feat_prop, feat_n2], dim=1) # two order feat_prop -1 & -2
feat_prop = self.deform_align[module_name](feat_prop, cond)
# fuse current features
feat = [feat_current] + \
[feats[k][idx] for k in feats if k not in ['spatial', module_name]] \
+ [feat_prop]
feat = torch.cat(feat, dim=1)
# embed current features
feat_prop = feat_prop + self.backbone[module_name](feat)
feats[module_name].append(feat_prop)
# end for
if 'backward' in module_name:
feats[module_name] = feats[module_name][::-1]
outputs = []
for i in range(0, t):
align_feats = [feats[k].pop(0) for k in feats if k != 'spatial']
align_feats = torch.cat(align_feats, dim=1)
outputs.append(self.fusion(align_feats))
return torch.stack(outputs, dim=1) + x
class deconv(nn.Module):
def __init__(self,
input_channel,
output_channel,
kernel_size=3,
padding=0):
super().__init__()
self.conv = nn.Conv2d(input_channel,
output_channel,
kernel_size=kernel_size,
stride=1,
padding=padding)
def forward(self, x):
x = F.interpolate(x,
scale_factor=2,
mode='bilinear',
align_corners=True)
return self.conv(x)
class P3DBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding, use_residual=0, bias=True):
super().__init__()
self.conv1 = nn.Sequential(
nn.Conv3d(in_channels, out_channels, kernel_size=(1, kernel_size, kernel_size),
stride=(1, stride, stride), padding=(0, padding, padding), bias=bias),
nn.LeakyReLU(0.2, inplace=True)
)
self.conv2 = nn.Sequential(
nn.Conv3d(out_channels, out_channels, kernel_size=(3, 1, 1), stride=(1, 1, 1),
padding=(2, 0, 0), dilation=(2, 1, 1), bias=bias)
)
self.use_residual = use_residual
def forward(self, feats):
feat1 = self.conv1(feats)
feat2 = self.conv2(feat1)
if self.use_residual:
output = feats + feat2
else:
output = feat2
return output
class EdgeDetection(nn.Module):
def __init__(self, in_ch=2, out_ch=1, mid_ch=16):
super().__init__()
self.projection = nn.Sequential(
nn.Conv2d(in_ch, mid_ch, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True)
)
self.mid_layer_1 = nn.Sequential(
nn.Conv2d(mid_ch, mid_ch, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True)
)
self.mid_layer_2 = nn.Sequential(
nn.Conv2d(mid_ch, mid_ch, 3, 1, 1)
)
self.l_relu = nn.LeakyReLU(0.01, inplace=True)
self.out_layer = nn.Conv2d(mid_ch, out_ch, 1, 1, 0)
def forward(self, flow):
flow = self.projection(flow)
edge = self.mid_layer_1(flow)
edge = self.mid_layer_2(edge)
edge = self.l_relu(flow + edge)
edge = self.out_layer(edge)
edge = torch.sigmoid(edge)
return edge
class RecurrentFlowCompleteNet(nn.Module):
def __init__(self, model_path=None):
super().__init__()
self.downsample = nn.Sequential(
nn.Conv3d(3, 32, kernel_size=(1, 5, 5), stride=(1, 2, 2),
padding=(0, 2, 2), padding_mode='replicate'),
nn.LeakyReLU(0.2, inplace=True)
)
self.encoder1 = nn.Sequential(
P3DBlock(32, 32, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True),
P3DBlock(32, 64, 3, 2, 1),
nn.LeakyReLU(0.2, inplace=True)
) # 4x
self.encoder2 = nn.Sequential(
P3DBlock(64, 64, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True),
P3DBlock(64, 128, 3, 2, 1),
nn.LeakyReLU(0.2, inplace=True)
) # 8x
self.mid_dilation = nn.Sequential(
nn.Conv3d(128, 128, (1, 3, 3), (1, 1, 1), padding=(0, 3, 3), dilation=(1, 3, 3)), # p = d*(k-1)/2
nn.LeakyReLU(0.2, inplace=True),
nn.Conv3d(128, 128, (1, 3, 3), (1, 1, 1), padding=(0, 2, 2), dilation=(1, 2, 2)),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv3d(128, 128, (1, 3, 3), (1, 1, 1), padding=(0, 1, 1), dilation=(1, 1, 1)),
nn.LeakyReLU(0.2, inplace=True)
)
# feature propagation module
self.feat_prop_module = BidirectionalPropagation(128)
self.decoder2 = nn.Sequential(
nn.Conv2d(128, 128, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True),
deconv(128, 64, 3, 1),
nn.LeakyReLU(0.2, inplace=True)
) # 4x
self.decoder1 = nn.Sequential(
nn.Conv2d(64, 64, 3, 1, 1),
nn.LeakyReLU(0.2, inplace=True),
deconv(64, 32, 3, 1),
nn.LeakyReLU(0.2, inplace=True)
) # 2x
self.upsample = nn.Sequential(
nn.Conv2d(32, 32, 3, padding=1),
nn.LeakyReLU(0.2, inplace=True),
deconv(32, 2, 3, 1)
)
# edge loss
self.edgeDetector = EdgeDetection(in_ch=2, out_ch=1, mid_ch=16)
# Need to initial the weights of MSDeformAttn specifically
for m in self.modules():
if isinstance(m, SecondOrderDeformableAlignment):
m.init_offset()
if model_path is not None:
print('Pretrained flow completion model has loaded...')
ckpt = torch.load(model_path, map_location='cpu')
self.load_state_dict(ckpt, strict=True)
def forward(self, masked_flows, masks):
# masked_flows: b t-1 2 h w
# masks: b t-1 2 h w
b, t, _, h, w = masked_flows.size()
masked_flows = masked_flows.permute(0, 2, 1, 3, 4)
masks = masks.permute(0, 2, 1, 3, 4)
inputs = torch.cat((masked_flows, masks), dim=1)
x = self.downsample(inputs)
feat_e1 = self.encoder1(x)
feat_e2 = self.encoder2(feat_e1) # b c t h w
feat_mid = self.mid_dilation(feat_e2) # b c t h w
feat_mid = feat_mid.permute(0, 2, 1, 3, 4) # b t c h w
feat_prop = self.feat_prop_module(feat_mid)
feat_prop = feat_prop.view(-1, 128, h // 8, w // 8) # b*t c h w
_, c, _, h_f, w_f = feat_e1.shape
feat_e1 = feat_e1.permute(0, 2, 1, 3, 4).contiguous().view(-1, c, h_f, w_f) # b*t c h w
feat_d2 = self.decoder2(feat_prop) + feat_e1
_, c, _, h_f, w_f = x.shape
x = x.permute(0, 2, 1, 3, 4).contiguous().view(-1, c, h_f, w_f) # b*t c h w
feat_d1 = self.decoder1(feat_d2)
flow = self.upsample(feat_d1)
if self.training:
edge = self.edgeDetector(flow)
edge = edge.view(b, t, 1, h, w)
else:
edge = None
flow = flow.view(b, t, 2, h, w)
return flow, edge
def forward_bidirect_flow(self, masked_flows_bi, masks):
"""
Args:
masked_flows_bi: [masked_flows_f, masked_flows_b] | (b t-1 2 h w), (b t-1 2 h w)
masks: b t 1 h w
"""
masks_forward = masks[:, :-1, ...].contiguous()
masks_backward = masks[:, 1:, ...].contiguous()
# mask flow
masked_flows_forward = masked_flows_bi[0] * (1 - masks_forward)
masked_flows_backward = masked_flows_bi[1] * (1 - masks_backward)
# -- completion --
# forward
pred_flows_forward, pred_edges_forward = self.forward(masked_flows_forward, masks_forward)
# backward
masked_flows_backward = torch.flip(masked_flows_backward, dims=[1])
masks_backward = torch.flip(masks_backward, dims=[1])
pred_flows_backward, pred_edges_backward = self.forward(masked_flows_backward, masks_backward)
pred_flows_backward = torch.flip(pred_flows_backward, dims=[1])
if self.training:
pred_edges_backward = torch.flip(pred_edges_backward, dims=[1])
return [pred_flows_forward, pred_flows_backward], [pred_edges_forward, pred_edges_backward]
def combine_flow(self, masked_flows_bi, pred_flows_bi, masks):
masks_forward = masks[:, :-1, ...].contiguous()
masks_backward = masks[:, 1:, ...].contiguous()
pred_flows_forward = pred_flows_bi[0] * masks_forward + masked_flows_bi[0] * (1 - masks_forward)
pred_flows_backward = pred_flows_bi[1] * masks_backward + masked_flows_bi[1] * (1 - masks_backward)
return pred_flows_forward, pred_flows_backward
-157
View File
@@ -1,157 +0,0 @@
import os
import torch
from collections import OrderedDict
from torch import nn as nn
from torchvision.models import vgg as vgg
VGG_PRETRAIN_PATH = 'experiments/pretrained_models/vgg19-dcbb9e9d.pth'
NAMES = {
'vgg11': [
'conv1_1', 'relu1_1', 'pool1', 'conv2_1', 'relu2_1', 'pool2', 'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2',
'pool3', 'conv4_1', 'relu4_1', 'conv4_2', 'relu4_2', 'pool4', 'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2',
'pool5'
],
'vgg13': [
'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1', 'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2',
'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'pool3', 'conv4_1', 'relu4_1', 'conv4_2', 'relu4_2', 'pool4',
'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2', 'pool5'
],
'vgg16': [
'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1', 'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2',
'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'conv3_3', 'relu3_3', 'pool3', 'conv4_1', 'relu4_1', 'conv4_2',
'relu4_2', 'conv4_3', 'relu4_3', 'pool4', 'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2', 'conv5_3', 'relu5_3',
'pool5'
],
'vgg19': [
'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1', 'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2',
'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'conv3_3', 'relu3_3', 'conv3_4', 'relu3_4', 'pool3', 'conv4_1',
'relu4_1', 'conv4_2', 'relu4_2', 'conv4_3', 'relu4_3', 'conv4_4', 'relu4_4', 'pool4', 'conv5_1', 'relu5_1',
'conv5_2', 'relu5_2', 'conv5_3', 'relu5_3', 'conv5_4', 'relu5_4', 'pool5'
]
}
def insert_bn(names):
"""Insert bn layer after each conv.
Args:
names (list): The list of layer names.
Returns:
list: The list of layer names with bn layers.
"""
names_bn = []
for name in names:
names_bn.append(name)
if 'conv' in name:
position = name.replace('conv', '')
names_bn.append('bn' + position)
return names_bn
class VGGFeatureExtractor(nn.Module):
"""VGG network for feature extraction.
In this implementation, we allow users to choose whether use normalization
in the input feature and the type of vgg network. Note that the pretrained
path must fit the vgg type.
Args:
layer_name_list (list[str]): Forward function returns the corresponding
features according to the layer_name_list.
Example: {'relu1_1', 'relu2_1', 'relu3_1'}.
vgg_type (str): Set the type of vgg network. Default: 'vgg19'.
use_input_norm (bool): If True, normalize the input image. Importantly,
the input feature must in the range [0, 1]. Default: True.
range_norm (bool): If True, norm images with range [-1, 1] to [0, 1].
Default: False.
requires_grad (bool): If true, the parameters of VGG network will be
optimized. Default: False.
remove_pooling (bool): If true, the max pooling operations in VGG net
will be removed. Default: False.
pooling_stride (int): The stride of max pooling operation. Default: 2.
"""
def __init__(self,
layer_name_list,
vgg_type='vgg19',
use_input_norm=True,
range_norm=False,
requires_grad=False,
remove_pooling=False,
pooling_stride=2):
super(VGGFeatureExtractor, self).__init__()
self.layer_name_list = layer_name_list
self.use_input_norm = use_input_norm
self.range_norm = range_norm
self.names = NAMES[vgg_type.replace('_bn', '')]
if 'bn' in vgg_type:
self.names = insert_bn(self.names)
# only borrow layers that will be used to avoid unused params
max_idx = 0
for v in layer_name_list:
idx = self.names.index(v)
if idx > max_idx:
max_idx = idx
if os.path.exists(VGG_PRETRAIN_PATH):
vgg_net = getattr(vgg, vgg_type)(pretrained=False)
state_dict = torch.load(VGG_PRETRAIN_PATH, map_location=lambda storage, loc: storage)
vgg_net.load_state_dict(state_dict)
else:
vgg_net = getattr(vgg, vgg_type)(pretrained=True)
features = vgg_net.features[:max_idx + 1]
modified_net = OrderedDict()
for k, v in zip(self.names, features):
if 'pool' in k:
# if remove_pooling is true, pooling operation will be removed
if remove_pooling:
continue
else:
# in some cases, we may want to change the default stride
modified_net[k] = nn.MaxPool2d(kernel_size=2, stride=pooling_stride)
else:
modified_net[k] = v
self.vgg_net = nn.Sequential(modified_net)
if not requires_grad:
self.vgg_net.eval()
for param in self.parameters():
param.requires_grad = False
else:
self.vgg_net.train()
for param in self.parameters():
param.requires_grad = True
if self.use_input_norm:
# the mean is for image with range [0, 1]
self.register_buffer('mean', torch.Tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
# the std is for image with range [0, 1]
self.register_buffer('std', torch.Tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
def forward(self, x):
"""Forward function.
Args:
x (Tensor): Input tensor with shape (n, c, h, w).
Returns:
Tensor: Forward results.
"""
if self.range_norm:
x = (x + 1) / 2
if self.use_input_norm:
x = (x - self.mean) / self.std
output = {}
for key, layer in self.vgg_net._modules.items():
x = layer(x)
if key in self.layer_name_list:
output[key] = x.clone()
return output
-2
View File
@@ -1,2 +0,0 @@
# from .demo import RAFT_infer
from .raft import RAFT
-91
View File
@@ -1,91 +0,0 @@
import torch
import torch.nn.functional as F
from .utils.utils import bilinear_sampler, coords_grid
try:
import alt_cuda_corr
except:
# alt_cuda_corr is not compiled
pass
class CorrBlock:
def __init__(self, fmap1, fmap2, num_levels=4, radius=4):
self.num_levels = num_levels
self.radius = radius
self.corr_pyramid = []
# all pairs correlation
corr = CorrBlock.corr(fmap1, fmap2)
batch, h1, w1, dim, h2, w2 = corr.shape
corr = corr.reshape(batch*h1*w1, dim, h2, w2)
self.corr_pyramid.append(corr)
for i in range(self.num_levels-1):
corr = F.avg_pool2d(corr, 2, stride=2)
self.corr_pyramid.append(corr)
def __call__(self, coords):
r = self.radius
coords = coords.permute(0, 2, 3, 1)
batch, h1, w1, _ = coords.shape
out_pyramid = []
for i in range(self.num_levels):
corr = self.corr_pyramid[i]
dx = torch.linspace(-r, r, 2*r+1)
dy = torch.linspace(-r, r, 2*r+1)
delta = torch.stack(torch.meshgrid(dy, dx), axis=-1).to(coords.device)
centroid_lvl = coords.reshape(batch*h1*w1, 1, 1, 2) / 2**i
delta_lvl = delta.view(1, 2*r+1, 2*r+1, 2)
coords_lvl = centroid_lvl + delta_lvl
corr = bilinear_sampler(corr, coords_lvl)
corr = corr.view(batch, h1, w1, -1)
out_pyramid.append(corr)
out = torch.cat(out_pyramid, dim=-1)
return out.permute(0, 3, 1, 2).contiguous().float()
@staticmethod
def corr(fmap1, fmap2):
batch, dim, ht, wd = fmap1.shape
fmap1 = fmap1.view(batch, dim, ht*wd)
fmap2 = fmap2.view(batch, dim, ht*wd)
corr = torch.matmul(fmap1.transpose(1,2), fmap2)
corr = corr.view(batch, ht, wd, 1, ht, wd)
return corr / torch.sqrt(torch.tensor(dim).float())
class AlternateCorrBlock:
def __init__(self, fmap1, fmap2, num_levels=4, radius=4):
self.num_levels = num_levels
self.radius = radius
self.pyramid = [(fmap1, fmap2)]
for i in range(self.num_levels):
fmap1 = F.avg_pool2d(fmap1, 2, stride=2)
fmap2 = F.avg_pool2d(fmap2, 2, stride=2)
self.pyramid.append((fmap1, fmap2))
def __call__(self, coords):
coords = coords.permute(0, 2, 3, 1)
B, H, W, _ = coords.shape
corr_list = []
for i in range(self.num_levels):
r = self.radius
fmap1_i = self.pyramid[0][0].permute(0, 2, 3, 1)
fmap2_i = self.pyramid[i][1].permute(0, 2, 3, 1)
coords_i = (coords / 2**i).reshape(B, 1, H, W, 2).contiguous()
corr = alt_cuda_corr(fmap1_i, fmap2_i, coords_i, r)
corr_list.append(corr.squeeze(1))
corr = torch.stack(corr_list, dim=1)
corr = corr.reshape(B, -1, H, W)
return corr / 16.0
-235
View File
@@ -1,235 +0,0 @@
# Data loading based on https://github.com/NVIDIA/flownet2-pytorch
import numpy as np
import torch
import torch.utils.data as data
import torch.nn.functional as F
import os
import math
import random
from glob import glob
import os.path as osp
from utils import frame_utils
from utils.augmentor import FlowAugmentor, SparseFlowAugmentor
class FlowDataset(data.Dataset):
def __init__(self, aug_params=None, sparse=False):
self.augmentor = None
self.sparse = sparse
if aug_params is not None:
if sparse:
self.augmentor = SparseFlowAugmentor(**aug_params)
else:
self.augmentor = FlowAugmentor(**aug_params)
self.is_test = False
self.init_seed = False
self.flow_list = []
self.image_list = []
self.extra_info = []
def __getitem__(self, index):
if self.is_test:
img1 = frame_utils.read_gen(self.image_list[index][0])
img2 = frame_utils.read_gen(self.image_list[index][1])
img1 = np.array(img1).astype(np.uint8)[..., :3]
img2 = np.array(img2).astype(np.uint8)[..., :3]
img1 = torch.from_numpy(img1).permute(2, 0, 1).float()
img2 = torch.from_numpy(img2).permute(2, 0, 1).float()
return img1, img2, self.extra_info[index]
if not self.init_seed:
worker_info = torch.utils.data.get_worker_info()
if worker_info is not None:
torch.manual_seed(worker_info.id)
np.random.seed(worker_info.id)
random.seed(worker_info.id)
self.init_seed = True
index = index % len(self.image_list)
valid = None
if self.sparse:
flow, valid = frame_utils.readFlowKITTI(self.flow_list[index])
else:
flow = frame_utils.read_gen(self.flow_list[index])
img1 = frame_utils.read_gen(self.image_list[index][0])
img2 = frame_utils.read_gen(self.image_list[index][1])
flow = np.array(flow).astype(np.float32)
img1 = np.array(img1).astype(np.uint8)
img2 = np.array(img2).astype(np.uint8)
# grayscale images
if len(img1.shape) == 2:
img1 = np.tile(img1[...,None], (1, 1, 3))
img2 = np.tile(img2[...,None], (1, 1, 3))
else:
img1 = img1[..., :3]
img2 = img2[..., :3]
if self.augmentor is not None:
if self.sparse:
img1, img2, flow, valid = self.augmentor(img1, img2, flow, valid)
else:
img1, img2, flow = self.augmentor(img1, img2, flow)
img1 = torch.from_numpy(img1).permute(2, 0, 1).float()
img2 = torch.from_numpy(img2).permute(2, 0, 1).float()
flow = torch.from_numpy(flow).permute(2, 0, 1).float()
if valid is not None:
valid = torch.from_numpy(valid)
else:
valid = (flow[0].abs() < 1000) & (flow[1].abs() < 1000)
return img1, img2, flow, valid.float()
def __rmul__(self, v):
self.flow_list = v * self.flow_list
self.image_list = v * self.image_list
return self
def __len__(self):
return len(self.image_list)
class MpiSintel(FlowDataset):
def __init__(self, aug_params=None, split='training', root='datasets/Sintel', dstype='clean'):
super(MpiSintel, self).__init__(aug_params)
flow_root = osp.join(root, split, 'flow')
image_root = osp.join(root, split, dstype)
if split == 'test':
self.is_test = True
for scene in os.listdir(image_root):
image_list = sorted(glob(osp.join(image_root, scene, '*.png')))
for i in range(len(image_list)-1):
self.image_list += [ [image_list[i], image_list[i+1]] ]
self.extra_info += [ (scene, i) ] # scene and frame_id
if split != 'test':
self.flow_list += sorted(glob(osp.join(flow_root, scene, '*.flo')))
class FlyingChairs(FlowDataset):
def __init__(self, aug_params=None, split='train', root='datasets/FlyingChairs_release/data'):
super(FlyingChairs, self).__init__(aug_params)
images = sorted(glob(osp.join(root, '*.ppm')))
flows = sorted(glob(osp.join(root, '*.flo')))
assert (len(images)//2 == len(flows))
split_list = np.loadtxt('chairs_split.txt', dtype=np.int32)
for i in range(len(flows)):
xid = split_list[i]
if (split=='training' and xid==1) or (split=='validation' and xid==2):
self.flow_list += [ flows[i] ]
self.image_list += [ [images[2*i], images[2*i+1]] ]
class FlyingThings3D(FlowDataset):
def __init__(self, aug_params=None, root='datasets/FlyingThings3D', dstype='frames_cleanpass'):
super(FlyingThings3D, self).__init__(aug_params)
for cam in ['left']:
for direction in ['into_future', 'into_past']:
image_dirs = sorted(glob(osp.join(root, dstype, 'TRAIN/*/*')))
image_dirs = sorted([osp.join(f, cam) for f in image_dirs])
flow_dirs = sorted(glob(osp.join(root, 'optical_flow/TRAIN/*/*')))
flow_dirs = sorted([osp.join(f, direction, cam) for f in flow_dirs])
for idir, fdir in zip(image_dirs, flow_dirs):
images = sorted(glob(osp.join(idir, '*.png')) )
flows = sorted(glob(osp.join(fdir, '*.pfm')) )
for i in range(len(flows)-1):
if direction == 'into_future':
self.image_list += [ [images[i], images[i+1]] ]
self.flow_list += [ flows[i] ]
elif direction == 'into_past':
self.image_list += [ [images[i+1], images[i]] ]
self.flow_list += [ flows[i+1] ]
class KITTI(FlowDataset):
def __init__(self, aug_params=None, split='training', root='datasets/KITTI'):
super(KITTI, self).__init__(aug_params, sparse=True)
if split == 'testing':
self.is_test = True
root = osp.join(root, split)
images1 = sorted(glob(osp.join(root, 'image_2/*_10.png')))
images2 = sorted(glob(osp.join(root, 'image_2/*_11.png')))
for img1, img2 in zip(images1, images2):
frame_id = img1.split('/')[-1]
self.extra_info += [ [frame_id] ]
self.image_list += [ [img1, img2] ]
if split == 'training':
self.flow_list = sorted(glob(osp.join(root, 'flow_occ/*_10.png')))
class HD1K(FlowDataset):
def __init__(self, aug_params=None, root='datasets/HD1k'):
super(HD1K, self).__init__(aug_params, sparse=True)
seq_ix = 0
while 1:
flows = sorted(glob(os.path.join(root, 'hd1k_flow_gt', 'flow_occ/%06d_*.png' % seq_ix)))
images = sorted(glob(os.path.join(root, 'hd1k_input', 'image_2/%06d_*.png' % seq_ix)))
if len(flows) == 0:
break
for i in range(len(flows)-1):
self.flow_list += [flows[i]]
self.image_list += [ [images[i], images[i+1]] ]
seq_ix += 1
def fetch_dataloader(args, TRAIN_DS='C+T+K+S+H'):
""" Create the data loader for the corresponding trainign set """
if args.stage == 'chairs':
aug_params = {'crop_size': args.image_size, 'min_scale': -0.1, 'max_scale': 1.0, 'do_flip': True}
train_dataset = FlyingChairs(aug_params, split='training')
elif args.stage == 'things':
aug_params = {'crop_size': args.image_size, 'min_scale': -0.4, 'max_scale': 0.8, 'do_flip': True}
clean_dataset = FlyingThings3D(aug_params, dstype='frames_cleanpass')
final_dataset = FlyingThings3D(aug_params, dstype='frames_finalpass')
train_dataset = clean_dataset + final_dataset
elif args.stage == 'sintel':
aug_params = {'crop_size': args.image_size, 'min_scale': -0.2, 'max_scale': 0.6, 'do_flip': True}
things = FlyingThings3D(aug_params, dstype='frames_cleanpass')
sintel_clean = MpiSintel(aug_params, split='training', dstype='clean')
sintel_final = MpiSintel(aug_params, split='training', dstype='final')
if TRAIN_DS == 'C+T+K+S+H':
kitti = KITTI({'crop_size': args.image_size, 'min_scale': -0.3, 'max_scale': 0.5, 'do_flip': True})
hd1k = HD1K({'crop_size': args.image_size, 'min_scale': -0.5, 'max_scale': 0.2, 'do_flip': True})
train_dataset = 100*sintel_clean + 100*sintel_final + 200*kitti + 5*hd1k + things
elif TRAIN_DS == 'C+T+K/S':
train_dataset = 100*sintel_clean + 100*sintel_final + things
elif args.stage == 'kitti':
aug_params = {'crop_size': args.image_size, 'min_scale': -0.2, 'max_scale': 0.4, 'do_flip': False}
train_dataset = KITTI(aug_params, split='training')
train_loader = data.DataLoader(train_dataset, batch_size=args.batch_size,
pin_memory=False, shuffle=True, num_workers=4, drop_last=True)
print('Training with %d image pairs' % len(train_dataset))
return train_loader
-79
View File
@@ -1,79 +0,0 @@
import sys
import argparse
import os
import cv2
import glob
import numpy as np
import torch
from PIL import Image
from .raft import RAFT
from .utils import flow_viz
from .utils.utils import InputPadder
DEVICE = 'cuda'
def load_image(imfile):
img = np.array(Image.open(imfile)).astype(np.uint8)
img = torch.from_numpy(img).permute(2, 0, 1).float()
return img
def load_image_list(image_files):
images = []
for imfile in sorted(image_files):
images.append(load_image(imfile))
images = torch.stack(images, dim=0)
images = images.to(DEVICE)
padder = InputPadder(images.shape)
return padder.pad(images)[0]
def viz(img, flo):
img = img[0].permute(1,2,0).cpu().numpy()
flo = flo[0].permute(1,2,0).cpu().numpy()
# map flow to rgb image
flo = flow_viz.flow_to_image(flo)
# img_flo = np.concatenate([img, flo], axis=0)
img_flo = flo
cv2.imwrite('/home/chengao/test/flow.png', img_flo[:, :, [2,1,0]])
# cv2.imshow('image', img_flo[:, :, [2,1,0]]/255.0)
# cv2.waitKey()
def demo(args):
model = torch.nn.DataParallel(RAFT(args))
model.load_state_dict(torch.load(args.model))
model = model.module
model.to(DEVICE)
model.eval()
with torch.no_grad():
images = glob.glob(os.path.join(args.path, '*.png')) + \
glob.glob(os.path.join(args.path, '*.jpg'))
images = load_image_list(images)
for i in range(images.shape[0]-1):
image1 = images[i,None]
image2 = images[i+1,None]
flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
viz(image1, flow_up)
def RAFT_infer(args):
model = torch.nn.DataParallel(RAFT(args))
model.load_state_dict(torch.load(args.model))
model = model.module
model.to(DEVICE)
model.eval()
return model
-267
View File
@@ -1,267 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class ResidualBlock(nn.Module):
def __init__(self, in_planes, planes, norm_fn='group', stride=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, padding=1, stride=stride)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1)
self.relu = nn.ReLU(inplace=True)
num_groups = planes // 8
if norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
self.norm2 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
if not stride == 1:
self.norm3 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
elif norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(planes)
self.norm2 = nn.BatchNorm2d(planes)
if not stride == 1:
self.norm3 = nn.BatchNorm2d(planes)
elif norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(planes)
self.norm2 = nn.InstanceNorm2d(planes)
if not stride == 1:
self.norm3 = nn.InstanceNorm2d(planes)
elif norm_fn == 'none':
self.norm1 = nn.Sequential()
self.norm2 = nn.Sequential()
if not stride == 1:
self.norm3 = nn.Sequential()
if stride == 1:
self.downsample = None
else:
self.downsample = nn.Sequential(
nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), self.norm3)
def forward(self, x):
y = x
y = self.relu(self.norm1(self.conv1(y)))
y = self.relu(self.norm2(self.conv2(y)))
if self.downsample is not None:
x = self.downsample(x)
return self.relu(x+y)
class BottleneckBlock(nn.Module):
def __init__(self, in_planes, planes, norm_fn='group', stride=1):
super(BottleneckBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes//4, kernel_size=1, padding=0)
self.conv2 = nn.Conv2d(planes//4, planes//4, kernel_size=3, padding=1, stride=stride)
self.conv3 = nn.Conv2d(planes//4, planes, kernel_size=1, padding=0)
self.relu = nn.ReLU(inplace=True)
num_groups = planes // 8
if norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=num_groups, num_channels=planes//4)
self.norm2 = nn.GroupNorm(num_groups=num_groups, num_channels=planes//4)
self.norm3 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
if not stride == 1:
self.norm4 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
elif norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(planes//4)
self.norm2 = nn.BatchNorm2d(planes//4)
self.norm3 = nn.BatchNorm2d(planes)
if not stride == 1:
self.norm4 = nn.BatchNorm2d(planes)
elif norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(planes//4)
self.norm2 = nn.InstanceNorm2d(planes//4)
self.norm3 = nn.InstanceNorm2d(planes)
if not stride == 1:
self.norm4 = nn.InstanceNorm2d(planes)
elif norm_fn == 'none':
self.norm1 = nn.Sequential()
self.norm2 = nn.Sequential()
self.norm3 = nn.Sequential()
if not stride == 1:
self.norm4 = nn.Sequential()
if stride == 1:
self.downsample = None
else:
self.downsample = nn.Sequential(
nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), self.norm4)
def forward(self, x):
y = x
y = self.relu(self.norm1(self.conv1(y)))
y = self.relu(self.norm2(self.conv2(y)))
y = self.relu(self.norm3(self.conv3(y)))
if self.downsample is not None:
x = self.downsample(x)
return self.relu(x+y)
class BasicEncoder(nn.Module):
def __init__(self, output_dim=128, norm_fn='batch', dropout=0.0):
super(BasicEncoder, self).__init__()
self.norm_fn = norm_fn
if self.norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=8, num_channels=64)
elif self.norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(64)
elif self.norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(64)
elif self.norm_fn == 'none':
self.norm1 = nn.Sequential()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.relu1 = nn.ReLU(inplace=True)
self.in_planes = 64
self.layer1 = self._make_layer(64, stride=1)
self.layer2 = self._make_layer(96, stride=2)
self.layer3 = self._make_layer(128, stride=2)
# output convolution
self.conv2 = nn.Conv2d(128, output_dim, kernel_size=1)
self.dropout = None
if dropout > 0:
self.dropout = nn.Dropout2d(p=dropout)
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.InstanceNorm2d, nn.GroupNorm)):
if m.weight is not None:
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def _make_layer(self, dim, stride=1):
layer1 = ResidualBlock(self.in_planes, dim, self.norm_fn, stride=stride)
layer2 = ResidualBlock(dim, dim, self.norm_fn, stride=1)
layers = (layer1, layer2)
self.in_planes = dim
return nn.Sequential(*layers)
def forward(self, x):
# if input is list, combine batch dimension
is_list = isinstance(x, tuple) or isinstance(x, list)
if is_list:
batch_dim = x[0].shape[0]
x = torch.cat(x, dim=0)
x = self.conv1(x)
x = self.norm1(x)
x = self.relu1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.conv2(x)
if self.training and self.dropout is not None:
x = self.dropout(x)
if is_list:
x = torch.split(x, [batch_dim, batch_dim], dim=0)
return x
class SmallEncoder(nn.Module):
def __init__(self, output_dim=128, norm_fn='batch', dropout=0.0):
super(SmallEncoder, self).__init__()
self.norm_fn = norm_fn
if self.norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=8, num_channels=32)
elif self.norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(32)
elif self.norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(32)
elif self.norm_fn == 'none':
self.norm1 = nn.Sequential()
self.conv1 = nn.Conv2d(3, 32, kernel_size=7, stride=2, padding=3)
self.relu1 = nn.ReLU(inplace=True)
self.in_planes = 32
self.layer1 = self._make_layer(32, stride=1)
self.layer2 = self._make_layer(64, stride=2)
self.layer3 = self._make_layer(96, stride=2)
self.dropout = None
if dropout > 0:
self.dropout = nn.Dropout2d(p=dropout)
self.conv2 = nn.Conv2d(96, output_dim, kernel_size=1)
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.InstanceNorm2d, nn.GroupNorm)):
if m.weight is not None:
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def _make_layer(self, dim, stride=1):
layer1 = BottleneckBlock(self.in_planes, dim, self.norm_fn, stride=stride)
layer2 = BottleneckBlock(dim, dim, self.norm_fn, stride=1)
layers = (layer1, layer2)
self.in_planes = dim
return nn.Sequential(*layers)
def forward(self, x):
# if input is list, combine batch dimension
is_list = isinstance(x, tuple) or isinstance(x, list)
if is_list:
batch_dim = x[0].shape[0]
x = torch.cat(x, dim=0)
x = self.conv1(x)
x = self.norm1(x)
x = self.relu1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.conv2(x)
if self.training and self.dropout is not None:
x = self.dropout(x)
if is_list:
x = torch.split(x, [batch_dim, batch_dim], dim=0)
return x
-146
View File
@@ -1,146 +0,0 @@
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from .update import BasicUpdateBlock, SmallUpdateBlock
from .extractor import BasicEncoder, SmallEncoder
from .corr import CorrBlock, AlternateCorrBlock
from .utils.utils import bilinear_sampler, coords_grid, upflow8
try:
autocast = torch.cuda.amp.autocast
except:
# dummy autocast for PyTorch < 1.6
class autocast:
def __init__(self, enabled):
pass
def __enter__(self):
pass
def __exit__(self, *args):
pass
class RAFT(nn.Module):
def __init__(self, args):
super(RAFT, self).__init__()
self.args = args
if args.small:
self.hidden_dim = hdim = 96
self.context_dim = cdim = 64
args.corr_levels = 4
args.corr_radius = 3
else:
self.hidden_dim = hdim = 128
self.context_dim = cdim = 128
args.corr_levels = 4
args.corr_radius = 4
if 'dropout' not in args._get_kwargs():
args.dropout = 0
if 'alternate_corr' not in args._get_kwargs():
args.alternate_corr = False
# feature network, context network, and update block
if args.small:
self.fnet = SmallEncoder(output_dim=128, norm_fn='instance', dropout=args.dropout)
self.cnet = SmallEncoder(output_dim=hdim+cdim, norm_fn='none', dropout=args.dropout)
self.update_block = SmallUpdateBlock(self.args, hidden_dim=hdim)
else:
self.fnet = BasicEncoder(output_dim=256, norm_fn='instance', dropout=args.dropout)
self.cnet = BasicEncoder(output_dim=hdim+cdim, norm_fn='batch', dropout=args.dropout)
self.update_block = BasicUpdateBlock(self.args, hidden_dim=hdim)
def freeze_bn(self):
for m in self.modules():
if isinstance(m, nn.BatchNorm2d):
m.eval()
def initialize_flow(self, img):
""" Flow is represented as difference between two coordinate grids flow = coords1 - coords0"""
N, C, H, W = img.shape
coords0 = coords_grid(N, H//8, W//8).to(img.device)
coords1 = coords_grid(N, H//8, W//8).to(img.device)
# optical flow computed as difference: flow = coords1 - coords0
return coords0, coords1
def upsample_flow(self, flow, mask):
""" Upsample flow field [H/8, W/8, 2] -> [H, W, 2] using convex combination """
N, _, H, W = flow.shape
mask = mask.view(N, 1, 9, 8, 8, H, W)
mask = torch.softmax(mask, dim=2)
up_flow = F.unfold(8 * flow, [3,3], padding=1)
up_flow = up_flow.view(N, 2, 9, 1, 1, H, W)
up_flow = torch.sum(mask * up_flow, dim=2)
up_flow = up_flow.permute(0, 1, 4, 2, 5, 3)
return up_flow.reshape(N, 2, 8*H, 8*W)
def forward(self, image1, image2, iters=12, flow_init=None, test_mode=True):
""" Estimate optical flow between pair of frames """
# image1 = 2 * (image1 / 255.0) - 1.0
# image2 = 2 * (image2 / 255.0) - 1.0
image1 = image1.contiguous()
image2 = image2.contiguous()
hdim = self.hidden_dim
cdim = self.context_dim
# run the feature network
with autocast(enabled=self.args.mixed_precision):
fmap1, fmap2 = self.fnet([image1, image2])
fmap1 = fmap1.float()
fmap2 = fmap2.float()
if self.args.alternate_corr:
corr_fn = AlternateCorrBlock(fmap1, fmap2, radius=self.args.corr_radius)
else:
corr_fn = CorrBlock(fmap1, fmap2, radius=self.args.corr_radius)
# run the context network
with autocast(enabled=self.args.mixed_precision):
cnet = self.cnet(image1)
net, inp = torch.split(cnet, [hdim, cdim], dim=1)
net = torch.tanh(net)
inp = torch.relu(inp)
coords0, coords1 = self.initialize_flow(image1)
if flow_init is not None:
coords1 = coords1 + flow_init
flow_predictions = []
for itr in range(iters):
coords1 = coords1.detach()
corr = corr_fn(coords1) # index correlation volume
flow = coords1 - coords0
with autocast(enabled=self.args.mixed_precision):
net, up_mask, delta_flow = self.update_block(net, inp, corr, flow)
# F(t+1) = F(t) + \Delta(t)
coords1 = coords1 + delta_flow
# upsample predictions
if up_mask is None:
flow_up = upflow8(coords1 - coords0)
else:
flow_up = self.upsample_flow(coords1 - coords0, up_mask)
flow_predictions.append(flow_up)
if test_mode:
return coords1 - coords0, flow_up
return flow_predictions
-139
View File
@@ -1,139 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class FlowHead(nn.Module):
def __init__(self, input_dim=128, hidden_dim=256):
super(FlowHead, self).__init__()
self.conv1 = nn.Conv2d(input_dim, hidden_dim, 3, padding=1)
self.conv2 = nn.Conv2d(hidden_dim, 2, 3, padding=1)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
return self.conv2(self.relu(self.conv1(x)))
class ConvGRU(nn.Module):
def __init__(self, hidden_dim=128, input_dim=192+128):
super(ConvGRU, self).__init__()
self.convz = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
self.convr = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
self.convq = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
def forward(self, h, x):
hx = torch.cat([h, x], dim=1)
z = torch.sigmoid(self.convz(hx))
r = torch.sigmoid(self.convr(hx))
q = torch.tanh(self.convq(torch.cat([r*h, x], dim=1)))
h = (1-z) * h + z * q
return h
class SepConvGRU(nn.Module):
def __init__(self, hidden_dim=128, input_dim=192+128):
super(SepConvGRU, self).__init__()
self.convz1 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (1,5), padding=(0,2))
self.convr1 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (1,5), padding=(0,2))
self.convq1 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (1,5), padding=(0,2))
self.convz2 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (5,1), padding=(2,0))
self.convr2 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (5,1), padding=(2,0))
self.convq2 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (5,1), padding=(2,0))
def forward(self, h, x):
# horizontal
hx = torch.cat([h, x], dim=1)
z = torch.sigmoid(self.convz1(hx))
r = torch.sigmoid(self.convr1(hx))
q = torch.tanh(self.convq1(torch.cat([r*h, x], dim=1)))
h = (1-z) * h + z * q
# vertical
hx = torch.cat([h, x], dim=1)
z = torch.sigmoid(self.convz2(hx))
r = torch.sigmoid(self.convr2(hx))
q = torch.tanh(self.convq2(torch.cat([r*h, x], dim=1)))
h = (1-z) * h + z * q
return h
class SmallMotionEncoder(nn.Module):
def __init__(self, args):
super(SmallMotionEncoder, self).__init__()
cor_planes = args.corr_levels * (2*args.corr_radius + 1)**2
self.convc1 = nn.Conv2d(cor_planes, 96, 1, padding=0)
self.convf1 = nn.Conv2d(2, 64, 7, padding=3)
self.convf2 = nn.Conv2d(64, 32, 3, padding=1)
self.conv = nn.Conv2d(128, 80, 3, padding=1)
def forward(self, flow, corr):
cor = F.relu(self.convc1(corr))
flo = F.relu(self.convf1(flow))
flo = F.relu(self.convf2(flo))
cor_flo = torch.cat([cor, flo], dim=1)
out = F.relu(self.conv(cor_flo))
return torch.cat([out, flow], dim=1)
class BasicMotionEncoder(nn.Module):
def __init__(self, args):
super(BasicMotionEncoder, self).__init__()
cor_planes = args.corr_levels * (2*args.corr_radius + 1)**2
self.convc1 = nn.Conv2d(cor_planes, 256, 1, padding=0)
self.convc2 = nn.Conv2d(256, 192, 3, padding=1)
self.convf1 = nn.Conv2d(2, 128, 7, padding=3)
self.convf2 = nn.Conv2d(128, 64, 3, padding=1)
self.conv = nn.Conv2d(64+192, 128-2, 3, padding=1)
def forward(self, flow, corr):
cor = F.relu(self.convc1(corr))
cor = F.relu(self.convc2(cor))
flo = F.relu(self.convf1(flow))
flo = F.relu(self.convf2(flo))
cor_flo = torch.cat([cor, flo], dim=1)
out = F.relu(self.conv(cor_flo))
return torch.cat([out, flow], dim=1)
class SmallUpdateBlock(nn.Module):
def __init__(self, args, hidden_dim=96):
super(SmallUpdateBlock, self).__init__()
self.encoder = SmallMotionEncoder(args)
self.gru = ConvGRU(hidden_dim=hidden_dim, input_dim=82+64)
self.flow_head = FlowHead(hidden_dim, hidden_dim=128)
def forward(self, net, inp, corr, flow):
motion_features = self.encoder(flow, corr)
inp = torch.cat([inp, motion_features], dim=1)
net = self.gru(net, inp)
delta_flow = self.flow_head(net)
return net, None, delta_flow
class BasicUpdateBlock(nn.Module):
def __init__(self, args, hidden_dim=128, input_dim=128):
super(BasicUpdateBlock, self).__init__()
self.args = args
self.encoder = BasicMotionEncoder(args)
self.gru = SepConvGRU(hidden_dim=hidden_dim, input_dim=128+hidden_dim)
self.flow_head = FlowHead(hidden_dim, hidden_dim=256)
self.mask = nn.Sequential(
nn.Conv2d(128, 256, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 64*9, 1, padding=0))
def forward(self, net, inp, corr, flow, upsample=True):
motion_features = self.encoder(flow, corr)
inp = torch.cat([inp, motion_features], dim=1)
net = self.gru(net, inp)
delta_flow = self.flow_head(net)
# scale mask to balence gradients
mask = .25 * self.mask(net)
return net, mask, delta_flow
@@ -1,2 +0,0 @@
from .flow_viz import flow_to_image
from .frame_utils import writeFlow
@@ -1,246 +0,0 @@
import numpy as np
import random
import math
from PIL import Image
import cv2
cv2.setNumThreads(0)
cv2.ocl.setUseOpenCL(False)
import torch
from torchvision.transforms import ColorJitter
import torch.nn.functional as F
class FlowAugmentor:
def __init__(self, crop_size, min_scale=-0.2, max_scale=0.5, do_flip=True):
# spatial augmentation params
self.crop_size = crop_size
self.min_scale = min_scale
self.max_scale = max_scale
self.spatial_aug_prob = 0.8
self.stretch_prob = 0.8
self.max_stretch = 0.2
# flip augmentation params
self.do_flip = do_flip
self.h_flip_prob = 0.5
self.v_flip_prob = 0.1
# photometric augmentation params
self.photo_aug = ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.5/3.14)
self.asymmetric_color_aug_prob = 0.2
self.eraser_aug_prob = 0.5
def color_transform(self, img1, img2):
""" Photometric augmentation """
# asymmetric
if np.random.rand() < self.asymmetric_color_aug_prob:
img1 = np.array(self.photo_aug(Image.fromarray(img1)), dtype=np.uint8)
img2 = np.array(self.photo_aug(Image.fromarray(img2)), dtype=np.uint8)
# symmetric
else:
image_stack = np.concatenate([img1, img2], axis=0)
image_stack = np.array(self.photo_aug(Image.fromarray(image_stack)), dtype=np.uint8)
img1, img2 = np.split(image_stack, 2, axis=0)
return img1, img2
def eraser_transform(self, img1, img2, bounds=[50, 100]):
""" Occlusion augmentation """
ht, wd = img1.shape[:2]
if np.random.rand() < self.eraser_aug_prob:
mean_color = np.mean(img2.reshape(-1, 3), axis=0)
for _ in range(np.random.randint(1, 3)):
x0 = np.random.randint(0, wd)
y0 = np.random.randint(0, ht)
dx = np.random.randint(bounds[0], bounds[1])
dy = np.random.randint(bounds[0], bounds[1])
img2[y0:y0+dy, x0:x0+dx, :] = mean_color
return img1, img2
def spatial_transform(self, img1, img2, flow):
# randomly sample scale
ht, wd = img1.shape[:2]
min_scale = np.maximum(
(self.crop_size[0] + 8) / float(ht),
(self.crop_size[1] + 8) / float(wd))
scale = 2 ** np.random.uniform(self.min_scale, self.max_scale)
scale_x = scale
scale_y = scale
if np.random.rand() < self.stretch_prob:
scale_x *= 2 ** np.random.uniform(-self.max_stretch, self.max_stretch)
scale_y *= 2 ** np.random.uniform(-self.max_stretch, self.max_stretch)
scale_x = np.clip(scale_x, min_scale, None)
scale_y = np.clip(scale_y, min_scale, None)
if np.random.rand() < self.spatial_aug_prob:
# rescale the images
img1 = cv2.resize(img1, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
img2 = cv2.resize(img2, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
flow = cv2.resize(flow, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
flow = flow * [scale_x, scale_y]
if self.do_flip:
if np.random.rand() < self.h_flip_prob: # h-flip
img1 = img1[:, ::-1]
img2 = img2[:, ::-1]
flow = flow[:, ::-1] * [-1.0, 1.0]
if np.random.rand() < self.v_flip_prob: # v-flip
img1 = img1[::-1, :]
img2 = img2[::-1, :]
flow = flow[::-1, :] * [1.0, -1.0]
y0 = np.random.randint(0, img1.shape[0] - self.crop_size[0])
x0 = np.random.randint(0, img1.shape[1] - self.crop_size[1])
img1 = img1[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
img2 = img2[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
flow = flow[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
return img1, img2, flow
def __call__(self, img1, img2, flow):
img1, img2 = self.color_transform(img1, img2)
img1, img2 = self.eraser_transform(img1, img2)
img1, img2, flow = self.spatial_transform(img1, img2, flow)
img1 = np.ascontiguousarray(img1)
img2 = np.ascontiguousarray(img2)
flow = np.ascontiguousarray(flow)
return img1, img2, flow
class SparseFlowAugmentor:
def __init__(self, crop_size, min_scale=-0.2, max_scale=0.5, do_flip=False):
# spatial augmentation params
self.crop_size = crop_size
self.min_scale = min_scale
self.max_scale = max_scale
self.spatial_aug_prob = 0.8
self.stretch_prob = 0.8
self.max_stretch = 0.2
# flip augmentation params
self.do_flip = do_flip
self.h_flip_prob = 0.5
self.v_flip_prob = 0.1
# photometric augmentation params
self.photo_aug = ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.3/3.14)
self.asymmetric_color_aug_prob = 0.2
self.eraser_aug_prob = 0.5
def color_transform(self, img1, img2):
image_stack = np.concatenate([img1, img2], axis=0)
image_stack = np.array(self.photo_aug(Image.fromarray(image_stack)), dtype=np.uint8)
img1, img2 = np.split(image_stack, 2, axis=0)
return img1, img2
def eraser_transform(self, img1, img2):
ht, wd = img1.shape[:2]
if np.random.rand() < self.eraser_aug_prob:
mean_color = np.mean(img2.reshape(-1, 3), axis=0)
for _ in range(np.random.randint(1, 3)):
x0 = np.random.randint(0, wd)
y0 = np.random.randint(0, ht)
dx = np.random.randint(50, 100)
dy = np.random.randint(50, 100)
img2[y0:y0+dy, x0:x0+dx, :] = mean_color
return img1, img2
def resize_sparse_flow_map(self, flow, valid, fx=1.0, fy=1.0):
ht, wd = flow.shape[:2]
coords = np.meshgrid(np.arange(wd), np.arange(ht))
coords = np.stack(coords, axis=-1)
coords = coords.reshape(-1, 2).astype(np.float32)
flow = flow.reshape(-1, 2).astype(np.float32)
valid = valid.reshape(-1).astype(np.float32)
coords0 = coords[valid>=1]
flow0 = flow[valid>=1]
ht1 = int(round(ht * fy))
wd1 = int(round(wd * fx))
coords1 = coords0 * [fx, fy]
flow1 = flow0 * [fx, fy]
xx = np.round(coords1[:,0]).astype(np.int32)
yy = np.round(coords1[:,1]).astype(np.int32)
v = (xx > 0) & (xx < wd1) & (yy > 0) & (yy < ht1)
xx = xx[v]
yy = yy[v]
flow1 = flow1[v]
flow_img = np.zeros([ht1, wd1, 2], dtype=np.float32)
valid_img = np.zeros([ht1, wd1], dtype=np.int32)
flow_img[yy, xx] = flow1
valid_img[yy, xx] = 1
return flow_img, valid_img
def spatial_transform(self, img1, img2, flow, valid):
# randomly sample scale
ht, wd = img1.shape[:2]
min_scale = np.maximum(
(self.crop_size[0] + 1) / float(ht),
(self.crop_size[1] + 1) / float(wd))
scale = 2 ** np.random.uniform(self.min_scale, self.max_scale)
scale_x = np.clip(scale, min_scale, None)
scale_y = np.clip(scale, min_scale, None)
if np.random.rand() < self.spatial_aug_prob:
# rescale the images
img1 = cv2.resize(img1, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
img2 = cv2.resize(img2, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
flow, valid = self.resize_sparse_flow_map(flow, valid, fx=scale_x, fy=scale_y)
if self.do_flip:
if np.random.rand() < 0.5: # h-flip
img1 = img1[:, ::-1]
img2 = img2[:, ::-1]
flow = flow[:, ::-1] * [-1.0, 1.0]
valid = valid[:, ::-1]
margin_y = 20
margin_x = 50
y0 = np.random.randint(0, img1.shape[0] - self.crop_size[0] + margin_y)
x0 = np.random.randint(-margin_x, img1.shape[1] - self.crop_size[1] + margin_x)
y0 = np.clip(y0, 0, img1.shape[0] - self.crop_size[0])
x0 = np.clip(x0, 0, img1.shape[1] - self.crop_size[1])
img1 = img1[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
img2 = img2[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
flow = flow[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
valid = valid[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
return img1, img2, flow, valid
def __call__(self, img1, img2, flow, valid):
img1, img2 = self.color_transform(img1, img2)
img1, img2 = self.eraser_transform(img1, img2)
img1, img2, flow, valid = self.spatial_transform(img1, img2, flow, valid)
img1 = np.ascontiguousarray(img1)
img2 = np.ascontiguousarray(img2)
flow = np.ascontiguousarray(flow)
valid = np.ascontiguousarray(valid)
return img1, img2, flow, valid
@@ -1,132 +0,0 @@
# Flow visualization code used from https://github.com/tomrunia/OpticalFlow_Visualization
# MIT License
#
# Copyright (c) 2018 Tom Runia
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to conditions.
#
# Author: Tom Runia
# Date Created: 2018-08-03
import numpy as np
def make_colorwheel():
"""
Generates a color wheel for optical flow visualization as presented in:
Baker et al. "A Database and Evaluation Methodology for Optical Flow" (ICCV, 2007)
URL: http://vision.middlebury.edu/flow/flowEval-iccv07.pdf
Code follows the original C++ source code of Daniel Scharstein.
Code follows the the Matlab source code of Deqing Sun.
Returns:
np.ndarray: Color wheel
"""
RY = 15
YG = 6
GC = 4
CB = 11
BM = 13
MR = 6
ncols = RY + YG + GC + CB + BM + MR
colorwheel = np.zeros((ncols, 3))
col = 0
# RY
colorwheel[0:RY, 0] = 255
colorwheel[0:RY, 1] = np.floor(255*np.arange(0,RY)/RY)
col = col+RY
# YG
colorwheel[col:col+YG, 0] = 255 - np.floor(255*np.arange(0,YG)/YG)
colorwheel[col:col+YG, 1] = 255
col = col+YG
# GC
colorwheel[col:col+GC, 1] = 255
colorwheel[col:col+GC, 2] = np.floor(255*np.arange(0,GC)/GC)
col = col+GC
# CB
colorwheel[col:col+CB, 1] = 255 - np.floor(255*np.arange(CB)/CB)
colorwheel[col:col+CB, 2] = 255
col = col+CB
# BM
colorwheel[col:col+BM, 2] = 255
colorwheel[col:col+BM, 0] = np.floor(255*np.arange(0,BM)/BM)
col = col+BM
# MR
colorwheel[col:col+MR, 2] = 255 - np.floor(255*np.arange(MR)/MR)
colorwheel[col:col+MR, 0] = 255
return colorwheel
def flow_uv_to_colors(u, v, convert_to_bgr=False):
"""
Applies the flow color wheel to (possibly clipped) flow components u and v.
According to the C++ source code of Daniel Scharstein
According to the Matlab source code of Deqing Sun
Args:
u (np.ndarray): Input horizontal flow of shape [H,W]
v (np.ndarray): Input vertical flow of shape [H,W]
convert_to_bgr (bool, optional): Convert output image to BGR. Defaults to False.
Returns:
np.ndarray: Flow visualization image of shape [H,W,3]
"""
flow_image = np.zeros((u.shape[0], u.shape[1], 3), np.uint8)
colorwheel = make_colorwheel() # shape [55x3]
ncols = colorwheel.shape[0]
rad = np.sqrt(np.square(u) + np.square(v))
a = np.arctan2(-v, -u)/np.pi
fk = (a+1) / 2*(ncols-1)
k0 = np.floor(fk).astype(np.int32)
k1 = k0 + 1
k1[k1 == ncols] = 0
f = fk - k0
for i in range(colorwheel.shape[1]):
tmp = colorwheel[:,i]
col0 = tmp[k0] / 255.0
col1 = tmp[k1] / 255.0
col = (1-f)*col0 + f*col1
idx = (rad <= 1)
col[idx] = 1 - rad[idx] * (1-col[idx])
col[~idx] = col[~idx] * 0.75 # out of range
# Note the 2-i => BGR instead of RGB
ch_idx = 2-i if convert_to_bgr else i
flow_image[:,:,ch_idx] = np.floor(255 * col)
return flow_image
def flow_to_image(flow_uv, clip_flow=None, convert_to_bgr=False):
"""
Expects a two dimensional flow image of shape.
Args:
flow_uv (np.ndarray): Flow UV image of shape [H,W,2]
clip_flow (float, optional): Clip maximum of flow values. Defaults to None.
convert_to_bgr (bool, optional): Convert output image to BGR. Defaults to False.
Returns:
np.ndarray: Flow visualization image of shape [H,W,3]
"""
assert flow_uv.ndim == 3, 'input flow must have three dimensions'
assert flow_uv.shape[2] == 2, 'input flow must have shape [H,W,2]'
if clip_flow is not None:
flow_uv = np.clip(flow_uv, 0, clip_flow)
u = flow_uv[:,:,0]
v = flow_uv[:,:,1]
rad = np.sqrt(np.square(u) + np.square(v))
rad_max = np.max(rad)
epsilon = 1e-5
u = u / (rad_max + epsilon)
v = v / (rad_max + epsilon)
return flow_uv_to_colors(u, v, convert_to_bgr)
@@ -1,118 +0,0 @@
# Flow visualization code adapted from https://github.com/tomrunia/OpticalFlow_Visualization
import torch
torch.pi = torch.acos(torch.zeros(1)).item() * 2 # which is 3.1415927410125732
@torch.no_grad()
def flow_to_image(flow: torch.Tensor) -> torch.Tensor:
"""
Converts a flow to an RGB image.
Args:
flow (Tensor): Flow of shape (N, 2, H, W) or (2, H, W) and dtype torch.float.
Returns:
img (Tensor): Image Tensor of dtype uint8 where each color corresponds
to a given flow direction. Shape is (N, 3, H, W) or (3, H, W) depending on the input.
"""
if flow.dtype != torch.float:
raise ValueError(f"Flow should be of dtype torch.float, got {flow.dtype}.")
orig_shape = flow.shape
if flow.ndim == 3:
flow = flow[None] # Add batch dim
if flow.ndim != 4 or flow.shape[1] != 2:
raise ValueError(f"Input flow should have shape (2, H, W) or (N, 2, H, W), got {orig_shape}.")
max_norm = torch.sum(flow**2, dim=1).sqrt().max()
epsilon = torch.finfo((flow).dtype).eps
normalized_flow = flow / (max_norm + epsilon)
img = _normalized_flow_to_image(normalized_flow)
if len(orig_shape) == 3:
img = img[0] # Remove batch dim
return img
@torch.no_grad()
def _normalized_flow_to_image(normalized_flow: torch.Tensor) -> torch.Tensor:
"""
Converts a batch of normalized flow to an RGB image.
Args:
normalized_flow (torch.Tensor): Normalized flow tensor of shape (N, 2, H, W)
Returns:
img (Tensor(N, 3, H, W)): Flow visualization image of dtype uint8.
"""
N, _, H, W = normalized_flow.shape
device = normalized_flow.device
flow_image = torch.zeros((N, 3, H, W), dtype=torch.uint8, device=device)
colorwheel = _make_colorwheel().to(device) # shape [55x3]
num_cols = colorwheel.shape[0]
norm = torch.sum(normalized_flow**2, dim=1).sqrt()
a = torch.atan2(-normalized_flow[:, 1, :, :], -normalized_flow[:, 0, :, :]) / torch.pi
fk = (a + 1) / 2 * (num_cols - 1)
k0 = torch.floor(fk).to(torch.long)
k1 = k0 + 1
k1[k1 == num_cols] = 0
f = fk - k0
for c in range(colorwheel.shape[1]):
tmp = colorwheel[:, c]
col0 = tmp[k0] / 255.0
col1 = tmp[k1] / 255.0
col = (1 - f) * col0 + f * col1
col = 1 - norm * (1 - col)
flow_image[:, c, :, :] = torch.floor(255. * col)
return flow_image
@torch.no_grad()
def _make_colorwheel() -> torch.Tensor:
"""
Generates a color wheel for optical flow visualization as presented in:
Baker et al. "A Database and Evaluation Methodology for Optical Flow" (ICCV, 2007)
URL: http://vision.middlebury.edu/flow/flowEval-iccv07.pdf.
Returns:
colorwheel (Tensor[55, 3]): Colorwheel Tensor.
"""
RY = 15
YG = 6
GC = 4
CB = 11
BM = 13
MR = 6
ncols = RY + YG + GC + CB + BM + MR
colorwheel = torch.zeros((ncols, 3))
col = 0
# RY
colorwheel[0:RY, 0] = 255
colorwheel[0:RY, 1] = torch.floor(255. * torch.arange(0., RY) / RY)
col = col + RY
# YG
colorwheel[col : col + YG, 0] = 255 - torch.floor(255. * torch.arange(0., YG) / YG)
colorwheel[col : col + YG, 1] = 255
col = col + YG
# GC
colorwheel[col : col + GC, 1] = 255
colorwheel[col : col + GC, 2] = torch.floor(255. * torch.arange(0., GC) / GC)
col = col + GC
# CB
colorwheel[col : col + CB, 1] = 255 - torch.floor(255. * torch.arange(CB) / CB)
colorwheel[col : col + CB, 2] = 255
col = col + CB
# BM
colorwheel[col : col + BM, 2] = 255
colorwheel[col : col + BM, 0] = torch.floor(255. * torch.arange(0., BM) / BM)
col = col + BM
# MR
colorwheel[col : col + MR, 2] = 255 - torch.floor(255. * torch.arange(MR) / MR)
colorwheel[col : col + MR, 0] = 255
return colorwheel
@@ -1,137 +0,0 @@
import numpy as np
from PIL import Image
from os.path import *
import re
import cv2
cv2.setNumThreads(0)
cv2.ocl.setUseOpenCL(False)
TAG_CHAR = np.array([202021.25], np.float32)
def readFlow(fn):
""" Read .flo file in Middlebury format"""
# Code adapted from:
# http://stackoverflow.com/questions/28013200/reading-middlebury-flow-files-with-python-bytes-array-numpy
# WARNING: this will work on little-endian architectures (eg Intel x86) only!
# print 'fn = %s'%(fn)
with open(fn, 'rb') as f:
magic = np.fromfile(f, np.float32, count=1)
if 202021.25 != magic:
print('Magic number incorrect. Invalid .flo file')
return None
else:
w = np.fromfile(f, np.int32, count=1)
h = np.fromfile(f, np.int32, count=1)
# print 'Reading %d x %d flo file\n' % (w, h)
data = np.fromfile(f, np.float32, count=2*int(w)*int(h))
# Reshape data into 3D array (columns, rows, bands)
# The reshape here is for visualization, the original code is (w,h,2)
return np.resize(data, (int(h), int(w), 2))
def readPFM(file):
file = open(file, 'rb')
color = None
width = None
height = None
scale = None
endian = None
header = file.readline().rstrip()
if header == b'PF':
color = True
elif header == b'Pf':
color = False
else:
raise Exception('Not a PFM file.')
dim_match = re.match(rb'^(\d+)\s(\d+)\s$', file.readline())
if dim_match:
width, height = map(int, dim_match.groups())
else:
raise Exception('Malformed PFM header.')
scale = float(file.readline().rstrip())
if scale < 0: # little-endian
endian = '<'
scale = -scale
else:
endian = '>' # big-endian
data = np.fromfile(file, endian + 'f')
shape = (height, width, 3) if color else (height, width)
data = np.reshape(data, shape)
data = np.flipud(data)
return data
def writeFlow(filename,uv,v=None):
""" Write optical flow to file.
If v is None, uv is assumed to contain both u and v channels,
stacked in depth.
Original code by Deqing Sun, adapted from Daniel Scharstein.
"""
nBands = 2
if v is None:
assert(uv.ndim == 3)
assert(uv.shape[2] == 2)
u = uv[:,:,0]
v = uv[:,:,1]
else:
u = uv
assert(u.shape == v.shape)
height,width = u.shape
f = open(filename,'wb')
# write the header
f.write(TAG_CHAR)
np.array(width).astype(np.int32).tofile(f)
np.array(height).astype(np.int32).tofile(f)
# arrange into matrix form
tmp = np.zeros((height, width*nBands))
tmp[:,np.arange(width)*2] = u
tmp[:,np.arange(width)*2 + 1] = v
tmp.astype(np.float32).tofile(f)
f.close()
def readFlowKITTI(filename):
flow = cv2.imread(filename, cv2.IMREAD_ANYDEPTH|cv2.IMREAD_COLOR)
flow = flow[:,:,::-1].astype(np.float32)
flow, valid = flow[:, :, :2], flow[:, :, 2]
flow = (flow - 2**15) / 64.0
return flow, valid
def readDispKITTI(filename):
disp = cv2.imread(filename, cv2.IMREAD_ANYDEPTH) / 256.0
valid = disp > 0.0
flow = np.stack([-disp, np.zeros_like(disp)], -1)
return flow, valid
def writeFlowKITTI(filename, uv):
uv = 64.0 * uv + 2**15
valid = np.ones([uv.shape[0], uv.shape[1], 1])
uv = np.concatenate([uv, valid], axis=-1).astype(np.uint16)
cv2.imwrite(filename, uv[..., ::-1])
def read_gen(file_name, pil=False):
ext = splitext(file_name)[-1]
if ext == '.png' or ext == '.jpeg' or ext == '.ppm' or ext == '.jpg':
return Image.open(file_name)
elif ext == '.bin' or ext == '.raw':
return np.load(file_name)
elif ext == '.flo':
return readFlow(file_name).astype(np.float32)
elif ext == '.pfm':
flow = readPFM(file_name).astype(np.float32)
if len(flow.shape) == 2:
return flow
else:
return flow[:, :, :-1]
return []
-82
View File
@@ -1,82 +0,0 @@
import torch
import torch.nn.functional as F
import numpy as np
from scipy import interpolate
class InputPadder:
""" Pads images such that dimensions are divisible by 8 """
def __init__(self, dims, mode='sintel'):
self.ht, self.wd = dims[-2:]
pad_ht = (((self.ht // 8) + 1) * 8 - self.ht) % 8
pad_wd = (((self.wd // 8) + 1) * 8 - self.wd) % 8
if mode == 'sintel':
self._pad = [pad_wd//2, pad_wd - pad_wd//2, pad_ht//2, pad_ht - pad_ht//2]
else:
self._pad = [pad_wd//2, pad_wd - pad_wd//2, 0, pad_ht]
def pad(self, *inputs):
return [F.pad(x, self._pad, mode='replicate') for x in inputs]
def unpad(self,x):
ht, wd = x.shape[-2:]
c = [self._pad[2], ht-self._pad[3], self._pad[0], wd-self._pad[1]]
return x[..., c[0]:c[1], c[2]:c[3]]
def forward_interpolate(flow):
flow = flow.detach().cpu().numpy()
dx, dy = flow[0], flow[1]
ht, wd = dx.shape
x0, y0 = np.meshgrid(np.arange(wd), np.arange(ht))
x1 = x0 + dx
y1 = y0 + dy
x1 = x1.reshape(-1)
y1 = y1.reshape(-1)
dx = dx.reshape(-1)
dy = dy.reshape(-1)
valid = (x1 > 0) & (x1 < wd) & (y1 > 0) & (y1 < ht)
x1 = x1[valid]
y1 = y1[valid]
dx = dx[valid]
dy = dy[valid]
flow_x = interpolate.griddata(
(x1, y1), dx, (x0, y0), method='nearest', fill_value=0)
flow_y = interpolate.griddata(
(x1, y1), dy, (x0, y0), method='nearest', fill_value=0)
flow = np.stack([flow_x, flow_y], axis=0)
return torch.from_numpy(flow).float()
def bilinear_sampler(img, coords, mode='bilinear', mask=False):
""" Wrapper for grid_sample, uses pixel coordinates """
H, W = img.shape[-2:]
xgrid, ygrid = coords.split([1,1], dim=-1)
xgrid = 2*xgrid/(W-1) - 1
ygrid = 2*ygrid/(H-1) - 1
grid = torch.cat([xgrid, ygrid], dim=-1)
img = F.grid_sample(img, grid, align_corners=True)
if mask:
mask = (xgrid > -1) & (ygrid > -1) & (xgrid < 1) & (ygrid < 1)
return img, mask.float()
return img
def coords_grid(batch, ht, wd):
coords = torch.meshgrid(torch.arange(ht), torch.arange(wd))
coords = torch.stack(coords[::-1], dim=0).float()
return coords[None].repeat(batch, 1, 1, 1)
def upflow8(flow, mode='bilinear'):
new_size = (8 * flow.shape[2], 8 * flow.shape[3])
return 8 * F.interpolate(flow, size=new_size, mode=mode, align_corners=True)
-5
View File
@@ -11,7 +11,6 @@ BasicSetting = 基础设置
AdvancedSetting = 高级设置
SubtitleDetectionSetting = 字幕检测设置
SttnSetting = STTN设置
ProPainterSetting = ProPainter设置
AboutSetting = 关于
HardwareAcceleration = 硬件加速
HardwareAccelerationDesc = 使用GPU或ONNX后端进行加速处理
@@ -36,8 +35,6 @@ SttnReferenceLength = 参考帧数量
SttnReferenceLengthDesc = 默认为10
SttnMaxLoadNum = 最大同时处理的帧数量
SttnMaxLoadNumDesc = 设置越大处理效果越好,但是要求显存越高,默认为50
PropainterMaxLoadNum = 最大同时处理的帧数量
PropainterMaxLoadNumDesc = 设置越大处理效果越好,但是要求显存越高,默认为70
CheckUpdateOnStartup = 在应用程序启动时检查更新
CheckUpdateOnStartupDesc = 新版本将更加稳定, 并拥有更多功能(建议启用此选项)
UpdatesAvailableTitle = 有可用更新
@@ -67,7 +64,6 @@ SelectSubtitleArea = 请在视频预览中框选处理区域: {}
InpaintModeDesc = STTN智能擦除, 对于真人视频效果较好,速度快, 智能擦除(最低4GB显存)
STTN字幕检测 带字幕检测版, 无智能擦除(最低4GB显存)
LAMA: 对于动画类视频效果好,速度一般(显存要求较低)
ProPainter: 需要消耗大量显存,速度较慢,对运动非常剧烈的视频效果较好(最低8GB显存)
OpenCV: 极速模式, 不保证inpaint效果,仅仅对包含文本的区域文本进行去除(显存要求较低)
SubtitleDetectMode = 字幕检测
ErrorDuringProcessing = 处理过程中发生错误: {}
@@ -122,7 +118,6 @@ RequestError = 尝试访问 {} 失败, 原因: {}
SttnAuto = STTN智能擦除
SttnDet = STTN字幕检测
LAMA = LAMA
ProPainter = ProPainter
OpenCV = OpenCV
[SubtitleDetectMode]
+2 -7
View File
@@ -11,7 +11,6 @@ BasicSetting = 基礎設定
AdvancedSetting = 進階設定
SubtitleDetectionSetting = 字幕檢測設定
SttnSetting = STTN設定
ProPainterSetting = ProPainter設定
AboutSetting = 關於
HardwareAcceleration = 硬體加速
HardwareAccelerationDesc = 使用GPU或ONNX後端進行加速處理
@@ -36,8 +35,6 @@ SttnReferenceLength = 參考影格數量
SttnReferenceLengthDesc = 預設為10
SttnMaxLoadNum = 最大同時處理的影格數量
SttnMaxLoadNumDesc = 數值越大處理效果越好,但需更高顯示記憶體,預設為50
PropainterMaxLoadNum = 最大同時處理的影格數量
PropainterMaxLoadNumDesc = 數值越大處理效果越好,但需更高顯示記憶體,預設為70
CheckUpdateOnStartup = 在應用程式啟動時檢查更新
CheckUpdateOnStartupDesc = 新版本將更穩定並提供更多功能(建議啟用此選項)
UpdatesAvailableTitle = 有可用更新
@@ -66,8 +63,7 @@ InpaintMode = 處理模型
SelectSubtitleArea = 請在影片預覽中框選處理區域: {}
InpaintModeDesc = STTN智能擦除,對於真人視頻效果較好,速度快,智能擦除(最低4GB顯存)
STTN字幕檢測 帶字幕檢測版,無智能擦除(最低4GB顯存)
LAMA:對於動畫類視頻效果好,速度一般(顯存要求較低)
ProPainter:需要消耗大量顯存,速度較慢,對運動非常劇烈的視頻效果較好(最低8GB顯存)
LAMA:對於動畫類視頻效果好,速度一般(顯存要求較低)
OpenCV:極速模式,不保證inpaint效果,僅僅對包含文本的區域文本進行去除(顯存要求較低)
SubtitleDetectMode = 字幕檢測模式
ErrorDuringProcessing = 處理過程中發生錯誤: {}
@@ -121,8 +117,7 @@ RequestError = 嘗試存取 {} 失敗,原因: {}
[InpaintMode]
SttnAuto = STTN智慧擦除
SttnDet = STTN字幕檢測
LAMA = LAMA
ProPainter = ProPainter
LAMA = LAMA
OpenCV = OpenCV
[SubtitleDetectMode]
-5
View File
@@ -11,7 +11,6 @@ BasicSetting = Basic Settings
AdvancedSetting = Advanced Settings
SubtitleDetectionSetting = Subtitle Detection Settings
SttnSetting = STTN Settings
ProPainterSetting = ProPainter Settings
AboutSetting = About
HardwareAcceleration = Hardware Acceleration
HardwareAccelerationDesc = Accelerate processing using GPU or ONNX backend
@@ -36,8 +35,6 @@ SttnReferenceLength = Reference Frame Count
SttnReferenceLengthDesc = Default: 10
SttnMaxLoadNum = Max Concurrent Processing Frames
SttnMaxLoadNumDesc = Higher values improve quality but require more VRAM (default 50).
PropainterMaxLoadNum = Max Concurrent Processing Frames
PropainterMaxLoadNumDesc = Higher values improve quality but require more VRAM (default 70).
CheckUpdateOnStartup = Check Updates on Startup
CheckUpdateOnStartupDesc = New versions offer improved stability and features (recommended).
UpdatesAvailableTitle = Update Available
@@ -67,7 +64,6 @@ SelectSubtitleArea = Select processing area in video preview: {}
InpaintModeDesc = STTN Smart Inpainting: Best for real-person videos, fast speed, smart inpainting (minimum 4GB VRAM)
STTN Subtitle Detection: With subtitle detection, no smart inpainting (minimum 4GB VRAM)
LAMA: Good for animation videos, moderate speed (low VRAM requirement)
ProPainter: Consumes a lot of VRAM, slower speed, best for videos with intense motion (minimum 8GB VRAM)
OpenCV: Ultra-fast mode, inpainting effect not guaranteed, only removes text in detected regions (low VRAM requirement)
SubtitleDetectMode = Subtitle Detection
ErrorDuringProcessing = Error during processing: {}
@@ -122,7 +118,6 @@ RequestError = Failed to access {}. Reason: {}
SttnAuto = STTN Smart Erase
SttnDet = STTN Detection
LAMA = LAMA
ProPainter = ProPainter
OpenCV = OpenCV
[SubtitleDetectMode]
+2 -7
View File
@@ -11,7 +11,6 @@ BasicSetting = Configuración básica
AdvancedSetting = Configuración avanzada
SubtitleDetectionSetting = Detección de subtítulos
SttnSetting = Configuración STTN
ProPainterSetting = Configuración ProPainter
AboutSetting = Acerca de
HardwareAcceleration = Aceleración hardware
HardwareAccelerationDesc = Usar GPU o backend ONNX para acelerar el procesamiento
@@ -36,8 +35,6 @@ SttnReferenceLength = Cantidad de referencias
SttnReferenceLengthDesc = Valor predeterminado: 10
SttnMaxLoadNum = Máx. fotogramas simultáneos
SttnMaxLoadNumDesc = Mayor valor mejora calidad pero requiere más VRAM (valor predeterminado 50).
PropainterMaxLoadNum = Máx. fotogramas simultáneos
PropainterMaxLoadNumDesc = Mayor valor mejora calidad pero requiere más VRAM (valor predeterminado 70).
CheckUpdateOnStartup = Buscar actualizaciones al iniciar
CheckUpdateOnStartupDesc = Versiones nuevas ofrecen mejor estabilidad y funciones (recomendado).
UpdatesAvailableTitle = Actualización disponible
@@ -66,8 +63,7 @@ InpaintMode = Modelo de procesamiento
SelectSubtitleArea = Selecciona área en vista previa: {}
InpaintModeDesc = STTN Borrado inteligente: Mejor para videos de personas reales, velocidad rápida, borrado inteligente (mínimo 4GB de VRAM)
STTN Detección de subtítulos: Con detección de subtítulos, sin borrado inteligente (mínimo 4GB de VRAM)
LAMA: Bueno para videos animados, velocidad media (bajo requerimiento de VRAM)
ProPainter: Consume mucha VRAM, velocidad lenta, mejor para videos con mucho movimiento (mínimo 8GB de VRAM)
LAMA: Bueno para videos animados, velocidad media (bajo requerimiento de VRAM)
OpenCV: Modo ultra rápido, el efecto de borrado no está garantizado, solo elimina texto en las áreas detectadas (bajo requerimiento de VRAM)
SubtitleDetectMode = Detección de subtítulos
ErrorDuringProcessing = Error durante el procesamiento: {}
@@ -121,8 +117,7 @@ RequestError = Error accediendo {}. Razón: {}
[InpaintMode]
SttnAuto = STTN borrado inteligente
SttnDet = STTN detección
LAMA = LAMA
ProPainter = ProPainter
LAMA = LAMA
OpenCV = OpenCV
[SubtitleDetectMode]
-5
View File
@@ -11,7 +11,6 @@ BasicSetting = 基本設定
AdvancedSetting = 高度設定
SubtitleDetectionSetting = 字幕検出設定
SttnSetting = STTN設定
ProPainterSetting = ProPainter設定
AboutSetting = 情報
HardwareAcceleration = ハードウェアアクセラレーション
HardwareAccelerationDesc = GPUまたはONNXバックエンドを使用した高速処理
@@ -36,8 +35,6 @@ SttnReferenceLength = 参照フレーム数
SttnReferenceLengthDesc = デフォルト: 10
SttnMaxLoadNum = 最大同時処理フレーム数
SttnMaxLoadNumDesc = 値が大きいほど高品質(VRAM要求増加、デフォルト50)
PropainterMaxLoadNum = 最大同時処理フレーム数
PropainterMaxLoadNumDesc = 値が大きいほど高品質(VRAM要求増加、デフォルト70)
CheckUpdateOnStartup = 起動時アップデート確認
CheckUpdateOnStartupDesc = 新バージョンは安定性/機能向上(推奨)
UpdatesAvailableTitle = 利用可能なアップデート
@@ -67,7 +64,6 @@ SelectSubtitleArea = プレビューで処理領域を選択: {}
InpaintModeDesc = STTNスマート消去:実写動画に最適、高速、スマート消去(最低4GB VRAM)
STTN字幕検出:字幕検出付き、スマート消去なし(最低4GB VRAM)
LAMA:アニメ動画に最適、速度は普通(VRAM要件低め)
ProPainter:大量のVRAMを消費、速度は遅い、激しい動きの動画に最適(最低8GB VRAM)
OpenCV:超高速モード、消去効果は保証されません、検出されたテキスト領域のみ削除(VRAM要件低め)
SubtitleDetectMode = 字幕検出
ErrorDuringProcessing = 処理中にエラーが発生しました: {}
@@ -122,7 +118,6 @@ RequestError = {} へのアクセス失敗。理由: {}
SttnAuto = STTNインテリジェント消去
SttnDet = STTN字幕検出
LAMA = LAMA
ProPainter = ProPainter
OpenCV = OpenCV
[SubtitleDetectMode]
+1 -6
View File
@@ -11,7 +11,6 @@ BasicSetting = 기본 설정
AdvancedSetting = 고급 설정
SubtitleDetectionSetting = 자막 감지 설정
SttnSetting = STTN 설정
ProPainterSetting = ProPainter 설정
AboutSetting = 정보
HardwareAcceleration = 하드웨어 가속
HardwareAccelerationDesc = GPU 또는 ONNX 백엔드 사용 가속 처리
@@ -36,8 +35,6 @@ SttnReferenceLength = 참조 프레임 수
SttnReferenceLengthDesc = 기본값: 10
SttnMaxLoadNum = 최대 동시 처리 프레임
SttnMaxLoadNumDesc = 값 클수록 품질 향상 (VRAM 요구 증가, 기본값 50)
PropainterMaxLoadNum = 최대 동시 처리 프레임
PropainterMaxLoadNumDesc = 값 클수록 품질 향상 (VRAM 요구 증가, 기본값 70)
CheckUpdateOnStartup = 시작시 업데이트 확인
CheckUpdateOnStartupDesc = 새 버전은 안정성/기능 개선 포함 (권장)
UpdatesAvailableTitle = 업데이트 가능
@@ -66,8 +63,7 @@ InpaintMode = 처리 모델
SelectSubtitleArea = 미리보기에서 처리 영역 선택: {}
InpaintModeDesc = STTN 스마트 지우기: 실제 인물 영상에 적합, 빠른 속도, 스마트 지우기(최소 4GB VRAM)
STTN 자막 감지: 자막 감지 버전, 스마트 지우기 없음(최소 4GB VRAM)
LAMA: 애니메이션 영상에 적합, 보통 속도(VRAM 요구량 낮음)
ProPainter: 많은 VRAM 소모, 느린 속도, 격렬한 움직임 영상에 적합(최소 8GB VRAM)
LAMA: 애니메이션 영상에 적합, 보통 속도(VRAM 요구량 낮음)
OpenCV: 초고속 모드, 인페인트 효과 보장 안 됨, 텍스트 영역만 제거(VRAM 요구량 낮음)
SubtitleDetectMode = 자막 감지
ErrorDuringProcessing = 처리 중 오류: {}
@@ -122,7 +118,6 @@ RequestError = {} 접근 실패. 이유: {}
SttnAuto = STTN 지능형 제거
SttnDet = STTN 자막 감지
LAMA = LAMA
ProPainter = ProPainter
OpenCV = OpenCV
[SubtitleDetectMode]
-5
View File
@@ -11,7 +11,6 @@ BasicSetting = Cài đặt cơ bản
AdvancedSetting = Cài đặt nâng cao
SubtitleDetectionSetting = Cài đặt phát hiện phụ đề
SttnSetting = Cài đặt STTN
ProPainterSetting = Cài đặt ProPainter
AboutSetting = Giới thiệu
HardwareAcceleration = Tăng tốc phần cứng
HardwareAccelerationDesc = Sử dụng GPU hoặc backend ONNX để tăng tốc xử lý
@@ -36,8 +35,6 @@ SttnReferenceLength = Số khung tham chiếu
SttnReferenceLengthDesc = Mặc định: 10
SttnMaxLoadNum = Số khung xử lý tối đa
SttnMaxLoadNumDesc = Càng cao càng tốt (yêu cầu nhiều VRAM, mặc định 50)
PropainterMaxLoadNum = Số khung xử lý tối đa
PropainterMaxLoadNumDesc = Càng cao càng tốt (yêu cầu nhiều VRAM, mặc định 70)
CheckUpdateOnStartup = Kiểm tra cập nhật khi khởi động
CheckUpdateOnStartupDesc = Phiên bản mới ổn định hơn (khuyến nghị bật)
UpdatesAvailableTitle = Có bản cập nhật
@@ -67,7 +64,6 @@ SelectSubtitleArea = Chọn vùng xử lý trong preview: {}
InpaintModeDesc = STTN Xóa thông minh: Phù hợp cho video người thật, tốc độ nhanh, xóa thông minh (tối thiểu 4GB VRAM)
STTN Phát hiện phụ đề: Có phát hiện phụ đề, không xóa thông minh (tối thiểu 4GB VRAM)
LAMA: Phù hợp cho video hoạt hình, tốc độ trung bình (yêu cầu VRAM thấp)
ProPainter: Tiêu tốn nhiều VRAM, tốc độ chậm, phù hợp cho video chuyển động mạnh (tối thiểu 8GB VRAM)
OpenCV: Chế độ siêu nhanh, không đảm bảo hiệu quả xóa, chỉ xóa vùng chứa văn bản (yêu cầu VRAM thấp)
SubtitleDetectMode = Chế độ phát hiện
ErrorDuringProcessing = Lỗi khi xử lý: {}
@@ -122,7 +118,6 @@ RequestError = Lỗi truy cập {}, lý do: {}
SttnAuto = STTN xóa thông minh
SttnDet = STTN phát hiện
LAMA = LAMA
ProPainter = ProPainter
OpenCV = OpenCV
[SubtitleDetectMode]
+2 -93
View File
@@ -19,7 +19,6 @@ from backend.inpaint.sttn_auto_inpaint import STTNAutoInpaint
from backend.inpaint.sttn_det_inpaint import STTNDetInpaint
from backend.inpaint.lama_inpaint import LamaInpaint
from backend.inpaint.opencv_inpaint import OpenCVInpaint
from backend.inpaint.propainter_inpaint import PropainterInpaint
from backend.tools.inpaint_tools import create_mask, batch_generator, expand_frame_ranges
from backend.tools.model_config import ModelConfig
from backend.tools.ffmpeg_cli import FFmpegCLI
@@ -67,7 +66,6 @@ class SubtitleRemover:
except Exception:
self.video_writer = cv2.VideoWriter(get_readable_path(self.video_temp_file.name), cv2.VideoWriter_fourcc(*'mp4v'), self.fps, self.size)
self.video_out_path = os.path.abspath(os.path.join(os.path.dirname(self.video_path), f'{self.vd_name}_no_sub.mp4'))
self.propainter_inpaint = None
self.ext = os.path.splitext(vd_path)[-1]
if self.is_picture:
pic_dir = os.path.join(os.path.dirname(self.video_path), 'no_sub')
@@ -156,94 +154,6 @@ class SubtitleRemover:
"""
pass
def propainter_mode(self, tbar):
sub_detector = SubtitleDetect(self.video_path, self.sub_areas)
sub_list = sub_detector.find_subtitle_frame_no(sub_remover=self)
if len(sub_list) == 0:
raise Exception(tr['Main']['NoSubtitleDetected'].format(self.video_path))
continuous_frame_no_list = sub_detector.find_continuous_ranges_with_same_mask(sub_list)
scene_div_points = sub_detector.get_scene_div_frame_no(self.video_path)
continuous_frame_no_list = sub_detector.split_range_by_scene(continuous_frame_no_list,
scene_div_points)
del sub_detector
gc.collect()
device = self.hardware_accelerator.device if self.hardware_accelerator.has_cuda() else torch.device("cpu")
propainter_inpaint = PropainterInpaint(device, self.model_config.PROPAINTER_MODEL_DIR, config.propainterMaxLoadNum.value)
self.append_output(tr['Main']['ProcessingStartRemovingSubtitles'])
index = 0
# 使用帧预读取,I/O 与推理重叠
reader = FramePrefetcher(self.video_cap)
while True:
ret, frame = reader.read()
if not ret:
break
index += 1
# 如果当前帧没有水印/文本则直接写
if index not in sub_list.keys():
self.video_writer.write(frame)
# self.append_output(f'write frame: {index}')
self.update_progress(tbar, increment=1)
self.update_preview_with_comp(frame, frame)
continue
# 如果有水印,判断该帧是不是开头帧
else:
# 如果是开头帧,则批推理到尾帧
if self.is_current_frame_no_start(index, continuous_frame_no_list):
# self.append_output(f'No 1 Current index: {index}')
start_frame_no = index
# self.append_output(f'find start: {start_frame_no}')
# 找到结束帧
end_frame_no = self.find_frame_no_end(index, continuous_frame_no_list)
# 判断当前帧号是不是字幕起始位置
# 如果获取的结束帧号不为-1则说明
if end_frame_no != -1:
# self.append_output(f'find end: {end_frame_no}')
# ************ 读取该区间所有帧 start ************
temp_frames = list()
# 将头帧加入处理列表
temp_frames.append(frame)
inner_index = 0
# 一直读取到尾帧
while index < end_frame_no:
ret, frame = reader.read()
if not ret:
break
index += 1
temp_frames.append(frame)
# ************ 读取该区间所有帧 end ************
if len(temp_frames) < 1:
# 没有待处理,直接跳过
continue
elif len(temp_frames) == 1:
inner_index += 1
single_mask = create_mask(self.mask_size, sub_list[index])
inpainted_frame = self.lama_inpaint.inpaint(frame, single_mask)
self.video_writer.write(inpainted_frame)
# self.append_output(f'write frame: {start_frame_no + inner_index} with mask {sub_list[start_frame_no]}')
self.update_progress(tbar, increment=1)
continue
else:
# 将读取的视频帧分批处理
# 1. 获取当前批次使用的mask
mask = create_mask(self.mask_size, sub_list[start_frame_no])
for batch in batch_generator(temp_frames, config.propainterMaxLoadNum.value):
# 2. 调用批推理
if len(batch) == 1:
single_mask = create_mask(self.mask_size, sub_list[start_frame_no])
inpainted_frame = self.lama_inpaint.inpaint(frame, single_mask)
self.video_writer.write(inpainted_frame)
# self.append_output(f'write frame: {start_frame_no + inner_index} with mask {sub_list[start_frame_no]}')
inner_index += 1
self.update_progress(tbar, increment=1)
elif len(batch) > 1:
inpainted_frames = propainter_inpaint(batch, mask)
for i, inpainted_frame in enumerate(inpainted_frames):
self.video_writer.write(inpainted_frame)
# self.append_output(f'write frame: {start_frame_no + inner_index} with mask {sub_list[index]}')
inner_index += 1
self.update_preview_with_comp(np.clip(batch[i]+mask[:,:,np.newaxis]*0.3,0,255).astype(np.uint8), inpainted_frame)
self.update_progress(tbar, increment=len(batch))
def sttn_auto_mode(self, tbar):
"""
使用sttn对选中区域进行重绘,不进行字幕检测
@@ -372,9 +282,7 @@ class SubtitleRemover:
else:
# 精准模式下,获取场景分割的帧号,进一步切割
self.log_model()
if config.inpaintMode.value == InpaintMode.PROPAINTER:
self.propainter_mode(tbar)
elif config.inpaintMode.value == InpaintMode.STTN_AUTO:
if config.inpaintMode.value == InpaintMode.STTN_AUTO:
self.sttn_auto_mode(tbar)
elif config.inpaintMode.value == InpaintMode.STTN_DET:
self.video_inpaint(tbar, self.sttn_det_inpaint)
@@ -471,6 +379,7 @@ class SubtitleRemover:
if __name__ == '__main__':
multiprocessing.freeze_support()
multiprocessing.set_start_method("spawn")
from backend.tools.args_handler import parse_args
args = parse_args()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
filename,filesize,encoding,header
ProPainter_1.pth,50000000,,
ProPainter_2.pth,50000000,,
ProPainter_3.pth,50000000,,
ProPainter_4.pth,7780510,,
1 filename filesize encoding header
2 ProPainter_1.pth 50000000
3 ProPainter_2.pth 50000000
4 ProPainter_3.pth 50000000
5 ProPainter_4.pth 7780510
Binary file not shown.
-1
View File
@@ -8,7 +8,6 @@ class InpaintMode(Enum):
STTN_AUTO = "sttn-auto"
STTN_DET = "sttn-det"
LAMA = "lama"
PROPAINTER = "propainter"
OPENCV = "opencv"
@unique
-2
View File
@@ -13,7 +13,6 @@ class ModelConfig:
self.LAMA_MODEL_DIR = os.path.join(BASE_DIR, 'models', 'big-lama')
self.STTN_AUTO_MODEL_PATH = os.path.join(BASE_DIR, 'models', 'sttn-auto', 'infer_model.pth')
self.STTN_DET_MODEL_PATH = os.path.join(BASE_DIR, 'models', 'sttn-det', 'sttn.pth')
self.PROPAINTER_MODEL_DIR = os.path.join(BASE_DIR,'models', 'propainter')
if config.subtitleDetectMode.value == SubtitleDetectMode.PP_OCRv5_MOBILE:
self.DET_MODEL_DIR = os.path.join(BASE_DIR,'models', 'V5', 'ch_det_fast')
elif config.subtitleDetectMode.value == SubtitleDetectMode.PP_OCRv5_SERVER:
@@ -23,4 +22,3 @@ class ModelConfig:
self.DET_MODEL_NAME = _MODEL_NAME_MAP[config.subtitleDetectMode.value]
merge_big_file_if_not_exists(self.LAMA_MODEL_DIR, 'bit-lama.pt')
merge_big_file_if_not_exists(self.PROPAINTER_MODEL_DIR, 'ProPainter.pth')
+10 -1
View File
@@ -8,6 +8,14 @@
import sys
import os
# PyInstaller 打包后资源路径处理
def resource_path(relative_path):
"""获取资源文件的绝对路径(兼容 PyInstaller 打包)"""
if getattr(sys, 'frozen', False):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath(os.path.dirname(__file__)), relative_path)
import configparser
import cv2
import multiprocessing
@@ -42,7 +50,7 @@ class SubtitleExtractorGUI(FluentWindow):
# self.themeListener.start()
# 设置窗口图标
self.setWindowIcon(QtGui.QIcon("design/vsr.ico"))
self.setWindowIcon(QtGui.QIcon(resource_path("design/vsr.ico")))
self.setWindowTitle(tr['SubtitleExtractorGUI']['Title'] + " v" + VERSION)
# 创建界面布局
self._create_layout()
@@ -162,6 +170,7 @@ class SubtitleExtractorGUI(FluentWindow):
if __name__ == '__main__':
multiprocessing.freeze_support()
multiprocessing.set_start_method("spawn")
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
-13
View File
@@ -56,9 +56,6 @@ class AdvancedSettingInterface(ScrollArea):
self.sttn_group.addSettingCard(self.sttn_max_load_num)
self.expandLayout.addWidget(self.sttn_group)
self.propainter_group.addSettingCard(self.propainter_max_load_num)
self.expandLayout.addWidget(self.propainter_group)
self.advanced_group.addSettingCard(self.save_directory)
self.advanced_group.addSettingCard(self.check_update_on_startup)
self.expandLayout.addWidget(self.advanced_group)
@@ -77,8 +74,6 @@ class AdvancedSettingInterface(ScrollArea):
self.subtitle_detection_group = SettingCardGroup(tr["Setting"]["SubtitleDetectionSetting"], self.scrollWidget)
# STTN设置组
self.sttn_group = SettingCardGroup(tr["Setting"]["SttnSetting"], self.scrollWidget)
# Propainter设置组
self.propainter_group = SettingCardGroup(tr["Setting"]["ProPainterSetting"], self.scrollWidget)
# 高级设置组
self.advanced_group = SettingCardGroup(tr["Setting"]["AdvancedSetting"], self.scrollWidget)
# 关于设置组
@@ -164,14 +159,6 @@ class AdvancedSettingInterface(ScrollArea):
parent=self.sttn_group
)
self.propainter_max_load_num = RangeSettingCard(
configItem=config.propainterMaxLoadNum,
icon=FluentIcon.DICTIONARY,
title=tr["Setting"]["PropainterMaxLoadNum"],
content=tr["Setting"]["PropainterMaxLoadNumDesc"],
parent=self.propainter_group
)
# 视频保存路径
self.save_directory = PushSettingCard(
text=tr["Setting"]["ChooseDirectory"],