Source code for stable_datasets.images.fer2013

import csv
import hashlib
import os
import tarfile
from pathlib import Path

import numpy as np
from tqdm import tqdm

from stable_datasets.schema import ClassLabel, DatasetInfo, DatasetSource, Features, Version
from stable_datasets.schema import Image as ImageFeature
from stable_datasets.splits import Split, SplitGenerator
from stable_datasets.utils import BaseDatasetBuilder, _default_dest_folder


_KAGGLE_URL = "https://www.kaggle.com/c/challenges-in-representation-learning-facial-expression-recognition-challenge"

# Kaggle-hosted files accepted by the builder, with the MD5s published by torchvision. Both CSVs hold the same
# 35,887 faces; icml_face_data.csv uses a different column order and spaces in its header (`emotion, Usage, pixels`).
_CSV_MD5 = {
    "fer2013.csv": "f8428a1edbd21e88f42c73edd2a14f95",
    "icml_face_data.csv": "b114b9e04e6949e5fe8b6a98b3892b1d",
}
_TARBALL = "fer2013.tar.gz"  # Kaggle archive that contains fer2013/fer2013.csv

# FER2013 `Usage` values -> splits (the same mapping as the FER+ release).
_USAGE_TO_SPLIT = {"Training": "train", "PublicTest": "val", "PrivateTest": "test"}


[docs] class FER2013(BaseDatasetBuilder): """FER2013 (Facial Expression Recognition 2013). Abstract 48x48 grayscale face crops labelled with one of seven facial expressions: angry, disgust, fear, happy, sad, surprise and neutral. Released for the ICML 2013 "Challenges in Representation Learning" Kaggle competition, it is one of the 27 datasets of CLIP's linear-probe evaluation suite. Context The faces were collected with Google image searches for emotion-related keywords, then automatically registered and cropped, so pose, lighting, age and occlusion vary widely. Human accuracy is estimated at ~65%, and the classes are imbalanced (disgust has 547 examples, happy 8,989), which makes it a demanding transfer benchmark. Content - Images: 35,887 grayscale 48x48 faces, decoded from the space-separated `pixels` column of the competition CSV. - Labels: the `emotion` column, 0-6 in the order angry, disgust, fear, happy, sad, surprise, neutral. - Splits: the competition's `Usage` column. Training -> train (28,709), PublicTest -> validation (3,589), PrivateTest -> test (3,589). Download Kaggle requires an account and acceptance of the competition rules, so the data cannot be fetched automatically. Download `fer2013.csv` (or `fer2013.tar.gz` / `icml_face_data.csv`) from the competition page and place it in the builder's download directory (`download_dir`, default `~/.stable-datasets/downloads`). """ VERSION = Version("1.0.0") SOURCE = DatasetSource( homepage=_KAGGLE_URL, assets={}, # manual download, see the class docstring license="Kaggle competition rules", citation="""@inproceedings{goodfellow2013challenges, title={Challenges in Representation Learning: A report on three machine learning contests}, author={Goodfellow, Ian J. and Erhan, Dumitru and Carrier, Pierre Luc and Courville, Aaron and Mirza, Mehdi and Hamner, Ben and Cukierski, Will and Tang, Yichuan and Thaler, David and Lee, Dong-Hyun and others}, booktitle={International Conference on Neural Information Processing (ICONIP)}, year={2013}}""", ) def _info(self): return DatasetInfo( description="""FER2013: 48x48 grayscale face images labelled with one of seven facial expressions (angry, disgust, fear, happy, sad, surprise, neutral), from the ICML 2013 Challenges in Representation Learning Kaggle competition.""", features=Features({"image": ImageFeature(), "label": ClassLabel(names=self._labels())}), supervised_keys=("image", "label"), homepage=self.SOURCE["homepage"], license=self.SOURCE["license"], citation=self.SOURCE["citation"], ) def _candidate_splits(self) -> list: return [Split.TRAIN, Split.VALIDATION, Split.TEST] def _split_generators(self): csv_path = _find_csv(Path(getattr(self, "_raw_download_dir", None) or _default_dest_folder())) return [ SplitGenerator(name=name, gen_kwargs={"csv_path": csv_path, "split": key}) for name, key in ((Split.TRAIN, "train"), (Split.VALIDATION, "val"), (Split.TEST, "test")) ] def _generate_examples(self, csv_path, split): with open(csv_path, newline="") as f: reader = csv.reader(f) columns = {name.strip(): i for i, name in enumerate(next(reader))} for idx, row in enumerate(tqdm(reader, desc=f"Processing FER2013 {split} split")): if _USAGE_TO_SPLIT[row[columns["Usage"]].strip()] != split: continue pixels = np.array(row[columns["pixels"]].split(), dtype=np.uint8).reshape(48, 48) yield f"{idx:05d}", {"image": pixels, "label": int(row[columns["emotion"]])} @staticmethod def _labels(): return ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"]
def _md5(path: Path) -> str: digest = hashlib.md5() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): digest.update(chunk) return digest.hexdigest() def _find_csv(download_dir: Path) -> Path: """Locate (and checksum) the Kaggle FER2013 CSV in ``download_dir``, extracting the tarball if needed.""" tarball = download_dir / _TARBALL if not (download_dir / "fer2013.csv").exists() and tarball.exists(): with tarfile.open(tarball, "r:gz") as archive: member = next(m for m in archive.getmembers() if m.name.endswith("fer2013.csv")) with open(download_dir / "fer2013.csv", "wb") as out: out.write(archive.extractfile(member).read()) for name, expected in _CSV_MD5.items(): path = download_dir / name if not path.exists(): continue if _md5(path) != expected: raise RuntimeError(f"{path} has an unexpected MD5 checksum; re-download it from {_KAGGLE_URL}") return path raise FileNotFoundError( f"FER2013 must be downloaded manually (Kaggle login and rule acceptance required). Download " f"'fer2013.csv' (or '{_TARBALL}' / 'icml_face_data.csv') from {_KAGGLE_URL}/data and place it in " f"{download_dir}{os.sep}" )