# # SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Copyright 2018 & 2016 The TensorFlow Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Changes made by NVIDIA (2022): - Added: load_data_tfrecord_tf() and load_data() functions - Modified preprocess_image_record(): preprocess_image() + tfrecord data deserialization + decode jpeg - Updated global constants with supported models: _DEFAULT_IMAGE_SIZE and _RESIZE_MIN About this file: Standalone script for ImageNet TFRecord data loading and input image pre-processing for supported models. Follows TensorFlow's codebase data_loading + pre-processing workflow. Important links: - TF's codebase: https://github.com/tensorflow/models/blob/master/official/legacy/image_classification/resnet/imagenet_preprocessing.py - Deserialize tfrecord: https://github.com/tensorflow/models/blob/master/official/vision/dataloaders/tf_example_decoder.py """ import os import tensorflow as tf import PIL.Image import numpy as np from typing import Dict, Union _SUPPORTED_MODEL_NAMES = [ "resnet_v1", "resnet_v2", "efficientnet_b0", "efficientnet_b3", "mobilenet_v1", "mobilenet_v2", "inception_v3", ] _NUM_CLASSES = 1000 _NUM_IMAGES = { "train": 1281167, "validation": 50000, } _DEFAULT_IMAGE_SIZE = { "resnet_v1": 224, "resnet_v2": 299, "efficientnet_b0": 224, "efficientnet_b3": 300, "mobilenet_v1": 224, "mobilenet_v2": 224, "inception_v3": 299, } _NUM_CHANNELS = 3 _RESIZE_MIN = { "resnet_v1": 256, "resnet_v2": 342, "efficientnet_b0": 256, "efficientnet_b3": 342, "mobilenet_v1": 256, "mobilenet_v2": 256, "inception_v3": 342, } def load_image_np(test_image, model_name: str = "resnet_v1"): # Image is loaded in NHWC format image_np = np.asarray(PIL.Image.open(test_image).convert('RGB')) image = tf.constant(image_np) image = _aspect_preserving_resize(image, _RESIZE_MIN[model_name]) image = _central_crop(image, _DEFAULT_IMAGE_SIZE[model_name], _DEFAULT_IMAGE_SIZE[model_name]) image = preprocess_model_func(image, model_name) return image def get_filenames( data_dir: str, is_training: bool = False, num_train_files: int = 1024, num_val_files: int = 128, ): """ Returns filenames for dataset. Args: data_dir (str): directory where data is stored. is_training (bool): indicates whether to return the 'train' (True) or 'validation' (False) data filenames. num_train_files (int): number of tfrecord shards available for training. num_val_files (int): number of tfrecord shards available for validation. Returns: List: list of shards filenames to compose the dataset. """ if is_training: return [ # Example: train-00000-of-01024 os.path.join(data_dir, "train-{:05d}-of-{:05d}".format(i, num_train_files)) for i in range(num_train_files) ] else: return [ os.path.join( data_dir, "validation-{:05d}-of-{:05d}".format(i, num_val_files) ) for i in range(num_val_files) ] def _deserialize_image_record(record): feature_map = { "image/encoded": tf.io.FixedLenFeature([], tf.string, ""), "image/class/label": tf.io.FixedLenFeature([], tf.int64, -1), "image/class/text": tf.io.FixedLenFeature([], tf.string, ""), "image/object/bbox/xmin": tf.io.VarLenFeature(dtype=tf.float32), "image/object/bbox/ymin": tf.io.VarLenFeature(dtype=tf.float32), "image/object/bbox/xmax": tf.io.VarLenFeature(dtype=tf.float32), "image/object/bbox/ymax": tf.io.VarLenFeature(dtype=tf.float32), } with tf.name_scope("deserialize_image_record"): obj = tf.io.parse_single_example(record, feature_map) imgdata = obj["image/encoded"] label = tf.cast(obj["image/class/label"], tf.int32) bbox = tf.stack( [ obj["image/object/bbox/%s" % x].values for x in ["ymin", "xmin", "ymax", "xmax"] ] ) bbox = tf.transpose(tf.expand_dims(bbox, 0), [0, 2, 1]) text = obj["image/class/text"] return imgdata, label, bbox, text def _aspect_preserving_resize(image: tf.Tensor, resize_min: Union[int, tf.Tensor]): """Resize images preserving the original aspect ratio. Args: image (tf.Tensor): A 3-D image `Tensor`. resize_min (int): A python integer or scalar `Tensor` indicating the size of the smallest side after resize. Returns: resized_image (tf.Tensor): A 3-D `Tensor` containing the resized image. """ shape = tf.shape(image) height, width = shape[0], shape[1] new_height, new_width = _smallest_size_at_least(height, width, resize_min) resized_image = tf.image.resize( image, [new_height, new_width], method=tf.image.ResizeMethod.BILINEAR ) return resized_image def _smallest_size_at_least(height, width, resize_min): resize_min = tf.cast(resize_min, tf.float32) # Convert to floats to make subsequent calculations go smoothly. height, width = tf.cast(height, tf.float32), tf.cast(width, tf.float32) smaller_dim = tf.minimum(height, width) scale_ratio = resize_min / smaller_dim # Convert back to ints to make heights and widths that TF ops will accept. new_height = tf.cast(height * scale_ratio, tf.int32) new_width = tf.cast(width * scale_ratio, tf.int32) return new_height, new_width def _central_crop(image, crop_height, crop_width): shape = tf.shape(image) height, width = shape[0], shape[1] amount_to_be_cropped_h = height - crop_height crop_top = amount_to_be_cropped_h // 2 amount_to_be_cropped_w = width - crop_width crop_left = amount_to_be_cropped_w // 2 return tf.slice(image, [crop_top, crop_left, 0], [crop_height, crop_width, -1]) def preprocess_image_record(record, min_size=256, image_height=224, image_width=224): """ This function performs image cropping so all images in the dataset have the same height and width dimensions. No value pre-processing is done here. """ imgdata, label, _, _ = _deserialize_image_record(record) # Subtract one so that ImageNet labels are in [0, 1000). This assumes your dataset contains 'background' as 0. label -= 1 try: image = tf.image.decode_jpeg( imgdata, channels=_NUM_CHANNELS, fancy_upscaling=False, dct_method="INTEGER_FAST", ) except: image = tf.image.decode_image(imgdata, channels=_NUM_CHANNELS) image = tf.cast(image, tf.float32) image = _aspect_preserving_resize(image, min_size) image = _central_crop(image, image_height, image_width) return image, label def preprocess_model_func(image: tf.Tensor, model_name: str = "resnet_v1"): if model_name == "resnet_v1": return tf.keras.applications.resnet.preprocess_input(image) elif model_name == "resnet_v2": return tf.keras.applications.resnet_v2.preprocess_input(image) elif model_name == "mobilenet_v1": return tf.keras.applications.mobilenet.preprocess_input(image) elif model_name == "mobilenet_v2": return tf.keras.applications.mobilenet_v2.preprocess_input(image) elif model_name == "inception_v3": return tf.keras.applications.inception_v3.preprocess_input(image) else: # efficientnet doesn't need specific pre-processing (included in the model itself). print("No further pre-processing found for {}".format(model_name)) return image def load_data_tfrecord_tf( data_dir: str = "./data/imagenet", batch_size: int = 8, num_train_files: int = 1024, num_val_files: int = 128, model_name: str = "resnet_v1", ) -> Dict[str, tf.data.Dataset]: """ Load ImageNet with TensorFlow Datasets (TFDS). Args: data_dir (str): directory where data is stored. batch_size (int): batch_size for dataloader. num_train_files (int): number of tfrecord shards available for training. num_val_files (int): number of tfrecord shards available for validation. model_name (str): Model name, used to decide which input pre-processing is needed. Options={supported_model_names}. Returns: dataset_dict (Dict[str, tf.data.Dataset]): dictionary with 'train' and 'validation' datasets. Raises: ValueError: raised if 'model_name' is not supported. """.format( supported_model_names=_SUPPORTED_MODEL_NAMES ) # 1. Load ImageNet2012 train dataset - needs to manually download the full ImageNet2012 dataset first. assert os.path.exists(data_dir) if model_name not in _SUPPORTED_MODEL_NAMES: raise ValueError( "Invalid model name ", model_name, " provided. Please select among {}".format(_SUPPORTED_MODEL_NAMES), ) # 2. Make train/validation datasets dataset_dict = {} for key, is_training in zip(["train", "validation"], [True, False]): filenames = get_filenames( data_dir, is_training=is_training, num_train_files=num_train_files, num_val_files=num_val_files, ) dataset = tf.data.TFRecordDataset(filenames) # Image cropping and resizing if model_name in _DEFAULT_IMAGE_SIZE and model_name in _RESIZE_MIN: dataset = dataset.map( lambda record: preprocess_image_record( record, min_size=_RESIZE_MIN[model_name], image_height=_DEFAULT_IMAGE_SIZE[model_name], image_width=_DEFAULT_IMAGE_SIZE[model_name], ) ) else: dataset = dataset.map(preprocess_image_record) dataset = dataset.map(lambda image, label: (preprocess_model_func(image, model_name), label)) # Divide dataset into batches dataset = dataset.batch(batch_size, drop_remainder=True) dataset_dict[key] = dataset return dataset_dict def load_data( hyperparams: Dict, model_name: str = "resnet_v1" ) -> [tf.data.Dataset, tf.data.Dataset]: """ Loads ImageNet data in `tfrecord` format (requires manual data download). Args: hyperparams (Dict): dictionary with necessary hyper-parameters for data loading. model_name (str): Model name, used to decide which input pre-processing is needed. Options={supported_model_names}. Returns: train_batches (tf.data.Dataset): 'train' dataset. val_batches (tf.data.Dataset): 'validation' dataset. """.format( supported_model_names=_SUPPORTED_MODEL_NAMES ) data_batches = load_data_tfrecord_tf( data_dir=hyperparams["tfrecord_data_dir"], batch_size=hyperparams["batch_size"], model_name=model_name, ) train_batches, val_batches = (data_batches["train"], data_batches["validation"]) if hyperparams["train_data_size"] is not None: train_batches = train_batches.take(hyperparams["train_data_size"]) if hyperparams["val_data_size"] is not None: val_batches = val_batches.take(hyperparams["val_data_size"]) return train_batches, val_batches