├── .gitignore ├── README.md ├── __init__.py ├── config ├── SHARP2020 │ └── track1.yaml ├── __init__.py ├── config_loader.py └── default_values.yaml ├── data_processing ├── convert_to_obj.py ├── create_split.py ├── eccv2020-sharp-workshop │ ├── changelog.md │ ├── doc │ │ ├── 3dbodytex2.png │ │ ├── challenge_1.md │ │ ├── challenge_1_track_1.md │ │ ├── challenge_1_track_2.md │ │ ├── challenge_2.md │ │ ├── cli.md │ │ ├── data │ │ │ ├── 3dbodytex2-casual-a-small.png │ │ │ ├── 3dbodytex2-casual-run-small.png │ │ │ ├── 3dbodytex2-casual-scape003-small.png │ │ │ ├── 3dbodytex2-casual-scape032-small.png │ │ │ ├── 3dbodytex2-casual-scape070-small.png │ │ │ ├── 3dbodytex2-casual-u-small.png │ │ │ ├── 3dbodytex2-fitness-a-small.png │ │ │ ├── 3dbodytex2-synthetic-fitted-small.png │ │ │ ├── 3dbodytex2-synthetic-scan-small.png │ │ │ ├── 3dbodytex2-synthetic-simulated-small.png │ │ │ ├── 3dbodytex2-texture_atlas-scan-small.png │ │ │ └── 3dbodytex2-texture_atlas-small.png │ │ ├── dataset_3dbodytex2.md │ │ ├── dataset_3dobjecttex.md │ │ ├── dataset_splits.md │ │ ├── datasets.md │ │ ├── evaluation.md │ │ ├── formats.md │ │ └── landmark_names.txt │ ├── readme.md │ ├── requirements.txt │ ├── scripts │ │ └── demo_holes │ ├── setup.py │ └── sharp │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── data.py │ │ ├── landmarks.py │ │ ├── linalg.py │ │ ├── trirender.py │ │ └── utils.py ├── sample_RGB_GT.py ├── sample_voxelized_colored_pointcloud.py └── utils.py ├── environment.yml ├── generate.py ├── if-net_env.yml ├── models ├── __init__.py ├── dataloader.py ├── generation.py ├── local_model.py └── training.py ├── sharp_teaser.png └── train.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.so 3 | build/ 4 | *.pyc 5 | *.c 6 | /experiments/* 7 | *.cpp 8 | /trash/ 9 | !.gitkeep 10 | dataset/SHARP2020/challenge1-track1/* 11 | .idea/* 12 | README.md~ 13 | sharp_teaser.xcf 14 | **/.fuse* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Implict Feature Networks (IF-Nets) - Texture Extention 3 | > Implicit Feature Networks for Texture Completion from Partial 3D Data
4 | > [Julian Chibane](http://virtualhumans.mpi-inf.mpg.de/people/Chibane.html), [Gerard Pons-Moll](http://virtualhumans.mpi-inf.mpg.de/people/pons-moll.html) 5 | 6 | ![Teaser](sharp_teaser.png) 7 | 8 | [Paper](https://virtualhumans.mpi-inf.mpg.de/papers/jchibane20ifnet/SHARP2020.pdf) - 9 | [Project Website](https://virtualhumans.mpi-inf.mpg.de/ifnets/) - 10 | [Arxiv](http://arxiv.org/abs/2009.09458) - 11 | Published in European Conference on Computer Vision (ECCV) Workshops. 12 | 13 | 14 | #### Citation 15 | If you find our code or paper usful for your project, please consider citing: 16 | 17 | @inproceedings{chibane20ifnet, 18 | title = {Implicit Functions in Feature Space for 3D Shape Reconstruction and Completion}, 19 | author = {Chibane, Julian and Alldieck, Thiemo and Pons-Moll, Gerard}, 20 | booktitle = {{IEEE} Conference on Computer Vision and Pattern Recognition (CVPR)}, 21 | month = {jun}, 22 | organization = {{IEEE}}, 23 | year = {2020}, 24 | } 25 | and 26 | 27 | @inproceedings{chibane2020ifnet_texture, 28 | title = {Implicit Feature Networks for Texture Completion from Partial 3D Data}, 29 | author = {Chibane, Julian and Pons-Moll, Gerard}, 30 | booktitle = {European Conference on Computer Vision (ECCV) Workshops}, 31 | month = {August}, 32 | organization = {{Springer}}, 33 | year = {2020}, 34 | } 35 | 36 | ## Install 37 | 38 | A linux system with cuda 9.0 or above is required for the project. 39 | 40 | Please clone the repository and navigate into it in your terminal, its location is assumed for all subsequent commands. 41 | 42 | The `if-net_env.yml` file contains necessary python dependencies for the project. 43 | To conveniently install them automatically with [anaconda](https://www.anaconda.com/) you can use: 44 | ``` 45 | conda env create -f if-net_env.yml 46 | conda activate tex_if-net 47 | ``` 48 | 49 | Install the library of the [workshop](https://gitlab.uni.lu/cvi2/eccv2020-sharp-workshop/) with : 50 | ``` 51 | cd data_processing/eccv2020-sharp-workshop 52 | pip install -r requirements.txt 53 | pip install . 54 | cd ../.. 55 | ``` 56 | 57 | ## Data Preparation 58 | 59 | Please [download](https://cvi2.uni.lu/sharp2020/registration/) and unzip the data of Challange 1 Track 1 into `dataset/SHARP2020/` such that the data is stored as `dataset/SHARP2020/challenge1-track1/{test,train}/File_ID/Files`. 60 | 61 | For each human scan we create 4 different, random incomplete ones with 62 | 63 | `python -m sharp shoot_dir dataset/SHARP2020/challenge1-track1 dataset/SHARP2020/challenge1-track1 --n-shapes 4` 64 | 65 | and convert everything to `.obj`-Format 66 | 67 | `python data_processing/convert_to_obj.py`. 68 | 69 | We sample points on the full colored surface, extract the corresponding RGB color and save it as training data with 70 | 71 | `python data_processing/sample_RGB_GT.py config/SHARP2020/track1.yaml`. 72 | 73 | Next, we create the input for the IF-Net (a colored, incomplete shape and a complete but uncolored shape) with 74 | 75 | `python data_processing/sample_voxelized_colored_pointcloud.py config/SHARP2020/track1.yaml`. 76 | 77 | A data split into training, test and validation is created with 78 | 79 | `python data_processing/create_split.py config/SHARP2020/track1.yaml`. 80 | 81 | ## Pretrained Model 82 | A pretrained model can be found [here](https://nextcloud.mpi-klsb.mpg.de/index.php/s/xPTb4oHb83Txi2W). 83 | 84 | 85 | ## Training and Generation 86 | 87 | A model is trained to predict the correct color of a surface point given colored, partial human and ground truth (complete), uncolored human. Use 88 | 89 | `python train.py config/SHARP2020/track1.yaml` 90 | 91 | to train until convergence. Generation on held out test data is done with 92 | 93 | `python generate.py config/SHARP2020/track1.yaml` 94 | 95 | automatically using the best model (lowest validation error). 96 | 97 | You can also alter the generation code to change the ground truth (complete), uncolored human input to a surface reconstruction. 98 | We used a standard [IF-Net](https://virtualhumans.mpi-inf.mpg.de/ifnets/) for surface reconstruction. 99 | 100 | ## Contact 101 | 102 | For questions and comments regarding the code please contact [Julian Chibane](http://virtualhumans.mpi-inf.mpg.de/people/Chibane.html) via mail. 103 | 104 | ## License 105 | Copyright (c) 2020 Julian Chibane, Max-Planck-Gesellschaft 106 | 107 | Please read carefully the following terms and conditions and any accompanying documentation before you download and/or use this software and associated documentation files (the "Software"). 108 | 109 | The authors hereby grant you a non-exclusive, non-transferable, free of charge right to copy, modify, merge, publish, distribute, and sublicense the Software for the sole purpose of performing non-commercial scientific research, non-commercial education, or non-commercial artistic projects. 110 | 111 | Any other use, in particular any use for commercial purposes, is prohibited. This includes, without limitation, incorporation in a commercial product, use in a commercial service, or production of other artefacts for commercial purposes. 112 | For commercial inquiries, please see above contact information. 113 | 114 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 115 | 116 | You understand and agree that the authors are under no obligation to provide either maintenance services, update services, notices of latent defects, or corrections of defects with regard to the Software. The authors nevertheless reserve the right to update, modify, or discontinue the Software at any time. 117 | 118 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. You agree to cite the `Implicit Functions in Feature Space for 3D Shape Reconstruction and Completion` and `Implicit Feature Networks for Texture Completion from Partial 3D Data` papers in documents and papers that report on research using this Software. 119 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/SHARP2020/track1.yaml: -------------------------------------------------------------------------------- 1 | input_type: pointcloud 2 | input_resolution: 128 3 | input_points_number: 100000 4 | model: TEXR 5 | folder_name: SHARP2020_c1_t1 6 | data_path: dataset/SHARP2020/challenge1-track1/ 7 | data_bounding_box: [-0.8, 0.8, -0.15, 2.1, -0.8, 0.8] 8 | split_file: dataset/SHARP2020/challenge1-track1/split.npz 9 | preprocessing: 10 | color_sampling: 11 | input_files_regex: /*/*/*_normalized.obj 12 | sample_number: 100000 13 | voxelized_colored_pointcloud_sampling: 14 | input_files_regex: /*/*/*-partial-*.obj 15 | training: 16 | batch_size: 2 17 | sample_points_per_object: 50000 18 | optimizer: Adam 19 | generation: 20 | retrieval_resolution: 256 21 | checkpoint: -1 22 | batch_points: 800000 23 | mode: test 24 | retrieval_threshold: 0.5 25 | 26 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/config/__init__.py -------------------------------------------------------------------------------- /config/config_loader.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import numpy as np 3 | 4 | def load(path): 5 | with open('config/default_values.yaml', 'r') as f: 6 | default_cfg = yaml.load(f, yaml.FullLoader) 7 | 8 | with open(path, 'r') as f: 9 | cfg = yaml.load(f, yaml.FullLoader) 10 | 11 | default_cfg.update(cfg) 12 | cfg = default_cfg 13 | 14 | cfg['data_bounding_box'] = np.array(cfg['data_bounding_box']) 15 | cfg['data_bounding_box_str'] = ",".join(str(x) for x in cfg['data_bounding_box']) 16 | 17 | 18 | return cfg -------------------------------------------------------------------------------- /config/default_values.yaml: -------------------------------------------------------------------------------- 1 | input_type: voxels 2 | input_resolution: 32 3 | input_points_number: 3000 4 | model: ShapeNet32Vox 5 | data_path: datasets/shapenet/data/ 6 | data_bounding_box: [-0.5,0.5,-0.5,0.5,-0.5,0.5] 7 | split_file: datasets/shapenet/split.npz 8 | preprocessing: 9 | boundary_sampling: 10 | regex_input_files: /*/*/isosurf_scaled.off 11 | sample_number: 100000 12 | append_inputname_to_outputname: false 13 | training: 14 | sample_distribution: [0.5,0.5] 15 | sample_sigmas: [0.1,0.01] 16 | batch_size: 6 17 | sample_points_per_object: 50000 18 | optimizer: Adam 19 | generation: 20 | retrieval_resolution: 256 21 | checkpoint: -1 22 | batch_points: 800000 23 | mode: test 24 | retrieval_threshold: 0.5 25 | 26 | -------------------------------------------------------------------------------- /data_processing/convert_to_obj.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | import tqdm 4 | import multiprocessing as mp 5 | from multiprocessing import Pool 6 | 7 | 8 | print('Finding Paths to convert (from .npz to .obj files).') 9 | paths = glob('dataset/SHARP2020/challenge1-track1/*/*/*.npz') 10 | 11 | 12 | print('Start converting.') 13 | def convert(path): 14 | outpath = path[:-4] + '.obj' 15 | 16 | cmd = 'python -m sharp convert {} {}'.format(path,outpath) 17 | os.system(cmd) 18 | 19 | 20 | p = Pool(mp.cpu_count()) 21 | for _ in tqdm.tqdm(p.imap_unordered(convert, paths), total=len(paths)): 22 | pass -------------------------------------------------------------------------------- /data_processing/create_split.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import random 3 | import numpy as np 4 | import config.config_loader as cfg_loader 5 | import argparse 6 | import random 7 | import os 8 | 9 | parser = argparse.ArgumentParser( 10 | description='Generates a data split file.' 11 | ) 12 | 13 | parser.add_argument('config', type=str, help='Path to config file.') 14 | args = parser.parse_args() 15 | 16 | cfg = cfg_loader.load(args.config) 17 | 18 | train_all = glob(os.path.join(cfg['data_path'], 'train', cfg['preprocessing']['voxelized_colored_pointcloud_sampling']['input_files_regex'][3:])) 19 | random.shuffle(train_all) 20 | val = train_all[:int(len(train_all)*0.1)] 21 | train = train_all[int(len(train_all)*0.1):] 22 | 23 | test = glob(os.path.join(cfg['data_path'], 'test', cfg['preprocessing']['voxelized_colored_pointcloud_sampling']['input_files_regex'][3:])) 24 | 25 | split_dict = {'train':train, 'test':test, 'val':val} 26 | 27 | np.savez(cfg['split_file'], **split_dict) 28 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/changelog.md: -------------------------------------------------------------------------------- 1 | # Changlog 2 | 3 | ## [1.4.1] - 2020-07-24 4 | 5 | ### Added 6 | 7 | - Add an optional `--seed` argument to `python -m sharp shoot` for 8 | repeatability. 9 | 10 | ### Changed 11 | 12 | - Make `python -m sharp shoot` faster, especially for larger meshes. 13 | 14 | 15 | ## [1.4.0] - 2020-07-22 16 | 17 | ### Added 18 | 19 | - Support masks (i.e. ROI's) in the shooting method for generating partial 20 | data. 21 | - Routine to generate partial data on a directory tree. 22 | - Make generation of partial data repeatble (with the `--seed` CLI option). 23 | 24 | ### Changed 25 | 26 | - Clarify the final evaluation metric in the documentation. 27 | - Clarify the CLI in the documentation. 28 | - Require numpy>=1.17.0 for the updated pseudo-random number generation API. 29 | - Support saving vertex colors in a .npz. 30 | 31 | ### Fixed 32 | 33 | - Fixed memory leak in the shooting method for generating partial data that 34 | made repeated calls to `sharp.utils.shoot_holes()` crash after some time. 35 | - Prevent black seams in the texture when generating partial data. 36 | 37 | ### Removed 38 | 39 | - Drop the 'cut' method for generating partial data. 40 | 41 | 42 | ## [1.3.0] - 2020-07-13 43 | 44 | ### Changed 45 | 46 | - Set entry point for the CLI at the top level of the module. 47 | 48 | ### Fixed 49 | 50 | - Support saving a mesh as .npz without texture information. 51 | - Various bugs in the method for shooting holes. 52 | 53 | 54 | ## [1.2.0] - 2020-06-10 55 | 56 | ### Changed 57 | 58 | - Clarify instructions. 59 | 60 | 61 | ## [1.1.0] - 2020-04-18 62 | 63 | ### Added 64 | 65 | - Support i/o for npz meshes. 66 | 67 | ### Changed 68 | 69 | - Organise into module. 70 | 71 | 72 | ## [1.0.0] - 2020-04-09 73 | 74 | ### Added 75 | 76 | - Initial release. 77 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/3dbodytex2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/3dbodytex2.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/challenge_1.md: -------------------------------------------------------------------------------- 1 | # Challenge 1: Recovery of Human Body Scans 2 | 3 | There are two tracks: 4 | 5 | - [Track 1: Recovery of large regions](challenge_1_track_1.md) 6 | - [Track 2: Recovery of fine details](challenge_1_track_2.md) 7 | 8 | with two complementary subsets of the [3DBodyTex 2](dataset_3dbodytex2.md) 9 | datasets. 10 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/challenge_1_track_1.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 - Track 1: Recovery of Large Regions In Human Scans 2 | 3 | Given a partial human body scan, `X`, the goal is to recover the complete scan, 4 | `Y`. 5 | 6 | This challenge uses the [3DBodyTex 2](dataset_3dbodytex2.md) dataset. 7 | 8 | 9 | ## Training data 10 | 11 | Provided: 12 | 13 | 1. A set of complete scans, `Y`. 14 | 2. Additional metadata: The 3D positions of detected body landmarks. 15 | 16 | Not provided: 17 | 18 | 1. The partial scans, `X`. They are generated by the participants. 19 | 20 | The body landmarks are used to generate the partial data. 21 | They may be freely exploited during training but they are not provided for the 22 | final evaluation. 23 | 24 | 25 | ## Evaluation data 26 | 27 | Provided: 28 | 29 | 1. A set of partial scans, `X`. 30 | 31 | Not provided: 32 | 33 | 1. The ground-truth scans, `Y`. 34 | (They will be released after the competition.) 35 | 2. Additional metadata. 36 | 37 | 38 | ## Evaluation 39 | 40 | Considered for the evaluation: 41 | 42 | - the shape, 43 | - the texture, 44 | 45 | Not considered for the evaluation: 46 | 47 | - the head and the hands. 48 | 49 | See [evaluation](evaluation.md) for the definition of the metric. 50 | 51 | The evaluation is performed on the whole body (not only on the completed parts) 52 | except for the head and the hands, where the reference is blurred or less 53 | reliable. 54 | 55 | The regions of the head and hands are identified automatically from the 56 | corresponding segmentation on the ground-truth data (not shared). 57 | 58 | 59 | ## Submission format 60 | 61 | The predicted complete mesh, `Y'`, should be in `.obj` or `.npz` (see 62 | [formats](formats.md)). 63 | 64 | The mesh colour information should be either: 65 | 66 | 1. a single texture atlas, 67 | 2. or RGB colour stored as vertex attributes. 68 | 69 | A mixture of vertex colour attribute and texture mapping is not allowed. 70 | 71 | The mesh geometry may be different than the input, `X`. 72 | 73 | The submission must be a `.zip` archive that unfolds in this directory 74 | structure: 75 | 76 | ``` 77 | eval/ 78 | / 79 | -completed.(npz|obj) 80 | .../ 81 | ``` 82 | 83 | where `` correponds to the name of the input scan. 84 | 85 | 86 | ## Directory structure of the dataset 87 | 88 | The data files are arranged in the following directory structure: 89 | 90 | ``` 91 | train/ 92 | / 93 | _normalized.npz 94 |     landmarks3d.txt 95 | .../ 96 | test/ 97 | / 98 | _normalized.npz 99 |     landmarks3d.txt 100 | .../ 101 | eval/ 102 | / 103 | _normalized.npz 104 | -partial.npz 105 |     landmarks3d.txt 106 | .../ 107 | ``` 108 | 109 | For each scan, there is one subdirectory with a unique ``, 110 | e.g. `170410-007-f-1moq-b682-low-res-result`. 111 | The files are: 112 | 113 | * `_normalized.npz` = `Y`: 114 | The ground-truth mesh, i.e. the raw scan. 115 | * `-partial.npz` = `X`: 116 | Partial scan generated from `Y`. 117 | * `landmarks3d.txt`: 3D positions of detected body landmarks. 118 | 119 | See [formats](formats.md) for the mesh and landmark data formats. 120 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/challenge_1_track_2.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 - Track 2: Recovery of Fine Details in Human Scans 2 | 3 | Given a partial human body scan, `X`, the goal is to recover the complete scan, 4 | `Y`. 5 | 6 | This challenge uses the [3DBodyTex 2](dataset_3dbodytex2.md) dataset. 7 | 8 | 9 | ## Training data 10 | 11 | Provided: 12 | 13 | 1. A set of simulated complete scans, `Ys`. 14 | 2. Additional metadata: The 3D positions of detected body landmarks. 15 | 16 | Not provided: 17 | 18 | 1. The partial scans, `X`. They are generated by the participants. 19 | 20 | The body landmarks are used to generate the partial data. 21 | They may be freely exploited during training but they are not provided for the 22 | final evaluation. 23 | 24 | Both meshes are aligned and in the same frame of reference. 25 | 26 | 27 | ## Evaluation data 28 | 29 | Provided: 30 | 31 | 1. A set of partial scans, `X`. 32 | These are generated by the organisers from `Ys`. 33 | 34 | Not provided: 35 | 36 | 1. The ground-truth scans, `Y = Yf`. 37 | (They will be released after the competition.) 38 | 2. Additional metadata. 39 | 40 | 41 | ## Evaluation 42 | 43 | Considered for the evaluation: 44 | 45 | - the shape, 46 | - ears, hands and feet. 47 | 48 | Not considered for the evaluation: 49 | 50 | - the texture, 51 | - the rest of the body. 52 | 53 | See [evaluation](evaluation.md) for the definition of the metric. 54 | 55 | The evaluation is performed on the ears, the hands and the feet (areas with 56 | fine details). 57 | The rest of the body is ignored. 58 | The hand region starts at the wrist. 59 | The foot region starts at the ankle. 60 | 61 | The texture is ignored as it is less reliable on the areas with fine details on 62 | the synthetic data. 63 | 64 | 65 | ## Submission format 66 | 67 | The predicted complete mesh, `Y'`, should be in `.obj` or `.npz` (see 68 | [formats](formats.md)). 69 | 70 | The mesh colour information should be either: 71 | 72 | 1. a single texture atlas, 73 | 2. or RGB colour stored as vertex attributes. 74 | 75 | A mixture of vertex colour attribute and texture mapping is not allowed. 76 | 77 | The mesh geometry may be different than the input, `X`. 78 | 79 | The submission must be a `.zip` archive that unfolds in this directory 80 | structure: 81 | 82 | ``` 83 | eval/ 84 | / 85 | -completed.(npz|obj) 86 | .../ 87 | ``` 88 | 89 | where `` correponds to the name of the input scan. 90 | 91 | 92 | ## Directory structure of the dataset 93 | 94 | The dataset has the following directory structure: 95 | 96 | ``` 97 | train/ 98 | / 99 | fitted_textured.npz 100 | fusion_textured.npz 101 | landmarks3d.txt 102 | .../ 103 | test/ 104 | / 105 | fitted_textured.npz 106 | fusion_textured.npz 107 | landmarks3d.txt 108 | .../ 109 | eval/ 110 | / 111 | fitted_textured.npz 112 | fusion_textured.npz 113 | -synthetic-partial.npz 114 | landmarks3d.txt 115 | .../ 116 | ``` 117 | 118 | For each scan, there is one subdirectory with a unique ``, 119 | e.g. `170926-001-fitness-run-sbt1-d8f6-low-res-result`. 120 | The files are: 121 | 122 | * `fitted_textured.npz` = `Yf`: 123 | The reference mesh with details. 124 | It is obtained by fitting the SMPL-X body model to a body scan and 125 | transferring the texture. 126 | * `fusion_textured.npz`: 127 | `Ys`. 128 | The simulated body scan. 129 | This is a textured mesh obtained by simulating the scanning process in 130 | software on `Yf`. 131 | It is less detailed and contains artefacts similar to a real 3D scan. 132 | * `-synthetic-partial.npz` = `X`: 133 | Partial scan generated from `Ys`. 134 | * `landmarks3d.txt`: 135 | The 3D positions of the detected 3D landmarks. 136 | They are common to both meshes. 137 | 138 | See [formats](formats.md) for the mesh and landmark data formats. 139 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/challenge_2.md: -------------------------------------------------------------------------------- 1 | # Challenge 2: Recovery of Generic Object Scans 2 | 3 | Given a partial object scan, `X`, the goal is to recover the complete scan, 4 | `Y`. 5 | 6 | This challenge uses the [3DObjectTex](dataset_3dobjecttex.md) dataset. 7 | 8 | 9 | ## Training data 10 | 11 | Provided: 12 | 13 | 1. A set of complete scans, `Y`. 14 | 15 | Not provided: 16 | 17 | 1. The partial scans, `X`. They are generated by the participants. 18 | 19 | 20 | ## Evaluation data 21 | 22 | Provided: 23 | 24 | 1. A set of partial scans, `X`. 25 | 26 | Not provided: 27 | 28 | 1. The ground-truth scans, `Y`. 29 | (They will be released after the competition.) 30 | 31 | 32 | ## Evaluation 33 | 34 | Considered for the evaluation: 35 | 36 | - the shape, 37 | - the texture. 38 | 39 | See [evaluation](evaluation.md) for the definition of the metric. 40 | 41 | 42 | ## Submission format 43 | 44 | The predicted complete mesh, `Y'`, should be in `.obj` or `.npz` (see 45 | [formats](formats.md)). 46 | 47 | The mesh colour information should be either: 48 | 49 | 1. a single texture atlas, 50 | 2. or RGB colour stored as vertex attributes. 51 | 52 | A mixture of vertex colour attribute and texture mapping is not allowed. 53 | 54 | The mesh geometry may be different than the input, `X`. 55 | 56 | The submission must be a `.zip` archive that unfolds in this directory 57 | structure: 58 | 59 | ``` 60 | eval/ 61 | / 62 | -completed.(npz|obj) 63 | .../ 64 | ``` 65 | 66 | where `` correponds to the name of the input scan. 67 | 68 | 69 | ## Directory structure of the dataset 70 | 71 | The data files are arranged in the following directory structure: 72 | 73 | ``` 74 | train/ 75 | / 76 | .obj 77 | .mtl 78 | _0.png 79 | .../ 80 | test/ 81 | / 82 | .obj 83 | .mtl 84 | _0.png 85 | .../ 86 | eval/ 87 | / 88 | .obj 89 | .mtl 90 | _0.png 91 | -partial.obj 92 | -partial.mtl 93 | -partial.png 94 | .../ 95 | ``` 96 | 97 | For each scan, there is one subdirectory with a unique ``, 98 | e.g. `model_1`, `model_1208`... 99 | The files are: 100 | 101 | * `(.obj|.mtl|_0.png)` = `Y`: 102 | The ground-truth mesh, i.e. the raw scan. 103 | * `-partial(.obj|.mtl|.png)` = `X`: 104 | Partial scan generated from `Y`. 105 | 106 | See [formats](formats.md) for the mesh format. 107 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/cli.md: -------------------------------------------------------------------------------- 1 | # Command line interface and tools 2 | 3 | Display help on available commands: 4 | 5 | ```bash 6 | $ python -m sharp 7 | ``` 8 | 9 | Display help on a specific command, e.g. `convert`: 10 | 11 | ```bash 12 | $ python -m sharp convert -h 13 | ``` 14 | 15 | 16 | ## Convert between mesh formats 17 | 18 | Supported formats: `.obj`, `.npz`. 19 | 20 | ```bash 21 | $ python -m sharp convert shoot path/to/input.obj path/to/output.npz 22 | $ python -m sharp convert path/to/input.npz path/to/output.obj 23 | ``` 24 | 25 | 26 | ## Generate partial data 27 | 28 | Supported formats: `.obj`, `.npz`. 29 | 30 | 31 | ### Holes shooting on a single mesh 32 | 33 | Usage example: 34 | 35 | ```bash 36 | # Shoot 40 holes with each hole removing 2% of the points of the mesh. 37 | $ python -m sharp shoot path/to/input.(npz|obj) path/to/output.(npz|obj) --holes 40 --dropout 0.02 [--mask path/to/mask.npy] 38 | ``` 39 | 40 | --mask: (optional) path to the mask (.npy) to generate holes only on regions considered for evaluation (only challenge 1). 41 | As mentioned in the [evaluation doc](https://gitlab.uni.lu/asaint/eccv2020-sharp-workshop/-/blob/update-instructions/doc/evaluation.md#challenge-specific-criteria), 42 | challenge 1 is evaluated on specific regions of the body mesh: 43 | 44 | - Track 1: head and hands are ignored, rest of the body is considered 45 | - Track 2: hands, ears, and feet are considered, rest of the body is ignored 46 | 47 | A mask is defined per face as boolean information: 0 if the face is to be ignored, and 1 if the face is to be kept. 48 | 49 | 50 | ### Holes shooting on a directory tree of meshes 51 | 52 | Usage examples: 53 | 54 | ```bash 55 | # Shoot 40 holes with each hole removing 2% of the points of the mesh. 56 | $ python -m sharp shoot_dir path/to/input_directory path/to/output_directory --holes 40 --dropout 0.02 [--mask-dir path/to/mask_directory] [--seed seed_value] [--n-workers n_workers] [--n-shapes n_shapes] 57 | ``` 58 | 59 | --mask-dir: (optional) Directory tree with the masks (.npy). If defined, the partial data is created only on the non-masked faces of the meshes (only challenge 1). 60 | 61 | --seed: Initial state for the pseudo random number generator. If not set, the initial state is not set explicitly. 62 | 63 | --n-workers: Number of parallel processes. By default, the number of available processors. 64 | 65 | -n: (or --n-shapes) Number of partial shapes to generate per mesh. Default is 1. 66 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-a-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-a-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-run-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-run-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-scape003-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-scape003-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-scape032-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-scape032-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-scape070-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-scape070-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-u-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-casual-u-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-fitness-a-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-fitness-a-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-synthetic-fitted-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-synthetic-fitted-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-synthetic-scan-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-synthetic-scan-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-synthetic-simulated-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-synthetic-simulated-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-texture_atlas-scan-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-texture_atlas-scan-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-texture_atlas-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/doc/data/3dbodytex2-texture_atlas-small.png -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/dataset_3dbodytex2.md: -------------------------------------------------------------------------------- 1 | # 3DBodyTex 2 2 | 3 | 3DBodyTex 2 contains thousands of 3D textured human scans. 4 | It is an extension of [3DBodyTex](https://cvi2.uni.lu/datasets/). 5 | 6 | ![Sample scans from the 3DBodyTex 2 dataset](3dbodytex2.png) 7 | *Subset of 3DBodyTex 2.* 8 | 9 | ## Content 10 | 11 | - 3D human scans (`Y`): 12 | - acquired with the [Shapify Booth][shapify booth], 13 | - many subjects, 14 | - varied poses, 15 | - varied casual clothing, 16 | - standard tight-fitting clothing, 17 | - 3D watertight meshes with a high-resolution texture atlas; 18 | - 3D landmarks: 19 | - body joints and landmark points on the surface, 20 | - estimated automatically; 21 | - Fitted textured SMPL-X model (`Yf`): 22 | - SMPL-X is fitted to all the raw scans `Y` with close-fitting clothing to 23 | obtain `Yf`, 24 | - pose and shape parameters, 25 | - fixed template mesh (SMPL-X), 26 | - texture mapped from the raw scans; 27 | - Simulated 3D scan acquisitions (`Ys`): 28 | - the process of the [Shapify Booth][shapify booth] is simulated in software, 29 | - the acquisition is simulated on the fitted SMPL-X models `Yf` to obtain 30 | `Ys`; 31 | - Synthetic pairs `(Yf, Ys)` of `(detailed ground truth, acquisition)`: 32 | - `Yf` is a synthetic groud truth with detailed shape and texture, 33 | - `Ys` is a synthetic scan acquisition with typical artefacts, 34 | - for scans with close-fitting clothing only. 35 | 36 | [shapify booth]: https://www.artec3d.com/portable-3d-scanners/shapifybooth 37 | 38 | 39 | ## Usage in the SHARP [Challenge 1](challenge_1.md) 40 | 41 | ### [Track 1](challenge_1_track_1.md): Recovery of large regions 42 | 43 | - The ground truth is `Y`. 44 | - The partial scans are generated from `Y`. 45 | - The 3D landmarks are provided at training time only, not at evaluation time. 46 | 47 | ### [Track 2](challenge_1_track_2.md): Recovery of fine details 48 | 49 | - The ground truth is `Yf`. 50 | - The partial scans are generated from `Ys`. 51 | - The 3D landmarks are provided at training time only, not at evaluation time. 52 | 53 | 54 | ## Detailed description and statistics 55 | 56 | ### Clothing 57 | 58 | casual | fitness 59 | -|- 60 | ![casual][img-casual] | ![fitness][img-fitness] 61 | 62 | [img-casual]: data/3dbodytex2-casual-a-small.png "casual" 63 | [img-fitness]: data/3dbodytex2-fitness-a-small.png "fitness" 64 | 65 | ### Poses 66 | 67 | A | U | run | scape | free 68 | -|-|-|-|- 69 | ![][img-pose-a] | ![][img-pose-u] | ![][img-pose-run] | ![][img-pose-scape-0] ![][img-pose-scape-1] ![][img-pose-scape-2] ... | unconstrained 70 | 71 | [img-pose-a]: data/3dbodytex2-casual-a-small.png "pose-a" 72 | [img-pose-u]: data/3dbodytex2-casual-u-small.png "pose-u" 73 | [img-pose-run]: data/3dbodytex2-casual-run-small.png "pose-run" 74 | [img-pose-scape-0]: data/3dbodytex2-casual-scape003-small.png "pose-scape-003" 75 | [img-pose-scape-1]: data/3dbodytex2-casual-scape032-small.png "pose-scape-032" 76 | [img-pose-scape-2]: data/3dbodytex2-casual-scape070-small.png "pose-scape-070" 77 | 78 | ### Synthetic detailed ground-truth and simulated acquisitions 79 | 80 | The goal is to obtain a ground-truth shape preserving the details of 81 | smaller-scale regions (e.g. fingers and feet). 82 | This cannot be acquired systematically with a body scanner. 83 | Instead, the template mesh of a body model (SMPL-X) is used as a reference for 84 | the fine-scale shape details. 85 | 86 | The procedure to synthesise the data is: 87 | 88 | 1. `Y` = input: raw scan in close-fitting clothing, 89 | 2. `Yf` = fit `Y`: fit the body model to `Y` and transfer the texture to obtain 90 | `Yf`, 91 | 3. `Ys` = acquire `Yf`: simulate the acquisition process on `Yf` to obtain 92 | `Ys`. 93 | 94 | The pair `(Yf, Ys)` is a synthetic pair of `(acquired, ground-truth)` shape 95 | that can be used for supervision. 96 | 97 | scan (`Y`) | fitted (`Yf`) | simulated acquisition (`Ys`) 98 | -|-|- 99 | ![fitness][img-synthetic-scan] | ![fitted][img-synthetic-fitted] | ![simulated][img-synthetic-simulated] 100 | 101 | [img-synthetic-scan]: data/3dbodytex2-synthetic-scan-small.png "scan" 102 | [img-synthetic-fitted]: data/3dbodytex2-synthetic-fitted-small.png "fitted" 103 | [img-synthetic-simulated]: data/3dbodytex2-synthetic-simulated-small.png "simulated acquisition" 104 | 105 | 106 | ## Encoding formats and conventions 107 | 108 | ### File formats 109 | 110 | Meshes are in the `.npz` format. 111 | Landmark positions are in a tabular text format. 112 | See [formats](formats.md). 113 | 114 | ### Orientation 115 | 116 | The up direction is the y axis. 117 | The ground is the x-z plane. 118 | Most scans are facing the -z direction, but not all. 119 | 120 | ### Units 121 | 122 | Vertex positions are in metres. 123 | 124 | 125 | ## 3D body landmarks 126 | 127 | There are 67 body landmarks detected automatically on each scan. 128 | They are provided to generate the partial data but may also be used for 129 | training. 130 | They comprise standard body joints and other keypoints on the body (eyes, nose, 131 | ears...). 132 | The detection of most landmarks is stable except for the finger joints and 133 | finger tips. 134 | 135 | Below, `` is either `left` or `right`. 136 | 137 | ### Body joints (20) 138 | 139 | | landmark name | 140 | | - | 141 | | ankle_\ | 142 | | elbow_\ | 143 | | heel_\ | 144 | | hip_\ | 145 | | hip_middle | 146 | | knee_\ | 147 | | neck | 148 | | shoulder_\ | 149 | | toe_1_\ | 150 | | toe_5_\ | 151 | | wrist_\ | 152 | 153 | ### Face landmarks (5) 154 | 155 | | landmark name | 156 | | - | 157 | | ear_\ | 158 | | eye_\ | 159 | | nose | 160 | 161 | ### Finger joints and tips (42) 162 | 163 | `` is one of `base`, `middle`, `top`, `tip`. 164 | 165 | | landmark name | 166 | | - | 167 | | finger_baby_\_\ | 168 | | finger_index_\_\ | 169 | | finger_middle_\_\ | 170 | | finger_ring_\_\ | 171 | | finger_thumb_\_\ | 172 | | hand_base_\ | 173 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/dataset_3dobjecttex.md: -------------------------------------------------------------------------------- 1 | # 3DObjectTex 2 | 3 | 3DObjectTex contains about 1200 scans of generic objects. 4 | 5 | ## Encoding formats and conventions 6 | 7 | ### File formats 8 | 9 | Meshes are in the `.obj` format. 10 | See [formats](formats.md). 11 | 12 | ### Units 13 | 14 | Vertex positions are in millimetres. 15 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/dataset_splits.md: -------------------------------------------------------------------------------- 1 | # Dataset splits 2 | 3 | In both challenges, the data is split into `train/test/eval` sets. 4 | 5 | In the first stage of the the competition, the `train/test` sets are provided 6 | for training. 7 | They contain only the ground-truth shapes, `Y`. 8 | The partial data, `X`, is generated by the participants. 9 | Some [example routines](../sharp/preprocess.py) are provided. 10 | Custom routines may be used if reported. 11 | 12 | At a later stage, the `eval` set is provided to generate the submission for the 13 | final evaluation. 14 | In this set, the partial data, `X`, is generated by the organisers and provided 15 | to the participants. 16 | The corresponding ground-truth shapes, `Y`, are kept secret. 17 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/datasets.md: -------------------------------------------------------------------------------- 1 | # Datasets 2 | 3 | Two new datasets with thousands of 3D textured scans are released as part of 4 | the SHARP competition: 5 | 6 | 1. [3DBodyTex 2](dataset_3dbodytex2.md): textured human body scans, 7 | 2. [3DObjectTex](dataset_3dobjecttex.md): textured object scans. 8 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/evaluation.md: -------------------------------------------------------------------------------- 1 | # Evaluation 2 | 3 | ## Quantitative 4 | 5 | Notation: 6 | 7 | - $`Y`$: the ground truth complete reference mesh, 8 | - $`Y'`$: the estimated complete mesh. 9 | 10 | The quality of the estimation, $`Y'`$, is evaluated quantitatively with respect 11 | to the ground truth, $`Y`$, using three criterions: 12 | 13 | ### 1. Surface-to-surface distances 14 | 15 | Consist of two directed distances: 16 | 17 | 1. $`d_{ER}`$ is computed from the estimation to the reference 18 | 2. $`d_{RE}`$ is computed from the reference to the estimation. 19 | 20 | These distances are inspired from [1] but have been adpated to fit the problem at hand. 21 | The directed distance $`d_{AB}`$ between meshes $`A`$ and $`B`$ is 22 | approximated in practice by sampling points on $`A`$ and computing their 23 | distances to the nearest triangles in mesh $`B`$. 24 | 25 | The directed distances $`d_{RE}`$ and $`d_{ER}`$ are given by, 26 | ```math 27 | d_{ER}(Y',Y) = \sum_{y' \in Y'} d(y', Y) ,\\ 28 | d_{RE}(Y,Y') = \sum_{y \in Y} d(y, Y') , 29 | ``` 30 | where $`y'`$ are the sampled points on the estimated surface $`Y'`$ and $`y`$ are the sampled points on the reference surface $`Y`$. 31 | 32 | In the two directions, the shape and texture reconstruction errors are measured separately. 33 | For the shape error, the distance, 34 | ```math 35 | d(a, B) = d_{shape}(a, B) , 36 | ``` 37 | operates on the 3D positions directly and computes a point-to-triangle distance between the sampled point $`a`$ on the source surface $`A`$ 38 | and its nearest triangle on the target surface $`B`$. 39 | For the texture error, the distance, 40 | ```math 41 | d(a, B) = d_{tex}(a, B) , 42 | ``` 43 | operates on the interpolated texture values at the source and target 3D positions used to compute the shape distance. 44 | 45 | This results in two shape distance values ($`d_{ER}^{shape}`$, $`d_{RE}^{shape}`$) and two texture distance values ($`d_{ER}^{tex}`$, $`d_{RE}^{tex}`$). 46 | Good estimations are expected to have low shape and texture distance values. 47 | 48 | ### 2. Surface hit-rates 49 | ith the point-to-triangle distance used above. 50 | Consist of two rates that are computed in two directions: 51 | 52 | 1. $`h_{ER}`$ computed from estimation to reference 53 | 2. $`h_{RE}`$ computed from reference to estimation. 54 | 55 | The hit-rate $`h_{AB}`$ indicates the amount of points sampled on the surface of a source mesh $`A`$ that have 56 | a correspondence on the target mesh $`B`$. A point in mesh $`A`$ has a correspondence (hit) in mesh $`B`$ if 57 | its projection on the plane of the nearest triangle in $`B`$ intersects the triangle. 58 | 59 | Let us consider: 60 | 61 | - $`H_{AB}`$: number of points of the source mesh $`A`$ that hit the target $`B`$ 62 | - $`M_{AB}`$: number of points of the source mesh $`A`$ that miss the target $`B`$. 63 | 64 | The hit-rate from $`A`$ to $`B`ith the point-to-triangle distance used above.$ is then given by, 65 | ```math 66 | h_{AB} = \frac{H_{AB}}{H_{AB} + M_{AB}} . 67 | ``` 68 | In the two directions, the hit-rate is a score with a value in [0,1]. Good estimations are expected to have high hit-rates. 69 | 70 | 71 | ### 3. Surface area score 72 | 73 | Consists of a score that quantifies the similarity between 74 | the surface area of the estimation and that of the reference. The surface area of the estimated mesh and the reference mesh 75 | denoted as $`A_{E}`$ and $`A_{R}`$, respectively, are computed by summing over the areas of the triangles of each mesh. 76 | These areas are then normalized as follows, 77 | ```math 78 | \bar{A_{R}} = \frac{A_{R}}{A_{R} + A_{E}} , \\ 79 | \bar{A_{E}} = \frac{A_{E}}{A_{R} + A_{E}} . 80 | ``` 81 | 82 | The area score $`S_a`$ is then given by, 83 | 84 | ```math 85 | S_a = 1 - | \bar{A_{R}} - \bar{A_{E}} | . 86 | ``` 87 | 88 | This score results in a value in [0,1]. Good estimations are expected to have high area scores. 89 | 90 | 91 | ### Final score 92 | 93 | Consists of a combination of the three measures explained above. 94 | 95 | The shape and texture scores are computed as follows, 96 | 97 | ```math 98 | S_s = \frac{1}{2} [ \Phi_{k_1}(d_{ER}^{shape}(Y',Y)) h_{ER} + \Phi_{k_2}(d_{RE}^{shape}(Y,Y')) h_{RE} ] , \\ 99 | S_t = \frac{1}{2} [ \Phi_{k_3}(d_{ER}^{tex}(Y',Y)) h_{ER} + \Phi_{k_4}(d_{RE}^{tex}(Y,Y')) h_{RE} ] , 100 | ``` 101 | 102 | where $`\Phi_{k_i}(d) = e^{-k_id^2}`$ maps a distance $`d`$ to a score in [0,1]. The parameters $`k_i`$ are chosen 103 | according to some conducted baselines. 104 | 105 | The final score is finally given by, 106 | 107 | ```math 108 | S = \frac{1}{2} S_a(S_s + S_t) . 109 | ``` 110 | 111 | ### Challenge-specific criteria 112 | 113 | | challenge(/track) | shape | texture | note | 114 | | - | - | - | - | 115 | | 1/1 | Yes | Yes | hands and head ignored | 116 | | 1/2 | Yes | No | only hands, feet and ears | 117 | | 2 | Yes | Yes | - | 118 | 119 | 120 | ## References 121 | 122 | [1] Jensen, Rasmus, et al. 123 | "Large scale multi-view stereopsis evaluation." 124 | Proceedings of the IEEE Conference on Computer Vision and Pattern 125 | Recognition. 126 | 2014. 127 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/formats.md: -------------------------------------------------------------------------------- 1 | # Data formats 2 | 3 | ## Meshes 4 | 5 | The meshes of generic objects are stored as [.obj](#wavefront-obj-mesh). 6 | 7 | The body scans are textured 3D meshes stored in [.npz](#npz-mesh) archives. 8 | 9 | The texture is encoded as a 10 | [texture atlas](https://en.wikipedia.org/wiki/Texture_atlas). 11 | 12 | | scan | texture atlas | 13 | | - | - | 14 | | ![][texture-scan] | ![][texture] | 15 | 16 | [texture-scan]: data/3dbodytex2-texture_atlas-scan-small.png 17 | [texture]: data/3dbodytex2-texture_atlas-small.png 18 | 19 | ### Wavefront OBJ mesh 20 | 21 | See [Wavefront `.obj`](https://en.wikipedia.org/wiki/Wavefront_.obj_file). 22 | 23 | ### Npz mesh 24 | 25 | This format stores the numpy arrays defining the mesh inside a 26 | [(compressed)](https://numpy.org/doc/stable/reference/generated/numpy.savez_compressed.html) 27 | [numpy `.npz`](https://numpy.org/doc/stable/reference/generated/numpy.savez.html) 28 | archive. 29 | 30 | The following arrays inside the `.npz` define a mesh: 31 | 32 | * `vertices`, float (N, 3): 33 | The 3D positions of the vertices. 34 | N varies across the meshes. 35 | * `faces`, int (20000, 3): 36 | The vertex indices defining the faces in 3D space (i.e. triplets of indices 37 | into the `vertices` array). Fixed number of faces (20000) for all meshes. 38 | * `texcoords`, float (Nt, 2): 39 | The 2D positions of the vertices in the texture atlas (Nt > N). 40 | * `texcoords_indices`, int (20000, 3): 41 | The vertex indices defining the faces in the UV space (2D texture image) 42 | (i.e. triplets of indices into the `texcoords` array). Fixed number of 43 | faces (20000) for all meshes 44 | * `texture`, uint8 (2048, 2048, 3): 45 | The RGB texture image. 46 | 47 | Fields not described above should not be relied upon. 48 | 49 | The mesh can be loaded with [`numpy.load`][np.load]. 50 | For example: 51 | 52 | ```python 53 | import numpy as np 54 | mesh = np.load("name.npz", allow_pickle=True) 55 | mesh["vertices"] 56 | mesh["faces"] 57 | # ... 58 | ``` 59 | 60 | [np.load]: https://numpy.org/doc/stable/reference/generated/numpy.load.html 61 | 62 | ## Body landmarks 63 | 64 | 3D positions of detected body landmarks are provided 65 | in the training data of [Challenge 1](challenge_1.md). 66 | They are stored in files with name `landmarks3d.txt`. 67 | The format is plain text and tabular, with one landmark per row and one space 68 | between columns: 69 | 70 | ``` 71 | name x y z 72 | ``` 73 | 74 | with `name`, the name of the landmark, and `(x, y, z)`, its 3D position in 75 | the frame of reference of the scan or mesh. 76 | If the landmark was not detected, the coordinates are `nan`. 77 | 78 | For example, 79 | 80 | ``` 81 | elbow_left 1.234 0.123 0.389 82 | finger_thumb_top_left nan nan nan 83 | ... 84 | ``` 85 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/doc/landmark_names.txt: -------------------------------------------------------------------------------- 1 | ankle_left 2 | ankle_right 3 | ear_left 4 | ear_right 5 | elbow_left 6 | elbow_right 7 | eye_left 8 | eye_right 9 | finger_baby_base_left 10 | finger_baby_base_right 11 | finger_baby_middle_left 12 | finger_baby_middle_right 13 | finger_baby_tip_left 14 | finger_baby_tip_right 15 | finger_baby_top_left 16 | finger_baby_top_right 17 | finger_index_base_left 18 | finger_index_base_right 19 | finger_index_middle_left 20 | finger_index_middle_right 21 | finger_index_tip_left 22 | finger_index_tip_right 23 | finger_index_top_left 24 | finger_index_top_right 25 | finger_mean_base_knuckle_left 26 | finger_mean_base_knuckle_right 27 | finger_middle_base_left 28 | finger_middle_base_right 29 | finger_middle_middle_left 30 | finger_middle_middle_right 31 | finger_middle_tip_left 32 | finger_middle_tip_right 33 | finger_middle_top_left 34 | finger_middle_top_right 35 | finger_ring_base_left 36 | finger_ring_base_right 37 | finger_ring_middle_left 38 | finger_ring_middle_right 39 | finger_ring_tip_left 40 | finger_ring_tip_right 41 | finger_ring_top_left 42 | finger_ring_top_right 43 | finger_thumb_base_left 44 | finger_thumb_base_right 45 | finger_thumb_middle_left 46 | finger_thumb_middle_right 47 | finger_thumb_tip_left 48 | finger_thumb_tip_right 49 | finger_thumb_top_left 50 | finger_thumb_top_right 51 | hand_base_left 52 | hand_base_right 53 | heel_left 54 | heel_right 55 | hip_left 56 | hip_middle 57 | hip_right 58 | knee_left 59 | knee_right 60 | neck 61 | nose 62 | shoulder_left 63 | shoulder_right 64 | toe_1_left 65 | toe_1_right 66 | toe_5_left 67 | toe_5_right 68 | wrist_left 69 | wrist_right 70 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/readme.md: -------------------------------------------------------------------------------- 1 | # SHARP: SHApe Recovery from Partial textured 3D scans 2 | 3 | [SHARP 2020 website](https://cvi2.uni.lu/sharp2020/) 4 | 5 | A workshop and challenge in conjunction with 6 | [ECCV 2020](https://eccv2020.eu/workshops/). 7 | 8 | Organised by [CVI²](https://cvi2.uni.lu) & [Artec3D](https://www.artec3d.com). 9 | 10 | ## Overview 11 | 12 | The goal of the competition is to recover a reference textured scan `Y` from a 13 | partial version of it `X`. 14 | This is a *completion task* of 3D shape and texture. 15 | 16 | The data consists in pairs `(X, Y)` generated from two 17 | [datasets](doc/datasets.md) of 3D textured scans: 18 | 19 | 1. [3DBodyTex 2](doc/dataset_3dbodytex2.md): a dataset of 3D human scans 20 | (extending [3DBodyTex](https://cvi2.uni.lu/datasets/)). 21 | 2. [3DObjectTex](doc/dataset_3dobjecttex.md): a dataset of 3D scans of generic 22 | objects. 23 | 24 | There are two challenges, one per dataset. 25 | 26 | The *ground-truth shapes*, `Y`, are the raw scans (or the fitted body models in 27 | a variant of the challenge on human scans). 28 | 29 | The *partial scans*, `X`, are generated synthetically from the ground-truth 30 | shapes, `Y`. 31 | The idea is to simulate the partial acquisition produced by a 3D scanner, e.g. 32 | a hand-held scanner. 33 | 34 | For *training*, only the ground-truth shapes, `Y`, are provided. 35 | The partial scans, `X`, are generated by the participants. 36 | Some [example routines](sharp/preprocess.py) are provided. 37 | Custom routines may be used if reported. 38 | 39 | For *evaluation*, a set of partial shapes, `X`, is provided to the 40 | participants. 41 | The corresponding ground-truth shapes, `Y`, are kept secret. 42 | The participants estimate a complete shape, `Y'`, from `X`. 43 | The submissions are evaluated quantitatively by computing the 44 | surface-to-surface distance between `Y` and `Y'`. 45 | 46 | 47 | ## Detailed instructions 48 | 49 | - [Challenge 1: Recovery of Human Body Scans](doc/challenge_1.md) 50 | - [Track 1: Recovery of large regions](doc/challenge_1_track_1.md) 51 | - [Track 2: Recovery of fine details](doc/challenge_1_track_2.md) 52 | - [Challenge 2: Recovery of Generic Object Scans](doc/challenge_2.md) 53 | - [Evaluation and metrics](doc/evaluation.md) 54 | - [Datasets](doc/datasets.md) 55 | - [Dataset splits](doc/dataset_splits.md) 56 | - [Data formats](doc/formats.md) 57 | - [Command line interface (CLI)](doc/cli.md) 58 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/requirements.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | numpy 3 | pathlib 4 | moderngl 5 | opencv-python 6 | scipy 7 | nptyping -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/scripts/demo_holes: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generate partial data with the hole method with different configurations. 3 | 4 | (( $# != 1 )) && { echo "usage: $0 MESH"; exit 1; } 5 | 6 | mesh=$1 7 | 8 | out=${mesh%.*}-holes-h_20-d_1.npz 9 | python -m sharp shoot $mesh $out --holes 20 --dropout 0.01 10 | printf . 11 | out=${mesh%.*}-holes-h_40-d_1.npz 12 | python -m sharp shoot $mesh $out --holes 40 --dropout 0.01 13 | printf . 14 | out=${mesh%.*}-holes-h_60-d_1.npz 15 | python -m sharp shoot $mesh $out --holes 60 --dropout 0.01 16 | printf . 17 | 18 | out=${mesh%.*}-holes-h_20-d_2.npz 19 | python -m sharp shoot $mesh $out --holes 20 --dropout 0.02 20 | printf . 21 | out=${mesh%.*}-holes-h_40-d_2.npz 22 | python -m sharp shoot $mesh $out --holes 40 --dropout 0.02 23 | printf . 24 | out=${mesh%.*}-holes-h_60-d_2.npz 25 | python -m sharp shoot $mesh $out --holes 60 --dropout 0.02 26 | printf . 27 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("readme.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="sharpcvi2", 8 | version="1.0.0", 9 | author="CVI2: Computer Vision, Imaging and Machine Intelligence Research Group", 10 | author_email="shapify3D@uni.lu", 11 | description="Routines for the SHARP Challenge, ECCV 2020", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://cvi2.uni.lu/sharp2020/", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "Operating System :: OS Independent", 19 | ], 20 | python_requires='>=3.6', 21 | ) 22 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/data_processing/eccv2020-sharp-workshop/sharp/__init__.py -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import concurrent.futures 3 | import itertools 4 | import logging 5 | import pathlib 6 | import sys 7 | 8 | import numpy as np 9 | 10 | from . import data 11 | from . import utils 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def _do_convert(args): 18 | mesh = data.load_mesh(args.input) 19 | data.save_mesh(args.output, mesh) 20 | 21 | 22 | def _do_shoot(args): 23 | mesh = data.load_mesh(str(args.input)) 24 | 25 | has_min_holes = args.min_holes is not None 26 | has_max_holes = args.max_holes is not None 27 | if has_min_holes != has_max_holes: 28 | raise ValueError("--min-holes and --max-holes must be set together") 29 | n_holes = ((args.min_holes, args.max_holes) 30 | if has_min_holes 31 | else args.holes) 32 | 33 | has_min_dropout = args.min_dropout is not None 34 | has_max_dropout = args.max_dropout is not None 35 | if has_min_dropout != has_max_dropout: 36 | raise ValueError( 37 | "--min-dropout and --max-dropout must be set together") 38 | dropout = ((args.min_dropout, args.max_dropout) 39 | if has_min_dropout 40 | else args.dropout) 41 | 42 | mask_faces = (np.load(args.mask) if args.mask is not None 43 | else None) 44 | faces = None if mask_faces is None else mesh.faces 45 | point_indices = utils.shoot_holes(mesh.vertices, 46 | n_holes, 47 | dropout, 48 | mask_faces=mask_faces, 49 | faces=faces) 50 | shot = utils.remove_points(mesh, point_indices) 51 | shot.save(str(args.output)) 52 | 53 | 54 | def identify_meshes(dir_): 55 | """List meshes and identify the challenge/track in a directory tree.""" 56 | meshes_track1 = list(dir_.glob("**/*_normalized.npz")) 57 | meshes_track2 = list(dir_.glob("**/fusion_textured.npz")) 58 | meshes_challenge2 = list(dir_.glob("**/model_*.obj")) 59 | if meshes_track1: 60 | meshes = sorted(meshes_track1) 61 | challenge = 1 62 | track = 1 63 | elif meshes_track2: 64 | meshes = sorted(meshes_track2) 65 | challenge = 1 66 | track = 2 67 | elif meshes_challenge2: 68 | meshes = sorted(meshes_challenge2) 69 | challenge = 2 70 | track = None 71 | else: 72 | meshes = [] 73 | challenge = None 74 | track = None 75 | 76 | return meshes, challenge, track 77 | 78 | 79 | def shoot_helper(mesh_index, 80 | path, 81 | n_holes, 82 | dropout, 83 | shape_seeds, 84 | n_meshes, 85 | input_dir, 86 | output_dir, 87 | mask_dir=None, 88 | ): 89 | logger.info(f"{mesh_index + 1}/{n_meshes} -------- TEST ------------ processing {path}") 90 | 91 | rel_path = path.relative_to(input_dir) 92 | 93 | # Lazily load the mesh and mask when it is sure they are needed. 94 | mesh_cached = None 95 | mask_cached = None 96 | 97 | def make_name_suffix(shape_index, n_shapes): 98 | if n_shapes == 1: 99 | return "partial" 100 | else: 101 | # Assuming 0 <= shape_index <= 99. 102 | return f"partial-{shape_index:02d}" 103 | 104 | def load_mask(rel_path): 105 | mask_name = f"{rel_path.stem}-mask.npy" 106 | mask_rel_path = rel_path.with_name(mask_name) 107 | mask_path = mask_dir / mask_rel_path 108 | logger.info(f"loading mask {mask_path}") 109 | mask_faces = np.load(mask_path) 110 | return mask_faces 111 | 112 | n_shapes = len(shape_seeds) 113 | for shape_index, shape_seed in enumerate(shape_seeds): 114 | out_name_suffix = make_name_suffix(shape_index, n_shapes) 115 | out_name = f"{rel_path.stem}-{out_name_suffix}.npz" 116 | out_rel_path = rel_path.with_name(out_name) 117 | out_path = output_dir / out_rel_path 118 | 119 | if out_path.exists(): 120 | logger.warning(f"shape exists, skipping {out_path}") 121 | continue 122 | 123 | logger.info(f"generating shape {shape_index + 1}/{n_shapes}") 124 | 125 | out_path.parent.mkdir(parents=True, exist_ok=True) 126 | 127 | if mesh_cached is None: 128 | mesh_cached = data.load_mesh(str(path)) 129 | if mask_cached is None and mask_dir is not None: 130 | mask_cached = load_mask(rel_path) 131 | 132 | mesh = mesh_cached 133 | mask = mask_cached 134 | 135 | logger.info(f"TEST shape seed = {shape_seed}") 136 | shape_rng = np.random.default_rng(shape_seed) 137 | point_indices = utils.shoot_holes(mesh.vertices, 138 | n_holes, 139 | dropout, 140 | mask_faces=mask, 141 | faces=mesh.faces, 142 | rng=shape_rng) 143 | partial = utils.remove_points(mesh, point_indices) 144 | 145 | logger.info(f"{shape_index + 1}/{n_shapes} saving {out_path}") 146 | partial.save(str(out_path)) 147 | 148 | 149 | def _do_shoot_dir(args): 150 | """Generate partial data in a directory tree of meshes. 151 | 152 | An independent PRNG is used for each partial shape. Independent seeds are 153 | created in advance for each partial shape. The process is thus reproducible 154 | and can also be interrupted and resumed without generating all the previous 155 | shapes. 156 | """ 157 | input_dir = args.input_dir 158 | output_dir = args.output_dir 159 | mask_dir = args.mask_dir 160 | seed = args.seed 161 | n_holes = args.holes 162 | dropout = args.dropout 163 | n_shapes = args.n_shapes 164 | n_workers = args.n_workers 165 | 166 | logger.info("generating partial data in directory tree") 167 | logger.info(f"input dir = {input_dir}") 168 | logger.info(f"output dir = {output_dir}") 169 | logger.info(f"mask dir = {mask_dir}") 170 | logger.info(f"seed = {seed}") 171 | logger.info(f"holes = {n_holes}") 172 | logger.info(f"dropout = {dropout}") 173 | logger.info(f"n_shapes = {n_shapes}") 174 | logger.info(f"n_workers = {n_workers}") 175 | 176 | mesh_paths, challenge, track = identify_meshes(input_dir) 177 | if challenge is None: 178 | raise ValueError(f"could not identify meshes in {input_dir}") 179 | logger.info(f"detected challenge {challenge} track {track}") 180 | n_meshes = len(mesh_paths) 181 | logger.info(f"found {n_meshes} meshes") 182 | 183 | logger.info(f"setting random seed {seed}") 184 | rng = np.random.default_rng(seed) 185 | 186 | # Create in advance the individual initial random states for each partial 187 | # shape to be generated so that the process of generation can be 188 | # - resumed without regenerating the previous shapes, 189 | # - parallelised. 190 | seeds = rng.integers(1e12, size=(n_meshes, n_shapes)) 191 | 192 | mesh_indices = range(n_meshes) 193 | with concurrent.futures.ProcessPoolExecutor(max_workers=n_workers) as executor: 194 | executor.map( 195 | shoot_helper, 196 | mesh_indices, 197 | mesh_paths, 198 | itertools.repeat(n_holes), 199 | itertools.repeat(dropout), 200 | seeds.tolist(), 201 | itertools.repeat(n_meshes), 202 | itertools.repeat(input_dir), 203 | itertools.repeat(output_dir), 204 | itertools.repeat(mask_dir), 205 | ) 206 | 207 | 208 | def _parse_args(): 209 | parser = argparse.ArgumentParser() 210 | subparsers = parser.add_subparsers() 211 | 212 | parser_convert = subparsers.add_parser( 213 | "convert", 214 | help="Convert between mesh formats.", 215 | ) 216 | parser_convert.add_argument("input", type=pathlib.Path) 217 | parser_convert.add_argument("output", type=pathlib.Path) 218 | parser_convert.set_defaults(func=_do_convert) 219 | 220 | parser_shoot = subparsers.add_parser( 221 | "shoot", 222 | help="Generate partial data with the shooting method.", 223 | ) 224 | parser_shoot.add_argument("input", type=pathlib.Path) 225 | parser_shoot.add_argument("output", type=pathlib.Path) 226 | parser_shoot.add_argument( 227 | "--holes", type=int, default=40, 228 | help="Number of holes to shoot.", 229 | ) 230 | parser_shoot.add_argument( 231 | "--min-holes", type=int, default=None, 232 | help="Minimum number of holes to generate." 233 | " (Supersedes --holes and requires --max-holes.)", 234 | ) 235 | parser_shoot.add_argument( 236 | "--max-holes", type=int, default=None, 237 | help="Maximum number of holes to generate." 238 | " (Supersedes --holes and requires --min-holes.)", 239 | ) 240 | parser_shoot.add_argument( 241 | "--dropout", type=float, default=2e-2, 242 | help="Proportion of points of the mesh to remove in a single hole.", 243 | ) 244 | parser_shoot.add_argument( 245 | "--min-dropout", type=float, default=None, 246 | help="Minimum proportion of points of the mesh to remove in a single " 247 | "hole." 248 | " (Supersedes --dropout and requires --max-dropout.)", 249 | ) 250 | parser_shoot.add_argument( 251 | "--max-dropout", type=float, default=None, 252 | help="Maximum proportion of points of the mesh to remove in a single " 253 | "hole." 254 | " (Supersedes --dropout and requires --min-dropout.)", 255 | ) 256 | parser_shoot.add_argument( 257 | "--mask", type=pathlib.Path, 258 | help=" (optional) Path to the mask (.npy) to generate holes only on" 259 | " regions considered for evaluation.", 260 | ) 261 | parser_shoot.set_defaults(func=_do_shoot) 262 | 263 | parser_shoot_dir = subparsers.add_parser( 264 | "shoot_dir", 265 | help="Generate partial data with the shooting method for a directory" 266 | " tree of meshes." 267 | ) 268 | parser_shoot_dir.add_argument("input_dir", type=pathlib.Path) 269 | parser_shoot_dir.add_argument("output_dir", type=pathlib.Path) 270 | parser_shoot_dir.add_argument( 271 | "--mask-dir", 272 | type=pathlib.Path, 273 | help=" (optional) Directory tree with the masks (.npy). If defined," 274 | " the partial data is created only on the non-masked faces of the" 275 | " meshes. (Only valid for challenge 1.)", 276 | ) 277 | parser_shoot_dir.add_argument( 278 | "--holes", 279 | type=int, 280 | default=40, 281 | help="Number of holes to shoot.", 282 | ) 283 | parser_shoot_dir.add_argument( 284 | "--dropout", 285 | type=float, 286 | default=2e-2, 287 | help="Proportion of points of the mesh to remove in a single hole.", 288 | ) 289 | parser_shoot_dir.add_argument( 290 | "--seed", 291 | type=int, 292 | default=None, 293 | help="Initial state for the pseudo random number generator." 294 | " If not set, the initial state is not set explicitly.", 295 | ) 296 | parser_shoot_dir.add_argument( 297 | "-n", "--n-shapes", 298 | type=int, 299 | default=1, 300 | help="Number of partial shapes to generate per mesh." 301 | " If n = 1 (default), the shape is saved as" 302 | " '-partial.npz'." 303 | " If n > 1, the shapes are saved as '-partial-XY.npz'" 304 | ", with XY as 00, 01, 02... (assuming n <= 99).", 305 | ) 306 | parser_shoot_dir.add_argument( 307 | "--n-workers", 308 | type=int, 309 | default=None, 310 | help="Number of parallel processes. By default, the number of" 311 | " available processors.", 312 | ) 313 | parser_shoot_dir.set_defaults(func=_do_shoot_dir) 314 | 315 | args = parser.parse_args() 316 | 317 | # Ensure the help message is displayed when no command is provided. 318 | if "func" not in args: 319 | parser.print_help() 320 | sys.exit(1) 321 | 322 | return args 323 | 324 | 325 | def main(): 326 | args = _parse_args() 327 | args.func(args) 328 | 329 | 330 | if __name__ == "__main__": 331 | logging.basicConfig(level=logging.INFO) 332 | main() 333 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import pathlib 3 | import re 4 | 5 | import cv2 6 | import numpy as np 7 | 8 | 9 | def read_3d_landmarks(inpath): 10 | """Read the 3d landmarks locations and names. 11 | 12 | Landmarks are stored in a CSV file with rows of the form: 13 | landmark_name x y z 14 | 15 | Parameters 16 | ---------- 17 | inpath : string 18 | path to the file 19 | 20 | Returns 21 | ------- 22 | landmarks: dict 23 | A dictionary of (landmark_name, 3d_location) pairs. 24 | """ 25 | landmarks = {} 26 | with open(inpath) as f: 27 | reader = csv.reader(f, delimiter=' ') 28 | for row in reader: 29 | name = row[0] 30 | location = np.array([float(v) for v in row[1:4]]) 31 | landmarks[name] = location 32 | return landmarks 33 | 34 | 35 | def imread(path): 36 | """Read a color image. 37 | 38 | Return an image in RGB format with float values into [0, 1]. 39 | """ 40 | img = cv2.imread(str(path)) 41 | # Convert to RGB. OpenCV returns BGR. 42 | img = img[..., ::-1] 43 | return img.astype(float) / np.iinfo(img.dtype).max 44 | 45 | 46 | def imwrite(path, img, dtype=np.uint8): 47 | """Save an RGB image to a file. 48 | 49 | Expect float values into [0, 1]. 50 | """ 51 | img = (img * np.iinfo(dtype).max).astype(dtype) 52 | # OpenCV expects BGR. 53 | img = img[..., ::-1] 54 | cv2.imwrite(str(path), img) 55 | 56 | 57 | def load_mesh(path): 58 | if str(path).endswith(".obj"): 59 | return load_obj(path) 60 | elif str(path).endswith(".npz"): 61 | return load_npz(path) 62 | raise ValueError(f"unknown mesh format {path}") 63 | 64 | 65 | def save_mesh(path, mesh): 66 | if str(path).endswith(".obj"): 67 | return save_obj(path, mesh) 68 | elif str(path).endswith(".npz"): 69 | return save_npz(path, mesh) 70 | raise ValueError(f"unknown mesh format for {path}") 71 | 72 | 73 | class Mesh: 74 | 75 | def __init__(self, path=None, 76 | vertices=None, vertex_normals=None, vertex_colors=None, 77 | faces=None, face_normals=None, faces_normal_indices=None, 78 | normals=None, 79 | texcoords=None, texture_indices=None, texture=None, 80 | material=None, mask_faces=None): 81 | self.path = path 82 | self.vertices = vertices 83 | self.vertex_normals = vertex_normals 84 | self.vertex_colors = vertex_colors 85 | self.faces = faces 86 | self.face_normals = face_normals 87 | self.faces_normal_indices = faces_normal_indices 88 | self.normals = normals 89 | self.texcoords = texcoords 90 | self.texture_indices = texture_indices 91 | self.texture = texture 92 | self.material = material 93 | self.mask_faces = mask_faces 94 | 95 | @staticmethod 96 | def load(path): 97 | return load_mesh(path) 98 | 99 | def save(self, path): 100 | save_mesh(path, self) 101 | 102 | 103 | def _read_mtl(mtl_path): 104 | if not mtl_path.exists(): 105 | return None 106 | with open(mtl_path) as f: 107 | for line in f: 108 | tokens = line.strip().split(maxsplit=1) 109 | if not tokens: 110 | continue 111 | if tokens[0] == 'map_Kd': 112 | texture_name = tokens[1] 113 | return texture_name 114 | return None 115 | 116 | 117 | def _parse_faces(obj_faces): 118 | """Parse the OBJ encoding of the face attribute index buffers. 119 | 120 | A value of -1 in the index array of the texture means the index is 121 | undefined (i.e. no texture is mapped to this face corner). 122 | 123 | Returns: 124 | faces 125 | faces_texture 126 | faces_normals 127 | """ 128 | face_pattern = re.compile(r'(\d+)/?(\d*)?/?(\d*)?') 129 | faces = [] 130 | faces_normals = [] 131 | faces_texture = [] 132 | for obj_face in obj_faces: 133 | fv, ft, fn = [], [], [] 134 | arrs = (fv, ft, fn) 135 | for element in obj_face: 136 | match = face_pattern.match(element) 137 | if match is not None: 138 | for i, group in enumerate(match.groups()): 139 | if len(group) > 0: 140 | arrs[i].append(int(group)) 141 | if len(fv) > 0: 142 | faces.append(fv) 143 | if len(ft) > 0: 144 | faces_texture.append(ft) 145 | else: 146 | el = [-1 for x in range(len(fv))] 147 | faces_texture.append(el) 148 | if len(fn) > 0: 149 | faces_normals.append(fn) 150 | # else: 151 | # el = ['' for x in range(len(fv))] 152 | # faces_normals.append(el) 153 | 154 | faces = np.array(faces, dtype=int) 155 | faces_texture = np.array(faces_texture, dtype=int) 156 | faces_normals = np.array(faces_normals, dtype=int) 157 | 158 | # Change to zero-based indexing. 159 | faces -= 1 160 | faces_texture -= 1 161 | faces_normals -= 1 162 | 163 | return faces, faces_texture, faces_normals 164 | 165 | 166 | def _complete_texcoords(texcoords, texture_indices): 167 | """Ensure all faces reference some texture coordinates. 168 | 169 | Make untextured faces reference a new dummy texture coordinate. 170 | The new texture coordinate is placed at `(u, v) = (0, 0)`, assuming no 171 | texture exists there (i.e. black color). 172 | """ 173 | no_texcoords = texture_indices < 0 174 | if np.any(no_texcoords): 175 | texcoords = np.append(texcoords, [[.0, .0]], axis=0) 176 | new_index = len(texcoords) - 1 177 | texture_indices[no_texcoords] = new_index 178 | return texcoords, texture_indices 179 | 180 | 181 | def _load_texture(path): 182 | return imread(path) 183 | 184 | 185 | def _save_texture(path, texture): 186 | imwrite(path, texture) 187 | 188 | 189 | def load_obj(path): 190 | path = pathlib.Path(path) 191 | 192 | vertices = [] 193 | vertex_colors = [] 194 | normals = [] 195 | faces = [] 196 | texcoords = [] 197 | material_name = None 198 | mtl_filename = None 199 | texture_filename = None 200 | texture = None 201 | with open(path) as f: 202 | for line in f: 203 | line = line.strip() 204 | 205 | if not line: 206 | # blank line 207 | continue 208 | if line.startswith('#'): 209 | # comment 210 | continue 211 | 212 | tokens = line.split() 213 | if tokens[0] == 'v': 214 | vertices.append([float(x) for x in tokens[1:]]) 215 | elif tokens[0] == 'vn': 216 | normals.append([float(x) for x in tokens[1:]]) 217 | elif tokens[0] == 'f': 218 | faces.append(tokens[1:]) 219 | elif tokens[0] == 'vt': 220 | texcoords.append([float(tokens[1]), float(tokens[2])]) 221 | elif tokens[0] in ('usemtl', 'usemat'): 222 | material_name = tokens[1] 223 | elif tokens[0] == 'mtllib': 224 | if len(tokens) > 1: 225 | mtl_filename = tokens[1] 226 | mtl_path = path.parent / mtl_filename 227 | texture_filename = _read_mtl(mtl_path) 228 | 229 | vertices = np.array(vertices) 230 | if vertices.shape[1] == 6: 231 | vertex_colors = vertices[:, 3:] 232 | vertices = vertices[:, :3] 233 | 234 | faces, texture_indices, faces_normal_indices = _parse_faces(faces) 235 | 236 | if texcoords: 237 | texcoords = np.asarray(texcoords, dtype=float) 238 | texture_indices = np.asarray(texture_indices, dtype=int) 239 | texcoords, texture_indices = _complete_texcoords(texcoords, 240 | texture_indices) 241 | else: 242 | texture_indices = [] 243 | 244 | if normals: 245 | normals = np.array(normals) 246 | 247 | if texture_filename is not None: 248 | texture_path = path.parent / texture_filename 249 | if texture_path.exists(): 250 | texture = _load_texture(texture_path) 251 | 252 | return Mesh( 253 | path=path, 254 | vertices=vertices, 255 | vertex_colors=vertex_colors if len(vertex_colors) > 0 else None, 256 | faces=faces, 257 | faces_normal_indices=(faces_normal_indices 258 | if len(faces_normal_indices) > 0 259 | else None), 260 | normals=normals if len(normals) > 0 else None, 261 | texcoords=texcoords if len(texcoords) > 0 else None, 262 | texture=texture, 263 | texture_indices=(texture_indices 264 | if len(texture_indices) > 0 265 | else None), 266 | material=material_name, 267 | ) 268 | 269 | 270 | def _save_mtl(path, texture_name=None): 271 | if texture_name is not None: 272 | texture_command = f"map_Kd {texture_name}" 273 | else: 274 | texture_command = "" 275 | 276 | content = f"""\ 277 | newmtl material_0 278 | Ka 1 1 1 279 | Kd 1 1 1 280 | Ks 1 1 1 281 | Ns 1000 282 | {texture_command} 283 | newmtl material_1 284 | Ka 1 1 1 285 | Kd 1 1 1 286 | Ks 1 1 1 287 | Ns 1000 288 | """ 289 | 290 | with open(path, 'w') as mtl_file: 291 | mtl_file.write(content) 292 | 293 | 294 | def save_obj(path, mesh, save_texture=True): 295 | path = pathlib.Path(path) 296 | out_dir = pathlib.Path(path).parent 297 | out_dir.mkdir(parents=True, exist_ok=True) 298 | 299 | # Write the texture image file. 300 | if save_texture and mesh.texture is not None: 301 | texture_path = path.with_suffix(".png") 302 | texture_name = texture_path.name 303 | _save_texture(texture_path, mesh.texture) 304 | else: 305 | texture_path = None 306 | texture_name = None 307 | 308 | # Write the mtl file. 309 | mtlpath = path.with_suffix(".mtl") 310 | mtlname = mtlpath.name 311 | _save_mtl(mtlpath, texture_name=texture_name) 312 | 313 | # Write the obj file. 314 | with open(path, 'w') as f: 315 | f.write(f"mtllib {mtlname}\n") 316 | f.write("usemtl material_0\n") 317 | 318 | if mesh.vertex_colors is not None: 319 | for vertex, color in zip(mesh.vertices, mesh.vertex_colors): 320 | f.write("v {} {} {} {} {} {}\n".format(*vertex, *color)) 321 | else: 322 | for vertex in mesh.vertices: 323 | f.write("v {} {} {}\n".format(*vertex)) 324 | 325 | if mesh.vertex_normals is not None: 326 | for normal in mesh.vertex_normals: 327 | f.write("vn {} {} {}\n".format(*normal)) 328 | 329 | if mesh.texcoords is not None: 330 | for coords in mesh.texcoords: 331 | f.write("vt {} {}\n".format(*coords)) 332 | 333 | def _interleave_columns(*arrays): 334 | n_rows = len(arrays[0]) 335 | return np.dstack(arrays).reshape(n_rows, -1) 336 | 337 | faces = mesh.faces + 1 338 | texture_indices = (mesh.texture_indices + 1 339 | if mesh.texture_indices is not None 340 | else None) 341 | normal_indices = (mesh.faces_normal_indices + 1 342 | if mesh.faces_normal_indices is not None 343 | else None) 344 | 345 | if texture_indices is None and normal_indices is None: 346 | for face in faces: 347 | f.write("f {} {} {}".format(*face)) 348 | elif normal_indices is None: 349 | indices = _interleave_columns(faces, texture_indices) 350 | for indices_ in indices: 351 | f.write("f {}/{} {}/{} {}/{}\n".format(*indices_)) 352 | elif texture_indices is None: 353 | indices = _interleave_columns(faces, normal_indices) 354 | for indices_ in indices: 355 | f.write("f {}//{} {}//{} {}//{}\n".format(*indices_)) 356 | 357 | 358 | def astype_or_none(array, type_): 359 | if array is None: 360 | return None 361 | return array.astype(type_) 362 | 363 | 364 | def load_npz(path): 365 | data = np.load(path) 366 | 367 | assert data.f.texture.dtype == np.uint8 368 | texture = data.f.texture.astype(float) / 255 369 | 370 | return Mesh( 371 | path=path, 372 | vertices=data.f.vertices.astype(float), 373 | faces=data.f.faces.astype(int), 374 | texcoords=data.f.texcoords.astype(float), 375 | texture_indices=data.f.texcoords_indices.astype(int), 376 | texture=texture, 377 | ) 378 | 379 | 380 | def save_npz(path, mesh): 381 | np.savez_compressed( 382 | path, 383 | vertices=mesh.vertices.astype("float32"), 384 | faces=mesh.faces.astype("uint32"), 385 | texcoords=astype_or_none(mesh.texcoords, "float32"), 386 | texcoords_indices=astype_or_none(mesh.texture_indices, "uint32"), 387 | texture=((255 * mesh.texture).astype("uint8") 388 | if mesh.texture is not None 389 | else None), 390 | mask_faces=astype_or_none(mesh.mask_faces, "uint32"), 391 | vertex_colors=astype_or_none(mesh.vertex_colors, "float32"), 392 | ) 393 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/landmarks.py: -------------------------------------------------------------------------------- 1 | NAMES = [ 2 | 'nose', 3 | 'neck', 4 | 'shoulder_right', 5 | 'elbow_right', 6 | 'wrist_right', 7 | 'shoulder_left', 8 | 'elbow_left', 9 | 'wrist_left', 10 | 'hip_middle', 11 | 'hip_right', 12 | 'knee_right', 13 | 'ankle_right', 14 | 'hip_left', 15 | 'knee_left', 16 | 'ankle_left', 17 | 'eye_right', 18 | 'eye_left', 19 | 'ear_right', 20 | 'ear_left', 21 | 'toe_1_left', 22 | 'toe_5_left', 23 | 'heel_left', 24 | 'toe_1_right', 25 | 'toe_5_right', 26 | 'heel_right', 27 | 'hand_base_left', 28 | 'finger_thumb_base_left', 29 | 'finger_thumb_middle_left', 30 | 'finger_thumb_top_left', 31 | 'finger_thumb_tip_left', 32 | 'finger_index_base_left', 33 | 'finger_index_middle_left', 34 | 'finger_index_top_left', 35 | 'finger_index_tip_left', 36 | 'finger_middle_base_left', 37 | 'finger_middle_middle_left', 38 | 'finger_middle_top_left', 39 | 'finger_middle_tip_left', 40 | 'finger_ring_base_left', 41 | 'finger_ring_middle_left', 42 | 'finger_ring_top_left', 43 | 'finger_ring_tip_left', 44 | 'finger_baby_base_left', 45 | 'finger_baby_middle_left', 46 | 'finger_baby_top_left', 47 | 'finger_baby_tip_left', 48 | 'hand_base_right', 49 | 'finger_thumb_base_right', 50 | 'finger_thumb_middle_right', 51 | 'finger_thumb_top_right', 52 | 'finger_thumb_tip_right', 53 | 'finger_index_base_right', 54 | 'finger_index_middle_right', 55 | 'finger_index_top_right', 56 | 'finger_index_tip_right', 57 | 'finger_middle_base_right', 58 | 'finger_middle_middle_right', 59 | 'finger_middle_top_right', 60 | 'finger_middle_tip_right', 61 | 'finger_ring_base_right', 62 | 'finger_ring_middle_right', 63 | 'finger_ring_top_right', 64 | 'finger_ring_tip_right', 65 | 'finger_baby_base_right', 66 | 'finger_baby_middle_right', 67 | 'finger_baby_top_right', 68 | 'finger_baby_tip_right', 69 | 'finger_mean_base_knuckle_left', 70 | 'finger_mean_base_knuckle_right', 71 | ] 72 | 73 | 74 | NAMES_HAND_RIGHT = [ 75 | 'wrist_right', 76 | 'hand_base_right', 77 | 'finger_thumb_base_right', 78 | 'finger_thumb_middle_right', 79 | 'finger_thumb_top_right', 80 | 'finger_thumb_tip_right', 81 | 'finger_index_base_right', 82 | 'finger_index_middle_right', 83 | 'finger_index_top_right', 84 | 'finger_index_tip_right', 85 | 'finger_middle_base_right', 86 | 'finger_middle_middle_right', 87 | 'finger_middle_top_right', 88 | 'finger_middle_tip_right', 89 | 'finger_ring_base_right', 90 | 'finger_ring_middle_right', 91 | 'finger_ring_top_right', 92 | 'finger_ring_tip_right', 93 | 'finger_baby_base_right', 94 | 'finger_baby_middle_right', 95 | 'finger_baby_top_right', 96 | 'finger_baby_tip_right', 97 | 'finger_mean_base_knuckle_right', 98 | 'finger_mean_base_knuckle_right', 99 | ] 100 | 101 | 102 | NAMES_HAND_LEFT = [ 103 | 'wrist_left', 104 | 'hand_base_left', 105 | 'finger_thumb_base_left', 106 | 'finger_thumb_middle_left', 107 | 'finger_thumb_top_left', 108 | 'finger_thumb_tip_left', 109 | 'finger_index_base_left', 110 | 'finger_index_middle_left', 111 | 'finger_index_top_left', 112 | 'finger_index_tip_left', 113 | 'finger_middle_base_left', 114 | 'finger_middle_middle_left', 115 | 'finger_middle_top_left', 116 | 'finger_middle_tip_left', 117 | 'finger_ring_base_left', 118 | 'finger_ring_middle_left', 119 | 'finger_ring_top_left', 120 | 'finger_ring_tip_left', 121 | 'finger_baby_base_left', 122 | 'finger_baby_middle_left', 123 | 'finger_baby_top_left', 124 | 'finger_baby_tip_left', 125 | 'finger_mean_base_knuckle_left', 126 | 'finger_mean_base_knuckle_left', 127 | ] 128 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/linalg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def squared_norm(x, axis=None, keepdims=False): 5 | """Squared Euclidean norm of an array.""" 6 | return (x ** 2).sum(axis=axis, keepdims=keepdims) 7 | 8 | 9 | def norm(x, axis=None, keepdims=False): 10 | """Euclidean norm of an array.""" 11 | return np.sqrt(squared_norm(x, axis=axis, keepdims=keepdims)) 12 | 13 | 14 | def normed(x, axis=None, keepdims=False): 15 | """Normalize an array.""" 16 | eps = np.finfo(x.dtype).eps 17 | return x / (norm(x, axis=axis, keepdims=True) + eps) 18 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/trirender.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Tuple 2 | import numpy as np 3 | from nptyping import NDArray 4 | import moderngl as MGL 5 | 6 | 7 | VERTEX_SHADER = """ 8 | #version 330 9 | 10 | in vec2 point_uv; 11 | out vec2 fragment_uv; 12 | 13 | void main() { 14 | gl_Position = vec4(point_uv.x * 2.0 - 1.0, point_uv.y * 2.0 - 1.0, 0.0, 1.0); 15 | fragment_uv = point_uv; 16 | } 17 | """ 18 | 19 | FRAGMENT_SHADER = """ 20 | #version 330 21 | 22 | in vec2 fragment_uv; 23 | out vec4 fragment_color; 24 | 25 | uniform sampler2D texture_color; 26 | 27 | void main() { 28 | vec3 rgb = texture(texture_color, fragment_uv).rgb; 29 | fragment_color = vec4(rgb, 1.0); 30 | } 31 | 32 | """ 33 | 34 | 35 | class UVTrianglesRenderer: 36 | def __init__(self, ctx: MGL.Context, output_size: Tuple[int, int]): 37 | self.ctx = ctx 38 | self.output_size = output_size 39 | self.shader = self.ctx.program( 40 | vertex_shader=VERTEX_SHADER, fragment_shader=FRAGMENT_SHADER 41 | ) 42 | self.fbo = self.ctx.framebuffer( 43 | self.ctx.renderbuffer(self.output_size, dtype="f4") 44 | ) 45 | self.background_color = (0, 0, 0) 46 | 47 | def __del__(self): 48 | self.ctx.release() 49 | self.fbo.release() 50 | self.shader.release() 51 | 52 | @staticmethod 53 | def with_standalone_ctx(output_size: Tuple[int, int]) -> "UVTrianglesRenderer": 54 | return UVTrianglesRenderer( 55 | MGL.create_standalone_context(require=330), output_size 56 | ) 57 | 58 | @staticmethod 59 | def with_window_ctx(output_size: Tuple[int, int]) -> "UVTrianglesRenderer": 60 | return UVTrianglesRenderer(MGL.create_context(require=330), output_size) 61 | 62 | def _init_ctx_object(self, tex_coords, tri_indices, texture): 63 | resources = [] 64 | tex_coords_buffer = self.ctx.buffer(tex_coords.astype("f4").tobytes()) 65 | resources.append(tex_coords_buffer) 66 | tri_indices_buffer = self.ctx.buffer(tri_indices.astype("i4").tobytes()) 67 | resources.append(tri_indices_buffer) 68 | 69 | texture_height = texture.shape[0] 70 | texture_width = texture.shape[1] 71 | if len(texture.shape) == 3: 72 | components = texture.shape[2] 73 | else: 74 | components = 1 75 | 76 | texture_object = self.ctx.texture( 77 | (texture_width, texture_height), 78 | components, 79 | texture.astype("f4").tobytes(), 80 | dtype="f4", 81 | ) 82 | 83 | resources.append(texture_object) 84 | 85 | content = (tex_coords_buffer, "2f4", "point_uv") 86 | self.shader["texture_color"] = 0 87 | texture_object.use(0) 88 | vao = self.ctx.vertex_array(self.shader, [content], tri_indices_buffer) 89 | resources.append(vao) 90 | 91 | return vao, resources 92 | 93 | def _render(self, vao): 94 | self.fbo.use() 95 | self.ctx.clear(*self.background_color) 96 | vao.render() 97 | self.ctx.finish() 98 | 99 | def _get_fbo_image(self): 100 | fbo_image = self.fbo.read(dtype="f4") 101 | fbo_image = np.frombuffer(fbo_image, dtype="f4").reshape( 102 | self.output_size[1], self.output_size[0], 3 103 | ) 104 | fbo_image = np.flipud(fbo_image) 105 | return fbo_image 106 | 107 | def render( 108 | self, 109 | tex_coords: NDArray[(Any, 2), float], 110 | tri_indices: NDArray[(Any, 3), int], 111 | texture: NDArray[(Any, Any, 3), float], 112 | flip_y: bool = True, 113 | ) -> NDArray[(Any, Any, 3), float]: 114 | assert isinstance(tex_coords, NDArray[(Any, 2), float]) 115 | assert isinstance(tri_indices, NDArray[(Any, 3), int]) 116 | assert isinstance(texture, NDArray[(Any, Any, 3), float]) 117 | 118 | if flip_y: 119 | texture = np.flipud(texture) 120 | 121 | resources = [] 122 | 123 | try: 124 | vao, resources = self._init_ctx_object(tex_coords, tri_indices, texture) 125 | self._render(vao) 126 | result = self._get_fbo_image() 127 | 128 | return result 129 | 130 | finally: 131 | for resource in resources: 132 | resource.release() 133 | -------------------------------------------------------------------------------- /data_processing/eccv2020-sharp-workshop/sharp/utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numbers 3 | 4 | import cv2 5 | import numpy as np 6 | try: 7 | from scipy.spatial import cKDTree as KDTree 8 | except ImportError: 9 | from scipy.spatial import KDTree 10 | 11 | from .trirender import UVTrianglesRenderer 12 | 13 | 14 | def slice_by_plane(mesh, center, n): 15 | c = np.dot(center, n) 16 | plane_side = lambda x: np.dot(x, n) >= c 17 | split = np.asarray([plane_side(v) for v in mesh.vertices]) 18 | slice1_indices = np.argwhere(split == True) 19 | slice2_indices = np.argwhere(split == False) 20 | return slice1_indices, slice2_indices 21 | 22 | 23 | def remove_points(mesh, indices, blackoutTexture=True): 24 | cpy = copy.deepcopy(mesh) 25 | cpy.vertices = np.delete(mesh.vertices, indices, axis=0) 26 | if mesh.vertex_colors is not None: 27 | cpy.vertex_colors = np.delete(mesh.vertex_colors, indices, axis=0) 28 | if mesh.vertex_normals is not None: 29 | cpy.vertex_normals = np.delete( 30 | mesh.vertex_normals, indices, axis=0) 31 | 32 | if mesh.faces is not None: 33 | face_indices = np.where( 34 | np.any(np.isin(mesh.faces[:], indices, assume_unique=False), 35 | axis=1) 36 | )[0] 37 | cpy.faces = np.delete(mesh.faces, face_indices, axis=0) 38 | fix_indices = np.vectorize( 39 | lambda x: np.sum(x >= indices))(cpy.faces) 40 | cpy.faces -= fix_indices 41 | 42 | if mesh.face_normals is not None: 43 | cpy.face_normals = np.delete( 44 | mesh.face_normals, face_indices, axis=0) 45 | unused_uv = None 46 | if mesh.texture_indices is not None: 47 | cpy.texture_indices = np.delete( 48 | mesh.texture_indices, face_indices, axis=0) 49 | used_uv = np.unique(cpy.texture_indices.flatten()) 50 | all_uv = np.arange(len(mesh.texcoords)) 51 | unused_uv = np.setdiff1d(all_uv, used_uv, assume_unique=True) 52 | fix_uv_idx = np.vectorize( 53 | lambda x: np.sum(x >= unused_uv))(cpy.texture_indices) 54 | cpy.texture_indices -= fix_uv_idx 55 | cpy.texcoords = np.delete(mesh.texcoords, unused_uv, axis=0) 56 | 57 | # render texture 58 | if blackoutTexture: 59 | tri_indices = cpy.texture_indices 60 | tex_coords = cpy.texcoords 61 | img = render_texture(mesh.texture, tex_coords, tri_indices) 62 | # dilate the result to remove sewing 63 | kernel = np.ones((3, 3), np.uint8) 64 | texture_f32 = cv2.dilate(img, kernel, iterations=1) 65 | cpy.texture = texture_f32.astype(np.float64) 66 | 67 | if mesh.faces_normal_indices is not None: 68 | cpy.faces_normal_indices = np.delete( 69 | mesh.faces_normal_indices, face_indices, axis=0) 70 | used_ni = np.unique(cpy.faces_normal_indices.flatten()) 71 | all_ni = np.arange(len(mesh.face_normals)) 72 | unused_ni = np.setdiff1d(all_ni, used_ni, assume_unique=True) 73 | fix_ni_idx = np.vectorize(lambda x: np.sum( 74 | x > unused_ni))(cpy.faces_normal_indices) 75 | cpy.faces_normal_indices -= fix_ni_idx 76 | cpy.face_normals = np.delete( 77 | mesh.face_normals, unused_ni, axis=0) 78 | 79 | return cpy 80 | 81 | 82 | def render_texture(texture, tex_coords, tri_indices): 83 | if len(texture.shape) == 3 and texture.shape[2] == 4: 84 | texture = texture[:, :, 0:3] 85 | elif len(texture.shape) == 2: 86 | texture = np.concatenate([texture, texture, texture], axis=2) 87 | 88 | renderer = UVTrianglesRenderer.with_standalone_ctx( 89 | (texture.shape[1], texture.shape[0]) 90 | ) 91 | 92 | return renderer.render(tex_coords, tri_indices, texture, True) 93 | 94 | 95 | def estimate_plane(a, b, c): 96 | """Estimate the parameters of the plane passing by three points. 97 | 98 | Returns: 99 | center(float): The center point of the three input points. 100 | normal(float): The normal to the plane. 101 | """ 102 | center = (a + b + c) / 3 103 | normal = np.cross(b - a, c - a) 104 | assert(np.isclose(np.dot(b - a, normal), np.dot(c - a, normal))) 105 | return center, normal 106 | 107 | 108 | def shoot_holes(vertices, n_holes, dropout, mask_faces=None, faces=None, 109 | rng=None): 110 | """Generate a partial shape by cutting holes of random location and size. 111 | 112 | Each hole is created by selecting a random point as the center and removing 113 | the k nearest-neighboring points around it. 114 | 115 | Args: 116 | vertices: The array of vertices of the mesh. 117 | n_holes (int or (int, int)): Number of holes to create, or bounds from 118 | which to randomly draw the number of holes. 119 | dropout (float or (float, float)): Proportion of points (with respect 120 | to the total number of points) in each hole, or bounds from which 121 | to randomly draw the proportions (a different proportion is drawn 122 | for each hole). 123 | mask_faces: A boolean mask on the faces. 1 to keep, 0 to ignore. If 124 | set, the centers of the holes are sampled only on the 125 | non-masked regions. 126 | faces: The array of faces of the mesh. Required only when `mask_faces` 127 | is set. 128 | rng: (optional) An initialised np.random.Generator object. If None, a 129 | default Generator is created. 130 | 131 | Returns: 132 | array: Indices of the points defining the holes. 133 | """ 134 | if rng is None: 135 | rng = np.random.default_rng() 136 | 137 | if not isinstance(n_holes, numbers.Integral): 138 | n_holes_min, n_holes_max = n_holes 139 | n_holes = rng.integers(n_holes_min, n_holes_max) 140 | 141 | if mask_faces is not None: 142 | valid_vertex_indices = np.unique(faces[mask_faces > 0]) 143 | valid_vertices = vertices[valid_vertex_indices] 144 | else: 145 | valid_vertices = vertices 146 | 147 | # Select random hole centers. 148 | center_indices = rng.choice(len(valid_vertices), size=n_holes) 149 | centers = valid_vertices[center_indices] 150 | 151 | n_vertices = len(valid_vertices) 152 | if isinstance(dropout, numbers.Number): 153 | hole_size = n_vertices * dropout 154 | hole_sizes = [hole_size] * n_holes 155 | else: 156 | hole_size_bounds = n_vertices * np.asarray(dropout) 157 | hole_sizes = rng.integers(*hole_size_bounds, size=n_holes) 158 | 159 | # Identify the points indices making up the holes. 160 | kdtree = KDTree(vertices, leafsize=200) 161 | to_crop = [] 162 | for center, size in zip(centers, hole_sizes): 163 | _, indices = kdtree.query(center, k=size) 164 | to_crop.append(indices) 165 | to_crop = np.unique(np.concatenate(to_crop)) 166 | 167 | return to_crop 168 | -------------------------------------------------------------------------------- /data_processing/sample_RGB_GT.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import trimesh 3 | from glob import glob 4 | import os 5 | import multiprocessing as mp 6 | from multiprocessing import Pool 7 | import argparse 8 | import config.config_loader as cfg_loader 9 | import utils 10 | import traceback 11 | import tqdm 12 | 13 | 14 | 15 | 16 | def sample_colors(gt_mesh_path): 17 | try: 18 | path = os.path.normpath(gt_mesh_path) 19 | challange = path.split(os.sep)[-4] 20 | split = path.split(os.sep)[-3] 21 | gt_file_name = path.split(os.sep)[-2] 22 | full_file_name = path.split(os.sep)[-1][:-4] 23 | 24 | out_file = cfg['data_path'] + '/{}/{}/{}_color_samples{}_bbox{}.npz' \ 25 | .format(split, gt_file_name, full_file_name, num_points, cfg['data_bounding_box_str']) 26 | 27 | if os.path.exists(out_file): 28 | print('File exists. Done.') 29 | return 30 | 31 | gt_mesh = utils.as_mesh(trimesh.load(gt_mesh_path)) 32 | sample_points, face_idxs = gt_mesh.sample(num_points, return_index = True) 33 | 34 | triangles = gt_mesh.triangles[face_idxs] 35 | face_vertices = gt_mesh.faces[face_idxs] 36 | faces_uvs = gt_mesh.visual.uv[face_vertices] 37 | 38 | q = triangles[:, 0] 39 | u = triangles[:, 1] 40 | v = triangles[:, 2] 41 | 42 | uvs = [] 43 | 44 | for i, p in enumerate(sample_points): 45 | barycentric_weights = utils.barycentric_coordinates(p, q[i], u[i], v[i]) 46 | uv = np.average(faces_uvs[i], 0, barycentric_weights) 47 | uvs.append(uv) 48 | 49 | texture = gt_mesh.visual.material.image 50 | 51 | colors = trimesh.visual.color.uv_to_color(np.array(uvs), texture) 52 | 53 | np.savez(out_file, points = sample_points, grid_coords = utils.to_grid_sample_coords(sample_points, bbox), colors = colors[:,:3]) 54 | except Exception as err: 55 | print('Error with {}: {}'.format(out_file, traceback.format_exc())) 56 | 57 | if __name__ == '__main__': 58 | parser = argparse.ArgumentParser( 59 | description='Run color sampling. Samples surface points on the GT objects, and saves their coordinates along with the RGB color at their location.' 60 | ) 61 | 62 | parser.add_argument('config', type=str, help='Path to config file.') 63 | args = parser.parse_args() 64 | 65 | cfg = cfg_loader.load(args.config) 66 | 67 | num_points = cfg['preprocessing']['color_sampling']['sample_number'] 68 | bbox = cfg['data_bounding_box'] 69 | 70 | print('Fining all gt object paths for point and RGB sampling.') 71 | paths = glob(cfg['data_path'] + cfg['preprocessing']['color_sampling']['input_files_regex']) 72 | 73 | print('Start sampling.') 74 | p = Pool(mp.cpu_count()) 75 | for _ in tqdm.tqdm(p.imap_unordered(sample_colors, paths), total=len(paths)): 76 | pass 77 | p.close() 78 | p.join() 79 | -------------------------------------------------------------------------------- /data_processing/sample_voxelized_colored_pointcloud.py: -------------------------------------------------------------------------------- 1 | import utils 2 | from scipy.spatial import cKDTree as KDTree 3 | import numpy as np 4 | import trimesh 5 | from glob import glob 6 | import os 7 | import multiprocessing as mp 8 | from multiprocessing import Pool 9 | import argparse 10 | import random 11 | import config.config_loader as cfg_loader 12 | import traceback 13 | import tqdm 14 | import utils 15 | 16 | 17 | def voxelized_colored_pointcloud_sampling(partial_mesh_path): 18 | try: 19 | path = os.path.normpath(partial_mesh_path) 20 | gt_file_name = path.split(os.sep)[-2] 21 | full_file_name = path.split(os.sep)[-1][:-4] 22 | 23 | out_file = os.path.dirname(partial_mesh_path) + '/{}_voxelized_colored_point_cloud_res{}_points{}_bbox{}.npz'\ 24 | .format(full_file_name, res, num_points, bbox_str) 25 | 26 | if os.path.exists(out_file): 27 | print('File exists. Done.') 28 | return 29 | 30 | # color from partial input 31 | partial_mesh = utils.as_mesh(trimesh.load(partial_mesh_path)) 32 | colored_point_cloud, face_idxs = partial_mesh.sample(num_points, return_index = True) 33 | 34 | triangles = partial_mesh.triangles[face_idxs] 35 | face_vertices = partial_mesh.faces[face_idxs] 36 | faces_uvs = partial_mesh.visual.uv[face_vertices] 37 | 38 | q = triangles[:, 0] 39 | u = triangles[:, 1] 40 | v = triangles[:, 2] 41 | 42 | uvs = [] 43 | 44 | for i, p in enumerate(colored_point_cloud): 45 | barycentric_weights = utils.barycentric_coordinates(p, q[i], u[i], v[i]) 46 | uv = np.average(faces_uvs[i], 0, barycentric_weights) 47 | uvs.append(uv) 48 | 49 | partial_texture = partial_mesh.visual.material.image 50 | 51 | colors = trimesh.visual.color.uv_to_color(np.array(uvs), partial_texture) 52 | 53 | R = - 1 * np.ones(len(grid_points), dtype=np.int16) 54 | G = - 1 * np.ones(len(grid_points), dtype=np.int16) 55 | B = - 1 * np.ones(len(grid_points), dtype=np.int16) 56 | 57 | _, idx = kdtree.query(colored_point_cloud) 58 | R[idx] = colors[:,0] 59 | G[idx] = colors[:,1] 60 | B[idx] = colors[:,2] 61 | 62 | # encode uncolorized, complete shape of object (at inference time obtained from IF-Nets surface reconstruction) 63 | # encoding is done by sampling a pointcloud and voxelizing it (into discrete grid for 3D CNN usage) 64 | full_shape = trimesh.load(os.path.join(os.path.dirname(partial_mesh_path) , gt_file_name +'_normalized.obj')) 65 | shape_point_cloud = full_shape.sample(num_points) 66 | S = np.zeros(len(grid_points), dtype=np.int8) 67 | 68 | _, idx = kdtree.query(shape_point_cloud) 69 | S[idx] = 1 70 | 71 | np.savez(out_file, R=R, G=G,B=B, S=S, colored_point_cloud=colored_point_cloud, bbox = bbox, res = res) 72 | 73 | except Exception as err: 74 | print('Error with {}: {}'.format(partial_mesh_path, traceback.format_exc())) 75 | 76 | if __name__ == '__main__': 77 | parser = argparse.ArgumentParser( 78 | description='Generates the input for the network: a partial colored shape and a uncolorized, but completed shape. \ 79 | Both encoded as 3D voxel grids for usage with a 3D CNN.' 80 | ) 81 | 82 | parser.add_argument('config', type=str, help='Path to config file.') 83 | args = parser.parse_args() 84 | 85 | cfg = cfg_loader.load(args.config) 86 | 87 | # shorthands 88 | bbox = cfg['data_bounding_box'] 89 | res = cfg['input_resolution'] 90 | num_points = cfg['input_points_number'] 91 | bbox_str = cfg['data_bounding_box_str'] 92 | 93 | grid_points = utils.create_grid_points_from_xyz_bounds(*bbox, res) 94 | kdtree = KDTree(grid_points) 95 | 96 | print('Fining all input partial paths for voxelization.') 97 | paths = glob(cfg['data_path'] + cfg['preprocessing']['voxelized_colored_pointcloud_sampling']['input_files_regex']) 98 | 99 | print('Start voxelization.') 100 | p = Pool(mp.cpu_count()) 101 | for _ in tqdm.tqdm(p.imap_unordered(voxelized_colored_pointcloud_sampling, paths), total=len(paths)): 102 | pass 103 | p.close() 104 | p.join() 105 | -------------------------------------------------------------------------------- /data_processing/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import trimesh 3 | 4 | def create_grid_points_from_xyz_bounds(min_x, max_x, min_y, max_y ,min_z, max_z, res): 5 | x = np.linspace(min_x, max_x, res) 6 | y = np.linspace(min_y, max_y, res) 7 | z = np.linspace(min_z, max_z, res) 8 | X, Y, Z = np.meshgrid(x, y, z, indexing='ij', sparse=False) 9 | X = X.reshape((np.prod(X.shape),)) 10 | Y = Y.reshape((np.prod(Y.shape),)) 11 | Z = Z.reshape((np.prod(Z.shape),)) 12 | 13 | points_list = np.column_stack((X, Y, Z)) 14 | del X, Y, Z, x 15 | return points_list 16 | 17 | 18 | # conversion of points into coordinates understood by pytorch's grid_sample function 19 | # bbox = [min_x,max_x,min_y,max_y,min_z,max_z] 20 | def to_grid_sample_coords(points, bbox): 21 | min_values, max_values = bbox[::2], bbox[1::2] 22 | points = 2 * 1 / (max_values - min_values) * (points - min_values) - 1 23 | grid_coords = points.copy() 24 | grid_coords[:, 0], grid_coords[:, 2] = points[:, 2], points[:, 0] 25 | return grid_coords 26 | 27 | def barycentric_coordinates(p, q, u, v): 28 | """ 29 | Calculate barycentric coordinates of the given point 30 | :param p: a given point 31 | :param q: triangle vertex 32 | :param u: triangle vertex 33 | :param v: triangle vertex 34 | :return: 1X3 ndarray with the barycentric coordinates of p 35 | """ 36 | v0 = u - q 37 | v1 = v - q 38 | v2 = p - q 39 | d00 = v0.dot(v0) 40 | d01 = v0.dot(v1) 41 | d11 = v1.dot(v1) 42 | d20 = v2.dot(v0) 43 | d21 = v2.dot(v1) 44 | denom = d00 * d11 - d01 * d01 45 | y = (d11 * d20 - d01 * d21) / denom 46 | z = (d00 * d21 - d01 * d20) / denom 47 | x = 1.0 - z - y 48 | return np.array([x, y, z]) 49 | 50 | def as_mesh(scene_or_mesh): 51 | if isinstance(scene_or_mesh, trimesh.Scene): 52 | mesh = trimesh.util.concatenate([ 53 | trimesh.Trimesh(vertices=m.vertices, faces=m.faces) 54 | for m in scene_or_mesh.geometry.values()]) 55 | else: 56 | mesh = scene_or_mesh 57 | return mesh -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: TexIF-Net 2 | channels: 3 | - pytorch 4 | - anaconda 5 | - conda-forge 6 | - defaults 7 | dependencies: 8 | - _libgcc_mutex=0.1=main 9 | - bzip2=1.0.8=h516909a_3 10 | - ca-certificates=2020.7.22=0 11 | - cairo=1.16.0=h18b612c_1001 12 | - certifi=2020.6.20=py36_0 13 | - cudatoolkit=9.2=0 14 | - dbus=1.13.6=he372182_0 15 | - expat=2.2.9=he1b5a44_2 16 | - ffmpeg=4.1.3=h167e202_0 17 | - fontconfig=2.13.1=he4413a7_1000 18 | - freetype=2.10.2=he06d7ca_0 19 | - giflib=5.2.1=h516909a_2 20 | - glib=2.65.0=h3eb4bd4_0 21 | - gmp=6.2.0=he1b5a44_2 22 | - gnutls=3.6.13=h79a8f9a_0 23 | - graphite2=1.3.13=he1b5a44_1001 24 | - gst-plugins-base=1.14.0=hbbd80ab_1 25 | - gstreamer=1.14.0=hb31296c_0 26 | - harfbuzz=2.4.0=h37c48d4_1 27 | - hdf5=1.10.5=nompi_h3c11f04_1104 28 | - icu=58.2=hf484d3e_1000 29 | - intel-openmp=2020.2=254 30 | - jasper=1.900.1=h07fcdf6_1006 31 | - jpeg=9d=h516909a_0 32 | - lame=3.100=h14c3975_1001 33 | - lcms2=2.11=h396b838_0 34 | - ld_impl_linux-64=2.33.1=h53a641e_7 35 | - libblas=3.8.0=17_openblas 36 | - libcblas=3.8.0=17_openblas 37 | - libedit=3.1.20191231=h14c3975_1 38 | - libffi=3.3=he6710b0_2 39 | - libgcc-ng=9.1.0=hdf63c60_0 40 | - libgfortran-ng=7.5.0=hdf63c60_16 41 | - libiconv=1.16=h516909a_0 42 | - liblapack=3.8.0=17_openblas 43 | - liblapacke=3.8.0=17_openblas 44 | - libopenblas=0.3.10=pthreads_hb3c22a3_4 45 | - libpng=1.6.37=hed695b0_2 46 | - libstdcxx-ng=9.1.0=hdf63c60_0 47 | - libtiff=4.1.0=hc3755c2_3 48 | - libuuid=2.32.1=h14c3975_1000 49 | - libwebp=1.0.2=h56121f0_5 50 | - libxcb=1.13=h14c3975_1002 51 | - libxml2=2.9.9=h13577e0_2 52 | - lz4-c=1.9.2=he1b5a44_3 53 | - mkl=2020.2=256 54 | - ncurses=6.2=he6710b0_1 55 | - nettle=3.4.1=h1bed415_1002 56 | - ninja=1.10.1=py36hfd86e86_0 57 | - olefile=0.46=py36_0 58 | - opencv=4.1.1=py36ha799480_1 59 | - openh264=1.8.0=hdbcaa40_1000 60 | - openssl=1.1.1h=h7b6447c_0 61 | - pcre=8.44=he1b5a44_0 62 | - pillow=7.2.0=py36hb39fc2d_0 63 | - pip=20.2.2=py36_0 64 | - pixman=0.38.0=h516909a_1003 65 | - pthread-stubs=0.4=h14c3975_1001 66 | - python=3.6.12=hcff3b4d_2 67 | - python_abi=3.6=1_cp36m 68 | - pytorch=1.4.0=py3.6_cuda9.2.148_cudnn7.6.3_0 69 | - qt=5.9.7=h5867ecd_1 70 | - readline=8.0=h7b6447c_0 71 | - setuptools=49.6.0=py36_0 72 | - six=1.15.0=py_0 73 | - sqlite=3.33.0=h62c20be_0 74 | - tk=8.6.10=hbc83047_0 75 | - torchvision=0.5.0=py36_cu92 76 | - tqdm=4.49.0=pyh9f0ad1d_0 77 | - wheel=0.35.1=py_0 78 | - x264=1!152.20180806=h14c3975_0 79 | - xorg-kbproto=1.0.7=h14c3975_1002 80 | - xorg-libice=1.0.10=h516909a_0 81 | - xorg-libsm=1.2.3=h84519dc_1000 82 | - xorg-libx11=1.6.12=h516909a_0 83 | - xorg-libxau=1.0.9=h14c3975_0 84 | - xorg-libxdmcp=1.1.3=h516909a_0 85 | - xorg-libxext=1.3.4=h516909a_0 86 | - xorg-libxrender=0.9.10=h516909a_1002 87 | - xorg-renderproto=0.11.1=h14c3975_1002 88 | - xorg-xextproto=7.3.0=h14c3975_1002 89 | - xorg-xproto=7.0.31=h14c3975_1007 90 | - xz=5.2.5=h7b6447c_0 91 | - yaml=0.2.5=h7b6447c_0 92 | - zlib=1.2.11=h7b6447c_3 93 | - zstd=1.4.5=h6597ccf_2 94 | - pip: 95 | - argparse==1.4.0 96 | - glcontext==2.2.0 97 | - moderngl==5.6.2 98 | - nptyping==1.3.0 99 | - numpy==1.19.2 100 | - opencv-python==4.4.0.42 101 | - pathlib==1.0.1 102 | - pyyaml==5.3.1 103 | - scipy==1.5.2 104 | - sharp-cvi2==1.0.0 105 | - trimesh==3.8.8 106 | - typish==1.7.0 107 | prefix: /BS/chiban/work/anaconda3/envs/TexIF-Net 108 | 109 | -------------------------------------------------------------------------------- /generate.py: -------------------------------------------------------------------------------- 1 | import models.local_model as model 2 | import models.dataloader as dataloader 3 | import numpy as np 4 | import argparse 5 | from models.generation import Generator 6 | import config.config_loader as cfg_loader 7 | import os 8 | import trimesh 9 | import torch 10 | from data_processing import utils 11 | from tqdm import tqdm 12 | 13 | 14 | 15 | if __name__ == "__main__": 16 | parser = argparse.ArgumentParser( 17 | description='Generation Model' 18 | ) 19 | 20 | parser.add_argument('config', type=str, help='Path to config file.') 21 | args = parser.parse_args() 22 | 23 | cfg = cfg_loader.load(args.config) 24 | 25 | net = model.get_models()[cfg['model']]() 26 | 27 | dataloader = dataloader.VoxelizedDataset('test', cfg, generation = True, num_workers=0).get_loader() 28 | 29 | gen = Generator(net, cfg) 30 | 31 | 32 | out_path = 'experiments/{}/evaluation_{}/'.format(cfg['folder_name'], gen.checkpoint) 33 | 34 | 35 | for data in tqdm(dataloader): 36 | 37 | 38 | try: 39 | inputs = data['inputs'] 40 | path = data['path'][0] 41 | except: 42 | print('none') 43 | continue 44 | 45 | 46 | path = os.path.normpath(path) 47 | challange = path.split(os.sep)[-4] 48 | split = path.split(os.sep)[-3] 49 | gt_file_name = path.split(os.sep)[-2] 50 | basename = path.split(os.sep)[-1] 51 | filename_partial = os.path.splitext(path.split(os.sep)[-1])[0] 52 | 53 | file_out_path = out_path + '/{}/'.format(gt_file_name) 54 | os.makedirs(file_out_path, exist_ok=True) 55 | 56 | if os.path.exists(file_out_path + 'colored_surface_reconstuction.obj'): 57 | continue 58 | 59 | 60 | path_surface = os.path.join(cfg['data_path'], split, gt_file_name, gt_file_name + '_normalized.obj') 61 | 62 | mesh = trimesh.load(path_surface) 63 | 64 | # create new uncolored mesh for color prediction 65 | pred_mesh = trimesh.Trimesh(mesh.vertices, mesh.faces) 66 | 67 | # colors will be attached per vertex 68 | # subdivide in order to have high enough number of vertices for good texture representation 69 | pred_mesh = pred_mesh.subdivide().subdivide() 70 | 71 | pred_verts_gird_coords = utils.to_grid_sample_coords( pred_mesh.vertices, cfg['data_bounding_box']) 72 | pred_verts_gird_coords = torch.tensor(pred_verts_gird_coords).unsqueeze(0) 73 | 74 | 75 | colors_pred_surface = gen.generate_colors(inputs, pred_verts_gird_coords) 76 | 77 | # attach predicted colors to the mesh 78 | pred_mesh.visual.vertex_colors = colors_pred_surface 79 | 80 | pred_mesh.export( file_out_path + f'{filename_partial}_color_reconstruction.obj') -------------------------------------------------------------------------------- /if-net_env.yml: -------------------------------------------------------------------------------- 1 | name: tex_if-net 2 | channels: 3 | - conda-forge 4 | - pytorch 5 | - defaults 6 | dependencies: 7 | - trimesh=3.6.18 8 | - pytorch==1.1.0 9 | - cudatoolkit=9.0 10 | - tensorboard=1.14.0 11 | - numpy=1.17 12 | - pyyaml 13 | - pillow 14 | - tqdm -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /models/dataloader.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from torch.utils.data import Dataset 3 | import os 4 | import numpy as np 5 | import pickle 6 | import imp 7 | import trimesh 8 | import torch 9 | 10 | 11 | 12 | class VoxelizedDataset(Dataset): 13 | 14 | 15 | def __init__(self, mode, cfg, generation = False, num_workers = 12): 16 | 17 | self.path = cfg['data_path'] 18 | self.mode = mode 19 | self.data = np.load(cfg['split_file'])[mode] 20 | self.res = cfg['input_resolution'] 21 | self.bbox_str = cfg['data_bounding_box_str'] 22 | self.bbox = cfg['data_bounding_box'] 23 | self.num_gt_rgb_samples = cfg['preprocessing']['color_sampling']['sample_number'] 24 | 25 | self.sample_points_per_object = cfg['training']['sample_points_per_object'] 26 | if generation: 27 | self.batch_size = 1 28 | else: 29 | self.batch_size = cfg['training']['batch_size'] 30 | self.num_workers = num_workers 31 | if cfg['input_type'] == 'voxels': 32 | self.voxelized_pointcloud = False 33 | else: 34 | self.voxelized_pointcloud = True 35 | self.pointcloud_samples = cfg['input_points_number'] 36 | 37 | 38 | 39 | def __len__(self): 40 | return len(self.data) 41 | 42 | def __getitem__(self, idx): 43 | path = self.data[idx] 44 | 45 | path = os.path.normpath(path) 46 | challange = path.split(os.sep)[-4] 47 | split = path.split(os.sep)[-3] 48 | gt_file_name = path.split(os.sep)[-2] 49 | full_file_name = os.path.splitext(path.split(os.sep)[-1])[0] 50 | 51 | voxel_path = os.path.join(self.path, split, gt_file_name,\ 52 | '{}_voxelized_colored_point_cloud_res{}_points{}_bbox{}.npz'\ 53 | .format(full_file_name, self.res, self.pointcloud_samples, self.bbox_str)) 54 | 55 | 56 | R = np.load(voxel_path)['R'] 57 | G = np.load(voxel_path)['G'] 58 | B = np.load(voxel_path)['B'] 59 | S = np.load(voxel_path)['S'] 60 | 61 | R = np.reshape(R, (self.res,)*3) 62 | G = np.reshape(G, (self.res,)*3) 63 | B = np.reshape(B, (self.res,)*3) 64 | S = np.reshape(S, (self.res,)*3) 65 | input = np.array([R,G,B,S]) 66 | 67 | if self.mode == 'test': 68 | return { 'inputs': np.array(input, dtype=np.float32), 'path' : path} 69 | 70 | 71 | rgb_samples_path = os.path.join(self.path, split, gt_file_name,\ 72 | '{}_normalized_color_samples{}_bbox{}.npz' \ 73 | .format(gt_file_name, self.num_gt_rgb_samples, self.bbox_str)) 74 | 75 | rgb_samples_npz = np.load(rgb_samples_path) 76 | rgb_coords = rgb_samples_npz['grid_coords'] 77 | rgb_values = rgb_samples_npz['colors'] 78 | subsample_indices = np.random.randint(0, len(rgb_values), self.sample_points_per_object) 79 | rgb_coords = rgb_coords[subsample_indices] 80 | rgb_values = rgb_values[subsample_indices] 81 | 82 | 83 | return {'grid_coords':np.array(rgb_coords, dtype=np.float32),'rgb': np.array(rgb_values, dtype=np.float32), 'inputs': np.array(input, dtype=np.float32), 'path' : path} 84 | 85 | def get_loader(self, shuffle =True): 86 | 87 | return torch.utils.data.DataLoader( 88 | self, batch_size=self.batch_size, num_workers=self.num_workers, shuffle=shuffle, 89 | worker_init_fn=self.worker_init_fn) 90 | 91 | def worker_init_fn(self, worker_id): 92 | random_data = os.urandom(4) 93 | base_seed = int.from_bytes(random_data, byteorder="big") 94 | np.random.seed(base_seed + worker_id) -------------------------------------------------------------------------------- /models/generation.py: -------------------------------------------------------------------------------- 1 | import data_processing.utils as utils 2 | import trimesh 3 | import torch 4 | import os 5 | from glob import glob 6 | import numpy as np 7 | 8 | class Generator(object): 9 | def __init__(self, model, cfg, device = torch.device("cuda")): 10 | self.model = model.to(device) 11 | self.model.eval() 12 | 13 | 14 | self.checkpoint_path = os.path.dirname(__file__) + '/../experiments/{}/checkpoints/'.format( 15 | cfg['folder_name']) 16 | self.exp_folder_name = cfg['folder_name'] 17 | self.checkpoint = self.load_checkpoint(cfg['generation']['checkpoint']) 18 | self.threshold = cfg['generation']['retrieval_threshold'] 19 | 20 | self.device = device 21 | self.resolution = cfg['generation']['retrieval_resolution'] 22 | self.batch_points = cfg['generation']['batch_points'] 23 | 24 | self.bbox = cfg['data_bounding_box'] 25 | self.min = self.bbox[::2] 26 | self.max = self.bbox[1::2] 27 | 28 | grid_points = utils.create_grid_points_from_xyz_bounds(*cfg['data_bounding_box'], self.resolution) 29 | 30 | grid_coords = utils.to_grid_sample_coords(grid_points, self.bbox) 31 | 32 | grid_coords = torch.from_numpy(grid_coords).to(self.device, dtype=torch.float) 33 | grid_coords = torch.reshape(grid_coords, (1, len(grid_points), 3)).to(self.device) 34 | self.grid_points_split = torch.split(grid_coords, self.batch_points, dim=1) 35 | 36 | 37 | 38 | def generate_colors(self, inputs,points): 39 | 40 | p = points.to(self.device).float() 41 | # p.shape is [1, n_verts, 3] 693016 -> > 21gb gram 42 | 43 | i = inputs.to(self.device).float() 44 | full_pred = [] 45 | 46 | p_batches = torch.split(p, 200000, dim=1) 47 | 48 | for p_batch in p_batches: 49 | with torch.no_grad(): 50 | pred_rgb = self.model(p_batch,i) 51 | full_pred.append(pred_rgb.squeeze(0).detach().cpu().transpose(0,1)) 52 | 53 | pred_rgb = torch.cat(full_pred, dim=0).numpy() 54 | pred_rgb.astype(np.int)[0] 55 | pred_rgb = np.clip(pred_rgb, 0, 255) 56 | 57 | return pred_rgb 58 | 59 | 60 | 61 | 62 | def load_checkpoint(self, checkpoint): 63 | if checkpoint == -1: 64 | val_min_npy = os.path.dirname(__file__) + '/../experiments/{}/val_min.npy'.format( 65 | self.exp_folder_name) 66 | checkpoint = int(np.load(val_min_npy)[0]) 67 | path = self.checkpoint_path + 'checkpoint_epoch_{}.tar'.format(checkpoint) 68 | else: 69 | path = self.checkpoint_path + 'checkpoint_epoch_{}.tar'.format(checkpoint) 70 | print('Loaded checkpoint from: {}'.format(path)) 71 | torch_checkpoint = torch.load(path) 72 | self.model.load_state_dict(torch_checkpoint['model_state_dict']) 73 | return checkpoint -------------------------------------------------------------------------------- /models/local_model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | 6 | 7 | def get_models(): 8 | return {'ShapeNet32Vox': ShapeNet32Vox, 'ShapeNet128Vox': ShapeNet128Vox, 'ShapeNetPoints': ShapeNetPoints, 'TEXR':TEXR} 9 | 10 | 11 | # 1D convolution is used for the decoder. It acts as a standard FC, but allows to use a batch of point samples features, 12 | # additionally to the batch over the input objects. 13 | # The dimensions are used as follows: 14 | # batch_size (N) = #3D objects , channels = features, signal_lengt (L) (convolution dimension) = #point samples 15 | # kernel_size = 1 i.e. every convolution is done over only all features of one point sample, this makes it a FC. 16 | 17 | 18 | # ShapeNet Voxel Super-Resolution -------------------------------------------------------------------- 19 | # ---------------------------------------------------------------------------------------------------- 20 | class ShapeNet32Vox(nn.Module): 21 | 22 | def __init__(self, hidden_dim=256): 23 | super(ShapeNet32Vox, self).__init__() 24 | 25 | self.conv_1 = nn.Conv3d(1, 32, 3, padding=1) # out: 32 26 | self.conv_1_1 = nn.Conv3d(32, 64, 3, padding=1) # out: 32 27 | self.conv_2 = nn.Conv3d(64, 128, 3, padding=1) # out: 16 28 | self.conv_2_1 = nn.Conv3d(128, 128, 3, padding=1) # out: 16 29 | self.conv_3 = nn.Conv3d(128, 128, 3, padding=1) # out: 8 30 | self.conv_3_1 = nn.Conv3d(128, 128, 3, padding=1) # out: 8 31 | 32 | feature_size = (1 + 64 + 128 + 128 ) * 7 33 | self.fc_0 = nn.Conv1d(feature_size, hidden_dim*2, 1) 34 | self.fc_1 = nn.Conv1d(hidden_dim*2, hidden_dim, 1) 35 | self.fc_2 = nn.Conv1d(hidden_dim, hidden_dim, 1) 36 | self.fc_out = nn.Conv1d(hidden_dim, 1, 1) 37 | self.actvn = nn.ReLU() 38 | 39 | self.maxpool = nn.MaxPool3d(2) 40 | 41 | self.conv1_1_bn = nn.BatchNorm3d(64) 42 | self.conv2_1_bn = nn.BatchNorm3d(128) 43 | self.conv3_1_bn = nn.BatchNorm3d(128) 44 | 45 | 46 | displacment = 0.035 47 | displacments = [] 48 | displacments.append([0, 0, 0]) 49 | for x in range(3): 50 | for y in [-1, 1]: 51 | input = [0, 0, 0] 52 | input[x] = y * displacment 53 | displacments.append(input) 54 | 55 | self.displacments = torch.Tensor(displacments).cuda() 56 | 57 | def forward(self, p, x): 58 | x = x.unsqueeze(1) 59 | 60 | p_features = p.transpose(1, -1) 61 | p = p.unsqueeze(1).unsqueeze(1) 62 | p = torch.cat([p + d for d in self.displacments], dim=2) # (B,1,7,num_samples,3) 63 | feature_0 = F.grid_sample(x, p) # out : (B,C (of x), 1,1,sample_num) 64 | 65 | net = self.actvn(self.conv_1(x)) 66 | net = self.actvn(self.conv_1_1(net)) 67 | net = self.conv1_1_bn(net) 68 | feature_1 = F.grid_sample(net, p) # out : (B,C (of x), 1,1,sample_num) 69 | net = self.maxpool(net) 70 | 71 | net = self.actvn(self.conv_2(net)) 72 | net = self.actvn(self.conv_2_1(net)) 73 | net = self.conv2_1_bn(net) 74 | feature_2 = F.grid_sample(net, p) 75 | net = self.maxpool(net) 76 | 77 | net = self.actvn(self.conv_3(net)) 78 | net = self.actvn(self.conv_3_1(net)) 79 | net = self.conv3_1_bn(net) 80 | feature_3 = F.grid_sample(net, p) 81 | 82 | # here every channel corresponse to one feature. 83 | 84 | features = torch.cat((feature_0, feature_1, feature_2, feature_3), 85 | dim=1) # (B, features, 1,7,sample_num) 86 | shape = features.shape 87 | features = torch.reshape(features, 88 | (shape[0], shape[1] * shape[3], shape[4])) # (B, featues_per_sample, samples_num) 89 | #features = torch.cat((features, p_features), dim=1) # (B, featue_size, samples_num) 90 | 91 | net = self.actvn(self.fc_0(features)) 92 | net = self.actvn(self.fc_1(net)) 93 | net = self.actvn(self.fc_2(net)) 94 | net = self.fc_out(net) 95 | out = net.squeeze(1) 96 | 97 | return out 98 | 99 | class ShapeNet128Vox(nn.Module): 100 | 101 | def __init__(self, hidden_dim=256): 102 | super(ShapeNet128Vox, self).__init__() 103 | # accepts 128**3 res input 104 | self.conv_in = nn.Conv3d(1, 16, 3, padding=1) # out: 128 105 | self.conv_0 = nn.Conv3d(16, 32, 3, padding=1) # out: 64 106 | self.conv_0_1 = nn.Conv3d(32, 32, 3, padding=1) # out: 64 107 | self.conv_1 = nn.Conv3d(32, 64, 3, padding=1) # out: 32 108 | self.conv_1_1 = nn.Conv3d(64, 64, 3, padding=1) # out: 32 109 | self.conv_2 = nn.Conv3d(64, 128, 3, padding=1) # out: 16 110 | self.conv_2_1 = nn.Conv3d(128, 128, 3, padding=1) # out: 16 111 | self.conv_3 = nn.Conv3d(128, 128, 3, padding=1) # out: 8 112 | self.conv_3_1 = nn.Conv3d(128, 128, 3, padding=1) # out: 8 113 | 114 | feature_size = (1 + 16 + 32 + 64 + 128 + 128 ) * 7 115 | self.fc_0 = nn.Conv1d(feature_size, hidden_dim, 1) 116 | self.fc_1 = nn.Conv1d(hidden_dim, hidden_dim, 1) 117 | self.fc_2 = nn.Conv1d(hidden_dim, hidden_dim, 1) 118 | self.fc_out = nn.Conv1d(hidden_dim, 1, 1) 119 | self.actvn = nn.ReLU() 120 | 121 | self.maxpool = nn.MaxPool3d(2) 122 | 123 | self.conv_in_bn = nn.BatchNorm3d(16) 124 | self.conv0_1_bn = nn.BatchNorm3d(32) 125 | self.conv1_1_bn = nn.BatchNorm3d(64) 126 | self.conv2_1_bn = nn.BatchNorm3d(128) 127 | self.conv3_1_bn = nn.BatchNorm3d(128) 128 | 129 | 130 | displacment = 0.0722 131 | displacments = [] 132 | displacments.append([0, 0, 0]) 133 | for x in range(3): 134 | for y in [-1, 1]: 135 | input = [0, 0, 0] 136 | input[x] = y * displacment 137 | displacments.append(input) 138 | 139 | self.displacments = torch.Tensor(displacments).cuda() 140 | 141 | def forward(self, p, x): 142 | x = x.unsqueeze(1) 143 | 144 | p_features = p.transpose(1, -1) 145 | p = p.unsqueeze(1).unsqueeze(1) 146 | p = torch.cat([p + d for d in self.displacments], dim=2) # (B,1,7,num_samples,3) 147 | feature_0 = F.grid_sample(x, p) # out : (B,C (of x), 1,1,sample_num) 148 | 149 | net = self.actvn(self.conv_in(x)) 150 | net = self.conv_in_bn(net) 151 | feature_1 = F.grid_sample(net, p) # out : (B,C (of x), 1,1,sample_num) 152 | net = self.maxpool(net) 153 | 154 | net = self.actvn(self.conv_0(net)) 155 | net = self.actvn(self.conv_0_1(net)) 156 | net = self.conv0_1_bn(net) 157 | feature_2 = F.grid_sample(net, p) # out : (B,C (of x), 1,1,sample_num) 158 | net = self.maxpool(net) 159 | 160 | net = self.actvn(self.conv_1(net)) 161 | net = self.actvn(self.conv_1_1(net)) 162 | net = self.conv1_1_bn(net) 163 | feature_3 = F.grid_sample(net, p) # out : (B,C (of x), 1,1,sample_num) 164 | net = self.maxpool(net) 165 | 166 | net = self.actvn(self.conv_2(net)) 167 | net = self.actvn(self.conv_2_1(net)) 168 | net = self.conv2_1_bn(net) 169 | feature_4 = F.grid_sample(net, p) 170 | net = self.maxpool(net) 171 | 172 | net = self.actvn(self.conv_3(net)) 173 | net = self.actvn(self.conv_3_1(net)) 174 | net = self.conv3_1_bn(net) 175 | feature_5 = F.grid_sample(net, p) 176 | 177 | # here every channel corresponse to one feature. 178 | 179 | features = torch.cat((feature_0, feature_1, feature_2, feature_3, feature_4, feature_5), 180 | dim=1) # (B, features, 1,7,sample_num) 181 | shape = features.shape 182 | features = torch.reshape(features, 183 | (shape[0], shape[1] * shape[3], shape[4])) # (B, featues_per_sample, samples_num) 184 | #features = torch.cat((features, p_features), dim=1) # (B, featue_size, samples_num) 185 | 186 | net = self.actvn(self.fc_0(features)) 187 | net = self.actvn(self.fc_1(net)) 188 | net = self.actvn(self.fc_2(net)) 189 | net = self.fc_out(net) 190 | out = net.squeeze(1) 191 | 192 | return out 193 | 194 | 195 | 196 | 197 | # ShapeNet Pointcloud Completion --------------------------------------------------------------------- 198 | # ---------------------------------------------------------------------------------------------------- 199 | 200 | class ShapeNetPoints(nn.Module): 201 | 202 | def __init__(self, hidden_dim=256): 203 | super(ShapeNetPoints, self).__init__() 204 | # 128**3 res input 205 | self.conv_in = nn.Conv3d(1, 16, 3, padding=1, padding_mode='border') 206 | self.conv_0 = nn.Conv3d(16, 32, 3, padding=1, padding_mode='border') 207 | self.conv_0_1 = nn.Conv3d(32, 32, 3, padding=1, padding_mode='border') 208 | self.conv_1 = nn.Conv3d(32, 64, 3, padding=1, padding_mode='border') 209 | self.conv_1_1 = nn.Conv3d(64, 64, 3, padding=1, padding_mode='border') 210 | self.conv_2 = nn.Conv3d(64, 128, 3, padding=1, padding_mode='border') 211 | self.conv_2_1 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') 212 | self.conv_3 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') 213 | self.conv_3_1 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') 214 | 215 | feature_size = (1 + 16 + 32 + 64 + 128 + 128 ) * 7 216 | self.fc_0 = nn.Conv1d(feature_size, hidden_dim, 1) 217 | self.fc_1 = nn.Conv1d(hidden_dim, hidden_dim, 1) 218 | self.fc_2 = nn.Conv1d(hidden_dim, hidden_dim, 1) 219 | self.fc_out = nn.Conv1d(hidden_dim, 1, 1) 220 | self.actvn = nn.ReLU() 221 | 222 | self.maxpool = nn.MaxPool3d(2) 223 | 224 | self.conv_in_bn = nn.BatchNorm3d(16) 225 | self.conv0_1_bn = nn.BatchNorm3d(32) 226 | self.conv1_1_bn = nn.BatchNorm3d(64) 227 | self.conv2_1_bn = nn.BatchNorm3d(128) 228 | self.conv3_1_bn = nn.BatchNorm3d(128) 229 | 230 | 231 | displacment = 0.0722 232 | displacments = [] 233 | displacments.append([0, 0, 0]) 234 | for x in range(3): 235 | for y in [-1, 1]: 236 | input = [0, 0, 0] 237 | input[x] = y * displacment 238 | displacments.append(input) 239 | 240 | self.displacments = torch.Tensor(displacments).cuda() 241 | 242 | def forward(self, p, x): 243 | x = x.unsqueeze(1) 244 | 245 | p_features = p.transpose(1, -1) 246 | p = p.unsqueeze(1).unsqueeze(1) 247 | p = torch.cat([p + d for d in self.displacments], dim=2) # (B,1,7,num_samples,3) 248 | feature_0 = F.grid_sample(x, p, padding_mode='border') # out : (B,C (of x), 1,1,sample_num) 249 | 250 | net = self.actvn(self.conv_in(x)) 251 | net = self.conv_in_bn(net) 252 | feature_1 = F.grid_sample(net, p, padding_mode='border') # out : (B,C (of x), 1,1,sample_num) 253 | net = self.maxpool(net) 254 | 255 | net = self.actvn(self.conv_0(net)) 256 | net = self.actvn(self.conv_0_1(net)) 257 | net = self.conv0_1_bn(net) 258 | feature_2 = F.grid_sample(net, p, padding_mode='border') # out : (B,C (of x), 1,1,sample_num) 259 | net = self.maxpool(net) 260 | 261 | net = self.actvn(self.conv_1(net)) 262 | net = self.actvn(self.conv_1_1(net)) 263 | net = self.conv1_1_bn(net) 264 | feature_3 = F.grid_sample(net, p, padding_mode='border') # out : (B,C (of x), 1,1,sample_num) 265 | net = self.maxpool(net) 266 | 267 | net = self.actvn(self.conv_2(net)) 268 | net = self.actvn(self.conv_2_1(net)) 269 | net = self.conv2_1_bn(net) 270 | feature_4 = F.grid_sample(net, p, padding_mode='border') 271 | net = self.maxpool(net) 272 | 273 | net = self.actvn(self.conv_3(net)) 274 | net = self.actvn(self.conv_3_1(net)) 275 | net = self.conv3_1_bn(net) 276 | feature_5 = F.grid_sample(net, p, padding_mode='border') 277 | 278 | # here every channel corresponds to one feature. 279 | 280 | features = torch.cat((feature_0, feature_1, feature_2, feature_3, feature_4, feature_5), 281 | dim=1) # (B, features, 1,7,sample_num) 282 | shape = features.shape 283 | features = torch.reshape(features, 284 | (shape[0], shape[1] * shape[3], shape[4])) # (B, featues_per_sample, samples_num) 285 | #features = torch.cat((features, p_features), dim=1) # (B, featue_size, samples_num) 286 | 287 | net = self.actvn(self.fc_0(features)) 288 | net = self.actvn(self.fc_1(net)) 289 | net = self.actvn(self.fc_2(net)) 290 | net = self.fc_out(net) 291 | out = net.squeeze(1) 292 | 293 | return out 294 | 295 | 296 | 297 | 298 | # 3D Single View Reconsturction (for 256**3 input voxelization) -------------------------------------- 299 | # ---------------------------------------------------------------------------------------------------- 300 | 301 | class TEXR(nn.Module): 302 | 303 | 304 | def __init__(self, hidden_dim=256): 305 | super(TEXR, self).__init__() 306 | 307 | self.conv_in = nn.Conv3d(4, 16, 3, padding=1, padding_mode='border') # out: 256 ->m.p. 128 308 | self.conv_0 = nn.Conv3d(16, 32, 3, padding=1, padding_mode='border') # out: 128 309 | self.conv_0_1 = nn.Conv3d(32, 32, 3, padding=1, padding_mode='border') # out: 128 ->m.p. 64 310 | self.conv_1 = nn.Conv3d(32, 64, 3, padding=1, padding_mode='border') # out: 64 311 | self.conv_1_1 = nn.Conv3d(64, 64, 3, padding=1, padding_mode='border') # out: 64 -> mp 32 312 | self.conv_2 = nn.Conv3d(64, 128, 3, padding=1, padding_mode='border') # out: 32 313 | self.conv_2_1 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') # out: 32 -> mp 16 314 | self.conv_3 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') # out: 16 315 | self.conv_3_1 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') # out: 16 -> mp 8 316 | self.conv_4 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') # out: 8 317 | self.conv_4_1 = nn.Conv3d(128, 128, 3, padding=1, padding_mode='border') # out: 8 318 | 319 | feature_size = (4 + 16 + 32 + 64 + 128 + 128 + 128) * 7 + 3 320 | self.fc_0 = nn.Conv1d(feature_size, hidden_dim * 2, 1) 321 | self.fc_1 = nn.Conv1d(hidden_dim *2, hidden_dim, 1) 322 | self.fc_2 = nn.Conv1d(hidden_dim , hidden_dim, 1) 323 | self.fc_out = nn.Conv1d(hidden_dim, 3, 1) 324 | self.actvn = nn.ReLU() 325 | 326 | self.maxpool = nn.MaxPool3d(2) 327 | 328 | self.conv_in_bn = nn.BatchNorm3d(16) 329 | self.conv0_1_bn = nn.BatchNorm3d(32) 330 | self.conv1_1_bn = nn.BatchNorm3d(64) 331 | self.conv2_1_bn = nn.BatchNorm3d(128) 332 | self.conv3_1_bn = nn.BatchNorm3d(128) 333 | self.conv4_1_bn = nn.BatchNorm3d(128) 334 | 335 | 336 | displacment = 0.0722 337 | displacments = [] 338 | displacments.append([0, 0, 0]) 339 | for x in range(3): 340 | for y in [-1, 1]: 341 | input = [0, 0, 0] 342 | input[x] = y * displacment 343 | displacments.append(input) 344 | 345 | self.displacments = torch.Tensor(displacments).cuda() 346 | 347 | def forward(self, p, x): 348 | # x = x.unsqueeze(1) 349 | 350 | p_features = p.transpose(1, -1) 351 | p = p.unsqueeze(1).unsqueeze(1) 352 | p = torch.cat([p + d for d in self.displacments], dim=2) 353 | feature_0 = F.grid_sample(x, p, padding_mode='border') 354 | # print(feature_0.shape) 355 | # print(feature_0[:,:,:,0,0]) 356 | 357 | net = self.actvn(self.conv_in(x)) 358 | net = self.conv_in_bn(net) 359 | feature_1 = F.grid_sample(net, p, padding_mode='border') 360 | net = self.maxpool(net) #out 128 361 | 362 | net = self.actvn(self.conv_0(net)) 363 | net = self.actvn(self.conv_0_1(net)) 364 | net = self.conv0_1_bn(net) 365 | feature_2 = F.grid_sample(net, p, padding_mode='border') 366 | net = self.maxpool(net) #out 64 367 | 368 | net = self.actvn(self.conv_1(net)) 369 | net = self.actvn(self.conv_1_1(net)) 370 | net = self.conv1_1_bn(net) 371 | feature_3 = F.grid_sample(net, p, padding_mode='border') 372 | net = self.maxpool(net) 373 | 374 | net = self.actvn(self.conv_2(net)) 375 | net = self.actvn(self.conv_2_1(net)) 376 | net = self.conv2_1_bn(net) 377 | feature_4 = F.grid_sample(net, p, padding_mode='border') 378 | net = self.maxpool(net) 379 | 380 | net = self.actvn(self.conv_3(net)) 381 | net = self.actvn(self.conv_3_1(net)) 382 | net = self.conv3_1_bn(net) 383 | feature_5 = F.grid_sample(net, p, padding_mode='border') 384 | net = self.maxpool(net) 385 | 386 | net = self.actvn(self.conv_4(net)) 387 | net = self.actvn(self.conv_4_1(net)) 388 | net = self.conv4_1_bn(net) 389 | feature_6 = F.grid_sample(net, p, padding_mode='border') 390 | 391 | # here every channel corresponse to one feature. 392 | 393 | features = torch.cat((feature_0, feature_1, feature_2, feature_3, feature_4, feature_5, feature_6), 394 | dim=1) # (B, features, 1,7,sample_num) 395 | shape = features.shape 396 | features = torch.reshape(features,(shape[0], shape[1] * shape[3], shape[4])) # (B, featues_per_sample, samples_num) 397 | features = torch.cat((features, p_features), dim=1) # (B, featue_size, samples_num) samples_num 0->0,...,N->N 398 | # print('features: ', features[:,:,:3]) 399 | 400 | net = self.actvn(self.fc_0(features)) 401 | net = self.actvn(self.fc_1(net)) 402 | net = self.actvn(self.fc_2(net)) 403 | out = self.fc_out(net) 404 | # out = net.squeeze(1) 405 | 406 | return out -------------------------------------------------------------------------------- /models/training.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import torch 3 | import torch.optim as optim 4 | from torch.nn import functional as F 5 | import os 6 | from glob import glob 7 | import numpy as np 8 | 9 | 10 | 11 | 12 | class Trainer(object): 13 | 14 | def __init__(self, model, device, train_dataset, val_dataset, exp_name, optimizer='Adam'): 15 | self.model = model.to(device) 16 | self.device = device 17 | if optimizer == 'Adam': 18 | self.optimizer = optim.Adam(self.model.parameters(), lr=1e-4) 19 | if optimizer == 'Adadelta': 20 | self.optimizer = optim.Adadelta(self.model.parameters()) 21 | if optimizer == 'RMSprop': 22 | self.optimizer = optim.RMSprop(self.model.parameters(), momentum=0.9) 23 | 24 | self.train_dataset = train_dataset 25 | self.val_dataset = val_dataset 26 | self.exp_path = os.path.dirname(__file__) + '/../experiments/{}/'.format( exp_name) 27 | self.checkpoint_path = self.exp_path + 'checkpoints/' 28 | if not os.path.exists(self.checkpoint_path): 29 | print(self.checkpoint_path) 30 | os.makedirs(self.checkpoint_path) 31 | self.val_min = None 32 | 33 | 34 | def train_step(self,batch): 35 | self.model.train() 36 | self.optimizer.zero_grad() 37 | loss = self.compute_loss(batch) 38 | loss.backward() 39 | self.optimizer.step() 40 | 41 | return loss.item() 42 | 43 | def compute_loss(self,batch): 44 | device = self.device 45 | 46 | p = batch.get('grid_coords').to(device) 47 | gt_rgb = batch.get('rgb').to(device) 48 | inputs = batch.get('inputs').to(device) 49 | 50 | # print(gt_rgb) 51 | # p = p.unsqueeze(1).unsqueeze(1) 52 | # print(F.grid_sample(inputs, p, padding_mode='border')) 53 | # General points 54 | 55 | # print(p[:,:3]) 56 | pred_rgb = self.model(p,inputs) 57 | pred_rgb = pred_rgb.transpose(-1,-2) 58 | # print(gt_rgb.shape) 59 | loss_i = torch.nn.L1Loss(reduction='none')(pred_rgb, gt_rgb) # out = (B,num_points,3) 60 | 61 | loss = loss_i.sum(-1).mean() # loss_i summed 3 rgb channels for all #num_samples samples -> out = (B,1) and mean over batch -> out = (1) 62 | 63 | return loss 64 | 65 | def train_model(self, epochs): 66 | loss = 0 67 | start = self.load_checkpoint() 68 | 69 | for epoch in range(start, epochs): 70 | sum_loss = 0 71 | print('Start epoch {}'.format(epoch)) 72 | train_data_loader = self.train_dataset.get_loader() 73 | 74 | if epoch % 1 == 0: 75 | self.save_checkpoint(epoch) 76 | val_loss = self.compute_val_loss() 77 | 78 | if self.val_min is None: 79 | self.val_min = val_loss 80 | 81 | if val_loss < self.val_min: 82 | self.val_min = val_loss 83 | # for path in glob(self.exp_path + 'val_min=*'): 84 | # os.remove(path) 85 | np.save(self.exp_path + 'val_min',[epoch,val_loss]) 86 | 87 | 88 | for batch in train_data_loader: 89 | loss = self.train_step(batch) 90 | print("Current loss: {}".format(loss)) 91 | sum_loss += loss 92 | 93 | 94 | 95 | 96 | def save_checkpoint(self, epoch): 97 | path = self.checkpoint_path + 'checkpoint_epoch_{}.tar'.format(epoch) 98 | if not os.path.exists(path): 99 | torch.save({'epoch':epoch,'model_state_dict': self.model.state_dict(), 100 | 'optimizer_state_dict': self.optimizer.state_dict()}, path) 101 | 102 | def load_checkpoint(self): 103 | checkpoints = glob(self.checkpoint_path+'/*') 104 | if len(checkpoints) == 0: 105 | print('No checkpoints found at {}'.format(self.checkpoint_path)) 106 | return 0 107 | 108 | checkpoints = [os.path.splitext(os.path.basename(path))[0][17:] for path in checkpoints] 109 | checkpoints = np.array(checkpoints, dtype=int) 110 | checkpoints = np.sort(checkpoints) 111 | path = self.checkpoint_path + 'checkpoint_epoch_{}.tar'.format(checkpoints[-1]) 112 | 113 | print('Loaded checkpoint from: {}'.format(path)) 114 | checkpoint = torch.load(path) 115 | self.model.load_state_dict(checkpoint['model_state_dict']) 116 | self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) 117 | epoch = checkpoint['epoch'] 118 | return epoch 119 | 120 | def compute_val_loss(self): 121 | self.model.eval() 122 | 123 | sum_val_loss = 0 124 | num_batches = 15 125 | for _ in range(num_batches): 126 | try: 127 | val_batch = self.val_data_iterator.next() 128 | except: 129 | self.val_data_iterator = self.val_dataset.get_loader().__iter__() 130 | val_batch = self.val_data_iterator.next() 131 | 132 | sum_val_loss += self.compute_loss( val_batch).item() 133 | 134 | return sum_val_loss / num_batches -------------------------------------------------------------------------------- /sharp_teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchibane/if-net_texture/989ffdc9afc0296d92a55a7a1f155c1cc3e6e5e4/sharp_teaser.png -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | import models.local_model as model 2 | import models.dataloader as dataloader 3 | from models import training 4 | import argparse 5 | import torch 6 | import config.config_loader as cfg_loader 7 | 8 | parser = argparse.ArgumentParser( 9 | description='Train Model' 10 | ) 11 | 12 | parser.add_argument('config', type=str, help='Path to config file.') 13 | args = parser.parse_args() 14 | 15 | 16 | cfg = cfg_loader.load(args.config) 17 | 18 | net = model.get_models()[cfg['model']]() 19 | 20 | train_dataset = dataloader.VoxelizedDataset('train', cfg) 21 | 22 | val_dataset = dataloader.VoxelizedDataset('val', cfg) 23 | 24 | trainer = training.Trainer(net,torch.device("cuda"),train_dataset, val_dataset, cfg['folder_name'], optimizer=cfg['training']['optimizer']) 25 | trainer.train_model(1500) 26 | --------------------------------------------------------------------------------