HerdNet icon indicating copy to clipboard operation
HerdNet copied to clipboard

Single class detection is yielding strange results

Open FadelMamar opened this issue 10 months ago • 4 comments

Hello Alexandre,

Thank you for open-sourcing this amazing work. I have tried to train HerdNet for single class detection (i.e. only 1 class for wildlife) but the evaluation results are a bit strange.

Image

I am using the same pipeline as described in the tutorials:

  • num classes: 2 # including background
  • learning rate: 1e-4
  • epochs: 30
  • image_size: 800
  • batchsize: 32
  • down_ratio: 2
  • target cls heatmap: 20
  • cross entropy weight: None
  • stitcher: None
  • evaluation radius for PointsMetrics class is 20
  • dataset type: FolderDataset for both training and validation
  • Validation dataset has 10 time more negative samples than positive samples
  • HerdNetLMDS arguments are default

Here is how I'm loading the data:

Thanks, Fadel

patch_size=image_size # 800 in my case
transforms=dict()
transforms["train"] = (
                [
                    A.Resize(width=patch_size, height=patch_size, p=1.0),
                    A.VerticalFlip(p=0.5),
                    A.HorizontalFlip(p=0.5),
                    A.RandomRotate90(p=0.5),
                    A.RandomBrightnessContrast(
                        brightness_limit=0.2, contrast_limit=0.2, p=0.2
                    ),
                    A.Blur(blur_limit=15, p=0.2),
                    A.Normalize(
                        normalization=normalization,
                        p=1.0,
                        mean=(0.485, 0.456, 0.406),
                        std=(0.229, 0.224, 0.225),
                    ),
                ],
                [
                    MultiTransformsWrapper(
                        [
                            FIDT(num_classes=num_classes, down_ratio=down_ratio),
                            PointsToMask(
                                radius=2,
                                num_classes=num_classes,
                                squeeze=True,
                                down_ratio=int(patch_size // (16 * patch_size / 512)),
                            ),
                        ]
                    )
                ],
            )
            transforms["val"] = (
                [
                    A.Resize(width=patch_size, height=patch_size, p=1.0),
                    A.Normalize(
                        normalization=normalization,
                        p=1.0,
                        mean=(0.485, 0.456, 0.406),
                        std=(0.229, 0.224, 0.225),
                    ),
                ],
                [
                    DownSample(down_ratio=down_ratio, anno_type="point"),
                ],
            )

split='val'
dataset = FolderDataset(  
            csv_file=df_annotations,
            root_dir="",
            albu_transforms=transforms[split][0],
            end_transforms=transforms[split][1],
            images_paths=selected_images, # I am overriding root_dir arguments and assigning directly FolderDataset.folder_images
        )

FadelMamar avatar Mar 19 '25 13:03 FadelMamar

Hi @FadelMamar ,

Thanks for opening this issue.

Could you share a sample of the csv file you're using for validation?

Thanks!

Alexandre

Alexandre-Delplanque avatar Mar 25 '25 08:03 Alexandre-Delplanque

Hi @Alexandre-Delplanque

Here is a example of the csv file

train_herdnet_df.csv

Best regards

FadelMamar avatar Mar 31 '25 09:03 FadelMamar

Hi @FadelMamar ,

Thanks, please replace the animaloc/datasets/folder.py module with the following code:

__copyright__ = \
    """
    Copyright (C) 2024 University of Liège, Gembloux Agro-Bio Tech, Forest Is Life
    All rights reserved.

    This source code is under the MIT License.

    Please contact the author Alexandre Delplanque ([email protected]) for any questions.

    Last modification: April 02, 2025
    """
__author__ = "Alexandre Delplanque"
__license__ = "MIT License"
__version__ = "0.2.1"


import os
import PIL
import pandas
import numpy

from typing import Optional, List, Any, Dict

from ..data.types import BoundingBox
from ..data.utils import group_by_image

from .register import DATASETS

from .csv import CSVDataset

@DATASETS.register()
class FolderDataset(CSVDataset):
    ''' Class to create a dataset from a folder containing images only, and a CSV file 
    containing annotations.

    This dataset is built on the basis of CSV files containing box coordinates, in 
    [x_min, y_min, x_max, y_max] format, or point coordinates in [x,y] format.

    All images that do not have corresponding annotations in the CSV file are considered as 
    background images. In this case, the dataset will return the image and empty target 
    (i.e. empty lists).

    The type of annotations is automatically detected internally. The only condition 
    is that the file contains at least the keys ['images', 'x_min', 'y_min', 'x_max', 
    'y_max', 'labels'] for the boxes and, ['images', 'x', 'y', 'labels'] for the points. 
    Any additional information (i.e. additional columns) will be associated and returned 
    by the dataset.

    If no data augmentation is specified, the dataset returns the image in PIL format 
    and the targets as lists. If transforms are specified, the conversion to torch.Tensor
    is done internally, no need to specify this. 
    '''

    def __init__(
        self, 
        csv_file: str, 
        root_dir: str, 
        albu_transforms: Optional[list] = None,
        end_transforms: Optional[list] = None
        ) -> None:
        ''' 
        Args:
            csv_file (str): absolute path to the csv file containing 
                annotations
            root_dir (str) : path to the images folder
            albu_transforms (list, optional): an albumentations' transformations 
                list that takes input sample as entry and returns a transformed 
                version. Defaults to None.
            end_transforms (list, optional): list of transformations that takes
                tensor and expected target as input and returns a transformed
                version. These will be applied after albu_transforms. Defaults
                to None.
        '''

        super(FolderDataset, self).__init__(csv_file, root_dir, albu_transforms, end_transforms)

        self.folder_images = [i for i in os.listdir(self.root_dir) 
                                if i.endswith(('.JPG','.jpg','.JPEG','.jpeg'))]
    
        self._img_names = self.folder_images        
        self.anno_keys = self.data.columns
        self.data['from_folder'] = 0

        folder_only_images = numpy.setdiff1d(self.folder_images, self.data['images'].unique().tolist())
        folder_df = pandas.DataFrame(data=dict(images = folder_only_images))
        folder_df['from_folder'] = 1

        self.data = pandas.concat([self.data, folder_df], ignore_index=True).convert_dtypes()

        self._ordered_img_names = group_by_image(self.data)['images'].values.tolist()

    def _load_image(self, index: int) -> PIL.Image.Image:
        img_name = self._ordered_img_names[index]
        img_path = os.path.join(self.root_dir, img_name)

        pil_img = PIL.Image.open(img_path).convert('RGB')
        pil_img.filename = img_name

        return pil_img

    def _load_target(self, index: int) -> Dict[str,List[Any]]:
        img_name = self._ordered_img_names[index]
        annotations = self.data[self.data['images'] == img_name]
        annotations = annotations.drop(columns='images')
        anno_keys = annotations.columns

        target = {
        'image_id': [index], 
        'image_name': [img_name]
        }

        nan_in_labels =  annotations["labels"].isnull().any()
        if not nan_in_labels:
            for key in anno_keys:
                target.update({key: list(annotations[key])})

                if key == 'annos': 
                    target.update({key: [list(a.get_tuple) for a in annotations[key]]})

        else:
            for key in anno_keys:
                if self.anno_type == 'BoundingBox':
                    if key == 'annos':  
                        target.update({key: [[0,1,2,3]]})
                    elif key == 'labels':
                        target.update({key: [0]})
                else:        
                    target.update({key: []})
        
        return target

If this solves your issue, I'll commit it to the repo.

Tell me if it works!

Best,

Alexandre

Alexandre-Delplanque avatar Apr 02 '25 08:04 Alexandre-Delplanque

Hi @Alexandre-Delplanque

Unfortunately, it's still not working.

Have a look

Image

FadelMamar avatar Apr 02 '25 18:04 FadelMamar