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
================================================
[](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

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
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
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": "[\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.