laywerrobot/lib/python3.6/site-packages/tensorboard/plugins/beholder/video_writing.py
2020-08-27 21:55:39 +02:00

200 lines
6.1 KiB
Python

# Copyright 2017 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.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import abc
import os
import subprocess
import time
import numpy as np
import tensorflow as tf
from tensorboard.plugins.beholder import im_util
class VideoWriter(object):
"""Video file writer that can use different output types.
Each VideoWriter instance writes video files to a specified directory, using
the first available VideoOutput from the provided list.
"""
def __init__(self, directory, outputs):
self.directory = directory
# Filter to the available outputs
self.outputs = [out for out in outputs if out.available()]
if not self.outputs:
raise IOError('No available video outputs')
self.output_index = 0
self.output = None
self.frame_shape = None
def current_output(self):
return self.outputs[self.output_index]
def write_frame(self, np_array):
# Reset whenever we encounter a new frame shape.
if self.frame_shape != np_array.shape:
if self.output:
self.output.close()
self.output = None
self.frame_shape = np_array.shape
tf.logging.info('Starting video with frame shape: %s', self.frame_shape)
# Write the frame, advancing across output types as necessary.
original_output_index = self.output_index
for self.output_index in range(original_output_index, len(self.outputs)):
try:
if not self.output:
new_output = self.outputs[self.output_index]
if self.output_index > original_output_index:
tf.logging.warn(
'Falling back to video output %s', new_output.name())
self.output = new_output(self.directory, self.frame_shape)
self.output.emit_frame(np_array)
return
except (IOError, OSError) as e:
tf.logging.warn(
'Video output type %s not available: %s',
self.current_output().name(), str(e))
if self.output:
self.output.close()
self.output = None
raise IOError('Exhausted available video outputs')
def finish(self):
if self.output:
self.output.close()
self.output = None
self.frame_shape = None
# Reconsider failed outputs when video is manually restarted.
self.output_index = 0
class VideoOutput(object):
"""Base class for video outputs supported by VideoWriter."""
__metaclass__ = abc.ABCMeta
# Would add @abc.abstractmethod in python 3.3+
@classmethod
def available(cls):
raise NotImplementedError()
@classmethod
def name(cls):
return cls.__name__
@abc.abstractmethod
def emit_frame(self, np_array):
raise NotImplementedError()
@abc.abstractmethod
def close(self):
raise NotImplementedError()
class PNGVideoOutput(VideoOutput):
"""Video output implemented by writing individual PNGs to disk."""
@classmethod
def available(cls):
return True
def __init__(self, directory, frame_shape):
del frame_shape # unused
self.directory = directory + '/video-frames-{}'.format(time.time())
self.frame_num = 0
tf.gfile.MakeDirs(self.directory)
def emit_frame(self, np_array):
filename = self.directory + '/{:05}.png'.format(self.frame_num)
im_util.write_image(np_array.astype(np.uint8), filename)
self.frame_num += 1
def close(self):
pass
class FFmpegVideoOutput(VideoOutput):
"""Video output implemented by streaming to FFmpeg with .mp4 output."""
@classmethod
def available(cls):
# Silently check if ffmpeg is available.
try:
with open(os.devnull, 'wb') as devnull:
subprocess.check_call(
['ffmpeg', '-version'], stdout=devnull, stderr=devnull)
return True
except (OSError, subprocess.CalledProcessError):
return False
def __init__(self, directory, frame_shape):
self.filename = directory + '/video-{}.webm'.format(time.time())
if len(frame_shape) != 3:
raise ValueError(
'Expected rank-3 array for frame, got %s' % str(frame_shape))
# Set input pixel format based on channel count.
if frame_shape[2] == 1:
pix_fmt = 'gray'
elif frame_shape[2] == 3:
pix_fmt = 'rgb24'
else:
raise ValueError('Unsupported channel count %d' % frame_shape[2])
command = [
'ffmpeg',
'-y', # Overwite output
# Input options - raw video file format and codec.
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-s', '%dx%d' % (frame_shape[1], frame_shape[0]), # Width x height.
'-pix_fmt', pix_fmt,
'-r', '15', # Frame rate: arbitrarily use 15 frames per second.
'-i', '-', # Use stdin.
'-an', # No audio.
# Output options - use lossless VP9 codec inside .webm.
'-vcodec', 'libvpx-vp9',
'-lossless', '1',
# Using YUV is most compatible, though conversion from RGB skews colors.
'-pix_fmt', 'yuv420p',
self.filename
]
PIPE = subprocess.PIPE
self.ffmpeg = subprocess.Popen(
command, stdin=PIPE, stdout=PIPE, stderr=PIPE)
def _handle_error(self):
_, stderr = self.ffmpeg.communicate()
bar = '=' * 40
tf.logging.error(
'Error writing to FFmpeg:\n%s\n%s\n%s', bar, stderr.rstrip('\n'), bar)
def emit_frame(self, np_array):
try:
self.ffmpeg.stdin.write(np_array.tobytes())
self.ffmpeg.stdin.flush()
except IOError:
self._handle_error()
raise IOError('Failure invoking FFmpeg')
def close(self):
if self.ffmpeg.poll() is None:
# Close stdin and consume and discard stderr/stdout.
self.ffmpeg.communicate()
self.ffmpeg = None