torchsight.datasets.logo32plus module

The dataset interface to interact with the Logo32plus dataset.

Dataset extracted from: http://www.ivl.disco.unimib.it/activities/logo-recognition/

Source code
"""The dataset interface to interact with the Logo32plus dataset.

Dataset extracted from:
http://www.ivl.disco.unimib.it/activities/logo-recognition/
"""
import json
import math
import os
import random

import torch
from PIL import Image
from scipy.io import loadmat

from .mixins import VisualizeMixin


class Logo32plusDataset(torch.utils.data.Dataset, VisualizeMixin):
    """Dataset to get the images and annotations of the Logo32plus dataset.

    Instructions:

    - Download the dataset from:
    http://www.ivl.disco.unimib.it/activities/logo-recognition/
    - Unzip the file in any directory.
    - Provide the path to that directory in the initialization.
    """

    def __init__(self, root, dataset='training', transform=None, annot_file='groundtruth.mat',
                 classes=None, split_file='train_valid.json'):
        """Initialize the dataset.

        Arguments:
            root (str): The path where are the unzipped files of te dataset.
            dataset (str, optional): Which dataset to load: 'training', 'validation' or 'both'.
            transform (callable, optional): A callable to transform the image and its bounding boxes
                before return them.
            annot_file (str, optional): The file that contains the annotations for the images.
            classes (list of str, optional): Only load this classes (identified by its name).
            split_file (str, optional): The file that contains the split between training and validation
                sets.
        """
        self.root = self.validate_root(root)
        self.dataset = self.validate_dataset(dataset)
        self.annot_file = annot_file
        self.classes = classes
        self.split = self.get_split(split_file)
        self.annotations = self.get_annotations()
        self.label_to_class, self.class_to_label = self.generate_classes()
        self.transform = transform

    @staticmethod
    def validate_root(root):
        """Validate that the root path already exists.

        Arguments:
            root (str): The path to validate.

        Returns:
            str: The path if it's correct.

        Raises:
            ValueError: When the path does not exists.
        """
        if not os.path.exists(root):
            raise ValueError('There is no directory with path: {}'.format(root))

        return root

    @staticmethod
    def validate_dataset(dataset):
        """Validate that the dataset is in ['training', 'validation', 'both'].

        Arguments:
            dataset (str): The string to validate.

        Returns:
            str: The dataset if it's valid.

        Raises:
            ValueError: If the given dataset is not a valid one.
        """
        if dataset not in ['training', 'validation', 'both']:
            raise ValueError('The dataset must be "training", "validation" or "both", not "{}"'.format(dataset))

        return dataset

    def get_split(self, split_file):
        """Get the JSON with the split file or generate a new one.

        Arguments:
            split_file (str): The name of the file that contains the json with the split.
        """
        filepath = os.path.join(self.root, split_file)

        if not os.path.exists(filepath):
            self.generate_split(annotations=self.get_annotations(), split_file=split_file)

        with open(filepath, 'r') as file:
            return json.loads(file.read())

    def get_annotations(self):
        """Load and parse the annotations of the images.

        Returns:
            list of tuples: like (image: str, boxes: tensor, name: str)
        """
        annotations = loadmat(os.path.join(self.root, self.annot_file))['groundtruth'][0]
        result = []
        for annot in annotations:
            name = annot[2][0]
            if self.classes is not None and name not in self.classes:
                continue

            image = annot[0][0].replace('\\', '/')
            if self.dataset != 'both' and getattr(self, 'split', None) is not None and image not in self.split[self.dataset]:
                continue

            boxes = self.transform_boxes(annot[1])
            result.append((image, boxes, name))

        return result

    def transform_boxes(self, boxes):
        """Transform the boxes with x,y,w,h 1-indexed to x1,y1,x2,y2 0-indexed.

        Arguments:
            boxes (list of list of int): A list with the annotations in format x,y,w,h 1-indexed.
        """
        boxes = torch.Tensor(boxes.astype('int32'))
        x, y, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
        x1, y1 = x - 1, y - 1  # 0-indexed
        x2, y2 = x1 + w, y1 + h
        boxes = torch.stack([x1, y1, x2, y2], dim=1)

        return boxes

    def generate_classes(self):
        """Generate the map dicts to assign a 0-indexed label to each one of the classes and viceversa."""
        classes = list({annot[2] for annot in self.annotations})
        classes.sort()
        label_to_class = {i: c for i, c in enumerate(classes)}
        class_to_label = {c: i for i, c in enumerate(classes)}

        return label_to_class, class_to_label

    def __len__(self):
        """Get the number of images in this dataset."""
        return len(self.annotations)

    def __getitem__(self, index):
        """Get an item from the dataset.

        Arguments:
            index (int): The index of the item that you want to get.

        Returns:
            tuple: A tuple with the image and the bounding boxes.
                The image is a PIL image or the result of the callable transform.
                The bounding boxes are a torch tensor with shape (num annot, 5),
                because an image could have more than one annotation and the 5 values are
                x1,y1,x2,y2 and the label.
        """
        image, boxes, name = self.annotations[index]

        info = {'brand': name, 'file': image}

        # Append the label to the boxes
        label = self.class_to_label[name]
        n_boxes = boxes.shape[0]
        labels = torch.full((n_boxes,), label)
        boxes = torch.cat([boxes, labels.unsqueeze(dim=1)], dim=1)

        # Load the image
        filepath = os.path.join(self.root, 'images', image)
        image = Image.open(filepath)

        if self.transform:
            image, boxes, info = self.transform((image, boxes, info))

        return image, boxes, info

    def generate_split(self, annotations, proportion=0.8, split_file='train_valid.json'):
        """Create the validation and training datasets with the given proportion.

        The proportion is used in each class. For example, with a proportion of 0.8 and a class with
        20 elements, this method creates a training dataset with 16 of those 20 images.

        Arguments:
            proportion (float): A float between [0, 1] that is the amount of training samples extracted
                from the total samples in each class.
        """
        brands = {}
        training = {}
        validation = {}

        for image, _, brand in annotations:
            if brand not in brands:
                brands[brand] = set()
                training[brand] = set()
                validation[brand] = set()

            brands[brand].add(image)

        result = {'training': [], 'validation': []}

        for brand, images in brands.items():
            n_train = math.ceil(len(images) * proportion)
            train = set(random.sample(images, n_train))
            valid = images - train

            result['training'] += list(train)
            result['validation'] += list(valid)

        with open(os.path.join(self.root, split_file), 'w') as file:
            file.write(json.dumps(result))

Classes

class Logo32plusDataset (ancestors: torch.utils.data.dataset.Dataset, VisualizeMixin)

Dataset to get the images and annotations of the Logo32plus dataset.

Instructions:

Source code
class Logo32plusDataset(torch.utils.data.Dataset, VisualizeMixin):
    """Dataset to get the images and annotations of the Logo32plus dataset.

    Instructions:

    - Download the dataset from:
    http://www.ivl.disco.unimib.it/activities/logo-recognition/
    - Unzip the file in any directory.
    - Provide the path to that directory in the initialization.
    """

    def __init__(self, root, dataset='training', transform=None, annot_file='groundtruth.mat',
                 classes=None, split_file='train_valid.json'):
        """Initialize the dataset.

        Arguments:
            root (str): The path where are the unzipped files of te dataset.
            dataset (str, optional): Which dataset to load: 'training', 'validation' or 'both'.
            transform (callable, optional): A callable to transform the image and its bounding boxes
                before return them.
            annot_file (str, optional): The file that contains the annotations for the images.
            classes (list of str, optional): Only load this classes (identified by its name).
            split_file (str, optional): The file that contains the split between training and validation
                sets.
        """
        self.root = self.validate_root(root)
        self.dataset = self.validate_dataset(dataset)
        self.annot_file = annot_file
        self.classes = classes
        self.split = self.get_split(split_file)
        self.annotations = self.get_annotations()
        self.label_to_class, self.class_to_label = self.generate_classes()
        self.transform = transform

    @staticmethod
    def validate_root(root):
        """Validate that the root path already exists.

        Arguments:
            root (str): The path to validate.

        Returns:
            str: The path if it's correct.

        Raises:
            ValueError: When the path does not exists.
        """
        if not os.path.exists(root):
            raise ValueError('There is no directory with path: {}'.format(root))

        return root

    @staticmethod
    def validate_dataset(dataset):
        """Validate that the dataset is in ['training', 'validation', 'both'].

        Arguments:
            dataset (str): The string to validate.

        Returns:
            str: The dataset if it's valid.

        Raises:
            ValueError: If the given dataset is not a valid one.
        """
        if dataset not in ['training', 'validation', 'both']:
            raise ValueError('The dataset must be "training", "validation" or "both", not "{}"'.format(dataset))

        return dataset

    def get_split(self, split_file):
        """Get the JSON with the split file or generate a new one.

        Arguments:
            split_file (str): The name of the file that contains the json with the split.
        """
        filepath = os.path.join(self.root, split_file)

        if not os.path.exists(filepath):
            self.generate_split(annotations=self.get_annotations(), split_file=split_file)

        with open(filepath, 'r') as file:
            return json.loads(file.read())

    def get_annotations(self):
        """Load and parse the annotations of the images.

        Returns:
            list of tuples: like (image: str, boxes: tensor, name: str)
        """
        annotations = loadmat(os.path.join(self.root, self.annot_file))['groundtruth'][0]
        result = []
        for annot in annotations:
            name = annot[2][0]
            if self.classes is not None and name not in self.classes:
                continue

            image = annot[0][0].replace('\\', '/')
            if self.dataset != 'both' and getattr(self, 'split', None) is not None and image not in self.split[self.dataset]:
                continue

            boxes = self.transform_boxes(annot[1])
            result.append((image, boxes, name))

        return result

    def transform_boxes(self, boxes):
        """Transform the boxes with x,y,w,h 1-indexed to x1,y1,x2,y2 0-indexed.

        Arguments:
            boxes (list of list of int): A list with the annotations in format x,y,w,h 1-indexed.
        """
        boxes = torch.Tensor(boxes.astype('int32'))
        x, y, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
        x1, y1 = x - 1, y - 1  # 0-indexed
        x2, y2 = x1 + w, y1 + h
        boxes = torch.stack([x1, y1, x2, y2], dim=1)

        return boxes

    def generate_classes(self):
        """Generate the map dicts to assign a 0-indexed label to each one of the classes and viceversa."""
        classes = list({annot[2] for annot in self.annotations})
        classes.sort()
        label_to_class = {i: c for i, c in enumerate(classes)}
        class_to_label = {c: i for i, c in enumerate(classes)}

        return label_to_class, class_to_label

    def __len__(self):
        """Get the number of images in this dataset."""
        return len(self.annotations)

    def __getitem__(self, index):
        """Get an item from the dataset.

        Arguments:
            index (int): The index of the item that you want to get.

        Returns:
            tuple: A tuple with the image and the bounding boxes.
                The image is a PIL image or the result of the callable transform.
                The bounding boxes are a torch tensor with shape (num annot, 5),
                because an image could have more than one annotation and the 5 values are
                x1,y1,x2,y2 and the label.
        """
        image, boxes, name = self.annotations[index]

        info = {'brand': name, 'file': image}

        # Append the label to the boxes
        label = self.class_to_label[name]
        n_boxes = boxes.shape[0]
        labels = torch.full((n_boxes,), label)
        boxes = torch.cat([boxes, labels.unsqueeze(dim=1)], dim=1)

        # Load the image
        filepath = os.path.join(self.root, 'images', image)
        image = Image.open(filepath)

        if self.transform:
            image, boxes, info = self.transform((image, boxes, info))

        return image, boxes, info

    def generate_split(self, annotations, proportion=0.8, split_file='train_valid.json'):
        """Create the validation and training datasets with the given proportion.

        The proportion is used in each class. For example, with a proportion of 0.8 and a class with
        20 elements, this method creates a training dataset with 16 of those 20 images.

        Arguments:
            proportion (float): A float between [0, 1] that is the amount of training samples extracted
                from the total samples in each class.
        """
        brands = {}
        training = {}
        validation = {}

        for image, _, brand in annotations:
            if brand not in brands:
                brands[brand] = set()
                training[brand] = set()
                validation[brand] = set()

            brands[brand].add(image)

        result = {'training': [], 'validation': []}

        for brand, images in brands.items():
            n_train = math.ceil(len(images) * proportion)
            train = set(random.sample(images, n_train))
            valid = images - train

            result['training'] += list(train)
            result['validation'] += list(valid)

        with open(os.path.join(self.root, split_file), 'w') as file:
            file.write(json.dumps(result))

Static methods

def validate_dataset(dataset)

Validate that the dataset is in ['training', 'validation', 'both'].

Arguments

dataset : str
The string to validate.

Returns

str
The dataset if it's valid.

Raises

ValueError
If the given dataset is not a valid one.
Source code
@staticmethod
def validate_dataset(dataset):
    """Validate that the dataset is in ['training', 'validation', 'both'].

    Arguments:
        dataset (str): The string to validate.

    Returns:
        str: The dataset if it's valid.

    Raises:
        ValueError: If the given dataset is not a valid one.
    """
    if dataset not in ['training', 'validation', 'both']:
        raise ValueError('The dataset must be "training", "validation" or "both", not "{}"'.format(dataset))

    return dataset
def validate_root(root)

Validate that the root path already exists.

Arguments

root : str
The path to validate.

Returns

str
The path if it's correct.

Raises

ValueError
When the path does not exists.
Source code
@staticmethod
def validate_root(root):
    """Validate that the root path already exists.

    Arguments:
        root (str): The path to validate.

    Returns:
        str: The path if it's correct.

    Raises:
        ValueError: When the path does not exists.
    """
    if not os.path.exists(root):
        raise ValueError('There is no directory with path: {}'.format(root))

    return root

Methods

def __init__(self, root, dataset='training', transform=None, annot_file='groundtruth.mat', classes=None, split_file='train_valid.json')

Initialize the dataset.

Arguments

root : str
The path where are the unzipped files of te dataset.
dataset : str, optional
Which dataset to load: 'training', 'validation' or 'both'.
transform : callable, optional
A callable to transform the image and its bounding boxes before return them.
annot_file : str, optional
The file that contains the annotations for the images.
classes : list of str, optional
Only load this classes (identified by its name).
split_file : str, optional
The file that contains the split between training and validation sets.
Source code
def __init__(self, root, dataset='training', transform=None, annot_file='groundtruth.mat',
             classes=None, split_file='train_valid.json'):
    """Initialize the dataset.

    Arguments:
        root (str): The path where are the unzipped files of te dataset.
        dataset (str, optional): Which dataset to load: 'training', 'validation' or 'both'.
        transform (callable, optional): A callable to transform the image and its bounding boxes
            before return them.
        annot_file (str, optional): The file that contains the annotations for the images.
        classes (list of str, optional): Only load this classes (identified by its name).
        split_file (str, optional): The file that contains the split between training and validation
            sets.
    """
    self.root = self.validate_root(root)
    self.dataset = self.validate_dataset(dataset)
    self.annot_file = annot_file
    self.classes = classes
    self.split = self.get_split(split_file)
    self.annotations = self.get_annotations()
    self.label_to_class, self.class_to_label = self.generate_classes()
    self.transform = transform
def generate_classes(self)

Generate the map dicts to assign a 0-indexed label to each one of the classes and viceversa.

Source code
def generate_classes(self):
    """Generate the map dicts to assign a 0-indexed label to each one of the classes and viceversa."""
    classes = list({annot[2] for annot in self.annotations})
    classes.sort()
    label_to_class = {i: c for i, c in enumerate(classes)}
    class_to_label = {c: i for i, c in enumerate(classes)}

    return label_to_class, class_to_label
def generate_split(self, annotations, proportion=0.8, split_file='train_valid.json')

Create the validation and training datasets with the given proportion.

The proportion is used in each class. For example, with a proportion of 0.8 and a class with 20 elements, this method creates a training dataset with 16 of those 20 images.

Arguments

proportion : float
A float between [0, 1] that is the amount of training samples extracted from the total samples in each class.
Source code
def generate_split(self, annotations, proportion=0.8, split_file='train_valid.json'):
    """Create the validation and training datasets with the given proportion.

    The proportion is used in each class. For example, with a proportion of 0.8 and a class with
    20 elements, this method creates a training dataset with 16 of those 20 images.

    Arguments:
        proportion (float): A float between [0, 1] that is the amount of training samples extracted
            from the total samples in each class.
    """
    brands = {}
    training = {}
    validation = {}

    for image, _, brand in annotations:
        if brand not in brands:
            brands[brand] = set()
            training[brand] = set()
            validation[brand] = set()

        brands[brand].add(image)

    result = {'training': [], 'validation': []}

    for brand, images in brands.items():
        n_train = math.ceil(len(images) * proportion)
        train = set(random.sample(images, n_train))
        valid = images - train

        result['training'] += list(train)
        result['validation'] += list(valid)

    with open(os.path.join(self.root, split_file), 'w') as file:
        file.write(json.dumps(result))
def get_annotations(self)

Load and parse the annotations of the images.

Returns

list of tuples: like (image: str, boxes: tensor, name: str)

Source code
def get_annotations(self):
    """Load and parse the annotations of the images.

    Returns:
        list of tuples: like (image: str, boxes: tensor, name: str)
    """
    annotations = loadmat(os.path.join(self.root, self.annot_file))['groundtruth'][0]
    result = []
    for annot in annotations:
        name = annot[2][0]
        if self.classes is not None and name not in self.classes:
            continue

        image = annot[0][0].replace('\\', '/')
        if self.dataset != 'both' and getattr(self, 'split', None) is not None and image not in self.split[self.dataset]:
            continue

        boxes = self.transform_boxes(annot[1])
        result.append((image, boxes, name))

    return result
def get_split(self, split_file)

Get the JSON with the split file or generate a new one.

Arguments

split_file : str
The name of the file that contains the json with the split.
Source code
def get_split(self, split_file):
    """Get the JSON with the split file or generate a new one.

    Arguments:
        split_file (str): The name of the file that contains the json with the split.
    """
    filepath = os.path.join(self.root, split_file)

    if not os.path.exists(filepath):
        self.generate_split(annotations=self.get_annotations(), split_file=split_file)

    with open(filepath, 'r') as file:
        return json.loads(file.read())
def transform_boxes(self, boxes)

Transform the boxes with x,y,w,h 1-indexed to x1,y1,x2,y2 0-indexed.

Arguments

boxes : list of list of int
A list with the annotations in format x,y,w,h 1-indexed.
Source code
def transform_boxes(self, boxes):
    """Transform the boxes with x,y,w,h 1-indexed to x1,y1,x2,y2 0-indexed.

    Arguments:
        boxes (list of list of int): A list with the annotations in format x,y,w,h 1-indexed.
    """
    boxes = torch.Tensor(boxes.astype('int32'))
    x, y, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    x1, y1 = x - 1, y - 1  # 0-indexed
    x2, y2 = x1 + w, y1 + h
    boxes = torch.stack([x1, y1, x2, y2], dim=1)

    return boxes

Inherited members