diff --git a/setup.py b/setup.py index 09f951b1090..53bba409407 100644 --- a/setup.py +++ b/setup.py @@ -495,7 +495,7 @@ def run(self): # Package info packages=find_packages(exclude=('test',)), package_data={ - package_name: ['*.dll', '*.dylib', '*.so'] + package_name: ['*.dll', '*.dylib', '*.so', 'prototype/datasets/_builtin/*.categories'] }, zip_safe=False, install_requires=requirements, diff --git a/test/prototype/datasets/conftest.py b/test/prototype/datasets/conftest.py new file mode 100644 index 00000000000..474d5fb6240 --- /dev/null +++ b/test/prototype/datasets/conftest.py @@ -0,0 +1,10 @@ +import pytest +from torchvision.prototype.datasets.utils import DatasetInfo + + +@pytest.fixture +def make_minimal_dataset_info(): + def make(name="name", categories=None, **kwargs): + return DatasetInfo(name, categories=categories or [], **kwargs) + + return make diff --git a/test/prototype/datasets/test_api.py b/test/prototype/datasets/test_api.py index 5ba432fd1f4..4e2102ee320 100644 --- a/test/prototype/datasets/test_api.py +++ b/test/prototype/datasets/test_api.py @@ -3,7 +3,7 @@ from torchvision.prototype import datasets from torchvision.prototype.datasets import _api from torchvision.prototype.datasets import _builtin -from torchvision.prototype.datasets.utils import Dataset, DatasetInfo, DatasetConfig +from torchvision.prototype.datasets.utils import Dataset, DatasetConfig @pytest.fixture @@ -14,9 +14,9 @@ def patch_datasets(monkeypatch): @pytest.fixture -def dataset(mocker): - info = DatasetInfo( - "name", valid_options=dict(split=("train", "test"), foo=("bar", "baz")) +def dataset(mocker, make_minimal_dataset_info): + info = make_minimal_dataset_info( + valid_options=dict(split=("train", "test"), foo=("bar", "baz")) ) class DatasetMock(Dataset): diff --git a/test/prototype/datasets/test_utils.py b/test/prototype/datasets/test_utils.py index 4cac156bc43..14673efef03 100644 --- a/test/prototype/datasets/test_utils.py +++ b/test/prototype/datasets/test_utils.py @@ -122,42 +122,43 @@ class TestDatasetInfo: def valid_options(): return dict(split=("train", "test"), foo=("bar", "baz")) - def test_no_valid_options(self): - info = utils.DatasetInfo("name") + @staticmethod + @pytest.fixture + def info(make_minimal_dataset_info, valid_options): + return make_minimal_dataset_info(valid_options=valid_options) + + def test_no_valid_options(self, make_minimal_dataset_info): + info = make_minimal_dataset_info() assert info.default_config.split == "train" - def test_valid_options_no_split(self): - info = utils.DatasetInfo("name", valid_options=dict(option=("argument",))) + def test_valid_options_no_split(self, make_minimal_dataset_info): + info = make_minimal_dataset_info(valid_options=dict(option=("argument",))) assert info.default_config.split == "train" - def test_valid_options_no_train(self): + def test_valid_options_no_train(self, make_minimal_dataset_info): with pytest.raises(ValueError): - utils.DatasetInfo("name", valid_options=dict(split=("test",))) + make_minimal_dataset_info(valid_options=dict(split=("test",))) - def test_default_config(self, valid_options): + def test_default_config(self, make_minimal_dataset_info, valid_options): default_config = utils.DatasetConfig( {key: values[0] for key, values in valid_options.items()} ) assert ( - utils.DatasetInfo("name", valid_options=valid_options).default_config + make_minimal_dataset_info(valid_options=valid_options).default_config == default_config ) - def test_make_config_unknown_option(self, valid_options): - info = utils.DatasetInfo("name", valid_options=valid_options) - + def test_make_config_unknown_option(self, info): with pytest.raises(ValueError): info.make_config(unknown_option=None) - def test_make_config_invalid_argument(self, valid_options): - info = utils.DatasetInfo("name", valid_options=valid_options) - + def test_make_config_invalid_argument(self, info): with pytest.raises(ValueError): info.make_config(split="unknown_split") - def test_repr(self, valid_options): - output = repr(utils.DatasetInfo("name", valid_options=valid_options)) + def test_repr(self, make_minimal_dataset_info, valid_options): + output = repr(make_minimal_dataset_info(valid_options=valid_options)) assert isinstance(output, str) assert "DatasetInfo" in output @@ -165,9 +166,9 @@ def test_repr(self, valid_options): assert f"{key}={value}" in output @pytest.mark.parametrize("optional_info", ("citation", "homepage", "license")) - def test_repr_optional_info(self, optional_info): + def test_repr_optional_info(self, make_minimal_dataset_info, optional_info): sentinel = "sentinel" - info = utils.DatasetInfo("name", **{optional_info: sentinel}) + info = make_minimal_dataset_info(**{optional_info: sentinel}) assert f"{optional_info}={sentinel}" in repr(info) @@ -183,6 +184,7 @@ def make(name="name", valid_options=None, resources=None): dict( info=utils.DatasetInfo( name, + categories=[], valid_options=valid_options or dict(split=("train", "test")), ), resources=mocker.Mock(return_value=[]) diff --git a/torchvision/prototype/datasets/__init__.py b/torchvision/prototype/datasets/__init__.py index 7bd35c72594..f35a3776d05 100644 --- a/torchvision/prototype/datasets/__init__.py +++ b/torchvision/prototype/datasets/__init__.py @@ -1,3 +1,4 @@ +from ._home import * from . import decoder, utils, datapipes # Load this last, since itself but especially _builtin/* depends on the above being available diff --git a/torchvision/prototype/datasets/_api.py b/torchvision/prototype/datasets/_api.py index 46c21a596d2..025b0927168 100644 --- a/torchvision/prototype/datasets/_api.py +++ b/torchvision/prototype/datasets/_api.py @@ -1,36 +1,17 @@ -import difflib import io -import os -import pathlib -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional import torch -from torch.hub import _get_torch_home from torch.utils.data import IterDataPipe +from torchvision.prototype.datasets import home from torchvision.prototype.datasets.decoder import pil from torchvision.prototype.datasets.utils import Dataset, DatasetInfo from torchvision.prototype.datasets.utils._internal import add_suggestion from . import _builtin -__all__ = ["home", "register", "list", "info", "load"] - -# TODO: This needs a better default -HOME = pathlib.Path(_get_torch_home()) / "datasets" / "vision" - - -def home(home: Optional[Union[str, pathlib.Path]] = None) -> pathlib.Path: - global HOME - if home is not None: - HOME = pathlib.Path(home).expanduser().resolve() - return HOME - - home = os.getenv("TORCHVISION_DATASETS_HOME") - if home is not None: - return pathlib.Path(home) - - return HOME +__all__ = ["register", "list", "info", "load"] DATASETS: Dict[str, Dataset] = {} diff --git a/torchvision/prototype/datasets/_builtin/caltech256.categories b/torchvision/prototype/datasets/_builtin/caltech256.categories new file mode 100644 index 00000000000..82128efba97 --- /dev/null +++ b/torchvision/prototype/datasets/_builtin/caltech256.categories @@ -0,0 +1,257 @@ +ak47 +american-flag +backpack +baseball-bat +baseball-glove +basketball-hoop +bat +bathtub +bear +beer-mug +billiards +binoculars +birdbath +blimp +bonsai-101 +boom-box +bowling-ball +bowling-pin +boxing-glove +brain-101 +breadmaker +buddha-101 +bulldozer +butterfly +cactus +cake +calculator +camel +cannon +canoe +car-tire +cartman +cd +centipede +cereal-box +chandelier-101 +chess-board +chimp +chopsticks +cockroach +coffee-mug +coffin +coin +comet +computer-keyboard +computer-monitor +computer-mouse +conch +cormorant +covered-wagon +cowboy-hat +crab-101 +desk-globe +diamond-ring +dice +dog +dolphin-101 +doorknob +drinking-straw +duck +dumb-bell +eiffel-tower +electric-guitar-101 +elephant-101 +elk +ewer-101 +eyeglasses +fern +fighter-jet +fire-extinguisher +fire-hydrant +fire-truck +fireworks +flashlight +floppy-disk +football-helmet +french-horn +fried-egg +frisbee +frog +frying-pan +galaxy +gas-pump +giraffe +goat +golden-gate-bridge +goldfish +golf-ball +goose +gorilla +grand-piano-101 +grapes +grasshopper +guitar-pick +hamburger +hammock +harmonica +harp +harpsichord +hawksbill-101 +head-phones +helicopter-101 +hibiscus +homer-simpson +horse +horseshoe-crab +hot-air-balloon +hot-dog +hot-tub +hourglass +house-fly +human-skeleton +hummingbird +ibis-101 +ice-cream-cone +iguana +ipod +iris +jesus-christ +joy-stick +kangaroo-101 +kayak +ketch-101 +killer-whale +knife +ladder +laptop-101 +lathe +leopards-101 +license-plate +lightbulb +light-house +lightning +llama-101 +mailbox +mandolin +mars +mattress +megaphone +menorah-101 +microscope +microwave +minaret +minotaur +motorbikes-101 +mountain-bike +mushroom +mussels +necktie +octopus +ostrich +owl +palm-pilot +palm-tree +paperclip +paper-shredder +pci-card +penguin +people +pez-dispenser +photocopier +picnic-table +playing-card +porcupine +pram +praying-mantis +pyramid +raccoon +radio-telescope +rainbow +refrigerator +revolver-101 +rifle +rotary-phone +roulette-wheel +saddle +saturn +school-bus +scorpion-101 +screwdriver +segway +self-propelled-lawn-mower +sextant +sheet-music +skateboard +skunk +skyscraper +smokestack +snail +snake +sneaker +snowmobile +soccer-ball +socks +soda-can +spaghetti +speed-boat +spider +spoon +stained-glass +starfish-101 +steering-wheel +stirrups +sunflower-101 +superman +sushi +swan +swiss-army-knife +sword +syringe +tambourine +teapot +teddy-bear +teepee +telephone-box +tennis-ball +tennis-court +tennis-racket +theodolite +toaster +tomato +tombstone +top-hat +touring-bike +tower-pisa +traffic-light +treadmill +triceratops +tricycle +trilobite-101 +tripod +t-shirt +tuning-fork +tweezer +umbrella-101 +unicorn +vcr +video-projector +washing-machine +watch-101 +waterfall +watermelon +welding-mask +wheelbarrow +windmill +wine-bottle +xylophone +yarmulke +yo-yo +zebra +airplanes-101 +car-side-101 +faces-easy-101 +greyhound +tennis-shoes +toad +clutter diff --git a/torchvision/prototype/datasets/_builtin/caltech256.py b/torchvision/prototype/datasets/_builtin/caltech256.py index b1b2c87428f..57a32a5e1b2 100644 --- a/torchvision/prototype/datasets/_builtin/caltech256.py +++ b/torchvision/prototype/datasets/_builtin/caltech256.py @@ -7,7 +7,6 @@ from torch.utils.data.datapipes.iter import ( Mapper, TarArchiveReader, - FileLoader, ) from torchvision.prototype.datasets.utils import ( @@ -16,6 +15,11 @@ DatasetInfo, HttpResource, ) +from torchvision.prototype.datasets.utils._internal import create_categories_file + +__all__ = ["Caltech256"] + +HERE = pathlib.Path(__file__).parent class Caltech256(Dataset): @@ -23,6 +27,7 @@ class Caltech256(Dataset): def info(self) -> DatasetInfo: return DatasetInfo( "caltech256", + categories=HERE / "caltech256.categories", homepage="http://www.vision.caltech.edu/Image_Datasets/Caltech256", ) @@ -63,3 +68,18 @@ def _make_datapipe( return Mapper( dp, self._collate_and_decode_sample, fn_kwargs=dict(decoder=decoder) ) + + def generate_categories_file(self, root): + dp = self.resources(self.default_config)[0].to_datapipe( + pathlib.Path(root) / self.name + ) + dp = TarArchiveReader(dp) + dir_names = {pathlib.Path(path).parent.name for path, _ in dp} + categories = [name.split(".")[1] for name in sorted(dir_names)] + create_categories_file(HERE, self.name, categories) + + +if __name__ == "__main__": + from torchvision.prototype.datasets import home + + Caltech256().generate_categories_file(home()) diff --git a/torchvision/prototype/datasets/_builtin/cifar.py b/torchvision/prototype/datasets/_builtin/cifar.py index 028cd5d5738..d8a88c142bb 100644 --- a/torchvision/prototype/datasets/_builtin/cifar.py +++ b/torchvision/prototype/datasets/_builtin/cifar.py @@ -2,8 +2,9 @@ import functools import io import os.path +import pathlib import pickle -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np import PIL.Image @@ -24,6 +25,11 @@ DatasetInfo, HttpResource, ) +from torchvision.prototype.datasets.utils._internal import create_categories_file + +__all__ = ["Cifar10", "Cifar100"] + +HERE = pathlib.Path(__file__).parent class _CifarBase(Dataset): @@ -96,12 +102,25 @@ def _make_datapipe( dp = Zipper(images_dp, labels_dp) return Mapper(dp, self._collate_and_decode, fn_kwargs=dict(decoder=decoder)) + def generate_categories_file( + self, root: Union[str, pathlib.Path], *, file_name: str, categories_key: str + ) -> None: + dp = self.resources(self.default_config)[0].to_datapipe( + pathlib.Path(root) / self.name + ) + dp = TarArchiveReader(dp) + dp = Filter(dp, lambda data: os.path.basename(data[0]) == file_name) + dp = Mapper(dp, self._unpickle) + categories = next(iter(dp))[categories_key] + create_categories_file(HERE, self.name, categories) + class Cifar10(_CifarBase): @property def info(self) -> DatasetInfo: return DatasetInfo( "cifar10", + categories=HERE / "cifar10.categories", homepage="https://www.cs.toronto.edu/~kriz/cifar.html", ) @@ -129,12 +148,24 @@ def _split_data_file(self, data: Tuple[str, Any]) -> Optional[int]: else: return None + def generate_categories_file( + self, + root: Union[str, pathlib.Path], + *, + file_name: str = "batches.meta", + categories_key: str = "label_names", + ) -> None: + super().generate_categories_file( + root, file_name=file_name, categories_key=categories_key + ) + class Cifar100(_CifarBase): @property def info(self) -> DatasetInfo: return DatasetInfo( "cifar100", + categories=HERE / "cifar100.categories", homepage="https://www.cs.toronto.edu/~kriz/cifar.html", valid_options=dict( split=("train", "test"), @@ -164,3 +195,22 @@ def _split_data_file(self, data: Tuple[str, Any]) -> Optional[int]: return 1 else: return None + + def generate_categories_file( + self, + root: Union[str, pathlib.Path], + *, + file_name: str = "meta", + categories_key: str = "fine_label_names", + ) -> None: + super().generate_categories_file( + root, file_name=file_name, categories_key=categories_key + ) + + +if __name__ == "__main__": + from torchvision.prototype.datasets import home + + root = home() + for cls in (Cifar10, Cifar100): + cls().generate_categories_file(root) diff --git a/torchvision/prototype/datasets/_builtin/cifar10.categories b/torchvision/prototype/datasets/_builtin/cifar10.categories new file mode 100644 index 00000000000..fa30c22b95d --- /dev/null +++ b/torchvision/prototype/datasets/_builtin/cifar10.categories @@ -0,0 +1,10 @@ +airplane +automobile +bird +cat +deer +dog +frog +horse +ship +truck diff --git a/torchvision/prototype/datasets/_builtin/cifar100.categories b/torchvision/prototype/datasets/_builtin/cifar100.categories new file mode 100644 index 00000000000..7f7bf51d1ab --- /dev/null +++ b/torchvision/prototype/datasets/_builtin/cifar100.categories @@ -0,0 +1,100 @@ +apple +aquarium_fish +baby +bear +beaver +bed +bee +beetle +bicycle +bottle +bowl +boy +bridge +bus +butterfly +camel +can +castle +caterpillar +cattle +chair +chimpanzee +clock +cloud +cockroach +couch +crab +crocodile +cup +dinosaur +dolphin +elephant +flatfish +forest +fox +girl +hamster +house +kangaroo +keyboard +lamp +lawn_mower +leopard +lion +lizard +lobster +man +maple_tree +motorcycle +mountain +mouse +mushroom +oak_tree +orange +orchid +otter +palm_tree +pear +pickup_truck +pine_tree +plain +plate +poppy +porcupine +possum +rabbit +raccoon +ray +road +rocket +rose +sea +seal +shark +shrew +skunk +skyscraper +snail +snake +spider +squirrel +streetcar +sunflower +sweet_pepper +table +tank +telephone +television +tiger +tractor +train +trout +tulip +turtle +wardrobe +whale +willow_tree +wolf +woman +worm diff --git a/torchvision/prototype/datasets/_builtin/mnist.py b/torchvision/prototype/datasets/_builtin/mnist.py index 426da3b03ae..68bd7f82829 100644 --- a/torchvision/prototype/datasets/_builtin/mnist.py +++ b/torchvision/prototype/datasets/_builtin/mnist.py @@ -23,8 +23,10 @@ DatasetInfo, HttpResource, OnlineResource, - image_buffer_from_array, ) +from torchvision.prototype.datasets.utils._internal import image_buffer_from_array + +__all__ = ["MNIST", "FashionMNIST", "KMNIST", "EMNIST", "QMNIST"] prod = functools.partial(functools.reduce, operator.mul) @@ -132,6 +134,7 @@ class MNIST(_MNISTBase): def info(self): return DatasetInfo( "mnist", + categories=10, homepage="http://yann.lecun.com/exdb/mnist", valid_options=dict( split=("train", "test"), @@ -163,6 +166,18 @@ class FashionMNIST(MNIST): def info(self): return DatasetInfo( "fashionmnist", + categories=[ + "T-shirt/top", + "Trouser", + "Pullover", + "Dress", + "Coat", + "Sandal", + "Shirt", + "Sneaker", + "Bag", + "Ankle boot", + ], homepage="https://github.com/zalandoresearch/fashion-mnist", valid_options=dict( split=("train", "test"), @@ -183,6 +198,7 @@ class KMNIST(MNIST): def info(self): return DatasetInfo( "kmnist", + categories=["o", "ki", "su", "tsu", "na", "ha", "ma", "ya", "re", "wo"], homepage="http://codh.rois.ac.jp/kmnist/index.html.en", valid_options=dict( split=("train", "test"), @@ -198,11 +214,16 @@ def info(self): } +import string + + class EMNIST(_MNISTBase): @property def info(self): return DatasetInfo( "emnist", + # FIXME: shift the labels at runtime to always produce this + categories=list(string.digits + string.ascii_letters), homepage="https://www.westernsydney.edu.au/icns/reproducible_research/publication_support_materials/emnist", valid_options=dict( split=("train", "test"), @@ -276,6 +297,7 @@ class QMNIST(_MNISTBase): def info(self): return DatasetInfo( "qmnist", + categories=10, homepage="https://github.com/facebookresearch/qmnist", valid_options=dict( split=("train", "test", "test10k", "test50k", "nist"), diff --git a/torchvision/prototype/datasets/_home.py b/torchvision/prototype/datasets/_home.py new file mode 100644 index 00000000000..333d570b9e1 --- /dev/null +++ b/torchvision/prototype/datasets/_home.py @@ -0,0 +1,21 @@ +import os +import pathlib +from typing import Optional, Union + +from torch.hub import _get_torch_home + +# TODO: This needs a better default +HOME = pathlib.Path(_get_torch_home()) / "datasets" / "vision" + + +def home(home: Optional[Union[str, pathlib.Path]] = None) -> pathlib.Path: + global HOME + if home is not None: + HOME = pathlib.Path(home).expanduser().resolve() + return HOME + + home = os.getenv("TORCHVISION_DATASETS_HOME") + if home is not None: + return pathlib.Path(home) + + return HOME diff --git a/torchvision/prototype/datasets/utils/__init__.py b/torchvision/prototype/datasets/utils/__init__.py index ee45837a255..114e6507fdb 100644 --- a/torchvision/prototype/datasets/utils/__init__.py +++ b/torchvision/prototype/datasets/utils/__init__.py @@ -1,5 +1,3 @@ from ._dataset import * -from ._helpers import * from ._resource import * - from . import _internal diff --git a/torchvision/prototype/datasets/utils/_dataset.py b/torchvision/prototype/datasets/utils/_dataset.py index 2b0b51623ea..2226fc7956f 100644 --- a/torchvision/prototype/datasets/utils/_dataset.py +++ b/torchvision/prototype/datasets/utils/_dataset.py @@ -100,12 +100,21 @@ def __init__( self, name: str, *, + categories: Union[int, Sequence[str], str, pathlib.Path], citation: Optional[str] = None, homepage: Optional[str] = None, license: Optional[str] = None, valid_options: Optional[Dict[str, Sequence[Any]]] = None, ) -> None: self.name = name.lower() + + if isinstance(categories, int): + categories = [str(label) for label in range(categories)] + elif isinstance(categories, (str, pathlib.Path)): + with open(pathlib.Path(categories).expanduser().resolve(), "r") as fh: + categories = fh.readlines() + self.categories = categories + self.citation = citation self.homepage = homepage self.license = license diff --git a/torchvision/prototype/datasets/utils/_helpers.py b/torchvision/prototype/datasets/utils/_helpers.py deleted file mode 100644 index 23df92b786e..00000000000 --- a/torchvision/prototype/datasets/utils/_helpers.py +++ /dev/null @@ -1,26 +0,0 @@ -import io -from typing import Any, Tuple - -import numpy as np -import PIL.Image - -__all__ = ["read_mat", "image_buffer_from_array"] - - -def read_mat(file: io.BufferedIOBase, **kwargs: Any) -> Any: - try: - import scipy.io as sio - except ImportError as error: - raise ModuleNotFoundError( - "Package `scipy` is required to be installed to read .mat files." - ) from error - - return sio.loadmat(file, **kwargs) - - -def image_buffer_from_array(array: np.array, *, format: str) -> Tuple[str, io.BytesIO]: - image = PIL.Image.fromarray(array) - buffer = io.BytesIO() - image.save(buffer, format=format) - buffer.seek(0) - return f"tmp.{format}", buffer diff --git a/torchvision/prototype/datasets/utils/_internal.py b/torchvision/prototype/datasets/utils/_internal.py index af0b87e9be3..8cfbe40d7db 100644 --- a/torchvision/prototype/datasets/utils/_internal.py +++ b/torchvision/prototype/datasets/utils/_internal.py @@ -1,6 +1,40 @@ import collections.abc import difflib +import io +import pathlib +from typing import Any, Tuple from typing import Collection, Sequence, Callable +from typing import Union + +import numpy as np +import PIL.Image + +__all__ = [ + "read_mat", + "image_buffer_from_array", + "sequence_to_str", + "add_suggestion", + "create_categories_file", +] + + +def read_mat(file: io.BufferedIOBase, **kwargs: Any) -> Any: + try: + import scipy.io as sio + except ImportError as error: + raise ModuleNotFoundError( + "Package `scipy` is required to be installed to read .mat files." + ) from error + + return sio.loadmat(file, **kwargs) + + +def image_buffer_from_array(array: np.array, *, format: str) -> Tuple[str, io.BytesIO]: + image = PIL.Image.fromarray(array) + buffer = io.BytesIO() + image.save(buffer, format=format) + buffer.seek(0) + return f"tmp.{format}", buffer def sequence_to_str(seq: Sequence, separate_last: str = "") -> str: @@ -34,3 +68,10 @@ def add_suggestion( else alternative_hint(possibilities) ) return f"{msg.strip()} {hint}" + + +def create_categories_file( + root: Union[str, pathlib.Path], name: str, categories: Sequence[str] +) -> None: + with open(root / f"{name}.categories", "w") as fh: + fh.write("\n".join(categories) + "\n") diff --git a/torchvision/prototype/datasets/utils/_resource.py b/torchvision/prototype/datasets/utils/_resource.py index 27d274c996a..3ea4a79ff0c 100644 --- a/torchvision/prototype/datasets/utils/_resource.py +++ b/torchvision/prototype/datasets/utils/_resource.py @@ -33,8 +33,9 @@ def __init__(self, url: str, *, sha256: str, file_name: str) -> None: self.file_name = file_name def to_datapipe(self, root: Union[str, pathlib.Path]) -> IterDataPipe: + path = (pathlib.Path(root) / self.file_name).expanduser().resolve() # FIXME - return FileLoader(IterableWrapper((str(pathlib.Path(root) / self.file_name),))) + return FileLoader(IterableWrapper((str(path),))) # TODO: add support for mirrors