diff --git a/README.md b/README.md index bee2f84..9aa6891 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -CV2 Threaded Video Capture +# CV2 Threaded Video Capture + +Not much to say here, it enables you to read an entire video or a number of frames from a video in an extra thread with some nice syntax. + +This project was initially part of my video synopsis project, wich is why the config dict() is required. + +### Full video +```python +from VideoReader import VideoReader +import os + +fileName = "out.mp4" +dirName = os.path.join(os.path.dirname(__file__), "generate test footage") + +config = {} +config["inputPath"] = os.path.join(dirName, fileName) +config["videoBufferLength"] = 100 + +with VideoReader(config) as reader: + while not reader.videoEnded(): + framenumber, frame = reader.pop() + print(framenumber) +``` + +### Selection of Frames +```python +from VideoReader import VideoReader +import os + +fileName = "out.mp4" +dirName = os.path.join(os.path.dirname(__file__), "generate test footage") + +config = {} +config["inputPath"] = os.path.join(dirName, fileName) +config["videoBufferLength"] = 100 + +frameList = list(range(100, 500)) + +with VideoReader(config, frameList) as reader: + while not reader.videoEnded(): + framenumber, frame = reader.pop() + print(framenumber) +``` \ No newline at end of file diff --git a/VideoReader.py b/VideoReader.py new file mode 100644 index 0000000..ae77f69 --- /dev/null +++ b/VideoReader.py @@ -0,0 +1,132 @@ +from queue import Queue + +import cv2 +import threading +import os + +class VideoReader: + listOfFrames = None + w = None + h = None + + def __init__(self, config, setOfFrames=None): + videoPath = config["inputPath"] + self.videoPath = videoPath + + if videoPath is None: + raise Exception("VideoReader:: Video reader needs a videoPath!") + if not os.path.exists(videoPath): + raise Exception("VideoReader:: Provided video path does not exist") + + self.lastFrame = 0 + # buffer data struct: + # buffer = Queue([(frameNumber, frame), ]) + self.buffer = Queue(config["videoBufferLength"]) + self.vc = cv2.VideoCapture(videoPath) + self.stopped = False + self.getWH() + self.calcFPS() + self.calcLength() + self.calcStartTime() + if setOfFrames is not None: + self.listOfFrames = sorted(setOfFrames) + + def __enter__(self): + self.fillBuffer() + return self + + def __exit__(self, type, value, traceback): + self.stop() + + def stop(self): + self.thread.join() + self.vc.release() + + def pop(self): + return self.buffer.get(block=True) + + def fillBuffer(self, listOfFrames=None): + self.endFrame = int(self.vc.get(cv2.CAP_PROP_FRAME_COUNT)) + if listOfFrames is not None: + self.listOfFrames = listOfFrames + + if self.listOfFrames is not None: + self.thread = threading.Thread( + target=self.readFramesByList, args=()) + else: + self.thread = threading.Thread(target=self.readFrames, args=()) + self.thread.start() + + def readFrames(self): + '''Reads video from start to finish''' + while self.lastFrame < self.endFrame: + res, frame = self.vc.read() + if res: + self.buffer.put((self.lastFrame, frame)) + self.lastFrame += 1 + + self.stopped = True + + def readFramesByList(self): + '''Reads all frames from a list of frame numbers''' + self.vc.set(1, self.listOfFrames[0]) + self.lastFrame = self.listOfFrames[0] + self.endFrame = self.listOfFrames[-1] + + while self.lastFrame < self.endFrame: + if self.lastFrame in self.listOfFrames: + res, frame = self.vc.read() + if res: + self.buffer.put((self.lastFrame, frame)) + else: + print("READING FRAMES IS FALSE") + # since the list is sorted the first element is always the lowest relevant framenumber + # [0,1,2,3,32,33,34,35,67,68,69] + self.listOfFrames.pop(0) + self.lastFrame += 1 + else: + # if current Frame number is not in list of Frames, we can skip a few frames + self.vc.set(1, self.listOfFrames[0]) + self.lastFrame = self.listOfFrames[0] + + self.stopped = True + + def videoEnded(self): + if self.stopped and self.buffer.empty(): + return True + else: + return False + + def calcFPS(self): + self.fps = self.vc.get(cv2.CAP_PROP_FPS) + + def getFPS(self): + if self.fps is None: + self.calcFPS() + return self.fps + + def calcLength(self): + fc = int(self.vc.get(cv2.CAP_PROP_FRAME_COUNT)) + self.length = fc / self.getFPS() + + def getLength(self): + if self.length is None: + self.calcLength() + return self.length + + def calcStartTime(self): + starttime = os.stat(self.videoPath).st_mtime + length = self.getLength() + starttime = starttime - length + self.starttime = starttime + + def getStartTime(self): + return self.starttime + + def getWH(self): + '''get width and height''' + if self.w is None or self.h is None: + res, image = self.vc.read() + self.w = image.shape[1] + self.h = image.shape[0] + return (self.w, self.h) diff --git a/__pycache__/VideoReader.cpython-37.pyc b/__pycache__/VideoReader.cpython-37.pyc new file mode 100644 index 0000000..cce8268 Binary files /dev/null and b/__pycache__/VideoReader.cpython-37.pyc differ diff --git a/test.py b/test.py new file mode 100644 index 0000000..2b1f282 --- /dev/null +++ b/test.py @@ -0,0 +1,14 @@ +from VideoReader import VideoReader +import os + +fileName = "out.mp4" +dirName = os.path.join(os.path.dirname(__file__), "generate test footage") + +config = {} +config["inputPath"] = os.path.join(dirName, fileName) +config["videoBufferLength"] = 100 + +with VideoReader(config) as reader: + while not reader.videoEnded(): + framenumber, frame = reader.pop() + print(framenumber)