diff --git a/.gitignore b/.gitignore index 134f2ac..2c4e7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,6 @@ ENV/ # Project files -.vscode \ No newline at end of file +.vscode +.idea/ +src/datasets/PPM-100 \ No newline at end of file diff --git a/README.md b/README.md index c30477b..e4ed531 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,24 @@ -

MODNet: Trimap-Free Portrait Matting in Real Time

+# 说明 +代码fork from [MODNet官方代码](https://github.com/ZHKKKe/MODNet) 。本项目完善了数据准备、模型评价及模型训练相关代码 +# 模型训练、评价、推理 +```bash +# 1. 下载代码并进入工作目录 +git clone https://github.com/actboy/MODNet +cd MODNet -
MODNet: Real-Time Trimap-Free Portrait Matting via Objective Decomposition (AAAI 2022)
+# 2. 安装依赖 +pip install -r src/requirements.txt -
+# 3. 下载并解压数据集 +wget -c https://paddleseg.bj.bcebos.com/matting/datasets/PPM-100.zip -O src/datasets/PPM-100.zip +unzip src/datasets/PPM-100.zip -d src/datasets - +# 4. 训练模型 +python src/trainer.py -
MODNet is a model for real-time portrait matting with only RGB image input
-
MODNet是一个仅需RGB图片输入实时人像抠图模型
+# 5. 模型评估 +python src/eval.py -
- -

- Online Application (在线应用) | - Research Demo | - AAAI 2022 Paper | - Supplementary Video -

- -

- Community | - Code | - PPM Benchmark | - License | - Acknowledgement | - Citation | - Contact -

- ---- - - -## Online Application (在线应用) - -A **Single** model! Only **7M**! Process **2K** resolution image with a **Fast** speed on common PCs or Mobiles! **Beter** than research demos! -Please try online portrait image matting via [this website](https://sight-x.cn/portrait_matting)! - -**单个**模型!大小仅为**7M**!可以在普通PC或移动设备上**快速**处理具有**2K**分辨率的图像!效果比研究示例**更好**! -请通过[此网站](https://sight-x.cn/portrait_matting)在线尝试图片抠像! - - -## Research Demo - -All the models behind the following demos are trained on the datasets mentioned in [our paper](https://arxiv.org/pdf/2011.11961.pdf). - -### Portrait Image Matting -We provide an [online Colab demo](https://colab.research.google.com/drive/1GANpbKT06aEFiW-Ssx0DQnnEADcXwQG6?usp=sharing) for portrait image matting. -It allows you to upload portrait images and predict/visualize/download the alpha mattes. - - - -### Portrait Video Matting -We provide two real-time portrait video matting demos based on WebCam. When using the demo, you can move the WebCam around at will. -If you have an Ubuntu system, we recommend you to try the [offline demo](demo/video_matting/webcam) to get a higher *fps*. Otherwise, you can access the [online Colab demo](https://colab.research.google.com/drive/1Pt3KDSc2q7WxFvekCnCLD8P0gBEbxm6J?usp=sharing). -We also provide an [offline demo](demo/video_matting/custom) that allows you to process custom videos. - - - - -## Community - -We share some cool applications/extentions of MODNet built by the community. - -- **WebGUI for Portrait Image Matting** -You can try [this WebGUI](https://www.gradio.app/hub/aliabd/modnet) (hosted on [Gradio](https://www.gradio.app/)) for portrait image matting from your browser without code! - -- **Colab Demo of Bokeh (Blur Background)** -You can try [this Colab demo](https://colab.research.google.com/github/eyaler/avatars4all/blob/master/yarok.ipynb) (built by [@eyaler](https://github.com/eyaler)) to blur the backgroud based on MODNet! - -- **ONNX Version of MODNet** -You can convert the pre-trained MODNet to an ONNX model by using [this code](onnx) (provided by [@manthan3C273](https://github.com/manthan3C273)). You can also try [this Colab demo](https://colab.research.google.com/drive/1P3cWtg8fnmu9karZHYDAtmm1vj1rgA-f?usp=sharing) for MODNet image matting (ONNX version). - -- **TorchScript Version of MODNet** -You can convert the pre-trained MODNet to an TorchScript model by using [this code](torchscript) (provided by [@yarkable](https://github.com/yarkable)). - -- **TensorRT Version of MODNet** -You can access [this Github repository](https://github.com/jkjung-avt/tensorrt_demos) to try the TensorRT version of MODNet (provided by [@jkjung-avt](https://github.com/jkjung-avt)). - - -There are some resources about MODNet from the community. -- [Video from What's AI YouTube Channel](https://youtu.be/rUo0wuVyefU) -- [Article from Louis Bouchard's Blog](https://www.louisbouchard.ai/remove-background/) - - -## Code -We provide the [code](src/trainer.py) of MODNet training iteration, including: -- **Supervised Training**: Train MODNet on a labeled matting dataset -- **SOC Adaptation**: Adapt a trained MODNet to an unlabeled dataset - -In code comments, we provide examples for using the functions. - - -## PPM Benchmark -The PPM benchmark is released in a separate repository [PPM](https://github.com/ZHKKKe/PPM). - - -## License -The code, models, and demos in this repository (excluding GIF files under the folder `doc/gif`) are released under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. - - -## Acknowledgement -- We thank -        [@yzhou0919](https://github.com/yzhou0919), [@eyaler](https://github.com/eyaler), [@manthan3C273](https://github.com/manthan3C273), [@yarkable](https://github.com/yarkable), [@jkjung-avt](https://github.com/jkjung-avt), [@manzke](https://github.com/manzke), -        [the Gradio team](https://github.com/gradio-app/gradio), [What's AI YouTube Channel](https://www.youtube.com/channel/UCUzGQrN-lyyc0BWTYoJM_Sg), [Louis Bouchard's Blog](https://www.louisbouchard.ai), -for their contributions to this repository or their cool applications/extentions/resources of MODNet. - - -## Citation -If this work helps your research, please consider to cite: - -```bibtex -@InProceedings{MODNet, - author = {Zhanghan Ke and Jiayu Sun and Kaican Li and Qiong Yan and Rynson W.H. Lau}, - title = {MODNet: Real-Time Trimap-Free Portrait Matting via Objective Decomposition}, - booktitle = {AAAI}, - year = {2022}, -} +# 6. 模型推理 +python src/infer.py ``` - - -## Contact -This repository is currently maintained by Zhanghan Ke ([@ZHKKKe](https://github.com/ZHKKKe)). -For questions, please contact `kezhanghan@outlook.com`. - - diff --git a/pretrained/mobilenetv2_human_seg.ckpt b/pretrained/mobilenetv2_human_seg.ckpt new file mode 100644 index 0000000..96f2da8 Binary files /dev/null and b/pretrained/mobilenetv2_human_seg.ckpt differ diff --git a/pretrained/modnet_photographic_portrait_matting.ckpt b/pretrained/modnet_photographic_portrait_matting.ckpt new file mode 100644 index 0000000..ddb4a4b Binary files /dev/null and b/pretrained/modnet_photographic_portrait_matting.ckpt differ diff --git a/src/eval.py b/src/eval.py new file mode 100644 index 0000000..7106051 --- /dev/null +++ b/src/eval.py @@ -0,0 +1,72 @@ +import numpy as np +from glob import glob +from models.modnet import MODNet +from PIL import Image +from infer import predit_matte +import torch.nn as nn +import torch + + +def cal_mad(pred, gt): + diff = pred - gt + diff = np.abs(diff) + mad = np.mean(diff) + return mad + + +def cal_mse(pred, gt): + diff = pred - gt + diff = diff ** 2 + mse = np.mean(diff) + return mse + + +def load_eval_dataset(dataset_root_dir='src/datasets/PPM-100'): + image_path = dataset_root_dir + '/val/fg/*' + matte_path = dataset_root_dir + '/val/alpha/*' + image_file_name_list = glob(image_path) + image_file_name_list = sorted(image_file_name_list) + matte_file_name_list = glob(matte_path) + matte_file_name_list = sorted(matte_file_name_list) + + return image_file_name_list, matte_file_name_list + + +def eval(modnet: MODNet, dataset): + mse = total_mse = 0.0 + mad = total_mad = 0.0 + cnt = 0 + + for im_pth, mt_pth in zip(dataset[0], dataset[1]): + im = Image.open(im_pth) + pd_matte = predit_matte(modnet, im) + + gt_matte = Image.open(mt_pth) + gt_matte = np.asarray(gt_matte) / 255 + + total_mse += cal_mse(pd_matte, gt_matte) + total_mad += cal_mad(pd_matte, gt_matte) + + cnt += 1 + if cnt > 0: + mse = total_mse / cnt + mad = total_mad / cnt + + return mse, mad + + +if __name__ == '__main__': + # create MODNet and load the pre-trained ckpt + modnet = MODNet(backbone_pretrained=False) + modnet = nn.DataParallel(modnet) + + ckp_pth = 'pretrained/modnet_photographic_portrait_matting.ckpt' + if torch.cuda.is_available(): + modnet = modnet.cuda() + weights = torch.load(ckp_pth) + else: + weights = torch.load(ckp_pth, map_location=torch.device('cpu')) + modnet.load_state_dict(weights) + dataset = load_eval_dataset('src/datasets/PPM-100') + mse, mad = eval(modnet, dataset) + print(f'mse: {mse:6f}, mad: {mad:6f}') diff --git a/src/infer.py b/src/infer.py new file mode 100644 index 0000000..116807b --- /dev/null +++ b/src/infer.py @@ -0,0 +1,85 @@ +from models.modnet import MODNet +from PIL import Image +import numpy as np +from torchvision import transforms +import torch +import torch.nn.functional as F +import torch.nn as nn + + +def predit_matte(modnet: MODNet, im: Image): + # define image to tensor transform + im_transform = transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) + ] + ) + + # define hyper-parameters + ref_size = 512 + + modnet.eval() + + # unify image channels to 3 + im = np.asarray(im) + if len(im.shape) == 2: + im = im[:, :, None] + if im.shape[2] == 1: + im = np.repeat(im, 3, axis=2) + elif im.shape[2] == 4: + im = im[:, :, 0:3] + + im = Image.fromarray(im) + # convert image to PyTorch tensor + im = im_transform(im) + + # add mini-batch dim + im = im[None, :, :, :] + + # resize image for input + im_b, im_c, im_h, im_w = im.shape + if max(im_h, im_w) < ref_size or min(im_h, im_w) > ref_size: + if im_w >= im_h: + im_rh = ref_size + im_rw = int(im_w / im_h * ref_size) + elif im_w < im_h: + im_rw = ref_size + im_rh = int(im_h / im_w * ref_size) + else: + im_rh = im_h + im_rw = im_w + + im_rw = im_rw - im_rw % 32 + im_rh = im_rh - im_rh % 32 + im = F.interpolate(im, size=(im_rh, im_rw), mode='area') + + # inference + _, _, matte = modnet(im.cuda() if torch.cuda.is_available() else im, True) + + # resize and save matte + matte = F.interpolate(matte, size=(im_h, im_w), mode='area') + matte = matte[0][0].data.cpu().numpy() + return matte + + +if __name__ == '__main__': + # create MODNet and load the pre-trained ckpt + modnet = MODNet(backbone_pretrained=False) + modnet = nn.DataParallel(modnet) + + ckp_pth = 'pretrained/modnet_photographic_portrait_matting.ckpt' + if torch.cuda.is_available(): + modnet = modnet.cuda() + weights = torch.load(ckp_pth) + else: + weights = torch.load(ckp_pth, map_location=torch.device('cpu')) + modnet.load_state_dict(weights) + + pth = 'src/datasets/PPM-100/val/fg/5588688353_3426d4b5d9_o.jpg' + img = Image.open(pth) + + matte = predit_matte(modnet, img) + prd_img = Image.fromarray(((matte * 255).astype('uint8')), mode='L') + prd_img.save('test_predic.jpg') + diff --git a/src/matting_dataset.py b/src/matting_dataset.py new file mode 100644 index 0000000..4e404fb --- /dev/null +++ b/src/matting_dataset.py @@ -0,0 +1,195 @@ +from torch.utils.data import Dataset +import numpy as np +import random +import cv2 +from glob import glob +import torch +from torchvision.transforms import functional as F +from torchvision import transforms +from PIL import Image + + +class MattingDataset(Dataset): + def __init__(self, + dataset_root_dir='src/datasets/PPM-100', + transform=None): + image_path = dataset_root_dir + '/train/fg/*' + matte_path = dataset_root_dir + '/train/alpha/*' + image_file_name_list = glob(image_path) + matte_file_name_list = glob(matte_path) + + self.image_file_name_list = sorted(image_file_name_list) + self.matte_file_name_list = sorted(matte_file_name_list) + for img, mat in zip(self.image_file_name_list, self.matte_file_name_list): + img_name = img.split('/')[-1] + mat_name = mat.split('/')[-1] + assert img_name == mat_name + + self.transform = transform + + def __len__(self): + return len(self.image_file_name_list) + + def __getitem__(self, index): + image_file_name = self.image_file_name_list[index] + matte_file_name = self.matte_file_name_list[index] + + image = Image.open(image_file_name) + matte = Image.open(matte_file_name) + + data = {'image': image, 'gt_matte': matte} + + if self.transform: + data = self.transform(data) + return data + + +class Rescale(object): + def __init__(self, output_size): + assert isinstance(output_size, (int, tuple)) + self.output_size = output_size + + def __call__(self, sample): + image, gt_matte = sample['image'], sample['gt_matte'] + + new_h, new_w = int(self.output_size), int(self.output_size) + new_img = F.resize(image, (new_h, new_w)) + new_gt_matte = F.resize(gt_matte, (new_h, new_w)) + + return {'image': new_img,'gt_matte': new_gt_matte} + + +class GenTrimap(object): + def __call__(self, sample): + gt_matte = sample['gt_matte'] + trimap = self.gen_trimap(gt_matte) + sample['trimap'] = trimap + return sample + + @staticmethod + def gen_trimap(matte): + """ + 根据归matte生成归一化的trimap + """ + matte = np.array(matte) + k_size = random.choice(range(2, 5)) + iterations = np.random.randint(5, 15) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, + (k_size, k_size)) # cv2.MORPH_RECT, cv2.MORPH_CROSS + dilated = cv2.dilate(matte, kernel, iterations=iterations) + eroded = cv2.erode(matte, kernel, iterations=iterations) + + trimap = np.zeros(matte.shape) + trimap.fill(0.5) + trimap[eroded > 254.5] = 1.0 + trimap[dilated < 0.5] = 0.0 + trimap = Image.fromarray(trimap) + return trimap + + +class ToTensor(object): + """Convert ndarrays in sample to Tensors.""" + + def __call__(self, sample): + image, trimap, gt_matte = sample['image'], sample['trimap'], sample['gt_matte'] + image = F.pil_to_tensor(image) + trimap = F.pil_to_tensor(trimap) + gt_matte = F.pil_to_tensor(gt_matte) + + + return {'image': image, + 'trimap': trimap, + 'gt_matte': gt_matte} + + +class ConvertImageDtype(object): + def __call__(self, sample): + image, trimap, gt_matte = sample['image'], sample['trimap'], sample['gt_matte'] + image = F.convert_image_dtype(image, torch.float) + trimap = F.convert_image_dtype(trimap, torch.float) + gt_matte = F.convert_image_dtype(gt_matte, torch.float) + + return {'image': image, 'trimap': trimap, 'gt_matte': gt_matte} + + +class Normalize(object): + def __init__(self, mean, std, inplace=False): + self.mean = mean + self.std = std + self.inplace = inplace + + def __call__(self, sample): + image, trimap, gt_matte = sample['image'], sample['trimap'], sample['gt_matte'] + image = image.type(torch.FloatTensor) + image = F.normalize(image, self.mean, self.std, self.inplace) + sample['image'] = image + sample['gt_matte'] = sample['gt_matte'] / 255 + return sample + + +class ToTrainArray(object): + def __call__(self, sample): + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + image = sample['image'].to(device) + trimap = sample['trimap'].to(device) + gt_matte = sample['gt_matte'].to(device) + + return [image, trimap, gt_matte] + # return [sample['image'], sample['trimap'], sample['gt_matte']] + + +if __name__ == '__main__': + + # test MattingDataset.gen_trimap + matte = Image.open('src/datasets/PPM-100/train/alpha/6146816_556eaff97f_o.jpg') + trimap1 = GenTrimap().gen_trimap(matte) + trimap1 = np.array(trimap1) * 255 + trimap1 = np.uint8(trimap1) + trimap1 = Image.fromarray(trimap1) + trimap1.save('test_trimap.png') + + # test MattingDataset + transform = transforms.Compose([ + Rescale(512), + GenTrimap(), + ToTensor(), + # Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) + ]) + mattingDataset = MattingDataset(transform=transform) + + import matplotlib.pyplot as plt + + fig = plt.figure() + + for i in range(len(mattingDataset)): + sample = mattingDataset[i] + print(mattingDataset.image_file_name_list[i]) + # print(sample) + print(i, sample['image'].shape, sample['trimap'].shape, sample['gt_matte'].shape) + + # break + + ax = plt.subplot(4, 3, 3 * i + 1) + plt.tight_layout() + ax.set_title('image #{}'.format(i)) + ax.axis('off') + img = transforms.ToPILImage()(sample['image']) + plt.imshow(img) + + ax = plt.subplot(4, 3, 3 * i + 2) + plt.tight_layout() + ax.set_title('gt_matte #{}'.format(i)) + ax.axis('off') + img = transforms.ToPILImage()(sample['gt_matte']) + plt.imshow(img) + + ax = plt.subplot(4, 3, 3 * i + 3) + plt.tight_layout() + ax.set_title('trimap #{}'.format(i)) + ax.axis('off') + img = transforms.ToPILImage()(sample['trimap']) + plt.imshow(img) + + if i == 3: + plt.show() + break diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..d1b497d --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,7 @@ +torch==1.10.2 +scipy==1.7.3 +numpy==1.21.5 +opencv-python==4.5.5.62 +matplotlib==3.5.1 +torchvision==0.11.3 +Pillow==9.0.1 \ No newline at end of file diff --git a/src/setting.py b/src/setting.py new file mode 100644 index 0000000..9e2070d --- /dev/null +++ b/src/setting.py @@ -0,0 +1,9 @@ +BS = 16 # BATCH SIZE +LR = 0.01 # LEARN RATE +EPOCHS = 4 # TOTAL EPOCHS + +SEMANTIC_SCALE = 10.0 +DETAIL_SCALE = 10.0 +MATTE_SCALE = 1.0 + +SAVE_EPOCH_STEP = 3 diff --git a/src/trainer.py b/src/trainer.py index bd3d8be..becb4af 100644 --- a/src/trainer.py +++ b/src/trainer.py @@ -7,7 +7,6 @@ import torch.nn as nn import torch.nn.functional as F - __all__ = [ 'supervised_training_iter', 'soc_adaptation_iter', @@ -80,7 +79,9 @@ def _init_kernel(self): # MODNet Training Functions # ---------------------------------------------------------------------------------- -blurer = GaussianBlurLayer(1, 3).cuda() +blurer = GaussianBlurLayer(1, 3) #.cuda +if torch.cuda.is_available(): + blurer.cuda() def supervised_training_iter( @@ -297,3 +298,59 @@ def soc_adaptation_iter( return soc_semantic_loss, soc_detail_loss # ---------------------------------------------------------------------------------- + + +if __name__ == '__main__': + from matting_dataset import MattingDataset, Rescale, \ + ToTensor, Normalize, ToTrainArray, \ + ConvertImageDtype, GenTrimap + from torchvision import transforms + from torch.utils.data import DataLoader + from models.modnet import MODNet + from setting import BS, LR, EPOCHS, SEMANTIC_SCALE, DETAIL_SCALE, MATTE_SCALE, SAVE_EPOCH_STEP + transform = transforms.Compose([ + Rescale(512), + GenTrimap(), + ToTensor(), + ConvertImageDtype(), + Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ToTrainArray() + ]) + mattingDataset = MattingDataset(transform=transform) + + modnet = torch.nn.DataParallel(MODNet()) #.cuda() + if torch.cuda.is_available(): + modnet = modnet.cuda() + + optimizer = torch.optim.SGD(modnet.parameters(), lr=LR, momentum=0.9) + lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=int(0.25 * EPOCHS), gamma=0.1) + + dataloader = DataLoader(mattingDataset, + batch_size=BS, + shuffle=True) + + for epoch in range(0, EPOCHS): + print(f'epoch: {epoch}/{EPOCHS-1}') + for idx, (image, trimap, gt_matte) in enumerate(dataloader): + semantic_loss, detail_loss, matte_loss = \ + supervised_training_iter(modnet, optimizer, image, trimap, gt_matte, + semantic_scale=SEMANTIC_SCALE, + detail_scale=DETAIL_SCALE, + matte_scale=MATTE_SCALE) + print(f'{(idx+1) * BS}/{len(mattingDataset)} --- ' + f'semantic_loss: {semantic_loss:f}, detail_loss: {detail_loss:f}, matte_loss: {matte_loss:f}\r', + end='') + lr_scheduler.step() + # 保存中间训练结果 + if epoch % SAVE_EPOCH_STEP == 0 and epoch > 1: + torch.save({ + 'epoch': epoch, + 'model_state_dict': modnet.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'loss': {'semantic_loss': semantic_loss, 'detail_loss': detail_loss, 'matte_loss': matte_loss}, + }, f'pretrained/modnet_custom_portrait_matting_{epoch}_th.ckpt') + print(f'{len(mattingDataset)}/{len(mattingDataset)} --- ' + f'semantic_loss: {semantic_loss:f}, detail_loss: {detail_loss:f}, matte_loss: {matte_loss:f}') + + # 仅保存模型权重参数 + torch.save(modnet.state_dict(), f'pretrained/modnet_custom_portrait_matting_last_epoch_weight.ckpt')