master 99a59eeeefa0 cached
46 files
55.5 MB
531.8k tokens
259 symbols
1 requests
Download .txt
Showing preview only (2,127K chars total). Download the full file or copy to clipboard to get everything.
Repository: maria-korosteleva/Garment-Pattern-Estimation
Branch: master
Commit: 99a59eeeefa0
Files: 46
Total size: 55.5 MB

Directory structure:
gitextract_b1_orxx2/

├── .gitignore
├── LICENSE
├── ReadMe.md
├── docs/
│   ├── Installation.md
│   └── Running.md
├── models/
│   ├── att/
│   │   ├── att.yaml
│   │   ├── neural_tailor_panels.pth
│   │   ├── neural_tailor_stitch_model.pth
│   │   └── stitch_model.yaml
│   └── baseline/
│       ├── lstm_stitch_tags.pth
│       └── lstm_stitch_tags.yaml
├── nn/
│   ├── data/
│   │   ├── __init__.py
│   │   ├── datasets.py
│   │   ├── panel_classes.py
│   │   ├── pattern_converter.py
│   │   ├── transforms.py
│   │   ├── utils.py
│   │   └── wrapper.py
│   ├── data_configs/
│   │   ├── data_split_on_filtered_dataset.json
│   │   ├── data_split_on_full_dataset.json
│   │   ├── panel_classes_condenced.json
│   │   ├── panel_classes_plus_one.json
│   │   └── param_filter.json
│   ├── evaluation_scripts/
│   │   ├── maya_att_weights.py
│   │   ├── noise_levels.py
│   │   ├── on_test_set.py
│   │   └── predict_per_example.py
│   ├── example_configs/
│   │   ├── debug.yaml
│   │   ├── debug_server.yaml
│   │   ├── eval_wandb.yaml
│   │   └── stitch_model_debug.yaml
│   ├── experiment.py
│   ├── metrics/
│   │   ├── composed_loss.py
│   │   ├── eval_utils.py
│   │   ├── losses.py
│   │   └── metrics.py
│   ├── net_blocks.py
│   ├── nets.py
│   ├── train.py
│   ├── trainer.py
│   └── utility_scripts/
│       ├── download_dataset.py
│       ├── igl_sampling_test.py
│       ├── param_filter_test.py
│       └── upload_dataset_to_wandb.py
├── requirements.txt
└── system.template.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Jupyter tmp files 
.ipynb_checkpoints

# Weights and Biases logs -- no need to save
wandb
artifacts

# VSCode settings
*.code-workspace
.vscode
__pycache__

# PyCharm
.idea

# Compiled files
*.pyc

# Maya
.mayaSwatches

# custom settings -- to have customized local copy
system.json

# file for small script tries
tmp*
*tmp*.mb


# tmp data
Garment-Pattern-Data*

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Maria Korosteleva

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

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.


================================================
FILE: ReadMe.md
================================================
[![PWC](https://img.shields.io/endpoint.svg?url=https://paperswithcode.com/badge/neuraltailor-reconstructing-sewing-pattern/on-dataset-of-3d-garments-with-sewing)](https://paperswithcode.com/sota/on-dataset-of-3d-garments-with-sewing?p=neuraltailor-reconstructing-sewing-pattern)

# NeuralTailor: Reconstructing Sewing Pattern Structures from 3D Point Clouds of Garments

![Overview of the Neural Tailor Pipeline](img/header.png)

Official implementation of [NeuralTailor: Reconstructing Sewing Pattern Structures from 3D Point Clouds of Garments](https://arxiv.org/abs/2201.13063). Provides our pre-trained models, scripts to evalute them, and tools to train framework components from scratch.

| :zap:        Our NeuralTailor paper was accepted to SIGGRAPH 2022!   |
|----------------------------------------------------------------------|

## Dataset

For training and evaluation, NeuralTailor uses dataset created with [Garment-Pattern-Generator](https://github.com/maria-korosteleva/Garment-Pattern-Generator).
* Dataset is available from Zenodo: [Dataset of 3D Garments with Sewing Patterns](https://doi.org/10.5281/zenodo.5267549)

## Docs
Provided in `./docs` folder

1. Installtion instructions: [Installation](docs/Installation.md)
2. How to run training and evaluation: [Running](docs/Running.md)

## Citation

If you are using our system in your research, consider citing our paper.

```
@article{NeuralTailor2022,
  author = {Korosteleva, Maria and Lee, Sung-Hee},
  title = {NeuralTailor: Reconstructing Sewing Pattern Structures from 3D Point Clouds of Garments},
  year = {2022},
  issue_date = {July 2022},
  publisher = {Association for Computing Machinery},
  address = {New York, NY, USA},
  volume = {41},
  number = {4},
  doi = {10.1145/3528223.3530179},
  journal = {ACM Trans. Graph.},
  numpages = {16},
  keywords = {structured deep learning, sewing patterns, garment reconstruction}
}
```


## Contact
For bug reports, feature suggestion, and code-related questions, please [open an issue](https://github.com/maria-korosteleva/Garment-Pattern-Estimation/issues). 

For other inquires, contact the authors: 

* Maria Korosteleva ([mariako@kaist.ac.kr](mailto:mariako@kaist.ac.kr)) (Main point of contact). 

* Sung-Hee Lee ([sunghee.lee@kaist.ac.kr](mailto:sunghee.lee@kaist.ac.kr)).


================================================
FILE: docs/Installation.md
================================================
# Installation & Dependencies

This file describes the process of setting up the environment from scratch (with the working versions). Skip to the relevant sections as needed

## 1. Dataset

* Download the [Dataset of 3D Garments with Sewing Patterns](https://zenodo.org/record/5267549#.Yk__mMgzaUk) in order to train\evaluate NeuralTailor.
    > NOTE: For evaluation of pre-trained NeuralTailor on unseen types you only need the _test.zip_ part of the dataset. 

* Unpack all ZIP archives to the same directory, keeping the directory structure (every zip archive is a subfolder of your root). 

## 2. Basic environment: Miniconda
```
apt-get update && apt-get install -y wget

# conda: https://stackoverflow.com/questions/58269375/how-to-install-packages-with-miniconda-in-dockerfile
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && bash Miniconda3-latest-Linux-x86_64.sh -b

# Env variable 
export PATH="$HOME/miniconda3/bin:$PATH"

# Create enviroment
conda create -n Garments python=3.9
conda activate Garments
```

## 3. Dependencies

* Pytoch (with cudatools if cuda is available on the machine)

* (PyG)[https://pytorch-geometric.readthedocs.io/en/latest/] for graph layers. Note that installing with pip requires specifying installed versions of pytorch and cuda. The installation may not support all possible combinations.

* [libigl](https://github.com/libigl/libigl-python-bindings) needs installation with conda. You could also check other options on [their GitHub page](https://github.com/libigl/libigl-python-bindings)

* The rest of the requirements are provided in `requirements.txt`. They can be used with pip to install in one go: 
    ```
    pip install -r requirements.txt
    ```

Example set of commands for installation on Windows [the version are checked and should work]:

```
conda create -n Garments python=3.9
conda activate Garments

conda install pytorch==1.12.0 cudatoolkit=11.6 -c pytorch -c conda-forge

pip install torch-scatter torch-sparse torch-cluster torch-spline-conv torch-geometric -f https://data.pyg.org/whl/torch-1.12.0+cu116.html

conda install -c conda-forge igl=2.2.1

conda install pywin32   # requirement for wmi on Windows -- conda installation needed in conda environements
# The rest are in requirements.txt
pip install -r requirements.txt

```

Example set of commands for installation on Linux [Ubuntu]:
```
conda create -n Garments python=3.9
conda activate Garments

conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
# for pytorch-cuda, insert cuda version that is listed under nvidia-smi
# in this case, the cuda version is 12.1

conda install pyg -c pyg
conda install pytorch-cluster -c pyg

conda install -c conda-forge igl=2.2.1 svgwrite svglib wandb

pip install sparsemax entmax
```

Development was done on _Windows 10\11_ and Ubuntu. If running on other OS ends up with errors, please, raise an issue!

**Notes on errors with PIL.Image**

You might experience errors related with PIL (pillow) Image module. Those most often come from the ReportLab library requiring older versions of pillow that are currently available, the compatibility issues of pillow and python version, or ReportLab and libigl visualization routines requiring different versions of pillow. 

*Working combinations*:
* For ReportLab (saving patterns as png images) to work: 
    * Python 3.8.5 + ReportLab 3.5.53 + pillow 7.1.1
    * Python 3.8.5 + ReportLab 3.5.55 + pillow 7.1.1

## 4. (Optional) Weights & Biases account

We are using [Weights & Biases](https://wandb.ai/) for experiment tracking. 

You can use evalution scripts on provided models or train new models without having your own W&B account, but we recommend to create one -- then all the information of your training runs will be fully private and will be stored in your account forever. Anonymous runs are only retained for 7 days (as of April 2022).

The prompt to authenticate will appear the first time you run any of the scripts that use w&b library.

> NOTE: Anonimous runs are not yet supported by our tool. 


## 5. Custom dependencies access

Download [Garment-Pattern-Generator](https://github.com/maria-korosteleva/Garment-Pattern-Generator) code (for pattern loading)

```
git clone https://github.com/maria-korosteleva/Garment-Pattern-Generator
```

Add path to custom packages to PYTHONPATH for correct importing of our custom modules. For example, in the terminal:
```
export PYTHONPATH=$PYTHONPATH:/home/user/maria/Garment-Pattern-Generator/packages
```

### Filesystem paths & W&B account settings
* Fill out system.json file
Create system.json file in the root of this directory with your machine's file paths using system.template.json as a template. 
system.json should include the following: 
* path for creating logs at (including generated data from dataset generation routined & NN predictions) ('output')
    ```
    mkdir outputs
    ```
* path to the root directory with downloaded and unpacked per-type garment datasets to be used for training\evaluating of NN ('datasets_path') 
* username for wandb for correct experiment tracking ('wandb_username'). This is optional for evaluating saved models or running training (training will fall into anonymous mode). However, if you were using anonymous mode for training and want to use evaluation scripts with that run, please specify the temporary account name (printed when training and can be found in the run URL) in this field for correct URL construction.



================================================
FILE: docs/Running.md
================================================
# How to run NeuralTailor

Don't forget to follow the installation steps first: [Installation](Installation.md)

> ☝ **NOTE**: All the config files specify local paths relative to the root, so we recommend running all the commands from the root of the code directory to avoid path problems.

## Evaluation

`nn/evaluation_scripts/` contains several tools that performs evaluations of the trained models. Run the script of your intrest with `--help` option to get the information about parameters.

`maya_att_weights.py` is the exception, this is just a helper script to visualize attention weights predicted by a model (they are saved alongside the sewing pattern predictions) within Autodesk Maya environment


### Saved models VS Weights & Biases runs

Every evaluation scrips takes in a config file which describes the experiment to evaluate. The scripts can work with either models saved locally or W&B runs.

1. Locally saved models. We provide pre-trained NeuralTailor models (patter shape prediction and stitch model prediction) in `./models/` folder. Corresponding configuration files (e.g., `./models/att/att.yaml`) contain full information about hyperparameters, dataset, and paths to the pre-trained model weights. You can similarly create configuration files to work with locally saved models produced by your experiments.

2. Weights&Biases runs (easier for your trained models). When training a framework, all the experiment information is logged to W&B cloud. Evaluation scripts can work with those runs directly without a need to manually download models and fill configurations. 

    To run scripts with W&B runs simply provide the related info of project name, runs name and run id in the `experiment` section of configuration file, and specify `unseen_data_folders` of `dataset` section if evaluating on unseen garment types. What's specified in the rest of the config is irrelevant since it will overriden by the information from the cloud run. Here is an [example of such evaluation config](../nn/example_configs/eval_wandb.yaml).

### Tweaking evaluation parameters


`/nn/evaluation_scripts/on_test_set.py` allows updating some parameters of the dataset for evaluation purposes, e.g. add point cloud noise or evaluate on scan imitation version of the input garments.
To do so, specify new values in the `load_dataset(..)` function calls in the script. The script itself contains some examples.


### Examples of evaluation commands

Evaluate NeuralTailor pattern shape prediction model on seen garment types only:

```
python nn/evaluation_scripts/on_test_set.py -sh models/att/att.yaml
```

> NOTE: when evaluating only the pattern shape model without stitches, the stitches are transferred from the corresponding GT sewing patterns (if available) for convenience of loading and draping. 

Evaluate full NeuralTailor framework on unseen garment types and save sewing pattern predictions:

```
python nn/evaluation_scripts/on_test_set.py -sh models/att/att.yaml -st models/att/stitch_model.yaml --unseen --predict
```

Evaluate stitch model on previously saved sewing pattern predictions: 

```
python nn/evaluation_scripts/on_test_set.py -st models/att/stitch_model.yaml --pred_path /path/to/sewing/pathern/data 
```

Evalute baseline model, LSTM + Stitch tags, on seen data types (it will produce both pattern shape and stitch quality stats):

```
python nn/evaluation_scripts/on_test_set.py -st models/baseline/lstm_stitch_tags.yaml
```

### Output structure when running on test set

> NOTE: clarification added after reviewing the issue #11

When running evaluations on test set, the `on_test_set.py` script creates one output folder per step
* either one when prediction only panel shapes (nn_<set>_pred_shape_<timestamp>) or only stitches (nn_<set>_pred_stitched_<timestamp>)
* Or two folders when predicting panel shapes and their stitches 

More details:
* The shape prediction step created a folder to store predictions of the final panel shapes. Since this script is evaluating the model on existing data with available ground truth, it copies the panel names and stitching information from the ground truth patterns to the output patterns, if possible. This is done purely for convence of visual comparison with ground truth patterns and to enable simulation of this partial prediction
* The stitching prediction step is running after the shape prediction is fully completed, including the serialization of prediction results. Stitching prediction loads these predicted patterns from the folder created earlier or from the path supplied in parameters, uses them as a model input, and then stores its own prediction in a **new** folder to keep the original input files intact. 

The naming convention choice was unfortunate and creates confusion, we know =(


## Training

The training of NeuralTailor is two-step -- separately for Pattern Shape and Stitch Information models. 
You can use config files saved in `models/` as training configs.

1. Pattern Shape Regression Network training
    To run with our final NeuralTailor architecture setup simply run this command from the directory root: 
    ```
    python nn/train.py -c ./models/att/att.yaml
    ```
    Training on the full dataset will take 2-4 days depending on your hardware. 
2. Stitch training 
    * Runs after the Shape Regression Network
    * Update the name & id of your shape training run in the Stitch model config file, 'old_experiment' section. Setting this option enables training on the Pattern Shape predictions. Put the 'old_experiment' -> 'predictions' to False, or removing the 'old_experiment' section altogehter will result in training on GT sewing patterns.
    * Run: 
    ```
    python nn/train.py -c ./models/att/stitch_model.yaml
    ```

### Resuming training

The training script supports resuming of training runs (if stopped for any reason). Resume will be attempted automatically when running training script if `run_id` is specified in the input config. 

So, to resume a run, one need to simply specify the full info about the wandb run in `experiment` section of training config, for example:

```
experiment:
    project_name: Test-Garments-Reconstruction
    run_name: NeuralTailor-Debug
    run_id: uzcw54zu
```

`run_id` is a hash found in W&B URL of the run page, e.g. link for above run looks like `https://wandb.ai/wandb_username/Test-Garments-Reconstruction/runs/uzcw54zu`*

*Link is invalid and only provided as an example

### Reproducing other experiments reported in the paper

By modifying the configuration files for corresponding models one could reproduce the setups used in our reported experiments. Some examples:
* setting `dataset->filter_by_params` option to empty string or null will force the training process to use full dataset without filtering out the desing overlaps.
* changing the model class name (`NN->model`) to `GarmentFullPattern3D` will result in training of LSTM-based model with global latent space and LSTM-based pattern decoder (our baseline)
* adding `stitch, free_class` to `loss_components` and `quality_components` will enable training a model that predicts stitches using stitch tags as part of sewing pattern shape model.
* changing `dataset->panel_classification` to `./nn/data_configs/panel_classes_plus_one.json` will give you a run with alternative panel classes arrangement.

>**NOTE:** if the config changes are expected to affect the list of the datapoints used for training (changing `filter_by_params` or `max_datapoints_per_type`), the provided data splits into train\valid\test might become invalid. Remove `data_split->filename` to allow training process to create new split on the go. We only provide splits for dataset with and without parameter filtering (in `nn/data_configs`).

### Offline training

By default it sycronises the training run information with (Weights&Biases)[wandb.ai] cloud. To disable this sycronization (run offline), set your environemtal variable: 

```
WANDB_MODE="offline"
```
Source: [W&B Documentation](https://docs.wandb.ai/guides/track/launch#is-it-possible-to-save-metrics-offline-and-sync-them-to-w-and-b-later)

> **NOTE:** All secondary scripts (eveluation, stitch training) will require setting up configs for using locally saved models (as described above) to evaluate on these offline runs. 

================================================
FILE: models/att/att.yaml
================================================
# Training configuration for Pattern Shape prediction model 
# (part I of NeuralTailor)

experiment:
  project_name: Garments-Reconstruction
  run_name: NeuralTailor-Train
  run_id: 

# ----- Dataset-related properties -----
dataset:
  class: Garment3DPatternFullDataset   # Garment2DPatternDataset

  data_folders:
    - dress_sleeveless_2550
    - jumpsuit_sleeveless_2000
    - skirt_8_panels_1000
    - wb_pants_straight_1500
    - skirt_2_panels_1200
    - jacket_2200
    - tee_sleeveless_1800
    - wb_dress_sleeveless_2600
    - jacket_hood_2700
    - pants_straight_sides_1000
    - tee_2300
    - skirt_4_panels_1600

  unseen_data_folders:
    - jacket_hood_sleeveless_150
    - skirt_waistband_150
    - tee_hood_150
    - jacket_sleeveless_150
    - dress_150
    - jumpsuit_150
    - wb_jumpsuit_sleeveless_150

  # old_experiment:  # Specify info of earlier run to load data info or model from
  #   project_name: 
  #   run_name: 
  #   run_id: 
  #   stats: True   # Use data stats from previous training run in this one
  #   predictions: False  # use predictions of the previously trained model to train this one

  # Loadable parameters -- overrwitten if old_experiment is specified
  mesh_samples: 2000
  obj_filetag: sim  # scan
  max_pattern_len: 23   # Overridden if panel_classification is specified
  max_panel_len: 14
  max_num_stitches: 24   # when training with stitches
  element_size: 4
  rotation_size: 4
  translation_size: 3
  explicit_stitch_tags: False
  point_noise_w: 0

  max_datapoints_per_type: 5000  # 5000 > more then any type has, so it basically means using all the data.
                                 # This value can be reduced to reduce the training dataset size and hence 
                                 # the training time.
  panel_classification: ./nn/data_configs/panel_classes_condenced.json 
  filter_by_params: ./nn/data_configs/param_filter.json
  
  standardize:     # Remove this key to re-calculate data stats at training time
    f_scale: [16.351303100585938, 30.945703506469727, 9.60141944885254]
    f_shift: [0.037076108157634735, -28.06070327758789, 1.0775548219680786]
    gt_scale:
      outlines: [25.267892837524418, 31.298505783081055, 0.2677369713783264, 0.2352069765329361]
      rotations: [1.7071068286895752, 1.9238795042037964, 1.7071068286895752, 1]
      stitch_tags: [119.98278045654295, 156.0384521484375, 105.92605590820312]
      translations: [109.58930206298828, 98.27909088134766, 37.84679412841797]
    gt_shift:
      outlines: [0, 0, 0.14890235662460327, 0.05642016604542732]
      rotations: [-0.7071067690849304, -0.9238795042037964, -1, 0]
      stitch_tags: [-59.99139022827149, -78.12358856201172, -52.95616912841797]
      translations: [-55.255470275878906, -20.001333236694336, -17.086795806884766]

data_split:
  valid_per_type: 100
  test_per_type: 100
  random_seed: 10
  type: count
  # NOTE addining 'filename' property to the split will force the data 
  # to be loaded from that list instead of being randomly generated
  filename: ./nn/data_configs/data_split_on_filtered_dataset.json


# ----- Network Architecture --------
NN:
  pre-trained: ./models/att/neural_tailor_panels.pth

  model: GarmentSegmentPattern3D   # GarmentFullPattern3D for LSTM-based model 

  # Point Cloud Encoder
  feature_extractor: EdgeConvFeatures
  conv_depth: 2
  k_neighbors: 5
  EConv_hidden: 200
  EConv_hidden_depth: 2
  EConv_feature: 150
  EConv_aggr: max
  global_pool: mean
  skip_connections: True
  graph_pooling: False
  pool_ratio: 0.1

  # Attention
  local_attention: True

  # Panel Decoder
  panel_decoder: LSTMDecoderModule   # MLPDecoder
  panel_encoding_size: 250
  panel_hidden_size: 250
  panel_n_layers: 3
  lstm_init: kaiming_normal_

  # Pattern decoder (in GarmentFullPattern3D)
  pattern_decoder: LSTMDecoderModule  # MLPDecoder
  pattern_encoding_size: 250
  pattern_hidden_size: 250
  pattern_n_layers: 2

  stitch_tag_dim: 3

  # ----- Losses ----
  loss:
    loss_components:  [shape, loop, rotation, translation]  #  stitch, free_class, segmentation
    quality_components:  [shape, discrete, rotation, translation]  # stitch, free_class

    stitch_tags_margin: 0.3  
    stitch_hardnet_version: False

    loop_loss_weight: 1.
    segm_loss_weight: 0.05

    epoch_with_stitches: 40

    panel_origin_invariant_loss: False

    panel_order_inariant_loss: False  # False to use ordering as in the data_config
    epoch_with_order_matching: 0
    order_by: shape_translation   # placement, translation, stitches, shape_translation


# ------- Trainer -----
trainer: 
  batch_size: 30
  devices: [cuda:0]
  epochs: 350
  random_seed: 916143406
  learning_rate: 0.002
  optimizer: Adam
  weight_decay: 0
  lr_scheduling: 
    mode: 1cyclic
  early_stopping:
    window: 0.0001
    patience: 50
  with_visualization: True  # Log visualizations of predicted sewing patterns

================================================
FILE: models/att/neural_tailor_panels.pth
================================================
[File too large to display: 21.2 MB]

================================================
FILE: models/att/stitch_model.yaml
================================================
# Training configuration for Sitching infomration prediction model 
# (part II of NeuralTailor)

experiment:
  project_name: Garments-Reconstruction
  run_name: NeuralTailor-Stitch-Model

# ----- Dataset-related properties -----
dataset:
  class: GarmentStitchPairsDataset  

  data_folders:
    - dress_sleeveless_2550
    - jumpsuit_sleeveless_2000
    - skirt_8_panels_1000
    - wb_pants_straight_1500
    - skirt_2_panels_1200
    - jacket_2200
    - tee_sleeveless_1800
    - wb_dress_sleeveless_2600
    - jacket_hood_2700
    - pants_straight_sides_1000
    - tee_2300
    - skirt_4_panels_1600

  unseen_data_folders:
    - jacket_hood_sleeveless_150
    - skirt_waistband_150
    - tee_hood_150
    - jacket_sleeveless_150
    - dress_150
    - jumpsuit_150
    - wb_jumpsuit_sleeveless_150

  old_experiment:
    # Change project_name, run_name, run_id to the info of preferred panel shape prediction
    # experiment on W&B cloud (prioritized if both options given)
    # Or give the path to the locally saved experiment (config should contain path to the model file)
    local_path: ./models/att/att.yaml 
    project_name: null
    run_name: null
    run_id: null
    stats: False   # Use data stats from previous training run in this one
    predictions: True  # use predictions of the previously trained model to train this one
                       # Training on shape predictions gives more robust stitch model
    
  stitched_edge_pairs_num: 200
  non_stitched_edge_pairs_num: 200
  shuffle_pairs: True
  shuffle_pairs_order': True
  element_size: 16

  filter_by_params: ./nn/data_configs/param_filter.json

  standardize:
    f_scale:
      - 181.52200317382812
      - 222.4815673828125
      - 195.82733154296875
      - 179.66943359375
      - 223.83230590820312
      - 200.90460205078125
      - 1.0593518018722534
      - 7.085371971130371
      - 181.52200317382812
      - 222.4815673828125
      - 195.82733154296875
      - 179.66943359375
      - 223.83230590820312
      - 200.90460205078125
      - 1.0593518018722534
      - 7.085371971130371
    f_shift:
      - -92.12037658691406
      - -121.35892486572266
      - -104.3437042236328
      - -90.84518432617188
      - -123.41600036621094
      - -110.9675064086914
      - -0.036235958337783813
      - -3.5510005950927734
      - -92.12037658691406
      - -121.35892486572266
      - -104.3437042236328
      - -90.84518432617188
      - -123.41600036621094
      - -110.9675064086914
      - -0.036235958337783813
      - -3.5510005950927734

data_split:
  valid_per_type: 100
  test_per_type: 100
  random_seed: 10
  type: count
  # NOTE addining 'filename' property to the split will force the data 
  # to be loaded from that list, instead of being randomly generated
  filename: ./nn/data_configs/data_split_on_filtered_dataset.json


# ----- Network Architecture --------
NN:
  pre-trained: ./models/att/neural_tailor_stitch_model.pth

  model: StitchOnEdge3DPairs
  stitch_hidden_size: 200
  stitch_mlp_n_layers: 3

  # ----- Losses ----
  loss:
    loss_components:  [edge_pair_class] 
    quality_components:  [edge_pair_class, edge_pair_stitch_recall] 


# ------- Trainer -----
trainer: 
  batch_size: 30   # For stitch experiments it corresponds to number of garment examples in a batch.
                   # Number of edge pairs in a batch would be (batch_size x (stitched_edge_pairs_num + non_stitched_edge_pairs_num))
  devices: [cuda:0]
  epochs: 350
  random_seed: 10
  learning_rate: 0.002
  optimizer: Adam
  weight_decay: 0
  lr_scheduling: 
    mode: 1cyclic
  early_stopping:
    window: 0.0001
    patience: 50
  with_visualization: False  # don't have good visualization for stitches anyway

================================================
FILE: models/baseline/lstm_stitch_tags.pth
================================================
[File too large to display: 32.3 MB]

================================================
FILE: models/baseline/lstm_stitch_tags.yaml
================================================
# Training configuration for Pattern Shape prediction model 
# (part I of NeuralTailor)

experiment:
  project_name: Garments-Reconstruction
  run_name: LSTM-Stitch-Tags-Train
  run_id: 

# ----- Dataset-related properties -----
dataset:
  class: Garment3DPatternFullDataset   # Garment2DPatternDataset

  data_folders:
    - dress_sleeveless_2550
    - jumpsuit_sleeveless_2000
    - skirt_8_panels_1000
    - wb_pants_straight_1500
    - skirt_2_panels_1200
    - jacket_2200
    - tee_sleeveless_1800
    - wb_dress_sleeveless_2600
    - jacket_hood_2700
    - pants_straight_sides_1000
    - tee_2300
    - skirt_4_panels_1600

  unseen_data_folders:
    - jacket_hood_sleeveless_150
    - skirt_waistband_150
    - tee_hood_150
    - jacket_sleeveless_150
    - dress_150
    - jumpsuit_150
    - wb_jumpsuit_sleeveless_150

  # old_experiment:  # Specify info of earlier run to load data info or model from
  #   project_name: 
  #   run_name: 
  #   run_id: 
  #   stats: True   # Use data stats from previous training run in this one
  #   predictions: False  # use predictions of the previously trained model to train this one

  # Loadable parameters -- overrwitten if old_experiment is specified
  mesh_samples: 2000
  obj_filetag: sim  # scan
  max_pattern_len: 23   # Overridden if panel_classification is specified
  max_panel_len: 14
  max_num_stitches: 24   # when training with stitches
  element_size: 4
  rotation_size: 4
  translation_size: 3
  explicit_stitch_tags: False
  stitch_tag_size: 3
  point_noise_w: 0

  max_datapoints_per_type: 5000  # 5000 > more then any type has, so it basically means using all the data.
                                 # This value can be reduced to reduce the training dataset size and hence 
                                 # the training time.
  panel_classification: ./nn/data_configs/panel_classes_condenced.json 
  filter_by_params: ./nn/data_configs/param_filter.json
  
  standardize:     # Remove this key to re-calculate data stats at training time
    f_scale: [16.351303100585938, 30.945703506469727, 9.60141944885254]
    f_shift: [0.037076108157634735, -28.06070327758789, 1.0775548219680786]
    gt_scale:
      outlines: [25.267892837524418, 31.298505783081055, 0.2677369713783264, 0.2352069765329361]
      rotations: [1.7071068286895752, 1.9238795042037964, 1.7071068286895752, 1]
      stitch_tags: [119.98278045654295, 156.0384521484375, 105.92605590820312]
      translations: [109.58930206298828, 98.27909088134766, 37.84679412841797]
    gt_shift:
      outlines: [0, 0, 0.14890235662460327, 0.05642016604542732]
      rotations: [-0.7071067690849304, -0.9238795042037964, -1, 0]
      stitch_tags: [-59.99139022827149, -78.12358856201172, -52.95616912841797]
      translations: [-55.255470275878906, -20.001333236694336, -17.086795806884766]

data_split:
  valid_per_type: 100
  test_per_type: 100
  random_seed: 10
  type: count
  # NOTE addining 'filename' property to the split will force the data 
  # to be loaded from that list instead of being randomly generated
  filename: ./nn/data_configs/data_split_on_filtered_dataset.json


# ----- Network Architecture --------
NN:
  pre-trained: ./models/baseline/lstm_stitch_tags.pth

  model: GarmentFullPattern3D   # GarmentFullPattern3D for LSTM-based model 

  # Point Cloud Encoder
  feature_extractor: EdgeConvFeatures
  conv_depth: 2
  k_neighbors: 5
  EConv_hidden: 200
  EConv_hidden_depth: 2
  EConv_feature: 150
  EConv_aggr: max
  global_pool: mean
  skip_connections: False
  graph_pooling: False
  pool_ratio: 0.1

  # Attention
  local_attention: True

  # Panel Decoder
  panel_decoder: LSTMDecoderModule   # MLPDecoder
  panel_encoding_size: 250
  panel_hidden_size: 250
  panel_n_layers: 3
  lstm_init: kaiming_normal_

  # Pattern decoder (in GarmentFullPattern3D)
  pattern_decoder: LSTMDecoderModule  # MLPDecoder
  pattern_encoding_size: 250
  pattern_hidden_size: 250
  pattern_n_layers: 2

  stitch_tag_dim: 3

  # ----- Losses ----
  loss:
    loss_components:  [shape, loop, rotation, translation, stitch, free_class]  
    quality_components:  [shape, discrete, rotation, translation, stitch, free_class]

    stitch_tags_margin: 0.3  
    stitch_hardnet_version: False

    loop_loss_weight: 1.
    segm_loss_weight: 0.05

    epoch_with_stitches: 40

    panel_origin_invariant_loss: False

    panel_order_inariant_loss: False  # False to use ordering as in the data_config
    epoch_with_order_matching: 0
    order_by: shape_translation   # placement, translation, stitches, shape_translation


# ------- Trainer -----
trainer: 
  batch_size: 30
  devices: [cuda:0]
  epochs: 350
  random_seed: 916143406
  learning_rate: 0.002
  optimizer: Adam
  weight_decay: 0
  lr_scheduling: 
    mode: 1cyclic
  early_stopping:
    window: 0.0001
    patience: 150
  with_visualization: True  # Log visualizations of predicted sewing patterns

================================================
FILE: nn/data/__init__.py
================================================


""" Custom datasets & dataset wrapper (split & dataset manager) """

from data.datasets import Garment3DPatternFullDataset, GarmentStitchPairsDataset
from data.wrapper import DatasetWrapper
from data.utils import sample_points_from_meshes, save_garments_prediction
from data.pattern_converter import NNSewingPattern, InvalidPatternDefError, EmptyPanelError

================================================
FILE: nn/data/datasets.py
================================================
import json
import numpy as np
import os
from pathlib import Path
import shutil

import torch
from torch.utils.data import DataLoader, Dataset, Subset
import igl
# import meshplot  # when uncommented, could lead to problems with wandb run syncing

# My modules
from customconfig import Properties
from data.pattern_converter import NNSewingPattern, InvalidPatternDefError
import data.transforms as transforms
from data.panel_classes import PanelClasses

# --------------------- Datasets -------------------------

class BaseDataset(Dataset):
    """
        * Implements base interface for my datasets
        * Implements routines for datapoint retrieval, structure & cashing 
        (agnostic of the desired feature & GT structure representation)
    """
    def __init__(self, root_dir, start_config={'data_folders': []}, gt_caching=False, feature_caching=False, in_transforms=[]):
        """Kind of Universal init for my datasets
            * Expects that all incoming datasets are located in the same root directory
            * The names of dataset_folders to use should be provided in start_config
                (defining it in dict allows to load data list as property from previous experiments)
            * if cashing is enabled, datapoints will stay stored in memory on first call to them: might speed up data processing by reducing file reads"""
        self.root_path = Path(root_dir)
        self.config = {}
        self.update_config(start_config)
        self.config['class'] = self.__class__.__name__

        self.data_folders = start_config['data_folders']
        self.data_folders_nicknames = dict(zip(self.data_folders, self.data_folders))
        
        # list of items = subfolders
        self.datapoints_names = []
        self.dataset_start_ids = []  # (folder, start_id) tuples -- ordered by start id
        for data_folder in self.data_folders:
            _, dirs, _ = next(os.walk(self.root_path / data_folder))
            # dataset name as part of datapoint name
            datapoints_names = [data_folder + '/' + name for name in dirs]
            self.dataset_start_ids.append((data_folder, len(self.datapoints_names)))
            clean_list = self._clean_datapoint_list(datapoints_names, data_folder)
            if ('max_datapoints_per_type' in self.config
                    and self.config['max_datapoints_per_type'] is not None
                    and len(clean_list) > self.config['max_datapoints_per_type']):
                # There is no need to do random sampling of requested number of datapoints
                # The sample sewing patterns are randomly generated in the first place without particulat order
                # hence, simple slicing of elements would be equivalent to sampling them randomly from the list
                clean_list = clean_list[:self.config['max_datapoints_per_type']] 

            self.datapoints_names += clean_list
        self.dataset_start_ids.append((None, len(self.datapoints_names)))  # add the total len as item for easy slicing
        self.config['size'] = len(self)

        # cashing setup
        self.gt_cached = {}
        self.gt_caching = gt_caching
        if gt_caching:
            print('BaseDataset::Info::Storing datapoints ground_truth info in memory')
        self.feature_cached = {}
        self.feature_caching = feature_caching
        if feature_caching:
            print('BaseDataset::Info::Storing datapoints feature info in memory')

        # Use default tensor transform + the ones from input
        self.transforms = [transforms.SampleToTensor()] + in_transforms

        # statistics already there --> need to apply it
        if 'standardize' in self.config:
            self.standardize()

        # FORDEBUG -- access all the datapoints to pre-populate the cache of the data
        # self._renew_cache()

        # in\out sizes
        self._estimate_data_shape()

    def save_to_wandb(self, experiment):
        """Save data cofiguration to current expetiment run"""
        # config
        experiment.add_config('dataset', self.config)


    def update_transform(self, transform):
        """apply new transform when loading the data"""
        raise NotImplementedError('BaseDataset:Error:current transform support is poor')
        # self.transform = transform

    def __len__(self):
        """Number of entries in the dataset"""
        return len(self.datapoints_names)  

    def __getitem__(self, idx):
        """Called when indexing: read the corresponding data. 
        Does not support list indexing"""
        
        if torch.is_tensor(idx):  # allow indexing by tensors
            idx = idx.tolist()

        folder_elements = None  
        datapoint_name = self.datapoints_names[idx]

        features, ground_truth = self._get_sample_info(datapoint_name)
        
        folder, name = tuple(datapoint_name.split('/'))
        sample = {'features': features, 'ground_truth': ground_truth, 'name': name, 'data_folder': folder}

        # apply transfomations (equally to samples from files or from cache)
        for transform in self.transforms:
            sample = transform(sample)

        return sample

    def update_config(self, in_config):
        """Define dataset configuration:
            * to be part of experimental setup on wandb
            * Control obtainign values for datapoints"""
        self.config.update(in_config)

        # check the correctness of provided list of datasets
        if ('data_folders' not in self.config 
                or not isinstance(self.config['data_folders'], list)
                or len(self.config['data_folders']) == 0):
            raise RuntimeError('BaseDataset::Error::information on datasets (folders) to use is missing in the incoming config')

        self._update_on_config_change()

    def _drop_cache(self):
        """Clean caches of datapoints info"""
        self.gt_cached = {}
        self.feature_cached = {}

    def _renew_cache(self):
        """Flush the cache and re-fill it with updated information if any kind of caching is enabled"""
        self.gt_cached = {}
        self.feature_cached = {}
        if self.feature_caching or self.gt_caching:
            for i in range(len(self)):
                self[i]
            print('Data cached!')

    def indices_by_data_folder(self, index_list):
        """
            Separate provided indices according to dataset folders used in current dataset
        """
        ids_dict = dict.fromkeys(self.data_folders)  # consists of elemens of index_list
        ids_mapping_dict = dict.fromkeys(self.data_folders)  # reference to the elements in index_list
        index_list = np.array(index_list)
        
        # assign by comparing with data_folders start & end ids
        # enforce sort Just in case
        self.dataset_start_ids = sorted(self.dataset_start_ids, key=lambda idx: idx[1])

        for i in range(0, len(self.dataset_start_ids) - 1):
            ids_filter = (index_list >= self.dataset_start_ids[i][1]) & (index_list < self.dataset_start_ids[i + 1][1])
            ids_dict[self.dataset_start_ids[i][0]] = index_list[ids_filter]
            ids_mapping_dict[self.dataset_start_ids[i][0]] = np.flatnonzero(ids_filter)
        
        return ids_dict, ids_mapping_dict

    def subsets_per_datafolder(self, index_list=None):
        """
            Group given indices by datafolder and Return dictionary with Subset objects for each group.
            if None, a breakdown for the full dataset is given
        """
        if index_list is None:
            index_list = range(len(self))
        per_data, _ = self.indices_by_data_folder(index_list)
        breakdown = {}
        for folder, ids_list in per_data.items():
            breakdown[self.data_folders_nicknames[folder]] = Subset(self, ids_list)
        return breakdown

    def random_split_by_dataset(self, valid_per_type, test_per_type=0, split_type='count', with_breakdown=False):
        """
            Produce subset wrappers for training set, validations set, and test set (if requested)
            Supported split_types: 
                * split_type='percent' takes a given percentage of the data for evaluation subsets. It also ensures the equal 
                proportions of elements from each datafolder in each subset -- according to overall proportions of 
                datafolders in the whole dataset
                * split_type='count' takes this exact number of elements for the elevaluation subselts from each datafolder. 
                    Maximizes the use of training elements, and promotes fair evaluation on uneven datafolder distribution. 

        Note: 
            * it's recommended to shuffle the training set on batching as random permute is not 
              guaranteed in this function
        """

        if split_type != 'count' and split_type != 'percent':
            raise NotImplementedError('{}::Error::Unsupported split type <{}> requested'.format(
                self.__class__.__name__, split_type))

        train_ids, valid_ids, test_ids = [], [], []

        train_breakdown, valid_breakdown, test_breakdown = {}, {}, {}

        for dataset_id in range(len(self.data_folders)):
            folder_nickname = self.data_folders_nicknames[self.data_folders[dataset_id]]

            start_id = self.dataset_start_ids[dataset_id][1]
            end_id = self.dataset_start_ids[dataset_id + 1][1]   # marker of the dataset end included
            data_len = end_id - start_id

            permute = (torch.randperm(data_len) + start_id).tolist()

            # size defined according to requested type
            valid_size = int(data_len * valid_per_type / 100) if split_type == 'percent' else valid_per_type
            test_size = int(data_len * test_per_type / 100) if split_type == 'percent' else test_per_type

            train_size = data_len - valid_size - test_size

            train_sub, valid_sub = permute[:train_size], permute[train_size:train_size + valid_size]

            train_ids += train_sub
            valid_ids += valid_sub

            if test_size:
                test_sub = permute[train_size + valid_size:train_size + valid_size + test_size]
                test_ids += test_sub
            
            if with_breakdown:
                train_breakdown[folder_nickname] = Subset(self, train_sub)
                valid_breakdown[folder_nickname] = Subset(self, valid_sub)
                test_breakdown[folder_nickname] = Subset(self, test_sub) if test_size else None

        if with_breakdown:
            return (
                Subset(self, train_ids), 
                Subset(self, valid_ids),
                Subset(self, test_ids) if test_per_type else None, 
                train_breakdown, valid_breakdown, test_breakdown
            )
            
        return (
            Subset(self, train_ids), 
            Subset(self, valid_ids),
            Subset(self, test_ids) if test_size else None
        )

    def split_from_dict(self, split_dict, with_breakdown=False):
        """
            Reproduce the data split in the provided dictionary: 
            the elements of the currect dataset should play the same role as in provided dict
        """
        train_ids, valid_ids, test_ids = [], [], []
        train_breakdown, valid_breakdown, test_breakdown = {}, {}, {}

        training_datanames = set(split_dict['training'])
        valid_datanames = set(split_dict['validation'])
        test_datanames = set(split_dict['test'])
        
        for idx in range(len(self.datapoints_names)):
            if self.datapoints_names[idx] in training_datanames:  # usually the largest, so check first
                train_ids.append(idx)
            elif len(test_datanames) > 0 and self.datapoints_names[idx] in test_datanames:
                test_ids.append(idx)
            elif len(valid_datanames) > 0 and self.datapoints_names[idx] in valid_datanames:
                valid_ids.append(idx)
            # othervise, just skip

        if with_breakdown:
            train_breakdown = self.subsets_per_datafolder(train_ids)
            valid_breakdown = self.subsets_per_datafolder(valid_ids)
            test_breakdown = self.subsets_per_datafolder(test_ids)

            return (
                Subset(self, train_ids), 
                Subset(self, valid_ids),
                Subset(self, test_ids) if len(test_ids) > 0 else None,
                train_breakdown, valid_breakdown, test_breakdown
            )

        return (
            Subset(self, train_ids), 
            Subset(self, valid_ids),
            Subset(self, test_ids) if len(test_ids) > 0 else None
        )

    # -------- Data-specific functions --------
    def save_prediction_batch(self, *args, **kwargs):
        """Saves predicted params of the datapoint to the original data folder"""
        print('{}::Warning::No prediction saving is implemented'.format(self.__class__.__name__))

    def standardize(self, training=None):
        """Use element normalization/standardization based on stats from the training subset.
            Dataset is the object most aware of the datapoint structure hence it's the place to calculate & use the normalization.
            Uses either of two: 
            * training subset to calculate the data statistics -- the stats are only based on training subsection of the data
            * if stats info is already defined in config, it's used instead of calculating new statistics (usually when calling to restore dataset from existing experiment)
            configuration has a priority: if it's given, the statistics are NOT recalculated even if training set is provided:
                this allows to save some time
        """
        print('{}::Warning::No standardization is implemented'.format(self.__class__.__name__))

    def _clean_datapoint_list(self, datapoints_names, dataset_folder):
        """Remove non-datapoints subfolders, failing cases, etc. Children are to override this function when needed"""
        # See https://stackoverflow.com/questions/57042695/calling-super-init-gives-the-wrong-method-when-it-is-overridden
        return datapoints_names

    def _get_sample_info(self, datapoint_name):
        """
            Get features and Ground truth prediction for requested data example
        """
        if datapoint_name in self.gt_cached:  # might not be compatible with list indexing
            ground_truth = self.gt_cached[datapoint_name]
        else:
            ground_truth = np.array([0])

            if self.gt_caching:
                self.gt_cached[datapoint_name] = ground_truth
        
        if datapoint_name in self.feature_cached:
            features = self.feature_cached[datapoint_name]
        else:
            features = np.array([0])
            
            if self.feature_caching:  # save read values 
                self.feature_cached[datapoint_name] = features
        
        return features, ground_truth

    def _estimate_data_shape(self):
        """Get sizes/shapes of a datapoint for external references"""
        elem = self[0]
        feature_size = elem['features'].shape[0]
        gt_size = elem['ground_truth'].shape[0] if hasattr(elem['ground_truth'], 'shape') else None

        self.config['feature_size'], self.config['ground_truth_size'] = feature_size, gt_size

    def _update_on_config_change(self):
        """Update object inner state after config values have changed"""
        pass


class GarmentBaseDataset(BaseDataset):
    """Base class to work with data from custom garment datasets"""
        
    def __init__(self, root_dir, start_config={'data_folders': []}, gt_caching=False, feature_caching=False, in_transforms=[]):
        """
            Initialize dataset of garments with patterns
            * the list of dataset folders to use should be supplied in start_config!!!
            * the initial value is only given for reference
        """
        # initialize keys for correct dataset initialization
        if ('max_pattern_len' not in start_config 
                or 'max_panel_len' not in start_config
                or 'max_num_stitches' not in start_config):
            start_config.update(max_pattern_len=None, max_panel_len=None, max_num_stitches=None)
            pattern_size_initialized = False
        else:
            pattern_size_initialized = True

        if 'obj_filetag' not in start_config:
            start_config['obj_filetag'] = 'sim'  # look for objects with this tag in filename when loading 3D models

        if 'panel_classification' not in start_config:
            start_config['panel_classification'] = None
        self.panel_classifier = None

        super().__init__(root_dir, start_config, gt_caching=gt_caching, feature_caching=feature_caching, in_transforms=in_transforms)

        # To make sure the datafolder names are unique after updates
        all_nicks = self.data_folders_nicknames.values()
        if len(all_nicks) > len(set(all_nicks)):
            print('{}::Warning::Some data folder nicknames are not unique: {}. Reverting to the use of original folder names'.format(
                self.__class__.__name__, self.data_folders_nicknames
            ))
            self.data_folders_nicknames = dict(zip(self.data_folders, self.data_folders))

        # Load panel classifier
        if self.config['panel_classification'] is not None:
            self.panel_classifier = PanelClasses(self.config['panel_classification'])
            self.config.update(max_pattern_len=len(self.panel_classifier))

        # evaluate base max values for number of panels, number of edges in panels among pattern in all the datasets
        # NOTE: max_pattern_len is influcened by presence or abcense of self.panel_classifier
        if not pattern_size_initialized:
            num_panels = []
            num_edges_in_panel = []
            num_stitches = []
            for data_folder, start_id in self.dataset_start_ids:
                if data_folder is None: 
                    break

                datapoint = self.datapoints_names[start_id]
                folder_elements = [file.name for file in (self.root_path / datapoint).glob('*')]
                pattern_flat, _, _, stitches, _ = self._read_pattern(datapoint, folder_elements, with_stitches=True)  # just the edge info needed
                num_panels.append(pattern_flat.shape[0])
                num_edges_in_panel.append(pattern_flat.shape[1])  # after padding
                num_stitches.append(stitches.shape[1])

            self.config.update(
                max_pattern_len=max(num_panels),
                max_panel_len=max(num_edges_in_panel),
                max_num_stitches=max(num_stitches)
            )

        # to make sure that all the new datapoints adhere to evaluated structure!
        self._drop_cache() 
     
    def save_to_wandb(self, experiment):
        """Save data cofiguration to current expetiment run"""
        super().save_to_wandb(experiment)

        # dataset props files
        for dataset_folder in self.data_folders:
            try:
                shutil.copy(
                    self.root_path / dataset_folder / 'dataset_properties.json', 
                    experiment.local_wandb_path() / (dataset_folder + '_properties.json'))
            except FileNotFoundError:
                pass
        
        # panel classes
        if self.panel_classifier is not None:
            shutil.copy(
                self.panel_classifier.filename, 
                experiment.local_wandb_path() / ('panel_classes.json'))

        # param filter file
        if 'filter_by_params' in self.config and self.config['filter_by_params']:
            shutil.copy(
                self.config['filter_by_params'], 
                experiment.local_wandb_path() / ('param_filter.json'))
    
    # ------ Garment Data-specific basic functions --------
    def _clean_datapoint_list(self, datapoints_names, dataset_folder):
        """
            Remove all elements marked as failure from the provided list
            Updates the currect dataset nickname as a small sideeffect
        """

        try: 
            datapoints_names.remove(dataset_folder + '/renders')  # TODO read ignore list from props
        except ValueError:  # it's ok if there is no subfolder for renders
            pass

        try: 
            dataset_props = Properties(self.root_path / dataset_folder / 'dataset_properties.json')
        except FileNotFoundError:
            # missing dataset props file -- skip failure processing
            print(f'{self.__class__.__name__}::Warning::No `dataset_properties.json` found. Using all datapoints without filtering.')
            self.data_folders_nicknames[dataset_folder] = dataset_folder
            return datapoints_names

        if not dataset_props['to_subfolders']:
            raise NotImplementedError('Only working with datasets organized with subfolders')

        # NOTE A little side-effect here, since we are loading the dataset_properties anyway
        self.data_folders_nicknames[dataset_folder] = dataset_props['templates'].split('/')[-1].split('.')[0]

        fails_dict = dataset_props['sim']['stats']['fails']
        # TODO allow not to ignore some of the subsections
        for subsection in fails_dict:
            for fail in fails_dict[subsection]:
                try:
                    datapoints_names.remove(dataset_folder + '/' + fail)
                except ValueError:  # if fail was already removed based on previous failure subsection
                    pass
        
        # filter by parameters
        if 'filter_by_params' in self.config and self.config['filter_by_params']:
            datapoints_names = self.filter_by_params(
                self.config['filter_by_params'], dataset_folder, datapoints_names)

        return datapoints_names

    def filter_by_params(self, filter_file, dataset_folder, datapoint_names):
        """ Remove from considerstion datapoint that don't pass the parameter filter

            * filter_file -- path to .json file with allowed parameter ranges
            * dataset_folder -- data folder to filter
            * datapoint_names -- list of samples to apply filter to
        """
        with open(filter_file, 'r') as f:
            param_filters = json.load(f)

        final_list = []
        for datapoint_name in datapoint_names:
            pattern = NNSewingPattern(self.root_path / datapoint_name / 'specification.json')
            template_name = self.template_name(datapoint_name)
            to_add = True
            for param in param_filters[template_name]:
                value = pattern.parameters[param]['value']
                if value < param_filters[template_name][param][0] or value > param_filters[template_name][param][1]:
                    to_add = False
                    break
            if to_add:
                final_list.append(datapoint_name)
        
        print(f'{self.__class__.__name__}::Filtering::{dataset_folder}::{len(final_list)} of {len(datapoint_names)}')

        return final_list

    # ------------- Datapoints Utils --------------
    def template_name(self, datapoint_name):
        """Get name of the garment template from the path to the datapoint"""
        return self.data_folders_nicknames[datapoint_name.split('/')[0]]

    def _read_pattern(self, datapoint_name, folder_elements, 
                      pad_panels_to_len=None, pad_panel_num=None, pad_stitches_num=None,
                      with_placement=False, with_stitches=False, with_stitch_tags=False):
        """Read given pattern in tensor representation from file"""
        spec_list = [file for file in folder_elements if 'specification.json' in file]
        if not spec_list:
            raise RuntimeError('GarmentBaseDataset::Error::*specification.json not found for {}'.format(datapoint_name))
        
        pattern = NNSewingPattern(
            self.root_path / datapoint_name / spec_list[0], 
            panel_classifier=self.panel_classifier, 
            template_name=self.template_name(datapoint_name))
        return pattern.pattern_as_tensors(
            pad_panels_to_len, pad_panels_num=pad_panel_num, pad_stitches_num=pad_stitches_num,
            with_placement=with_placement, with_stitches=with_stitches, 
            with_stitch_tags=with_stitch_tags)

    # -------- Generalized Utils -----
    def _unpad(self, element, tolerance=1.e-5):
        """Return copy of input element without padding from given element. Used to unpad edge sequences in pattern-oriented datasets"""
        # NOTE: might be some false removal of zero edges in the middle of the list.
        if torch.is_tensor(element):        
            bool_matrix = torch.isclose(element, torch.zeros_like(element), atol=tolerance)  # per-element comparison with zero
            selection = ~torch.all(bool_matrix, axis=1)  # only non-zero rows
        else:  # numpy
            selection = ~np.all(np.isclose(element, 0, atol=tolerance), axis=1)  # only non-zero rows
        return element[selection]

    def _get_distribution_stats(self, input_batch, padded=False):
        """Calculates mean & std values for the input tenzor along the last dimention"""

        input_batch = input_batch.view(-1, input_batch.shape[-1])
        if padded:
            input_batch = self._unpad(input_batch)  # remove rows with zeros

        # per dimention means
        mean = input_batch.mean(axis=0)
        # per dimention stds
        stds = ((input_batch - mean) ** 2).sum(0)
        stds = torch.sqrt(stds / input_batch.shape[0])

        return mean, stds

    def _get_norm_stats(self, input_batch, padded=False):
        """Calculate shift & scaling values needed to normalize input tenzor 
            along the last dimention to [0, 1] range"""
        input_batch = input_batch.view(-1, input_batch.shape[-1])
        if padded:
            input_batch = self._unpad(input_batch)  # remove rows with zeros

        # per dimention info
        min_vector, _ = torch.min(input_batch, dim=0)
        max_vector, _ = torch.max(input_batch, dim=0)
        scale = torch.empty_like(min_vector)

        # avoid division by zero
        for idx, (tmin, tmax) in enumerate(zip(min_vector, max_vector)): 
            if torch.isclose(tmin, tmax):
                scale[idx] = tmin if not torch.isclose(tmin, torch.zeros(1)) else 1.
            else:
                scale[idx] = tmax - tmin
        
        return min_vector, scale


class Garment3DPatternFullDataset(GarmentBaseDataset):
    """Dataset with full pattern definition as ground truth
        * it includes not only every panel outline geometry, but also 3D placement and stitches information
        Defines 3D samples from the point cloud as features
    """
    def __init__(self, root_dir, start_config={'data_folders': []}, gt_caching=False, feature_caching=False, in_transforms=[]):
        if 'mesh_samples' not in start_config:
            start_config['mesh_samples'] = 2000  # default value if not given -- a bettern gurantee than a default value in func params
        if 'point_noise_w' not in start_config:
            start_config['point_noise_w'] = 0  # default value if not given -- a bettern gurantee than a default value in func params
        
        # to cache segmentation mask if enabled
        self.segm_cached = {}

        super().__init__(root_dir, start_config, 
                         gt_caching=gt_caching, feature_caching=feature_caching, in_transforms=in_transforms)
        
        self.config.update(
            element_size=self[0]['ground_truth']['outlines'].shape[2],
            rotation_size=self[0]['ground_truth']['rotations'].shape[1],
            translation_size=self[0]['ground_truth']['translations'].shape[1],
            stitch_tag_size=self[0]['ground_truth']['stitch_tags'].shape[-1],
            explicit_stitch_tags=False
        )
    
    def standardize(self, training=None):
        """Use shifting and scaling for fitting data to interval comfortable for NN training.
            Accepts either of two inputs: 
            * training subset to calculate the data statistics -- the stats are only based on training subsection of the data
            * if stats info is already defined in config, it's used instead of calculating new statistics (usually when calling to restore dataset from existing experiment)
            configuration has a priority: if it's given, the statistics are NOT recalculated even if training set is provided
                => speed-up by providing stats or speeding up multiple calls to this function
        """
        print('Garment3DPatternFullDataset::Using data normalization for features & ground truth')

        if 'standardize' in self.config:
            print('{}::Using stats from config'.format(self.__class__.__name__))
            stats = self.config['standardize']
        elif training is not None:
            loader = DataLoader(training, batch_size=len(training), shuffle=False)
            for batch in loader:
                feature_shift, feature_scale = self._get_distribution_stats(batch['features'], padded=False)

                gt = batch['ground_truth']
                panel_shift, panel_scale = self._get_distribution_stats(gt['outlines'], padded=True)
                # NOTE mean values for panels are zero due to loop property 
                # panel components SHOULD NOT be shifted to keep the loop property intact 
                panel_shift[0] = panel_shift[1] = 0

                # Use min\scale (normalization) instead of Gaussian stats for translation
                # No padding as zero translation is a valid value
                transl_min, transl_scale = self._get_norm_stats(gt['translations'])
                rot_min, rot_scale = self._get_norm_stats(gt['rotations'])

                # stitch tags if given
                st_tags_min, st_tags_scale = self._get_norm_stats(gt['stitch_tags'])

                break  # only one batch out there anyway

            self.config['standardize'] = {
                'f_shift': feature_shift.cpu().numpy(), 
                'f_scale': feature_scale.cpu().numpy(),
                'gt_shift': {
                    'outlines': panel_shift.cpu().numpy(), 
                    'rotations': rot_min.cpu().numpy(),
                    'translations': transl_min.cpu().numpy(), 
                    'stitch_tags': st_tags_min.cpu().numpy()
                },
                'gt_scale': {
                    'outlines': panel_scale.cpu().numpy(), 
                    'rotations': rot_scale.cpu().numpy(),
                    'translations': transl_scale.cpu().numpy(),
                    'stitch_tags': st_tags_scale.cpu().numpy()
                }
            }
            stats = self.config['standardize']
        else:  # nothing is provided
            raise ValueError('Garment3DPatternFullDataset::Error::Standardization cannot be applied: supply either stats in config or training set to use standardization')

        # clean-up tranform list to avoid duplicates
        self.transforms = [t for t in self.transforms if not isinstance(t, transforms.GTtandartization) and not isinstance(t, transforms.FeatureStandartization)]

        self.transforms.append(transforms.GTtandartization(stats['gt_shift'], stats['gt_scale']))
        self.transforms.append(transforms.FeatureStandartization(stats['f_shift'], stats['f_scale']))

    # ----- Saving predictions -----
    def save_prediction_batch(
            self, predictions, datanames, data_folders, 
            save_to, features=None, weights=None, orig_folder_names=False, **kwargs):
        """ 
            Saving predictions on batched from the current dataset
            Saves predicted params of the datapoint to the requested data folder.
            Returns list of paths to files with prediction visualizations
            Assumes that the number of predictions matches the number of provided data names"""

        save_to = Path(save_to)
        prediction_imgs = []
        for idx, (name, folder) in enumerate(zip(datanames, data_folders)):

            # "unbatch" dictionary
            prediction = {}
            for key in predictions:
                prediction[key] = predictions[key][idx]

            # add values from GT if not present in prediction
            if (('order_matching' in self.config and self.config['order_matching'])
                    or 'origin_matching' in self.config and self.config['origin_matching']
                    or not self.gt_caching):
                print(f'{self.__class__.__name__}::Warning::Propagating '
                      'information from GT on prediction is not implemented in given context')
            else:
                gt = self.gt_cached[folder + '/' + name]
                for key in gt:
                    if key not in prediction:
                        prediction[key] = gt[key]

            # Transform to pattern object
            pattern = self._pred_to_pattern(prediction, name)

            # log gt number of panels
            if self.gt_caching:
                gt = self.gt_cached[folder + '/' + name]
                pattern.spec['properties']['correct_num_panels'] = gt['num_panels']

            # save prediction
            folder_nick = self.data_folders_nicknames[folder] if not orig_folder_names else folder

            try: 
                final_dir = pattern.serialize(save_to / folder_nick, to_subfolder=True, tag='_predicted_')
            except (RuntimeError, InvalidPatternDefError, TypeError) as e:
                print('Garment3DPatternDataset::Error::{} serializing skipped: {}'.format(name, e))
                continue
            
            final_file = pattern.name + '_predicted__pattern.png'
            prediction_imgs.append(Path(final_dir) / final_file)

            # copy originals for comparison
            for file in (self.root_path / folder / name).glob('*'):
                if ('.png' in file.suffix) or ('.json' in file.suffix):
                    shutil.copy2(str(file), str(final_dir))

            # save point samples if given 
            if features is not None:
                shift = self.config['standardize']['f_shift']
                scale = self.config['standardize']['f_scale']
                point_cloud = features[idx] * scale + shift

                np.savetxt(
                    save_to / folder_nick / name / (name + '_point_cloud.txt'), 
                    point_cloud
                )
            # save per-point weights if given
            if 'att_weights' in prediction:
                np.savetxt(
                    save_to / folder_nick / name / (name + '_att_weights.txt'), 
                    prediction['att_weights'].cpu().numpy()
                )
                    
        return prediction_imgs

    def _pred_to_pattern(self, prediction, dataname):
        """Convert given predicted value to pattern object
        """

        # undo standardization  (outside of generinc conversion function due to custom std structure)
        gt_shifts = self.config['standardize']['gt_shift']
        gt_scales = self.config['standardize']['gt_scale']
        for key in gt_shifts:
            if key == 'stitch_tags' and not self.config['explicit_stitch_tags']:  
                # ignore stitch tags update if explicit tags were not used
                continue
            prediction[key] = prediction[key].cpu().numpy() * gt_scales[key] + gt_shifts[key]

        # recover stitches
        if 'stitches' in prediction:  # if somehow prediction already has an answer
            stitches = prediction['stitches']
        else:  # stitch tags to stitch list
            stitches = self.tags_to_stitches(
                torch.from_numpy(prediction['stitch_tags']) if isinstance(prediction['stitch_tags'], np.ndarray) else prediction['stitch_tags'],
                prediction['free_edges_mask']
            )

        # Construct the pattern from the data
        pattern = NNSewingPattern(view_ids=False, panel_classifier=self.panel_classifier)
        pattern.name = dataname
        try: 
            pattern.pattern_from_tensors(
                prediction['outlines'], 
                panel_rotations=prediction['rotations'],
                panel_translations=prediction['translations'], 
                stitches=stitches,
                padded=True)   
        except (RuntimeError, InvalidPatternDefError) as e:
            print('Garment3DPatternDataset::Warning::{}: {}'.format(dataname, e))
            pass

        return pattern

    # ----- Sample -----
    def _get_sample_info(self, datapoint_name):
        """
            Get features and Ground truth prediction for requested data example
        """
        folder_elements = [file.name for file in (self.root_path / datapoint_name).glob('*')]  # all files in this directory

        # features -- points
        if datapoint_name in self.feature_cached:
            points = self.feature_cached[datapoint_name]
            segm = self.segm_cached[datapoint_name]
        else:
            points, verts = self._sample_points(datapoint_name, folder_elements)

            # Segmentation
            segm = self._point_classes_from_mesh(
                points, verts, datapoint_name, folder_elements)
            
            if self.feature_caching:  # save read values 
                self.feature_cached[datapoint_name] = points
                self.segm_cached[datapoint_name] = segm
        
        # GT -- pattern and segmentation
        if datapoint_name in self.gt_cached:  # might not be compatible with list indexing
            ground_truth = self.gt_cached[datapoint_name]
        else:
            ground_truth = self._get_pattern_ground_truth(datapoint_name, folder_elements)
            ground_truth['segmentation'] = segm

            if self.gt_caching:
                self.gt_cached[datapoint_name] = ground_truth

        return points, ground_truth
      
    def _get_pattern_ground_truth(self, datapoint_name, folder_elements):
        """Get the pattern representation with 3D placement"""
        pattern, num_edges, num_panels, rots, tranls, stitches, num_stitches, stitch_tags = self._read_pattern(
            datapoint_name, folder_elements, 
            pad_panels_to_len=self.config['max_panel_len'],
            pad_panel_num=self.config['max_pattern_len'],
            pad_stitches_num=self.config['max_num_stitches'],
            with_placement=True, with_stitches=True, with_stitch_tags=True)
        free_edges_mask = self.free_edges_mask(pattern, stitches, num_stitches)
        empty_panels_mask = self._empty_panels_mask(num_edges)  # useful for evaluation


        return {
            'outlines': pattern, 'num_edges': num_edges,
            'rotations': rots, 'translations': tranls, 
            'num_panels': num_panels, 'empty_panels_mask': empty_panels_mask, 'num_stitches': num_stitches,
            'stitches': stitches, 'free_edges_mask': free_edges_mask, 'stitch_tags': stitch_tags}

    # ----- Mesh tools -----
    def _sample_points(self, datapoint_name, folder_elements):
        """Make a sample from the 3d surface from a given datapoint files

            Returns: sampled points and vertices of original mesh
        
        """
        obj_list = [file for file in folder_elements if self.config['obj_filetag'] in file and '.obj' in file]
        if not obj_list:
            raise RuntimeError('Dataset:Error: geometry file *{}*.obj not found for {}'.format(self.config['obj_filetag'], datapoint_name))
        
        verts, faces = igl.read_triangle_mesh(str(self.root_path / datapoint_name / obj_list[0]))
        points = self.sample_mesh_points(self.config['mesh_samples'], verts, faces)

        # add gaussian noise
        if self.config['point_noise_w']:
            points += np.random.normal(loc=0.0, scale=self.config['point_noise_w'], size=points.shape)

        # Debug
        # if 'skirt_4_panels_00HUVRGNCG' in datapoint_name:
        #     meshplot.offline()
        #     meshplot.plot(points, c=points[:, 0], shading={"point_size": 3.0})
        return points, verts

    @staticmethod
    def sample_mesh_points(num_points, verts, faces):
        """A routine to sample requested number of points from a given mesh
            Returns points in world coordinates"""

        barycentric_samples, face_ids = igl.random_points_on_mesh(num_points, verts, faces)
        face_ids[face_ids >= len(faces)] = len(faces) - 1  # workaround for https://github.com/libigl/libigl/issues/1531

        # convert to world coordinates
        points = np.empty(barycentric_samples.shape)
        for i in range(len(face_ids)):
            face = faces[face_ids[i]]
            barycentric_coords = barycentric_samples[i]
            face_verts = verts[face]
            points[i] = np.dot(barycentric_coords, face_verts)

        return points

    def _point_classes_from_mesh(self, points, verts, datapoint_name, folder_elements):
        """Map segmentation from original mesh to sampled points"""

        # load segmentation
        seg_path_list = [file for file in folder_elements if self.config['obj_filetag'] in file and 'segmentation.txt' in file]

        with open(str(self.root_path / datapoint_name / seg_path_list[0]), 'r') as f:
            vert_labels = np.array([line.rstrip() for line in f])  # remove \n
        map_list, _, _ = igl.snap_points(points, verts)

        if len(verts) > len(vert_labels):
            point_segmentation = np.zeros(len(map_list))
            print(f'{self.__class__.__name__}::{datapoint_name}::WARNING::Not enough segmentation labels -- {len(vert_labels)} for {len(verts)} vertices. Setting segmenations to zero')

            return point_segmentation.astype(np.int64)

        point_segmentation_names = vert_labels[map_list]

        # find those that map to stitches and assign them the closest panel label
        # Also doing this for occasional 'None's 
        stitch_points_ids = np.logical_or(
            point_segmentation_names == 'stitch', point_segmentation_names == 'None')
        non_stitch_points_ids = np.logical_and(
            point_segmentation_names != 'stitch', point_segmentation_names != 'None')

        map_stitches, _, _ = igl.snap_points(points[stitch_points_ids], points[non_stitch_points_ids])

        non_stitch_points_ids = np.flatnonzero(non_stitch_points_ids)
        point_segmentation_names[stitch_points_ids] = point_segmentation_names[non_stitch_points_ids[map_stitches]]

        # Map class names to int ids of loaded classes!
        if self.panel_classifier is not None:
            point_segmentation = self.panel_classifier.map(
                self.template_name(datapoint_name), point_segmentation_names)
        else:
            # assign unique ids within given list
            unique_names = np.unique(point_segmentation_names)
            unique_dict = {name: idx for idx, name in enumerate(unique_names)}
            point_segmentation = np.empty(len(point_segmentation_names))
            for idx, name in enumerate(point_segmentation_names):
                point_segmentation[idx] = unique_dict[name]

        return point_segmentation.astype(np.int64)   # type conversion for PyTorch NLLoss

    def _empty_panels_mask(self, num_edges):
        """Empty panels as boolean mask"""

        mask = np.zeros(len(num_edges), dtype=np.bool)
        mask[num_edges == 0] = True

        return mask

    # ----- Stitches tools -----
    @staticmethod
    def tags_to_stitches(stitch_tags, free_edges_score):
        """
        Convert per-edge per panel stitch tags into the list of connected edge pairs
        NOTE: expects inputs to be torch tensors, numpy is not supported
        """
        flat_tags = stitch_tags.view(-1, stitch_tags.shape[-1])  # with pattern-level edge ids
        
        # to edge classes from logits
        flat_edges_score = free_edges_score.view(-1) 
        flat_edges_mask = torch.round(torch.sigmoid(flat_edges_score)).type(torch.BoolTensor)

        # filter free edges
        non_free_mask = ~flat_edges_mask
        non_free_edges = torch.nonzero(non_free_mask, as_tuple=False).squeeze(-1)  # mapping of non-free-edges ids to full edges list id
        if not any(non_free_mask) or non_free_edges.shape[0] < 2:  # -> no stitches
            print('Garment3DPatternFullDataset::Warning::no non-zero stitch tags detected')
            return torch.tensor([])

        # Check for even number of tags
        if len(non_free_edges) % 2:  # odd => at least one of tags is erroneously non-free
            # -> remove the edge that is closest to free edges class from comparison
            to_remove = flat_edges_score[non_free_mask].argmax()  # the higer the score, the closer the edge is to free edges
            non_free_mask[non_free_edges[to_remove]] = False
            non_free_edges = torch.nonzero(non_free_mask, as_tuple=False).squeeze(-1)

        # Now we have even number of tags to match
        num_non_free = len(non_free_edges) 
        dist_matrix = torch.cdist(flat_tags[non_free_mask], flat_tags[non_free_mask])

        # remove self-distance on diagonal & lower triangle elements (duplicates)
        tril_ids = torch.tril_indices(num_non_free, num_non_free)
        dist_matrix[tril_ids[0], tril_ids[1]] = float('inf')

        # pair egdes by min distance to each other starting by the closest pair
        stitches = []
        for _ in range(num_non_free // 2):  # this many pair to arrange
            to_match_idx = dist_matrix.argmin()  # current global min is also a best match for the pair it's calculated for!
            row = to_match_idx // dist_matrix.shape[0]
            col = to_match_idx % dist_matrix.shape[0]
            stitches.append([non_free_edges[row], non_free_edges[col]])

            # exlude distances with matched edges from further consideration
            dist_matrix[row, :] = float('inf')
            dist_matrix[:, row] = float('inf')
            dist_matrix[:, col] = float('inf')
            dist_matrix[col, :] = float('inf')
        
        if torch.isfinite(dist_matrix).any():
            raise ValueError('Garment3DPatternFullDataset::Error::Tags-to-stitches::Number of stitches {} & dist_matrix shape {} mismatch'.format(
                num_non_free / 2, dist_matrix.shape))

        return torch.tensor(stitches).transpose(0, 1).to(stitch_tags.device) if len(stitches) > 0 else torch.tensor([])

    @staticmethod
    def free_edges_mask(pattern, stitches, num_stitches):
        """
        Construct the mask to identify edges that are not connected to any other
        """
        mask = np.ones((pattern.shape[0], pattern.shape[1]), dtype=np.bool)
        max_edge = pattern.shape[1]

        for side in stitches[:, :num_stitches]:  # ignore the padded part
            for edge_id in side:
                mask[edge_id // max_edge][edge_id % max_edge] = False
        
        return mask


class GarmentStitchPairsDataset(GarmentBaseDataset):
    """
        Dataset targets the task of predicting if a particular pair of edges is connected by a stitch or not
    """
    def __init__(
            self, 
            root_dir, start_config={'data_folders': []}, 
            gt_caching=False, feature_caching=False, in_transforms=[], 
            filter_correct_n_panels=False):
        if gt_caching or feature_caching:
            gt_caching = feature_caching = True  # ensure that both are simulataneously True or False
        
        self.filter_correct_n_panels = filter_correct_n_panels
        # data-specific defaults
        init_config = {
            'data_folders': [],
            'random_pairs_mode': True,   # False to use all possible pairs
            'stitched_edge_pairs_num': 200,
            'non_stitched_edge_pairs_num': 200,
            'shuffle_pairs': True, 
            'shuffle_pairs_order': True
        }
        init_config.update(start_config)  # values from input

        super().__init__(root_dir, init_config, 
                         gt_caching=gt_caching, feature_caching=feature_caching, in_transforms=in_transforms)

        self.config.update(
            element_size=self[0]['features'].shape[-1],
        )
        
        self.correctness_mask = []

    def standardize(self, training=None):
        """Use shifting and scaling for fitting data to interval comfortable for NN training.
            Accepts either of two inputs: 
            * training subset to calculate the data statistics -- the stats are only based on training subsection of the data
            * if stats info is already defined in config, it's used instead of calculating new statistics (usually when calling to restore dataset from existing experiment)
            configuration has a priority: if it's given, the statistics are NOT recalculated even if training set is provided
                => speed-up by providing stats or speeding up multiple calls to this function
        """
        print('{}::Using data normalization for features & ground truth'.format(self.__class__.__name__))

        if 'standardize' in self.config:
            print('{}::Using stats from config'.format(self.__class__.__name__))
            stats = self.config['standardize']
        elif training is not None:
            loader = DataLoader(training, batch_size=len(training), shuffle=False)
            for batch in loader:
                feature_shift, feature_scale = self._get_norm_stats(batch['features'], padded=False)
                break  # only one batch out there anyway

            self.config['standardize'] = {
                'f_shift': feature_shift.cpu().numpy(), 
                'f_scale': feature_scale.cpu().numpy(),
            }
            stats = self.config['standardize']
        else:  # nothing is provided
            raise ValueError('Garment3DPatternFullDataset::Error::Standardization cannot be applied: supply either stats in config or training set to use standardization')

        # clean-up tranform list to avoid duplicates
        self.transforms = [t for t in self.transforms if not isinstance(t, transforms.GTtandartization) and not isinstance(t, transforms.FeatureStandartization)]

        self.transforms.append(transforms.FeatureStandartization(stats['f_shift'], stats['f_scale']))


    def save_prediction_batch(
            self, predictions, datanames, data_folders, save_to, 
            model=None, orig_folder_names=False, **kwargs):
        """ 
            Saving predictions on batch from the current dataset based on given model
            Saves predicted params of the datapoint to the requested data folder.
            Returns list of paths to files with prediction visualizations
        """

        save_to = Path(save_to)
        prediction_imgs = []
        for idx, (name, folder) in enumerate(zip(datanames, data_folders)):

            # Load corresponding pattern
            folder_elements = [file.name for file in (self.root_path / folder / name).glob('*')]  # all files in this directory
            spec_list = [file for file in folder_elements if 'specification.json' in file]
            if not spec_list:
                print('{}::Error::{} serializing skipped: *specification.json not found'.format(
                    self.__class__.__name__, name))
                continue
            
            pattern = NNSewingPattern(self.root_path / folder / name / spec_list[0])

            # find stitches
            pattern.stitches_from_pair_classifier(model, self.config['standardize'])

            # save prediction
            folder_nick = self.data_folders_nicknames[folder] if not orig_folder_names else folder

            try: 
                final_dir = pattern.serialize(save_to / folder_nick, to_subfolder=True, tag='_predicted_')
            except (RuntimeError, InvalidPatternDefError, TypeError) as e:
                print('{}::Error::{} serializing skipped: {}'.format(self.__class__.__name__, name, e))
                continue
            
            final_file = pattern.name + '_predicted__pattern.png'
            prediction_imgs.append(Path(final_dir) / final_file)

            # copy originals for comparison
            for file in (self.root_path / folder / name).glob('*'):
                if ('.png' in file.suffix) or ('.json' in file.suffix):
                    shutil.copy2(str(file), str(final_dir))
                    
        return prediction_imgs


    def _get_sample_info(self, datapoint_name):
        """
            Get features and Ground truth prediction for requested data example
        """
        if datapoint_name in self.gt_cached:  # autpmatically means that features are cashed too
            ground_truth = self.gt_cached[datapoint_name]
            features = self.feature_cached[datapoint_name]

            return features, ground_truth

        # Get stitch pairs & mask from spec
        folder_elements = [file.name for file in (self.root_path / datapoint_name).glob('*')]  # all files in this directory
        spec_list = [file for file in folder_elements if 'specification.json' in file]
        if not spec_list:
            raise RuntimeError('GarmentBaseDataset::Error::*specification.json not found for {}'.format(datapoint_name))
        
        # Load from prediction if exists
        predicted_list = [file for file in spec_list if 'predicte' in file]
        spec = predicted_list[0] if len(predicted_list) > 0 else spec_list[0]

        pattern = NNSewingPattern(self.root_path / datapoint_name / spec)

        if self.config['random_pairs_mode']:
            features, ground_truth = pattern.stitches_as_3D_pairs(
                self.config['stitched_edge_pairs_num'], self.config['non_stitched_edge_pairs_num'],
                self.config['shuffle_pairs'], self.config['shuffle_pairs_order'])
        else:
            features, _, ground_truth = pattern.all_edge_pairs()
        
        # save elements
        if self.gt_caching and self.feature_caching:
            self.gt_cached[datapoint_name] = ground_truth
            self.feature_cached[datapoint_name] = features
            
        
        return features, ground_truth

    def _clean_datapoint_list(self, datapoints_names, dataset_folder):
        super()._clean_datapoint_list(datapoints_names, dataset_folder)
        
        final_list = []
        for datapoint_name in datapoints_names:
            folder_elements = [file.name for file in (self.root_path / datapoint_name).glob('*')]  # all files in this directory
            spec_list = [file for file in folder_elements if 'specification.json' in file]
            if not spec_list:
                raise RuntimeError('GarmentBaseDataset::Error::*specification.json not found for {}'.format(datapoint_name))
            
            # Load from prediction if exists
            predicted_list = [file for file in spec_list if 'predicte' in file]
            spec = predicted_list[0] if len(predicted_list) > 0 else spec_list[0]
            pattern = NNSewingPattern(self.root_path / datapoint_name / spec)

            if not len(pattern.pattern['stitches']):
                print(f'{self.__class__.__name__}::ERROR::{datapoint_name}::has no stitches')
                continue
            if self.filter_correct_n_panels:
                correct_num_panels = pattern.spec['properties']['correct_num_panels']
                actual_num_panels = len(pattern.pattern['panels'])
                if correct_num_panels != actual_num_panels:
                    continue
            
            final_list.append(datapoint_name)
        return final_list


if __name__ == "__main__":
    from wrapper import DatasetWrapper

    # Basic debug of the data classes
    system = Properties('./system.json')
    dataset = Garment3DPatternFullDataset(system['datasets_path'], {
        'data_folders': [
            'tee_2300',
            'skirt_4_panels_1000'
        ]
    })

    print(len(dataset), dataset.config)

    datawrapper = DatasetWrapper(dataset)
    datawrapper.new_split(10, 10, 3000)
    datawrapper.standardize_data()

    print(dataset.config['standardize'])


================================================
FILE: nn/data/panel_classes.py
================================================
""" Panel classification Interface """

from collections import OrderedDict
import json
import numpy as np


class PanelClasses():
    """ Interface to access panel classification by role """
    def __init__(self, classes_file):

        self.filename = classes_file

        with open(classes_file, 'r') as f:
            # preserve the order of classes names
            self.classes = json.load(f, object_pairs_hook=OrderedDict)

        self.names = list(self.classes.keys())
        
        self.panel_to_idx = {}
        for idx, class_name in enumerate(self.classes):
            panels_list = self.classes[class_name]
            for panel in panels_list:
                self.panel_to_idx[tuple(panel)] = idx
        
    def __len__(self):
        return len(self.classes)

    def class_idx(self, template, panel):
        """
            Return idx of class for given panel (name) from given template(name)
        """
        # TODO process cases when given pair does not exist in the classes
        return self.panel_to_idx[(template, panel)]

    def class_name(self, idx):
        return self.names[idx]

    def map(self, template_name, panel_list):
        """Map the list of panels for given template to the given list"""
        
        out_list = np.empty(len(panel_list))
        for idx, panel in enumerate(panel_list):
            if panel == 'stitch':
                out_list[idx] = -1
                print(f'{self.__class__.__name__}::Warning::Mapping stitch label')
            else:
                out_list[idx] = self.panel_to_idx[(template_name, panel)]

        return out_list


================================================
FILE: nn/data/pattern_converter.py
================================================
import copy
from datetime import datetime
import numpy as np
from numpy.random import default_rng
from pathlib import Path
import sys
import torch

if sys.version_info[0] >= 3:
    from scipy.spatial.transform import Rotation as scipy_rot  # Not available in scipy 0.19.1 installed for Maya

# My modules
from pattern.core import panel_spec_template
from pattern.wrappers import VisPattern
from pattern import rotation as rotation_tools


# ------- Custom Errors --------
class EmptyPanelError(Exception):
    pass

class InvalidPatternDefError(Exception):
    """
        The given pattern definition (e.g. numeric representation) is not self-consistent.
        Examples: stitches refer to non-existing edges
    """
    def __init__(self, pattern_name='', message=''):
        self.message = 'Pattern {} is invalid'.format(pattern_name)
        if message:
            self.message += ': ' + message
        super().__init__(self.message)


# -------- Pattern Interface -----
class NNSewingPattern(VisPattern):
    """
        Interface to Sewing patterns with Neural Net friendly representation
    """
    def __init__(self, pattern_file=None, view_ids=False, panel_classifier=None, template_name=None):
        """
            `template_name` is need to use `panel_classifier` for panel ordering
        """
        self.panel_classifier = panel_classifier
        self.template_name = template_name

        super().__init__(pattern_file=pattern_file, view_ids=view_ids)        

    def pattern_as_tensors(
            self, 
            pad_panels_to_len=None, pad_panels_num=None, pad_stitches_num=None,
            with_placement=False, with_stitches=False, with_stitch_tags=False):
        """Return pattern in format suitable for NN inputs/outputs
            * 3D tensor of panel edges
            * 3D tensor of panel's 3D translations
            * 3D tensor of panel's 3D rotations
        Parameters to control padding: 
            * pad_panels_to_len -- pad the list edges of every panel to this number of edges
            * pad_panels_num -- pad the list of panels of the pattern to this number of panels
        """
        if sys.version_info[0] < 3:
            raise RuntimeError('BasicPattern::Error::pattern_as_tensors() is only supported for Python 3.6+ and Scipy 1.2+')
        
        # get panel ordering
        panel_order = self.panel_order(pad_to_len=pad_panels_num)

        # Calculate max edge count among panels -- if not provided
        panel_lens = [len(self.pattern['panels'][name]['edges']) if name is not None else 0 for name in panel_order]
        max_len = pad_panels_to_len if pad_panels_to_len is not None else max(panel_lens)

        # Main info per panel
        panel_seqs, panel_translations, panel_rotations = [], [], []
        for panel_name in panel_order:
            if panel_name is not None:
                edges, rot, transl = self.panel_as_numeric(panel_name, pad_to_len=max_len)
            else:  # empty panel
                edges, rot, transl = self._empty_panel(max_len)
            panel_seqs.append(edges)
            panel_translations.append(transl)
            panel_rotations.append(rot)

        # Stitches info. Order of stitches doesn't matter
        stitches_num = len(self.pattern['stitches']) if pad_stitches_num is None else pad_stitches_num
        if stitches_num < len(self.pattern['stitches']):
            raise ValueError(
                'BasicPattern::Error::requested number of stitches {} is less the number of stitches {} in pattern {}'.format(
                    stitches_num, len(self.pattern['stitches']), self.name
                ))
        
        # Padded value is zero allows to treat the whole thing as index array
        # But need care when using -- as indexing will not crush when padded values are not filtered
        stitches_indicies = np.zeros((2, stitches_num), dtype=np.int) 
        if with_stitch_tags:
            # padding happens automatically, if panels are padded =)
            stitch_tags = self.stitches_as_tags()
            tags_per_edge = np.zeros((len(panel_seqs), len(panel_seqs[0]), stitch_tags.shape[-1]))
        for idx, stitch in enumerate(self.pattern['stitches']):
            for id_side, side in enumerate(stitch):
                panel_id = panel_order.index(side['panel'])
                edge_id = side['edge']
                stitches_indicies[id_side][idx] = panel_id * max_len + edge_id  # pattern-level edge id
                if with_stitch_tags:
                    tags_per_edge[panel_id][edge_id] = stitch_tags[idx]

        # format result as requested
        result = [np.stack(panel_seqs), np.array(panel_lens)]
        result.append(len(self.pattern['panels']))  # actual number of panels 
        if with_placement:
            result.append(np.stack(panel_rotations))
            result.append(np.stack(panel_translations))
        if with_stitches:
            result.append(stitches_indicies)
            result.append(len(self.pattern['stitches']))  # actual number of stitches
        if with_stitch_tags:
            result.append(tags_per_edge)

        return tuple(result) if len(result) > 1 else result[0]

    def pattern_from_tensors(
            self, pattern_representation, 
            panel_rotations=None, panel_translations=None, stitches=None,
            padded=False):
        """Create panels from given panel representation. 
            Assuming that representation uses cm as units"""
        if sys.version_info[0] < 3:
            raise RuntimeError('BasicPattern::Error::pattern_from_tensors() is only supported for Python 3.6+ and Scipy 1.2+')

        # Invalidate parameter & constraints values
        self._invalidate_all_values()

        # Assuming the input (from NN) follows the norm -- no updates will be made on further loads
        self.properties.update(
            curvature_coords='relative', 
            normalize_panel_translation=False, 
            normalized_edge_loops=True,
            units_in_meter=100  # cm
        )

        # remove existing panels -- start anew
        self.pattern['panels'] = {}
        in_panel_order = []
        new_panel_ids = [None] * len(pattern_representation)  # for correct stitches assignment in case of empty panels in-between
        for idx in range(len(pattern_representation)):
            panel_name = 'panel_' + str(idx) if self.panel_classifier is None else self.panel_classifier.class_name(idx)
            
            try:
                self.panel_from_numeric(
                    panel_name, 
                    pattern_representation[idx], 
                    rotation=panel_rotations[idx] if panel_rotations is not None else None,
                    translation=panel_translations[idx] if panel_translations is not None else None,
                    padded=padded)
                in_panel_order.append(panel_name)
                new_panel_ids[idx] = len(in_panel_order) - 1
            except EmptyPanelError as e:
                # Found an empty panel in the input -- moving on to the next one
                pass

        self.pattern['panel_order'] = in_panel_order  # save the incoming panel order

        # remove existing stitches -- start anew
        self.pattern['stitches'] = []
        if stitches is not None and len(stitches) > 0:
            if not padded:
                # TODO implement mapping of pattern-level edge ids -> (panel_id, edge_id) for panels with different number of edges
                raise NotImplementedError('BasicPattern::Recovering stitches for unpadded pattern is not supported')
            
            edges_per_panel = pattern_representation.shape[1]
            for stitch_id in range(stitches.shape[1]):
                stitch_object = []
                if stitches[0][stitch_id] == 0 and stitches[1][stitch_id] == 0:
                    # This is padding -- skip
                    continue 
                for side_id in range(stitches.shape[0]):
                    pattern_edge_id = stitches[side_id][stitch_id]
                    in_panel_id = int(pattern_edge_id // edges_per_panel)

                    if in_panel_id > (len(pattern_representation) - 1) or new_panel_ids[in_panel_id] is None:  # validity of stitch definition
                        raise InvalidPatternDefError(self.name, 'stitch {} referes to non-existing panel {}'.format(stitch_id, in_panel_id))
                    stitch_object.append(
                        {
                            "panel": in_panel_order[new_panel_ids[in_panel_id]],  # map to names of filteres non-empty panels
                            "edge": int(pattern_edge_id % edges_per_panel), 
                        }
                    )
                self.pattern['stitches'].append(stitch_object)
        else:
            print('BasicPattern::Warning::{}::Panels were updated but new stitches info was not provided. Stitches are removed.'.format(self.name))

    def panel_as_numeric(self, panel_name, pad_to_len=None):
        """
            Represent panel as sequence of edges with each edge as vector of fixed length plus the info on panel placement.
            * Edges are returned in additive manner: 
                each edge as a vector that needs to be added to previous edges to get a 2D coordinate of end vertex
            * Panel translation is represented with "universal" heuristic -- as translation of midpoint of the top-most bounding box edge
            * Panel rotation is returned as is but in quaternions

            NOTE: 
                The conversion uses the panels edges order as is, and 
                DOES NOT take resposibility to ensure the same traversal order of panel edges is used across datapoints of similar garment type.
                (the latter is done on sampling or on load)
        """
        if sys.version_info[0] < 3:
            raise RuntimeError('BasicPattern::Error::panel_as_numeric() is only supported for Python 3.6+ and Scipy 1.2+')

        panel = self.pattern['panels'][panel_name]
        vertices = np.array(panel['vertices'])
        
        # -- Construct the edge sequence in the recovered order --
        edge_sequence = [self._edge_as_vector(vertices, edge) for edge in panel['edges']]

        # padding if requested
        if pad_to_len is not None:
            if len(edge_sequence) > pad_to_len:
                raise ValueError('BasicPattern::{}::panel {} cannot fit into requested length: {} edges to fit into {}'.format(
                    self.name, panel_name, len(edge_sequence), pad_to_len))
            for _ in range(len(edge_sequence), pad_to_len):
                edge_sequence.append(np.zeros_like(edge_sequence[0]))
        
        # ----- 3D placement convertion  ------
        # Global Translation (more-or-less stable across designs)
        translation, _ = self._panel_universal_transtation(panel_name)

        panel_rotation = scipy_rot.from_euler('xyz', panel['rotation'], degrees=True)  # pattern rotation follows the Maya convention: intrinsic xyz Euler Angles
        rotation_representation = np.array(panel_rotation.as_quat())

        return np.stack(edge_sequence, axis=0), rotation_representation, translation

    def panel_from_numeric(self, panel_name, edge_sequence, rotation=None, translation=None, padded=False):
        """ Updates or creates panel from NN-compatible numeric representation
            * Set panel vertex (local) positions & edge dictionaries from given edge sequence
            * Set panel 3D translation and orientation if given. Accepts 6-element rotation representation -- first two colomns of rotation matrix"""
        if sys.version_info[0] < 3:
            raise RuntimeError('BasicPattern::Error::panel_from_numeric() is only supported for Python 3.6+ and Scipy 1.2+')

        if padded:
            # edge sequence might be ending with pad values or the whole panel might be a mock object
            selection = ~np.all(np.isclose(edge_sequence, 0, atol=1.5), axis=1)  # only non-zero rows
            edge_sequence = edge_sequence[selection]
            if len(edge_sequence) < 3:
                # 0, 1, 2 edges are not enough to form a panel -> assuming this is a mock panel
                raise EmptyPanelError('{}::EmptyPanelError::Supplied <{}> is empty'.format(self.__class__.__name__, panel_name))

        if panel_name not in self.pattern['panels']:
            # add new panel! =)
            self.pattern['panels'][panel_name] = copy.deepcopy(panel_spec_template)

        # ---- Convert edge representation ----
        vertices = np.array([[0, 0]])  # first vertex is always at origin
        edges = []
        for idx in range(len(edge_sequence) - 1):
            edge_info = edge_sequence[idx]
            next_vert = vertices[idx] + edge_info[:2]
            vertices = np.vstack([vertices, next_vert])
            edges.append(self._edge_dict(idx, idx + 1, edge_info[2:4]))

        # last edge is a special case
        idx = len(vertices) - 1
        edge_info = edge_sequence[-1]
        fin_vert = vertices[-1] + edge_info[:2]
        if all(np.isclose(fin_vert, 0, atol=3)):  # 3 cm per coordinate is a tolerable error
            edges.append(self._edge_dict(idx, 0, edge_info[2:4]))
        else:
            print('BasicPattern::Warning::{} with panel {}::Edge sequence do not return to origin. '
                  'Creating extra vertex'.format(self.name, panel_name))
            vertices = np.vstack([vertices, fin_vert])
            edges.append(self._edge_dict(idx, idx + 1, edge_info[2:4]))

        # update panel itself
        panel = self.pattern['panels'][panel_name]
        panel['vertices'] = vertices.tolist()
        panel['edges'] = edges

        # ----- 3D placement setup --------
        if rotation is not None:
            rotation_obj = scipy_rot.from_quat(rotation)
            panel['rotation'] = rotation_obj.as_euler('xyz', degrees=True).tolist()

        if translation is not None:
            # we are getting translation of 3D top-midpoint (aka 'universal translation')
            # convert it to the translation from the origin 
            _, transl_origin = self._panel_universal_transtation(panel_name)

            shift = np.append(transl_origin, 0)  # to 3D
            panel_rotation = scipy_rot.from_euler('xyz', panel['rotation'], degrees=True)
            comenpensating_shift = - panel_rotation.as_matrix().dot(shift)
            translation = translation + comenpensating_shift

            panel['translation'] = translation.tolist()
        
    def stitches_as_tags(self, panel_order=None, pad_to_len=None):
        """For every stitch, assign an approximate identifier (tag) of the stitch to the edges that are part of that stitch
            * tags are calculated as ~3D locations of the stitch when the garment is draped on the body in T-pose
            * It's calculated as average of the participating edges' endpoint -- Although very approximate, this should be enough
            to separate stitches from each other and from free edges
        Return
            * List of stitch tags for every stitch in the panel
        """
        # NOTE stitch tags values are independent from the choice of origin & edge order within a panel
        # iterate over stitches
        stitch_tags = []
        for stitch in self.pattern['stitches']:
            edge_tags = np.empty((2, 3))  # two 3D tags per edge
            for side_idx, side in enumerate(stitch):
                panel = self.pattern['panels'][side['panel']]
                edge_endpoints = panel['edges'][side['edge']]['endpoints']
                # get 2D locations of participating vertices -- per panel
                edge_endpoints = np.array([
                    panel['vertices'][edge_endpoints[side]] for side in [0, 1]
                ])
                # Get edges midpoints (2D)
                edge_mean = edge_endpoints.mean(axis=0)

                # calculate their 3D locations
                edge_tags[side_idx] = self._point_in_3D(edge_mean, panel['rotation'], panel['translation'])

            # take average
            stitch_tags.append(edge_tags.mean(axis=0))

        return np.array(stitch_tags)

    def stitches_as_3D_pairs(self, stitch_pairs_num=None, non_stitch_pairs_num=None, randomize_edges=False, randomize_list_order=False):
        """
            Return a collection of edge pairs with each pair marked as stitched or not, with 
            edges represented as 3D vertex positions and (relative) curvature values.
            All stitched pairs that exist in the pattern are guaranteed to be included. 
            It's not guaranteed that the pairs would be unique (hence any number of pairs could be requested,
            regardless of the total number of unique pairs)

            * stitch_pairs -- number of edge pairs that are part of a stitch to return. Should be larger then the number of stitches.
            * non_stitch_pairs -- total number of non-connected edge pairs to return.
            * randomize_edges -- to randomize direction of edges and the order within each pair.
            * randomize_list_order -- to randomize the list of 
        """

        if stitch_pairs_num is not None and stitch_pairs_num < len(self.pattern['stitches']):
            raise ValueError(
                '{}::{}::Error::Requested less edge pairs ({}) that there are stitches ({})'.format(
                    self.__class__.__name__, self.name, stitch_pairs_num, len(self.pattern['stitches'])))

        rng = default_rng()  # new Numpy random number generator API

        # collect edges representations per panels
        edges_3d = self._3D_edges_per_panel(randomize_edges)

        # construct edge pairs (stitched & some random selection of non-stitched)
        pairs = []
        mask = []

        # ---- Stitched ----
        stitched_pairs_ids = set()
        # stitches
        for stitch in self.pattern['stitches']:
            pair = []
            try:
                for side in [0, 1]:
                    pair.append(edges_3d[stitch[side]['panel']][stitch[side]['edge']])
            except IndexError:
                # this might happen on incorrectly predicted panels
                print(f'Warning::{self.name}::Missing edge while constructing stitch pairs')
                continue

            if randomize_edges and rng.integers(2):  # randomly change the order in pair
                # flip the edge
                pair[0], pair[1] = pair[1], pair[0]

            pairs.append(np.concatenate(pair))
            mask.append(True)
            stitched_pairs_ids.add((
                (stitch[0]['panel'], stitch[0]['edge']),
                (stitch[1]['panel'], stitch[1]['edge'])
            ))
        if stitch_pairs_num is not None and stitch_pairs_num > len(stitched_pairs_ids):
            for _ in range(len(stitched_pairs_ids), stitch_pairs_num):
                # choose of the existing pairs to duplicate
                pairs.append(pairs[rng.integers(len(stitched_pairs_ids))])
                mask.append(True)
        
        if non_stitch_pairs_num is not None:
            panel_order = self.panel_order()
            if len(pairs) < stitch_pairs_num:
                # e.g., no stitches constructed at all
                non_stitch_pairs_num += stitch_pairs_num - len(pairs)
            for _ in range(non_stitch_pairs_num):
                while True:
                    # random pairs
                    pair_names, pair_edges = [], []
                    for _ in [0, 1]:
                        pair_names.append(panel_order[rng.integers(len(panel_order))])
                        pair_edges.append(rng.integers(len(self.pattern['panels'][pair_names[-1]]['edges'])))

                    if pair_names[0] == pair_names[1] and pair_edges[0] == pair_edges[1]:
                        continue  # try again

                    # check if pair is already used
                    pair_id = ((pair_names[0], pair_edges[0]), (pair_names[1], pair_edges[1]))
                    if pair_id in stitched_pairs_ids or (pair_id[1], pair_id[0]) in stitched_pairs_ids:
                        continue  # try again -- accudentially came up with a stitch

                    # success! Use it
                    pairs.append(np.concatenate([edges_3d[pair_names[0]][pair_edges[0]], edges_3d[pair_names[1]][pair_edges[1]]]))
                    mask.append(False)  # at this point, all pairs are non-stitched!

                    break 
            
        if randomize_list_order:
            permutation = rng.permutation(len(pairs))
            return np.stack(pairs)[permutation], np.array(mask, dtype=bool)[permutation]
        else:
            return np.stack(pairs), np.array(mask, dtype=bool)

    def stitches_from_pair_classifier(self, model, data_stats):
        """ Update stitches in the pattern by predictions of edge pairs classification model"""

        self.pattern['stitches'] = []
        model.eval()

        edge_pairs_list, pairs_mapping, _ = self.all_edge_pairs(device=model.device_ids[0])

        # apply appropriate scaling
        shift = torch.tensor(data_stats['f_shift'], device=model.device_ids[0])
        scale = torch.tensor(data_stats['f_scale'], device=model.device_ids[0])
        edge_pairs_list = (edge_pairs_list - shift) / scale

        preds = model(edge_pairs_list)
        preds_probability = torch.sigmoid(preds)
        preds_class = torch.round(preds_probability)

        # record stitches
        stitched_ids = preds_class.nonzero(as_tuple=False).squeeze().cpu().tolist()
        if len(stitched_ids) > 0:  # some stitches found!
            for stitch_idx in range(len(stitched_ids)):
                edge_pair = pairs_mapping[stitch_idx]

                self.pattern['stitches'].append(self._stitch_entry(
                    edge_pair[0][0], edge_pair[0][1], 
                    edge_pair[1][0], edge_pair[1][1], 
                    score=preds[stitched_ids[stitch_idx]].cpu().tolist()
                ))

        # Post-analysis: check if any of the edges parttake in multiple stitches & only leave the stronger ones
        to_remove = set()
        for base_stitch_id in range(len(self.pattern['stitches'])):
            base_stitch = self.pattern['stitches'][base_stitch_id]
            for side in [0, 1]:
                base_edge = base_stitch[side]
                for other_stitch_id in range(base_stitch_id + 1, len(self.pattern['stitches'])):
                    curr_stitch = self.pattern['stitches'][other_stitch_id]
                    if (base_edge['panel'] == curr_stitch[0]['panel'] and base_edge['edge'] == curr_stitch[0]['edge']
                            or base_edge['panel'] == curr_stitch[1]['panel'] and base_edge['edge'] == curr_stitch[1]['edge']):
                        # same edge, multiple stitches!
                        # score is the same for both sides, so it doesn't matter which one to take
                        to_remove.add(
                            base_stitch_id if base_stitch[0]['score'] < curr_stitch[0]['score'] else other_stitch_id)

        if len(to_remove):
            self.pattern['stitches'] = [value for i, value in enumerate(self.pattern['stitches']) if i not in to_remove]

    def all_edge_pairs(self, device='cpu'):
        """
            Construct all possible edge pairs for given sewing pattern 
            with GT stitching labels if available and requested in `with_labels`
        """
        edges_3D = self._3D_edges_per_panel()
        num_panels = len(self.panel_order())

        stitch_set = self._stitches_as_set()
        mask = []

        edge_pairs_list = []
        pairs_mapping = []
        for i in range(num_panels):
            panel_i = self.panel_order()[i]
            edges_i = np.array(edges_3D[panel_i])
            for j in range(i + 1, num_panels):  # assuming panels are not connected to themselves
                panel_j = self.panel_order()[j]
                edges_j = np.array(edges_3D[panel_j])

                rows, cols = np.indices((len(edges_i), len(edges_j)))
                edge_pairs = np.concatenate([edges_i[rows], edges_j[cols]], axis=-1)

                # record the pair
                edge_pairs = torch.from_numpy(edge_pairs).float().to(device)
                edge_pairs = edge_pairs.view(-1, edge_pairs.shape[-1])  # flatten to the list of pairs
                edge_pairs_list.append(edge_pairs)

                # record backward mapping & labels
                for row_idx in range(len(edges_i)):
                    for col_idx in range(len(edges_j)):
                        pair_id = ((panel_i, row_idx), (panel_j, col_idx))
                        pairs_mapping.append(pair_id)
                        
                        mask.append(pair_id in stitch_set or (pair_id[1], pair_id[0]) in stitch_set)

        if len(edge_pairs_list) == 0:
            raise InvalidPatternDefError(self.name, 'No edges to construct')

        edge_pairs_list = torch.cat(edge_pairs_list)

        return edge_pairs_list, pairs_mapping, mask

    def _stitches_as_set(self):
        stitches_set = set()
        for stitch in self.pattern['stitches']:
            stitches_set.add((
                (stitch[0]['panel'], stitch[0]['edge']),
                (stitch[1]['panel'], stitch[1]['edge'])
            ))
        return stitches_set

    def _edge_dict(self, vstart, vend, curvature):
        """Convert given info into the proper edge dictionary representation"""
        edge_dict = {'endpoints': [vstart, vend]}
        if not all(np.isclose(curvature, 0, atol=0.01)):  # 0.01 is tolerable error for local curvature coords
            edge_dict['curvature'] = curvature.tolist()
        return edge_dict

    def _3D_edges_per_panel(self, randomize_direction=False):
        """ 
            Return all edges in the pattern (grouped by panels)
            represented as 3D vertex positions and (relative) curvature values.

            * 'randomize_direction' -- request to randomly flip the direction of some edges
        """
        if randomize_direction:
            rng = default_rng()  # new Numpy random number generator API

        # collect edges representations per panels
        edges_3d = {}
        for panel_name in self.panel_order():
            edges_3d[panel_name] = []
            panel = self.pattern['panels'][panel_name]
            vertices = np.array(panel['vertices'])

            # To 3D
            rot_matrix = rotation_tools.euler_xyz_to_R(panel['rotation'])
            vertices_3d = np.stack([self._point_in_3D(vertices[i], rot_matrix, panel['translation']) for i in range(len(vertices))])

            # edge feature
            for edge_dict in panel['edges']:
                edge_verts = vertices_3d[edge_dict['endpoints']]  # ravel does not copy elements
                curvature = np.array(edge_dict['curvature']) if 'curvature' in edge_dict else [0, 0]

                if randomize_direction and rng.integers(2):
                    # flip the edge
                    edge_verts[[0, 1], :] = edge_verts[[1, 0], :]

                    curvature[0] = 1 - curvature[0] if curvature[0] else 0
                    curvature[1] = -curvature[1] 

                edges_3d[panel_name].append(np.concatenate([edge_verts.ravel(), curvature]))

        return edges_3d

    def _stitch_entry(self, panel_1, edge_1, panel_2, edge_2, score=None):
        """ element of a stitch list with given parameters (all need to be json-serializible)"""
        return [
            {
                'panel': panel_1, 
                'edge': edge_1, 
                'score': score
            },
            {
                'panel': panel_2, 
                'edge': edge_2, 
                'score': score
            },
        ]

    def _empty_panel(self, max_edge_num):
        """ Shape, rotation, and translation for empty panels"""
        # edge is 4-elem vector, 4 rotation element for quaternion, 3 element for world translation
        return np.zeros((max_edge_num, 4)), np.zeros(4), np.zeros(3)

    # ordering of panels according to classification
    def panel_order(self, force_update=False, pad_to_len=None):
        """
            Return order of panels either 
                * according to the one provided in the pattern spec
                * According to external panels classification if self.panel_classifier is set!
            Note: 'None' represent empty panels at that place of ordered elements

            Reloading 'panel_order' instead of 'define_panel_order' to preserve order from file 
                if self.panel_classifier is not defined and 'force_update' is false
        """
        if self.panel_classifier is None or self.template_name is None:
            # preserves the order is given in pattern spec!
            order = super().panel_order(force_update=force_update)
            
        else:  
            # NOTE: re-evaluate even if `force_update` flag is false
            # as we need update even if the pattern spec already contains some order

            # construct the order according to class indices
            # -None- represents empty panels-placeholders
            order = [None] * len(self.panel_classifier)
            for panel_name in self.pattern['panels']:
                class_idx = self.panel_classifier.class_idx(self.template_name, panel_name)
                order[class_idx] = panel_name
        
        # Additionally pad to requested value if given
        if pad_to_len is not None:
            if pad_to_len < len(order):
                raise ValueError(
                    f'{self.__class__.__name__}::{self.name}::Error::Requested max num of panels {pad_to_len} '
                    f'is smaller then evaluated number of panels {len(order)}')
            order += [None] * (pad_to_len - len(order))

        # Remember the order for future reference
        self.pattern['panel_order'] = order

        return order


# ---------- test -------------
if __name__ == "__main__":
    # the pattern converter loading check
    from pathlib import Path
    from datetime import datetime
    import customconfig
    from pattern.wrappers import VisPattern
    from data.panel_classes import PanelClasses

    np.set_printoptions(precision=4, suppress=True)

    system_config = customconfig.Properties('./system.json')
    base_path = system_config['output']

    # NOTE 
    pattern = NNSewingPattern(
        Path(system_config['datasets_path']) / 'tee_2300' / 'tee_template_specification.json', 
        panel_classifier=PanelClasses('./nn/data_configs/panel_classes.json'), 
        template_name='tee')

    empty_pattern = NNSewingPattern(panel_classifier=PanelClasses('./nn/data_configs/panel_classes_extended.json'))
    print(pattern.panel_order())

    tensor, edge_lens, num_panels, rot, transl, stitches, stitch_num, stitch_tags = pattern.pattern_as_tensors(
        with_placement=True, with_stitches=True, with_stitch_tags=True)

    empty_pattern.pattern_from_tensors(tensor, rot, transl, stitches, padded=True)
    print(empty_pattern.pattern['stitches'])

    # Save
    empty_pattern.name = pattern.name + 'from_empty_with_class' + '_' + datetime.now().strftime('%y%m%d-%H-%M-%S')
    pattern.name = pattern.name + '_with_class' + '_' + datetime.now().strftime('%y%m%d-%H-%M-%S')
    
    empty_pattern.serialize(system_config['output'], to_subfolder=True)
    pattern.serialize(system_config['output'], to_subfolder=True)


================================================
FILE: nn/data/transforms.py
================================================
import numpy as np
import torch


# ------------------ Transforms ----------------
def _dict_to_tensors(dict_obj):  # helper
    """convert a dictionary with numeric values into a new dictionary with torch tensors"""
    new_dict = dict.fromkeys(dict_obj.keys())
    for key, value in dict_obj.items():
        if value is None:
            new_dict[key] = torch.Tensor()
        elif isinstance(value, dict):
            new_dict[key] = _dict_to_tensors(value)
        elif isinstance(value, str):  # no changes for strings
            new_dict[key] = value
        elif isinstance(value, np.ndarray):
            new_dict[key] = torch.from_numpy(value)

            # TODO more stable way of converting the types (or detecting ints)
            if value.dtype not in [np.int, np.int64, np.bool]:
                new_dict[key] = new_dict[key].float()  # cast all doubles and ofther stuff to floats
        else:
            new_dict[key] = torch.tensor(value)  # just try directly, if nothing else works
    return new_dict


# Custom transforms -- to tensor
class SampleToTensor(object):
    """Convert ndarrays in sample to Tensors."""
    
    def __call__(self, sample):        
        return _dict_to_tensors(sample)


class FeatureStandartization():
    """Normalize features of provided sample with given stats"""
    def __init__(self, shift, scale):
        self.shift = torch.Tensor(shift)
        self.scale = torch.Tensor(scale)
    
    def __call__(self, sample):
        updated_sample = {}
        for key, value in sample.items():
            if key == 'features':
                updated_sample[key] = (sample[key] - self.shift) / self.scale
            else: 
                updated_sample[key] = sample[key]

        return updated_sample


class GTtandartization():
    """Normalize features of provided sample with given stats
        * Supports multimodal gt represented as dictionary
        * For dictionary gts, only those values are updated for which the stats are provided
    """
    def __init__(self, shift, scale):
        """If ground truth is a dictionary in itself, the provided values should also be dictionaries"""
        
        self.shift = _dict_to_tensors(shift) if isinstance(shift, dict) else torch.Tensor(shift)
        self.scale = _dict_to_tensors(scale) if isinstance(scale, dict) else torch.Tensor(scale)
    
    def __call__(self, sample):
        gt = sample['ground_truth']
        if isinstance(gt, dict):
            new_gt = dict.fromkeys(gt.keys())
            for key, value in gt.items():
                new_gt[key] = value
                if key in self.shift:
                    new_gt[key] = new_gt[key] - self.shift[key]
                if key in self.scale:
                    new_gt[key] = new_gt[key] / self.scale[key]
                # if shift and scale are not set, the value is kept as it is
        else:
            new_gt = (gt - self.shift) / self.scale

        # gather sample
        updated_sample = {}
        for key, value in sample.items():
            updated_sample[key] = new_gt if key == 'ground_truth' else sample[key]

        return updated_sample


================================================
FILE: nn/data/utils.py
================================================
import copy
import numpy as np
from pathlib import Path
import random

import torch
import igl
# import meshplot  # when uncommented, could lead to problems with wandb run syncing

# My modules
from data.pattern_converter import NNSewingPattern, InvalidPatternDefError
from data.datasets import Garment3DPatternFullDataset


# -------------- Sampler ----------
class BalancedBatchSampler():
    """ Sampler creates batches that have the same class distribution as in given subset"""
    # https://stackoverflow.com/questions/66065272/customizing-the-batch-with-specific-elements
    def __init__(self, ids_by_type, batch_size=10, drop_last=True):
        """
            * ids_by_type provided as dictionary of torch.Subset() objects
            * drop_last is True by default to better guarantee that all batches are well-balanced
        """
        if len(ids_by_type) > batch_size:
            raise NotImplementedError('{}::Error::Creating batches that are smaller then total number of data classes is not implemented!'.format(
                self.__class__.__name__
            ))

        print('{}::Using custom balanced batch data sampler'.format(self.__class__.__name__))

        # represent as lists of ids for simplicity
        self.data_ids_by_type = dict.fromkeys(ids_by_type)
        for data_class in self.data_ids_by_type:
            self.data_ids_by_type[data_class] = ids_by_type[data_class].tolist()

        self.class_names = list(self.data_ids_by_type.keys())
        self.batch_size = batch_size
        self.data_size = sum(len(self.data_ids_by_type[i]) for i in ids_by_type)
        self.num_full_batches = self.data_size // batch_size  # int division
        
        # extra batch left?
        last_batch_len = self.data_size - self.batch_size * self.num_full_batches
        self.drop_last = drop_last or last_batch_len == 0  # by request or because there is no batch with leftovers 
        
        # num of elements per type in each batch
        self.batch_len_per_type = dict.fromkeys(ids_by_type)
        for data_class in self.class_names:
            self.batch_len_per_type[data_class] = int((len(ids_by_type[data_class]) / self.data_size) * batch_size)  # always rounds down
        
        if sum(self.batch_len_per_type.values()) > self.batch_size:
            raise('BalancedBatchSampler::Error:: Failed to evaluate per-type length correctly')


    def __iter__(self):
        ids_by_type = copy.deepcopy(self.data_ids_by_type)

        # shuffle
        for data_class in ids_by_type:
            random.shuffle(ids_by_type[data_class])

        batches = []
        for _ in range(self.num_full_batches):
            batch = []
            for data_class in self.class_names:
                
                for _ in range(self.batch_len_per_type[data_class]):
                    if not len(ids_by_type[data_class]):  # exausted
                        break
                    batch.append(ids_by_type[data_class].pop())

            # Fill the rest of the batch randomly if needed
            diff = self.batch_size - len(batch)
            for _ in range(diff):
                non_empty_class_names = [name for name in self.class_names if len(ids_by_type[name])]
                batch.append(ids_by_type[random.choice(non_empty_class_names)].pop())
            
            random.shuffle(batch)  # to avoid grouping by type in case it matters
            batches.append(batch)

        if not self.drop_last:  
            # put the rest of elements in the last batch
            batch = []
            for ids_list in ids_by_type.values():
                batch += ids_list

            random.shuffle(batch)  # to avoid grouping by type in case it matters
            batches.append(batch)
        
        return iter(batches)

    def __len__(self):
        return self.num_full_batches + (not self.drop_last)


# ------------------------- Utils for non-dataset examples --------------------------
def sample_points_from_meshes(mesh_paths, data_config):
    """
        Sample points from the given list of triangle meshes (as .obj files -- or other file formats supported by libigl)
    """
    points_list = []
    for mesh in mesh_paths:
        verts, faces = igl.read_triangle_mesh(str(mesh))
        points = Garment3DPatternFullDataset.sample_mesh_points(data_config['mesh_samples'], verts, faces)
        if 'standardize' in data_config:
            points = (points - data_config['standardize']['f_shift']) / data_config['standardize']['f_scale']
        points_list.append(torch.Tensor(points))
    return points_list


def save_garments_prediction(predictions, save_to, data_config=None, datanames=None, stitches_from_stitch_tags=False):
    """ 
        Saving arbitrary sewing pattern predictions that
        
        * They do NOT have to be coming from garmet dataset samples.
    """

    save_to = Path(save_to)
    batch_size = predictions['outlines'].shape[0]

    if datanames is None:
        datanames = ['pred_{}'.format(i) for i in range(batch_size)]
        
    for idx, name in enumerate(datanames):
        # "unbatch" dictionary
        prediction = {}
        for key in predictions:
            prediction[key] = predictions[key][idx]

        if data_config is not None and 'standardize' in data_config:
            # undo standardization  (outside of generinc conversion function due to custom std structure)
            gt_shifts = data_config['standardize']['gt_shift']
            gt_scales = data_config['standardize']['gt_scale']
            for key in gt_shifts:
                if key == 'stitch_tags' and not data_config['explicit_stitch_tags']:  
                    # ignore stitch tags update if explicit tags were not used
                    continue
                prediction[key] = prediction[key].cpu().numpy() * gt_scales[key] + gt_shifts[key]

        # stitch tags to stitch list
        if stitches_from_stitch_tags:
            stitches = Garment3DPatternFullDataset.tags_to_stitches(
                torch.from_numpy(prediction['stitch_tags']) if isinstance(prediction['stitch_tags'], np.ndarray) else prediction['stitch_tags'],
                prediction['free_edges_mask']
            )
        else:
            stitches = None

        pattern = NNSewingPattern(view_ids=False)
        pattern.name = name
        try:
            pattern.pattern_from_tensors(
                prediction['outlines'], prediction['rotations'], prediction['translations'], 
                stitches=stitches,
                padded=True)   
            # save
            pattern.serialize(save_to, to_subfolder=True)
        except (RuntimeError, InvalidPatternDefError, TypeError) as e:
            print(e)
            print('Saving predictions::Skipping pattern {}'.format(name))
            pass


================================================
FILE: nn/data/wrapper.py
================================================
from argparse import Namespace
import json
import numpy as np
import random
import time
from datetime import datetime

import torch
from torch.utils.data import DataLoader, Subset

# My modules
from data.utils import BalancedBatchSampler


# ---------------------- Main Wrapper ------------------
class DatasetWrapper(object):
    """Resposible for keeping dataset, its splits, loaders & processing routines.
        Allows to reproduce earlier splits
    """
    def __init__(self, in_dataset, known_split=None, batch_size=None, shuffle_train=True):
        """Initialize wrapping around provided dataset. If splits/batch_size is known """

        self.dataset = in_dataset
        self.data_section_list = ['full', 'train', 'validation', 'test']

        self.training = in_dataset
        self.validation = None
        self.test = None
        self.full_per_datafolder = None

        self.batch_size = None

        self.loaders = Namespace(
            full=None,
            full_per_data_folder=None,
            train=None,
            test=None,
            test_per_data_folder=None,
            validation=None,
            valid_per_data_folder=None
        )

        self.split_info = {
            'random_seed': None, 
            'valid_per_type': None, 
            'test_per_type': None
        }

        if known_split is not None:
            self.load_split(known_split)
        if batch_size is not None:
            self.batch_size = batch_size
            self.new_loaders(batch_size, shuffle_train)
    
    def get_loader(self, data_section='full'):
        """Return loader that corresponds to given data section. None if requested loader does not exist"""
        try:
            return getattr(self.loaders, data_section)
        except AttributeError:
            raise ValueError('DataWrapper::requested loader on unknown data section {}'.format(data_section))
        

    def new_loaders(self, batch_size=None, shuffle_train=True):
        """Create loaders for current data split. Note that result depends on the random number generator!
        
            if the data split was not specified, only the 'full' loaders are created
        """
        if batch_size is not None:
            self.batch_size = batch_size
        if self.batch_size is None:
            raise RuntimeError('DataWrapper:Error:cannot create loaders: batch_size is not set')

        self.loaders.full = DataLoader(self.dataset, self.batch_size)
        if self.full_per_datafolder is None:
            self.full_per_datafolder = self.dataset.subsets_per_datafolder()
        self.loaders.full_per_data_folder = self._loaders_dict(self.full_per_datafolder, self.batch_size)

        if self.validation is not None and self.test is not None:
            # we have a loaded split!
            try:
                self.dataset.config['balanced_batch_sampling'] = True
                # indices IN the training set breakdown per type
                _, train_indices_per_type = self.dataset.indices_by_data_folder(self.training.indices)
                batch_sampler = BalancedBatchSampler(train_indices_per_type, batch_size=self.batch_size)
                self.loaders.train = DataLoader(self.training, batch_sampler=batch_sampler)
            except (AttributeError, NotImplementedError) as e:  # cannot create balanced batches
                print('{}::Warning::Failed to create balanced batches for training. Using default sampling'.format(self.__class__.__name__))
                self.dataset.config['balanced_batch_sampling'] = False
                self.loaders.train = DataLoader(self.training, self.batch_size, shuffle=shuffle_train)
            # no need for breakdown per datafolder for training -- for now

            self.loaders.validation = DataLoader(self.validation, self.batch_size)
            self.loaders.valid_per_data_folder = self._loaders_dict(self.validation_per_datafolder, self.batch_size) 

            # loader with one example per garment type -- for visualization of the training process
            single_sample_ids = [folder_ids.indices[0] for folder_ids in self.validation_per_datafolder.values()]
            self.loaders.valid_single_per_data = DataLoader(
                Subset(self.dataset, single_sample_ids), batch_size=self.batch_size, shuffle=False) 

            self.loaders.test = DataLoader(self.test, self.batch_size)
            self.loaders.test_per_data_folder = self._loaders_dict(self.test_per_datafolder, self.batch_size)

        return self.loaders.train, self.loaders.validation, self.loaders.test

    def _loaders_dict(self, subsets_dict, batch_size, shuffle=False):
        """Create loaders for all subsets in dict"""
        loaders_dict = {}
        for name, subset in subsets_dict.items():
            loaders_dict[name] = DataLoader(subset, batch_size, shuffle=shuffle)
        return loaders_dict

    # -------- Reproducibility ---------------
    def new_split(self, valid, test=None, random_seed=None):
        """Creates train/validation or train/validation/test splits
            depending on provided parameters
            """
        self.split_info['random_seed'] = random_seed if random_seed else int(time.time())
        self.split_info.update(valid_per_type=valid, test_per_type=test, type='count')
        
        return self.load_split()

    def load_split(self, split_info=None, batch_size=None):
        """Get the split by provided parameters. Can be used to reproduce splits on the same dataset.
            NOTE this function re-initializes torch random number generator!
        """
        if split_info:
            self.split_info = split_info

        if 'random_seed' not in self.split_info or self.split_info['random_seed'] is None:
            self.split_info['random_seed'] = int(time.time())
        # init for all libs =)
        torch.manual_seed(self.split_info['random_seed'])
        random.seed(self.split_info['random_seed'])
        np.random.seed(self.split_info['random_seed'])

        # if file is provided
        if 'filename' in self.split_info and self.split_info['filename'] is not None:
            print('DataWrapper::Loading data split from {}'.format(self.split_info['filename']))
            with open(self.split_info['filename'], 'r') as f_json:
                split_dict = json.load(f_json)
            self.training, self.validation, self.test, self.training_per_datafolder, self.validation_per_datafolder, self.test_per_datafolder = self.dataset.split_from_dict(
                split_dict, 
                with_breakdown=True)
        else:
            keys_required = ['test_per_type', 'valid_per_type', 'type']
            if any([key not in self.split_info for key in keys_required]):
                raise ValueError('Specified split information is not full: {}. It needs to contain: {}'.format(split_info, keys_required))
            print('DataWrapper::Loading data split from split config: {}: valid per type {} / test per type {}'.format(
                self.split_info['type'], self.split_info['valid_per_type'], self.split_info['test_per_type']))
            self.training, self.validation, self.test, self.training_per_datafolder, self.validation_per_datafolder, self.test_per_datafolder = self.dataset.random_split_by_dataset(
                self.split_info['valid_per_type'], 
                self.split_info['test_per_type'],
                self.split_info['type'],
                with_breakdown=True)

        if batch_size is not None:
            self.batch_size = batch_size
        if self.batch_size is not None:
            self.new_loaders()  # s.t. loaders could be used right away

        print('DatasetWrapper::Dataset split: {} / {} / {}'.format(
            len(self.training) if self.training else None, 
            len(self.validation) if self.validation else None, 
            len(self.test) if self.test else None))
        self.split_info['size_train'] = len(self.training) if self.training else 0
        self.split_info['size_valid'] = len(self.validation) if self.validation else 0
        self.split_info['size_test'] = len(self.test) if self.test else 0
        
        self.print_subset_stats(self.training_per_datafolder, len(self.training), 'Training')
        self.print_subset_stats(self.validation_per_datafolder, len(self.validation), 'Validation')
        self.print_subset_stats(self.test_per_datafolder, len(self.test), 'Test')

        return self.training, self.validation, self.test

    def print_subset_stats(self, subset_breakdown_dict, total_len, subset_name='', log_to_config=True):
        """Print stats on the elements of each datafolder contained in given subset"""
        # gouped by data_folders
        if not total_len:
            print('{}::Warning::Subset {} is empty, no stats printed'.format(self.__class__.__name__, subset_name))
            return
        self.split_info[subset_name] = {}
        message = ''
        for data_folder, subset in subset_breakdown_dict.items():
            if log_to_config:
                self.split_info[subset_name][data_folder] = len(subset)
            message += '{} : {:.1f}%;\n'.format(data_folder, 100 * len(subset) / total_len)
        
        print('DatasetWrapper::{} subset breakdown::\n{}'.format(subset_name, message))

    def save_to_wandb(self, experiment):
        """Save current data info to the wandb experiment"""
        # Split
        experiment.add_config('data_split', self.split_info)
        # save serialized split s.t. it's loaded to wandb
        split_datanames = {}
        split_datanames['training'] = [self.dataset.datapoints_names[idx] for idx in self.training.indices]
        split_datanames['validation'] = [self.dataset.datapoints_names[idx] for idx in self.validation.indices]
        split_datanames['test'] = [self.dataset.datapoints_names[idx] for idx in self.test.indices]
        with open(experiment.local_wandb_path() / 'data_split.json', 'w') as f_json:
            json.dump(split_datanames, f_json, indent=2, sort_keys=True)

        # data info
        self.dataset.save_to_wandb(experiment)

    # ---------- Standardinzation ----------------
    def standardize_data(self):
        """Apply data normalization based on stats from training set"""
        self.dataset.standardize(self.training)

    # --------- Managing predictions on this data ---------
    def predict(self, model, save_to, dir_tag='pred', sections=['test'], single_batch=False, orig_folder_names=False):
        """Save model predictions on the given dataset section"""
        # Main path
        prediction_path = save_to / (f'nn_{dir_tag}_' + datetime.now().strftime('%y%m%d-%H-%M-%S'))
        prediction_path.mkdir(parents=True, exist_ok=True)

        device = model.device_ids[0] if hasattr(model, 'device_ids') else torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
        model.to(device)
        model.eval()

        # turn on att weights saving during prediction!
        model.module.save_att_weights = True  # model that don't have this poperty will just ignore it

        for section in sections:
            # Section path
            section_dir = prediction_path / section
            section_dir.mkdir(parents=True, exist_ok=True)
            with torch.no_grad():
                loader = self.get_loader(section)
                if loader:
                    for batch in loader:
                        features_device = batch['features'].to(device)
                        preds = model(features_device)
                        self.dataset.save_prediction_batch(
                            preds, batch['name'], batch['data_folder'], section_dir, features=batch['features'].numpy(), 
                            model=model, orig_folder_names=orig_folder_names)
                        
                        if single_batch:  # stop after first iteration
                            break
            
        # Turn of to avoid wasting time\memory diring other operations
        model.module.save_att_weights = False

        return prediction_path



================================================
FILE: nn/data_configs/data_split_on_filtered_dataset.json
================================================
{
  "test": [
    "dress_sleeveless_2550/dress_sleeveless_6VN3H6FMOY",
    "dress_sleeveless_2550/dress_sleeveless_94LPP4A6ZO",
    "dress_sleeveless_2550/dress_sleeveless_GXPP75840D",
    "dress_sleeveless_2550/dress_sleeveless_2DEB2PFTY8",
    "dress_sleeveless_2550/dress_sleeveless_P4TR8J4LSN",
    "dress_sleeveless_2550/dress_sleeveless_HFYS1JKZY2",
    "dress_sleeveless_2550/dress_sleeveless_KD5ZWSVRB9",
    "dress_sleeveless_2550/dress_sleeveless_0MAXMII1XO",
    "dress_sleeveless_2550/dress_sleeveless_668FPM3QEK",
    "dress_sleeveless_2550/dress_sleeveless_RPDJWM8SXE",
    "dress_sleeveless_2550/dress_sleeveless_0KKZ4MH0VQ",
    "dress_sleeveless_2550/dress_sleeveless_4X2LXGLUGT",
    "dress_sleeveless_2550/dress_sleeveless_O4MYYQLLBG",
    "dress_sleeveless_2550/dress_sleeveless_KFYJMQMBG0",
    "dress_sleeveless_2550/dress_sleeveless_6EH43JYGCQ",
    "dress_sleeveless_2550/dress_sleeveless_JWUEU7AJDI",
    "dress_sleeveless_2550/dress_sleeveless_T8CXOHWQJS",
    "dress_sleeveless_2550/dress_sleeveless_QAS1SVE38V",
    "dress_sleeveless_2550/dress_sleeveless_Y1YZ1EOA34",
    "dress_sleeveless_2550/dress_sleeveless_872II66O2M",
    "dress_sleeveless_2550/dress_sleeveless_5DGX5RO9ZF",
    "dress_sleeveless_2550/dress_sleeveless_K6XIVX4X87",
    "dress_sleeveless_2550/dress_sleeveless_7NPF6TXWRG",
    "dress_sleeveless_2550/dress_sleeveless_Z9Q94Z7CFB",
    "dress_sleeveless_2550/dress_sleeveless_NXTGJSSXUK",
    "dress_sleeveless_2550/dress_sleeveless_THFJHRMCD4",
    "dress_sleeveless_2550/dress_sleeveless_494H68C4EK",
    "dress_sleeveless_2550/dress_sleeveless_8HXAFQ2PYU",
    "dress_sleeveless_2550/dress_sleeveless_IC9DOFED6R",
    "dress_sleeveless_2550/dress_sleeveless_CIR3TFRJ39",
    "dress_sleeveless_2550/dress_sleeveless_71PEP0P9HG",
    "dress_sleeveless_2550/dress_sleeveless_5O4A0IK3LM",
    "dress_sleeveless_2550/dress_sleeveless_BBAM6BDG3Q",
    "dress_sleeveless_2550/dress_sleeveless_U22JXQE5MG",
    "dress_sleeveless_2550/dress_sleeveless_5BIDTK101X",
    "dress_sleeveless_2550/dress_sleeveless_4GP9N02KB1",
    "dress_sleeveless_2550/dress_sleeveless_FXYVSAOYLA",
    "dress_sleeveless_2550/dress_sleeveless_KDYH5TSMG6",
    "dress_sleeveless_2550/dress_sleeveless_XDJDEQ6XPJ",
    "dress_sleeveless_2550/dress_sleeveless_8TM0NJV7ZT",
    "dress_sleeveless_2550/dress_sleeveless_VYAEKVVDP5",
    "dress_sleeveless_2550/dress_sleeveless_CEPSGG0DHQ",
    "dress_sleeveless_2550/dress_sleeveless_NXI3LXXMTO",
    "dress_sleeveless_2550/dress_sleeveless_HCDVEIKTW0",
    "dress_sleeveless_2550/dress_sleeveless_HHB9S94TMT",
    "dress_sleeveless_2550/dress_sleeveless_GZD0I6CD1W",
    "dress_sleeveless_2550/dress_sleeveless_0FWU87POG8",
    "dress_sleeveless_2550/dress_sleeveless_470K3M8FXP",
    "dress_sleeveless_2550/dress_sleeveless_LQX9FRVS94",
    "dress_sleeveless_2550/dress_sleeveless_AZ9OPW5WF3",
    "dress_sleeveless_2550/dress_sleeveless_KDGPY6T935",
    "dress_sleeveless_2550/dress_sleeveless_XSQ428YP1V",
    "dress_sleeveless_2550/dress_sleeveless_4WC1W8OH3Z",
    "dress_sleeveless_2550/dress_sleeveless_O0Y7AWV98I",
    "dress_sleeveless_2550/dress_sleeveless_D44EZHEAN2",
    "dress_sleeveless_2550/dress_sleeveless_QBYSY3E3LT",
    "dress_sleeveless_2550/dress_sleeveless_D1TVHAO9O5",
    "dress_sleeveless_2550/dress_sleeveless_4HBMADSHS3",
    "dress_sleeveless_2550/dress_sleeveless_YWH6N61LD9",
    "dress_sleeveless_2550/dress_sleeveless_DSNJPIJ75V",
    "dress_sleeveless_2550/dress_sleeveless_QD14NKTSLI",
    "dress_sleeveless_2550/dress_sleeveless_YOHL8U1F10",
    "dress_sleeveless_2550/dress_sleeveless_EP7ZJP92FT",
    "dress_sleeveless_2550/dress_sleeveless_EI9NBMSQXU",
    "dress_sleeveless_2550/dress_sleeveless_DK6WCLC7YY",
    "dress_sleeveless_2550/dress_sleeveless_H4M669SD52",
    "dress_sleeveless_2550/dress_sleeveless_O6MAW25DYJ",
    "dress_sleeveless_2550/dress_sleeveless_DU7XNCTSUO",
    "dress_sleeveless_2550/dress_sleeveless_A2I7CZN43N",
    "dress_sleeveless_2550/dress_sleeveless_OAGV19HHJN",
    "dress_sleeveless_2550/dress_sleeveless_J6JU2FV4FE",
    "dress_sleeveless_2550/dress_sleeveless_UOZH0FMN2H",
    "dress_sleeveless_2550/dress_sleeveless_OGATUEZSCS",
    "dress_sleeveless_2550/dress_sleeveless_LZ1M6J0GBX",
    "dress_sleeveless_2550/dress_sleeveless_9CB1BTM5F6",
    "dress_sleeveless_2550/dress_sleeveless_ZLEN6S06LR",
    "dress_sleeveless_2550/dress_sleeveless_7I1DCEDZ6X",
    "dress_sleeveless_2550/dress_sleeveless_P02XC4T2MG",
    "dress_sleeveless_2550/dress_sleeveless_6X6ZBJE6PA",
    "dress_sleeveless_2550/dress_sleeveless_FCDX2N6U08",
    "dress_sleeveless_2550/dress_sleeveless_QC39CV3HOO",
    "dress_sleeveless_2550/dress_sleeveless_QA4JTMXVAF",
    "dress_sleeveless_2550/dress_sleeveless_ZG2ZYZZWU6",
    "dress_sleeveless_2550/dress_sleeveless_2NCK1GHW2D",
    "dress_sleeveless_2550/dress_sleeveless_PTLT0L2C66",
    "dress_sleeveless_2550/dress_sleeveless_S6H6SF0XHM",
    "dress_sleeveless_2550/dress_sleeveless_BZ06FHNVCR",
    "dress_sleeveless_2550/dress_sleeveless_UQGZXM3Z8G",
    "dress_sleeveless_2550/dress_sleeveless_9VOK4IC4N3",
    "dress_sleeveless_2550/dress_sleeveless_6FCYEK7RI0",
    "dress_sleeveless_2550/dress_sleeveless_5ZUCUAJ3BB",
    "dress_sleeveless_2550/dress_sleeveless_QXQNFMVNOB",
    "dress_sleeveless_2550/dress_sleeveless_U1E4ARIF8G",
    "dress_sleeveless_2550/dress_sleeveless_7481MJ7RKS",
    "dress_sleeveless_2550/dress_sleeveless_UFKL3JB8P2",
    "dress_sleeveless_2550/dress_sleeveless_ZBYJ7LLF73",
    "dress_sleeveless_2550/dress_sleeveless_DF9G3YE5PN",
    "dress_sleeveless_2550/dress_sleeveless_GDR78FW9IN",
    "dress_sleeveless_2550/dress_sleeveless_REUI7H5CSO",
    "dress_sleeveless_2550/dress_sleeveless_XE2F3QVMLR",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_YS8U2TL0XX",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_KDPWANS0KU",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_8H4GIZ89MR",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_680GMYVMTW",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_QKBY7HZS00",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_N7LDOVIFC1",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_255DLZ4GPZ",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_M3SUWGLDW7",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_1PNMU1JSPD",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_4514INWWEQ",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_SSL9SO1DTU",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_ZJKKIQE03Y",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_YPMEBK5LQE",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_U4335IA3T3",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_2DPKSSIOXI",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_XQJZ66JE6U",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_8YEA4X0ZKV",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_ZCRA8QDD2Y",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_3EWW56PZ49",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_XLEUTAC3S0",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_FWJRK31PXL",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_IC0W7PIO2N",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_5X9FDD680O",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_I1SCS6A39P",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_5A0O5JK56Y",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_T4G6TTDPXG",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_7TYHBLOYOF",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_4HE4IK4EY1",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_T16MJTV488",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_21NQYSW38A",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_S7J0Y0CHPS",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_LA1JYJEW7T",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_XXM984PKD4",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_RAMY0K52LX",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_JUZ4WLSP0B",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_YRW8C9F2PL",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_FSDQSE9CS8",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_AYYGFKYN9Q",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_7Y6D91Y9KU",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_UM673YG5EC",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_UKM2CKATPL",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_QJJS74TB6M",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_O48NXWVL54",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_AUVQ97B50I",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_UQMS75V78M",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_SGPC8P0ERN",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_5JZD9IFMA4",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_PFUYYU2U2B",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_AFYVBVNOQG",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_VIO8RBT14Z",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_ETRYJXKSNM",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_1EWYEEBXNC",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_IPRHCDH28X",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_P4U0R57BA0",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_3HJ6N5K4GC",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_C2M7ZWCGJO",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_6E4YSF3JPU",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_14KNRLDW4P",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_FJ1CKBXYOF",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_NXG3HUM2KC",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_7SYBDF9S8A",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_VU70TGR6PJ",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_HVB4O9EDFX",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_O7CBQ7OU07",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_ND1XNQIDFX",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_RWST8JDBN1",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_3TJYWCAY47",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_TA2IXQTJOY",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_D5K4RMZFNS",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_9SUYSJ74GA",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_JF65TCSUET",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_23FYOB540O",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_DI0LB26529",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_PYDYHEB6R3",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_HTD9Z1RHL6",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_HO5VIG7X2B",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_I649E8G1X9",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_WM0MDCEUC8",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_LCRGQDJ95G",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_393M7ZXV1T",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_UPXUNX70VO",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_HVADMM7SC4",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_JIMLR6COLY",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_QFWVVEWEAS",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_NWDCAEZWYG",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_6MSU5Z5DU3",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_J8KRWXAAAP",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_GT6QX5G1U0",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_8W79SY0ZYO",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_8A6R8XYE9N",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_1ZQBAPI6IB",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_6XFGV8EGVH",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_7F1DXBUYRF",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_I8LW7YHLDH",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_NS1ERIDQH9",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_TCRU0DIRQZ",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_YJQKIK7WW4",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_BHKQCGABGY",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_W9WCTRH9O2",
    "jumpsuit_sleeveless_2000/jumpsuit_sleeveless_7I5YAMFWIQ",
    "skirt_8_panels_1000/skirt_8_panels_5QARLGYGPR",
    "skirt_8_panels_1000/skirt_8_panels_KJSU0Y7BH6",
    "skirt_8_panels_1000/skirt_8_panels_0ZFZA9K3LS",
    "skirt_8_panels_1000/skirt_8_panels_1KX3CVGFNY",
    "skirt_8_panels_1000/skirt_8_panels_89QWGHRTDC",
    "skirt_8_panels_1000/skirt_8_panels_OVHQUJ53DV",
    "skirt_8_panels_1000/skirt_8_panels_PWMJ753KTY",
    "skirt_8_panels_1000/skirt_8_panels_2TN8YPTK1Q",
    "skirt_8_panels_1000/skirt_8_panels_3WL4T0S2RC",
    "skirt_8_panels_1000/skirt_8_panels_XIHJ1N57F9",
    "skirt_8_panels_1000/skirt_8_panels_9PVP2WR9FX",
    "skirt_8_panels_1000/skirt_8_panels_R1WC5ZYT1W",
    "skirt_8_panels_1000/skirt_8_panels_3YY4TRF8EN",
    "skirt_8_panels_1000/skirt_8_panels_5M8TSWL685",
    "skirt_8_panels_1000/skirt_8_panels_9GKWCQNI9S",
    "skirt_8_panels_1000/skirt_8_panels_RW4BMF3UOX",
    "skirt_8_panels_1000/skirt_8_panels_N9EAZIDLT7",
    "skirt_8_panels_1000/skirt_8_panels_FA4YSJVL96",
    "skirt_8_panels_1000/skirt_8_panels_AGG2ZNKFDG",
    "skirt_8_panels_1000/skirt_8_panels_M10EK08P25",
    "skirt_8_panels_1000/skirt_8_panels_BKEYZ42CUG",
    "skirt_8_panels_1000/skirt_8_panels_BZJ1JN7R8E",
    "skirt_8_panels_1000/skirt_8_panels_HWLWU20HP1",
    "skirt_8_panels_1000/skirt_8_panels_BXWOWNOTUY",
    "skirt_8_panels_1000/skirt_8_panels_Q0ZLMO63CX",
    "skirt_8_panels_1000/skirt_8_panels_CUNPVTABT0",
    "skirt_8_panels_1000/skirt_8_panels_FHUR7CC6M6",
    "skirt_8_panels_1000/skirt_8_panels_NEXZPNM3NE",
    "skirt_8_panels_1000/skirt_8_panels_XZHH2NTEP0",
    "skirt_8_panels_1000/skirt_8_panels_E3TU4TJTZX",
    "skirt_8_panels_1000/skirt_8_panels_6AMD9X5C00",
    "skirt_8_panels_1000/skirt_8_panels_FHJW1449BM",
    "skirt_8_panels_1000/skirt_8_panels_G9V2KOMFJJ",
    "skirt_8_panels_1000/skirt_8_panels_YE0N7QAA7N",
    "skirt_8_panels_1000/skirt_8_panels_V1QU604N2A",
    "skirt_8_panels_1000/skirt_8_panels_552Z0I49BI",
    "skirt_8_panels_1000/skirt_8_panels_AAI2J944OY",
    "skirt_8_panels_1000/skirt_8_panels_B7OC6QW5DZ",
    "skirt_8_panels_1000/skirt_8_panels_9CFN8SEB4G",
    "skirt_8_panels_1000/skirt_8_panels_O535U6Q4IR",
    "skirt_8_panels_1000/skirt_8_panels_OI5ZYZP8J2",
    "skirt_8_panels_1000/skirt_8_panels_C9AICRC3NK",
    "skirt_8_panels_1000/skirt_8_panels_D165A9JOFF",
    "skirt_8_panels_1000/skirt_8_panels_V8FNG7G1SS",
    "skirt_8_panels_1000/skirt_8_panels_HCV37LS8R8",
    "skirt_8_panels_1000/skirt_8_panels_IUGZU6CMCS",
    "skirt_8_panels_1000/skirt_8_panels_LM0A5SSBM6",
    "skirt_8_panels_1000/skirt_8_panels_P82O9L6NAZ",
    "skirt_8_panels_1000/skirt_8_panels_CLEI3702J2",
    "skirt_8_panels_1000/skirt_8_panels_Z126BPV1DI",
    "skirt_8_panels_1000/skirt_8_panels_YHMCJKTSQQ",
    "skirt_8_panels_1000/skirt_8_panels_RJL5QQRPN3",
    "skirt_8_panels_1000/skirt_8_panels_8552WKQZQ9",
    "skirt_8_panels_1000/skirt_8_panels_B84KZW2E1C",
    "skirt_8_panels_1000/skirt_8_panels_9GL2ORLWXV",
    "skirt_8_panels_1000/skirt_8_panels_JVD6ZPRANI",
    "skirt_8_panels_1000/skirt_8_panels_I3XTGPEUO8",
    "skirt_8_panels_1000/skirt_8_panels_1GKHHIA8F4",
    "skirt_8_panels_1000/skirt_8_panels_KMCKSG158O",
    "skirt_8_panels_1000/skirt_8_panels_NCDJSZ9ZSO",
    "skirt_8_panels_1000/skirt_8_panels_0Y9LUL9ODR",
    "skirt_8_panels_1000/skirt_8_panels_CAWBC4IA0P",
    "skirt_8_panels_1000/skirt_8_panels_Z2L08H4AQB",
    "skirt_8_panels_1000/skirt_8_panels_09WER7TYKW",
    "skirt_8_panels_1000/skirt_8_panels_AW9J3A8MPF",
    "skirt_8_panels_1000/skirt_8_panels_CZ597ESNEV",
    "skirt_8_panels_1000/skirt_8_panels_BET9699BUB",
    "skirt_8_panels_1000/skirt_8_panels_CMS92DVCHE",
    "skirt_8_panels_1000/skirt_8_panels_C363ACWMS4",
    "skirt_8_panels_1000/skirt_8_panels_GWN4SQF8L5",
    "skirt_8_panels_1000/skirt_8_panels_0GS1AO8ZM0",
    "skirt_8_panels_1000/skirt_8_panels_0XA5VN10EB",
    "skirt_8_panels_1000/skirt_8_panels_IDIGH78VJ3",
    "skirt_8_panels_1000/skirt_8_panels_C1WKER7TJ4",
    "skirt_8_panels_1000/skirt_8_panels_VG4VHQ25NU",
    "skirt_8_panels_1000/skirt_8_panels_B4EWYBQYS0",
    "skirt_8_panels_1000/skirt_8_panels_G5PEVKW2X8",
    "skirt_8_panels_1000/skirt_8_panels_3T39GOW4I3",
    "skirt_8_panels_1000/skirt_8_panels_ZBUVNMWP7S",
    "skirt_8_panels_1000/skirt_8_panels_LVYVSEU6ZV",
    "skirt_8_panels_1000/skirt_8_panels_6UMDB4EQE2",
    "skirt_8_panels_1000/skirt_8_panels_LYKN9Z3A6D",
    "skirt_8_panels_1000/skirt_8_panels_UZ7KIG7ZJH",
    "skirt_8_panels_1000/skirt_8_panels_Y94TQMEYH5",
    "skirt_8_panels_1000/skirt_8_panels_2VWHX4XX9Y",
    "skirt_8_panels_1000/skirt_8_panels_LAG3BXR99Y",
    "skirt_8_panels_1000/skirt_8_panels_5WJBOUD0YO",
    "skirt_8_panels_1000/skirt_8_panels_4U4XVVGPGX",
    "skirt_8_panels_1000/skirt_8_panels_8W63VIUSLW",
    "skirt_8_panels_1000/skirt_8_panels_NFIQI4TO4M",
    "skirt_8_panels_1000/skirt_8_panels_53DJ4SRV67",
    "skirt_8_panels_1000/skirt_8_panels_BOBMT81KP7",
    "skirt_8_panels_1000/skirt_8_panels_ULYKZOR6KD",
    "skirt_8_panels_1000/skirt_8_panels_3SAKYBEWYO",
    "skirt_8_panels_1000/skirt_8_panels_A69KSGZEB1",
    "skirt_8_panels_1000/skirt_8_panels_Y42QQ7WEIL",
    "skirt_8_panels_1000/skirt_8_panels_WYZFHITRDE",
    "skirt_8_panels_1000/skirt_8_panels_1V44T6B4O8",
    "skirt_8_panels_1000/skirt_8_panels_9PLBD5NQAQ",
    "skirt_8_panels_1000/skirt_8_panels_CPXYR91RX5",
    "wb_pants_straight_1500/wb_pants_straight_S4OQ1JILXS",
    "wb_pants_straight_1500/wb_pants_straight_0577Q8K3YE",
    "wb_pants_straight_1500/wb_pants_straight_PHS8I97S2C",
    "wb_pants_straight_1500/wb_pants_straight_922OJJIJPU",
    "wb_pants_straight_1500/wb_pants_straight_8T38ZAY9Z1",
    "wb_pants_straight_1500/wb_pants_straight_RTWKW7K1X9",
    "wb_pants_straight_1500/wb_pants_straight_XYGADPQD8G",
    "wb_pants_straight_1500/wb_pants_straight_KYPAD9JQ6Y",
    "wb_pants_straight_1500/wb_pants_straight_VJD8W6TOD5",
    "wb_pants_straight_1500/wb_pants_straight_MZTES81CZY",
    "wb_pants_straight_1500/wb_pants_straight_T6EE63QHZB",
    "wb_pants_straight_1500/wb_pants_straight_8QWD0ZOSRY",
    "wb_pants_straight_1500/wb_pants_straight_C562UC1Q73",
    "wb_pants_straight_1500/wb_pants_straight_DSTRCPVFEJ",
    "wb_pants_straight_1500/wb_pants_straight_L8G9RO3WDM",
    "wb_pants_straight_1500/wb_pants_straight_W0HRH8JYAT",
    "wb_pants_straight_1500/wb_pants_straight_HRDBYLB3LY",
    "wb_pants_straight_1500/wb_pants_straight_Y78KRU1IZR",
    "wb_pants_straight_1500/wb_pants_straight_BVGG16MFTR",
    "wb_pants_straight_1500/wb_pants_straight_VTZTKR7T7M",
    "wb_pants_straight_1500/wb_pants_straight_MA1SQHWIO1",
    "wb_pants_straight_1500/wb_pants_straight_SWL0L60NXS",
    "wb_pants_straight_1500/wb_pants_straight_1SGWXT9W8X",
    "wb_pants_straight_1500/wb_pants_straight_FIIZBX3FHT",
    "wb_pants_straight_1500/wb_pants_straight_WWECY708WQ",
    "wb_pants_straight_1500/wb_pants_straight_A62X042T09",
    "wb_pants_straight_1500/wb_pants_straight_GLWNM1OQYH",
    "wb_pants_straight_1500/wb_pants_straight_9BTU5FQTME",
    "wb_pants_straight_1500/wb_pants_straight_MBU8VJLU8B",
    "wb_pants_straight_1500/wb_pants_straight_9UVOG468QC",
    "wb_pants_straight_1500/wb_pants_straight_RG2CFWWHBG",
    "wb_pants_straight_1500/wb_pants_straight_N0418WGNK3",
    "wb_pants_straight_1500/wb_pants_straight_FS3SQ33PGR",
    "wb_pants_straight_1500/wb_pants_straight_N8ZMS4GBO3",
    "wb_pants_straight_1500/wb_pants_straight_1GDSX6FY1H",
    "wb_pants_straight_1500/wb_pants_straight_VIP0WC3EAP",
    "wb_pants_straight_1500/wb_pants_straight_JFQ3305XV3",
    "wb_pants_straight_1500/wb_pants_straight_CHMYUNYP3B",
    "wb_pants_straight_1500/wb_pants_straight_ITUCK2E5M7",
    "wb_pants_straight_1500/wb_pants_straight_8HSWMQJUGP",
    "wb_pants_straight_1500/wb_pants_straight_PJU0PBJZXO",
    "wb_pants_straight_1500/wb_pants_straight_7ZJDN0SWHW",
    "wb_pants_straight_1500/wb_pants_straight_TGVS61U457",
    "wb_pants_straight_1500/wb_pants_straight_I1QVSEJLFS",
    "wb_pants_straight_1500/wb_pants_straight_5R6AAWKW4V",
    "wb_pants_straight_1500/wb_pants_straight_S74V4TN8FR",
    "wb_pants_straight_1500/wb_pants_straight_PKWYYCLUGY",
    "wb_pants_straight_1500/wb_pants_straight_RPZIARIK9N",
    "wb_pants_straight_1500/wb_pants_straight_5VHL2WRG8V",
    "wb_pants_straight_1500/wb_pants_straight_R5D6CWSU1A",
    "wb_pants_straight_1500/wb_pants_straight_9WAEQXPA06",
    "wb_pants_straight_1500/wb_pants_straight_HKLKED0NNR",
    "wb_pants_straight_1500/wb_pants_straight_OLVXV934TR",
    "wb_pants_straight_1500/wb_pants_straight_JFH3Z0NZUM",
    "wb_pants_straight_1500/wb_pants_straight_A1APRF1I3M",
    "wb_pants_straight_1500/wb_pants_straight_PIB175C0YH",
    "wb_pants_straight_1500/wb_pants_straight_C7WN0RTJ1N",
    "wb_pants_straight_1500/wb_pants_straight_K14UCYJT3D",
    "wb_pants_straight_1500/wb_pants_straight_X0KQ7V57T4",
    "wb_pants_straight_1500/wb_pants_straight_6O1BXII3HH",
    "wb_pants_straight_1500/wb_pants_straight_1BUAZB6LV9",
    "wb_pants_straight_1500/wb_pants_straight_VCHQAKSTSX",
    "wb_pants_straight_1500/wb_pants_straight_PSRO5EN00T",
    "wb_pants_straight_1500/wb_pants_straight_QVHCLN47TC",
    "wb_pants_straight_1500/wb_pants_straight_66X62I8WAM",
    "wb_pants_straight_1500/wb_pants_straight_X59SBIIPU0",
    "wb_pants_straight_1500/wb_pants_straight_P247ZJMHE4",
    "wb_pants_straight_1500/wb_pants_straight_G1F5BHFM2B",
    "wb_pants_straight_1500/wb_pants_straight_LVKW7T5DG9",
    "wb_pants_straight_1500/wb_pants_straight_7CIHFBAFEW",
    "wb_pants_straight_1500/wb_pants_straight_FFV03Z0AH7",
    "wb_pants_straight_1500/wb_pants_straight_L75N7K7HBB",
    "wb_pants_straight_1500/wb_pants_straight_E3VFPQD1EO",
    "wb_pants_straight_1500/wb_pants_straight_D53CMKNOA4",
    "wb_pants_straight_1500/wb_pants_straight_OALAMA65UD",
    "wb_pants_straight_1500/wb_pants_straight_Z4G425M6UR",
    "wb_pants_straight_1500/wb_pants_straight_ZGQYQ30AX2",
    "wb_pants_straight_1500/wb_pants_straight_X74OUN6USX",
    "wb_pants_straight_1500/wb_pants_straight_79QQ9VKI62",
    "wb_pants_straight_1500/wb_pants_straight_25ENDH3G2R",
    "wb_pants_straight_1500/wb_pants_straight_G2EMB8P8EG",
    "wb_pants_straight_1500/wb_pants_straight_CXOVY2GGDV",
    "wb_pants_straight_1500/wb_pants_straight_U3LRC3FSPZ",
    "wb_pants_straight_1500/wb_pants_straight_ATR9YYR0K8",
    "wb_pants_straight_1500/wb_pants_straight_V0TVBLW5QH",
    "wb_pants_straight_1500/wb_pants_straight_39VQ2I478P",
    "wb_pants_straight_1500/wb_pants_straight_AFQSVOOB23",
    "wb_pants_straight_1500/wb_pants_straight_OO8IYIZJHX",
    "wb_pants_straight_1500/wb_pants_straight_U4R8KBCF4L",
    "wb_pants_straight_1500/wb_pants_straight_S6WBCKC1YB",
    "wb_pants_straight_1500/wb_pants_straight_F2JP115XEM",
    "wb_pants_straight_1500/wb_pants_straight_W1SSJT1RSX",
    "wb_pants_straight_1500/wb_pants_straight_Q1EKM3RUU2",
    "wb_pants_straight_1500/wb_pants_straight_IM823PTOM3",
    "wb_pants_straight_1500/wb_pants_straight_45D2RE0RNW",
    "wb_pants_straight_1500/wb_pants_straight_TXJANO7H7K",
    "wb_pants_straight_1500/wb_pants_straight_H4QFDHKTYC",
    "wb_pants_straight_1500/wb_pants_straight_XXRKVSYGUJ",
    "wb_pants_straight_1500/wb_pants_straight_P1J2TXPLYJ",
    "wb_pants_straight_1500/wb_pants_straight_IFCMXBKX7R",
    "skirt_2_panels_1200/skirt_2_panels_MQ3JEZ968H",
    "skirt_2_panels_1200/skirt_2_panels_4L4Q2KREAL",
    "skirt_2_panels_1200/skirt_2_panels_HNI60BBUIA",
    "skirt_2_panels_1200/skirt_2_panels_OKBWN0VP7Y",
    "skirt_2_panels_1200/skirt_2_panels_FM1GUARPYK",
    "skirt_2_panels_1200/skirt_2_panels_CGTLOHCS3F",
    "skirt_2_panels_1200/skirt_2_panels_SYSBMPG8UU",
    "skirt_2_panels_1200/skirt_2_panels_WXL9ZEOSYB",
    "skirt_2_panels_1200/skirt_2_panels_EX2EBGM0KR",
    "skirt_2_panels_1200/skirt_2_panels_565PFB5D5A",
    "skirt_2_panels_1200/skirt_2_panels_75A9AGCE7E",
    "skirt_2_panels_1200/skirt_2_panels_QIZMEV1FTX",
    "skirt_2_panels_1200/skirt_2_panels_TRIHAY3YW7",
    "skirt_2_panels_1200/skirt_2_panels_K03H89C9T7",
    "skirt_2_panels_1200/skirt_2_panels_DS0V2LY4T3",
    "skirt_2_panels_1200/skirt_2_panels_XU68KBO3I1",
    "skirt_2_panels_1200/skirt_2_panels_2NLVQXVTM3",
    "skirt_2_panels_1200/skirt_2_panels_KUEVIMPX0R",
    "skirt_2_panels_1200/skirt_2_panels_7JUO936WA4",
    "skirt_2_panels_1200/skirt_2_panels_FADDZR8OF7",
    "skirt_2_panels_1200/skirt_2_panels_M25HOPFJ1Y",
    "skirt_2_panels_1200/skirt_2_panels_BNR2S5W3GQ",
    "skirt_2_panels_1200/skirt_2_panels_N1WMN1RX9I",
    "skirt_2_panels_1200/skirt_2_panels_REB384B95P",
    "skirt_2_panels_1200/skirt_2_panels_QVXJLSZ3IW",
    "skirt_2_panels_1200/skirt_2_panels_Z58E9B10UH",
    "skirt_2_panels_1200/skirt_2_panels_TFQG0IPERQ",
    "skirt_2_panels_1200/skirt_2_panels_V5BS8HOSC1",
    "skirt_2_panels_1200/skirt_2_panels_PWJ38WBXVA",
    "skirt_2_panels_1200/skirt_2_panels_WQMGLEQJHK",
    "skirt_2_panels_1200/skirt_2_panels_8NC9AW4VNT",
    "skirt_2_panels_1200/skirt_2_panels_HXA7QBA087",
    "skirt_2_panels_1200/skirt_2_panels_GHJL3VJAKR",
    "skirt_2_panels_1200/skirt_2_panels_X2LBVZVDND",
    "skirt_2_panels_1200/skirt_2_panels_9Q2KXNYDFX",
    "skirt_2_panels_1200/skirt_2_panels_DYMDIDSN0B",
    "skirt_2_panels_1200/skirt_2_panels_475MHVCS5X",
    "skirt_2_panels_1200/skirt_2_panels_U36CAZSRJU",
    "skirt_2_panels_1200/skirt_2_panels_VBD1N7OMY4",
    "skirt_2_panels_1200/skirt_2_panels_HL0DZP5ZRP",
    "skirt_2_panels_1200/skirt_2_panels_767SZE0VIF",
    "skirt_2_panels_1200/skirt_2_panels_3KEBU1PTCX",
    "skirt_2_panels_1200/skirt_2_panels_6G5RCKU6UT",
    "skirt_2_panels_1200/skirt_2_panels_0KYE67XMI5",
    "skirt_2_panels_1200/skirt_2_panels_SA9YVPKE51",
    "skirt_2_panels_1200/skirt_2_panels_E7FP5Y6SVC",
    "skirt_2_panels_1200/skirt_2_panels_BXWX0STO07",
    "skirt_2_panels_1200/skirt_2_panels_Q98K5U937R",
    "skirt_2_panels_1200/skirt_2_panels_N5OB3TD42D",
    "skirt_2_panels_1200/skirt_2_panels_SUYFVIZA75",
    "skirt_2_panels_1200/skirt_2_panels_1AGU3LOZRZ",
    "skirt_2_panels_1200/skirt_2_panels_EWIB363K3R",
    "skirt_2_panels_1200/skirt_2_panels_2HQH45WKYR",
    "skirt_2_panels_1200/skirt_2_panels_E5RJYX2E7H",
    "skirt_2_panels_1200/skirt_2_panels_E0WGD9KXGZ",
    "skirt_2_panels_1200/skirt_2_panels_Z46IC56RY5",
    "skirt_2_panels_1200/skirt_2_panels_4O71CZ1O5O",
    "skirt_2_panels_1200/skirt_2_panels_O7U09FD4PQ",
    "skirt_2_panels_1200/skirt_2_panels_AE4P434FNW",
    "skirt_2_panels_1200/skirt_2_panels_GDRI9UWYD8",
    "skirt_2_panels_1200/skirt_2_panels_RJR64PHJMJ",
    "skirt_2_panels_1200/skirt_2_panels_3487KOAIRS",
    "skirt_2_panels_1200/skirt_2_panels_UQ76XRM4B6",
    "skirt_2_panels_1200/skirt_2_panels_522NLDCFI5",
    "skirt_2_panels_1200/skirt_2_panels_XVBINOMNMU",
    "skirt_2_panels_1200/skirt_2_panels_IJ0Z6E6TRW",
    "skirt_2_panels_1200/skirt_2_panels_KYKTZ7GNZ6",
    "skirt_2_panels_1200/skirt_2_panels_AMFAC0OO4J",
    "skirt_2_panels_1200/skirt_2_panels_PGG3HLUR82",
    "skirt_2_panels_1200/skirt_2_panels_G2BWHU162U",
    "skirt_2_panels_1200/skirt_2_panels_BEEVJC55B5",
    "skirt_2_panels_1200/skirt_2_panels_TSB1X6071H",
    "skirt_2_panels_1200/skirt_2_panels_BINZOWQ0M6",
    "skirt_2_panels_1200/skirt_2_panels_MMRHCY6UVJ",
    "skirt_2_panels_1200/skirt_2_panels_ETVGD32EEY",
    "skirt_2_panels_1200/skirt_2_panels_KY1RWT3Y7A",
    "skirt_2_panels_1200/skirt_2_panels_SV5C4GAK38",
    "skirt_2_panels_1200/skirt_2_panels_XO4IWGBDRF",
    "skirt_2_panels_1200/skirt_2_panels_P6G5PV3ZWF",
    "skirt_2_panels_1200/skirt_2_panels_462G4MECYE",
    "skirt_2_panels_1200/skirt_2_panels_T9CHO9BU9E",
    "skirt_2_panels_1200/skirt_2_panels_JD1S62RDG8",
    "skirt_2_panels_1200/skirt_2_panels_Y51UDM21TQ",
    "skirt_2_panels_1200/skirt_2_panels_78TSTMTKAR",
    "skirt_2_panels_1200/skirt_2_panels_0L4CEREPFD",
    "skirt_2_panels_1200/skirt_2_panels_FBA6ABTNW6",
    "skirt_2_panels_1200/skirt_2_panels_NMXIAVCJ19",
    "skirt_2_panels_1200/skirt_2_panels_6X3SDDTVXZ",
    "skirt_2_panels_1200/skirt_2_panels_V335SDK3XU",
    "skirt_2_panels_1200/skirt_2_panels_0NW4TDXIV3",
    "skirt_2_panels_1200/skirt_2_panels_5UB83KQGAG",
    "skirt_2_panels_1200/skirt_2_panels_SHN7RUUOV7",
    "skirt_2_panels_1200/skirt_2_panels_BWCB6675RH",
    "skirt_2_panels_1200/skirt_2_panels_N1530X1CBV",
    "skirt_2_panels_1200/skirt_2_panels_SJYN1DY9P9",
    "skirt_2_panels_1200/skirt_2_panels_CWI1KSQVO2",
    "skirt_2_panels_1200/skirt_2_panels_65S96IOZG4",
    "skirt_2_panels_1200/skirt_2_panels_I0PETMMB2Z",
    "skirt_2_panels_1200/skirt_2_panels_03Q4C6J8PM",
    "skirt_2_panels_1200/skirt_2_panels_6IQGD1M4NR",
    "jacket_2200/jacket_BNKTJO90FD",
    "jacket_2200/jacket_NE8XBORLST",
    "jacket_2200/jacket_H3283LITMV",
    "jacket_2200/jacket_SGPF1EP4WN",
    "jacket_2200/jacket_BL7ZKR5YR2",
    "jacket_2200/jacket_L58BCC05Z4",
    "jacket_2200/jacket_HVGYK4Z3HF",
    "jacket_2200/jacket_6X63L3NP9G",
    "jacket_2200/jacket_WVJKTR3P3O",
    "jacket_2200/jacket_REW4U3QBUG",
    "jacket_2200/jacket_2T3CQBTOI7",
    "jacket_2200/jacket_CXEHM7DVD2",
    "jacket_2200/jacket_N9DFT03LV0",
    "jacket_2200/jacket_MLKWEMKY4O",
    "jacket_2200/jacket_KD10UK85DO",
    "jacket_2200/jacket_UT8AX1AVVY",
    "jacket_2200/jacket_INZDU07PZ8",
    "jacket_2200/jacket_RDZI5JRI6D",
    "jacket_2200/jacket_GSOTQQETSX",
    "jacket_2200/jacket_4Q59RF6UER",
    "jacket_2200/jacket_U9209G0FRA",
    "jacket_2200/jacket_T47EHPWJQT",
    "jacket_2200/jacket_WRJAEI9HZB",
    "jacket_2200/jacket_Y039AV89YV",
    "jacket_2200/jacket_EUXCJW01QS",
    "jacket_2200/jacket_Z1TQX243YQ",
    "jacket_2200/jacket_NS9JL4SM3B",
    "jacket_2200/jacket_1BEXJMF6HA",
    "jacket_2200/jacket_KJXIXHR9LJ",
    "jacket_2200/jacket_DK4BSOJTHM",
    "jacket_2200/jacket_65NK0JWL0X",
    "jacket_2200/jacket_C2MLSDER0M",
    "jacket_2200/jacket_NV5CJOVGIQ",
    "jacket_2200/jacket_HJJ7879H7J",
    "jacket_2200/jacket_12C2A5KCKH",
    "jacket_2200/jacket_77GOFG1TYI",
    "jacket_2200/jacket_4FJY34V5HK",
    "jacket_2200/jacket_A4WF5P97UI",
    "jacket_2200/jacket_YK5T0DLPDS",
    "jacket_2200/jacket_FMOKQECM5H",
    "jacket_2200/jacket_NPFI6CI93I",
    "jacket_2200/jacket_OMR0CCERCP",
    "jacket_2200/jacket_O50LJWYIMW",
    "jacket_2200/jacket_AUJ23F1DI9",
    "jacket_2200/jacket_ITF2HM5FR4",
    "jacket_2200/jacket_TMHHFB44DU",
    "jacket_2200/jacket_YZPZ93DT6F",
    "jacket_2200/jacket_8SF4C2J0N0",
    "jacket_2200/jacket_SFIKQVSZQH",
    "jacket_2200/jacket_5PLWYHYKUO",
    "jacket_2200/jacket_B6GL2TDJLV",
    "jacket_2200/jacket_IJ7MTXLH3W",
    "jacket_2200/jacket_UF06O4D72X",
    "jacket_2200/jacket_7D485EP7SO",
    "jacket_2200/jacket_F8QY84QSO3",
    "jacket_2200/jacket_TVRHW5H4W5",
    "jacket_2200/jacket_KBLKZU8JEA",
    "jacket_2200/jacket_MVTK4IO9O2",
    "jacket_2200/jacket_RU9EH3RFKV",
    "jacket_2200/jacket_O3Q8EKUOWU",
    "jacket_2200/jacket_2FMGN4YH1W",
    "jacket_2200/jacket_HVVPY2H8NF",
    "jacket_2200/jacket_8RHF4KL0X9",
    "jacket_2200/jacket_L7VZ1VZ3SB",
    "jacket_2200/jacket_NJWE244ED0",
    "jacket_2200/jacket_TEA6LMPRO6",
    "jacket_2200/jacket_EC5SB6GZ9W",
    "jacket_2200/jacket_114UPJMJBT",
    "jacket_2200/jacket_JQ3U5XXHWG",
    "jacket_2200/jacket_9V50YCTYP2",
    "jacket_2200/jacket_JVYR3EP01N",
    "jacket_2200/jacket_CFB2RG8XZ9",
    "jacket_2200/jacket_22MMNLRLYG",
    "jacket_2200/jacket_88JSCDJBCC",
    "jacket_2200/jacket_24WOPEQIST",
    "jacket_2200/jacket_J9Y76D4KXY",
    "jacket_2200/jacket_THD75KN09E",
    "jacket_2200/jacket_3GGOEDYRCY",
    "jacket_2200/jacket_C3TM4D739D",
    "jacket_2200/jacket_FGKZP49K2P",
    "jacket_2200/jacket_SOD3NTOSU1",
    "jacket_2200/jacket_MBXTXVJSGU",
    "jacket_2200/jacket_JWCAWZYRL0",
    "jacket_2200/jacket_TT922FEO6U",
    "jacket_2200/jacket_2XKB2BBO5N",
    "jacket_2200/jacket_68ICRQ6O53",
    "jacket_2200/jacket_9DNOS6RGZX",
    "jacket_2200/jacket_Y4X4F12OL0",
    "jacket_2200/jacket_52ATEEVQUK",
    "jacket_2200/jacket_F5JKTZQ0PF",
    "jacket_2200/jacket_1B8IUBUO0K",
    "jacket_2200/jacket_Y1VHKZ7ULE",
    "jacket_2200/jacket_O7YHRRL2IC",
    "jacket_2200/jacket_CTUADUGP4B",
    "jacket_2200/jacket_01LF58XJQI",
    "jacket_2200/jacket_C3E7TWHUC7",
    "jacket_2200/jacket_6XG4632CBU",
    "jacket_2200/jacket_EMXVT8UKYT",
    "jacket_2200/jacket_RTINNARJQW",
    "jacket_2200/jacket_H80GZXKBYA",
    "tee_sleeveless_1800/tee_sleeveless_X328HFSZCS",
    "tee_sleeveless_1800/tee_sleeveless_0029TFA1QK",
    "tee_sleeveless_1800/tee_sleeveless_X7A45U8FVI",
    "tee_sleeveless_1800/tee_sleeveless_0C2JTMTSIK",
    "tee_sleeveless_1800/tee_sleeveless_CGIOG7322U",
    "tee_sleeveless_1800/tee_sleeveless_1B0WDCFQTW",
    "tee_sleeveless_1800/tee_sleeveless_GCNRYNXWCF",
    "tee_sleeveless_1800/tee_sleeveless_N7L457O4TZ",
    "tee_sleeveless_1800/tee_sleeveless_NQRZI2EG9F",
    "tee_sleeveless_1800/tee_sleeveless_07O1317FE6",
    "tee_sleeveless_1800/tee_sleeveless_YBFIEOQ7DI",
    "tee_sleeveless_1800/tee_sleeveless_ICSXBB6SGP",
    "tee_sleeveless_1800/tee_sleeveless_1PK5QXS8SV",
    "tee_sleeveless_1800/tee_sleeveless_EPSK17GK01",
    "tee_sleeveless_1800/tee_sleeveless_7WQXR8QH7X",
    "tee_sleeveless_1800/tee_sleeveless_96H0DVM0LM",
    "tee_sleeveless_1800/tee_sleeveless_I1X0HVWXOD",
    "tee_sleeveless_1800/tee_sleeveless_QN6VTPKXFE",
    "tee_sleeveless_1800/tee_sleeveless_639QUETMZN",
    "tee_sleeveless_1800/tee_sleeveless_B57M5M29HC",
    "tee_sleeveless_1800/tee_sleeveless_35D9WWSNBN",
    "tee_sleeveless_1800/tee_sleeveless_H34HNN3Q6M",
    "tee_sleeveless_1800/tee_sleeveless_EAVVU1IB03",
    "tee_sleeveless_1800/tee_sleeveless_DC5TZM1Y6J",
    "tee_sleeveless_1800/tee_sleeveless_ZD2OSNZ2NP",
    "tee_sleeveless_1800/tee_sleeveless_Q05L69O5U8",
    "tee_sleeveless_1800/tee_sleeveless_R2AHB9DQRC",
    "tee_sleeveless_1800/tee_sleeveless_HZYLM9UX61",
    "tee_sleeveless_1800/tee_sleeveless_CU9RT19ZHH",
    "tee_sleeveless_1800/tee_sleeveless_Q7L62H9QAB",
    "tee_sleeveless_1800/tee_sleeveless_J4WAXL29GO",
    "tee_sleeveless_1800/tee_sleeveless_2WZF5H4B5C",
    "tee_sleeveless_1800/tee_sleeveless_VLCXGMRCVO",
    "tee_sleeveless_1800/tee_sleeveless_HT6VIKCDTR",
    "tee_sleeveless_1800/tee_sleeveless_R75EN5CXRP",
    "tee_sleeveless_1800/tee_sleeveless_AD0RUPHJ1C",
    "tee_sleeveless_1800/tee_sleeveless_AZ619HSHFW",
    "tee_sleeveless_1800/tee_sleeveless_ZAI6I5EEAL",
    "tee_sleeveless_1800/tee_sleeveless_GT3LHX7H5Q",
    "tee_sleeveless_1800/tee_sleeveless_QON7SSF8H6",
    "tee_sleeveless_1800/tee_sleeveless_F8E4BPKL9B",
    "tee_sleeveless_1800/tee_sleeveless_2XN9BJR6TD",
    "tee_sleeveless_1800/tee_sleeveless_GHG0ZBP7QA",
    "tee_sleeveless_1800/tee_sleeveless_072U5NLI68",
    "tee_sleeveless_1800/tee_sleeveless_28T583POT1",
    "tee_sleeveless_1800/tee_sleeveless_ZKX16JBS3D",
    "tee_sleeveless_1800/tee_sleeveless_JCNQVBTXE7",
    "tee_sleeveless_1800/tee_sleeveless_2CNGEGYASF",
    "tee_sleeveless_1800/tee_sleeveless_CJKNT6NTUV",
    "tee_sleeveless_1800/tee_sleeveless_ZU474P024E",
    "tee_sleeveless_1800/tee_sleeveless_32JAR5QSEB",
    "tee_sleeveless_1800/tee_sleeveless_B8ATMP2Q96",
    "tee_sleeveless_1800/tee_sleeveless_DQHV10T1NW",
    "tee_sleeveless_1800/tee_sleeveless_UI1J53CNHA",
    "tee_sleeveless_1800/tee_sleeveless_BVDSILGJM2",
    "tee_sleeveless_1800/tee_sleeveless_MJAD4AK9LI",
    "tee_sleeveless_1800/tee_sleeveless_DJFG96XF2F",
    "tee_sleeveless_1800/tee_sleeveless_XPRPSHQIDG",
    "tee_sleeveless_1800/tee_sleeveless_FGNDA4P2FM",
    "tee_sleeveless_1800/tee_sleeveless_IH9EBNRWO3",
    "tee_sleeveless_1800/tee_sleeveless_SZQU810B9N",
    "tee_sleeveless_1800/tee_sleeveless_QBOHTWL6EW",
    "tee_sleeveless_1800/tee_sleeveless_F183B9VO7K",
    "tee_sleeveless_1800/tee_sleeveless_17AML3HBFK",
    "tee_sleeveless_1800/tee_sleeveless_J9OJJ9MTC3",
    "tee_sleeveless_1800/tee_sleeveless_9UNZVO3J8W",
    "tee_sleeveless_1800/tee_sleeveless_CCEXUJ4SYZ",
    "tee_sleeveless_1800/tee_sleeveless_M71O2LFEC8",
    "tee_sleeveless_1800/tee_sleeveless_QB09ALA1G1",
    "tee_sleeveless_1800/tee_sleeveless_UX22Q21W4F",
    "tee_sleeveless_1800/tee_sleeveless_Y57NIV8EK5",
    "tee_sleeveless_1800/tee_sleeveless_BLC235L821",
    "tee_sleeveless_1800/tee_sleeveless_WN0YXBBI5R",
    "tee_sleeveless_1800/tee_sleeveless_J4NLBXBUU9",
    "tee_sleeveless_1800/tee_sleeveless_T05I8XY93J",
    "tee_sleeveless_1800/tee_sleeveless_PUDY3V9RT8",
    "tee_sleeveless_1800/tee_sleeveless_UH8N4VQM7Q",
    "tee_sleeveless_1800/tee_sleeveless_UGL8V0TURO",
    "tee_sleeveless_1800/tee_sleeveless_HO30T9355F",
    "tee_sleeveless_1800/tee_sleeveless_4XYFBG831M",
    "tee_sleeveless_1800/tee_sleeveless_3BWK7N816Z",
    "tee_sleeveless_1800/tee_sleeveless_MA4X67Z25Q",
    "tee_sleeveless_1800/tee_sleeveless_YTJEJNL3R8",
    "tee_sleeveless_1800/tee_sleeveless_QV0D0D4WJD",
    "tee_sleeveless_1800/tee_sleeveless_U0TWOTB6KO",
    "tee_sleeveless_1800/tee_sleeveless_3AA6VT7HKR",
    "tee_sleeveless_1800/tee_sleeveless_LXVUPEGTDT",
    "tee_sleeveless_1800/tee_sleeveless_CFLDMEW2WJ",
    "tee_sleeveless_1800/tee_sleeveless_W2TFB5O623",
    "tee_sleeveless_1800/tee_sleeveless_RT0LHRVNL7",
    "tee_sleeveless_1800/tee_sleeveless_TIOCTKG6IH",
    "tee_sleeveless_1800/tee_sleeveless_8AD7Z5ZK69",
    "tee_sleeveless_1800/tee_sleeveless_DRK15LZWTK",
    "tee_sleeveless_1800/tee_sleeveless_VKUIXA667O",
    "tee_sleeveless_1800/tee_sleeveless_JHF417KL60",
    "tee_sleeveless_1800/tee_sleeveless_5JWPAHGZ0C",
    "tee_sleeveless_1800/tee_sleeveless_LFXU2ABP7Z",
    "tee_sleeveless_1800/tee_sleeveless_OXGJTGW1XQ",
    "tee_sleeveless_1800/tee_sleeveless_KWXI54M7Z3",
    "tee_sleeveless_1800/tee_sleeveless_WOFAGBKC08",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_26HWT9M1SQ",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_PM7LUKX42R",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_U5PV9QM55G",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_FOUVFI21IZ",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_XYHHRNM9GR",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_XL8K7M7JAI",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_43XBFBOE8N",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_MKU02EM75J",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_Y6SE0OAENQ",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_1ZL81TXBVM",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_7NGIZQ6FTD",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_UM9Z6CKNCI",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_DG6W4MF2JT",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_61CTT9USLE",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_ARKVCDUU0J",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_X0N2U0P60P",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_TOU1V1YLCY",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_2W0FTEHTAL",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_BRAH0WE973",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_AK3BG5ECQH",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_ULUEWRRQ0A",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_N3DKVEWCJQ",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_KQU70D3MCT",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_N9FB2BT1AE",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_G0JS82C2A3",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_6FSLC7E67U",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_5D0SECD1H5",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_AAJHJV2TAY",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_V47SWR6U9C",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_JC4DEOYOE9",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_FHXJG6POCM",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_I76T8EG2CW",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_APOH77MZ9X",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_RDKRRUPS2Q",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_7RSSALUV4Y",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_HCL9AKC4P8",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_KF1EAOXDKV",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_KBADOL695S",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_2KZ9N7J4L6",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_KGSMPSVR0J",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_ZLNKW7GQ8B",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_3XX00LZK0N",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_YCF7CE8OMR",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_92COEHLOOD",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_PK3ESZFNDF",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_C0OMGM1J9C",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_L0AURZS53V",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_CEZ3A1IPAY",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_M05ZS908YS",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_8RVQFYUYQI",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_XCOOI71IFD",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_KJPWFBBO40",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_N66U2G8QWB",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_8TDIC3UAA3",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_J55YS2D77A",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_SO5HU4ATTP",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_YF31U1YCRY",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_TWSOUUD1G1",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_FC1E66ZXG2",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_RMFZX2AP8J",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_73LVRRYSBF",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_WZOV11LPZT",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_WSVHFUCB1Y",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_EU4K9C3XD8",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_LYI1IVWZHV",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_UH432PNFAV",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_ZV3YQ11AGX",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_E8JVPDB15C",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_KCRTUSR6LU",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_JUV091KSJP",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_3BCN62C90F",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_9K9ZUI2ZJR",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_HSI644YDRN",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_EVVJ0V2II1",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_689SEYMKMD",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_M31IAKRXX2",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_F98PQ8662E",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_0CWS496SWG",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_5CTJLJ0PDR",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_F7638WBG4H",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_VZTHEATRNL",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_CTQZFGEG0W",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_WZVZFL5504",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_LEKP728FS7",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_OSNQWCX5UH",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_AH4PHHLIQ2",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_JKHXCJJB2D",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_NJH2AWYFVU",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_LM2461F6WJ",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_Q717AOZ7OV",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_JONQ2KWRS0",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_YXTRE5DHQ1",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_4H20JKOKTU",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_PG9BRFG834",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_YNGBQ1NA5Y",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_ILK7HVXY5W",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_5VLI8FRXWZ",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_JI5YNUDSAT",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_VF1H2CNV38",
    "wb_dress_sleeveless_2600/wb_dress_sleeveless_SVKT5OZBPI",
    "jacket_hood_2700/jacket_hood_PJRF5FCRNC",
    "jacket_hood_2700/jacket_hood_4ID8II9O1V",
    "jacket_hood_2700/jacket_hood_9DOWBNL374",
    "jacket_hood_2700/jacket_hood_NB5KV06G5C",
    "jacket_hood_2700/jacket_hood_J6W45IUSRX",
    "jacket_hood_2700/jacket_hood_3OQNME04HZ",
    "jacket_hood_2700/jacket_hood_BU01QWPCNH",
    "jacket_hood_2700/jacket_hood_SKWTD2LYO3",
    "jacket_hood_2700/jacket_hood_HKIGY8BMV3",
    "jacket_hood_2700/jacket_hood_LV2S15LDY6",
    "jacket_hood_2700/jacket_hood_7L5VQ9UHQK",
    "jacket_hood_2700/jacket_hood_DCZ2LYQNN9",
    "jacket_hood_2700/jacket_hood_UTIRLPXGXH",
    "jacket_hood_2700/jacket_hood_OTDO91TZTX",
    "jacket_hood_2700/jacket_hood_ROE62NPY8S",
    "jacket_hood_2700/jacket_hood_8O7PYC44XX",
    "jacket_hood_2700/jacket_hood_L29EJP277R",
    "jacket_hood_2700/jacket_hood_DORJYN8G2A",
    "jacket_hood_2700/jacket_hood_DYXLQA2VHZ",
    "jacket_hood_2700/jacket_hood_AT5BRRER48",
    "jacket_hood_2700/jacket_hood_CR5YQ05BFR",
    "jacket_hood_2700/jacket_hood_2FOQ36VWSB",
    "jacket_hood_2700/jacket_hood_B1HFX24AYW",
    "jacket_hood_2700/jacket_hood_QOP382QQ16",
    "jacket_hood_2700/jacket_hood_O4Y7QDRVXT",
    "jacket_hood_2700/jacket_hood_FRBWK8UUDI",
    "jacket_hood_2700/jacket_hood_4A9G8NIE8G",
    "jacket_hood_2700/jacket_hood_AZNHCZBMIZ",
    "jacket_hood_2700/jacket_hood_DN9PIBT82B",
    "jacket_hood_2700/jacket_hood_N68NRFZW7S",
    "jacket_hood_2700/jacket_hood_3IFE36A51L",
    "jacket_hood_2700/jacket_hood_O0HZ3JAG5F",
    "jacket_hood_2700/jacket_hood_WM8LV49SS3",
    "jacket_hood_2700/jacket_hood_1Z57NL4N4I",
    "jacket_hood_2700/jacket_hood_ST57L7YNQP",
    "jacket_hood_2700/jacket_hood_2AFUGM3WYB",
    "jacket_hood_2700/jacket_hood_KHB0F5KT2L",
    "jacket_hood_2700/jacket_hood_RHGD73DSM6",
    "jacket_hood_2700/jacket_hood_GB3HTGPQNW",
    "jacket_hood_2700/jacket_hood_RVT7BBYQKU",
    "jacket_hood_2700/jacket_hood_TRGCDWIQT6",
    "jacket_hood_2700/jacket_hood_VK5FU6OBX1",
    "jacket_hood_2700/jacket_hood_BUAJ812AFC",
    "jacket_hood_2700/jacket_hood_R0UG8O4W9A",
    "jacket_hood_2700/jacket_hood_7F5Y35B13W",
    "jacket_hood_2700/jacket_hood_9EOJCGRSRU",
    "jacket_hood_2700/jacket_hood_8IOQDNGZLK",
    "jacket_hood_2700/jacket_hood_6L00OGDVY9",
    "jacket_hood_2700/jacket_hood_4ENBEV0669",
    "jacket_hood_2700/jacket_hood_K0VFD0YKZE",
    "jacket_hood_2700/jacket_hood_BXKPQ2ZUQ7",
    "jacket_hood_2700/jacket_hood_QTADUC2KGM",
    "jacket_hood_2700/jacket_hood_TX1UPR4B01",
    "jacket_hood_2700/jacket_hood_6Y1KO2PEPK",
    "jacket_hood_2700/jacket_hood_DKEJBOAB7H",
    "jacket_hood_2700/jacket_hood_B91QF0FBNA",
    "jacket_hood_2700/jacket_hood_AI7Y1KAF14",
    "jacket_hood_2700/jacket_hood_XPWCZ9KQUW",
    "jacket_hood_2700/jacket_hood_MXAVQP7UBW",
    "jacket_hood_2700/jacket_hood_4FWWFL6H2Y",
    "jacket_hood_2700/jacket_hood_D9HMB69AKC",
    "jacket_hood_2700/jacket_hood_3DW1QICH4A",
    "jacket_hood_2700/jacket_hood_QV5VY7R4YY",
    "jacket_hood_2700/jacket_hood_4HIZLZDMIF",
    "jacket_hood_2700/jacket_hood_GJG4T3UH8I",
    "jacket_hood_2700/jacket_hood_MX7VB74X2D",
    "jacket_hood_2700/jacket_hood_PZNGXQR0YQ",
    "jacket_hood_2700/jacket_hood_7QT4YPV2XH",
    "jacket_hood_2700/jacket_hood_KXD73AEY0Y",
    "jacket_hood_2700/jacket_hood_UFUPOKJEYD",
    "jacket_hood_2700/jacket_hood_PGD4T3HW4X",
    "jacket_hood_2700/jacket_hood_8LD57OWF5C",
    "jacket_hood_2700/jacket_hood_JBU8FQW39W",
    "jacket_hood_2700/jacket_hood_6VQ0FR6HPW",
    "jacket_hood_2700/jacket_hood_V7N29D2CTZ",
    "jacket_hood_2700/jacket_hood_AJ64XUQLJZ",
    "jacket_hood_2700/jacket_hood_E5YIDPI8XK",
    "jacket_hood_2700/jacket_hood_FVCHGBSEQ0",
    "jacket_hood_2700/jacket_hood_5UGZTM1QN5",
    "jacket_hood_2700/jacket_hood_RRQDRM56P2",
    "jacket_hood_2700/jacket_hood_OZCF4TX0TA",
    "jacket_hood_2700/jacket_hood_04NLYABXWC",
    "jacket_hood_2700/jacket_hood_Q1Y2CL1DI8",
    "jacket_hood_2700/jacket_hood_J7QCTTJXFX",
    "jacket_hood_2700/jacket_hood_O91JI3JL2S",
    "jacket_hood_2700/jacket_hood_QCFHTD8721",
    "jacket_hood_2700/jacket_hood_H6NPY25F68",
    "jacket_hood_2700/jacket_hood_HU1YKQP2OZ",
    "jacket_hood_2700/jacket_hood_1ZUNERH83B",
    "jacket_hood_2700/jacket_hood_6Z10RQUAJ8",
    "jacket_hood_2700/jacket_hood_ZC88GLL31K",
    "jacket_hood_2700/jacket_hood_K79U2V2WOS",
    "jacket_hood_2700/jacket_hood_XI48WDYDBV",
    "jacket_hood_2700/jacket_hood_83E8GIRV0Z",
    "jacket_hood_2700/jacket_hood_VAPSR2JE8D",
    "jacket_hood_2700/jacket_hood_C14NNZAYMK",
    "jacket_hood_2700/jacket_hood_DK014Y8VYR",
    "jacket_hood_2700/jacket_hood_RWLLPLA1YS",
    "jacket_hood_2700/jacket_hood_H5L8FYMQV9",
    "jacket_hood_2700/jacket_hood_A10M5DOZ0Z",
    "pants_straight_sides_1000/pants_straight_sides_MCYY6YKUR5",
    "pants_straight_sides_1000/pants_straight_sides_KZ6WMMP17W",
    "pants_straight_sides_1000/pants_straight_sides_9P45F4CKEH",
    "pants_straight_sides_1000/pants_straight_sides_ODCU00VSQC",
    "pants_straight_sides_1000/pants_straight_sides_YZX8YZ26Y1",
    "pants_straight_sides_1000/pants_straight_sides_U2UC57SSNW",
    "pants_straight_sides_1000/pants_straight_sides_IZUZJBYELJ",
    "pants_straight_sides_1000/pants_straight_sides_BZE2Y4EQ7N",
    "pants_straight_sides_1000/pants_straight_sides_BI2Y37F7GB",
    "pants_straight_sides_1000/pants_straight_sides_N48RCKF2ZR",
    "pants_straight_sides_1000/pants_straight_sides_I1C1OYXQQS",
    "pants_straight_sides_1000/pants_straight_sides_IEW6FTY49E",
    "pants_straight_sides_1000/pants_straight_sides_5U6E4785KJ",
    "pants_straight_sides_1000/pants_straight_sides_C5VIN1FGDC",
    "pants_straight_sides_1000/pants_straight_sides_MU5GT2R3RZ",
    "pants_straight_sides_1000/pants_straight_sides_36AZFBKSXU",
    "pants_straight_sides_1000/pants_straight_sides_5FR9YVBR4B",
    "pants_straight_sides_1000/pants_straight_sides_80CNX32PMV",
    "pants_straight_sides_1000/pants_straight_sides_6AW3LO89MV",
    "pants_straight_sides_1000/pants_straight_sides_36WG2KCB8R",
    "pants_straight_sides_1000/pants_straight_sides_KY45U3K8EZ",
    "pants_straight_sides_1000/pants_straight_sides_FDJXBK0GOW",
    "pants_straight_sides_1000/pants_straight_sides_9V1Y84GH6T",
    "pants_straight_sides_1000/pants_straight_sides_FVNRTOVO4N",
    "pants_straight_sides_1000/pants_straight_sides_Q1VEIW8HUU",
    "pants_straight_sides_1000/pants_straight_sides_74VO0D3854",
    "pants_straight_sides_1000/pants_straight_sides_YPSQNCGVW5",
    "pants_straight_sides_1000/pants_straight_sides_NMZ10SJGW5",
    "pants_straight_sides_1000/pants_straight_sides_JW846MSF3Z",
    "pants_straight_sides_1000/pants_straight_sides_AAYXACGPIU",
    "pants_straight_sides_1000/pants_straight_sides_8EA79JVN5S",
    "pants_straight_sides_1000/pants_straight_sides_N68MF0SOOG",
    "pants_straight_sides_1000/pants_straight_sides_FE4KQIE84Z",
    "pants_straight_sides_1000/pants_straight_sides_QOF0I56DIJ",
    "pants_straight_sides_1000/pants_straight_sides_XMPPU6HG1A",
    "pants_straight_sides_1000/pants_straight_sides_8G19BGH1H3",
    "pants_straight_sides_1000/pants_straight_sides_MBO7Y75ET7",
    "pants_straight_sides_1000/pants_straight_sides_TI4NLWRWWK",
    "pants_straight_sides_1000/pants_straight_sides_CZCGHX1Y9F",
    "pants_straight_sides_1000/pants_straight_sides_RCI1QWW3C6",
    "pants_straight_sides_1000/pants_straight_sides_H17582GQWS",
    "pants_straight_sides_1000/pants_straight_sides_8LK88F1FMA",
    "pants_straight_sides_1000/pants_straight_sides_3H3Y0F4PZF",
    "pants_straight_sides_1000/pants_straight_sides_OBQ3163NY9",
    "pants_straight_sides_1000/pants_straight_sides_Q5HXRKY3XZ",
    "pants_straight_sides_1000/pants_straight_sides_1QO8383QXZ",
    "pants_straight_sides_1000/pants_straight_sides_TUBDB5H2YQ",
    "pants_straight_sides_1000/pants_straight_sides_FEFA8JBFV9",
    "pants_straight_sides_1000/pants_straight_sides_OOKDYQUIUM",
    "pants_straight_sides_1000/pants_straight_sides_GQXLLIW1NL",
    "pants_straight_sides_1000/pants_straight_sides_MRGFQTWZ9U",
    "pants_straight_sides_1000/pants_straight_sides_S6QY4Z88CT",
    "pants_straight_sides_1000/pants_straight_sides_RMQPBJ7QOX",
    "pants_straight_sides_1000/pants_straight_sides_V8XLQMCOLB",
    "pants_straight_sides_1000/pants_straight_sides_YZTWO8D2MJ",
    "pants_straight_sides_1000/pants_straight_sides_2RRU5SSZ7H",
    "pants_straight_sides_1000/pants_straight_sides_AOV8M88TSH",
    "pants_straight_sides_1000/pants_straight_sides_DOATUEG004",
    "pants_straight_sides_1000/pants_straight_sides_6T1RGELBMJ",
    "pants_straight_sides_1000/pants_straight_sides_S0B0A1FOSY",
    "pants_straight_sides_1000/pants_straight_sides_89C8GXP884",
    "pants_straight_sides_1000/pants_straight_sides_TLOPZT2123",
    "pants_straight_sides_1000/pants_straight_sides_QSB2S0IEH4",
    "pants_straight_sides_1000/pants_straight_sides_RBH9HPGEI9",
    "pants_straight_sides_1000/pants_straight_sides_Z6HM9TEVK4",
    "pants_straight_sides_1000/pants_straight_sides_65GCR56DQD",
    "pants_straight_sides_1000/pants_straight_sides_40HDNXVDYS",
    "pants_straight_sides_1000/pants_straight_sides_JT8TF29VML",
    "pants_straight_sides_1000/pants_straight_sides_54PPOCELET",
    "pants_straight_sides_1000/pants_straight_sides_Y5TJIPFQH4",
    "pants_straight_sides_1000/pants_straight_sides_0TCJMYPC5C",
    "pants_straight_sides_1000/pants_straight_sides_LU8FLSO6C6",
    "pants_straight_sides_1000/pants_straight_sides_XAA1JH62B3",
    "pants_straight_sides_1000/pants_straight_sides_BG5UFC848T",
    "pants_straight_sides_1000/pants_straight_sides_RWWORVMOYK",
    "pants_straight_sides_1000/pants_straight_sides_PKJ0M13404",
    "pants_straight_sides_1000/pants_straight_sides_4WK2LJ2F5Z",
    "pants_straight_sides_1000/pants_straight_sides_MRQ6OZFB3O",
    "pants_straight_sides_1000/pants_straight_sides_SBW5RROIUG",
    "pants_straight_sides_1000/pants_straight_sides_HYR526LNTP",
    "pants_straight_sides_1000/pants_straight_sides_93QJ8OIQ1I",
    "pants_straight_sides_1000/pants_straight_sides_KSQ194XSV6",
    "pants_straight_sides_1000/pants_straight_sides_SA5F5X7RQW",
    "pants_straight_sides_1000/pants_straight_sides_T9RQAP43HY",
    "pants_straight_sides_1000/pants_straight_sides_GNAIYZ58ZA",
    "pants_straight_sides_1000/pants_straight_sides_P4UVRCPASI",
    "pants_straight_sides_1000/pants_straight_sides_S6NTAEWX1K",
    "pants_straight_sides_1000/pants_straight_sides_X49B01JLV7",
    "pants_straight_sides_1000/pants_straight_sides_EFLI3L6Q20",
    "pants_straight_sides_1000/pants_straight_sides_X48WDHHCTD",
    "pants_straight_sides_1000/pants_straight_sides_UZK0E27UOT",
    "pants_straight_sides_1000/pants_straight_sides_GLX51A5CB3",
    "pants_straight_sides_1000/pants_straight_sides_73PD8LJDVA",
    "pants_straight_sides_1000/pants_straight_sides_Q749ROU916",
    "pants_straight_sides_1000/pants_straight_sides_A7DKPZ3E61",
    "pants_straight_sides_1000/pants_straight_sides_PGCHFQR0
Download .txt
gitextract_b1_orxx2/

├── .gitignore
├── LICENSE
├── ReadMe.md
├── docs/
│   ├── Installation.md
│   └── Running.md
├── models/
│   ├── att/
│   │   ├── att.yaml
│   │   ├── neural_tailor_panels.pth
│   │   ├── neural_tailor_stitch_model.pth
│   │   └── stitch_model.yaml
│   └── baseline/
│       ├── lstm_stitch_tags.pth
│       └── lstm_stitch_tags.yaml
├── nn/
│   ├── data/
│   │   ├── __init__.py
│   │   ├── datasets.py
│   │   ├── panel_classes.py
│   │   ├── pattern_converter.py
│   │   ├── transforms.py
│   │   ├── utils.py
│   │   └── wrapper.py
│   ├── data_configs/
│   │   ├── data_split_on_filtered_dataset.json
│   │   ├── data_split_on_full_dataset.json
│   │   ├── panel_classes_condenced.json
│   │   ├── panel_classes_plus_one.json
│   │   └── param_filter.json
│   ├── evaluation_scripts/
│   │   ├── maya_att_weights.py
│   │   ├── noise_levels.py
│   │   ├── on_test_set.py
│   │   └── predict_per_example.py
│   ├── example_configs/
│   │   ├── debug.yaml
│   │   ├── debug_server.yaml
│   │   ├── eval_wandb.yaml
│   │   └── stitch_model_debug.yaml
│   ├── experiment.py
│   ├── metrics/
│   │   ├── composed_loss.py
│   │   ├── eval_utils.py
│   │   ├── losses.py
│   │   └── metrics.py
│   ├── net_blocks.py
│   ├── nets.py
│   ├── train.py
│   ├── trainer.py
│   └── utility_scripts/
│       ├── download_dataset.py
│       ├── igl_sampling_test.py
│       ├── param_filter_test.py
│       └── upload_dataset_to_wandb.py
├── requirements.txt
└── system.template.json
Download .txt
SYMBOL INDEX (259 symbols across 19 files)

FILE: nn/data/datasets.py
  class BaseDataset (line 20) | class BaseDataset(Dataset):
    method __init__ (line 26) | def __init__(self, root_dir, start_config={'data_folders': []}, gt_cac...
    method save_to_wandb (line 84) | def save_to_wandb(self, experiment):
    method update_transform (line 90) | def update_transform(self, transform):
    method __len__ (line 95) | def __len__(self):
    method __getitem__ (line 99) | def __getitem__(self, idx):
    method update_config (line 120) | def update_config(self, in_config):
    method _drop_cache (line 134) | def _drop_cache(self):
    method _renew_cache (line 139) | def _renew_cache(self):
    method indices_by_data_folder (line 148) | def indices_by_data_folder(self, index_list):
    method subsets_per_datafolder (line 167) | def subsets_per_datafolder(self, index_list=None):
    method random_split_by_dataset (line 180) | def random_split_by_dataset(self, valid_per_type, test_per_type=0, spl...
    method split_from_dict (line 246) | def split_from_dict(self, split_dict, with_breakdown=False):
    method save_prediction_batch (line 286) | def save_prediction_batch(self, *args, **kwargs):
    method standardize (line 290) | def standardize(self, training=None):
    method _clean_datapoint_list (line 301) | def _clean_datapoint_list(self, datapoints_names, dataset_folder):
    method _get_sample_info (line 306) | def _get_sample_info(self, datapoint_name):
    method _estimate_data_shape (line 328) | def _estimate_data_shape(self):
    method _update_on_config_change (line 336) | def _update_on_config_change(self):
  class GarmentBaseDataset (line 341) | class GarmentBaseDataset(BaseDataset):
    method __init__ (line 344) | def __init__(self, root_dir, start_config={'data_folders': []}, gt_cac...
    method save_to_wandb (line 407) | def save_to_wandb(self, experiment):
    method _clean_datapoint_list (line 433) | def _clean_datapoint_list(self, datapoints_names, dataset_folder):
    method filter_by_params (line 474) | def filter_by_params(self, filter_file, dataset_folder, datapoint_names):
    method template_name (line 502) | def template_name(self, datapoint_name):
    method _read_pattern (line 506) | def _read_pattern(self, datapoint_name, folder_elements,
    method _unpad (line 524) | def _unpad(self, element, tolerance=1.e-5):
    method _get_distribution_stats (line 534) | def _get_distribution_stats(self, input_batch, padded=False):
    method _get_norm_stats (line 549) | def _get_norm_stats(self, input_batch, padded=False):
  class Garment3DPatternFullDataset (line 571) | class Garment3DPatternFullDataset(GarmentBaseDataset):
    method __init__ (line 576) | def __init__(self, root_dir, start_config={'data_folders': []}, gt_cac...
    method standardize (line 596) | def standardize(self, training=None):
    method save_prediction_batch (line 657) | def save_prediction_batch(
    method _pred_to_pattern (line 731) | def _pred_to_pattern(self, prediction, dataname):
    method _get_sample_info (line 770) | def _get_sample_info(self, datapoint_name):
    method _get_pattern_ground_truth (line 803) | def _get_pattern_ground_truth(self, datapoint_name, folder_elements):
    method _sample_points (line 822) | def _sample_points(self, datapoint_name, folder_elements):
    method sample_mesh_points (line 846) | def sample_mesh_points(num_points, verts, faces):
    method _point_classes_from_mesh (line 863) | def _point_classes_from_mesh(self, points, verts, datapoint_name, fold...
    method _empty_panels_mask (line 907) | def _empty_panels_mask(self, num_edges):
    method tags_to_stitches (line 917) | def tags_to_stitches(stitch_tags, free_edges_score):
    method free_edges_mask (line 971) | def free_edges_mask(pattern, stitches, num_stitches):
  class GarmentStitchPairsDataset (line 985) | class GarmentStitchPairsDataset(GarmentBaseDataset):
    method __init__ (line 989) | def __init__(
    method standardize (line 1018) | def standardize(self, training=None):
    method save_prediction_batch (line 1051) | def save_prediction_batch(
    method _get_sample_info (line 1097) | def _get_sample_info(self, datapoint_name):
    method _clean_datapoint_list (line 1134) | def _clean_datapoint_list(self, datapoints_names, dataset_folder):

FILE: nn/data/panel_classes.py
  class PanelClasses (line 8) | class PanelClasses():
    method __init__ (line 10) | def __init__(self, classes_file):
    method __len__ (line 26) | def __len__(self):
    method class_idx (line 29) | def class_idx(self, template, panel):
    method class_name (line 36) | def class_name(self, idx):
    method map (line 39) | def map(self, template_name, panel_list):

FILE: nn/data/pattern_converter.py
  class EmptyPanelError (line 19) | class EmptyPanelError(Exception):
  class InvalidPatternDefError (line 22) | class InvalidPatternDefError(Exception):
    method __init__ (line 27) | def __init__(self, pattern_name='', message=''):
  class NNSewingPattern (line 35) | class NNSewingPattern(VisPattern):
    method __init__ (line 39) | def __init__(self, pattern_file=None, view_ids=False, panel_classifier...
    method pattern_as_tensors (line 48) | def pattern_as_tensors(
    method pattern_from_tensors (line 118) | def pattern_from_tensors(
    method panel_as_numeric (line 189) | def panel_as_numeric(self, panel_name, pad_to_len=None):
    method panel_from_numeric (line 228) | def panel_from_numeric(self, panel_name, edge_sequence, rotation=None,...
    method stitches_as_tags (line 290) | def stitches_as_tags(self, panel_order=None, pad_to_len=None):
    method stitches_as_3D_pairs (line 321) | def stitches_as_3D_pairs(self, stitch_pairs_num=None, non_stitch_pairs...
    method stitches_from_pair_classifier (line 411) | def stitches_from_pair_classifier(self, model, data_stats):
    method all_edge_pairs (line 458) | def all_edge_pairs(self, device='cpu'):
    method _stitches_as_set (line 501) | def _stitches_as_set(self):
    method _edge_dict (line 510) | def _edge_dict(self, vstart, vend, curvature):
    method _3D_edges_per_panel (line 517) | def _3D_edges_per_panel(self, randomize_direction=False):
    method _stitch_entry (line 554) | def _stitch_entry(self, panel_1, edge_1, panel_2, edge_2, score=None):
    method _empty_panel (line 569) | def _empty_panel(self, max_edge_num):
    method panel_order (line 575) | def panel_order(self, force_update=False, pad_to_len=None):

FILE: nn/data/transforms.py
  function _dict_to_tensors (line 6) | def _dict_to_tensors(dict_obj):  # helper
  class SampleToTensor (line 28) | class SampleToTensor(object):
    method __call__ (line 31) | def __call__(self, sample):
  class FeatureStandartization (line 35) | class FeatureStandartization():
    method __init__ (line 37) | def __init__(self, shift, scale):
    method __call__ (line 41) | def __call__(self, sample):
  class GTtandartization (line 52) | class GTtandartization():
    method __init__ (line 57) | def __init__(self, shift, scale):
    method __call__ (line 63) | def __call__(self, sample):

FILE: nn/data/utils.py
  class BalancedBatchSampler (line 16) | class BalancedBatchSampler():
    method __init__ (line 19) | def __init__(self, ids_by_type, batch_size=10, drop_last=True):
    method __iter__ (line 54) | def __iter__(self):
    method __len__ (line 91) | def __len__(self):
  function sample_points_from_meshes (line 96) | def sample_points_from_meshes(mesh_paths, data_config):
  function save_garments_prediction (line 110) | def save_garments_prediction(predictions, save_to, data_config=None, dat...

FILE: nn/data/wrapper.py
  class DatasetWrapper (line 16) | class DatasetWrapper(object):
    method __init__ (line 20) | def __init__(self, in_dataset, known_split=None, batch_size=None, shuf...
    method get_loader (line 55) | def get_loader(self, data_section='full'):
    method new_loaders (line 63) | def new_loaders(self, batch_size=None, shuffle_train=True):
    method _loaders_dict (line 105) | def _loaders_dict(self, subsets_dict, batch_size, shuffle=False):
    method new_split (line 113) | def new_split(self, valid, test=None, random_seed=None):
    method load_split (line 122) | def load_split(self, split_info=None, batch_size=None):
    method print_subset_stats (line 175) | def print_subset_stats(self, subset_breakdown_dict, total_len, subset_...
    method save_to_wandb (line 190) | def save_to_wandb(self, experiment):
    method standardize_data (line 206) | def standardize_data(self):
    method predict (line 211) | def predict(self, model, save_to, dir_tag='pred', sections=['test'], s...

FILE: nn/evaluation_scripts/noise_levels.py
  function get_values_from_args (line 22) | def get_values_from_args():

FILE: nn/evaluation_scripts/on_test_set.py
  function get_values_from_args (line 23) | def get_values_from_args():

FILE: nn/evaluation_scripts/predict_per_example.py
  function get_values_from_args (line 32) | def get_values_from_args():
  function sample_points_obj (line 85) | def sample_points_obj(filename, num_points):
  function load_points (line 103) | def load_points(filename):

FILE: nn/experiment.py
  class ExperimentWrappper (line 17) | class ExperimentWrappper(object):
    method __init__ (line 26) | def __init__(self, config, wandb_username='', no_sync=False):
    method init_run (line 47) | def init_run(self, config={}):
    method stop (line 68) | def stop(self):
    method full_name (line 75) | def full_name(self):
    method last_epoch (line 85) | def last_epoch(self):
    method data_info (line 92) | def data_info(self):
    method last_best_validation_loss (line 126) | def last_best_validation_loss(self):
    method NN_config (line 133) | def NN_config(self):
    method add_statistic (line 138) | def add_statistic(self, tag, info, log=''):
    method add_config (line 163) | def add_config(self, tag, info):
    method add_artifact (line 170) | def add_artifact(self, path, name, type):
    method is_finished (line 195) | def is_finished(self):
    method load_dataset (line 203) | def load_dataset(self, data_root, eval_config={}, unseen=False, batch_...
    method load_model (line 227) | def load_model(self, data_config=None):
    method prediction (line 243) | def prediction(self, save_to, model, datawrapper, dir_tag='pred', nick...
    method checkpoint_filename (line 258) | def checkpoint_filename(self, check_id=None):
    method artifactname (line 263) | def artifactname(self, tag, with_version=True, version=None, custom_al...
    method final_filename (line 273) | def final_filename(self):
    method cloud_path (line 277) | def cloud_path(self):
    method local_wandb_path (line 285) | def local_wandb_path(self):
    method local_artifact_path (line 290) | def local_artifact_path(self):
    method get_checkpoint_file (line 298) | def get_checkpoint_file(self, to_path=None, version=None, device=None):
    method get_best_model (line 311) | def get_best_model(self, to_path=None, device=None):
    method save_checkpoint (line 337) | def save_checkpoint(self, state, aliases=[], wait_for_upload=False):
    method get_file (line 362) | def get_file(self, filename, to_path='.'):
    method _load_artifact (line 369) | def _load_artifact(self, artifact_name, to_path=None):
    method _run_object (line 380) | def _run_object(self):
    method _run_config (line 385) | def _run_config(self):
    method _wait_for_upload (line 393) | def _wait_for_upload(self, artifact_name, max_attempts=10):
    method _load_model_from_file (line 410) | def _load_model_from_file(self, file, device=None):

FILE: nn/metrics/composed_loss.py
  class ComposedLoss (line 11) | class ComposedLoss():
    method __init__ (line 14) | def __init__(self, data_config, in_config={}):
    method __call__ (line 39) | def __call__(self, preds, ground_truth, names=None, epoch=1000):
    method eval (line 69) | def eval(self):
    method train (line 73) | def train(self, mode=True):
    method _main_losses (line 76) | def _main_losses(self, preds, ground_truth, gt_num_edges, epoch):
    method _main_quality_metrics (line 92) | def _main_quality_metrics(self, preds, ground_truth, gt_num_edges, nam...
    method _prec_recall (line 112) | def _prec_recall(self, preds, ground_truth, target_label):
  class ComposedPatternLoss (line 129) | class ComposedPatternLoss():
    method __init__ (line 134) | def __init__(self, data_config, in_config={}):
    method __call__ (line 222) | def __call__(self, preds, ground_truth, names=None, epoch=1000):
    method eval (line 286) | def eval(self):
    method train (line 290) | def train(self, mode=True):
    method _main_losses (line 294) | def _main_losses(self, preds, ground_truth, gt_num_edges):
    method _stitch_losses (line 336) | def _stitch_losses(self, preds, ground_truth, gt_num_edges):
    method _main_quality_metrics (line 365) | def _main_quality_metrics(self, preds, ground_truth, gt_num_edges, nam...
    method _stitch_quality_metrics (line 400) | def _stitch_quality_metrics(self, preds, ground_truth, gt_num_edges, n...
    method _gt_order_match (line 429) | def _gt_order_match(self, preds, ground_truth):
    method _panel_order_match (line 530) | def _panel_order_match(self, pred_features, gt_features):
    method _feature_permute (line 573) | def _feature_permute(pattern_features, permutation):
    method _stitch_after_permute (line 592) | def _stitch_after_permute(stitches, stitches_num, permutation, max_pan...
    method _rotate_gt (line 621) | def _rotate_gt(self, preds, ground_truth, gt_num_edges, epoch):
    method _batch_edge_order_match (line 656) | def _batch_edge_order_match(predicted_panels, gt_panels, gt_num_edges):
    method _panel_egde_match (line 687) | def _panel_egde_match(pred_panel, gt_panel, num_edges):
    method _per_panel_shift (line 706) | def _per_panel_shift(panel_features, per_panel_leading_edges, panel_nu...
    method _gt_stitches_shift (line 727) | def _gt_stitches_shift(
    method _rotate_edges (line 758) | def _rotate_edges(panel, num_edges):

FILE: nn/metrics/eval_utils.py
  function eval_metrics (line 12) | def eval_metrics(model, data_wrapper, section='test'):
  function _eval_metrics_per_loader (line 35) | def _eval_metrics_per_loader(model, loss, loader, device):
  function eval_pad_vector (line 80) | def eval_pad_vector(data_stats={}):

FILE: nn/metrics/losses.py
  class PanelLoopLoss (line 8) | class PanelLoopLoss():
    method __init__ (line 11) | def __init__(self, max_edges_in_panel, data_stats={}):
    method __call__ (line 19) | def __call__(self, predicted_panels, gt_panel_num_edges=None):
  class PatternStitchLoss (line 54) | class PatternStitchLoss():
    method __init__ (line 60) | def __init__(self, triplet_margin=0.1, use_hardnet=True):
    method __call__ (line 65) | def __call__(self, stitch_tags, gt_stitches, gt_stitches_nums):
    method extended_triplet_neg_loss (line 114) | def extended_triplet_neg_loss(self, total_tags, gt_stitches_nums):
    method HardNet_neg_loss (line 150) | def HardNet_neg_loss(self, total_tags, gt_stitches_nums):

FILE: nn/metrics/metrics.py
  class PatternStitchPrecisionRecall (line 13) | class PatternStitchPrecisionRecall():
    method __init__ (line 18) | def __init__(self, data_stats=None):
    method __call__ (line 24) | def __call__(
    method on_loader (line 81) | def on_loader(self, data_loader, model):
  class NumbersInPanelsAccuracies (line 95) | class NumbersInPanelsAccuracies():
    method __init__ (line 99) | def __init__(self, max_edges_in_panel, data_stats=None):
    method __call__ (line 110) | def __call__(self, predicted_outlines, gt_num_edges, gt_panel_nums, pa...
  class PanelVertsL2 (line 185) | class PanelVertsL2():
    method __init__ (line 191) | def __init__(self, max_edges_in_panel, data_stats={}):
    method __call__ (line 203) | def __call__(self, predicted_outlines, gt_outlines, gt_num_edges, corr...
    method _to_verts (line 259) | def _to_verts(self, panel_edges):
  class UniversalL2 (line 284) | class UniversalL2():
    method __init__ (line 288) | def __init__(self, data_stats={}):
    method __call__ (line 296) | def __call__(self, predicted, gt, correct_mask=None):

FILE: nn/net_blocks.py
  class _SetAbstractionModule (line 10) | class _SetAbstractionModule(nn.Module):
    method __init__ (line 12) | def __init__(self, ratio, conv_radius, per_point_nn):
    method forward (line 18) | def forward(self, features, pos, batch):
  class _GlobalSetAbstractionModule (line 28) | class _GlobalSetAbstractionModule(nn.Module):
    method __init__ (line 30) | def __init__(self, per_point_net):
    method forward (line 34) | def forward(self, features, pos, batch):
  function MLP (line 43) | def MLP(channels, batch_norm=True):
  class PointNetPlusPlus (line 50) | class PointNetPlusPlus(nn.Module):
    method __init__ (line 56) | def __init__(self, out_size, config={}):
    method forward (line 71) | def forward(self, positions):
  class EdgeConvFeatures (line 93) | class EdgeConvFeatures(nn.Module):
    method __init__ (line 95) | def __init__(self, out_size, config={}):
    method forward (line 160) | def forward(self, positions, global_pool=True):
  class DynamicASAPool (line 194) | class DynamicASAPool(nn.Module):
    method __init__ (line 201) | def __init__(self, feature_size, k=10, pool_ratio=0.5):
    method forward (line 207) | def forward(self, node_features, batch):
  class EdgeConvPoolingFeatures (line 221) | class EdgeConvPoolingFeatures(nn.Module):
    method __init__ (line 224) | def __init__(self, out_size, config={}):
    method forward (line 246) | def forward(self, positions):
  class MLPDecoder (line 273) | class MLPDecoder(nn.Module):
    method __init__ (line 277) | def __init__(
    method forward (line 289) | def forward(self, batch_enc, *args):
  function _init_tenzor (line 302) | def _init_tenzor(*shape, device='cpu', init_type=''):
  function _init_weights (line 318) | def _init_weights(module, init_type=''):
  class LSTMEncoderModule (line 336) | class LSTMEncoderModule(nn.Module):
    method __init__ (line 338) | def __init__(self, elem_len, encoding_size, n_layers, dropout=0, custo...
    method forward (line 350) | def forward(self, batch_sequence):
  class LSTMDecoderModule (line 363) | class LSTMDecoderModule(nn.Module):
    method __init__ (line 365) | def __init__(self, encoding_size, hidden_size, out_elem_size, n_layers...
    method forward (line 382) | def forward(self, batch_enc, out_len):
  class LSTMDoubleReverseDecoderModule (line 405) | class LSTMDoubleReverseDecoderModule(nn.Module):
    method __init__ (line 408) | def __init__(self, encoding_size, hidden_size, out_elem_size, n_layers...
    method forward (line 429) | def forward(self, batch_enc, out_len):
  class GRUDecoderModule (line 457) | class GRUDecoderModule(nn.Module):
    method __init__ (line 459) | def __init__(self, encoding_size, hidden_size, out_elem_size, n_layers...
    method forward (line 477) | def forward(self, batch_enc, out_len):

FILE: nn/nets.py
  class BaseModule (line 11) | class BaseModule(nn.Module):
    method __init__ (line 13) | def __init__(self):
    method loss (line 21) | def loss(self, preds, ground_truth, **kwargs):
    method train (line 29) | def train(self, mode=True):
    method eval (line 34) | def eval(self):
  class GarmentFullPattern3D (line 41) | class GarmentFullPattern3D(BaseModule):
    method __init__ (line 49) | def __init__(self, data_config, config={}, in_loss_config={}):
    method forward_encode (line 132) | def forward_encode(self, positions_batch):
    method forward_pattern_decode (line 138) | def forward_pattern_decode(self, garment_encodings):
    method forward_panel_decode (line 148) | def forward_panel_decode(self, flat_panel_encodings, batch_size):
    method forward_decode (line 171) | def forward_decode(self, garment_encodings):
    method forward (line 179) | def forward(self, positions_batch, **kwargs):
  class GarmentSegmentPattern3D (line 187) | class GarmentSegmentPattern3D(GarmentFullPattern3D):
    method __init__ (line 192) | def __init__(self, data_config, config={}, in_loss_config={}):
    method forward_panel_enc_from_3d (line 238) | def forward_panel_enc_from_3d(self, positions_batch):
    method forward (line 285) | def forward(self, positions_batch, **kwargs):
  class StitchOnEdge3DPairs (line 303) | class StitchOnEdge3DPairs(BaseModule):
    method __init__ (line 311) | def __init__(self, data_config, config={}, in_loss_config={}):
    method forward (line 343) | def forward(self, pairs_batch, **kwargs):

FILE: nn/train.py
  function get_values_from_args (line 20) | def get_values_from_args():
  function get_old_data_config (line 34) | def get_old_data_config(in_config):
  function merge_repos (line 65) | def merge_repos(root, repos):

FILE: nn/trainer.py
  class Trainer (line 13) | class Trainer():
    method __init__ (line 14) | def __init__(
    method init_randomizer (line 31) | def init_randomizer(self, random_seed=None):
    method use_dataset (line 46) | def use_dataset(self, dataset, split_info):
    method fit (line 57) | def fit(self, model):
    method _fit_loop (line 83) | def _fit_loop(self, model, train_loader, valid_loader, start_epoch=0):
    method _start_experiment (line 140) | def _start_experiment(self, model):
    method _add_optimizer (line 162) | def _add_optimizer(self, model):
    method _add_scheduler (line 174) | def _add_scheduler(self, steps_per_epoch):
    method _restore_run (line 187) | def _restore_run(self, model):
    method _early_stopping (line 215) | def _early_stopping(self, last_loss, last_tracking_loss, last_lr):
    method _log_an_image (line 243) | def _log_an_image(self, model, loader, epoch, log_step):
    method _save_checkpoint (line 275) | def _save_checkpoint(self, model, epoch, best=False):

FILE: nn/utility_scripts/param_filter_test.py
  function isAllowed (line 10) | def isAllowed(pattern, param_filter):
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,232K chars).
[
  {
    "path": ".gitignore",
    "chars": 364,
    "preview": "# Jupyter tmp files \n.ipynb_checkpoints\n\n# Weights and Biases logs -- no need to save\nwandb\nartifacts\n\n# VSCode settings"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2022 Maria Korosteleva\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "ReadMe.md",
    "chars": 2311,
    "preview": "[![PWC](https://img.shields.io/endpoint.svg?url=https://paperswithcode.com/badge/neuraltailor-reconstructing-sewing-patt"
  },
  {
    "path": "docs/Installation.md",
    "chars": 5505,
    "preview": "# Installation & Dependencies\n\nThis file describes the process of setting up the environment from scratch (with the work"
  },
  {
    "path": "docs/Running.md",
    "chars": 8322,
    "preview": "# How to run NeuralTailor\n\nDon't forget to follow the installation steps first: [Installation](Installation.md)\n\n> ☝ **N"
  },
  {
    "path": "models/att/att.yaml",
    "chars": 4888,
    "preview": "# Training configuration for Pattern Shape prediction model \n# (part I of NeuralTailor)\n\nexperiment:\n  project_name: Gar"
  },
  {
    "path": "models/att/stitch_model.yaml",
    "chars": 3707,
    "preview": "# Training configuration for Sitching infomration prediction model \n# (part II of NeuralTailor)\n\nexperiment:\n  project_n"
  },
  {
    "path": "models/baseline/lstm_stitch_tags.yaml",
    "chars": 4896,
    "preview": "# Training configuration for Pattern Shape prediction model \n# (part I of NeuralTailor)\n\nexperiment:\n  project_name: Gar"
  },
  {
    "path": "nn/data/__init__.py",
    "chars": 358,
    "preview": "\n\n\"\"\" Custom datasets & dataset wrapper (split & dataset manager) \"\"\"\n\nfrom data.datasets import Garment3DPatternFullDat"
  },
  {
    "path": "nn/data/datasets.py",
    "chars": 56252,
    "preview": "import json\nimport numpy as np\nimport os\nfrom pathlib import Path\nimport shutil\n\nimport torch\nfrom torch.utils.data impo"
  },
  {
    "path": "nn/data/panel_classes.py",
    "chars": 1616,
    "preview": "\"\"\" Panel classification Interface \"\"\"\n\nfrom collections import OrderedDict\nimport json\nimport numpy as np\n\n\nclass Panel"
  },
  {
    "path": "nn/data/pattern_converter.py",
    "chars": 31515,
    "preview": "import copy\nfrom datetime import datetime\nimport numpy as np\nfrom numpy.random import default_rng\nfrom pathlib import Pa"
  },
  {
    "path": "nn/data/transforms.py",
    "chars": 3144,
    "preview": "import numpy as np\nimport torch\n\n\n# ------------------ Transforms ----------------\ndef _dict_to_tensors(dict_obj):  # he"
  },
  {
    "path": "nn/data/utils.py",
    "chars": 6806,
    "preview": "import copy\nimport numpy as np\nfrom pathlib import Path\nimport random\n\nimport torch\nimport igl\n# import meshplot  # when"
  },
  {
    "path": "nn/data/wrapper.py",
    "chars": 12085,
    "preview": "from argparse import Namespace\nimport json\nimport numpy as np\nimport random\nimport time\nfrom datetime import datetime\n\ni"
  },
  {
    "path": "nn/data_configs/data_split_on_filtered_dataset.json",
    "chars": 658718,
    "preview": "{\n  \"test\": [\n    \"dress_sleeveless_2550/dress_sleeveless_6VN3H6FMOY\",\n    \"dress_sleeveless_2550/dress_sleeveless_94LPP"
  },
  {
    "path": "nn/data_configs/data_split_on_full_dataset.json",
    "chars": 1126592,
    "preview": "{\n  \"test\": [\n    \"dress_sleeveless_2550/dress_sleeveless_5ERKKPNZKV\",\n    \"dress_sleeveless_2550/dress_sleeveless_A2I7C"
  },
  {
    "path": "nn/data_configs/panel_classes_condenced.json",
    "chars": 4831,
    "preview": "{\n    \"top_front\": [\n        [\"tee\", \"front\"],\n        [\"tee_sleeveless\", \"front\"],\n        [\"tee_hood\", \"front\"],\n     "
  },
  {
    "path": "nn/data_configs/panel_classes_plus_one.json",
    "chars": 4862,
    "preview": "{\n    \"top_front\": [\n        [\"tee\", \"front\"],\n        [\"tee_sleeveless\", \"front\"],\n        [\"tee_hood\", \"front\"],\n     "
  },
  {
    "path": "nn/data_configs/param_filter.json",
    "chars": 1452,
    "preview": "{\n    \"dress_sleeveless\": {\n        \"waist_wideness\": [0.9, 1.2],\n        \"skirt_length\": [0.9, 1.9]\n    },\n    \"jacket\""
  },
  {
    "path": "nn/evaluation_scripts/maya_att_weights.py",
    "chars": 2407,
    "preview": "\"\"\"\n    Visualize requested point cloud with attention weights per point \n    * To be run within Maya environment (just "
  },
  {
    "path": "nn/evaluation_scripts/noise_levels.py",
    "chars": 2728,
    "preview": "\"\"\"Evaluate a model on the data\"\"\"\n\nfrom pathlib import Path\nimport argparse\nimport json\nfrom datetime import datetime\ni"
  },
  {
    "path": "nn/evaluation_scripts/on_test_set.py",
    "chars": 6521,
    "preview": "\"\"\"Evaluate Panel Shape prediction model or full NeuralTailor framework on the seen garment types\n\n    Outputs the resul"
  },
  {
    "path": "nn/evaluation_scripts/predict_per_example.py",
    "chars": 8312,
    "preview": "\"\"\"Predicting a 2D pattern for the given 3D point clouds of garments -- \n    not necessarily from the garment dataset of"
  },
  {
    "path": "nn/example_configs/debug.yaml",
    "chars": 4618,
    "preview": "# Training configuration for Pattern Shape prediction model \n# (part I of NeuralTailor)\n\nexperiment:\n  project_name: Tes"
  },
  {
    "path": "nn/example_configs/debug_server.yaml",
    "chars": 4657,
    "preview": "# Training configuration for Pattern Shape prediction model \n# (part I of NeuralTailor)\n\nexperiment:\n  project_name: Tes"
  },
  {
    "path": "nn/example_configs/eval_wandb.yaml",
    "chars": 440,
    "preview": "# Example config file for evaluating experiment saved to Weigths&Biases cloud\n\nexperiment:\n  project_name: Garments-Reco"
  },
  {
    "path": "nn/example_configs/stitch_model_debug.yaml",
    "chars": 2784,
    "preview": "# Training configuration for Sitching infomration prediction model \n# (part II of NeuralTailor)\n\nexperiment:\n  project_n"
  },
  {
    "path": "nn/experiment.py",
    "chars": 18551,
    "preview": "import os\nfrom pathlib import Path\nimport requests\nimport time\nimport json\n\nimport torch\nfrom torch import nn\nimport wan"
  },
  {
    "path": "nn/metrics/composed_loss.py",
    "chars": 36427,
    "preview": "import torch\nimport torch.nn as nn\n\nfrom entmax import SparsemaxLoss  # https://github.com/deep-spin/entmax\n\n# My module"
  },
  {
    "path": "nn/metrics/eval_utils.py",
    "chars": 3193,
    "preview": "\"\"\"\n    List of metrics to evalute on a model and a dataset, along with pre-processing methods needed for such evaluatio"
  },
  {
    "path": "nn/metrics/losses.py",
    "chars": 9246,
    "preview": "import torch\n\n# My modules\nfrom metrics.eval_utils import eval_pad_vector\n\n\n# ------- custom losses --------\nclass Panel"
  },
  {
    "path": "nn/metrics/metrics.py",
    "chars": 15788,
    "preview": "\"\"\"\n    List of metrics to evalute on a model and a dataset, along with pre-processing methods needed for such evaluatio"
  },
  {
    "path": "nn/net_blocks.py",
    "chars": 22068,
    "preview": "\"\"\"Basic building blocks for custom neural network architectures\"\"\"\nimport torch\nimport torch.nn as nn\nimport torch_geom"
  },
  {
    "path": "nn/nets.py",
    "chars": 16077,
    "preview": "import torch\nimport torch.nn as nn\nfrom sparsemax import Sparsemax\n\n# my modules\nfrom metrics.composed_loss import Compo"
  },
  {
    "path": "nn/train.py",
    "chars": 6416,
    "preview": "from distutils import dir_util\nfrom pathlib import Path\nimport argparse\nimport numpy as np\nimport torch.nn as nn\nimport "
  },
  {
    "path": "nn/trainer.py",
    "chars": 13975,
    "preview": "# Training loop func\nfrom pathlib import Path\nimport time\nimport traceback\n\nimport torch\nimport wandb as wb\n\n# My module"
  },
  {
    "path": "nn/utility_scripts/download_dataset.py",
    "chars": 986,
    "preview": "from pathlib import Path\nimport wandb\nimport customconfig\n\nsystem_props = customconfig.Properties('./system.json')\n\n# na"
  },
  {
    "path": "nn/utility_scripts/igl_sampling_test.py",
    "chars": 1099,
    "preview": "import igl\nimport numpy as np\nimport random\nfrom pathlib import Path\n\nimport customconfig\n\nsystem_props = customconfig.P"
  },
  {
    "path": "nn/utility_scripts/param_filter_test.py",
    "chars": 1939,
    "preview": "\"\"\"In simulated dataset, filter only samples which parameter passes the filter \"\"\"\n\nimport os\n\nimport customconfig\nfrom "
  },
  {
    "path": "nn/utility_scripts/upload_dataset_to_wandb.py",
    "chars": 822,
    "preview": "from pathlib import Path\nimport customconfig\nimport wandb\n\nsystem_props = customconfig.Properties('./system.json')\n\nto_u"
  },
  {
    "path": "requirements.txt",
    "chars": 132,
    "preview": "matplotlib~=3.6.2\nsparsemax~=0.1.9\nrequests~=2.28.1\nwandb~=0.13.5\nsvglib~=1.4.1\nsvgwrite~=1.4.3\nentmax==1.0\nnumba==0.56."
  },
  {
    "path": "system.template.json",
    "chars": 65,
    "preview": "{\n  \"output\": \"\",\n  \"datasets_path\": \"\",\n  \"wandb_username\": \"\"\n}"
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the maria-korosteleva/Garment-Pattern-Estimation GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (55.5 MB), approximately 531.8k tokens, and a symbol index with 259 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!