├── GOODBYE_LAVER.ipynb ├── README.md ├── hent-AI ├── ColabESRGAN │ ├── LICENSE │ ├── QA.md │ ├── architecture.py │ ├── block.py │ ├── models │ │ └── README.md │ ├── net_interp.py │ └── test.py ├── LICENSE ├── README.md ├── detector.py ├── green_mask_project_mosaic_resolution.py ├── main.py ├── mrcnn │ ├── __init__.py │ ├── config.py │ ├── model.py │ ├── parallel_model.py │ ├── utils.py │ └── visualize.py ├── requirements.txt ├── samples │ └── hentai │ │ └── hentai.py ├── setup.cfg ├── setup.py └── test_combined_generator.py ├── inpainting.py ├── lama.py └── lama_cleaner ├── Dockerfile ├── LICENSE ├── README.md ├── __init__.py ├── app ├── .env ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── build │ ├── asset-manifest.json │ ├── index.html │ └── static │ │ ├── css │ │ └── main.4201b632.chunk.css │ │ ├── js │ │ ├── 2.2d367d07.chunk.js │ │ ├── 2.2d367d07.chunk.js.LICENSE.txt │ │ ├── main.18cd2cfc.chunk.js │ │ └── runtime-main.5e86ac81.js │ │ └── media │ │ ├── WorkSans-Black.67c2c5a1.ttf │ │ ├── WorkSans-Bold.2bea7a7f.ttf │ │ ├── WorkSans-Regular.bb287b89.ttf │ │ └── WorkSans-SemiBold.1e98db4e.ttf ├── package.json ├── public │ └── index.html ├── src │ ├── App.tsx │ ├── adapters │ │ └── inpainting.ts │ ├── components │ │ ├── Editor │ │ │ ├── Editor.scss │ │ │ ├── Editor.tsx │ │ │ ├── SizeSelector.tsx │ │ │ └── Slider.tsx │ │ ├── FileSelect │ │ │ ├── FileSelect.scss │ │ │ └── FileSelect.tsx │ │ ├── Header │ │ │ ├── Header.scss │ │ │ ├── Header.tsx │ │ │ ├── ThemeChanger.scss │ │ │ └── ThemeChanger.tsx │ │ ├── LandingPage │ │ │ ├── LandingPage.scss │ │ │ └── LandingPage.tsx │ │ ├── Settings │ │ │ ├── HDSettingBlock.scss │ │ │ ├── HDSettingBlock.tsx │ │ │ ├── ManualRunInpaintingSettingBlock.tsx │ │ │ ├── ModelSettingBlock.scss │ │ │ ├── ModelSettingBlock.tsx │ │ │ ├── NumberInputSetting.tsx │ │ │ ├── SettingBlock.scss │ │ │ ├── SettingBlock.tsx │ │ │ ├── SettingIcon.tsx │ │ │ ├── Settings.scss │ │ │ └── SettingsModal.tsx │ │ ├── Shortcuts │ │ │ ├── Shortcuts.scss │ │ │ ├── Shortcuts.tsx │ │ │ └── ShortcutsModal.tsx │ │ ├── Workspace.tsx │ │ └── shared │ │ │ ├── Button.scss │ │ │ ├── Button.tsx │ │ │ ├── Link.tsx │ │ │ ├── Modal.scss │ │ │ ├── Modal.tsx │ │ │ ├── NumberInput.scss │ │ │ ├── NumberInput.tsx │ │ │ ├── Selector.scss │ │ │ ├── Selector.tsx │ │ │ ├── Switch.scss │ │ │ ├── Switch.tsx │ │ │ ├── Toast.scss │ │ │ └── Toast.tsx │ ├── hooks │ │ ├── useInputImage.tsx │ │ └── useResolution.tsx │ ├── index.tsx │ ├── media │ │ └── fonts │ │ │ └── Work_Sans │ │ │ ├── WorkSans-Black.ttf │ │ │ ├── WorkSans-BlackItalic.ttf │ │ │ ├── WorkSans-Bold.ttf │ │ │ ├── WorkSans-BoldItalic.ttf │ │ │ ├── WorkSans-ExtraBold.ttf │ │ │ ├── WorkSans-ExtraBoldItalic.ttf │ │ │ ├── WorkSans-ExtraLight.ttf │ │ │ ├── WorkSans-ExtraLightItalic.ttf │ │ │ ├── WorkSans-Italic.ttf │ │ │ ├── WorkSans-Light.ttf │ │ │ ├── WorkSans-LightItalic.ttf │ │ │ ├── WorkSans-Medium.ttf │ │ │ ├── WorkSans-MediumItalic.ttf │ │ │ ├── WorkSans-Regular.ttf │ │ │ ├── WorkSans-SemiBold.ttf │ │ │ ├── WorkSans-SemiBoldItalic.ttf │ │ │ ├── WorkSans-Thin.ttf │ │ │ └── WorkSans-ThinItalic.ttf │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── store │ │ └── Atoms.tsx │ ├── styles │ │ ├── App.scss │ │ ├── Mixins │ │ │ ├── _MediaQueries.scss │ │ │ ├── _Mixins.scss │ │ │ └── _index.scss │ │ ├── _Animations.scss │ │ ├── _Colors.scss │ │ ├── _ColorsDark.scss │ │ ├── _Fonts.scss │ │ └── _index.scss │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── benchmark.py ├── helper.py ├── model ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── base.cpython-38.pyc │ ├── lama.cpython-38.pyc │ ├── ldm.cpython-38.pyc │ └── utils.cpython-38.pyc ├── base.py ├── lama.py ├── ldm.py └── utils.py ├── model_manager.py ├── parse_args.py ├── publish.sh ├── requirements-dev.txt ├── requirements.txt ├── schema.py ├── server.py ├── setup.py └── tests ├── __init__.py ├── image.png ├── lama_crop_result.png ├── lama_original_result.png ├── lama_resize_result.png ├── ldm_crop_result.png ├── ldm_original_result.png ├── ldm_resize_result.png ├── mask.png └── test_model.py /GOODBYE_LAVER.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "id": "95a87y7f8t0z" 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "# Clone GoodbyeLaver\n", 12 | "!git clone https://github.com/kdrkdrkdr/GoodbyeLaver\n", 13 | "\n", 14 | "# Install hent-AI\n", 15 | "%cd /content/GoodbyeLaver/hent-AI/\n", 16 | "!wget https://github.com/kdrkdrkdr/GoodbyeLaver/releases/download/1.0/4x_FatalPixels_340000_G.pth\n", 17 | "!wget https://github.com/kdrkdrkdr/GoodbyeLaver/releases/download/1.0/weights.h5\n", 18 | "!pip install --upgrade pip\n", 19 | "!pip install -r requirements.txt\n", 20 | "\n", 21 | "# Downgrade tensorflow and keras\n", 22 | "!pip uninstall tensorflow -y\n", 23 | "!pip uninstall keras -y\n", 24 | "!pip install keras==2.3.1\n", 25 | "!pip install tensorflow-gpu==1.15\n", 26 | "!apt install --allow-change-held-packages libcudnn7=7.4.1.5-1+cuda10.0\n", 27 | "\n", 28 | "!python setup.py install\n", 29 | "!add-apt-repository ppa:jon-severinsson/ffmpeg\n", 30 | "!apt-get update\n", 31 | "!apt-get install ffmpeg\n", 32 | "\n", 33 | "# Install lama_cleaner\n", 34 | "%cd /content/GoodbyeLaver/lama_cleaner/\n", 35 | "!pip install -r requirements.txt\n", 36 | "\n", 37 | "# Make directory\n", 38 | "!mkdir /content/image_original/\n", 39 | "!mkdir /content/image_detected/\n", 40 | "!mkdir /content/image_uncensored/\n", 41 | "\n", 42 | "# Run lama server\n", 43 | "%cd /content/GoodbyeLaver/\n", 44 | "!nohup python lama.py --port 5003 &\n", 45 | "\n", 46 | "# Load CUDA\n", 47 | "%cd /content/GoodbyeLaver/hent-AI/\n", 48 | "from detector import Detector\n", 49 | "d = Detector('weights.h5')" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": { 56 | "id": "FdonSouNSo2l" 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "# Run Decensoring!\n", 61 | "# Upload the censored image to the \"/content/image_original/\" folder, and run this cell.\n", 62 | "!rm -rf /content/image_detected/*\n", 63 | "!rm -rf /content/image_uncensored/*\n", 64 | "!rm -rf /content/uncensored_image.zip\n", 65 | "\n", 66 | "# Convert image files to png files.\n", 67 | "import os\n", 68 | "from PIL import Image\n", 69 | "files = os.listdir(\"/content/image_original/\")\n", 70 | "for filename in files:\n", 71 | " file_wo_ext, file_ext = os.path.splitext(filename)\n", 72 | " if file_ext.lower() != \".png\":\n", 73 | " img_path = f'/content/image_original/{filename}'\n", 74 | " Image.open(img_path).save(f'/content/image_original/{file_wo_ext}.png', 'PNG')\n", 75 | " os.remove(img_path) \n", 76 | "\n", 77 | "# Detecting bar censored\n", 78 | "%cd /content/GoodbyeLaver/hent-AI/\n", 79 | "d.run_on_folder(\n", 80 | " input_folder='/content/image_original/',\n", 81 | " output_folder='/content/image_detected/', \n", 82 | " is_video=False, \n", 83 | " is_mosaic=False,\n", 84 | " dilation=3\n", 85 | ")\n", 86 | "\n", 87 | "# Inpainting image\n", 88 | "%cd /content/GoodbyeLaver/\n", 89 | "!python inpainting.py\n", 90 | "\n", 91 | "# Download Uncensored Image\n", 92 | "!zip -r /content/uncensored_image.zip /content/image_uncensored/\n", 93 | "from google.colab import files\n", 94 | "files.download(\"/content/uncensored_image.zip\")" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 6, 100 | "metadata": { 101 | "id": "cRm-J9-nofNX" 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "# Run this cell if you want to delete all uploaded images.\n", 106 | "!rm -rf /content/image_original/*" 107 | ] 108 | } 109 | ], 110 | "metadata": { 111 | "accelerator": "GPU", 112 | "colab": { 113 | "collapsed_sections": [], 114 | "name": "hentai.ipynb", 115 | "provenance": [] 116 | }, 117 | "kernelspec": { 118 | "display_name": "Python 3", 119 | "name": "python3" 120 | }, 121 | "language_info": { 122 | "name": "python" 123 | } 124 | }, 125 | "nbformat": 4, 126 | "nbformat_minor": 0 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoodbyeLaver 2 | Laver means censored bar. 3 | 4 | ### Project Purpose 5 | This project is designed to automate the process of eliminating censorship through lama-inpainting and hent-AI. 6 | 7 | ### Usage 8 | You must set runtime as GPU. 9 | Open the [GOODBYE_LAVER.ipynb](https://colab.research.google.com/drive/1MJ6w3nCzVfAX2bwEdxkPfgzQgAa12oFu?usp=sharing) file in colab. -------------------------------------------------------------------------------- /hent-AI/ColabESRGAN/QA.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ### 1. How to reproduce your results in the [PIRM18-SR Challenge](https://www.pirm2018.org/PIRM-SR.html) (with low perceptual index)? 4 | 5 | First, the released ESRGAN model in the GitHub (`RRDB_ESRGAN_x4.pth`) is **different** from the model we submitted in the competition. 6 | We found that the lower perceptual index does not always guarantee a better visual quality. 7 | The aims for the competition and our ESRGAN work will be a bit different. 8 | We think the aim for the competition is the lower perceptual index and the aim for our ESRGAN work is the better visual quality. 9 | > More analyses can be found in Sec 4.1 and Sec 5 in [PIRM18-SR Chanllenge report](https://arxiv.org/pdf/1809.07517.pdf). 10 | > It points out that PI (perceptual index) is well correlated with the human-opinion-scores on a coarse scale, but it is not always well-correlated with these scores on a finer scale. This highlights the urgent need for better perceptual quality metrics.) 11 | 12 | Therefore, in the PIRM18-SR Challenge competition, we used several tricks for the best perceptual index (see Section 4.5 in the [paper](https://arxiv.org/abs/1809.00219)). 13 | 14 | Here, we provid the models and codes used in the competition, which is able to produce the results on the `PIRM test dataset` (we use MATLAB 2016b/2017a): 15 | 16 | | Group | Perceptual index | RMSE | 17 | | ------------- |:-------------:| -----:| 18 | | SuperSR | 1.978 | 15.30 | 19 | 20 | > 1. Download the model and codes from [GoogleDrive](https://drive.google.com/file/d/1l0gBRMqhVLpL_-7R7aN-q-3hnv5ADFSM/view?usp=sharing) 21 | > 2. Put LR input images in the `LR` folder 22 | > 3. Run `python test.py` 23 | > 4. Run `main_reverse_filter.m` in MATLAB as a post processing 24 | > 5. The results on my computer are: Perceptual index: **1.9777** and RMSE: **15.304** 25 | 26 | 27 | ### 2. How do you get the perceptual index in your ESRGAN paper? 28 | In our paper, we provide the perceptual index in two places. 29 | 30 | 1). In the Fig. 2, the perceptual index on PIRM self validation dataset is obtained with the **model we submitted in the competition**. 31 | Since the pupose of this figure is to show the perception-distortion plane. And we also use the post-precessing here same as in the competition. 32 | 33 | 2). In the Fig.7, the perceptual indexs are provided as references and they are tested on the data generated by the released ESRGAN model `RRDB_ESRGAN_x4.pth` in the GiuHub. 34 | Also, there is **no** post-processing when testing the ESRGAN model for better visual quality. 35 | -------------------------------------------------------------------------------- /hent-AI/ColabESRGAN/architecture.py: -------------------------------------------------------------------------------- 1 | import math 2 | import torch 3 | import torch.nn as nn 4 | import block as B 5 | 6 | 7 | class RRDB_Net(nn.Module): 8 | def __init__(self, in_nc, out_nc, nf, nb, gc=32, upscale=4, norm_type=None, act_type='leakyrelu', \ 9 | mode='CNA', res_scale=1, upsample_mode='upconv'): 10 | super(RRDB_Net, self).__init__() 11 | n_upscale = int(math.log(upscale, 2)) 12 | if upscale == 3: 13 | n_upscale = 1 14 | 15 | fea_conv = B.conv_block(in_nc, nf, kernel_size=3, norm_type=None, act_type=None) 16 | rb_blocks = [B.RRDB(nf, kernel_size=3, gc=32, stride=1, bias=True, pad_type='zero', \ 17 | norm_type=norm_type, act_type=act_type, mode='CNA') for _ in range(nb)] 18 | LR_conv = B.conv_block(nf, nf, kernel_size=3, norm_type=norm_type, act_type=None, mode=mode) 19 | 20 | if upsample_mode == 'upconv': 21 | upsample_block = B.upconv_blcok 22 | elif upsample_mode == 'pixelshuffle': 23 | upsample_block = B.pixelshuffle_block 24 | else: 25 | raise NotImplementedError('upsample mode [%s] is not found' % upsample_mode) 26 | if upscale == 3: 27 | upsampler = upsample_block(nf, nf, 3, act_type=act_type) 28 | else: 29 | upsampler = [upsample_block(nf, nf, act_type=act_type) for _ in range(n_upscale)] 30 | HR_conv0 = B.conv_block(nf, nf, kernel_size=3, norm_type=None, act_type=act_type) 31 | HR_conv1 = B.conv_block(nf, out_nc, kernel_size=3, norm_type=None, act_type=None) 32 | 33 | self.model = B.sequential(fea_conv, B.ShortcutBlock(B.sequential(*rb_blocks, LR_conv)),\ 34 | *upsampler, HR_conv0, HR_conv1) 35 | 36 | def forward(self, x): 37 | x = self.model(x) 38 | return x 39 | -------------------------------------------------------------------------------- /hent-AI/ColabESRGAN/block.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import torch 3 | import torch.nn as nn 4 | 5 | #################### 6 | # Basic blocks 7 | #################### 8 | 9 | 10 | def act(act_type, inplace=True, neg_slope=0.2, n_prelu=1): 11 | # helper selecting activation 12 | # neg_slope: for leakyrelu and init of prelu 13 | # n_prelu: for p_relu num_parameters 14 | act_type = act_type.lower() 15 | if act_type == 'relu': 16 | layer = nn.ReLU(inplace) 17 | elif act_type == 'leakyrelu': 18 | layer = nn.LeakyReLU(neg_slope, inplace) 19 | elif act_type == 'prelu': 20 | layer = nn.PReLU(num_parameters=n_prelu, init=neg_slope) 21 | else: 22 | raise NotImplementedError('activation layer [%s] is not found' % act_type) 23 | return layer 24 | 25 | 26 | def norm(norm_type, nc): 27 | # helper selecting normalization layer 28 | norm_type = norm_type.lower() 29 | if norm_type == 'batch': 30 | layer = nn.BatchNorm2d(nc, affine=True) 31 | elif norm_type == 'instance': 32 | layer = nn.InstanceNorm2d(nc, affine=False) 33 | else: 34 | raise NotImplementedError('normalization layer [%s] is not found' % norm_type) 35 | return layer 36 | 37 | 38 | def pad(pad_type, padding): 39 | # helper selecting padding layer 40 | # if padding is 'zero', do by conv layers 41 | pad_type = pad_type.lower() 42 | if padding == 0: 43 | return None 44 | if pad_type == 'reflect': 45 | layer = nn.ReflectionPad2d(padding) 46 | elif pad_type == 'replicate': 47 | layer = nn.ReplicationPad2d(padding) 48 | else: 49 | raise NotImplementedError('padding layer [%s] is not implemented' % pad_type) 50 | return layer 51 | 52 | 53 | def get_valid_padding(kernel_size, dilation): 54 | kernel_size = kernel_size + (kernel_size - 1) * (dilation - 1) 55 | padding = (kernel_size - 1) // 2 56 | return padding 57 | 58 | 59 | class ConcatBlock(nn.Module): 60 | # Concat the output of a submodule to its input 61 | def __init__(self, submodule): 62 | super(ConcatBlock, self).__init__() 63 | self.sub = submodule 64 | 65 | def forward(self, x): 66 | output = torch.cat((x, self.sub(x)), dim=1) 67 | return output 68 | 69 | def __repr__(self): 70 | tmpstr = 'Identity .. \n|' 71 | modstr = self.sub.__repr__().replace('\n', '\n|') 72 | tmpstr = tmpstr + modstr 73 | return tmpstr 74 | 75 | 76 | class ShortcutBlock(nn.Module): 77 | #Elementwise sum the output of a submodule to its input 78 | def __init__(self, submodule): 79 | super(ShortcutBlock, self).__init__() 80 | self.sub = submodule 81 | 82 | def forward(self, x): 83 | output = x + self.sub(x) 84 | return output 85 | 86 | def __repr__(self): 87 | tmpstr = 'Identity + \n|' 88 | modstr = self.sub.__repr__().replace('\n', '\n|') 89 | tmpstr = tmpstr + modstr 90 | return tmpstr 91 | 92 | 93 | def sequential(*args): 94 | # Flatten Sequential. It unwraps nn.Sequential. 95 | if len(args) == 1: 96 | if isinstance(args[0], OrderedDict): 97 | raise NotImplementedError('sequential does not support OrderedDict input.') 98 | return args[0] # No sequential is needed. 99 | modules = [] 100 | for module in args: 101 | if isinstance(module, nn.Sequential): 102 | for submodule in module.children(): 103 | modules.append(submodule) 104 | elif isinstance(module, nn.Module): 105 | modules.append(module) 106 | return nn.Sequential(*modules) 107 | 108 | 109 | def conv_block(in_nc, out_nc, kernel_size, stride=1, dilation=1, groups=1, bias=True, 110 | pad_type='zero', norm_type=None, act_type='relu', mode='CNA'): 111 | """ 112 | Conv layer with padding, normalization, activation 113 | mode: CNA --> Conv -> Norm -> Act 114 | NAC --> Norm -> Act --> Conv (Identity Mappings in Deep Residual Networks, ECCV16) 115 | """ 116 | assert mode in ['CNA', 'NAC', 'CNAC'], 'Wong conv mode [%s]' % mode 117 | padding = get_valid_padding(kernel_size, dilation) 118 | p = pad(pad_type, padding) if pad_type and pad_type != 'zero' else None 119 | padding = padding if pad_type == 'zero' else 0 120 | 121 | c = nn.Conv2d(in_nc, out_nc, kernel_size=kernel_size, stride=stride, padding=padding, \ 122 | dilation=dilation, bias=bias, groups=groups) 123 | a = act(act_type) if act_type else None 124 | if 'CNA' in mode: 125 | n = norm(norm_type, out_nc) if norm_type else None 126 | return sequential(p, c, n, a) 127 | elif mode == 'NAC': 128 | if norm_type is None and act_type is not None: 129 | a = act(act_type, inplace=False) 130 | # Important! 131 | # input----ReLU(inplace)----Conv--+----output 132 | # |________________________| 133 | # inplace ReLU will modify the input, therefore wrong output 134 | n = norm(norm_type, in_nc) if norm_type else None 135 | return sequential(n, a, p, c) 136 | 137 | 138 | #################### 139 | # Useful blocks 140 | #################### 141 | 142 | 143 | class ResNetBlock(nn.Module): 144 | """ 145 | ResNet Block, 3-3 style 146 | with extra residual scaling used in EDSR 147 | (Enhanced Deep Residual Networks for Single Image Super-Resolution, CVPRW 17) 148 | """ 149 | 150 | def __init__(self, in_nc, mid_nc, out_nc, kernel_size=3, stride=1, dilation=1, groups=1, \ 151 | bias=True, pad_type='zero', norm_type=None, act_type='relu', mode='CNA', res_scale=1): 152 | super(ResNetBlock, self).__init__() 153 | conv0 = conv_block(in_nc, mid_nc, kernel_size, stride, dilation, groups, bias, pad_type, \ 154 | norm_type, act_type, mode) 155 | if mode == 'CNA': 156 | act_type = None 157 | if mode == 'CNAC': # Residual path: |-CNAC-| 158 | act_type = None 159 | norm_type = None 160 | conv1 = conv_block(mid_nc, out_nc, kernel_size, stride, dilation, groups, bias, pad_type, \ 161 | norm_type, act_type, mode) 162 | # if in_nc != out_nc: 163 | # self.project = conv_block(in_nc, out_nc, 1, stride, dilation, 1, bias, pad_type, \ 164 | # None, None) 165 | # print('Need a projecter in ResNetBlock.') 166 | # else: 167 | # self.project = lambda x:x 168 | self.res = sequential(conv0, conv1) 169 | self.res_scale = res_scale 170 | 171 | def forward(self, x): 172 | res = self.res(x).mul(self.res_scale) 173 | return x + res 174 | 175 | 176 | class ResidualDenseBlock_5C(nn.Module): 177 | """ 178 | Residual Dense Block 179 | style: 5 convs 180 | The core module of paper: (Residual Dense Network for Image Super-Resolution, CVPR 18) 181 | """ 182 | 183 | def __init__(self, nc, kernel_size=3, gc=32, stride=1, bias=True, pad_type='zero', \ 184 | norm_type=None, act_type='leakyrelu', mode='CNA'): 185 | super(ResidualDenseBlock_5C, self).__init__() 186 | # gc: growth channel, i.e. intermediate channels 187 | self.conv1 = conv_block(nc, gc, kernel_size, stride, bias=bias, pad_type=pad_type, \ 188 | norm_type=norm_type, act_type=act_type, mode=mode) 189 | self.conv2 = conv_block(nc+gc, gc, kernel_size, stride, bias=bias, pad_type=pad_type, \ 190 | norm_type=norm_type, act_type=act_type, mode=mode) 191 | self.conv3 = conv_block(nc+2*gc, gc, kernel_size, stride, bias=bias, pad_type=pad_type, \ 192 | norm_type=norm_type, act_type=act_type, mode=mode) 193 | self.conv4 = conv_block(nc+3*gc, gc, kernel_size, stride, bias=bias, pad_type=pad_type, \ 194 | norm_type=norm_type, act_type=act_type, mode=mode) 195 | if mode == 'CNA': 196 | last_act = None 197 | else: 198 | last_act = act_type 199 | self.conv5 = conv_block(nc+4*gc, nc, 3, stride, bias=bias, pad_type=pad_type, \ 200 | norm_type=norm_type, act_type=last_act, mode=mode) 201 | 202 | def forward(self, x): 203 | x1 = self.conv1(x) 204 | x2 = self.conv2(torch.cat((x, x1), 1)) 205 | x3 = self.conv3(torch.cat((x, x1, x2), 1)) 206 | x4 = self.conv4(torch.cat((x, x1, x2, x3), 1)) 207 | x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1)) 208 | return x5.mul(0.2) + x 209 | 210 | 211 | class RRDB(nn.Module): 212 | """ 213 | Residual in Residual Dense Block 214 | """ 215 | 216 | def __init__(self, nc, kernel_size=3, gc=32, stride=1, bias=True, pad_type='zero', \ 217 | norm_type=None, act_type='leakyrelu', mode='CNA'): 218 | super(RRDB, self).__init__() 219 | self.RDB1 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type, \ 220 | norm_type, act_type, mode) 221 | self.RDB2 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type, \ 222 | norm_type, act_type, mode) 223 | self.RDB3 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type, \ 224 | norm_type, act_type, mode) 225 | 226 | def forward(self, x): 227 | out = self.RDB1(x) 228 | out = self.RDB2(out) 229 | out = self.RDB3(out) 230 | return out.mul(0.2) + x 231 | 232 | 233 | #################### 234 | # Upsampler 235 | #################### 236 | 237 | 238 | def pixelshuffle_block(in_nc, out_nc, upscale_factor=2, kernel_size=3, stride=1, bias=True, 239 | pad_type='zero', norm_type=None, act_type='relu'): 240 | """ 241 | Pixel shuffle layer 242 | (Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional 243 | Neural Network, CVPR17) 244 | """ 245 | conv = conv_block(in_nc, out_nc * (upscale_factor ** 2), kernel_size, stride, bias=bias, 246 | pad_type=pad_type, norm_type=None, act_type=None) 247 | pixel_shuffle = nn.PixelShuffle(upscale_factor) 248 | 249 | n = norm(norm_type, out_nc) if norm_type else None 250 | a = act(act_type) if act_type else None 251 | return sequential(conv, pixel_shuffle, n, a) 252 | 253 | 254 | def upconv_blcok(in_nc, out_nc, upscale_factor=2, kernel_size=3, stride=1, bias=True, 255 | pad_type='zero', norm_type=None, act_type='relu', mode='nearest'): 256 | # Up conv 257 | # described in https://distill.pub/2016/deconv-checkerboard/ 258 | upsample = nn.Upsample(scale_factor=upscale_factor, mode=mode) 259 | conv = conv_block(in_nc, out_nc, kernel_size, stride, bias=bias, 260 | pad_type=pad_type, norm_type=norm_type, act_type=act_type) 261 | return sequential(upsample, conv) 262 | -------------------------------------------------------------------------------- /hent-AI/ColabESRGAN/models/README.md: -------------------------------------------------------------------------------- 1 | ## Place pretrained models here. 2 | 3 | We provide two pretrained models: 4 | 5 | 1. `RRDB_ESRGAN_x4.pth`: the final ESRGAN model we used in our [paper](https://arxiv.org/abs/1809.00219). 6 | 2. `RRDB_PSNR_x4.pth`: the PSNR-oriented model with **high PSNR performance**. 7 | 8 | *Note that* the pretrained models are trained under the `MATLAB bicubic` kernel. 9 | If the downsampled kernel is different from that, the results may have artifacts. 10 | -------------------------------------------------------------------------------- /hent-AI/ColabESRGAN/net_interp.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import torch 3 | from collections import OrderedDict 4 | 5 | alpha = float(sys.argv[1]) 6 | 7 | net_PSNR_path = './models/RRDB_PSNR_x4.pth' 8 | net_ESRGAN_path = './models/RRDB_ESRGAN_x4.pth' 9 | net_interp_path = './models/interp_{:02d}.pth'.format(int(alpha*10)) 10 | 11 | net_PSNR = torch.load(net_PSNR_path) 12 | net_ESRGAN = torch.load(net_ESRGAN_path) 13 | net_interp = OrderedDict() 14 | 15 | print('Interpolating with alpha = ', alpha) 16 | 17 | for k, v_PSNR in net_PSNR.items(): 18 | v_ESRGAN = net_ESRGAN[k] 19 | net_interp[k] = (1 - alpha) * v_PSNR + alpha * v_ESRGAN 20 | 21 | torch.save(net_interp, net_interp_path) 22 | -------------------------------------------------------------------------------- /hent-AI/ColabESRGAN/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import cv2 4 | import numpy as np 5 | import torch 6 | import architecture 7 | import math 8 | 9 | # ESRGAN class allows abstraction of warmup and inference. 10 | class esrgan(): 11 | # hw = cpu, or cuda 12 | def __init__(self, model_path=None, hw='cpu'): 13 | assert model_path 14 | if hw=='cpu': 15 | self.device = torch.device('cpu') 16 | if hw=='cuda': 17 | self.device = torch.device('cuda') 18 | self.model = architecture.RRDB_Net(3, 3, 64, 23, gc=32, upscale=4, norm_type=None, act_type='leakyrelu', \ 19 | mode='CNA', res_scale=1, upsample_mode='upconv') 20 | self.model.load_state_dict(torch.load(model_path), strict=True) 21 | self.model.eval() 22 | for k, v in self.model.named_parameters(): 23 | v.requires_grad = False 24 | self.model = self.model.to(self.device) 25 | print('Model warmup complete') 26 | 27 | # Function to run esrgan on single image, and single output. 28 | def run_esrgan(self, test_img_folder=None, out_filename=None, mosaic_res=1): 29 | assert out_filename 30 | assert test_img_folder 31 | 32 | img = cv2.imdecode(np.fromfile(test_img_folder, np.uint8), cv2.IMREAD_UNCHANGED) 33 | # from alex: codes to shrink image if memory is an issue 34 | # GPUmem = torch.cuda.get_device_properties(0).total_memory 35 | # Sx = int(1.2*img.shape[0]/mosaic_res) 36 | # Sy = int(1.2*img.shape[1]/mosaic_res) 37 | # maxres = math.sqrt((Sx*Sy)/(GPUmem*0.00008)) 38 | # if maxres < 1: 39 | # maxres = 1 40 | # img = cv2.resize(img, (int(Sx/maxres),int(Sy/maxres))) 41 | 42 | img = img * 1.0 / 255 43 | img = torch.from_numpy(np.transpose(img[:, :, [2, 1, 0]], (2, 0, 1))).float() 44 | img_LR = img.unsqueeze(0) 45 | 46 | # image to device 47 | img_LR = img_LR.to(self.device) 48 | 49 | output = self.model(img_LR).data.squeeze().float().cpu().clamp_(0, 1).numpy() 50 | output = np.transpose(output[[2, 1, 0], :, :], (1, 2, 0)) 51 | output = (output * 255.0).round() 52 | # cv2.imwrite(out_filename, output) 53 | is_success, im_buf_arr = cv2.imencode(".png", output) 54 | im_buf_arr.tofile(out_filename) 55 | -------------------------------------------------------------------------------- /hent-AI/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hent-AI/green_mask_project_mosaic_resolution.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import os 4 | import glob 5 | from scipy.signal import argrelextrema 6 | from PIL import Image 7 | 8 | # Conversion from file based script to individual image usage 9 | def get_mosaic_res(root_img=None): 10 | # assert root_img 11 | #You can change those folder paths 12 | # os.makedirs(root_img, exist_ok=True) 13 | 14 | # files = glob.glob(rootdir + '/**/*.png', recursive=True) 15 | # files_jpg = glob.glob(rootdir + '/**/*.jpg', recursive=True) 16 | # files.extend(files_jpg) 17 | f = root_img # use input image path 18 | #-----------------------Logic----------------------- 19 | GBlur = 5 20 | CannyTr1 = 20 21 | CannyTr2 = 100 22 | LowRange = 2 23 | HighRange = 24 24 | DetectionTr = 0.32 25 | 26 | pattern = [None] * (HighRange+2) 27 | for masksize in range(HighRange+2, LowRange+1, -1): 28 | maskimg = 2+masksize+masksize-1+2 29 | screen = (maskimg, maskimg) 30 | img = Image.new('RGB', screen, (255,255,255)) 31 | pix = img.load() 32 | for i in range(2,maskimg,masksize-1): 33 | for j in range(2,maskimg,masksize-1): 34 | for k in range(0,maskimg): 35 | pix[i, k] = (0,0,0) 36 | pix[k, j] = (0,0,0) 37 | pattern[masksize-2] = img 38 | 39 | #Working with files 40 | # for f in files: 41 | #-----------------------Files----------------------- 42 | img_C = Image.fromarray(f).convert("RGBA") 43 | x, y = img_C.size 44 | card = Image.new("RGBA", (x, y), (255, 255, 255, 0)) 45 | cvI = Image.alpha_composite(card, img_C) 46 | cvI = np.array(cvI) 47 | img_rgb = cv2.cvtColor(cvI, cv2.COLOR_BGRA2RGBA) 48 | 49 | img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY) 50 | img_gray = cv2.Canny(img_gray,CannyTr1,CannyTr2) 51 | img_gray = 255-img_gray 52 | img_gray = cv2.GaussianBlur(img_gray,(GBlur,GBlur),0) 53 | 54 | #-----------------------Detection----------------------- 55 | resolutions = [-1] * (HighRange+2) 56 | for masksize in range(HighRange+2, LowRange+1, -1): 57 | template = cv2.cvtColor(np.array(pattern[masksize-2]), cv2.COLOR_BGR2GRAY) 58 | w, h = pattern[masksize-2].size[::-1] 59 | 60 | img_detection = cv2.matchTemplate(img_gray,template,cv2.TM_CCOEFF_NORMED) 61 | loc = np.where(img_detection >= DetectionTr) 62 | rects = 0 63 | for pt in zip(*loc[::-1]): 64 | rects += 1 #increase rectangle count of single resolution 65 | # cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,255,0), 1) #DEBUG To see regions on image 66 | resolutions[masksize-1] = rects 67 | 68 | resolutions.append(0) 69 | # print(resolutions) #DEBUG Resolutions array 70 | extremaMIN = argrelextrema(np.array(resolutions), np.less, axis=0)[0] 71 | extremaMIN = np.insert(extremaMIN,0,LowRange) 72 | extremaMIN = np.append(extremaMIN,HighRange+2) 73 | 74 | Extremas = [] 75 | for i, ExtGroup in enumerate(extremaMIN[:-1]): 76 | Extremas.append((ExtGroup, resolutions[extremaMIN[i]:extremaMIN[i+1]+1])) 77 | 78 | ExtremasSum = [] 79 | BigExtrema = [0,0,[0,0]] 80 | for i, _ in enumerate(Extremas): 81 | ExtremasSum.append(sum(Extremas[i][1])) 82 | if BigExtrema[0] <= sum(Extremas[i][1])+int(sum(Extremas[i][1])*0.05): #5% precedency for smaller resolution 83 | BigExtrema = [sum(Extremas[i][1]),Extremas[i][0],Extremas[i][1]] 84 | MosaicResolutionOfImage = BigExtrema[1]+BigExtrema[2].index(max(BigExtrema[2])) #Output 85 | if MosaicResolutionOfImage == 0: #If nothing found - set resolution as smallest 86 | MosaicResolutionOfImage = HighRange+1 87 | # print('Mosaic Resolution of "' + os.path.basename(f) + '" is: ' + str(MosaicResolutionOfImage)) #The Resolution of Mosaiced Image 88 | return MosaicResolutionOfImage 89 | 90 | #DEBUG Show image 91 | # cv2.imshow('image',img_rgb) 92 | # cv2.waitKey(0) 93 | # cv2.destroyAllWindows() 94 | -------------------------------------------------------------------------------- /hent-AI/mrcnn/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hent-AI/mrcnn/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mask R-CNN 3 | Base Configurations class. 4 | 5 | Copyright (c) 2017 Matterport, Inc. 6 | Licensed under the MIT License (see LICENSE for details) 7 | Written by Waleed Abdulla 8 | """ 9 | 10 | import numpy as np 11 | 12 | 13 | # Base Configuration Class 14 | # Don't use this class directly. Instead, sub-class it and override 15 | # the configurations you need to change. 16 | 17 | class Config(object): 18 | """Base configuration class. For custom configurations, create a 19 | sub-class that inherits from this one and override properties 20 | that need to be changed. 21 | """ 22 | # Name the configurations. For example, 'COCO', 'Experiment 3', ...etc. 23 | # Useful if your code needs to do things differently depending on which 24 | # experiment is running. 25 | NAME = None # Override in sub-classes 26 | 27 | # NUMBER OF GPUs to use. When using only a CPU, this needs to be set to 1. 28 | GPU_COUNT = 1 29 | 30 | # Number of images to train with on each GPU. A 12GB GPU can typically 31 | # handle 2 images of 1024x1024px. 32 | # Adjust based on your GPU memory and image sizes. Use the highest 33 | # number that your GPU can handle for best performance. 34 | IMAGES_PER_GPU = 1 35 | 36 | # Number of training steps per epoch 37 | # This doesn't need to match the size of the training set. Tensorboard 38 | # updates are saved at the end of each epoch, so setting this to a 39 | # smaller number means getting more frequent TensorBoard updates. 40 | # Validation stats are also calculated at each epoch end and they 41 | # might take a while, so don't set this too small to avoid spending 42 | # a lot of time on validation stats. 43 | STEPS_PER_EPOCH = 1000 44 | 45 | # Number of validation steps to run at the end of every training epoch. 46 | # A bigger number improves accuracy of validation stats, but slows 47 | # down the training. 48 | VALIDATION_STEPS = 150 49 | 50 | # Backbone network architecture 51 | # Supported values are: resnet50, resnet101. 52 | # You can also provide a callable that should have the signature 53 | # of model.resnet_graph. If you do so, you need to supply a callable 54 | # to COMPUTE_BACKBONE_SHAPE as well 55 | BACKBONE = "resnet101" 56 | 57 | # Only useful if you supply a callable to BACKBONE. Should compute 58 | # the shape of each layer of the FPN Pyramid. 59 | # See model.compute_backbone_shapes 60 | COMPUTE_BACKBONE_SHAPE = None 61 | 62 | # The strides of each layer of the FPN Pyramid. These values 63 | # are based on a Resnet101 backbone. 64 | BACKBONE_STRIDES = [4, 8, 16, 32, 64] 65 | 66 | # Size of the fully-connected layers in the classification graph 67 | FPN_CLASSIF_FC_LAYERS_SIZE = 1024 68 | 69 | # Size of the top-down layers used to build the feature pyramid 70 | TOP_DOWN_PYRAMID_SIZE = 256 71 | 72 | # Number of classification classes (including background) 73 | NUM_CLASSES = 1 # Override in sub-classes 74 | 75 | # Length of square anchor side in pixels 76 | RPN_ANCHOR_SCALES = (32, 64, 128, 256, 512) 77 | 78 | # Ratios of anchors at each cell (width/height) 79 | # A value of 1 represents a square anchor, and 0.5 is a wide anchor 80 | RPN_ANCHOR_RATIOS = [0.5, 1, 2] 81 | 82 | # Anchor stride 83 | # If 1 then anchors are created for each cell in the backbone feature map. 84 | # If 2, then anchors are created for every other cell, and so on. 85 | RPN_ANCHOR_STRIDE = 1 86 | 87 | # Non-max suppression threshold to filter RPN proposals. 88 | # You can increase this during training to generate more propsals. 89 | RPN_NMS_THRESHOLD = 0.7 90 | 91 | # How many anchors per image to use for RPN training 92 | RPN_TRAIN_ANCHORS_PER_IMAGE = 256 93 | 94 | # ROIs kept after tf.nn.top_k and before non-maximum suppression 95 | PRE_NMS_LIMIT = 6000 96 | 97 | # ROIs kept after non-maximum suppression (training and inference) 98 | POST_NMS_ROIS_TRAINING = 2000 99 | POST_NMS_ROIS_INFERENCE = 2000 100 | 101 | # If enabled, resizes instance masks to a smaller size to reduce 102 | # memory load. Recommended when using high-resolution images. 103 | USE_MINI_MASK = True 104 | MINI_MASK_SHAPE = (56, 56) # (height, width) of the mini-mask 105 | 106 | # Input image resizing 107 | # Generally, use the "square" resizing mode for training and predicting 108 | # and it should work well in most cases. In this mode, images are scaled 109 | # up such that the small side is = IMAGE_MIN_DIM, but ensuring that the 110 | # scaling doesn't make the long side > IMAGE_MAX_DIM. Then the image is 111 | # padded with zeros to make it a square so multiple images can be put 112 | # in one batch. 113 | # Available resizing modes: 114 | # none: No resizing or padding. Return the image unchanged. 115 | # square: Resize and pad with zeros to get a square image 116 | # of size [max_dim, max_dim]. 117 | # pad64: Pads width and height with zeros to make them multiples of 64. 118 | # If IMAGE_MIN_DIM or IMAGE_MIN_SCALE are not None, then it scales 119 | # up before padding. IMAGE_MAX_DIM is ignored in this mode. 120 | # The multiple of 64 is needed to ensure smooth scaling of feature 121 | # maps up and down the 6 levels of the FPN pyramid (2**6=64). 122 | # crop: Picks random crops from the image. First, scales the image based 123 | # on IMAGE_MIN_DIM and IMAGE_MIN_SCALE, then picks a random crop of 124 | # size IMAGE_MIN_DIM x IMAGE_MIN_DIM. Can be used in training only. 125 | # IMAGE_MAX_DIM is not used in this mode. 126 | IMAGE_RESIZE_MODE = "square" 127 | IMAGE_MIN_DIM = 800 128 | IMAGE_MAX_DIM = 1024 129 | # Minimum scaling ratio. Checked after MIN_IMAGE_DIM and can force further 130 | # up scaling. For example, if set to 2 then images are scaled up to double 131 | # the width and height, or more, even if MIN_IMAGE_DIM doesn't require it. 132 | # However, in 'square' mode, it can be overruled by IMAGE_MAX_DIM. 133 | IMAGE_MIN_SCALE = 0 134 | # Number of color channels per image. RGB = 3, grayscale = 1, RGB-D = 4 135 | # Changing this requires other changes in the code. See the WIKI for more 136 | # details: https://github.com/matterport/Mask_RCNN/wiki 137 | IMAGE_CHANNEL_COUNT = 3 138 | 139 | # Image mean (RGB) 140 | MEAN_PIXEL = np.array([123.7, 116.8, 103.9]) 141 | 142 | # Number of ROIs per image to feed to classifier/mask heads 143 | # The Mask RCNN paper uses 512 but often the RPN doesn't generate 144 | # enough positive proposals to fill this and keep a positive:negative 145 | # ratio of 1:3. You can increase the number of proposals by adjusting 146 | # the RPN NMS threshold. 147 | TRAIN_ROIS_PER_IMAGE = 200 148 | 149 | # Percent of positive ROIs used to train classifier/mask heads 150 | ROI_POSITIVE_RATIO = 0.33 151 | 152 | # Pooled ROIs 153 | POOL_SIZE = 7 154 | MASK_POOL_SIZE = 14 155 | 156 | # Shape of output mask 157 | # To change this you also need to change the neural network mask branch 158 | MASK_SHAPE = [28, 28] 159 | 160 | # Maximum number of ground truth instances to use in one image 161 | MAX_GT_INSTANCES = 30 162 | 163 | # Bounding box refinement standard deviation for RPN and final detections. 164 | RPN_BBOX_STD_DEV = np.array([0.1, 0.1, 0.2, 0.2]) 165 | BBOX_STD_DEV = np.array([0.1, 0.1, 0.2, 0.2]) 166 | 167 | # Max number of final detections 168 | DETECTION_MAX_INSTANCES = 35 169 | 170 | # Minimum probability value to accept a detected instance 171 | # ROIs below this threshold are skipped 172 | DETECTION_MIN_CONFIDENCE = 0.7 173 | 174 | # Non-maximum suppression threshold for detection 175 | DETECTION_NMS_THRESHOLD = 0.3 176 | 177 | # Learning rate and momentum 178 | # The Mask RCNN paper uses lr=0.02, but on TensorFlow it causes 179 | # weights to explode. Likely due to differences in optimizer 180 | # implementation. 181 | LEARNING_RATE = 0.001 182 | LEARNING_MOMENTUM = 0.9 183 | 184 | # Weight decay regularization 185 | WEIGHT_DECAY = 0.0001 186 | 187 | # Loss weights for more precise optimization. 188 | # Can be used for R-CNN training setup. 189 | LOSS_WEIGHTS = { 190 | "rpn_class_loss": 1., 191 | "rpn_bbox_loss": 1., 192 | "mrcnn_class_loss": 1., 193 | "mrcnn_bbox_loss": 1., 194 | "mrcnn_mask_loss": 1. 195 | } 196 | 197 | # Use RPN ROIs or externally generated ROIs for training 198 | # Keep this True for most situations. Set to False if you want to train 199 | # the head branches on ROI generated by code rather than the ROIs from 200 | # the RPN. For example, to debug the classifier head without having to 201 | # train the RPN. 202 | USE_RPN_ROIS = True 203 | 204 | # Train or freeze batch normalization layers 205 | # None: Train BN layers. This is the normal mode 206 | # False: Freeze BN layers. Good when using a small batch size 207 | # True: (don't use). Set layer in training mode even when predicting 208 | TRAIN_BN = False # Defaulting to False since batch size is often small 209 | 210 | # Gradient norm clipping 211 | GRADIENT_CLIP_NORM = 5.0 212 | 213 | def __init__(self): 214 | """Set values of computed attributes.""" 215 | # Effective batch size 216 | self.BATCH_SIZE = self.IMAGES_PER_GPU * self.GPU_COUNT 217 | 218 | # Input image size 219 | if self.IMAGE_RESIZE_MODE == "crop": 220 | self.IMAGE_SHAPE = np.array([self.IMAGE_MIN_DIM, self.IMAGE_MIN_DIM, 221 | self.IMAGE_CHANNEL_COUNT]) 222 | else: 223 | self.IMAGE_SHAPE = np.array([self.IMAGE_MAX_DIM, self.IMAGE_MAX_DIM, 224 | self.IMAGE_CHANNEL_COUNT]) 225 | 226 | # Image meta data length 227 | # See compose_image_meta() for details 228 | self.IMAGE_META_SIZE = 1 + 3 + 3 + 4 + 1 + self.NUM_CLASSES 229 | 230 | def display(self): 231 | """Display Configuration values.""" 232 | print("\nConfigurations:") 233 | for a in dir(self): 234 | if not a.startswith("__") and not callable(getattr(self, a)): 235 | print("{:30} {}".format(a, getattr(self, a))) 236 | print("\n") 237 | -------------------------------------------------------------------------------- /hent-AI/mrcnn/parallel_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mask R-CNN 3 | Multi-GPU Support for Keras. 4 | 5 | Copyright (c) 2017 Matterport, Inc. 6 | Licensed under the MIT License (see LICENSE for details) 7 | Written by Waleed Abdulla 8 | 9 | Ideas and a small code snippets from these sources: 10 | https://github.com/fchollet/keras/issues/2436 11 | https://medium.com/@kuza55/transparent-multi-gpu-training-on-tensorflow-with-keras-8b0016fd9012 12 | https://github.com/avolkov1/keras_experiments/blob/master/keras_exp/multigpu/ 13 | https://github.com/fchollet/keras/blob/master/keras/utils/training_utils.py 14 | """ 15 | 16 | import tensorflow as tf 17 | import keras.backend as K 18 | import keras.layers as KL 19 | import keras.models as KM 20 | 21 | 22 | class ParallelModel(KM.Model): 23 | """Subclasses the standard Keras Model and adds multi-GPU support. 24 | It works by creating a copy of the model on each GPU. Then it slices 25 | the inputs and sends a slice to each copy of the model, and then 26 | merges the outputs together and applies the loss on the combined 27 | outputs. 28 | """ 29 | 30 | def __init__(self, keras_model, gpu_count): 31 | """Class constructor. 32 | keras_model: The Keras model to parallelize 33 | gpu_count: Number of GPUs. Must be > 1 34 | """ 35 | self.inner_model = keras_model 36 | self.gpu_count = gpu_count 37 | merged_outputs = self.make_parallel() 38 | super(ParallelModel, self).__init__(inputs=self.inner_model.inputs, 39 | outputs=merged_outputs) 40 | 41 | def __getattribute__(self, attrname): 42 | """Redirect loading and saving methods to the inner model. That's where 43 | the weights are stored.""" 44 | if 'load' in attrname or 'save' in attrname: 45 | return getattr(self.inner_model, attrname) 46 | return super(ParallelModel, self).__getattribute__(attrname) 47 | 48 | def summary(self, *args, **kwargs): 49 | """Override summary() to display summaries of both, the wrapper 50 | and inner models.""" 51 | super(ParallelModel, self).summary(*args, **kwargs) 52 | self.inner_model.summary(*args, **kwargs) 53 | 54 | def make_parallel(self): 55 | """Creates a new wrapper model that consists of multiple replicas of 56 | the original model placed on different GPUs. 57 | """ 58 | # Slice inputs. Slice inputs on the CPU to avoid sending a copy 59 | # of the full inputs to all GPUs. Saves on bandwidth and memory. 60 | input_slices = {name: tf.split(x, self.gpu_count) 61 | for name, x in zip(self.inner_model.input_names, 62 | self.inner_model.inputs)} 63 | 64 | output_names = self.inner_model.output_names 65 | outputs_all = [] 66 | for i in range(len(self.inner_model.outputs)): 67 | outputs_all.append([]) 68 | 69 | # Run the model call() on each GPU to place the ops there 70 | for i in range(self.gpu_count): 71 | with tf.device('/gpu:%d' % i): 72 | with tf.name_scope('tower_%d' % i): 73 | # Run a slice of inputs through this replica 74 | zipped_inputs = zip(self.inner_model.input_names, 75 | self.inner_model.inputs) 76 | inputs = [ 77 | KL.Lambda(lambda s: input_slices[name][i], 78 | output_shape=lambda s: (None,) + s[1:])(tensor) 79 | for name, tensor in zipped_inputs] 80 | # Create the model replica and get the outputs 81 | outputs = self.inner_model(inputs) 82 | if not isinstance(outputs, list): 83 | outputs = [outputs] 84 | # Save the outputs for merging back together later 85 | for l, o in enumerate(outputs): 86 | outputs_all[l].append(o) 87 | 88 | # Merge outputs on CPU 89 | with tf.device('/cpu:0'): 90 | merged = [] 91 | for outputs, name in zip(outputs_all, output_names): 92 | # Concatenate or average outputs? 93 | # Outputs usually have a batch dimension and we concatenate 94 | # across it. If they don't, then the output is likely a loss 95 | # or a metric value that gets averaged across the batch. 96 | # Keras expects losses and metrics to be scalars. 97 | if K.int_shape(outputs[0]) == (): 98 | # Average 99 | m = KL.Lambda(lambda o: tf.add_n(o) / len(outputs), name=name)(outputs) 100 | else: 101 | # Concatenate 102 | m = KL.Concatenate(axis=0, name=name)(outputs) 103 | merged.append(m) 104 | return merged 105 | 106 | 107 | if __name__ == "__main__": 108 | # Testing code below. It creates a simple model to train on MNIST and 109 | # tries to run it on 2 GPUs. It saves the graph so it can be viewed 110 | # in TensorBoard. Run it as: 111 | # 112 | # python3 parallel_model.py 113 | 114 | import os 115 | import numpy as np 116 | import keras.optimizers 117 | from keras.datasets import mnist 118 | from keras.preprocessing.image import ImageDataGenerator 119 | 120 | GPU_COUNT = 2 121 | 122 | # Root directory of the project 123 | ROOT_DIR = os.path.abspath("../") 124 | 125 | # Directory to save logs and trained model 126 | MODEL_DIR = os.path.join(ROOT_DIR, "logs") 127 | 128 | def build_model(x_train, num_classes): 129 | # Reset default graph. Keras leaves old ops in the graph, 130 | # which are ignored for execution but clutter graph 131 | # visualization in TensorBoard. 132 | tf.reset_default_graph() 133 | 134 | inputs = KL.Input(shape=x_train.shape[1:], name="input_image") 135 | x = KL.Conv2D(32, (3, 3), activation='relu', padding="same", 136 | name="conv1")(inputs) 137 | x = KL.Conv2D(64, (3, 3), activation='relu', padding="same", 138 | name="conv2")(x) 139 | x = KL.MaxPooling2D(pool_size=(2, 2), name="pool1")(x) 140 | x = KL.Flatten(name="flat1")(x) 141 | x = KL.Dense(128, activation='relu', name="dense1")(x) 142 | x = KL.Dense(num_classes, activation='softmax', name="dense2")(x) 143 | 144 | return KM.Model(inputs, x, "digit_classifier_model") 145 | 146 | # Load MNIST Data 147 | (x_train, y_train), (x_test, y_test) = mnist.load_data() 148 | x_train = np.expand_dims(x_train, -1).astype('float32') / 255 149 | x_test = np.expand_dims(x_test, -1).astype('float32') / 255 150 | 151 | print('x_train shape:', x_train.shape) 152 | print('x_test shape:', x_test.shape) 153 | 154 | # Build data generator and model 155 | datagen = ImageDataGenerator() 156 | model = build_model(x_train, 10) 157 | 158 | # Add multi-GPU support. 159 | model = ParallelModel(model, GPU_COUNT) 160 | 161 | optimizer = keras.optimizers.SGD(lr=0.01, momentum=0.9, clipnorm=5.0) 162 | 163 | model.compile(loss='sparse_categorical_crossentropy', 164 | optimizer=optimizer, metrics=['accuracy']) 165 | 166 | model.summary() 167 | 168 | # Train 169 | model.fit_generator( 170 | datagen.flow(x_train, y_train, batch_size=64), 171 | steps_per_epoch=50, epochs=10, verbose=1, 172 | validation_data=(x_test, y_test), 173 | callbacks=[keras.callbacks.TensorBoard(log_dir=MODEL_DIR, 174 | write_graph=True)] 175 | ) 176 | -------------------------------------------------------------------------------- /hent-AI/requirements.txt: -------------------------------------------------------------------------------- 1 | h5py==2.10.0 2 | ffmpeg-python -------------------------------------------------------------------------------- /hent-AI/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license-file = LICENSE 4 | requirements-file = requirements.txt -------------------------------------------------------------------------------- /hent-AI/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | The build/compilations setup 3 | 4 | >> pip install -r requirements.txt 5 | >> python setup.py install 6 | """ 7 | import pip 8 | import logging 9 | import pkg_resources 10 | try: 11 | from setuptools import setup 12 | except ImportError: 13 | from distutils.core import setup 14 | 15 | 16 | def _parse_requirements(file_path): 17 | pip_ver = pkg_resources.get_distribution('pip').version 18 | pip_version = list(map(int, pip_ver.split('.')[:2])) 19 | if pip_version >= [6, 0]: 20 | raw = pip.req.parse_requirements(file_path, 21 | session=pip.download.PipSession()) 22 | else: 23 | raw = pip.req.parse_requirements(file_path) 24 | return [str(i.req) for i in raw] 25 | 26 | 27 | # parse_requirements() returns generator of pip.req.InstallRequirement objects 28 | try: 29 | install_reqs = _parse_requirements("requirements.txt") 30 | except Exception: 31 | logging.warning('Fail load requirements file, so using default ones.') 32 | install_reqs = [] 33 | 34 | setup( 35 | name='mask-rcnn', 36 | version='2.1', 37 | url='https://github.com/matterport/Mask_RCNN', 38 | author='Matterport', 39 | author_email='waleed.abdulla@gmail.com', 40 | license='MIT', 41 | description='Mask R-CNN for object detection and instance segmentation', 42 | packages=["mrcnn"], 43 | install_requires=install_reqs, 44 | include_package_data=True, 45 | python_requires='>=3.4', 46 | long_description="""This is an implementation of Mask R-CNN on Python 3, Keras, and TensorFlow. 47 | The model generates bounding boxes and segmentation masks for each instance of an object in the image. 48 | It's based on Feature Pyramid Network (FPN) and a ResNet101 backbone.""", 49 | classifiers=[ 50 | "Development Status :: 5 - Production/Stable", 51 | "Environment :: Console", 52 | "Intended Audience :: Developers", 53 | "Intended Audience :: Information Technology", 54 | "Intended Audience :: Education", 55 | "Intended Audience :: Science/Research", 56 | "License :: OSI Approved :: MIT License", 57 | "Natural Language :: English", 58 | "Operating System :: OS Independent", 59 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 60 | "Topic :: Scientific/Engineering :: Image Recognition", 61 | "Topic :: Scientific/Engineering :: Visualization", 62 | "Topic :: Scientific/Engineering :: Image Segmentation", 63 | 'Programming Language :: Python :: 3.4', 64 | 'Programming Language :: Python :: 3.5', 65 | 'Programming Language :: Python :: 3.6', 66 | ], 67 | keywords="image instance segmentation object detection mask rcnn r-cnn tensorflow keras", 68 | ) 69 | -------------------------------------------------------------------------------- /inpainting.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from PIL import Image 3 | from os import listdir 4 | 5 | def run_inpaint(image_path, mask_path, output_path): 6 | w, h = Image.open(image_path).size 7 | r = requests.post( 8 | url = 'http://127.0.0.1:5003/inpaint', 9 | files={ 10 | 'image':open(image_path, 'rb'), 11 | 'mask':open(mask_path, 'rb'), 12 | }, 13 | data={ 14 | 'ldmSteps':'50', 15 | 'hdStrategy':'Resize', 16 | 'hdStrategyCropMargin':'128', 17 | 'hdStrategyCropTrigerSize':'2048', 18 | 'hdStrategyResizeLimit':'2048', 19 | 'sizeLimit': '%s' %(w if w>h else h), 20 | }, 21 | ) 22 | open(output_path, 'wb').write(r.content) 23 | 24 | 25 | def make_mask(detected_image): 26 | img = Image.open(detected_image) 27 | img_pixels = img.load() 28 | 29 | for i in range(img.size[0]): 30 | for j in range(img.size[1]): 31 | if img_pixels[i, j] == (0, 255, 0): 32 | img_pixels[i, j] = (255, 255, 255) 33 | else: 34 | img_pixels[i, j] = (0, 0, 0) 35 | 36 | mask_fname = '%s_mask.png' %detected_image[:-4] 37 | img.save(mask_fname) 38 | return mask_fname 39 | 40 | 41 | 42 | 43 | 44 | 45 | if __name__ == '__main__': 46 | 47 | original_image = [i for i in listdir('/content/image_original/') if i.endswith('.png')] 48 | 49 | for o in original_image: 50 | try: 51 | img = '/content/image_original/{}'.format(o) 52 | print("original_image ->", img) 53 | mask = make_mask('/content/image_detected/{}.png'.format(o[:-4])) 54 | print('masked completed ->', mask) 55 | 56 | run_inpaint( 57 | image_path=img, 58 | mask_path=mask, 59 | output_path='/content/image_uncensored/{}'.format(o) 60 | ) 61 | print('inpaint completed ->', '/content/image_uncensored/{}'.format(o), '\n') 62 | 63 | 64 | except Exception as e: 65 | print(e, '\n') -------------------------------------------------------------------------------- /lama.py: -------------------------------------------------------------------------------- 1 | from lama_cleaner import entry_point 2 | 3 | if __name__ == "__main__": 4 | entry_point() 5 | -------------------------------------------------------------------------------- /lama_cleaner/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Lama Cleaner Dockerfile 3 | # @author Loreto Parisi (loretoparisi at gmail dot com) 4 | # 5 | 6 | FROM python:3.7.4-slim-buster 7 | 8 | LABEL maintainer Loreto Parisi loretoparisi@gmail.com 9 | 10 | WORKDIR app 11 | 12 | RUN apt-get update && apt-get install -y --no-install-recommends \ 13 | software-properties-common \ 14 | libsm6 libxext6 ffmpeg libfontconfig1 libxrender1 libgl1-mesa-glx \ 15 | curl \ 16 | npm 17 | 18 | # python requirements 19 | COPY . . 20 | COPY requirements.txt /etc/tmp/requirements.txt 21 | RUN pip install -r /etc/tmp/requirements.txt 22 | 23 | # nodejs 24 | RUN npm install n -g && \ 25 | n lts 26 | # yarn 27 | RUN npm install -g yarn 28 | 29 | # webapp 30 | RUN cd lama_cleaner/app/ && \ 31 | yarn && \ 32 | yarn build 33 | 34 | EXPOSE 8080 35 | 36 | CMD ["bash"] -------------------------------------------------------------------------------- /lama_cleaner/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lama_cleaner/README.md: -------------------------------------------------------------------------------- 1 | # Lama-cleaner: Image inpainting tool powered by SOTA AI model 2 | 3 | ![downloads](https://img.shields.io/pypi/dm/lama-cleaner) 4 | ![version](https://img.shields.io/pypi/v/lama-cleaner) 5 | 6 | https://user-images.githubusercontent.com/3998421/153323093-b664bb68-2928-480b-b59b-7c1ee24a4507.mp4 7 | 8 | - [x] Support multiple model architectures 9 | 1. [LaMa](https://github.com/saic-mdal/lama) 10 | 1. [LDM](https://github.com/CompVis/latent-diffusion) 11 | - [x] High resolution support 12 | - [x] Run as a desktop APP 13 | - [x] Multi stroke support. Press and hold the `cmd/ctrl` key to enable multi stroke mode. 14 | - [x] Zoom & Pan 15 | - [ ] Keep image EXIF data 16 | 17 | ## Install 18 | 19 | ```bash 20 | pip install lama-cleaner 21 | 22 | lama-cleaner --device=cpu --port=8080 23 | ``` 24 | 25 | Available commands: 26 | 27 | | Name | Description | Default | 28 | | ---------- | ------------------------------------------------ | -------- | 29 | | --model | lama or ldm. See details in **Model Comparison** | lama | 30 | | --device | cuda or cpu | cuda | 31 | | --gui | Launch lama-cleaner as a desktop application | | 32 | | --gui_size | Set the window size for the application | 1200 900 | 33 | | --input | Path to image you want to load by default | None | 34 | | --port | Port for flask web server | 8080 | 35 | | --debug | Enable debug mode for flask web server | | 36 | 37 | ## Model Comparison 38 | 39 | Diffusion model(ldm) is **MUCH MORE** slower than GANs(lama)(1080x720 image takes 8s on 3090), but it's possible to get better 40 | result, see below example: 41 | 42 | | Original Image | LaMa | LDM | 43 | | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | 44 | | ![photo-1583445095369-9c651e7e5d34](https://user-images.githubusercontent.com/3998421/156923525-d6afdec3-7b98-403f-ad20-88ebc6eb8d6d.jpg) | ![photo-1583445095369-9c651e7e5d34_cleanup_lama](https://user-images.githubusercontent.com/3998421/156923620-a40cc066-fd4a-4d85-a29f-6458711d1247.png) | ![photo-1583445095369-9c651e7e5d34_cleanup_ldm](https://user-images.githubusercontent.com/3998421/156923652-0d06c8c8-33ad-4a42-a717-9c99f3268933.png) | 45 | 46 | Blogs about diffusion models: 47 | 48 | - https://lilianweng.github.io/posts/2021-07-11-diffusion-models/ 49 | - https://yang-song.github.io/blog/2021/score/ 50 | 51 | ## Development 52 | 53 | Only needed if you plan to modify the frontend and recompile yourself. 54 | 55 | ### Frontend 56 | 57 | Frontend code are modified from [cleanup.pictures](https://github.com/initml/cleanup.pictures), You can experience their 58 | great online services [here](https://cleanup.pictures/). 59 | 60 | - Install dependencies:`cd lama_cleaner/app/ && yarn` 61 | - Start development server: `yarn start` 62 | - Build: `yarn build` 63 | 64 | ## Docker 65 | 66 | Run within a Docker container. Set the `CACHE_DIR` to models location path. Optionally add a `-d` option to 67 | the `docker run` command below to run as a daemon. 68 | 69 | ### Build Docker image 70 | 71 | ``` 72 | docker build -f Dockerfile -t lamacleaner . 73 | ``` 74 | 75 | ### Run Docker (cpu) 76 | 77 | ``` 78 | docker run -p 8080:8080 -e CACHE_DIR=/app/models -v $(pwd)/models:/app/models -v $(pwd):/app --rm lamacleaner python3 main.py --device=cpu --port=8080 79 | ``` 80 | 81 | ### Run Docker (gpu) 82 | 83 | ``` 84 | docker run --gpus all -p 8080:8080 -e CACHE_DIR=/app/models -v $(pwd)/models:/app/models -v $(pwd):/app --rm lamacleaner python3 main.py --device=cuda --port=8080 85 | ``` 86 | 87 | Then open [http://localhost:8080](http://localhost:8080) 88 | 89 | ## Like My Work? 90 | 91 | 92 | Sanster 93 | 94 | -------------------------------------------------------------------------------- /lama_cleaner/__init__.py: -------------------------------------------------------------------------------- 1 | from lama_cleaner.parse_args import parse_args 2 | from lama_cleaner.server import main 3 | 4 | 5 | def entry_point(): 6 | args = parse_args() 7 | main(args) 8 | -------------------------------------------------------------------------------- /lama_cleaner/app/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_INPAINTING_URL="" -------------------------------------------------------------------------------- /lama_cleaner/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "airbnb/hooks", 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier", 7 | "plugin:prettier/recommended" 8 | ], 9 | "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.json" 18 | }, 19 | "rules": { 20 | "jsx-a11y/click-events-have-key-events": 0, 21 | "react/jsx-props-no-spreading": 0, 22 | "import/no-unresolved": 0, 23 | "react/jsx-no-bind": "off", 24 | "react/jsx-filename-extension": [ 25 | 1, 26 | { 27 | "extensions": [".ts", ".tsx"] 28 | } 29 | ], 30 | "prettier/prettier": [ 31 | "error", 32 | { 33 | "singleQuote": true, 34 | "arrowParens": "avoid", 35 | "endOfLine": "auto" 36 | } 37 | ], 38 | "consistent-return": "off", 39 | "no-use-before-define": "off", 40 | "import/extensions": "off", 41 | "react/prop-types": 0, 42 | "react/require-default-props": "off", 43 | "no-shadow": "off", 44 | "@typescript-eslint/ban-ts-comment": "off", 45 | "@typescript-eslint/no-shadow": ["error"], 46 | "@typescript-eslint/no-explicit-any": "off", 47 | "@typescript-eslint/explicit-function-return-type": "off", 48 | "@typescript-eslint/explicit-module-boundary-types": "off", 49 | "react-hooks/rules-of-hooks": "error", 50 | "react-hooks/exhaustive-deps": "warn" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lama_cleaner/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Tailwind processed CSS 25 | index.css 26 | 27 | .firebase 28 | -------------------------------------------------------------------------------- /lama_cleaner/app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "es5", 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /lama_cleaner/app/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.4201b632.chunk.css", 4 | "main.js": "/static/js/main.18cd2cfc.chunk.js", 5 | "runtime-main.js": "/static/js/runtime-main.5e86ac81.js", 6 | "static/js/2.2d367d07.chunk.js": "/static/js/2.2d367d07.chunk.js", 7 | "index.html": "/index.html", 8 | "static/js/2.2d367d07.chunk.js.LICENSE.txt": "/static/js/2.2d367d07.chunk.js.LICENSE.txt", 9 | "static/media/_index.scss": "/static/media/WorkSans-SemiBold.1e98db4e.ttf" 10 | }, 11 | "entrypoints": [ 12 | "static/js/runtime-main.5e86ac81.js", 13 | "static/js/2.2d367d07.chunk.js", 14 | "static/css/main.4201b632.chunk.css", 15 | "static/js/main.18cd2cfc.chunk.js" 16 | ] 17 | } -------------------------------------------------------------------------------- /lama_cleaner/app/build/index.html: -------------------------------------------------------------------------------- 1 | lama-cleaner - Image inpainting powered by LaMa
-------------------------------------------------------------------------------- /lama_cleaner/app/build/static/js/2.2d367d07.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.20.2 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v17.0.2 17 | * react-dom.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v17.0.2 26 | * react-jsx-runtime.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v17.0.2 35 | * react.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | -------------------------------------------------------------------------------- /lama_cleaner/app/build/static/js/runtime-main.5e86ac81.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,a=r[0],f=r[1],i=r[2],p=0,s=[];p0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@typescript-eslint/eslint-plugin": "^5.1.0", 50 | "eslint-config-airbnb": "^18.2.1", 51 | "eslint-config-prettier": "^8.3.0", 52 | "eslint-plugin-import": "^2.25.2", 53 | "eslint-plugin-jsx-a11y": "^6.4.1", 54 | "eslint-plugin-prettier": "^4.0.0", 55 | "eslint-plugin-react": "^7.26.1", 56 | "eslint-plugin-react-hooks": "^4.2.0", 57 | "prettier": "^2.4.1", 58 | "sass": "^1.49.9" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lama_cleaner/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 20 | lama-cleaner - Image inpainting powered by LaMa 21 | 22 | 23 | 24 |
25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useKeyPressEvent } from 'react-use' 3 | import { useRecoilState } from 'recoil' 4 | import useInputImage from './hooks/useInputImage' 5 | import LandingPage from './components/LandingPage/LandingPage' 6 | import { themeState } from './components/Header/ThemeChanger' 7 | import Workspace from './components/Workspace' 8 | import { fileState } from './store/Atoms' 9 | import { keepGUIAlive } from './utils' 10 | import Header from './components/Header/Header' 11 | 12 | // Keeping GUI Window Open 13 | keepGUIAlive() 14 | 15 | function App() { 16 | const [file, setFile] = useRecoilState(fileState) 17 | const [theme, setTheme] = useRecoilState(themeState) 18 | const userInputImage = useInputImage() 19 | 20 | // Set Input Image 21 | useEffect(() => { 22 | setFile(userInputImage) 23 | }, [userInputImage, setFile]) 24 | 25 | // Dark Mode Hotkey 26 | useKeyPressEvent('D', ev => { 27 | ev?.preventDefault() 28 | const newTheme = theme === 'light' ? 'dark' : 'light' 29 | setTheme(newTheme) 30 | }) 31 | 32 | return ( 33 |
34 |
35 | {file ? : } 36 |
37 | ) 38 | } 39 | 40 | export default App 41 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/adapters/inpainting.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from '../store/Atoms' 2 | import { dataURItoBlob } from '../utils' 3 | 4 | export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}` 5 | 6 | export default async function inpaint( 7 | imageFile: File, 8 | maskBase64: string, 9 | settings: Settings, 10 | sizeLimit?: string 11 | ) { 12 | // 1080, 2000, Original 13 | const fd = new FormData() 14 | fd.append('image', imageFile) 15 | const mask = dataURItoBlob(maskBase64) 16 | fd.append('mask', mask) 17 | 18 | fd.append('ldmSteps', settings.ldmSteps.toString()) 19 | fd.append('hdStrategy', settings.hdStrategy) 20 | fd.append('hdStrategyCropMargin', settings.hdStrategyCropMargin.toString()) 21 | fd.append( 22 | 'hdStrategyCropTrigerSize', 23 | settings.hdStrategyCropTrigerSize.toString() 24 | ) 25 | fd.append('hdStrategyResizeLimit', settings.hdStrategyResizeLimit.toString()) 26 | 27 | if (sizeLimit === undefined) { 28 | fd.append('sizeLimit', '1080') 29 | } else { 30 | fd.append('sizeLimit', sizeLimit) 31 | } 32 | 33 | const res = await fetch(`${API_ENDPOINT}/inpaint`, { 34 | method: 'POST', 35 | body: fd, 36 | }).then(async r => { 37 | return r.blob() 38 | }) 39 | 40 | return URL.createObjectURL(res) 41 | } 42 | 43 | export function switchModel(name: string) { 44 | const fd = new FormData() 45 | fd.append('name', name) 46 | return fetch(`${API_ENDPOINT}/model`, { 47 | method: 'POST', 48 | body: fd, 49 | }) 50 | } 51 | 52 | export function currentModel() { 53 | return fetch(`${API_ENDPOINT}/model`, { 54 | method: 'GET', 55 | }) 56 | } 57 | 58 | export function modelDownloaded(name: string) { 59 | return fetch(`${API_ENDPOINT}/model_downloaded/${name}`, { 60 | method: 'GET', 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Editor/Editor.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/Mixins' as *; 2 | 3 | .editor-container { 4 | display: flex; 5 | width: 100vw; 6 | height: 100vh; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .react-transform-wrapper { 12 | display: grid !important; 13 | width: 100% !important; 14 | height: 100% !important; 15 | } 16 | 17 | .editor-canvas-container { 18 | display: grid; 19 | grid-template-areas: 'editor-content'; 20 | row-gap: 1rem; 21 | } 22 | 23 | .editor-canvas { 24 | grid-area: editor-content; 25 | z-index: 2; 26 | } 27 | 28 | .original-image-container { 29 | grid-area: editor-content; 30 | pointer-events: none; 31 | display: grid; 32 | grid-template-areas: 'original-image-content'; 33 | 34 | img { 35 | grid-area: original-image-content; 36 | } 37 | 38 | .editor-slider { 39 | grid-area: original-image-content; 40 | height: 100%; 41 | width: 6px; 42 | justify-self: end; 43 | background-color: var(--yellow-accent); 44 | transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1); 45 | z-index: 2; 46 | } 47 | } 48 | 49 | .editor-canvas-loading { 50 | pointer-events: none; 51 | animation: pulsing 750ms infinite; 52 | } 53 | 54 | .editor-toolkit-panel { 55 | // width: 100%; 56 | position: fixed; 57 | bottom: 0.5rem; 58 | border-radius: 3rem; 59 | padding: 1rem 3rem; 60 | display: grid; 61 | grid-template-areas: 'toolkit-size-selector toolkit-brush-slider toolkit-btns'; 62 | column-gap: 2rem; 63 | align-items: center; 64 | justify-content: center; 65 | backdrop-filter: blur(12px); 66 | background-color: var(--page-bg-light); 67 | animation: slideUp 0.2s ease-out; 68 | border: var(--editor-toolkit-panel-border); 69 | box-shadow: 0 0 0 1px #0000001a, 0 3px 16px #00000014, 0 2px 6px 1px #00000017; 70 | 71 | @include mobile { 72 | padding: 1rem 2rem; 73 | grid-template-areas: 74 | 'toolkit-size-selector toolkit-size-selector' 75 | 'toolkit-brush-slider toolkit-brush-slider' 76 | 'toolkit-btns toolkit-btns'; 77 | row-gap: 2rem; 78 | justify-items: center; 79 | } 80 | 81 | .eyeicon-active { 82 | background-color: var(--yellow-accent); 83 | color: var(--btn-text-hover-color); 84 | } 85 | } 86 | 87 | .editor-brush-slider { 88 | grid-area: toolkit-brush-slider; 89 | user-select: none; 90 | display: grid; 91 | grid-template-columns: repeat(2, max-content); 92 | height: max-content; 93 | column-gap: 1rem; 94 | align-items: center; 95 | 96 | @include slider-bar; 97 | } 98 | 99 | .editor-toolkit-btns { 100 | grid-area: toolkit-btns; 101 | display: grid; 102 | grid-auto-flow: column; 103 | column-gap: 1rem; 104 | } 105 | 106 | .brush-shape { 107 | position: absolute; 108 | border-radius: 50%; 109 | background-color: #ffcc00bb; 110 | border: 1px solid var(--yellow-accent); 111 | pointer-events: none; 112 | } 113 | 114 | .editor-size-selector-options { 115 | position: fixed; 116 | display: grid; 117 | } 118 | 119 | .editor-size-selector { 120 | grid-area: toolkit-size-selector; 121 | display: grid; 122 | grid-template-columns: repeat(2, max-content); 123 | align-items: center; 124 | } 125 | 126 | .editor-size-selector-main { 127 | @include accented-display(var(white)); 128 | user-select: none; 129 | display: flex; 130 | justify-content: space-between; 131 | align-items: center; 132 | cursor: pointer; 133 | outline: none; 134 | gap: 8px; 135 | width: 128px; 136 | 137 | border: 1px solid var(--editor-size-border-color); 138 | color: var(--options-text-color); 139 | 140 | svg { 141 | width: 1rem; 142 | height: 1rem; 143 | margin-top: 0.25rem; 144 | } 145 | } 146 | 147 | .editor-size-options { 148 | @include accented-display(var(--btn-primary-bg)); 149 | width: 128px; 150 | padding: 0; 151 | display: grid; 152 | justify-self: center; 153 | position: fixed; 154 | bottom: 4rem; 155 | cursor: pointer; 156 | 157 | color: var(--options-text-color); 158 | background-color: var(--page-bg); 159 | border: 1px solid var(--editor-size-border-color); 160 | 161 | border-radius: 0.6rem; 162 | 163 | @include mobile { 164 | bottom: 11.5rem; 165 | } 166 | 167 | .editor-size-option { 168 | display: flex; 169 | align-items: center; 170 | height: 40px; 171 | user-select: none; 172 | padding: 0.2rem 0.8rem; 173 | 174 | &:first-of-type { 175 | border-top-right-radius: 0.5rem; 176 | border-top-left-radius: 0.5rem; 177 | } 178 | 179 | &:last-of-type { 180 | border-bottom-left-radius: 0.5rem; 181 | border-bottom-right-radius: 0.5rem; 182 | } 183 | 184 | &:hover { 185 | background-color: var(--yellow-accent); 186 | color: var(--btn-text-hover-color); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Editor/SizeSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState } from 'react' 2 | import { useClickAway } from 'react-use' 3 | import { ChevronUpIcon } from '@heroicons/react/outline' 4 | 5 | const sizes = ['720', '1080', '2000', 'Original'] 6 | 7 | type SizeSelectorProps = { 8 | originalWidth: number 9 | originalHeight: number 10 | onChange: (value: number) => void 11 | } 12 | 13 | export default function SizeSelector(props: SizeSelectorProps) { 14 | const { originalHeight, originalWidth, onChange } = props 15 | const [showOptions, setShowOptions] = useState(false) 16 | const sizeSelectorRef = useRef(null) 17 | const [activeSize, setActiveSize] = useState('Original') 18 | const longSide: number = Math.max(originalWidth, originalHeight) 19 | 20 | const getValidSizes = useCallback(() => { 21 | const validSizes: string[] = [] 22 | for (let i = 0; i < sizes.length; i += 1) { 23 | if (sizes[i] === 'Original') { 24 | validSizes.push(sizes[i]) 25 | } 26 | if (parseInt(sizes[i], 10) < longSide) { 27 | validSizes.push(sizes[i]) 28 | } 29 | } 30 | return validSizes 31 | }, [longSide]) 32 | 33 | const getSizeShowName = useCallback( 34 | (size: string) => { 35 | if (size === 'Original') { 36 | return `${originalWidth}x${originalHeight}` 37 | } 38 | const scale = parseInt(size, 10) / longSide 39 | if (originalWidth > originalHeight) { 40 | const newHeight = Math.ceil(originalHeight * scale) 41 | return `${size}x${newHeight}` 42 | } 43 | const newWidth = Math.ceil(originalWidth * scale) 44 | return `${newWidth}x${size}` 45 | }, 46 | [originalWidth, originalHeight, longSide] 47 | ) 48 | 49 | const showOptionsHandler = () => { 50 | setShowOptions(currentShowOptionsState => !currentShowOptionsState) 51 | } 52 | 53 | useClickAway(sizeSelectorRef, () => { 54 | setShowOptions(false) 55 | }) 56 | 57 | const sizeChangeHandler = (e: any) => { 58 | const currentRes = e.target.textContent.split('x') 59 | if (originalWidth > originalHeight) { 60 | setActiveSize(currentRes[0]) 61 | onChange(currentRes[0]) 62 | } else { 63 | setActiveSize(currentRes[1]) 64 | onChange(currentRes[1]) 65 | } 66 | setShowOptions(!showOptions) 67 | } 68 | 69 | return ( 70 |
71 | 83 | 84 | {showOptions && ( 85 |
86 | {getValidSizes().map(size => ( 87 | 97 | ))} 98 |
99 | )} 100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Editor/Slider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type SliderProps = { 4 | label?: any 5 | value?: number 6 | min?: number 7 | max?: number 8 | onChange: (value: number) => void 9 | } 10 | 11 | export default function Slider(props: SliderProps) { 12 | const { value, onChange, label, min, max } = props 13 | 14 | const step = ((max || 100) - (min || 0)) / 100 15 | 16 | return ( 17 |
18 | {label} 19 | { 26 | ev.preventDefault() 27 | ev.stopPropagation() 28 | onChange(parseInt(ev.currentTarget.value, 10)) 29 | }} 30 | /> 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/FileSelect/FileSelect.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/Mixins/' as *; 2 | 3 | .file-select-label { 4 | display: grid; 5 | cursor: pointer; 6 | border: 2px dashed var(--border-color); 7 | border-radius: 0.5rem; 8 | min-width: 600px; 9 | 10 | @include mobile { 11 | min-width: 300px; 12 | } 13 | 14 | &:hover, 15 | .file-select-label-hover { 16 | color: black; 17 | background-color: var(--yellow-accent); 18 | } 19 | } 20 | 21 | .file-select-container { 22 | display: grid; 23 | padding: 4rem; 24 | width: 100%; 25 | height: 100%; 26 | 27 | input { 28 | display: none; 29 | } 30 | } 31 | 32 | .file-select-message { 33 | font-family: 'WorkSans'; 34 | text-align: center; 35 | } 36 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/FileSelect/FileSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import useResolution from '../../hooks/useResolution' 3 | 4 | type FileSelectProps = { 5 | onSelection: (file: File) => void 6 | } 7 | 8 | export default function FileSelect(props: FileSelectProps) { 9 | const { onSelection } = props 10 | 11 | const [dragHover, setDragHover] = useState(false) 12 | const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`) 13 | 14 | const resolution = useResolution() 15 | 16 | function onFileSelected(file: File) { 17 | if (!file) { 18 | return 19 | } 20 | // Skip non-image files 21 | const isImage = file.type.match('image.*') 22 | if (!isImage) { 23 | return 24 | } 25 | try { 26 | // Check if file is larger than 20mb 27 | if (file.size > 20 * 1024 * 1024) { 28 | throw new Error('file too large') 29 | } 30 | onSelection(file) 31 | } catch (e) { 32 | // eslint-disable-next-line 33 | alert(`error: ${(e as any).message}`) 34 | } 35 | } 36 | 37 | async function getFile(entry: any): Promise { 38 | return new Promise(resolve => { 39 | entry.file((file: File) => resolve(file)) 40 | }) 41 | } 42 | 43 | /* eslint-disable no-await-in-loop */ 44 | 45 | // Drop handler function to get all files 46 | async function getAllFileEntries(items: DataTransferItemList) { 47 | const fileEntries: Array = [] 48 | // Use BFS to traverse entire directory/file structure 49 | const queue = [] 50 | // Unfortunately items is not iterable i.e. no forEach 51 | for (let i = 0; i < items.length; i += 1) { 52 | queue.push(items[i].webkitGetAsEntry()) 53 | } 54 | while (queue.length > 0) { 55 | const entry = queue.shift() 56 | if (entry?.isFile) { 57 | // Only append images 58 | const file = await getFile(entry) 59 | fileEntries.push(file) 60 | } else if (entry?.isDirectory) { 61 | queue.push( 62 | ...(await readAllDirectoryEntries((entry as any).createReader())) 63 | ) 64 | } 65 | } 66 | return fileEntries 67 | } 68 | 69 | // Get all the entries (files or sub-directories) in a directory 70 | // by calling readEntries until it returns empty array 71 | async function readAllDirectoryEntries(directoryReader: any) { 72 | const entries = [] 73 | let readEntries = await readEntriesPromise(directoryReader) 74 | while (readEntries.length > 0) { 75 | entries.push(...readEntries) 76 | readEntries = await readEntriesPromise(directoryReader) 77 | } 78 | return entries 79 | } 80 | 81 | /* eslint-enable no-await-in-loop */ 82 | 83 | // Wrap readEntries in a promise to make working with readEntries easier 84 | // readEntries will return only some of the entries in a directory 85 | // e.g. Chrome returns at most 100 entries at a time 86 | async function readEntriesPromise(directoryReader: any): Promise { 87 | return new Promise((resolve, reject) => { 88 | directoryReader.readEntries(resolve, reject) 89 | }) 90 | } 91 | 92 | async function handleDrop(ev: React.DragEvent) { 93 | ev.preventDefault() 94 | const items = await getAllFileEntries(ev.dataTransfer.items) 95 | setDragHover(false) 96 | onFileSelected(items[0]) 97 | } 98 | 99 | return ( 100 | 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | height: 60px; 3 | padding: 1rem 2rem; 4 | position: absolute; 5 | top: 0; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | width: 100%; 10 | z-index: 20; 11 | backdrop-filter: blur(12px); 12 | border-bottom: 1px solid rgb(100, 100, 120, 0.2); 13 | background-color: var(--page-bg-light); 14 | } 15 | 16 | .shortcuts { 17 | z-index: 1; 18 | } 19 | 20 | .header-icons-wrapper { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | gap: 12px; 25 | justify-self: end; 26 | } 27 | 28 | .header-icons { 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | gap: 6px; 33 | justify-self: end; 34 | } -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeftIcon } from '@heroicons/react/outline' 2 | import React from 'react' 3 | import { useRecoilState } from 'recoil' 4 | import { fileState } from '../../store/Atoms' 5 | import Button from '../shared/Button' 6 | import Shortcuts from '../Shortcuts/Shortcuts' 7 | import useResolution from '../../hooks/useResolution' 8 | import { ThemeChanger } from './ThemeChanger' 9 | import SettingIcon from '../Settings/SettingIcon' 10 | 11 | const Header = () => { 12 | const [file, setFile] = useRecoilState(fileState) 13 | const resolution = useResolution() 14 | 15 | const renderHeader = () => { 16 | return ( 17 |
18 |
19 | 28 |
29 |
30 |
34 | 35 | 36 |
37 | 38 |
39 |
40 | ) 41 | } 42 | return renderHeader() 43 | } 44 | 45 | export default Header 46 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Header/ThemeChanger.scss: -------------------------------------------------------------------------------- 1 | .theme-toggle-ui { 2 | z-index: 10; 3 | transition: all 0.2s ease-in; 4 | user-select: none; 5 | 6 | .theme-btn { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | cursor: pointer; 11 | outline: none; 12 | 13 | svg { 14 | width: 22px; 15 | height: 22px; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Header/ThemeChanger.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { atom, useRecoilState } from 'recoil' 3 | import { SunIcon, MoonIcon } from '@heroicons/react/outline' 4 | 5 | export const themeState = atom({ 6 | key: 'themeState', 7 | default: 'light', 8 | }) 9 | 10 | export const ThemeChanger = () => { 11 | const [theme, setTheme] = useRecoilState(themeState) 12 | 13 | useEffect(() => { 14 | const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)') 15 | if (darkThemeMq.matches) { 16 | setTheme('dark') 17 | } else { 18 | setTheme('light') 19 | } 20 | }, [setTheme]) 21 | 22 | const themeSwitchHandler = () => { 23 | const newTheme = theme === 'light' ? 'dark' : 'light' 24 | setTheme(newTheme) 25 | } 26 | 27 | return ( 28 |
29 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/LandingPage/LandingPage.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/Mixins/' as *; 2 | 3 | .landing-page { 4 | display: grid; 5 | place-self: center; 6 | justify-items: center; 7 | row-gap: 2rem; 8 | grid-auto-rows: max-content; 9 | 10 | @include mobile { 11 | padding: 1rem; 12 | } 13 | 14 | h1 { 15 | text-align: center; 16 | font-size: 1.4rem; 17 | 18 | @include mobile { 19 | font-size: 1.2rem; 20 | } 21 | } 22 | 23 | a { 24 | color: var(--link-color); 25 | } 26 | } 27 | 28 | .landing-file-selector { 29 | display: grid; 30 | } 31 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/LandingPage/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSetRecoilState } from 'recoil' 3 | import { fileState } from '../../store/Atoms' 4 | import FileSelect from '../FileSelect/FileSelect' 5 | 6 | const LandingPage = () => { 7 | const setFile = useSetRecoilState(fileState) 8 | 9 | return ( 10 |
11 |

12 | Image inpainting powered by 🦙 13 | LaMa 14 |

15 |
16 | { 18 | setFile(f) 19 | }} 20 | /> 21 |
22 |
23 | ) 24 | } 25 | 26 | export default LandingPage 27 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/HDSettingBlock.scss: -------------------------------------------------------------------------------- 1 | .hd-setting-block { 2 | .inline-tip { 3 | display: inline; 4 | cursor: pointer; 5 | color: var(--text-color); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/HDSettingBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import { settingState } from '../../store/Atoms' 4 | import Selector from '../shared/Selector' 5 | import NumberInputSetting from './NumberInputSetting' 6 | import SettingBlock from './SettingBlock' 7 | 8 | export enum HDStrategy { 9 | ORIGINAL = 'Original', 10 | RESIZE = 'Resize', 11 | CROP = 'Crop', 12 | } 13 | 14 | function HDSettingBlock() { 15 | const [setting, setSettingState] = useRecoilState(settingState) 16 | 17 | const onStrategyChange = (value: HDStrategy) => { 18 | setSettingState(old => { 19 | return { ...old, hdStrategy: value } 20 | }) 21 | } 22 | 23 | const onResizeLimitChange = (value: string) => { 24 | const val = value.length === 0 ? 0 : parseInt(value, 10) 25 | setSettingState(old => { 26 | return { ...old, hdStrategyResizeLimit: val } 27 | }) 28 | } 29 | 30 | const onCropTriggerSizeChange = (value: string) => { 31 | const val = value.length === 0 ? 0 : parseInt(value, 10) 32 | setSettingState(old => { 33 | return { ...old, hdStrategyCropTrigerSize: val } 34 | }) 35 | } 36 | 37 | const onCropMarginChange = (value: string) => { 38 | const val = value.length === 0 ? 0 : parseInt(value, 10) 39 | setSettingState(old => { 40 | return { ...old, hdStrategyCropMargin: val } 41 | }) 42 | } 43 | 44 | const renderOriginalOptionDesc = () => { 45 | return ( 46 |
47 | Use the original resolution of the picture, suitable for picture size 48 | below 2K. Try{' '} 49 |
onStrategyChange(HDStrategy.RESIZE)} 54 | > 55 | Resize Strategy 56 |
{' '} 57 | if you do not get good results on high resolution images. 58 |
59 | ) 60 | } 61 | 62 | const renderResizeOptionDesc = () => { 63 | return ( 64 |
65 |
66 | Resize the longer side of the image to a specific size(keep ratio), 67 | then do inpainting on the resized image. 68 |
69 | 75 |
76 | ) 77 | } 78 | 79 | const renderCropOptionDesc = () => { 80 | return ( 81 |
82 |
83 | Crop masking area from the original image to do inpainting, and paste 84 | the result back. Mainly for performance and memory reasons on high 85 | resolution image. 86 |
87 | 93 | 99 |
100 | ) 101 | } 102 | 103 | const renderHDStrategyOptionDesc = (): ReactNode => { 104 | switch (setting.hdStrategy) { 105 | case HDStrategy.ORIGINAL: 106 | return renderOriginalOptionDesc() 107 | case HDStrategy.CROP: 108 | return renderCropOptionDesc() 109 | case HDStrategy.RESIZE: 110 | return renderResizeOptionDesc() 111 | default: 112 | return renderOriginalOptionDesc() 113 | } 114 | } 115 | 116 | return ( 117 | onStrategyChange(val as HDStrategy)} 125 | /> 126 | } 127 | optionDesc={renderHDStrategyOptionDesc()} 128 | /> 129 | ) 130 | } 131 | 132 | export default HDSettingBlock 133 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/ManualRunInpaintingSettingBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import { settingState } from '../../store/Atoms' 4 | import { Switch, SwitchThumb } from '../shared/Switch' 5 | import SettingBlock from './SettingBlock' 6 | 7 | const ManualRunInpaintingSettingBlock: React.FC = () => { 8 | const [setting, setSettingState] = useRecoilState(settingState) 9 | 10 | const onCheckChange = (checked: boolean) => { 11 | setSettingState(old => { 12 | return { ...old, runInpaintingManually: checked } 13 | }) 14 | } 15 | 16 | return ( 17 | 24 | 25 | 26 | } 27 | /> 28 | ) 29 | } 30 | 31 | export default ManualRunInpaintingSettingBlock 32 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/ModelSettingBlock.scss: -------------------------------------------------------------------------------- 1 | .model-desc-link { 2 | color: var(--text-color-gray); 3 | text-decoration: none; 4 | } 5 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/ModelSettingBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import { settingState } from '../../store/Atoms' 4 | import Selector from '../shared/Selector' 5 | import NumberInputSetting from './NumberInputSetting' 6 | import SettingBlock from './SettingBlock' 7 | 8 | export enum AIModel { 9 | LAMA = 'lama', 10 | LDM = 'ldm', 11 | } 12 | 13 | function ModelSettingBlock() { 14 | const [setting, setSettingState] = useRecoilState(settingState) 15 | 16 | const onModelChange = (value: AIModel) => { 17 | setSettingState(old => { 18 | return { ...old, model: value } 19 | }) 20 | } 21 | 22 | const renderModelDesc = ( 23 | name: string, 24 | paperUrl: string, 25 | githubUrl: string 26 | ) => { 27 | return ( 28 | 47 | ) 48 | } 49 | 50 | const renderLDMModelDesc = () => { 51 | return ( 52 |
53 | {renderModelDesc( 54 | 'High-Resolution Image Synthesis with Latent Diffusion Models', 55 | 'https://arxiv.org/abs/2112.10752', 56 | 'https://github.com/CompVis/latent-diffusion' 57 | )} 58 | { 62 | const val = value.length === 0 ? 0 : parseInt(value, 10) 63 | setSettingState(old => { 64 | return { ...old, ldmSteps: val } 65 | }) 66 | }} 67 | /> 68 |
69 | ) 70 | } 71 | 72 | const renderOptionDesc = (): ReactNode => { 73 | switch (setting.model) { 74 | case AIModel.LAMA: 75 | return renderModelDesc( 76 | 'Resolution-robust Large Mask Inpainting with Fourier Convolutions', 77 | 'https://arxiv.org/abs/2109.07161', 78 | 'https://github.com/saic-mdal/lama' 79 | ) 80 | case AIModel.LDM: 81 | return renderLDMModelDesc() 82 | default: 83 | return <> 84 | } 85 | } 86 | 87 | return ( 88 | onModelChange(val as AIModel)} 96 | /> 97 | } 98 | optionDesc={renderOptionDesc()} 99 | /> 100 | ) 101 | } 102 | 103 | export default ModelSettingBlock 104 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/NumberInputSetting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NumberInput from '../shared/NumberInput' 3 | import SettingBlock from './SettingBlock' 4 | 5 | interface NumberInputSettingProps { 6 | title: string 7 | value: string 8 | suffix?: string 9 | onValue: (val: string) => void 10 | } 11 | 12 | function NumberInputSetting(props: NumberInputSettingProps) { 13 | const { title, value, suffix, onValue } = props 14 | 15 | return ( 16 | 28 | 33 | {suffix && {suffix}} 34 | 35 | } 36 | /> 37 | ) 38 | } 39 | 40 | export default NumberInputSetting 41 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/SettingBlock.scss: -------------------------------------------------------------------------------- 1 | .setting-block { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | .option-desc { 6 | color: var(--text-color-gray); 7 | margin-top: 12px; 8 | border: 1px solid var(--border-color); 9 | border-radius: 0.3rem; 10 | padding: 1rem; 11 | 12 | .sub-setting-block { 13 | margin-top: 8px; 14 | color: var(--text-color); 15 | } 16 | } 17 | } 18 | 19 | .setting-block-content { 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | gap: 12rem; 24 | } 25 | 26 | .setting-block-content-title { 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: space-between; 30 | } 31 | 32 | .setting-block-desc { 33 | font-size: 1rem; 34 | margin-top: 8px; 35 | color: var(--text-color-gray); 36 | } 37 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/SettingBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | 3 | interface SettingBlockProps { 4 | title: string 5 | desc?: string 6 | input: ReactNode 7 | optionDesc?: ReactNode 8 | className?: string 9 | } 10 | 11 | function SettingBlock(props: SettingBlockProps) { 12 | const { title, desc, input, optionDesc, className } = props 13 | return ( 14 |
15 |
16 |
17 | {title} 18 | {desc && {desc}} 19 |
20 | {input} 21 |
22 | {optionDesc &&
{optionDesc}
} 23 |
24 | ) 25 | } 26 | 27 | export default SettingBlock 28 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/SettingIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import { settingState } from '../../store/Atoms' 4 | import Button from '../shared/Button' 5 | 6 | const SettingIcon = () => { 7 | const [setting, setSettingState] = useRecoilState(settingState) 8 | 9 | const onClick = () => { 10 | setSettingState({ ...setting, show: !setting.show }) 11 | } 12 | 13 | return ( 14 |
15 |
43 | ) 44 | } 45 | 46 | export default SettingIcon 47 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/Settings.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/Mixins/' as *; 2 | @import './SettingBlock.scss'; 3 | @import './HDSettingBlock.scss'; 4 | @import './ModelSettingBlock.scss'; 5 | 6 | .modal-setting { 7 | grid-area: main-content; 8 | background-color: var(--modal-bg); 9 | color: var(--modal-text-color); 10 | box-shadow: 0px 0px 20px rgb(0, 0, 40, 0.2); 11 | width: 700px; 12 | 13 | @include mobile { 14 | display: grid; 15 | width: 100%; 16 | height: auto; 17 | margin-top: -11rem; 18 | animation: slideDown 0.2s ease-out; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Settings/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useRecoilState } from 'recoil' 4 | import { settingState } from '../../store/Atoms' 5 | import Modal from '../shared/Modal' 6 | import ManualRunInpaintingSettingBlock from './ManualRunInpaintingSettingBlock' 7 | import HDSettingBlock from './HDSettingBlock' 8 | import ModelSettingBlock from './ModelSettingBlock' 9 | 10 | interface SettingModalProps { 11 | onClose: () => void 12 | } 13 | export default function SettingModal(props: SettingModalProps) { 14 | const { onClose } = props 15 | const [setting, setSettingState] = useRecoilState(settingState) 16 | 17 | const handleOnClose = () => { 18 | setSettingState(old => { 19 | return { ...old, show: false } 20 | }) 21 | onClose() 22 | } 23 | 24 | return ( 25 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Shortcuts/Shortcuts.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/Mixins/' as *; 2 | 3 | .modal-shortcuts { 4 | grid-area: main-content; 5 | background-color: var(--modal-bg); 6 | color: var(--modal-text-color); 7 | box-shadow: 0px 0px 20px rgb(0, 0, 40, 0.2); 8 | 9 | @include mobile { 10 | display: grid; 11 | width: 100%; 12 | height: auto; 13 | margin-top: -11rem; 14 | animation: slideDown 0.2s ease-out; 15 | } 16 | } 17 | 18 | .shortcut-options { 19 | display: grid; 20 | row-gap: 1rem; 21 | 22 | .shortcut-option { 23 | display: grid; 24 | grid-template-columns: repeat(2, auto); 25 | column-gap: 6rem; 26 | align-items: center; 27 | 28 | @include mobile { 29 | grid-template-columns: auto; 30 | column-gap: 0; 31 | row-gap: 0.6rem; 32 | } 33 | } 34 | 35 | .shortcut-key { 36 | justify-self: end; 37 | font-family: 'WorkSans-Bold'; 38 | border: 1px solid var(--modal-hotkey-border-color); 39 | padding: 0.4rem 1rem; 40 | width: max-content; 41 | border-radius: 0.4rem; 42 | 43 | @include mobile { 44 | padding: 0.2rem 0.4rem; 45 | } 46 | } 47 | 48 | .shortcut-description { 49 | justify-self: start; 50 | text-align: left; 51 | width: 18rem; 52 | 53 | @include mobile { 54 | text-align: left; 55 | width: auto; 56 | justify-self: start; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Shortcuts/Shortcuts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useKeyPressEvent } from 'react-use' 3 | import { useRecoilState } from 'recoil' 4 | import { shortcutsState } from '../../store/Atoms' 5 | import Button from '../shared/Button' 6 | 7 | const Shortcuts = () => { 8 | const [shortcutVisibility, setShortcutState] = useRecoilState(shortcutsState) 9 | 10 | const shortcutStateHandler = () => { 11 | setShortcutState(prevShortcutState => { 12 | return !prevShortcutState 13 | }) 14 | } 15 | 16 | useKeyPressEvent('h', ev => { 17 | ev?.preventDefault() 18 | shortcutStateHandler() 19 | }) 20 | 21 | return ( 22 |
23 |
53 | ) 54 | } 55 | 56 | export default Shortcuts 57 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Shortcuts/ShortcutsModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import { shortcutsState } from '../../store/Atoms' 4 | import Modal from '../shared/Modal' 5 | 6 | interface Shortcut { 7 | children: ReactNode 8 | content: string 9 | } 10 | 11 | function ShortCut(props: Shortcut) { 12 | const { children, content } = props 13 | 14 | return ( 15 |
16 |
{content}
17 |
{children}
18 |
19 | ) 20 | } 21 | 22 | export default function ShortcutsModal() { 23 | const [shortcutsShow, setShortcutState] = useRecoilState(shortcutsState) 24 | 25 | const shortcutStateHandler = () => { 26 | setShortcutState(false) 27 | } 28 | 29 | return ( 30 | 36 |
37 | 38 |

Hold Cmd/Ctrl

39 |
40 | 41 |

Cmd/Ctrl + Z

42 |
43 | 44 |

Space & Drag

45 |
46 | 47 |

Hold Tab

48 |
49 | 50 |

Esc

51 |
52 | 53 |

Esc

54 |
55 | 56 |

[

57 |
58 | 59 |

]

60 |
61 | 62 |

Shift + D

63 |
64 | 65 |

H

66 |
67 |
68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/Workspace.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import Editor from './Editor/Editor' 4 | import ShortcutsModal from './Shortcuts/ShortcutsModal' 5 | import SettingModal from './Settings/SettingsModal' 6 | import Toast from './shared/Toast' 7 | import { Settings, settingState, toastState } from '../store/Atoms' 8 | import { 9 | currentModel, 10 | modelDownloaded, 11 | switchModel, 12 | } from '../adapters/inpainting' 13 | import { AIModel } from './Settings/ModelSettingBlock' 14 | 15 | interface WorkspaceProps { 16 | file: File 17 | } 18 | 19 | const Workspace = ({ file }: WorkspaceProps) => { 20 | const [settings, setSettingState] = useRecoilState(settingState) 21 | const [toastVal, setToastState] = useRecoilState(toastState) 22 | 23 | const onSettingClose = async () => { 24 | const curModel = await currentModel().then(res => res.text()) 25 | if (curModel === settings.model) { 26 | return 27 | } 28 | const downloaded = await modelDownloaded(settings.model).then(res => 29 | res.text() 30 | ) 31 | 32 | const { model } = settings 33 | 34 | let loadingMessage = `Switching to ${model} model` 35 | let loadingDuration = 3000 36 | if (downloaded === 'False') { 37 | loadingMessage = `Downloading ${model} model, this may take a while` 38 | loadingDuration = 9999999999 39 | } 40 | 41 | setToastState({ 42 | open: true, 43 | desc: loadingMessage, 44 | state: 'loading', 45 | duration: loadingDuration, 46 | }) 47 | 48 | switchModel(model) 49 | .then(res => { 50 | if (res.ok) { 51 | setToastState({ 52 | open: true, 53 | desc: `Switch to ${model} model success`, 54 | state: 'success', 55 | duration: 3000, 56 | }) 57 | } else { 58 | throw new Error('Server error') 59 | } 60 | }) 61 | .catch(() => { 62 | setToastState({ 63 | open: true, 64 | desc: `Switch to ${model} model failed`, 65 | state: 'error', 66 | duration: 3000, 67 | }) 68 | setSettingState(old => { 69 | return { ...old, model: curModel as AIModel } 70 | }) 71 | }) 72 | } 73 | 74 | useEffect(() => { 75 | currentModel() 76 | .then(res => res.text()) 77 | .then(model => { 78 | setSettingState(old => { 79 | return { ...old, model: model as AIModel } 80 | }) 81 | }) 82 | }, []) 83 | 84 | return ( 85 | <> 86 | 87 | 88 | 89 | { 92 | setToastState(old => { 93 | return { ...old, open } 94 | }) 95 | }} 96 | /> 97 | 98 | ) 99 | } 100 | 101 | export default Workspace 102 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Button.scss: -------------------------------------------------------------------------------- 1 | .btn-primary { 2 | display: grid; 3 | grid-auto-flow: column; 4 | column-gap: 1rem; 5 | border: 1px solid var(--btn-border-color); 6 | color: var(--btn-text-color); 7 | font-family: 'WorkSans', sans-serif; 8 | width: max-content; 9 | padding: 0.5rem; 10 | place-items: center; 11 | border-radius: 0.5rem; 12 | z-index: 1; 13 | cursor: pointer; 14 | 15 | &:hover { 16 | background-color: var(--btn-primary-hover-bg); 17 | color: var(--btn-text-hover-color); 18 | } 19 | 20 | svg { 21 | width: 20px; 22 | height: auto; 23 | } 24 | } 25 | 26 | .btn-primary-disabled { 27 | pointer-events: none; 28 | opacity: 0.5; 29 | } 30 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | 3 | interface ButtonProps { 4 | disabled?: boolean 5 | children?: ReactNode 6 | className?: string 7 | icon?: ReactNode 8 | onKeyDown?: () => void 9 | onClick?: () => void 10 | onDown?: (ev: PointerEvent) => void 11 | onUp?: (ev: PointerEvent) => void 12 | style?: React.CSSProperties 13 | } 14 | 15 | const Button: React.FC = props => { 16 | const { 17 | children, 18 | className, 19 | disabled, 20 | icon, 21 | onKeyDown, 22 | onClick, 23 | onDown, 24 | onUp, 25 | style, 26 | } = props 27 | 28 | const blurOnClick = (e: React.MouseEvent) => { 29 | e.currentTarget.blur() 30 | onClick?.() 31 | } 32 | 33 | return ( 34 |
) => { 40 | onDown?.(ev.nativeEvent) 41 | }} 42 | onPointerUp={(ev: React.PointerEvent) => { 43 | onUp?.(ev.nativeEvent) 44 | }} 45 | tabIndex={-1} 46 | className={[ 47 | 'btn-primary', 48 | children ? 'btn-primary-content' : '', 49 | disabled === true ? 'btn-primary-disabled' : '', 50 | className, 51 | ].join(' ')} 52 | > 53 | {icon} 54 | {children ? {children} : null} 55 |
56 | ) 57 | } 58 | 59 | Button.defaultProps = { 60 | disabled: false, 61 | } 62 | 63 | export default Button 64 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface LinkProps { 4 | children: string 5 | href: string 6 | } 7 | 8 | export default function Link(props: LinkProps) { 9 | const { children, href } = props 10 | return {children} 11 | } 12 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Modal.scss: -------------------------------------------------------------------------------- 1 | .modal-mask { 2 | z-index: 9999; 3 | height: 100%; 4 | width: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | position: absolute; 9 | background-color: var(--model-mask-bg); 10 | backdrop-filter: blur(12px); 11 | -webkit-backdrop-filter: blur(12px); 12 | } 13 | 14 | .modal { 15 | display: grid; 16 | grid-auto-rows: max-content; 17 | row-gap: 2rem; 18 | place-self: center; 19 | padding: 2rem; 20 | border-radius: 0.95rem; 21 | 22 | .modal-header { 23 | display: grid; 24 | grid-template-columns: repeat(2, auto); 25 | align-items: center; 26 | 27 | .btn-primary { 28 | justify-self: end; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { XIcon } from '@heroicons/react/outline' 2 | import React, { ReactNode, useRef } from 'react' 3 | import { useClickAway, useKey, useKeyPress, useKeyPressEvent } from 'react-use' 4 | import Button from './Button' 5 | 6 | export interface ModalProps { 7 | show: boolean 8 | children?: ReactNode 9 | onClose?: () => void 10 | title: string 11 | className?: string 12 | } 13 | 14 | export default function Modal(props: ModalProps) { 15 | const { show, children, onClose, className, title } = props 16 | const ref = useRef(null) 17 | 18 | useClickAway(ref, () => { 19 | if (show) { 20 | onClose?.() 21 | } 22 | }) 23 | 24 | useKeyPressEvent('Escape', e => { 25 | if (show) { 26 | onClose?.() 27 | } 28 | }) 29 | 30 | return ( 31 |
35 |
36 |
37 |

{title}

38 |
40 | {children} 41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/NumberInput.scss: -------------------------------------------------------------------------------- 1 | .number-input { 2 | all: unset; 3 | flex: 1 0 auto; 4 | border-radius: 0.5rem; 5 | padding: 0.2rem 0.8rem; 6 | line-height: 1; 7 | outline: 1px solid var(--border-color); 8 | 9 | &:focus-visible { 10 | outline: 1px solid var(--yellow-accent); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/NumberInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, InputHTMLAttributes } from 'react' 2 | 3 | interface NumberInputProps extends InputHTMLAttributes { 4 | value: string 5 | onValue?: (val: string) => void 6 | } 7 | 8 | const NumberInput = React.forwardRef( 9 | (props: NumberInputProps, forwardedRef) => { 10 | const { value, onValue, ...itemProps } = props 11 | 12 | const handleOnInput = (evt: FormEvent) => { 13 | const target = evt.target as HTMLInputElement 14 | const val = target.value.replace(/\D/g, '') 15 | onValue?.(val) 16 | } 17 | 18 | return ( 19 | 27 | ) 28 | } 29 | ) 30 | 31 | export default NumberInput 32 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Selector.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/Mixins' as *; 2 | 3 | .selector { 4 | position: relative; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-between; 9 | } 10 | 11 | .selector-main { 12 | @include accented-display(var(white)); 13 | width: 100%; 14 | user-select: none; 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | cursor: pointer; 19 | outline: none; 20 | gap: 8px; 21 | padding: 0.2rem 0.8rem; 22 | 23 | border: 1px solid var(--border-color); 24 | color: var(--options-text-color); 25 | 26 | svg { 27 | width: 1rem; 28 | height: 1rem; 29 | margin-top: 0.25rem; 30 | } 31 | } 32 | 33 | .selector-options { 34 | @include accented-display(var(--btn-primary-bg)); 35 | width: 100%; 36 | padding: 0; 37 | display: grid; 38 | justify-self: center; 39 | position: absolute; 40 | cursor: pointer; 41 | top: 3rem; 42 | 43 | color: var(--options-text-color); 44 | background-color: var(--page-bg); 45 | border: 1px solid var(--border-color); 46 | 47 | border-radius: 0.6rem; 48 | 49 | @include mobile { 50 | bottom: 11.5rem; 51 | } 52 | 53 | .selector-option { 54 | display: flex; 55 | align-items: center; 56 | user-select: none; 57 | padding: 0.5rem 0.8rem; 58 | 59 | &:first-of-type { 60 | border-top-right-radius: 0.5rem; 61 | border-top-left-radius: 0.5rem; 62 | } 63 | 64 | &:last-of-type { 65 | border-bottom-left-radius: 0.5rem; 66 | border-bottom-right-radius: 0.5rem; 67 | } 68 | 69 | &:hover { 70 | background-color: var(--yellow-accent); 71 | color: var(--btn-text-hover-color); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Selector.tsx: -------------------------------------------------------------------------------- 1 | import React, { MutableRefObject, useCallback, useRef, useState } from 'react' 2 | import { useClickAway, useKeyPressEvent } from 'react-use' 3 | import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline' 4 | 5 | type SelectorChevronDirection = 'up' | 'down' 6 | 7 | type SelectorProps = { 8 | minWidth?: number 9 | chevronDirection?: SelectorChevronDirection 10 | value: string 11 | options: string[] 12 | onChange: (value: string) => void 13 | } 14 | 15 | const selectorDefaultProps = { 16 | minWidth: 128, 17 | chevronDirection: 'down', 18 | } 19 | 20 | function Selector(props: SelectorProps) { 21 | const { minWidth, chevronDirection, value, options, onChange } = props 22 | const [showOptions, setShowOptions] = useState(false) 23 | const selectorRef = useRef(null) 24 | 25 | const showOptionsHandler = () => { 26 | // console.log(selectorRef.current?.focus) 27 | // selectorRef?.current?.focus() 28 | setShowOptions(currentShowOptionsState => !currentShowOptionsState) 29 | } 30 | 31 | useClickAway(selectorRef, () => { 32 | setShowOptions(false) 33 | }) 34 | 35 | // TODO: how to prevent Modal close? 36 | // useKeyPressEvent('Escape', (e: KeyboardEvent) => { 37 | // if (showOptions === true) { 38 | // console.log(`selector ${e}`) 39 | // e.preventDefault() 40 | // e.stopPropagation() 41 | // setShowOptions(false) 42 | // } 43 | // }) 44 | 45 | const onOptionClick = (e: any, newIndex: number) => { 46 | const currentRes = e.target.textContent.split('x') 47 | onChange(currentRes[0]) 48 | setShowOptions(false) 49 | } 50 | 51 | return ( 52 |
53 | 64 | 65 | {showOptions && ( 66 |
67 | {options.map((val, _index) => ( 68 |
onOptionClick(e, _index)} 74 | aria-hidden="true" 75 | > 76 | {val} 77 |
78 | ))} 79 |
80 | )} 81 |
82 | ) 83 | } 84 | 85 | Selector.defaultProps = selectorDefaultProps 86 | export default Selector 87 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Switch.scss: -------------------------------------------------------------------------------- 1 | .switch-root { 2 | all: 'unset'; 3 | width: 42px; 4 | height: 25px; 5 | background-color: var(--switch-root-background-color); 6 | border-radius: 9999px; 7 | border: none; 8 | position: relative; 9 | transition: background-color 100ms; 10 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 11 | 12 | &:focus-visible { 13 | outline: none; 14 | } 15 | } 16 | 17 | .switch-root[data-state='checked'] { 18 | background-color: var(--yellow-accent); 19 | } 20 | 21 | .switch-thumb { 22 | display: block; 23 | width: 17px; 24 | height: 17px; 25 | background-color: var(--switch-thumb-color); 26 | border-radius: 9999px; 27 | transition: transform 100ms; 28 | transform: translateX(4px); 29 | will-change: transform; 30 | } 31 | 32 | .switch-thumb[data-state='checked'] { 33 | transform: translateX(21px); 34 | background-color: var(--switch-thumb-checked-color); 35 | outline: 1px solid rgb(100, 100, 120, 0.5); 36 | } 37 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as SwitchPrimitive from '@radix-ui/react-switch' 3 | 4 | const Switch = React.forwardRef< 5 | React.ElementRef, 6 | React.ComponentProps 7 | >((props, forwardedRef) => { 8 | const { className, ...itemProps } = props 9 | 10 | return ( 11 | 16 | ) 17 | }) 18 | 19 | const SwitchThumb = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentProps 22 | >((props, forwardedRef) => { 23 | const { className, ...itemProps } = props 24 | 25 | return ( 26 | 31 | ) 32 | }) 33 | 34 | export { Switch, SwitchThumb } 35 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Toast.scss: -------------------------------------------------------------------------------- 1 | .toast-viewpoint { 2 | position: fixed; 3 | top: 48px; 4 | right: 0; 5 | display: flex; 6 | flex-direction: row; 7 | padding: 25px; 8 | gap: 10px; 9 | max-width: 100vw; 10 | margin: 0; 11 | z-index: 999999; 12 | 13 | &:focus-visible { 14 | outline: none; 15 | } 16 | } 17 | 18 | .toast-root { 19 | border: 1px solid var(--border-color-light); 20 | background-color: var(--page-bg); 21 | border-radius: 0.6rem; 22 | padding: 15px; 23 | display: flex; 24 | align-items: center; 25 | 26 | gap: 12px; 27 | 28 | &[data-state='open'] { 29 | animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); 30 | } 31 | 32 | &[data-state='close'] { 33 | animation: opacityReveal 100ms ease-in forwards; 34 | } 35 | 36 | &[data-state='cancel'] { 37 | transform: translateX(0); 38 | animation: transform 100ms ease-out; 39 | } 40 | 41 | &.error { 42 | border: 1px solid var(--error-color); 43 | } 44 | 45 | &.success { 46 | border: 1px solid var(--success-color); 47 | } 48 | } 49 | 50 | .error-icon { 51 | height: 24px; 52 | width: 24px; 53 | color: var(--error-color); 54 | } 55 | 56 | .success-icon { 57 | height: 24px; 58 | width: 24px; 59 | color: var(--success-color); 60 | } 61 | 62 | .loading-icon { 63 | display: flex; 64 | align-items: center; 65 | animation-name: spin; 66 | animation-duration: 1500ms; 67 | animation-iteration-count: infinite; 68 | transform-origin: center center; 69 | animation-timing-function: linear; 70 | } 71 | 72 | .toast-icon { 73 | display: flex; 74 | align-items: center; 75 | } 76 | 77 | .toast-desc { 78 | display: flex; 79 | align-items: center; 80 | margin: 0; 81 | color: var(--text-color); 82 | min-width: 240px; 83 | } 84 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/components/shared/Toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ToastPrimitive from '@radix-ui/react-toast' 3 | import { ToastProps } from '@radix-ui/react-toast' 4 | import { CheckIcon, ExclamationCircleIcon } from '@heroicons/react/outline' 5 | 6 | const LoadingIcon = () => { 7 | return ( 8 | 9 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export type ToastState = 'default' | 'error' | 'loading' | 'success' 34 | 35 | interface MyToastProps extends ToastProps { 36 | desc: string 37 | state?: ToastState 38 | } 39 | 40 | const Toast = React.forwardRef< 41 | React.ElementRef, 42 | MyToastProps 43 | >((props, forwardedRef) => { 44 | const { state, desc, ...itemProps } = props 45 | 46 | const getIcon = () => { 47 | switch (state) { 48 | case 'error': 49 | return 50 | case 'success': 51 | return 52 | case 'loading': 53 | return 54 | default: 55 | return <> 56 | } 57 | } 58 | 59 | return ( 60 | 61 | 66 |
{getIcon()}
67 | 68 | {desc} 69 | 70 |
71 | 72 |
73 | ) 74 | }) 75 | 76 | Toast.defaultProps = { 77 | desc: '', 78 | state: 'loading', 79 | } 80 | 81 | export default Toast 82 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/hooks/useInputImage.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | export default function useInputImage() { 4 | const [inputImage, setInputImage] = useState() 5 | 6 | const fetchInputImage = useCallback(() => { 7 | const headers = new Headers() 8 | headers.append('pragma', 'no-cache') 9 | headers.append('cache-control', 'no-cache') 10 | 11 | fetch('/inputimage', { headers }).then(async res => { 12 | const filename = res.headers 13 | .get('content-disposition') 14 | ?.split('filename=')[1] 15 | .split(';')[0] 16 | 17 | const data = await res.blob() 18 | if (data && data.type.startsWith('image')) { 19 | const userInput = new File( 20 | [data], 21 | filename !== undefined ? filename : 'inputImage' 22 | ) 23 | setInputImage(userInput) 24 | } 25 | }) 26 | }, [setInputImage]) 27 | 28 | useEffect(() => { 29 | fetchInputImage() 30 | }, [fetchInputImage]) 31 | 32 | return inputImage 33 | } 34 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/hooks/useResolution.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | const useResolution = () => { 4 | const [width, setWidth] = useState(window.innerWidth) 5 | 6 | const windowSizeHandler = useCallback(() => { 7 | setWidth(window.innerWidth) 8 | }, []) 9 | 10 | useEffect(() => { 11 | window.addEventListener('resize', windowSizeHandler) 12 | 13 | return () => { 14 | window.removeEventListener('resize', windowSizeHandler) 15 | } 16 | }) 17 | 18 | if (width < 768) { 19 | return 'mobile' 20 | } 21 | 22 | if (width >= 768 && width < 1224) { 23 | return 'tablet' 24 | } 25 | 26 | if (width >= 1224) { 27 | return 'desktop' 28 | } 29 | } 30 | 31 | export default useResolution 32 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './styles/_index.scss' 4 | import { RecoilRoot } from 'recoil' 5 | import App from './App' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ) 15 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Black.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-BlackItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Bold.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-BoldItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraBold.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraLight.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Italic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Light.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-LightItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Medium.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-MediumItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Regular.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-SemiBold.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Thin.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-ThinItalic.ttf -------------------------------------------------------------------------------- /lama_cleaner/app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect' 6 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/store/Atoms.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil' 2 | import { HDStrategy } from '../components/Settings/HDSettingBlock' 3 | import { AIModel } from '../components/Settings/ModelSettingBlock' 4 | import { ToastState } from '../components/shared/Toast' 5 | 6 | export const fileState = atom({ 7 | key: 'fileState', 8 | default: undefined, 9 | }) 10 | 11 | interface ToastAtomState { 12 | open: boolean 13 | desc: string 14 | state: ToastState 15 | duration: number 16 | } 17 | 18 | export const toastState = atom({ 19 | key: 'toastState', 20 | default: { 21 | open: false, 22 | desc: '', 23 | state: 'default', 24 | duration: 3000, 25 | }, 26 | }) 27 | 28 | export const shortcutsState = atom({ 29 | key: 'shortcutsState', 30 | default: false, 31 | }) 32 | 33 | export interface Settings { 34 | show: boolean 35 | runInpaintingManually: boolean 36 | model: AIModel 37 | 38 | // For LaMa 39 | hdStrategy: HDStrategy 40 | hdStrategyResizeLimit: number 41 | hdStrategyCropTrigerSize: number 42 | hdStrategyCropMargin: number 43 | 44 | // For LDM 45 | ldmSteps: number 46 | } 47 | 48 | export const settingStateDefault = { 49 | show: false, 50 | runInpaintingManually: false, 51 | model: AIModel.LAMA, 52 | ldmSteps: 50, 53 | hdStrategy: HDStrategy.RESIZE, 54 | hdStrategyResizeLimit: 2048, 55 | hdStrategyCropTrigerSize: 2048, 56 | hdStrategyCropMargin: 128, 57 | } 58 | 59 | const localStorageEffect = 60 | (key: string) => 61 | ({ setSelf, onSet }: any) => { 62 | const savedValue = localStorage.getItem(key) 63 | if (savedValue != null) { 64 | const storageSettings = JSON.parse(savedValue) 65 | storageSettings.show = false 66 | setSelf(storageSettings) 67 | } 68 | 69 | onSet((newValue: Settings, _: string, isReset: boolean) => 70 | isReset 71 | ? localStorage.removeItem(key) 72 | : localStorage.setItem(key, JSON.stringify(newValue)) 73 | ) 74 | } 75 | 76 | // Each atom can reference an array of these atom effect functions which are called in priority order when the atom is initialized 77 | // https://recoiljs.org/docs/guides/atom-effects/#local-storage-persistence 78 | export const settingState = atom({ 79 | key: 'settingsState', 80 | default: settingStateDefault, 81 | effects: [localStorageEffect('settingsState')], 82 | }) 83 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/App.scss: -------------------------------------------------------------------------------- 1 | .lama-cleaner { 2 | display: grid; 3 | grid-template-areas: 'main-content'; 4 | width: 100vw; 5 | height: 100vh; 6 | background-color: var(--page-bg); 7 | color: var(--page-text-color); 8 | transition-property: background-color, color; 9 | transition-duration: 0.2s; 10 | transition-timing-function: repeat(2, ease-out); 11 | } 12 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/Mixins/_MediaQueries.scss: -------------------------------------------------------------------------------- 1 | // Define Breakpoints 2 | $breakpoints: ( 3 | mobile-res: 768px, 4 | desktop-res: 1224px, 5 | ); 6 | 7 | // Calcualte Min and Max Widths Based on Breakpoints 8 | $sizes: ( 9 | mobile-max-width: calc(map-get($breakpoints, mobile-res) - 1px), 10 | tablet-min-width: map-get($breakpoints, mobile-res), 11 | tablet-max-width: calc(map-get($breakpoints, desktop-res) - 1px), 12 | desktop-min-width: map-get($breakpoints, desktop-res), 13 | ); 14 | 15 | // Mobile Mixin 16 | @mixin mobile { 17 | @media screen and (max-width: map-get($sizes, mobile-max-width)) { 18 | @content; 19 | } 20 | } 21 | 22 | // Tablet Mixin 23 | @mixin tablet { 24 | @media screen and (min-width: map-get($sizes, tablet-min-width)) and (max-width: map-get($sizes, tablet-max-width)) { 25 | @content; 26 | } 27 | } 28 | 29 | // Desktop Mixin 30 | @mixin desktop { 31 | @media screen and (min-width: map-get($sizes, desktop-min-width)) { 32 | @content; 33 | } 34 | } 35 | 36 | @mixin large { 37 | @media screen and (min-width: map-get($sizes, tablet-min-width)) { 38 | @content; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/Mixins/_Mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin accented-display($bg-color) { 2 | background: $bg-color; 3 | color: rgb(0, 0, 0); 4 | font-family: 'WorkSans'; 5 | padding: 0.5rem; 6 | border-radius: 0.5rem; 7 | } 8 | 9 | @mixin slider-bar { 10 | input[type='range'] { 11 | -webkit-appearance: none; 12 | appearance: none; 13 | width: 100%; 14 | cursor: pointer; 15 | background: transparent; 16 | border-color: transparent; 17 | color: transparent; 18 | } 19 | 20 | input[type='range']::-webkit-slider-thumb { 21 | -webkit-appearance: none; 22 | } 23 | 24 | input[type='range']:focus { 25 | outline: none; 26 | } 27 | 28 | input[type='range']::-webkit-slider-thumb { 29 | -webkit-appearance: none; 30 | height: 1.2rem; 31 | width: 1.2rem; 32 | border-radius: 50%; 33 | border: 1px solid rgb(0, 0, 0); 34 | z-index: 2; 35 | background: var(--yellow-accent); 36 | margin-top: -0.5rem; 37 | } 38 | 39 | input[type='range']::-webkit-slider-runnable-track { 40 | border-radius: 2rem; 41 | height: 0.2rem; 42 | background: var(--btn-primary-bg); 43 | } 44 | 45 | input[type='range']::-moz-range-track { 46 | border-radius: 2rem; 47 | background: var(--btn-primary-bg); 48 | } 49 | 50 | input[type='range']::-moz-range-progress { 51 | background: var(--yellow-accent); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/Mixins/_index.scss: -------------------------------------------------------------------------------- 1 | @forward './Mixins'; 2 | @forward './MediaQueries'; 3 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/_Animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes pulsing { 2 | 0% { 3 | opacity: 1; 4 | } 5 | 50% { 6 | opacity: 0.75; 7 | background-color: var(--animation-pulsing-bg); 8 | } 9 | 100% { 10 | opacity: 1; 11 | } 12 | } 13 | 14 | @keyframes opacityReveal { 15 | 0% { 16 | opacity: 0; 17 | } 18 | 100% { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | @keyframes slideDown { 24 | 0% { 25 | transform: translateY(-100%); 26 | } 27 | 100% { 28 | transform: translateY(0); 29 | } 30 | } 31 | 32 | @keyframes slideUp { 33 | 0% { 34 | transform: translateY(100%); 35 | } 36 | 100% { 37 | transform: translateY(0); 38 | } 39 | } 40 | 41 | @keyframes slideIn { 42 | 0% { 43 | transform: translateX(calc(100% + 25px)); 44 | } 45 | 100% { 46 | transform: translateX(0); 47 | } 48 | } 49 | 50 | @keyframes spin { 51 | 0% { 52 | transform: rotate(0deg); 53 | } 54 | 100% { 55 | transform: rotate(360deg); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/_Colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // General 3 | 4 | // Theme 5 | --page-bg: rgb(255, 255, 255); 6 | --page-bg-light: rgb(255, 255, 255, 0.5); 7 | --page-text-color: #040404; 8 | --yellow-accent: #ffcc00; 9 | --link-color: rgb(0, 0, 0); 10 | --border-color: rgb(100, 100, 120); 11 | --border-color-light: rgba(100, 100, 120, 0.5); 12 | 13 | --error-color: rgb(239, 68, 68); 14 | --success-color: rgb(16, 185, 129); 15 | 16 | // Editor 17 | --editor-toolkit-bg: rgba(255, 255, 255, 0.5); 18 | --options-text-color: var(--page-text-color); 19 | --editor-size-border-color: var(--border-color); 20 | --editor-toolkit-panel-border: 0; 21 | 22 | // Modal 23 | --modal-bg: var(--page-bg); 24 | --modal-text-color: rgb(0, 0, 0); 25 | --modal-hotkey-border-color: rgb(0, 0, 0); 26 | --model-mask-bg: rgba(209, 213, 219, 0.4); 27 | 28 | // Text 29 | --text-color: #040404; 30 | --text-color-gray: rgb(107, 111, 118); 31 | 32 | // Shared 33 | --btn-primary-bg: rgb(210, 210, 220); 34 | --btn-text-color: var(--text-color); 35 | --btn-text-hover-color: #040404; 36 | --btn-border-color: rgb(100, 100, 120); 37 | --btn-primary-hover-bg: var(--yellow-accent); 38 | --animation-pulsing-bg: rgb(255, 255, 255, 0.5); 39 | 40 | // switch 41 | --switch-root-background-color: rgb(223, 225, 228); 42 | --switch-thumb-color: var(--page-bg); 43 | --switch-thumb-checked-color: var(--page-bg); 44 | } 45 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/_ColorsDark.scss: -------------------------------------------------------------------------------- 1 | [data-theme='dark'] { 2 | // General 3 | 4 | // Theme 5 | --page-bg: #040404; 6 | --page-bg-light: #04040488; 7 | --page-text-color: #f9f9f9; 8 | --yellow-accent: #ffcc00; 9 | --link-color: var(--yellow-accent); 10 | --border-color: rgb(100, 100, 120); 11 | --border-color-light: rgba(102, 102, 102); 12 | 13 | // Editor 14 | --editor-toolkit-bg: rgba(0, 0, 0, 0.5); 15 | --options-text-color: var(--page-text-color); 16 | --editor-size-border-color: var(--yellow-accent); 17 | --editor-toolkit-panel-border: 1px solid rgb(100, 100, 120, 0.4); 18 | 19 | // Modal 20 | --modal-bg: var(--page-bg); 21 | --modal-text-color: var(--page-text-color); 22 | // --modal-hotkey-bg: rgb(60, 60, 90); 23 | --modal-hotkey-border-color: var(--page-text-color); 24 | --model-mask-bg: rgba(76, 76, 87, 0.4); 25 | 26 | // Text 27 | --text-color: white; 28 | --text-color-gray: rgb(138, 143, 152); 29 | 30 | // Shared 31 | --btn-primary-bg: rgb(140, 140, 180); 32 | --btn-text-color: var(--text-color); 33 | --btn-text-hover-color: var(--page-bg); 34 | --btn-border-color: var(--yellow-accent); 35 | --btn-primary-hover-bg: var(--yellow-accent); 36 | --animation-pulsing-bg: rgb(240, 240, 255); 37 | 38 | // switch 39 | --switch-root-background-color: rgb(60, 63, 68); 40 | --switch-thumb-color: rgb(31, 32, 35); 41 | --switch-thumb-checked-color: white; 42 | } 43 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/_Fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'WorkSans'; 3 | src: url('../media/fonts/Work_Sans/WorkSans-Regular.ttf'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'WorkSans-Semibold'; 8 | src: url('../media/fonts/Work_Sans/WorkSans-SemiBold.ttf'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'WorkSans-Bold'; 13 | src: url('../media/fonts/Work_Sans/WorkSans-Bold.ttf'); 14 | } 15 | 16 | @font-face { 17 | font-family: 'WorkSans-Black'; 18 | src: url('../media/fonts/Work_Sans/WorkSans-Black.ttf'); 19 | } 20 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/styles/_index.scss: -------------------------------------------------------------------------------- 1 | // General 2 | @use './Fonts'; 3 | @use './Colors'; 4 | @use './ColorsDark'; 5 | @use './Animations'; 6 | 7 | // App 8 | @use './App'; 9 | @use '../components/Editor/Editor'; 10 | @use '../components/LandingPage/LandingPage'; 11 | @use '../components/Header/Header'; 12 | @use '../components/Header/ThemeChanger'; 13 | @use '../components/Shortcuts/Shortcuts'; 14 | @use '../components/Settings/Settings.scss'; 15 | 16 | // Shared 17 | @use '../components/FileSelect/FileSelect'; 18 | @use '../components/shared/Button'; 19 | @use '../components/shared/Modal'; 20 | @use '../components/shared/Selector'; 21 | @use '../components/shared/Switch'; 22 | @use '../components/shared/NumberInput'; 23 | @use '../components/shared/Toast'; 24 | 25 | // Main CSS 26 | *, 27 | *:before, 28 | *:after { 29 | box-sizing: border-box; 30 | margin: 0; 31 | padding: 0; 32 | } 33 | 34 | html, 35 | body { 36 | font-family: 'WorkSans', sans-serif; 37 | } 38 | -------------------------------------------------------------------------------- /lama_cleaner/app/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export function dataURItoBlob(dataURI: string) { 4 | const mime = dataURI.split(',')[0].split(':')[1].split(';')[0] 5 | const binary = atob(dataURI.split(',')[1]) 6 | const array = [] 7 | for (let i = 0; i < binary.length; i += 1) { 8 | array.push(binary.charCodeAt(i)) 9 | } 10 | return new Blob([new Uint8Array(array)], { type: mime }) 11 | } 12 | 13 | // const dataURItoBlob = (dataURI: string) => { 14 | // const bytes = 15 | // dataURI.split(',')[0].indexOf('base64') >= 0 16 | // ? atob(dataURI.split(',')[1]) 17 | // : unescape(dataURI.split(',')[1]) 18 | // const mime = dataURI.split(',')[0].split(':')[1].split(';')[0] 19 | // const max = bytes.length 20 | // const ia = new Uint8Array(max) 21 | // for (var i = 0; i < max; i++) ia[i] = bytes.charCodeAt(i) 22 | // return new Blob([ia], { type: mime }) 23 | // } 24 | 25 | export function downloadImage(uri: string, name: string) { 26 | const link = document.createElement('a') 27 | link.href = uri 28 | link.download = name 29 | 30 | // this is necessary as link.click() does not work on the latest firefox 31 | link.dispatchEvent( 32 | new MouseEvent('click', { 33 | bubbles: true, 34 | cancelable: true, 35 | view: window, 36 | }) 37 | ) 38 | 39 | setTimeout(() => { 40 | // For Firefox it is necessary to delay revoking the ObjectURL 41 | // window.URL.revokeObjectURL(base64) 42 | link.remove() 43 | }, 100) 44 | } 45 | 46 | export function shareImage(base64: string, name: string) { 47 | const blob = dataURItoBlob(base64) 48 | const filesArray = [new File([blob], name, { type: 'image/jpeg' })] 49 | const shareData = { 50 | files: filesArray, 51 | } 52 | // eslint-disable-nextline 53 | const nav: any = navigator 54 | const canShare = nav.canShare && nav.canShare(shareData) 55 | const userAgent = navigator.userAgent || navigator.vendor 56 | const isMobile = /android|iPad|iPhone|iPod/i.test(userAgent) 57 | if (canShare && isMobile) { 58 | navigator.share(shareData) 59 | return true 60 | } 61 | return false 62 | } 63 | 64 | export function loadImage(image: HTMLImageElement, src: string) { 65 | return new Promise((resolve, reject) => { 66 | const initSRC = image.src 67 | const img = image 68 | img.onload = resolve 69 | img.onerror = err => { 70 | img.src = initSRC 71 | reject(err) 72 | } 73 | img.src = src 74 | }) 75 | } 76 | 77 | export function useImage(file: File): [HTMLImageElement, boolean] { 78 | const [image] = useState(new Image()) 79 | const [isLoaded, setIsLoaded] = useState(false) 80 | 81 | useEffect(() => { 82 | image.onload = () => { 83 | setIsLoaded(true) 84 | } 85 | setIsLoaded(false) 86 | image.src = URL.createObjectURL(file) 87 | return () => { 88 | image.onload = null 89 | } 90 | }, [file, image]) 91 | 92 | return [image, isLoaded] 93 | } 94 | 95 | // https://stackoverflow.com/questions/23945494/use-html5-to-resize-an-image-before-upload 96 | interface ResizeImageFileResult { 97 | file: File 98 | resized: boolean 99 | originalWidth?: number 100 | originalHeight?: number 101 | } 102 | export function resizeImageFile( 103 | file: File, 104 | maxSize: number 105 | ): Promise { 106 | const reader = new FileReader() 107 | const image = new Image() 108 | const canvas = document.createElement('canvas') 109 | 110 | const resize = (): ResizeImageFileResult => { 111 | let { width, height } = image 112 | 113 | if (width > height) { 114 | if (width > maxSize) { 115 | height *= maxSize / width 116 | width = maxSize 117 | } 118 | } else if (height > maxSize) { 119 | width *= maxSize / height 120 | height = maxSize 121 | } 122 | 123 | if (width === image.width && height === image.height) { 124 | return { file, resized: false } 125 | } 126 | 127 | canvas.width = width 128 | canvas.height = height 129 | const ctx = canvas.getContext('2d') 130 | if (!ctx) { 131 | throw new Error('could not get context') 132 | } 133 | canvas.getContext('2d')?.drawImage(image, 0, 0, width, height) 134 | const dataUrl = canvas.toDataURL('image/jpeg') 135 | const blob = dataURItoBlob(dataUrl) 136 | const f = new File([blob], file.name, { 137 | type: file.type, 138 | }) 139 | return { 140 | file: f, 141 | resized: true, 142 | originalWidth: image.width, 143 | originalHeight: image.height, 144 | } 145 | } 146 | 147 | return new Promise((resolve, reject) => { 148 | if (!file.type.match(/image.*/)) { 149 | reject(new Error('Not an image')) 150 | return 151 | } 152 | reader.onload = (readerEvent: any) => { 153 | image.onload = () => resolve(resize()) 154 | image.src = readerEvent.target.result 155 | } 156 | reader.readAsDataURL(file) 157 | }) 158 | } 159 | 160 | export function keepGUIAlive() { 161 | async function getRequest(url = '') { 162 | const response = await fetch(url, { 163 | method: 'GET', 164 | cache: 'no-cache', 165 | }) 166 | return response.json() 167 | } 168 | 169 | const keepAliveServer = () => { 170 | const url = document.location 171 | const route = '/flaskwebgui-keep-server-alive' 172 | getRequest(url + route).then(data => { 173 | return data 174 | }) 175 | } 176 | 177 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'production') { 178 | document.addEventListener('DOMContentLoaded', () => { 179 | const intervalRequest = 3 * 1000 180 | keepAliveServer() 181 | setInterval(keepAliveServer, intervalRequest) 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /lama_cleaner/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /lama_cleaner/benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import multiprocessing 5 | import os 6 | import time 7 | 8 | import numpy as np 9 | import nvidia_smi 10 | import psutil 11 | import torch 12 | from tqdm import tqdm 13 | 14 | from lama_cleaner.lama import LaMa 15 | 16 | try: 17 | torch._C._jit_override_can_fuse_on_cpu(False) 18 | torch._C._jit_override_can_fuse_on_gpu(False) 19 | torch._C._jit_set_texpr_fuser_enabled(False) 20 | torch._C._jit_set_nvfuser_enabled(False) 21 | except: 22 | pass 23 | 24 | from lama_cleaner.helper import norm_img 25 | 26 | NUM_THREADS = str(4) 27 | 28 | os.environ["OMP_NUM_THREADS"] = NUM_THREADS 29 | os.environ["OPENBLAS_NUM_THREADS"] = NUM_THREADS 30 | os.environ["MKL_NUM_THREADS"] = NUM_THREADS 31 | os.environ["VECLIB_MAXIMUM_THREADS"] = NUM_THREADS 32 | os.environ["NUMEXPR_NUM_THREADS"] = NUM_THREADS 33 | if os.environ.get("CACHE_DIR"): 34 | os.environ["TORCH_HOME"] = os.environ["CACHE_DIR"] 35 | 36 | 37 | def run_model(model, size): 38 | # RGB 39 | image = np.random.randint(0, 256, (size[0], size[1], 3)).astype(np.uint8) 40 | image = norm_img(image) 41 | 42 | mask = np.random.randint(0, 255, size).astype(np.uint8) 43 | mask = norm_img(mask) 44 | model(image, mask) 45 | 46 | 47 | def benchmark(model, times: int, empty_cache: bool): 48 | sizes = [ 49 | (512, 512), 50 | (640, 640), 51 | (1080, 800), 52 | (2000, 2000) 53 | ] 54 | 55 | nvidia_smi.nvmlInit() 56 | device_id = 0 57 | handle = nvidia_smi.nvmlDeviceGetHandleByIndex(device_id) 58 | 59 | def format(metrics): 60 | return f"{np.mean(metrics):.2f} ± {np.std(metrics):.2f}" 61 | 62 | process = psutil.Process(os.getpid()) 63 | # 每个 size 给出显存和内存占用的指标 64 | for size in sizes: 65 | torch.cuda.empty_cache() 66 | time_metrics = [] 67 | cpu_metrics = [] 68 | memory_metrics = [] 69 | gpu_memory_metrics = [] 70 | for _ in range(times): 71 | start = time.time() 72 | run_model(model, size) 73 | torch.cuda.synchronize() 74 | if empty_cache: 75 | torch.cuda.empty_cache() 76 | 77 | # cpu_metrics.append(process.cpu_percent()) 78 | time_metrics.append((time.time() - start) * 1000) 79 | memory_metrics.append(process.memory_info().rss / 1024 / 1024) 80 | gpu_memory_metrics.append(nvidia_smi.nvmlDeviceGetMemoryInfo(handle).used / 1024 / 1024) 81 | 82 | print(f"size: {size}".center(80, "-")) 83 | # print(f"cpu: {format(cpu_metrics)}") 84 | print(f"latency: {format(time_metrics)}ms") 85 | print(f"memory: {format(memory_metrics)} MB") 86 | print(f"gpu memory: {format(gpu_memory_metrics)} MB") 87 | 88 | nvidia_smi.nvmlShutdown() 89 | 90 | 91 | def get_args_parser(): 92 | parser = argparse.ArgumentParser() 93 | parser.add_argument("--device", default="cuda", type=str) 94 | parser.add_argument("--times", default=20, type=int) 95 | parser.add_argument("--empty-cache", action="store_true") 96 | return parser.parse_args() 97 | 98 | 99 | if __name__ == "__main__": 100 | args = get_args_parser() 101 | device = torch.device(args.device) 102 | model = LaMa(device) 103 | benchmark(model, args.times, args.empty_cache) 104 | -------------------------------------------------------------------------------- /lama_cleaner/helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import List 4 | 5 | from urllib.parse import urlparse 6 | import cv2 7 | import numpy as np 8 | import torch 9 | from torch.hub import download_url_to_file, get_dir 10 | 11 | 12 | def get_cache_path_by_url(url): 13 | parts = urlparse(url) 14 | hub_dir = get_dir() 15 | model_dir = os.path.join(hub_dir, "checkpoints") 16 | if not os.path.isdir(model_dir): 17 | os.makedirs(os.path.join(model_dir, "hub", "checkpoints")) 18 | filename = os.path.basename(parts.path) 19 | cached_file = os.path.join(model_dir, filename) 20 | return cached_file 21 | 22 | 23 | def download_model(url): 24 | cached_file = get_cache_path_by_url(url) 25 | if not os.path.exists(cached_file): 26 | sys.stderr.write('Downloading: "{}" to {}\n'.format(url, cached_file)) 27 | hash_prefix = None 28 | download_url_to_file(url, cached_file, hash_prefix, progress=True) 29 | return cached_file 30 | 31 | 32 | def ceil_modulo(x, mod): 33 | if x % mod == 0: 34 | return x 35 | return (x // mod + 1) * mod 36 | 37 | 38 | def numpy_to_bytes(image_numpy: np.ndarray, ext: str) -> bytes: 39 | data = cv2.imencode(f".{ext}", image_numpy, 40 | [ 41 | int(cv2.IMWRITE_JPEG_QUALITY), 100, 42 | int(cv2.IMWRITE_PNG_COMPRESSION), 0 43 | ])[1] 44 | image_bytes = data.tobytes() 45 | return image_bytes 46 | 47 | 48 | def load_img(img_bytes, gray: bool = False): 49 | alpha_channel = None 50 | nparr = np.frombuffer(img_bytes, np.uint8) 51 | if gray: 52 | np_img = cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE) 53 | else: 54 | np_img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) 55 | if len(np_img.shape) == 3 and np_img.shape[2] == 4: 56 | alpha_channel = np_img[:, :, -1] 57 | np_img = cv2.cvtColor(np_img, cv2.COLOR_BGRA2RGB) 58 | else: 59 | np_img = cv2.cvtColor(np_img, cv2.COLOR_BGR2RGB) 60 | 61 | return np_img, alpha_channel 62 | 63 | 64 | def norm_img(np_img): 65 | if len(np_img.shape) == 2: 66 | np_img = np_img[:, :, np.newaxis] 67 | np_img = np.transpose(np_img, (2, 0, 1)) 68 | np_img = np_img.astype("float32") / 255 69 | return np_img 70 | 71 | 72 | def resize_max_size( 73 | np_img, size_limit: int, interpolation=cv2.INTER_CUBIC 74 | ) -> np.ndarray: 75 | # Resize image's longer size to size_limit if longer size larger than size_limit 76 | h, w = np_img.shape[:2] 77 | if max(h, w) > size_limit: 78 | ratio = size_limit / max(h, w) 79 | new_w = int(w * ratio + 0.5) 80 | new_h = int(h * ratio + 0.5) 81 | return cv2.resize(np_img, dsize=(new_w, new_h), interpolation=interpolation) 82 | else: 83 | return np_img 84 | 85 | 86 | def pad_img_to_modulo(img: np.ndarray, mod: int): 87 | """ 88 | 89 | Args: 90 | img: [H, W, C] 91 | mod: 92 | 93 | Returns: 94 | 95 | """ 96 | if len(img.shape) == 2: 97 | img = img[:, :, np.newaxis] 98 | height, width = img.shape[:2] 99 | out_height = ceil_modulo(height, mod) 100 | out_width = ceil_modulo(width, mod) 101 | return np.pad( 102 | img, 103 | ((0, out_height - height), (0, out_width - width), (0, 0)), 104 | mode="symmetric", 105 | ) 106 | 107 | 108 | def boxes_from_mask(mask: np.ndarray) -> List[np.ndarray]: 109 | """ 110 | Args: 111 | mask: (h, w, 1) 0~255 112 | 113 | Returns: 114 | 115 | """ 116 | height, width = mask.shape[:2] 117 | _, thresh = cv2.threshold(mask, 127, 255, 0) 118 | contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 119 | 120 | boxes = [] 121 | for cnt in contours: 122 | x, y, w, h = cv2.boundingRect(cnt) 123 | box = np.array([x, y, x + w, y + h]).astype(np.int) 124 | 125 | box[::2] = np.clip(box[::2], 0, width) 126 | box[1::2] = np.clip(box[1::2], 0, height) 127 | boxes.append(box) 128 | 129 | return boxes 130 | -------------------------------------------------------------------------------- /lama_cleaner/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/model/__init__.py -------------------------------------------------------------------------------- /lama_cleaner/model/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/model/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /lama_cleaner/model/__pycache__/base.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/model/__pycache__/base.cpython-38.pyc -------------------------------------------------------------------------------- /lama_cleaner/model/__pycache__/lama.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/model/__pycache__/lama.cpython-38.pyc -------------------------------------------------------------------------------- /lama_cleaner/model/__pycache__/ldm.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/model/__pycache__/ldm.cpython-38.pyc -------------------------------------------------------------------------------- /lama_cleaner/model/__pycache__/utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/model/__pycache__/utils.cpython-38.pyc -------------------------------------------------------------------------------- /lama_cleaner/model/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | import cv2 4 | import torch 5 | from loguru import logger 6 | 7 | from lama_cleaner.helper import boxes_from_mask, resize_max_size, pad_img_to_modulo 8 | from lama_cleaner.schema import Config, HDStrategy 9 | 10 | 11 | class InpaintModel: 12 | pad_mod = 8 13 | 14 | def __init__(self, device): 15 | """ 16 | 17 | Args: 18 | device: 19 | """ 20 | self.device = device 21 | self.init_model(device) 22 | 23 | @abc.abstractmethod 24 | def init_model(self, device): 25 | ... 26 | 27 | @staticmethod 28 | @abc.abstractmethod 29 | def is_downloaded() -> bool: 30 | ... 31 | 32 | @abc.abstractmethod 33 | def forward(self, image, mask, config: Config): 34 | """Input image and output image have same size 35 | image: [H, W, C] RGB 36 | mask: [H, W] 37 | return: BGR IMAGE 38 | """ 39 | ... 40 | 41 | def _pad_forward(self, image, mask, config: Config): 42 | origin_height, origin_width = image.shape[:2] 43 | padd_image = pad_img_to_modulo(image, mod=self.pad_mod) 44 | padd_mask = pad_img_to_modulo(mask, mod=self.pad_mod) 45 | result = self.forward(padd_image, padd_mask, config) 46 | result = result[0:origin_height, 0:origin_width, :] 47 | 48 | original_pixel_indices = mask != 255 49 | result[original_pixel_indices] = image[:, :, ::-1][original_pixel_indices] 50 | return result 51 | 52 | @torch.no_grad() 53 | def __call__(self, image, mask, config: Config): 54 | """ 55 | image: [H, W, C] RGB, not normalized 56 | mask: [H, W] 57 | return: BGR IMAGE 58 | """ 59 | inpaint_result = None 60 | logger.info(f"hd_strategy: {config.hd_strategy}") 61 | if config.hd_strategy == HDStrategy.CROP: 62 | if max(image.shape) > config.hd_strategy_crop_trigger_size: 63 | logger.info(f"Run crop strategy") 64 | boxes = boxes_from_mask(mask) 65 | crop_result = [] 66 | for box in boxes: 67 | crop_image, crop_box = self._run_box(image, mask, box, config) 68 | crop_result.append((crop_image, crop_box)) 69 | 70 | inpaint_result = image[:, :, ::-1] 71 | for crop_image, crop_box in crop_result: 72 | x1, y1, x2, y2 = crop_box 73 | inpaint_result[y1:y2, x1:x2, :] = crop_image 74 | 75 | elif config.hd_strategy == HDStrategy.RESIZE: 76 | if max(image.shape) > config.hd_strategy_resize_limit: 77 | origin_size = image.shape[:2] 78 | downsize_image = resize_max_size(image, size_limit=config.hd_strategy_resize_limit) 79 | downsize_mask = resize_max_size(mask, size_limit=config.hd_strategy_resize_limit) 80 | 81 | logger.info(f"Run resize strategy, origin size: {image.shape} forward size: {downsize_image.shape}") 82 | inpaint_result = self._pad_forward(downsize_image, downsize_mask, config) 83 | 84 | # only paste masked area result 85 | inpaint_result = cv2.resize(inpaint_result, 86 | (origin_size[1], origin_size[0]), 87 | interpolation=cv2.INTER_CUBIC) 88 | 89 | original_pixel_indices = mask != 255 90 | inpaint_result[original_pixel_indices] = image[:, :, ::-1][original_pixel_indices] 91 | 92 | if inpaint_result is None: 93 | inpaint_result = self._pad_forward(image, mask, config) 94 | 95 | return inpaint_result 96 | 97 | def _run_box(self, image, mask, box, config: Config): 98 | """ 99 | 100 | Args: 101 | image: [H, W, C] RGB 102 | mask: [H, W, 1] 103 | box: [left,top,right,bottom] 104 | 105 | Returns: 106 | BGR IMAGE 107 | """ 108 | box_h = box[3] - box[1] 109 | box_w = box[2] - box[0] 110 | cx = (box[0] + box[2]) // 2 111 | cy = (box[1] + box[3]) // 2 112 | img_h, img_w = image.shape[:2] 113 | 114 | w = box_w + config.hd_strategy_crop_margin * 2 115 | h = box_h + config.hd_strategy_crop_margin * 2 116 | 117 | l = max(cx - w // 2, 0) 118 | t = max(cy - h // 2, 0) 119 | r = min(cx + w // 2, img_w) 120 | b = min(cy + h // 2, img_h) 121 | 122 | crop_img = image[t:b, l:r, :] 123 | crop_mask = mask[t:b, l:r] 124 | 125 | logger.info(f"box size: ({box_h},{box_w}) crop size: {crop_img.shape}") 126 | 127 | return self._pad_forward(crop_img, crop_mask, config), [l, t, r, b] 128 | -------------------------------------------------------------------------------- /lama_cleaner/model/lama.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cv2 4 | import numpy as np 5 | import torch 6 | from loguru import logger 7 | 8 | from lama_cleaner.helper import pad_img_to_modulo, download_model, norm_img, get_cache_path_by_url 9 | from lama_cleaner.model.base import InpaintModel 10 | from lama_cleaner.schema import Config 11 | 12 | LAMA_MODEL_URL = os.environ.get( 13 | "LAMA_MODEL_URL", 14 | "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt", 15 | ) 16 | 17 | 18 | class LaMa(InpaintModel): 19 | pad_mod = 8 20 | 21 | def __init__(self, device): 22 | """ 23 | 24 | Args: 25 | device: 26 | """ 27 | super().__init__(device) 28 | self.device = device 29 | 30 | def init_model(self, device): 31 | if os.environ.get("LAMA_MODEL"): 32 | model_path = os.environ.get("LAMA_MODEL") 33 | if not os.path.exists(model_path): 34 | raise FileNotFoundError( 35 | f"lama torchscript model not found: {model_path}" 36 | ) 37 | else: 38 | model_path = download_model(LAMA_MODEL_URL) 39 | logger.info(f"Load LaMa model from: {model_path}") 40 | model = torch.jit.load(model_path, map_location="cpu") 41 | model = model.to(device) 42 | model.eval() 43 | self.model = model 44 | self.model_path = model_path 45 | 46 | @staticmethod 47 | def is_downloaded() -> bool: 48 | return os.path.exists(get_cache_path_by_url(LAMA_MODEL_URL)) 49 | 50 | def forward(self, image, mask, config: Config): 51 | """Input image and output image have same size 52 | image: [H, W, C] RGB 53 | mask: [H, W] 54 | return: BGR IMAGE 55 | """ 56 | image = norm_img(image) 57 | mask = norm_img(mask) 58 | 59 | mask = (mask > 0) * 1 60 | image = torch.from_numpy(image).unsqueeze(0).to(self.device) 61 | mask = torch.from_numpy(mask).unsqueeze(0).to(self.device) 62 | 63 | inpainted_image = self.model(image, mask) 64 | 65 | cur_res = inpainted_image[0].permute(1, 2, 0).detach().cpu().numpy() 66 | cur_res = np.clip(cur_res * 255, 0, 255).astype("uint8") 67 | cur_res = cv2.cvtColor(cur_res, cv2.COLOR_RGB2BGR) 68 | return cur_res 69 | -------------------------------------------------------------------------------- /lama_cleaner/model/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import torch 4 | import numpy as np 5 | 6 | 7 | def make_beta_schedule(device, schedule, n_timestep, linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3): 8 | if schedule == "linear": 9 | betas = ( 10 | torch.linspace(linear_start ** 0.5, linear_end ** 0.5, n_timestep, dtype=torch.float64) ** 2 11 | ) 12 | 13 | elif schedule == "cosine": 14 | timesteps = (torch.arange(n_timestep + 1, dtype=torch.float64) / n_timestep + cosine_s).to(device) 15 | alphas = timesteps / (1 + cosine_s) * np.pi / 2 16 | alphas = torch.cos(alphas).pow(2).to(device) 17 | alphas = alphas / alphas[0] 18 | betas = 1 - alphas[1:] / alphas[:-1] 19 | betas = np.clip(betas, a_min=0, a_max=0.999) 20 | 21 | elif schedule == "sqrt_linear": 22 | betas = torch.linspace(linear_start, linear_end, n_timestep, dtype=torch.float64) 23 | elif schedule == "sqrt": 24 | betas = torch.linspace(linear_start, linear_end, n_timestep, dtype=torch.float64) ** 0.5 25 | else: 26 | raise ValueError(f"schedule '{schedule}' unknown.") 27 | return betas.numpy() 28 | 29 | 30 | def make_ddim_sampling_parameters(alphacums, ddim_timesteps, eta, verbose=True): 31 | # select alphas for computing the variance schedule 32 | alphas = alphacums[ddim_timesteps] 33 | alphas_prev = np.asarray([alphacums[0]] + alphacums[ddim_timesteps[:-1]].tolist()) 34 | 35 | # according the the formula provided in https://arxiv.org/abs/2010.02502 36 | sigmas = eta * np.sqrt((1 - alphas_prev) / (1 - alphas) * (1 - alphas / alphas_prev)) 37 | if verbose: 38 | print(f'Selected alphas for ddim sampler: a_t: {alphas}; a_(t-1): {alphas_prev}') 39 | print(f'For the chosen value of eta, which is {eta}, ' 40 | f'this results in the following sigma_t schedule for ddim sampler {sigmas}') 41 | return sigmas, alphas, alphas_prev 42 | 43 | 44 | def make_ddim_timesteps(ddim_discr_method, num_ddim_timesteps, num_ddpm_timesteps, verbose=True): 45 | if ddim_discr_method == 'uniform': 46 | c = num_ddpm_timesteps // num_ddim_timesteps 47 | ddim_timesteps = np.asarray(list(range(0, num_ddpm_timesteps, c))) 48 | elif ddim_discr_method == 'quad': 49 | ddim_timesteps = ((np.linspace(0, np.sqrt(num_ddpm_timesteps * .8), num_ddim_timesteps)) ** 2).astype(int) 50 | else: 51 | raise NotImplementedError(f'There is no ddim discretization method called "{ddim_discr_method}"') 52 | 53 | # assert ddim_timesteps.shape[0] == num_ddim_timesteps 54 | # add one to get the final alpha values right (the ones from first scale to data during sampling) 55 | steps_out = ddim_timesteps + 1 56 | if verbose: 57 | print(f'Selected timesteps for ddim sampler: {steps_out}') 58 | return steps_out 59 | 60 | 61 | def noise_like(shape, device, repeat=False): 62 | repeat_noise = lambda: torch.randn((1, *shape[1:]), device=device).repeat(shape[0], *((1,) * (len(shape) - 1))) 63 | noise = lambda: torch.randn(shape, device=device) 64 | return repeat_noise() if repeat else noise() 65 | 66 | 67 | def timestep_embedding(device, timesteps, dim, max_period=10000, repeat_only=False): 68 | """ 69 | Create sinusoidal timestep embeddings. 70 | :param timesteps: a 1-D Tensor of N indices, one per batch element. 71 | These may be fractional. 72 | :param dim: the dimension of the output. 73 | :param max_period: controls the minimum frequency of the embeddings. 74 | :return: an [N x dim] Tensor of positional embeddings. 75 | """ 76 | half = dim // 2 77 | freqs = torch.exp( 78 | -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half 79 | ).to(device=device) 80 | 81 | args = timesteps[:, None].float() * freqs[None] 82 | 83 | embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) 84 | if dim % 2: 85 | embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) 86 | return embedding 87 | -------------------------------------------------------------------------------- /lama_cleaner/model_manager.py: -------------------------------------------------------------------------------- 1 | from lama_cleaner.model.lama import LaMa 2 | from lama_cleaner.model.ldm import LDM 3 | from lama_cleaner.schema import Config 4 | 5 | 6 | class ModelManager: 7 | LAMA = 'lama' 8 | LDM = 'ldm' 9 | 10 | def __init__(self, name: str, device): 11 | self.name = name 12 | self.device = device 13 | self.model = self.init_model(name, device) 14 | 15 | def init_model(self, name: str, device): 16 | if name == self.LAMA: 17 | model = LaMa(device) 18 | elif name == self.LDM: 19 | model = LDM(device) 20 | else: 21 | raise NotImplementedError(f"Not supported model: {name}") 22 | return model 23 | 24 | def is_downloaded(self, name: str) -> bool: 25 | if name == self.LAMA: 26 | return LaMa.is_downloaded() 27 | elif name == self.LDM: 28 | return LDM.is_downloaded() 29 | else: 30 | raise NotImplementedError(f"Not supported model: {name}") 31 | 32 | def __call__(self, image, mask, config: Config): 33 | return self.model(image, mask, config) 34 | 35 | def switch(self, new_name: str): 36 | if new_name == self.name: 37 | return 38 | try: 39 | self.model = self.init_model(new_name, self.device) 40 | self.name = new_name 41 | except NotImplementedError as e: 42 | raise e 43 | -------------------------------------------------------------------------------- /lama_cleaner/parse_args.py: -------------------------------------------------------------------------------- 1 | import os 2 | import imghdr 3 | import argparse 4 | 5 | 6 | def parse_args(): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("--host", default="127.0.0.1") 9 | parser.add_argument("--port", default=8080, type=int) 10 | parser.add_argument("--model", default="lama", choices=["lama", "ldm"]) 11 | parser.add_argument("--device", default="cuda", type=str, choices=["cuda", "cpu"]) 12 | parser.add_argument("--gui", action="store_true", help="Launch as desktop app") 13 | parser.add_argument( 14 | "--gui-size", 15 | default=[1600, 1000], 16 | nargs=2, 17 | type=int, 18 | help="Set window size for GUI", 19 | ) 20 | parser.add_argument( 21 | "--input", type=str, help="Path to image you want to load by default" 22 | ) 23 | parser.add_argument("--debug", action="store_true") 24 | 25 | args = parser.parse_args() 26 | if args.input is not None: 27 | if not os.path.exists(args.input): 28 | parser.error(f"invalid --input: {args.input} not exists") 29 | if imghdr.what(args.input) is None: 30 | parser.error(f"invalid --input: {args.input} is not a valid image file") 31 | 32 | return args 33 | -------------------------------------------------------------------------------- /lama_cleaner/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | pushd ./lama_cleaner/app 5 | yarn run build 6 | popd 7 | 8 | rm -r -f dist 9 | python3 setup.py sdist bdist_wheel 10 | twine upload dist/* 11 | -------------------------------------------------------------------------------- /lama_cleaner/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | twine -------------------------------------------------------------------------------- /lama_cleaner/requirements.txt: -------------------------------------------------------------------------------- 1 | torch 2 | opencv-python 3 | flask_cors 4 | flask==1.1.4 5 | flaskwebgui 6 | tqdm 7 | pydantic 8 | loguru 9 | pytest 10 | markupsafe==2.0.1 -------------------------------------------------------------------------------- /lama_cleaner/schema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class HDStrategy(str, Enum): 7 | ORIGINAL = 'Original' 8 | RESIZE = 'Resize' 9 | CROP = 'Crop' 10 | 11 | 12 | class Config(BaseModel): 13 | ldm_steps: int 14 | hd_strategy: str 15 | hd_strategy_crop_margin: int 16 | hd_strategy_crop_trigger_size: int 17 | hd_strategy_resize_limit: int 18 | -------------------------------------------------------------------------------- /lama_cleaner/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import io 4 | import logging 5 | import multiprocessing 6 | import os 7 | import time 8 | import imghdr 9 | from pathlib import Path 10 | from typing import Union 11 | 12 | import cv2 13 | import torch 14 | import numpy as np 15 | from loguru import logger 16 | 17 | from lama_cleaner.model_manager import ModelManager 18 | from lama_cleaner.schema import Config 19 | 20 | try: 21 | torch._C._jit_override_can_fuse_on_cpu(False) 22 | torch._C._jit_override_can_fuse_on_gpu(False) 23 | torch._C._jit_set_texpr_fuser_enabled(False) 24 | torch._C._jit_set_nvfuser_enabled(False) 25 | except: 26 | pass 27 | 28 | from flask import Flask, request, send_file, cli 29 | 30 | # Disable ability for Flask to display warning about using a development server in a production environment. 31 | # https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356 32 | cli.show_server_banner = lambda *_: None 33 | from flask_cors import CORS 34 | 35 | from lama_cleaner.helper import ( 36 | load_img, 37 | numpy_to_bytes, 38 | resize_max_size, 39 | ) 40 | 41 | NUM_THREADS = str(multiprocessing.cpu_count()) 42 | 43 | os.environ["OMP_NUM_THREADS"] = NUM_THREADS 44 | os.environ["OPENBLAS_NUM_THREADS"] = NUM_THREADS 45 | os.environ["MKL_NUM_THREADS"] = NUM_THREADS 46 | os.environ["VECLIB_MAXIMUM_THREADS"] = NUM_THREADS 47 | os.environ["NUMEXPR_NUM_THREADS"] = NUM_THREADS 48 | if os.environ.get("CACHE_DIR"): 49 | os.environ["TORCH_HOME"] = os.environ["CACHE_DIR"] 50 | 51 | BUILD_DIR = os.environ.get("LAMA_CLEANER_BUILD_DIR", "app/build") 52 | 53 | 54 | class NoFlaskwebgui(logging.Filter): 55 | def filter(self, record): 56 | return "GET //flaskwebgui-keep-server-alive" not in record.getMessage() 57 | 58 | 59 | logging.getLogger("werkzeug").addFilter(NoFlaskwebgui()) 60 | 61 | app = Flask(__name__, static_folder=os.path.join(BUILD_DIR, "static")) 62 | app.config["JSON_AS_ASCII"] = False 63 | CORS(app, expose_headers=["Content-Disposition"]) 64 | 65 | model: ModelManager = None 66 | device = None 67 | input_image_path: str = None 68 | 69 | 70 | def get_image_ext(img_bytes): 71 | w = imghdr.what("", img_bytes) 72 | if w is None: 73 | w = "jpeg" 74 | return w 75 | 76 | 77 | @app.route("/inpaint", methods=["POST"]) 78 | def process(): 79 | input = request.files 80 | # RGB 81 | origin_image_bytes = input["image"].read() 82 | 83 | image, alpha_channel = load_img(origin_image_bytes) 84 | original_shape = image.shape 85 | interpolation = cv2.INTER_CUBIC 86 | 87 | form = request.form 88 | size_limit: Union[int, str] = form.get("sizeLimit", "1080") 89 | if size_limit == "Original": 90 | size_limit = max(image.shape) 91 | else: 92 | size_limit = int(size_limit) 93 | 94 | print("============================") 95 | print(dict(form)) 96 | print('Size Limit Value -> ', size_limit) 97 | print("============================") 98 | 99 | 100 | config = Config( 101 | ldm_steps=form["ldmSteps"], 102 | hd_strategy=form["hdStrategy"], 103 | hd_strategy_crop_margin=form["hdStrategyCropMargin"], 104 | hd_strategy_crop_trigger_size=form["hdStrategyCropTrigerSize"], 105 | hd_strategy_resize_limit=form["hdStrategyResizeLimit"], 106 | ) 107 | 108 | logger.info(f"Origin image shape: {original_shape}") 109 | image = resize_max_size(image, size_limit=size_limit, interpolation=interpolation) 110 | logger.info(f"Resized image shape: {image.shape}") 111 | 112 | mask, _ = load_img(input["mask"].read(), gray=True) 113 | mask = resize_max_size(mask, size_limit=size_limit, interpolation=interpolation) 114 | 115 | start = time.time() 116 | res_np_img = model(image, mask, config) 117 | logger.info(f"process time: {(time.time() - start) * 1000}ms") 118 | 119 | torch.cuda.empty_cache() 120 | 121 | if alpha_channel is not None: 122 | if alpha_channel.shape[:2] != res_np_img.shape[:2]: 123 | alpha_channel = cv2.resize( 124 | alpha_channel, dsize=(res_np_img.shape[1], res_np_img.shape[0]) 125 | ) 126 | res_np_img = np.concatenate( 127 | (res_np_img, alpha_channel[:, :, np.newaxis]), axis=-1 128 | ) 129 | 130 | ext = get_image_ext(origin_image_bytes) 131 | return send_file( 132 | io.BytesIO(numpy_to_bytes(res_np_img, ext)), 133 | mimetype=f"image/{ext}", 134 | ) 135 | 136 | 137 | @app.route("/model") 138 | def current_model(): 139 | return model.name, 200 140 | 141 | 142 | @app.route("/model_downloaded/") 143 | def model_downloaded(name): 144 | return str(model.is_downloaded(name)), 200 145 | 146 | 147 | @app.route("/model", methods=["POST"]) 148 | def switch_model(): 149 | new_name = request.form.get("name") 150 | if new_name == model.name: 151 | return "Same model", 200 152 | 153 | try: 154 | model.switch(new_name) 155 | except NotImplementedError: 156 | return f"{new_name} not implemented", 403 157 | return f"ok, switch to {new_name}", 200 158 | 159 | 160 | @app.route("/") 161 | def index(): 162 | return send_file(os.path.join(BUILD_DIR, "index.html")) 163 | 164 | 165 | @app.route("/inputimage") 166 | def set_input_photo(): 167 | if input_image_path: 168 | with open(input_image_path, "rb") as f: 169 | image_in_bytes = f.read() 170 | return send_file( 171 | input_image_path, 172 | as_attachment=True, 173 | attachment_filename=Path(input_image_path).name, 174 | mimetype=f"image/{get_image_ext(image_in_bytes)}", 175 | ) 176 | else: 177 | return "No Input Image" 178 | 179 | 180 | def main(args): 181 | global model 182 | global device 183 | global input_image_path 184 | 185 | device = torch.device(args.device) 186 | input_image_path = args.input 187 | 188 | model = ModelManager(name=args.model, device=device) 189 | 190 | if args.gui: 191 | app_width, app_height = args.gui_size 192 | from flaskwebgui import FlaskUI 193 | 194 | ui = FlaskUI( 195 | app, width=app_width, height=app_height, host=args.host, port=args.port 196 | ) 197 | ui.run() 198 | else: 199 | app.run(host=args.host, port=args.port, debug=args.debug) 200 | -------------------------------------------------------------------------------- /lama_cleaner/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from pathlib import Path 3 | 4 | web_files = Path("lama_cleaner/app/build/").glob("**/*") 5 | web_files = [str(it).replace("lama_cleaner/", "") for it in web_files] 6 | 7 | with open("README.md", "r", encoding="utf-8") as fh: 8 | long_description = fh.read() 9 | 10 | 11 | def load_requirements(): 12 | requirements_file_name = "requirements.txt" 13 | requires = [] 14 | with open(requirements_file_name) as f: 15 | for line in f: 16 | if line: 17 | requires.append(line.strip()) 18 | return requires 19 | 20 | 21 | # https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files 22 | setuptools.setup( 23 | name="lama-cleaner", 24 | version="0.10.0", 25 | author="PanicByte", 26 | author_email="cwq1913@gmail.com", 27 | description="Image inpainting tool powered by SOTA AI Model", 28 | long_description=long_description, 29 | long_description_content_type="text/markdown", 30 | url="https://github.com/Sanster/lama-cleaner", 31 | packages=setuptools.find_packages("./"), 32 | package_data={"lama_cleaner": web_files}, 33 | install_requires=load_requirements(), 34 | python_requires=">=3.6", 35 | entry_points={"console_scripts": ["lama-cleaner=lama_cleaner:entry_point"]}, 36 | classifiers=[ 37 | "Programming Language :: Python :: 3", 38 | "License :: OSI Approved :: MIT License", 39 | "Operating System :: OS Independent", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /lama_cleaner/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/__init__.py -------------------------------------------------------------------------------- /lama_cleaner/tests/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/image.png -------------------------------------------------------------------------------- /lama_cleaner/tests/lama_crop_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/lama_crop_result.png -------------------------------------------------------------------------------- /lama_cleaner/tests/lama_original_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/lama_original_result.png -------------------------------------------------------------------------------- /lama_cleaner/tests/lama_resize_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/lama_resize_result.png -------------------------------------------------------------------------------- /lama_cleaner/tests/ldm_crop_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/ldm_crop_result.png -------------------------------------------------------------------------------- /lama_cleaner/tests/ldm_original_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/ldm_original_result.png -------------------------------------------------------------------------------- /lama_cleaner/tests/ldm_resize_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/ldm_resize_result.png -------------------------------------------------------------------------------- /lama_cleaner/tests/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdrkdrkdr/GoodbyeLaver/4666db8d4de886c27ec6d3d9e679684b00037da0/lama_cleaner/tests/mask.png -------------------------------------------------------------------------------- /lama_cleaner/tests/test_model.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import cv2 5 | import numpy as np 6 | import pytest 7 | 8 | from lama_cleaner.model_manager import ModelManager 9 | from lama_cleaner.schema import Config, HDStrategy 10 | 11 | current_dir = Path(__file__).parent.absolute().resolve() 12 | 13 | 14 | def get_data(): 15 | img = cv2.imread(str(current_dir / 'image.png')) 16 | img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB) 17 | mask = cv2.imread(str(current_dir / 'mask.png'), cv2.IMREAD_GRAYSCALE) 18 | return img, mask 19 | 20 | 21 | def get_config(strategy): 22 | return Config( 23 | ldm_steps=1, 24 | hd_strategy=strategy, 25 | hd_strategy_crop_margin=32, 26 | hd_strategy_crop_trigger_size=200, 27 | hd_strategy_resize_limit=200, 28 | ) 29 | 30 | 31 | def assert_equal(model, config, gt_name): 32 | img, mask = get_data() 33 | res = model(img, mask, config) 34 | # cv2.imwrite(gt_name, res, 35 | # [int(cv2.IMWRITE_JPEG_QUALITY), 100, int(cv2.IMWRITE_PNG_COMPRESSION), 0]) 36 | 37 | """ 38 | Note that JPEG is lossy compression, so even if it is the highest quality 100, 39 | when the saved image is reloaded, a difference occurs with the original pixel value. 40 | If you want to save the original image as it is, save it as PNG or BMP. 41 | """ 42 | gt = cv2.imread(str(current_dir / gt_name), cv2.IMREAD_UNCHANGED) 43 | assert np.array_equal(res, gt) 44 | 45 | 46 | @pytest.mark.parametrize('strategy', [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]) 47 | def test_lama(strategy): 48 | model = ModelManager(name='lama', device='cpu') 49 | assert_equal(model, get_config(strategy), f'lama_{strategy[0].upper() + strategy[1:]}_result.png') 50 | 51 | 52 | @pytest.mark.parametrize('strategy', [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]) 53 | def test_ldm(strategy): 54 | model = ModelManager(name='ldm', device='cpu') 55 | assert_equal(model, get_config(strategy), f'ldm_{strategy[0].upper() + strategy[1:]}_result.png') 56 | --------------------------------------------------------------------------------