import cv2
import numpy as np
import os
import subprocess
import uuid
from time import time, sleep
import copy
from colorama import Fore
from queue import Queue
[docs]class FrameDecoder:
"""Class to decode frames from h264 video.
:param config: configuration including data.json template
:type config: object
:param video_path: folder containing video file
:type video_path: string
:param data_json: json dictionary with import/output-format for KInsecta web app
:type data_json: dict
"""
def __init__(self, config, video_path, data_json):
self.config = config
self.executor = config.executor
self.video_path = video_path
self.video_filename = self.get_video_filename()
self.convert_to_mp4()
self.stream = cv2.VideoCapture(self.video_filename)
self.data_json = data_json
self.trigger_save = False
self.stopped = False
# initialize the queue used to store frames read from
# the video file
self.Q = Queue(maxsize=self.number_of_frames)
self.skip_read = False
[docs] def get_video_filename(self):
"""Returns the filename of the video.
:return: filename
:rtype: string
"""
f = []
for (dir_path, dir_names, filenames) in os.walk(self.video_path):
f.extend(filenames)
break
return os.path.join(self.video_path, f[0])
[docs] def convert_to_mp4(self):
"""Convert ``h264`` to ``mp4`` with `GPAC` and get number of frames from its stdout.
"""
# convert h264 to mp4 video, dependencies gpac
video_filename = self.video_filename
print(Fore.RED + 'Converting h264 to mp4 video with MP4Box(gpac)!' + Fore.WHITE)
cmd = '{} {} {} {} {}'.format('MP4Box -fps', self.config.camera_fps, '-add', video_filename,
video_filename.replace('.h264', '.mp4'))
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
_, stdout = process.communicate()
self.number_of_frames = int(stdout.decode('utf-8').split('Slices:')[1].split(' ')[1])
os.system('{} {}'.format('rm', video_filename))
self.video_filename = video_filename.replace('.h264', '.mp4')
[docs] def start(self):
"""Start decoding thread to fill queue with frames.
"""
# start a thread to read frames from the file video stream
self.executor.submit(self.update)
return self
[docs] def update(self):
"""Main decoding thread, that fills queue with decoded frames. Every second frame the faster grab method is \
used instead of decoding a frame.
"""
# keep looping infinitely
while True:
# if the thread indicator variable is set, stop the
# thread
if self.stopped:
self.stream.release()
return
# otherwise, ensure the queue has room in it
if not self.Q.full():
if self.skip_read:
# read the next frame from the file
(grabbed, frame) = self.stream.read()
if not grabbed:
self.stop()
return
self.Q.put(frame)
else:
grabbed = self.stream.grab()
if not grabbed:
self.stop()
return
self.skip_read = not self.skip_read
sleep(0.001)
[docs] def more(self):
"""Check if frame queue is not empty or if the decoding process is not stopped yet.
:return: flag to decode next frame
:rtype: boolean
"""
# return True if there are still frames in the queue
return self.Q.qsize() > 0 or not self.stopped
[docs] def stop(self):
"""Stop the decoding thread and insert `None` as last queue item. Set corresponding flag.
"""
# indicate that the thread should be stopped
self.Q.put(None)
self.stopped = True
[docs] def create_filename(self):
"""Creates UUID filename for frame and write this information to ``data.json``.
"""
UUID = uuid.uuid1()
UUID_filename = os.path.join(self.video_path, '{}.{}'.format(UUID, 'png'))
images_copy = copy.deepcopy(self.config.data["images"])[0]
images_copy["filename"] = '{}.{}'.format(UUID, 'png')
self.data_json["images"].append(images_copy)
return UUID_filename
[docs] @staticmethod
def write_frame(frame, UUID_filename):
"""Saves the frame with UUID filename.
:param frame: frame to save to disc
:type frame: numpy array
:param UUID_filename: filename of frame
:type UUID_filename: string
"""
cv2.imwrite(UUID_filename, frame)
[docs] def frames(self):
"""Loops over all the video frames and computes the mean pixel values and the difference to the mean of \
the preceded frame in each loop. The root mean square (rms) of the mean differences gets updated in each loop \
and if a frame's mean difference is five times higher than the current rms, a sequence of frames is saved to \
disc. If no such trigger event occurs, a sequence of frames from the middle of the video gets saved.
"""
# empty images list and add list entries for multiple images by appending
self.data_json["images"] = []
count = 1
saved_frames_count, mean_old, rms = 0, 0, 0
index_start = self.number_of_frames // 4
center_frames = []
triggered_frames = []
start_time = time()
while self.more():
frame = self.Q.get(block=True)
if frame is None:
break
if not self.trigger_save:
# approx 0.05 sec per mean calculation
res_frame = np.resize(frame, (320, 240, 1))
mean_new = np.mean(res_frame)
# skip first loop due to non existing difference
if count != 1:
mean_diff = mean_new - mean_old
# set reasonable starting value for rms
if count == 2:
rms = np.abs(mean_diff)
if rms > 5:
rms = 5
if rms < 0.05:
rms = 0.5
if index_start <= count < index_start + self.config.number_frames:
center_frames.append(frame)
# trigger save frames sequence
if mean_diff > 5 * rms:
self.trigger_save = True
# print("{:1.4f}".format(mean_diff), "{:1.4f}".format(rms))
print(Fore.GREEN + 'Trigger event detected! ' + Fore.WHITE)
print('Saving sequence of %d frames!' % self.config.number_frames)
# ignore great negative differences and don't update rms
elif mean_diff < - 5 * rms:
pass
# update rms
else:
rms = np.sqrt((np.square(rms) * (count - 1) + np.square(mean_diff)) / count)
# print("{:1.4f}".format(mean_diff), "{:1.4f}".format(rms))
mean_old = mean_new
else:
if saved_frames_count < self.config.number_frames:
triggered_frames.append(frame)
saved_frames_count += 1
else:
self.stop()
break
count += 1
# check if save sequence got triggered while iterating through frames. If not then frames from
# the center of the mp4 video get saved
if self.trigger_save:
save_frames = triggered_frames
starting_frame = 2 * (count - 1) - 1
else:
print(Fore.RED + 'No trigger event detected!' + Fore.WHITE + ' Decode frames from center of the video!')
print('Saving sequence of %d frames!' % self.config.number_frames)
save_frames = center_frames
starting_frame = self.number_of_frames // 2
await_save_first_frame = True
for frame in save_frames:
UUID_filename = self.create_filename()
if await_save_first_frame:
await_save_first_frame = False
self.plot_png_filename = os.path.join('/sensors/static/data/img',
os.path.basename(os.path.dirname(UUID_filename)),
os.path.basename(UUID_filename))
self.write_frame(frame, UUID_filename)
else:
self.executor.submit(self.write_frame, frame, UUID_filename)
decoding_save_time = "{:2.1f}".format(time() - start_time)
print('From {} total video frames, decoded {} frames (starting frame #{}) and saved in {} seconds!'.
format(self.number_of_frames, self.config.number_frames, starting_frame, decoding_save_time))