diff --git a/pyodi/apps/train_config/train_config_evaluation.py b/pyodi/apps/train_config/train_config_evaluation.py index 44810a5..799d631 100644 --- a/pyodi/apps/train_config/train_config_evaluation.py +++ b/pyodi/apps/train_config/train_config_evaluation.py @@ -71,9 +71,10 @@ from pathlib import Path from shutil import copyfile from tempfile import TemporaryDirectory -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple, Union import numpy as np +import pandas as pd from loguru import logger from pyodi.core.anchor_generator import AnchorGenerator @@ -120,23 +121,27 @@ def load_anchor_config_file(anchor_config_file: str) -> Dict[str, Any]: @logger.catch def train_config_evaluation( - ground_truth_file: str, + ground_truth_file: Union[str, pd.DataFrame], anchor_config: str, input_size: Tuple[int, int] = (1280, 720), show: bool = True, output: Optional[str] = None, output_size: Tuple[int, int] = (1600, 900), + keep_ratio: bool = False, ) -> None: """Evaluates the fitness between `ground_truth_file` and `anchor_config_file`. Args: - ground_truth_file: Path to COCO ground truth file. + ground_truth_file: Path to COCO ground truth file or coco df_annotations DataFrame + to be used from + [`pyodi train-config generation`][pyodi.apps.train_config.train_config_generation.train_config_generation] anchor_config: Path to MMDetection-like `anchor_generator` section. It can also be a dictionary with the required data. input_size: Model image input size. Defaults to (1333, 800). show: Show results or not. Defaults to True. - output: Output file where results are saved. Defaults to None. + output: Output directory where results going to be saved. Defaults to None. output_size: Size of saved images. Defaults to (1600, 900). + keep_ratio: Whether to keep the aspect ratio or not. Defaults to False. Examples: ```python @@ -150,20 +155,25 @@ def train_config_evaluation( ``` """ if output is not None: - output = str(Path(output) / Path(ground_truth_file).stem) Path(output).mkdir(parents=True, exist_ok=True) - coco_ground_truth = load_ground_truth_file(ground_truth_file) + if isinstance(ground_truth_file, str): + coco_ground_truth = load_ground_truth_file(ground_truth_file) - df_images, df_annotations = coco_ground_truth_to_dfs(coco_ground_truth) + df_images, df_annotations = coco_ground_truth_to_dfs(coco_ground_truth) - df_annotations = join_annotations_with_image_sizes(df_annotations, df_images) + df_annotations = join_annotations_with_image_sizes(df_annotations, df_images) - df_annotations = filter_zero_area_bboxes(df_annotations) + df_annotations = filter_zero_area_bboxes(df_annotations) - df_annotations = scale_bbox_dimensions(df_annotations, input_size=input_size) + df_annotations = scale_bbox_dimensions( + df_annotations, input_size=input_size, keep_ratio=keep_ratio + ) + + df_annotations = get_scale_and_ratio(df_annotations, prefix="scaled") - df_annotations = get_scale_and_ratio(df_annotations, prefix="scaled") + else: + df_annotations = ground_truth_file df_annotations["log_scaled_ratio"] = np.log(df_annotations["scaled_ratio"]) diff --git a/pyodi/apps/train_config/train_config_generation.py b/pyodi/apps/train_config/train_config_generation.py index 9fff2cd..26d779c 100644 --- a/pyodi/apps/train_config/train_config_generation.py +++ b/pyodi/apps/train_config/train_config_generation.py @@ -103,6 +103,7 @@ import numpy as np from loguru import logger +from pyodi.apps.train_config.train_config_evaluation import train_config_evaluation from pyodi.core.anchor_generator import AnchorGenerator from pyodi.core.boxes import ( filter_zero_area_bboxes, @@ -131,7 +132,8 @@ def train_config_generation( output: Optional[str] = None, output_size: Tuple[int, int] = (1600, 900), keep_ratio: bool = False, -) -> AnchorGenerator: + evaluate: bool = True, +) -> None: """Computes optimal anchors for a given COCO dataset based on iou clustering. Args: @@ -143,15 +145,17 @@ def train_config_generation( base_sizes: The basic sizes of anchors in multiple levels. If None is given, strides will be used as base_sizes. show: Show results or not. Defaults to True. - output: Output file where results are saved. Defaults to None. + output: Output directory where results going to be saved. Defaults to None. output_size: Size of saved images. Defaults to (1600, 900). keep_ratio: Whether to keep the aspect ratio or not. Defaults to False. + evaluate: Whether to evaluate or not the anchors. Check + [`pyodi train-config evaluation`][pyodi.apps.train_config.train_config_evaluation.train_config_evaluation] + for more information. Returns: Anchor generator instance. """ if output is not None: - output = str(Path(output) / Path(ground_truth_file).stem) Path(output).mkdir(parents=True, exist_ok=True) coco_ground_truth = load_ground_truth_file(ground_truth_file) @@ -216,9 +220,18 @@ def train_config_generation( title="COCO_anchor_generation", ) + if evaluate: + anchor_config = dict(anchor_generator=anchor_generator.to_dict()) + train_config_evaluation( + ground_truth_file=df_annotations, + anchor_config=anchor_config, # type: ignore + input_size=input_size, + show=show, + output=output, + output_size=output_size, + ) + if output: output_file = Path(output) / "anchor_config.py" with open(output_file, "w") as f: f.write(anchor_generator.to_string()) - - return anchor_generator diff --git a/pyodi/core/anchor_generator.py b/pyodi/core/anchor_generator.py index b514120..06c7314 100644 --- a/pyodi/core/anchor_generator.py +++ b/pyodi/core/anchor_generator.py @@ -115,16 +115,6 @@ def __init__( self.center_offset = center_offset self.base_anchors = self.gen_base_anchors() - @property - def num_base_anchors(self) -> List[int]: - """Returns the number of anchors per level. - - Returns: - List with number of anchors per level. - - """ - return [base_anchors.size(0) for base_anchors in self.base_anchors] - @property def num_levels(self) -> int: """Returns the number of levels. @@ -264,59 +254,6 @@ def single_level_grid_anchors( # then (0, 1), (0, 2), ... return all_anchors - # todo: this function depends on the following commented function - # def valid_flags( - # self, - # featmap_sizes: List[Tuple[int, int]], - # pad_shape: Tuple[int, int], - # device: str = "cuda", - # ) -> List: - # """Generate valid flags of anchors in multiple feature levels - # - # Args: - # featmap_sizes: List of feature map sizes in multiple feature levels. - # pad_shape: The padded shape of the image. - # device: Device where the anchors will be put on. Defaults to "cuda". - # - # Returns: - # Valid flags of anchors in multiple levels (List[torch.Tensor]). - # """ - # assert self.num_levels == len(featmap_sizes) - # multi_level_flags = [] - # for i in range(self.num_levels): - # anchor_stride = self.strides[i] - # feat_h, feat_w = featmap_sizes[i] - # h, w = pad_shape[:2] - # valid_feat_h = min(int(np.ceil(h / anchor_stride)), feat_h) - # valid_feat_w = min(int(np.ceil(w / anchor_stride)), feat_w) - # flags = self.single_level_valid_flags( - # (feat_h, feat_w), - # (valid_feat_h, valid_feat_w), - # self.num_base_anchors[i], - # device=device, - # ) - # multi_level_flags.append(flags) - # return multi_level_flags - - # todo: update with numpy if necessary - # def single_level_valid_flags( - # self, featmap_size, valid_size, num_base_anchors, device="cuda" - # ): - # feat_h, feat_w = featmap_size - # valid_h, valid_w = valid_size - # assert valid_h <= feat_h and valid_w <= feat_w - # valid_x = torch.zeros(feat_w, dtype=torch.uint8, device=device) - # valid_y = torch.zeros(feat_h, dtype=torch.uint8, device=device) - # valid_x[:valid_w] = 1 - # valid_y[:valid_h] = 1 - # valid_xx, valid_yy = self._meshgrid(valid_x, valid_y) - # valid = valid_xx & valid_yy - # valid = ( - # valid[:, None] - # .expand(valid.size(0), num_base_anchors).contiguous().view(-1) - # ) - # return valid - def __repr__(self) -> str: indent_str = " " repr_str = self.__class__.__name__ + "(\n"