Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/es-419/tutorials/load_data/video.ipynb
25118 views
Kernel: Python 3
#@title 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 # # https://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.

Carga de datos de video

En este tutorial se demuestra cómo cargar y preprocesar datos de video AVI con el conjunto de datos de acciones humanas UCF101. Una vez que haya preprocesado los datos, se pueden usar para tareas como la clasificación, el reconocimiento, el subtitulado o la agrupación de videos. El conjunto de datos original contiene videos de acción realistas recopilados de YouTube con 101 categorías que incluyen tocar el violonchelo, lavarse los dientes y maquillarse. Aprenderá lo siguiente:

  • Cargue los datos de un archivo zip.

  • Lea las secuencias de cuadros de los archivos de video.

  • Visualice los datos de video.

  • Encapsule el generador de cuadros tf.data.Dataset.

Este tutorial de carga y preprocesamiento de videos es la primera parte de una serie de tutoriales sobre videos de TensorFlow. A continuación, compartimos otros tres tutoriales:

Preparación

Comience por instalar e importar algunas de las bibliotecas necesarias, incluidas: remotezip, para inspeccionar el contenido de un archivo ZIP; tqdm, para usar la barra de progreso; OpenCV, para procesar archivos de video; y tensorflow_docs, para incorporar datos de video en un cuaderno Jupyter.

# The way this tutorial uses the `TimeDistributed` layer requires TF>=2.10 !pip install -U "tensorflow>=2.10.0"
!pip install remotezip tqdm opencv-python !pip install -q git+https://github.com/tensorflow/docs
import tqdm import random import pathlib import itertools import collections import os import cv2 import numpy as np import remotezip as rz import tensorflow as tf # Some modules to display an animation using imageio. import imageio from IPython import display from urllib import request from tensorflow_docs.vis import embed

Descarga de un subconjunto del conjunto de datos UCF101

El conjunto de datos UCF101 contiene 101 categorías de acciones diferentes en video, principalmente usadas para reconocimiento de acciones. En este demo se usará un subconjunto de estas categorías.

URL = 'https://storage.googleapis.com/thumos14_files/UCF101_videos.zip'

La URL de aquí arriba lleva a un archivo zip con el conjunto de datos UCF 101. Cree una función que use la biblioteca remotezip para examinar el contenido del archivo zip de esa URL:

def list_files_from_zip_url(zip_url): """ List the files in each class of the dataset given a URL with the zip file. Args: zip_url: A URL from which the files can be extracted from. Returns: List of files in each of the classes. """ files = [] with rz.RemoteZip(zip_url) as zip: for zip_info in zip.infolist(): files.append(zip_info.filename) return files
files = list_files_from_zip_url(URL) files = [f for f in files if f.endswith('.avi')] files[:10]

Empiece con algunos videos y una cantidad limitada de clases para el entrenamiento. Después de ejecutar el bloque de código de arriba, notará que el nombre de la clase se incluye en el nombre del archivo de cada video.

Defina la función get_class que obtiene el nombre de la clase a partir del nombre del archivo. Después, cree una función llamada get_files_per_class que convierte la lista de todos los archivos (arriba, files) en un diccionario en el que se enumeran los archivos para cada clase:

def get_class(fname): """ Retrieve the name of the class given a filename. Args: fname: Name of the file in the UCF101 dataset. Returns: Class that the file belongs to. """ return fname.split('_')[-3]
def get_files_per_class(files): """ Retrieve the files that belong to each class. Args: files: List of files in the dataset. Returns: Dictionary of class names (key) and files (values). """ files_for_class = collections.defaultdict(list) for fname in files: class_name = get_class(fname) files_for_class[class_name].append(fname) return files_for_class

Una vez que tenga la lista de archivos por clase, podrá elegir cuántas clases quisiera usar y cuántos videos desearía conservar por clase para crear su conjunto de datos.

NUM_CLASSES = 10 FILES_PER_CLASS = 50
files_for_class = get_files_per_class(files) classes = list(files_for_class.keys())
print('Num classes:', len(classes)) print('Num videos for class[0]:', len(files_for_class[classes[0]]))

Cree una función nueva denominada select_subset_of_classes con la que se seleccione un subconjunto de las clases presentes en el conjunto de datos y una cantidad particular de archivos por clase:

def select_subset_of_classes(files_for_class, classes, files_per_class): """ Create a dictionary with the class name and a subset of the files in that class. Args: files_for_class: Dictionary of class names (key) and files (values). classes: List of classes. files_per_class: Number of files per class of interest. Returns: Dictionary with class as key and list of specified number of video files in that class. """ files_subset = dict() for class_name in classes: class_files = files_for_class[class_name] files_subset[class_name] = class_files[:files_per_class] return files_subset
files_subset = select_subset_of_classes(files_for_class, classes[:NUM_CLASSES], FILES_PER_CLASS) list(files_subset.keys())

Defina las funciones ayudante que separan los videos en los conjuntos de entrenamiento, validación y prueba. Los videos se descargan de una URL en la que está el archivo zip que los contiene y se colocan en sus respectivos subdirectorios.

def download_from_zip(zip_url, to_dir, file_names): """ Download the contents of the zip file from the zip URL. Args: zip_url: A URL with a zip file containing data. to_dir: A directory to download data to. file_names: Names of files to download. """ with rz.RemoteZip(zip_url) as zip: for fn in tqdm.tqdm(file_names): class_name = get_class(fn) zip.extract(fn, str(to_dir / class_name)) unzipped_file = to_dir / class_name / fn fn = pathlib.Path(fn).parts[-1] output_file = to_dir / class_name / fn unzipped_file.rename(output_file)

La siguiente función devuelve los datos restantes que todavía no se han colocado en un subconjunto de datos. Le permitirá colocar esos datos sobrantes en el siguiente subconjunto especificado de datos.

def split_class_lists(files_for_class, count): """ Returns the list of files belonging to a subset of data as well as the remainder of files that need to be downloaded. Args: files_for_class: Files belonging to a particular class of data. count: Number of files to download. Returns: Files belonging to the subset of data and dictionary of the remainder of files that need to be downloaded. """ split_files = [] remainder = {} for cls in files_for_class: split_files.extend(files_for_class[cls][:count]) remainder[cls] = files_for_class[cls][count:] return split_files, remainder

La siguiente función download_ucf_101_subset le permitirá bajar un subconjunto del conjunto de datos UCF101 y separarlo en los conjuntos de entrenamiento, validación y prueba. Puede especificar la cantidad de clases que quisiera usar. El argumento splits le permitirá pasar un diccionario en el que los valores clave son el nombre del subconjunto (p. ej., "entrenamiento") y la cantidad de videos que desearía tener por clase.

def download_ucf_101_subset(zip_url, num_classes, splits, download_dir): """ Download a subset of the UCF101 dataset and split them into various parts, such as training, validation, and test. Args: zip_url: A URL with a ZIP file with the data. num_classes: Number of labels. splits: Dictionary specifying the training, validation, test, etc. (key) division of data (value is number of files per split). download_dir: Directory to download data to. Return: Mapping of the directories containing the subsections of data. """ files = list_files_from_zip_url(zip_url) for f in files: path = os.path.normpath(f) tokens = path.split(os.sep) if len(tokens) <= 2: files.remove(f) # Remove that item from the list if it does not have a filename files_for_class = get_files_per_class(files) classes = list(files_for_class.keys())[:num_classes] for cls in classes: random.shuffle(files_for_class[cls]) # Only use the number of classes you want in the dictionary files_for_class = {x: files_for_class[x] for x in classes} dirs = {} for split_name, split_count in splits.items(): print(split_name, ":") split_dir = download_dir / split_name split_files, files_for_class = split_class_lists(files_for_class, split_count) download_from_zip(zip_url, split_dir, split_files) dirs[split_name] = split_dir return dirs
download_dir = pathlib.Path('./UCF101_subset/') subset_paths = download_ucf_101_subset(URL, num_classes = NUM_CLASSES, splits = {"train": 30, "val": 10, "test": 10}, download_dir = download_dir)

Después de descargar los datos, ahora, debería tener una copia de un subconjunto proveniente del conjunto UCF101. Ejecute el código que se encuentra a continuación para imprimir la cantidad total de videos que tiene entre todos los subconjuntos de datos.

video_count_train = len(list(download_dir.glob('train/*/*.avi'))) video_count_val = len(list(download_dir.glob('val/*/*.avi'))) video_count_test = len(list(download_dir.glob('test/*/*.avi'))) video_total = video_count_train + video_count_val + video_count_test print(f"Total videos: {video_total}")

Ahora también puede acceder a una vista previa del directorio en el que se encuentran los archivos de datos.

!find ./UCF101_subset

Creación de cuadros a partir de cada uno de los archivos de video

La función frames_from_video_file separa los videos en cuadros, lee un grupo de n_frames elegidos al azar de un archivo de video y los devuelve como un array NumPy. Para reducir la superposición de cálculos y memoria, elija una pequeña cantidad de cuadros. Además, elija la misma cantidad de cuadros de cada uno de los videos. De este modo, el trabajo con los lotes de datos será más sencillo.

def format_frames(frame, output_size): """ Pad and resize an image from a video. Args: frame: Image that needs to resized and padded. output_size: Pixel size of the output frame image. Return: Formatted frame with padding of specified output size. """ frame = tf.image.convert_image_dtype(frame, tf.float32) frame = tf.image.resize_with_pad(frame, *output_size) return frame
def frames_from_video_file(video_path, n_frames, output_size = (224,224), frame_step = 15): """ Creates frames from each video file present for each category. Args: video_path: File path to the video. n_frames: Number of frames to be created per video file. output_size: Pixel size of the output frame image. Return: An NumPy array of frames in the shape of (n_frames, height, width, channels). """ # Read each video frame by frame result = [] src = cv2.VideoCapture(str(video_path)) video_length = src.get(cv2.CAP_PROP_FRAME_COUNT) need_length = 1 + (n_frames - 1) * frame_step if need_length > video_length: start = 0 else: max_start = video_length - need_length start = random.randint(0, max_start + 1) src.set(cv2.CAP_PROP_POS_FRAMES, start) # ret is a boolean indicating whether read was successful, frame is the image itself ret, frame = src.read() result.append(format_frames(frame, output_size)) for _ in range(n_frames - 1): for _ in range(frame_step): ret, frame = src.read() if ret: frame = format_frames(frame, output_size) result.append(frame) else: result.append(np.zeros_like(result[0])) src.release() result = np.array(result)[..., [2, 1, 0]] return result

Visualización de los datos de video

La función frames_from_video_file devuelve un conjunto de cuadros como un arreglo NumPy array. Intente usar esta función con un video de Wikimedia{:.external} de Patrick Gillett:

!curl -O https://upload.wikimedia.org/wikipedia/commons/8/86/End_of_a_jam.ogv
video_path = "End_of_a_jam.ogv"
sample_video = frames_from_video_file(video_path, n_frames = 10) sample_video.shape
def to_gif(images): converted_images = np.clip(images * 255, 0, 255).astype(np.uint8) imageio.mimsave('./animation.gif', converted_images, fps=10) return embed.embed_file('./animation.gif')
to_gif(sample_video)

Además de examinar este video, podrá ver los datos de UCF-101. Para hacerlo, ejecute el siguiente código:

# docs-infra: no-execute ucf_sample_video = frames_from_video_file(next(subset_paths['train'].glob('*/*.avi')), 50) to_gif(ucf_sample_video)

A continuación, defina la clase FrameGenerator para crear un objeto iterable que pueda introducir los datos en la canalización de datos de TensorFlow. La función (__call__) de generador arroja un arreglo de cuadros producido por frames_from_video_file y un vector codificado en un solo paso (one-hot) de la etiqueta asociada al conjunto de cuadros.

class FrameGenerator: def __init__(self, path, n_frames, training = False): """ Returns a set of frames with their associated label. Args: path: Video file paths. n_frames: Number of frames. training: Boolean to determine if training dataset is being created. """ self.path = path self.n_frames = n_frames self.training = training self.class_names = sorted(set(p.name for p in self.path.iterdir() if p.is_dir())) self.class_ids_for_name = dict((name, idx) for idx, name in enumerate(self.class_names)) def get_files_and_class_names(self): video_paths = list(self.path.glob('*/*.avi')) classes = [p.parent.name for p in video_paths] return video_paths, classes def __call__(self): video_paths, classes = self.get_files_and_class_names() pairs = list(zip(video_paths, classes)) if self.training: random.shuffle(pairs) for path, name in pairs: video_frames = frames_from_video_file(path, self.n_frames) label = self.class_ids_for_name[name] # Encode labels yield video_frames, label

Antes de encapsular el objeto FrameGenerator como un conjunto de datos de TensorFlow Dataset, haga las pruebas correspondientes. También, verifique que el modo de entrenamiento esté activado para el conjunto de datos de entrenamiento, a fin de que los datos se puedan aleatorizar.

fg = FrameGenerator(subset_paths['train'], 10, training=True) frames, label = next(fg()) print(f"Shape: {frames.shape}") print(f"Label: {label}")

Finalmente, cree una canalización de entrada de datos de TensorFlow. Esta canalización que cree a partir del objeto generador le permitirá introducir los datos en su modelo de aprendizaje profundo. En esta canalización de video, cada elemento es un único conjunto de cuadros con su etiqueta asociada.

# Create the training set output_signature = (tf.TensorSpec(shape = (None, None, None, 3), dtype = tf.float32), tf.TensorSpec(shape = (), dtype = tf.int16)) train_ds = tf.data.Dataset.from_generator(FrameGenerator(subset_paths['train'], 10, training=True), output_signature = output_signature)

Controle que las etiquetas sean aleatorias.

for frames, labels in train_ds.take(10): print(labels)
# Create the validation set val_ds = tf.data.Dataset.from_generator(FrameGenerator(subset_paths['val'], 10), output_signature = output_signature)
# Print the shapes of the data train_frames, train_labels = next(iter(train_ds)) print(f'Shape of training set of frames: {train_frames.shape}') print(f'Shape of training labels: {train_labels.shape}') val_frames, val_labels = next(iter(val_ds)) print(f'Shape of validation set of frames: {val_frames.shape}') print(f'Shape of validation labels: {val_labels.shape}')

Configuración del conjunto de datos para rendimiento

Use una preextracción almacenada en el búfer para que pueda producir datos desde el disco sin provocar un bloqueo en la entrada ni en la salida. Hay dos funciones importantes que habría que usar al cargar los datos:

  • Dataset.cache conserva los datos en la memoria después de que se carga desde el disco durante la primera época. Así se garantiza que el conjunto de datos no forme un cuello de botella mientras entrena su modelo. Si el conjunto de datos es muy grande como para guardarlo en la memoria, también puede usar este método para crear un caché en disco de alto rendimiento.

  • Dataset.prefetch: superpone el preprocesamiento de datos y la ejecución del modelo mientras se entrena. Para más detalles, consulte la información sobre mejor rendimiento con tf.data.

AUTOTUNE = tf.data.AUTOTUNE train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size = AUTOTUNE) val_ds = val_ds.cache().shuffle(1000).prefetch(buffer_size = AUTOTUNE)

Para preparar los datos con que se alimentará el modelo, use la agrupación en lotes como se muestra a continuación. Advierta que cuando se trabaja con datos de video, como con los archivos AVI, los datos deberían tomar la forma de un objeto de cinco dimensiones. Esas dimensiones son las siguientes: [batch_size, number_of_frames, height, width, channels]. Si comparamos, una imagen tendría cuatro dimensiones: [batch_size, height, width, channels]. La siguiente imagen es una ilustración de cómo se representa la forma de los datos de video.

Forma de datos de video

train_ds = train_ds.batch(2) val_ds = val_ds.batch(2) train_frames, train_labels = next(iter(train_ds)) print(f'Shape of training set of frames: {train_frames.shape}') print(f'Shape of training labels: {train_labels.shape}') val_frames, val_labels = next(iter(val_ds)) print(f'Shape of validation set of frames: {val_frames.shape}') print(f'Shape of validation labels: {val_labels.shape}')

Próximos pasos

Ahora que ya ha creado un Dataset de TensorFlow de cuadros de video, podrá usarlo con un modelo de aprendizaje profundo. El siguiente modelo de clasificación que usa una EfficientNet{:.external} previamente entrenada, entrena con gran exactitud en unos pocos minutos:

net = tf.keras.applications.EfficientNetB0(include_top = False) net.trainable = False model = tf.keras.Sequential([ tf.keras.layers.Rescaling(scale=255), tf.keras.layers.TimeDistributed(net), tf.keras.layers.Dense(10), tf.keras.layers.GlobalAveragePooling3D() ]) model.compile(optimizer = 'adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits = True), metrics=['accuracy']) model.fit(train_ds, epochs = 10, validation_data = val_ds, callbacks = tf.keras.callbacks.EarlyStopping(patience = 2, monitor = 'val_loss'))