Single class detection is yielding strange results
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.
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
)
Hi @FadelMamar ,
Thanks for opening this issue.
Could you share a sample of the csv file you're using for validation?
Thanks!
Alexandre
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
Hi @Alexandre-Delplanque
Unfortunately, it's still not working.
Have a look