diff --git a/Dockerfile b/Dockerfile index 72e148d5..4ee3ae2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,62 @@ -# tagged aloception-oss:cuda-11.3.1-pytorch1.10.1-lightning1.4.1 - FROM nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04 -#FROM nvidia/cuda:11.6.0-cudnn8-devel-ubuntu20.04 +#ARG py=3.9 +#ARG pytorch=2.1.0.dev20230313+cu117 +#ARG torchvision=0.15.0.dev20230313+cu117 +#ARG torchaudio=2.0.0.dev20230313+cu117 +#ARG pytorch_lightning=1.9.3 +#ARG pycyda=11.7 ARG py=3.9 ARG pytorch=1.13.1 ARG torchvision=0.14.1 ARG torchaudio=0.13.1 -ARG pytorch_lightning=1.9.0 +ARG pytorch_lightning=1.9.3 ARG pycyda=11.7 + +ARG HOME=/home/aloception + ENV TZ=Europe/Paris ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update +RUN apt-get -y update; apt-get -y install sudo + RUN apt-get install -y build-essential nano git wget libgl1-mesa-glx -# Usefull for scipy -RUN apt-get install -y gfortran -# required for aloscene -RUN apt-get install -y libglib2.0-0 +# Usefull for scipy / required for aloscene +RUN apt-get install -y gfortran libglib2.0-0 + +# Create aloception user +RUN useradd --create-home --uid 1000 --shell /bin/bash aloception && usermod -aG sudo aloception && echo "aloception ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers +ENV HOME /home/aloception +WORKDIR /home/aloception -RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -RUN bash Miniconda3-latest-Linux-x86_64.sh -b -p /miniconda -ENV PATH=$PATH:/miniconda/condabin:/miniconda/bin + +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && \ + /bin/bash /tmp/miniconda.sh -b -p /opt/miniconda && \ + rm /tmp/miniconda.sh +ENV CONDA_HOME /opt/miniconda +ENV PATH ${CONDA_HOME}/condabin:${CONDA_HOME}/bin:${PATH} RUN /bin/bash -c "source activate base" -ENV HOME /workspace -WORKDIR /workspace + +# The following so that any user can install packages inside this Image +RUN chmod -R o+w /opt/miniconda && chmod -R o+w /home/aloception + +USER aloception # Pytorch & pytorch litning +#RUN conda install py pytorch-cuda=${pycuda} -c pytorch -c nvidia +#RUN pip install --pre torch==${pytorch} torchvision==${torchvision} torchaudio==${torchaudio} --index-url https://download.pytorch.org/whl/nightly/cu117 +#RUN pip install pytorch_lightning==${pytorch_lightning} RUN conda install pytorch==${pytorch} torchvision==${torchvision} torchaudio==${torchaudio} pytorch-cuda=${pycuda} -c pytorch -c nvidia RUN pip install pytorch_lightning==${pytorch_lightning} -COPY requirements-torch1.13.1.txt /install/requirements-torch1.13.1.txt -RUN pip install -r /install/requirements-torch1.13.1.txt + +COPY --chown=aloception:aloception requirements/requirements-torch1.13.1.txt /home/aloception/install/requirements-torch1.13.1.txt +RUN pip install -r /home/aloception/install/requirements-torch1.13.1.txt +COPY --chown=aloception:aloception ./aloscene/utils /home/aloception/install/utils + +USER root +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 6e295777..1ccc158d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Documentation -[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-0.5.1-green.svg)](https://conventionalcommits.org) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-0.6.0-green.svg)](https://conventionalcommits.org) # Aloception open source software @@ -78,17 +78,17 @@ training pipelines with **augmented tensors**. ### Docker install ``` -docker build -t aloception-oss:cuda-11.3.1-pytorch1.13.1-lightning1.9.0 . +docker build -t aloception-oss:cuda-11.3-pytorch1.13.1-lightning1.9.3 . ``` ``` -docker run --gpus all -it -v /YOUR/WORKSPACE/:/workspace --privileged -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix aloception-oss:cuda-11.3.1-pytorch1.13.1-lightning1.9.0 +docker run -e LOCAL_USER_ID=$(id -u) --gpus all -it -v /YOUR/WORKSPACE/:/workspace --privileged -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix aloception-oss:cuda-11.3-pytorch1.13.1-lightning1.9.3 ``` Or without building the image ``` -docker run --gpus all -it -v /YOUR/WORKSPACE/:/workspace --privileged -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix visualbehaviorofficial/aloception-oss:cuda-11.3.1-pytorch1.13.1-lightning1.9.0 +docker run -e LOCAL_USER_ID=$(id -u) --gpus all -it -v /YOUR/WORKSPACE/:/workspace --privileged -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix visualbehaviorofficial/aloception-oss:cuda-11.3-pytorch1.13.1-lightning1.9.3 ``` diff --git a/alodataset/__init__.py b/alodataset/__init__.py index f3d81dc4..697de3dc 100644 --- a/alodataset/__init__.py +++ b/alodataset/__init__.py @@ -1,3 +1,5 @@ +from . import utils +from . import prepare from .base_dataset import BaseDataset, Split from .sequence_mixin import SequenceMixin from .split_mixin import SplitMixin @@ -17,3 +19,6 @@ from .from_directory_dataset import FromDirectoryDataset from .woodScape_dataset import WooodScapeDataset from .woodScape_split_dataset import WoodScapeSplitDataset + +import pkg_resources +__version__ = pkg_resources.get_distribution("aloception").version diff --git a/alodataset/io/__init__.py b/alodataset/io/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alodataset/prepare/__init__.py b/alodataset/prepare/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alodataset/utils/__init__.py b/alodataset/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alonet/__init__.py b/alonet/__init__.py index d0275b60..13be1cc5 100644 --- a/alonet/__init__.py +++ b/alonet/__init__.py @@ -10,3 +10,6 @@ from . import detr_panoptic from . import deformable_detr_panoptic + +import pkg_resources +__version__ = pkg_resources.get_distribution("aloception").version diff --git a/alonet/common/base_datamodule.py b/alonet/common/base_datamodule.py new file mode 100644 index 00000000..60780276 --- /dev/null +++ b/alonet/common/base_datamodule.py @@ -0,0 +1,177 @@ +import pytorch_lightning as pl +import alonet +import torch +from torch.utils.data.sampler import RandomSampler, SequentialSampler + + +class BaseDataModule(pl.LightningDataModule): + """ + Base class for all data modules. + """ + + def __init__( + self, args, **kwargs, + ): + super().__init__() + alonet.common.pl_helpers.params_update(self, args, kwargs) + + @staticmethod + def add_argparse_args(parent_parser): + parser = parent_parser.add_argument_group("BaseDataModule") + parser.add_argument("--batch_size", type=int, default=5, help="Batch size (Default: %(default)s)") + parser.add_argument( + "--num_workers", type=int, default=8, help="num_workers to use on the dataset (Default: %(default)s)" + ) + parser.add_argument("--sequential_sampler", action="store_true", help="sample data sequentially (no shuffle)") + parser.add_argument( + "--sample", action="store_true", help="Download a sample for train/val process (Default: %(default)s)" + ) + parser.add_argument("--train_on_val", action="store_true", help="Train on validation set (Default: %(default)s)") + + parser.add_argument("--no_aug", action="store_true", help="Disable data augmentation (Default: %(default)s)") + return parent_parser + + @property + def train_dataset(self): + if not hasattr(self, "_train_dataset"): + self.setup() + return self._train_dataset + + @train_dataset.setter + def train_dataset(self, new_dataset): + self._train_dataset = new_dataset + + @property + def val_dataset(self): + if not hasattr(self, "_val_dataset"): + self.setup() + return self._val_dataset + + @val_dataset.setter + def val_dataset(self, new_dataset): + self._val_dataset = new_dataset + + @property + def test_dataset(self): + if not hasattr(self, "_test_dataset"): + self.setup() + return self._test_dataset + + @test_dataset.setter + def test_dataset(self, new_dataset): + self._test_dataset = new_dataset + + def train_transform(self, frames, **kwargs): + """ + A structure to select the train transform function. + Parameters + ---------- + frames : aloscene.Frame + Input frames + Returns + ------- + aloscene.Frame + """ + if self.no_aug: + return self._train_transform_no_aug(frames) + else: + return self._train_transform_aug(frames, **kwargs) + + def _train_transform_no_aug(self, frames): + """ + Train_transform with no data augmentation. + Parameters + ---------- + frames : aloscene.Frame + Input frames + Returns + ------- + aloscene.Frame + """ + + raise NotImplementedError("Should be implemented in child class.") + + def _train_transform_aug(self, frames): + """ + Train_transform with data augmentation. + Parameters + ---------- + frames : aloscene.Frame + Input frames + Returns + ------- + aloscene.Frame + """ + + raise NotImplementedError("Should be implemented in child class.") + + def val_transform(self, frames, **kwargs): + """ + Val transform. + Parameters + ---------- + frames : aloscene.Frame + Input frames + Returns + ------- + aloscene.Frame + """ + + raise NotImplementedError("Should be implemented in child class.") + + def setup(self, stage=None): + """:attr:`train_dataset`, :attr:`val_dataset`, attr:`test_dataset` datasets setup + Parameters + ---------- + stage : str, optional + Stage either `fit`, `validate`, `test` or `predict`, by default None""" + + raise NotImplementedError("Should be implemented in child class.") + + def train_dataloader(self, sampler: torch.utils.data = None): + """Get train dataloader + Parameters + ---------- + sampler : torch.utils.data, optional + Sampler to load batches, by default None + Returns + ------- + torch.utils.data.DataLoader + Dataloader for training process + """ + if sampler is None: + sampler = RandomSampler if not self.sequential_sampler else SequentialSampler + + return self.train_dataset.train_loader(batch_size=self.batch_size, num_workers=self.num_workers, sampler=sampler) + + def val_dataloader(self, sampler: torch.utils.data = None): + """Get val dataloader + Parameters + ---------- + sampler : torch.utils.data, optional + Sampler to load batches, by default None + Returns + ------- + torch.utils.data.DataLoader + Dataloader for validation process + """ + if sampler is None: + sampler = SequentialSampler + + return self.val_dataset.train_loader(batch_size=self.batch_size, num_workers=self.num_workers, sampler=sampler) + + def test_dataloader(self, sampler: torch.utils.data = None): + """Get test dataloader + Parameters + ---------- + sampler : torch.utils.data, optional + Sampler to load batches, by default None + Returns + ------- + torch.utils.data.DataLoader + Dataloader for inference process + """ + if sampler is None: + sampler = SequentialSampler + + return self.test_dataset.train_loader(batch_size=self.batch_size, num_workers=self.num_workers, sampler=sampler) diff --git a/alonet/common/base_lightningmodule.py b/alonet/common/base_lightningmodule.py new file mode 100644 index 00000000..756a5070 --- /dev/null +++ b/alonet/common/base_lightningmodule.py @@ -0,0 +1,261 @@ +import pytorch_lightning as pl +from argparse import Namespace, ArgumentParser +import torch +import alonet +from typing import Union +import aloscene + + +class BaseLightningModule(pl.LightningModule): + """ + Parameters + ---------- + args : Namespace, optional + Attributes stored in specific Namespace, by default None + weights : str, optional + Weights name to load, by default None + gradient_clip_val : float, optional + pytorch_lightning.trainer.trainer parameter. 0 means don’t clip, by default 0.1 + accumulate_grad_batches : int, optional + Accumulates grads every k batches or as set up in the dict, by default 4 + model_name : str, optional + Name use to define the model, by default "detr-r50" + model : torch.nn, optional + Custom model to train + Notes + ----- + Arguments entered by the user (kwargs) will replace those stored in args attribute + """ + + def __init__(self, args: Namespace = None, model: torch.nn = None, **kwargs): + super().__init__() + # Update class attributes with args and kwargs inputs + alonet.common.pl_helpers.params_update(self, args, kwargs) + self._init_kwargs_config.update({"model": model}) + + self.model = self.build_model(weights=self.weights) + self.criterion = self.build_criterion() + + @staticmethod + def add_argparse_args(parent_parser, parser=None): + """Add arguments to parent parser with default values + Parameters + ---------- + parent_parser : ArgumentParser + Object to append new arguments + parser : ArgumentParser.argument_group, optional + Argument group to append the parameters, by default None + Returns + ------- + ArgumentParser + Object with new arguments concatenated + """ + parser = parent_parser.add_argument_group("BaseLightningModule") if parser is None else parser + parser.add_argument("--weights", type=str, default=None, help="One of (detr-r50). (Default: %(default)s)") + + return parent_parser + + def configure_optimizers(self): + """AdamW default optimizer configuration, using different learning rates for backbone and others parameters + Returns + ------- + torch.optim + `AdamW `_ optimizer to update weights + """ + param_dicts = [ + {"params": [p for n, p in self.model.named_parameters() if p.requires_grad]}, + ] + optimizer = torch.optim.AdamW(param_dicts, lr=1e-4, weight_decay=1e-4) + return optimizer + + def build_model(self, weights): + raise NotImplementedError("Should be implemented in child class.") + + def build_criterion(self): + raise NotImplementedError("Should be implemented in child class.") + + def forward(self, frames: Union[list, aloscene.Frame], **kwargs): + """Run a forward pass through the model. + Parameters + ---------- + frames : Union[list, :mod:`Frames `] + List of :mod:`~aloscene.frame` without batch dimension or a Frame with the batch dimension + Returns + ------- + m_outputs: dict + A dict with the forward outputs. + """ + # Batch list of frame if needed + if isinstance(frames, list): + frames = aloscene.Frame.batch_list(frames) + + # Assert inputs content + self.assert_input(frames, inference=True) + + m_outputs = self.model(frames) + + return m_outputs + + def inference(self, m_outputs: dict, frames: aloscene.Frame, **kwargs): + """Given the model forward outputs, run inference. + Parameters + ---------- + m_outputs: dict + Dict with the model forward outptus + """ + raise NotImplementedError("Should be implemented in child class.") + + def training_step(self, frames: Union[list, aloscene.Frame], batch_idx: int): + """Train the model for one step + Parameters + ---------- + frames : Union[list, :mod:`Frames `] + List of :mod:`~aloscene.frame` without batch dimension or a Frame with the batch dimension + batch_idx : int + Batch id given by Lightning + Returns + ------- + dict + Dictionary with the :attr:`loss` to optimize, :attr:`m_outputs` forward outputs and :attr:`metrics` to log. + """ + if isinstance(frames, list): + frames = aloscene.Frame.batch_list(frames) + + # Assert inputs content + self.assert_input(frames) + + m_outputs = self.model(frames) + + loss, metrics = self.criterion(frames, m_outputs) + + outputs = {"loss": loss} + outputs.update({"m_outputs": m_outputs}) + outputs.update({"metrics": metrics}) + + return outputs + + @torch.no_grad() + def validation_step(self, frames: Union[list, aloscene.Frame], batch_idx: int): + """Run one step of validation + Parameters + ---------- + frames : Union[list, :mod:`Frames `] + List of :mod:`~aloscene.frame` without batch dimension or a Frame with the batch dimension + batch_idx : int + Batch id given by Lightning + Returns + ------- + dict + Dictionary with the :attr:`loss` to optimize, and :attr:`metrics` to log. + """ + # Batch list of frame if needed + with torch.no_grad(): + if isinstance(frames, list): + frames = aloscene.Frame.batch_list(frames) + + # Assert inputs content + self.assert_input(frames) + + m_outputs = self.model(frames) + + loss, metrics = self.criterion(frames, m_outputs) + + outputs = {"loss": loss} + outputs.update({"metrics": metrics}) + + return outputs + + def assert_input(self, frames: aloscene.Frame, inference=False): + """Check if input-frames have the correct format + Parameters + ---------- + frames : :mod:`Frames ` + Input frames + inference : bool, optional + Check input from inference procedure, by default False + """ + raise NotImplementedError("Should be implemented in child class.") + + def callbacks(self,): + """Given a data loader, this method will return the default callbacks of the training loop. + Returns + ------- + List[:doc:`alonet.callbacks`] + Callbacks use in train process + """ + + metrics_callback = alonet.callbacks.MetricsCallback() + ap_metrics_callback = alonet.callbacks.ApMetricsCallback() + return [metrics_callback, ap_metrics_callback] + + def run_train( + self, + data_loader: torch.utils.data.DataLoader, + args: Namespace = None, + project: str = None, + expe_name: str = None, + callbacks: list = None, + ): + """Train the model using pytorch lightning + Parameters + ---------- + data_loader : torch.utils.data.DataLoader + Dataloader use in :func:`callbacks` function + project : str, optional + Project name using to save checkpoints, by default None + expe_name : str, optional + Specific experiment name to save checkpoints, by default None + callbacks : list, optional + List of callbacks to use, by default :func:`callbacks` output + args : Namespace, optional + Additional arguments use in training process, by default None + """ + # Set the default callbacks if not provide. + callbacks = callbacks if callbacks is not None else self.callbacks(data_loader) + + alonet.common.pl_helpers.run_pl_training( + # Trainer, data & callbacks + lit_model=self, + data_loader=data_loader, + callbacks=callbacks, + # Project info + args=args, + project=project, + expe_name=expe_name, + ) + + def run_validation( + self, + data_loader: torch.utils.data.DataLoader, + args: Namespace = None, + project: str = None, + expe_name: str = None, + callbacks: list = None, + ): + """ + Validate the model using pytorch lightning + Parameters + ---------- + data_loader : torch.utils.data.DataLoader + Dataloader use in :func:`callbacks` function + project : str, optional + Project name using to save checkpoints, by default None + expe_name : str, optional + Specific experiment name to save checkpoints, by default None + callbacks : list, optional + List of callbacks to use, by default :func:`callbacks` output + args : Namespace, optional + Additional arguments use in training process, by default None + """ + # Set the default callbacks if not provide. + callbacks = callbacks if callbacks is not None else self.callbacks(data_loader) + + alonet.common.pl_helpers.run_pl_validate( + # Trainer, data & callbacks + lit_model=self, + data_loader=data_loader, + callbacks=callbacks, + # Project info + args=args, + project=project, + ) diff --git a/alonet/common/pl_helpers.py b/alonet/common/pl_helpers.py index 2a48888d..75533f26 100644 --- a/alonet/common/pl_helpers.py +++ b/alonet/common/pl_helpers.py @@ -4,9 +4,12 @@ from pytorch_lightning.loggers import TensorBoardLogger, WandbLogger import datetime import os +import json import re import torch +from alodataset.base_dataset import _user_prompt + parser = ArgumentParser() @@ -15,6 +18,7 @@ def vb_folder(create_if_not_found=False): alofolder = os.path.join(home, ".aloception") if not os.path.exists(alofolder): if create_if_not_found: + print(f"{alofolder} cannot be found so will be created.") os.mkdir(alofolder) else: raise Exception( @@ -92,6 +96,20 @@ def add_argparse_args(parent_parser, add_pl_args=True, mode="training"): const="wandb", help="Log results, can specify logger, by default %(default)s. If set but value not provided:wandb", ) + parser.add_argument( + "--log_save_dir", + type=str, + default=None, + nargs="?", + help="Path to save training log, by default %(default)s. If not set, use the default value in alonet_config.json", + ) + parser.add_argument( + "--cp_save_dir", + type=str, + default=None, + nargs="?", + help="Path to save training checkpoint, by default %(default)s. If not set, use the default value in alonet_config.json", + ) parser.add_argument("--cpu", action="store_true", help="Use the CPU instead of scaling on the vaiable GPUs") parser.add_argument("--run_id", type=str, help="Load the weights from this saved experiment") parser.add_argument( @@ -201,6 +219,66 @@ def load_training( return lit_model +def set_save_dir_config(key): + """ + Create /home/USER/.aloception/alonet_config.json with path to log/weights save dir. + """ + streaming_config = os.path.join(vb_folder(create_if_not_found=True), "alonet_config.json") + if not os.path.exists(streaming_config): + with open(streaming_config, "w") as f: # Json init as empty config + json.dump(dict(), f, indent=4) + with open(streaming_config) as f: + content = json.loads(f.read()) + key = f"{key}_save_dir" + default_dir = vb_folder() + default_dir_message = f"Do you want to use the default dir {default_dir} ? (Y)es or Please write a new directory for {key}: " + if key not in content: + save_dir = _user_prompt( + f"{key} is not set in config file. " + default_dir_message + ) + if save_dir.lower() in ["y", "yes"]: + save_dir = default_dir + content[key] = save_dir + with open(streaming_config, "w") as f: # Save new directory + json.dump(content, f, indent=4) + return content[key] + + +def get_dir_from_config(args, key): + """ + Get the directory to save log/checkpoint from the config in + /home/USER/.aloception/alonet_config.json or from args if the dir is set. + This file will be created if not exist. + + Parameters + ---------- + args: Namespace + key: str + log or checkpoint + + Return + ------ + The directory to save log/checkpoint + """ + assert key in ["log", "checkpoint"], "Value of key must be log or checkpoint" + save_dir = args.log_save_dir if key == "log" else args.cp_save_dir + + # get dir from config.json file if save_dir is not Set. Create the file if not exist + if save_dir is None: + streaming_config = os.path.join(vb_folder(create_if_not_found=True), "alonet_config.json") + if not os.path.exists(streaming_config): + save_dir = set_save_dir_config(key) + with open(streaming_config) as f: + content = json.loads(f.read()) + key_dir = f"{key}_save_dir" + if key_dir not in content: + return set_save_dir_config(key) + else: + return content[key_dir] + else: + return save_dir + + def get_expe_infos(project, expe_name, args=None): """ Get the directories for the project and the experimentation @@ -209,7 +287,7 @@ def get_expe_infos(project, expe_name, args=None): expe_name = expe_name or args.expe_name if args is not None and not args.no_suffix: expe_name = "{}_{:%B-%d-%Y-%Hh-%M}".format(expe_name, datetime.datetime.now()) - project_dir = os.path.join(vb_folder(), f"project_{project}") + project_dir = os.path.join(get_dir_from_config(args, "checkpoint"), f"project_{project}") expe_dir = os.path.join(project_dir, expe_name) return project_dir, expe_dir, expe_name @@ -228,6 +306,8 @@ def run_pl_training( args.logger = "wandb" args.save = False args.cpu = False + args.log_save_dir = None + args.cp_save_dir = None # Set the experiment name ID project_dir, expe_dir, expe_name = get_expe_infos(project, expe_name, args) @@ -251,12 +331,19 @@ def run_pl_training( expe_dir = os.path.join(run_id_project_dir, args.run_id) if args.log is not None: + # get dir to save log + save_dir = os.path.join(get_dir_from_config(args, "log"), f"project_{project}", expe_name) + + # create path if args.log == "wandb": - logger = WandbLogger(name=expe_name, project=project, id=expe_name) + save_dir = os.path.join(save_dir, "wandb") + os.makedirs(save_dir, exist_ok=True) + logger = WandbLogger(name=expe_name, save_dir=save_dir, project=project, id=expe_name) logger.log_hyperparams(args) - elif args.log == "tensorboard": - logger = TensorBoardLogger(save_dir="tensorboard/", name=expe_name, sub_dir=expe_name) + save_dir = os.path.join(save_dir, "tensorboard") + os.makedirs(save_dir, exist_ok=True) + logger = TensorBoardLogger(save_dir=save_dir, name=expe_name, sub_dir=expe_name) else: raise ValueError("Unknown or not implemented logger") else: diff --git a/alonet/deformable_detr/ops/__init__.py b/alonet/deformable_detr/ops/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alonet/deformable_detr/ops/src/__init__.py b/alonet/deformable_detr/ops/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alonet/metrics/compute_pq.py b/alonet/metrics/compute_pq.py index 57379e80..db6ae104 100644 --- a/alonet/metrics/compute_pq.py +++ b/alonet/metrics/compute_pq.py @@ -29,11 +29,12 @@ def __iadd__(self, pq_stat_cat): class PQMetrics(object): - def __init__(self): + def __init__(self, iou_threshold=0.5): self.pq_per_cat = defaultdict(PQStatCat) self.class_names = None self.isfull = False self.categories = dict() + self.iou_threshold = iou_threshold def __getitem__(self, label_id: int): return self.pq_per_cat[label_id] @@ -75,7 +76,7 @@ def update_data_objects(self, cat_labels: aloscene.Labels, isthing_labels: alosc ) self.isfull = False - def pq_average(self, isthing: bool = None, print_result: bool = False, **kwargs): + def pq_average(self, isthing: bool = None, print_result: bool = False, recall_precision: bool = False, **kwargs): """Calculate SQ, RQ and PQ metrics from the categories, and thing/stuff/all if desired Parameters @@ -85,6 +86,8 @@ def pq_average(self, isthing: bool = None, print_result: bool = False, **kwargs) By default the metric is executed over both print_result : bool Print result in console, by default False + recall_precision : bool + Include precision and reacall. Returns ------- @@ -92,6 +95,8 @@ def pq_average(self, isthing: bool = None, print_result: bool = False, **kwargs) Dictionaries with general PQ average metrics and for each class, respectively """ pq, sq, rq, n = 0, 0, 0, 0 + t_fp, t_fn, t_tp = 0, 0, 0 + per_class_results = {} for label, label_info in self.categories.items(): if isthing is not None: @@ -110,11 +115,23 @@ def pq_average(self, isthing: bool = None, print_result: bool = False, **kwargs) sq_class = iou / tp if tp != 0 else 0 rq_class = tp / (tp + 0.5 * fp + 0.5 * fn) per_class_results[label_info["category"]] = {"pq": pq_class, "sq": sq_class, "rq": rq_class} + pq += pq_class sq += sq_class rq += rq_class + t_fp += fp + t_tp += tp + t_fn += fn + + if n == 0: + n = 1 + result = {"pq": pq / n, "sq": sq / n, "rq": rq / n, "n": n} + if recall_precision: + recall = t_tp / (t_tp + t_fn) if t_tp + t_fn > 0 else 0 + precision = t_tp / (t_tp + t_fp) if t_tp + t_fn > 0 else 0 + result.update({"precision": precision, "recall": recall}) if print_result: suffix = "" if isthing is not None and isthing: @@ -222,7 +239,7 @@ def add_sample( - gt_pred_map.get((VOID, pred_label), 0) ) iou = intersection / union - if iou > 0.5: # Add matches from this IoU (take from original paper) + if iou > self.iou_threshold: # Add matches from this IoU (take from original paper) self.pq_per_cat[gt_segms[gt_label]["cat_id"]].tp += 1 self.pq_per_cat[gt_segms[gt_label]["cat_id"]].iou += iou gt_matched.add(gt_label) @@ -245,13 +262,15 @@ def add_sample( continue self.pq_per_cat[pred_info["cat_id"]].fp += 1 - def calc_map(self, print_result: bool = False): + def calc_map(self, print_result: bool = False, recall_precision: bool = False): """Calcule PQ-RQ-SQ maps Parameters ---------- print_result : bool, optional Print results, by default False + recall_precision : bool. + Include precision and recall in returned results Returns ------- @@ -267,13 +286,15 @@ def calc_map(self, print_result: bool = False): keys, cats = ["all"], [None] for key, cat in zip(keys, cats): if cat is not None or not self.isfull: - all_maps[key], all_maps_per_class[key] = self.pq_average(cat, - print_result, - clm_size=9, - head_elm=["PQ", "SQ", "RQ"], - ) + all_maps[key], all_maps_per_class[key] = self.pq_average( + cat, + print_result, + clm_size=9, + head_elm=["PQ", "SQ", "RQ"], + recall_precision=recall_precision, + ) else: - all_maps[key], all_maps_per_class[key] = self.pq_average(cat) + all_maps[key], all_maps_per_class[key] = self.pq_average(cat, recall_precision=recall_precision) if print_result and self.isfull: _print_head(head_elm=["PQ", "SQ", "RQ"], clm_size=9) diff --git a/alonet/torch2trt/base_exporter.py b/alonet/torch2trt/base_exporter.py index 50032fc1..4b04c046 100644 --- a/alonet/torch2trt/base_exporter.py +++ b/alonet/torch2trt/base_exporter.py @@ -308,7 +308,6 @@ def _torch2onnx(self): self.onnx_path, # where to save the model export_params=True, # store the trained parameter weights inside the model file output_names=onames, - enable_onnx_checker=True, input_names=self.input_names, # the model's input names dynamic_axes=self.dynamic_axes, custom_opsets=self.custom_opset, diff --git a/aloscene/__init__.py b/aloscene/__init__.py index 7340ab5d..da3ae2bd 100644 --- a/aloscene/__init__.py +++ b/aloscene/__init__.py @@ -1,26 +1,27 @@ ALOSCENE_ROOT = "/".join(__file__.split("/")[:-1]) -from . import tensors -from .labels import Labels -from .camera_calib import CameraExtrinsic, CameraIntrinsic -from .mask import Mask -from .flow import Flow -from .depth import Depth -from .points_2d import Points2D -from .points_3d import Points3D -from .disparity import Disparity -from .pose import Pose -from .bounding_boxes_2d import BoundingBoxes2D -from .bounding_boxes_3d import BoundingBoxes3D -from .oriented_boxes_2d import OrientedBoxes2D -from .scene_flow import SceneFlow -from .frame import Frame -from .tensors.spatial_augmented_tensor import SpatialAugmentedTensor - -from .renderer import Renderer +from . tensors import AugmentedTensor, SpatialAugmentedTensor +from . labels import Labels +from . camera_calib import CameraExtrinsic, CameraIntrinsic +from . mask import Mask +from . flow import Flow +from . depth import Depth +from . points_2d import Points2D +from . points_3d import Points3D +from . disparity import Disparity +from . pose import Pose +from . bounding_boxes_2d import BoundingBoxes2D +from . bounding_boxes_3d import BoundingBoxes3D +from . oriented_boxes_2d import OrientedBoxes2D +from . scene_flow import SceneFlow +from . frame import Frame +from . renderer import Renderer from typing import Union +import pkg_resources +__version__ = pkg_resources.get_distribution("aloception").version + def batch_list(tensors, intersection=False): return SpatialAugmentedTensor.batch_list(tensors, intersection=intersection) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..98fa5cb6 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +if [ -v LOCAL_USER_ID ]; then + if id -u ${LOCAL_USER_ID} >/dev/null 2>&1; then + BASE_USER=aloception + else + # Create a new user with the specified UID and GI + useradd --home /home/aloception --uid $LOCAL_USER_ID --shell /bin/bash dynamic && usermod -aG sudo dynamic && echo "dynamic ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + BASE_USER=dynamic + fi +else + BASE_USER=aloception +fi + +echo "Starting with UID : $LOCAL_USER_ID, base user: $BASE_USER" + +export CONDA_HOME=/opt/miniconda; +export PATH=${CONDA_HOME}/condabin:${CONDA_HOME}/bin:${PATH}; +source activate base; + +if [ "$#" -ne 0 ]; then + su -s /bin/bash $BASE_USER -c "export CONDA_HOME=/opt/miniconda; export PATH=${CONDA_HOME}/condabin:${CONDA_HOME}/bin:${PATH}; source activate base; $@" +else + su -s /bin/bash $BASE_USER -c "export CONDA_HOME=/opt/miniconda; export PATH=${CONDA_HOME}/condabin:${CONDA_HOME}/bin:${PATH}; source activate base; script -q /dev/null -c 'bash -i'" +fi + diff --git a/requirements-torch1.13.1.txt b/requirements/requirements-torch1.13.1.txt similarity index 99% rename from requirements-torch1.13.1.txt rename to requirements/requirements-torch1.13.1.txt index e6a22f91..95cc045a 100644 --- a/requirements-torch1.13.1.txt +++ b/requirements/requirements-torch1.13.1.txt @@ -19,3 +19,4 @@ tqdm==4.62.3 captum==0.4.0 setuptools==59.5.0 + diff --git a/setup.py b/setup.py index 551f474c..2d400fdc 100644 --- a/setup.py +++ b/setup.py @@ -3,32 +3,27 @@ setup( name='aloception', author='Visual Behavior', - version='0.3.0', + version='0.6.0beta', description='Aloception is a set of package for computer vision: aloscene, alodataset, alonet.', - packages=find_packages(include=['aloscene', 'alodataset', 'alonet']), + packages=find_packages(include=["aloscene", "aloscene.*", "alodataset", "alodataset.*", "alonet", "alonet.*"]), url='https://visualbehavior.ai/', download_url='https://github.com/Visual-Behavior/aloception-oss', install_requires=[ - 'matplotlib==3.5.3', - 'more-itertools==8.8.0', # required for alodataset waymo - 'onnx==1.12.0', - 'onnx_graphsurgeon==0.0.1.dev5', - 'onnxsim==0.4.8', - 'opencv-python==4.5.3.56' - 'Pillow==9.2.0', - 'pycocotools==2.0.2', # required for alodataset coco - 'pytorch_lightning==1.4.1', - 'pytorch_quantization==0.0.1.dev5', - 'Requests==2.28.1', - 'scipy==1.4.1', # required for alonet/detr/matcher - 'setuptools==63.4.1', - 'tensorflow==2.10.0', # required for alodataset/prepare/waymo_converter - 'tensorrt==0.0.1.dev5', - 'torchvision==0.13.1', + 'PyYAML==5.4.1', + 'chardet==4.0.0', + 'idna==2.10', + 'scipy==1.10.0', + 'more_itertools==8.8.0', + 'requests==2.25.1', + 'opencv-python==4.7.0.68', + 'python-dateutil==2.8.2', + 'urllib3==1.26.6', + 'protobuf==4.21.12', + 'wandb==0.13.9', 'tqdm==4.62.3', - 'ts==0.5.1', - 'wandb==0.12.2', - 'waymo_open_dataset==1.0.1'], + 'captum==0.4.0', + 'setuptools==59.5.0', + ], setup_requires=['numpy', 'torch', 'nvidia-pyindex', 'pycuda'], license_files=['LICENSE'], keywords=['artificial intelligence', 'computer vision'],