#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import os
from moviepy.editor import ImageSequenceClip
class VideoStitcher:
def __init__(self, left_video_in_path, right_video_in_path, video_out_path, video_out_width=400, display=True):
# Initialize arguments
self.left_video_in_path = left_video_in_path
self.right_video_in_path = right_video_in_path
self.video_out_path = video_out_path
self.video_out_width = video_out_width
self.display = display
# Initialize the saved homography matrix
self.saved_homo_matrix = None
def stitch(self, images, ratio=0.75, reproj_thresh=4.0):
# Unpack the images
(image_b, image_a) = images
# If the saved homography matrix is None, then we need to apply keypoint matching to construct it
if self.saved_homo_matrix is None:
# Detect keypoints and extract
(keypoints_a, features_a) = self.detect_and_extract(image_a)
(keypoints_b, features_b) = self.detect_and_extract(image_b)
# Match features between the two images
matched_keypoints = self.match_keypoints(keypoints_a, keypoints_b, features_a, features_b, ratio, reproj_thresh)
# If the match is None, then there aren't enough matched keypoints to create a panorama
if matched_keypoints is None:
return None
# Save the homography matrix
self.saved_homo_matrix = matched_keypoints[1]
# Apply a perspective transform to stitch the images together using the saved homography matrix
output_shape = (image_a.shape[1] + image_b.shape[1], image_a.shape[0])
result = cv2.warpPerspective(image_a, self.saved_homo_matrix, output_shape)
result[0:image_b.shape[0], 0:image_b.shape[1]] = image_b
# Return the stitched image
return result
@staticmethod
def detect_and_extract(image):
# Detect and extract features from the image (DoG keypoint detector and SIFT feature extractor)
descriptor = cv2.xfeatures2d.SIFT_create()
(keypoints, features) = descriptor.detectAndCompute(image, None)
# Convert the keypoints from KeyPoint objects to numpy arrays
keypoints = np.float32([keypoint.pt for keypoint in keypoints])
# Return a tuple of keypoints and features
return (keypoints, features)
@staticmethod
def match_keypoints(keypoints_a, keypoints_b, features_a, features_b, ratio, reproj_thresh):
# Compute the raw matches and initialize the list of actual matches
matcher = cv2.DescriptorMatcher_create("BruteForce")
raw_matches = matcher.knnMatch(features_a, features_b, k=2)
matches = []
for raw_match in raw_matches:
# Ensure the distance is within a certain ratio of each other (i.e. Lowe's ratio test)
if len(raw_match) == 2 and raw_match[0].distance < raw_match[1].distance * ratio:
matches.append((raw_match[0].trainIdx, raw_match[0].queryIdx))
# Computing a homography requires at least 4 matches
if len(matches) > 4:
# Construct the two sets of points
points_a = np.float32([keypoints_a[i] for (_, i) in matches])
points_b = np.float32([keypoints_b[i] for (i, _) in matches])
# Compute the homography between the two sets of points
(homography_matrix, status) = cv2.findHomography(points_a, points_b, cv2.RANSAC, reproj_thresh)
# Return the matches, homography matrix and status of each matched point
return (matches, homography_matrix, status)
# No homography could be computed
return None
@staticmethod
def draw_matches(image_a, image_b, keypoints_a, keypoints_b, matches, status):
# Initialize the output visualization image
(height_a, width_a) = image_a.shape[:2]
(height_b, width_b) = image_b.shape[:2]
visualisation = np.zeros((max(height_a, height_b), width_a + width_b, 3), dtype="uint8")
visualisation[0:height_a, 0:width_a] = image_a
visualisation[0:height_b, width_a:] = image_b
for ((train_index, query_index), s) in zip(matches, status):
# Only process the match if the keypoint was successfully matched
if s == 1:
# Draw the match
point_a = (int(keypoints_a[query_index][0]), int(keypoints_a[query_index][1]))
point_b = (int(keypoints_b[train_index][0]) + width_a, int(keypoints_b[train_index][1]))
cv2.line(visualisation, point_a, point_b, (0, 255, 0), 1)
# return the visualization
return visualisation
def run(self):
# Set up video capture
left_video = cv2.VideoCapture(0)#eo_in_path)
right_video = cv2.VideoCapture(1)#self.right_video_in_path)
print('[INFO]: {} and {} loaded'.format(self.left_video_in_path.split('/')[-1],
self.right_video_in_path.split('/')[-1]))
print('[INFO]: Video stitching starting....')
# Get information about the videos
n_frames = min(int(left_video.get(cv2.CAP_PROP_FRAME_COUNT)),
int(right_video.get(cv2.CAP_PROP_FRAME_COUNT)))
fps = int(left_video.get(cv2.CAP_PROP_FPS))
frames = []
# for _ in tqdm.tqdm(np.arange(n_frames)):
while True:
# Grab the frames from their respective video streams
ok, left = left_video.read()
_, right = right_video.read()
if ok:
cv2.imshow("a", left)
cv2.imshow("b", right)
# Stitch the frames together to form the panorama
stitched_frame = self.stitch([left, right])
# No homography could not be computed
if stitched_frame is None:
print("[INFO]: Homography could not be computed!")
break
# Add frame to video
# stitched_frame = imutils.resize(stitched_frame, width=self.video_out_width)
# stitched_frame = cv2.resize(stitched_frame, (self.video_out_width,self.video_out_width))
frames.append(stitched_frame)
if self.display:
# Show the output images
cv2.imshow("Result", stitched_frame)
# If the 'q' key was pressed, break from the loop
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cv2.destroyAllWindows()
print('[INFO]: Video stitching finished')
# Save video
print('[INFO]: Saving {} in {}'.format(self.video_out_path.split('/')[-1],
os.path.dirname(self.video_out_path)))
clip = ImageSequenceClip(frames, fps=fps)
clip.write_videofile(self.video_out_path, codec='mpeg4', audio=False, progress_bar=True, verbose=False)
print('[INFO]: {} saved'.format(self.video_out_path.split('/')[-1]))
# Example call to 'VideoStitcher'
stitcher = VideoStitcher(left_video_in_path='videos/bike_left_01.mp4',
right_video_in_path='videos/bike_right_01.mp4',
video_out_path='bike_01_stitched.mp4')
stitcher.run()