⬅ All snippets

FDDB Loader


Imports

from glob import glob
from itertools import combinations
from random import shuffle

import cv2 as cv
import numpy as np

Code

class FDDBLoader:
    """
    FDDB Dataset Loader

    Setup:
        Download the images      [http://vis-www.cs.umass.edu/fddb/originalPics.tar.gz]
                 and annotations [http://vis-www.cs.umass.edu/fddb/FDDB-folds.tgz]
        and extract them in `../dataset`

        It should look like this:
            ```
            ../dataset
            ├── 2002
            ├── 2003
            └── FDDB-folds
            ```

    Usage:
        Defaults are good enough.
        for configuration options, read doc for the constructor

            ```
            fddb = FDDBLoader(n_samples=10)
            for face in fddb.load():
                ...
            ```
    """

    def __init__(
        self,
        allow_occluding=False,
        n_samples=1000,
        return_grayscale=True,
        random_samples=True,
    ):
        """
        Args:
            allow_occluding: should overlapping faces be returned?
            n_samples: number of faces which are needed
            return_grayscale: should returned face be grayscale?
            random_samples: should this return the faces in (semi-)random order?
        """
        self.n_samples = n_samples
        self.imread_mode = 0 if return_grayscale else 1
        self.allow_occluding = allow_occluding
        self.annotation_files = glob(
            "../dataset/FDDB-folds/FDDB-fold-*-ellipseList.txt"
        )
        self.returned_faces = 0

        if random_samples:
            shuffle(self.annotation_files)

    @staticmethod
    def elliptical2rect(centre, axes, angle):
        """
        Converts elliptical parameters to a rectangular bounding box

        TODO: Since this doesn't know about image bounds, its possible
                that this returns coordinates that are outside the image.
                I haven't faced this issue so far though

        Args:
            centre: (cx, cy)
            axes: (a, b) # aligned to x and y. not major/minor
            angle: rotation angle (as present in the FDDB annotations)

        Returns:
            (x, y, w, h): top left coords and size of bounding box
        """
        cx, cy = centre
        a, b = axes

        cos_angle = np.cos(np.deg2rad(angle))

        vertical_component = a * cos_angle
        horizontal_component = b * cos_angle

        tl_y = max(0, int(cy - vertical_component))
        height = 2 * int(vertical_component)

        tl_x = max(0, int(cx - horizontal_component))
        width = 2 * int(horizontal_component)

        return (tl_x, tl_y, width, height)

    @staticmethod
    def does_intersect(rect1, rect2):
        """
        Checks if two bounding boxes intersect
        """
        x1, y1, w1, h1 = rect1
        x2, y2, w2, h2 = rect2
        minarea = (w1 + w2) * (h1 + h2)

        minx, miny = min(x1, x2), min(y1, y2)
        maxx, maxy = max(x1 + w1, x2 + w2), max(y1 + h1, y2 + h2)

        return ((maxx - minx) * (maxy - miny)) < minarea

    @staticmethod
    def load_from_file(annotation_file, allow_occluding, imread_mode):
        """
        Generator, yields faces from a single file until they can be loaded.

        Args:
            annotation_file: annotation file to read data from
            allow_occluding: should overlapping faces be returned?
            imread_mode: opencv imread mode
        """
        file_obj = open(annotation_file, "r")

        while True:
            img_name = file_obj.readline().strip()
            if img_name == "":
                break  # EOF
            img = cv.imread(f"../dataset/{img_name}.jpg", imread_mode)

            n_faces = int(file_obj.readline().strip())
            faces = []
            rects = []

            for i in range(n_faces):
                elliptical_params = list(
                    map(float, filter(bool, file_obj.readline().strip().split(" ")))
                )[:-1]
                a, b, angle, cx, cy = elliptical_params
                rect_params = FDDBLoader.elliptical2rect((cx, cy), (a, b), angle)
                x, y, w, h = rect_params

                rects.append(rect_params)
                faces.append(img[y : y + h, x : x + w])

            filtered_faces = []

            if allow_occluding or n_faces == 1:
                filtered_faces = faces
            else:
                ret_allowed = [True for _ in range(n_faces)]
                # for all pairs, check if they intersect
                combos_to_check = combinations(range(n_faces), 2)

                for i, j in combos_to_check:
                    if FDDBLoader.does_intersect(rects[i], rects[j]):
                        ret_allowed[i] = ret_allowed[j] = False

                for i in range(n_faces):
                    if ret_allowed[i]:
                        filtered_faces.append(faces[i])

            yield from filtered_faces

    def load(self):
        """
        Generator, yields faces from all the annotation files present
        """
        current_file_idx = 0
        while current_file_idx < len(self.annotation_files):
            current_file_loader = self.load_from_file(
                self.annotation_files[current_file_idx],
                self.allow_occluding,
                self.imread_mode,
            )

            for ret in current_file_loader:
                yield ret
                self.returned_faces += 1

                if self.returned_faces == self.n_samples:
                    current_file_loader.close()
                    return

            current_file_idx += 1
· · ·