// Stitching Pipeline adapted from OpenCV source files
// Reading of textfile from: http://stackoverflow.com/questions/14265581/parse-split-a-string-in-c-using-string-delimiter-standard-c
// Timing of multiple threads from: http://stackoverflow.com/questions/2962785/c-using-clock-to-measure-time-in-multi-threaded-programs

#include "stitching.h"

// ---------- DEFAULT SETTINGS ---------- //
std::vector<cv::Mat> video_frames; // stores the frames of each respective video
std::vector<cv::VideoCapture> videos; // stores the videos
int no_of_videos;
bool try_gpu = false; // do not use GPU
double work_megapix = 0.6;
double seam_megapix = 0.1;
float confidence_threshold = 0.5f;
std::string features_type = "surf"; // feature detector type
std::string ba_cost_func = "ray"; // bundle adjustment category optimize
std::string ba_refine_mask = "xxxxx";
bool do_wave_correct = true;
cv::detail::WaveCorrectKind wave_correct = cv::detail::WAVE_CORRECT_HORIZ;
std::string warp_type = "paniniA2B1"; // type of warping
int exposure_compensator_type = cv::detail::ExposureCompensator::GAIN_BLOCKS;
float match_confidence = 0.3f;
std::string seam_find_type = "gc_color";
int blend_type = cv::detail::Blender::MULTI_BAND;
float blend_strength = 5;
bool showStitchSeam = false; // edits contrasts and brightness of frames to visibly display stitch seam
int lengthToStitch = 0; // how long of a video to stitch
int redoStitching = 0; // how long before re-executing the entire stitching pipeline

// Variables to store first time properties
cv::VideoWriter outputVideo;
bool initialise = false;
int width, height; // stores the dimensions of the panoramic stitched frame
bool frameEmpty = false;

// Variables for multithreading
std::vector<cv::Mat> results(1010); // stores stitched frames
int start_index = 0; // index for thread to start storing stitched frames
std::vector<std::thread> threads(10); // thread vector
int no_of_active_threads = 0;

// Variables to time the program
struct timespec start, finish;
double elapsed;

void readSettings() {
	std::string line; // stores lines read from file
	std::ifstream file("stitching.ini");
	std::string delimiter = " = "; // split lines by this token
	size_t position = 0; // stores position of delimiter
	std::string category, value;

	std::cout << "----- Stitching Pipeline Settings -----" << std::endl;

	if (file.is_open()) {
		while (std::getline(file, line)) {
			if (line.length() > 0) {
				if (line[0] != '#') { // lines starting with '#' are comments
					position = line.find(delimiter);
					if (position != std::string::npos) { // if delimiter exists
						category = line.substr(0, position);
						value = line.substr(position + delimiter.length());

						setValue(category, value);
					}
					else {
						std::cout << "Line is incorrectly formatted: ";
						std::cout << line << std::endl;
						std::cout << "--- Skipping this line. ---" << std::endl;
					}
				}
			}
		}
		file.close();
	}
	else {
		std::cout << "Could not open stitching.ini. Using default settings." << std::endl;
	}
}

void setValue(std::string category, std::string value) {
	if (category == "GPU") {
		if (value == "0") {
			try_gpu = false;
			std::cout << "GPU set to false." << std::endl;
		}
		else if (value == "1") {
			try_gpu = true;
			std::cout << "GPU set to true." << std::endl;
		}
		else {
			std::cout << "Unknown value for GPU: " << value << ". Using default: false." << std::endl;
		}
	}
	else if (category == "WorkMegapix") {
		double temp = atof(value.c_str());
		if (temp == 0) {
			std::cout << "Invalid value for WorkMegapix: " << value << ". Using default: 0.6." << std::endl;
		}
		else {
			std::cout << "Setting WorkMegapix to: " << value << "." << std::endl;
			work_megapix = temp;
		}
	}
	else if (category == "SeamMegapix") {
		double temp = atof(value.c_str());
		if (temp == 0) {
			std::cout << "Invalid value for SeamMegapix: " << value << ". Using default: 0.1." << std::endl;
		}
		else {
			std::cout << "Setting SeamMegapix to: " << value << "." << std::endl;
			seam_megapix = temp;
		}
	}
	else if (category == "ConfidenceThreshold") {
		float temp = static_cast<float>(atof(value.c_str()));
		if (temp == 0) {
			std::cout << "Invalid value for ConfidenceThreshold: " << value << ". Using default: 0.5." << std::endl;
		}
		else {
			std::cout << "Setting ConfidenceThreshold to: " << value << "." << std::endl;
			confidence_threshold = temp;
		}
	}
	else if (category == "FeatureType") {
		if (value == "surf") {
			features_type = "surf";
			std::cout << "FeatureType set to \"surf\"." << std::endl;
		}
		else if (value == "orb") {
			features_type = "orb";
			std::cout << "FeatureTye set to \"orb\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for FeatureType: " << value << ". Using default: surf." << std::endl;
		}
	}
	else if (category == "BACostFunction") {
		if (value == "ray") {
			ba_cost_func = "ray";
			std::cout << "BACostFunction set to \"ray\"." << std::endl;
		}
		else if (category == "reproj") {
			ba_cost_func = "reproj";
			std::cout << "BACostFunction set to \"reproj\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for BACostFunction: " << value << ". Using default: ray." << std::endl;
		}
	}
	else if (category == "BARefineMask") {
		if (value.length() != 5) {
			std::cout << "Incorrect refinement mask length: " << value << ". Using default: xxxxx." << std::endl;
		}
		else {
			ba_refine_mask = value;
			std::cout << "BARefineMask set to: " << value << "." << std::endl;
		}
	}
	else if (category == "DoWaveCorrect") {
		if (value == "1") {
			do_wave_correct = true;
			std::cout << "DoWaveCorrect set to true." << std::endl;
		}
		else if (value == "0") {
			do_wave_correct = false;
			std::cout << "DoWaveCorrect set to false." << std::endl;
		}
		else {
			std::cout << "Unknown value for DoWaveCorrect: " << value << ". Using default: true." << std::endl;
		}
	}
	else if (category == "WaveCorrect") {
		if (value == "horizontal") {
			wave_correct = cv::detail::WAVE_CORRECT_HORIZ;
			std::cout << "WaveCorrect set to \"horizontal\"." << std::endl;
		}
		else if (value == "vertical") {
			wave_correct = cv::detail::WAVE_CORRECT_VERT;
			std::cout << "WaveCorrect set to \"vertical\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for WaveCorrect: " << value << ". Using default: horizontal." << std::endl;
		}
	}
	else if (category == "WarpType") {
		if (value == "paniniA2B1") {
			warp_type = "paniniA2B1";
			std::cout << "WarpType set to \"paniniA2B1\"." << std::endl;
		}
		else if (value == "plane") {
			warp_type = "plane";
			std::cout << "WarpType set to \"plane\"." << std::endl;
		}
		else if (value == "cylindrical") {
			warp_type = "cylindrical";
			std::cout << "WarpType set to \"cylindrical\"." << std::endl;
		}
		else if (value == "spherical") {
			warp_type = "spherical";
			std::cout << "WarpType set to \"spherical\"." << std::endl;
		}
		else if (value == "fisheye") {
			warp_type = "fisheye";
			std::cout << "WarpType set to \"fisheye\"." << std::endl;
		}
		else if (value == "stereographic") {
			warp_type = "stereographic";
			std::cout << "WarpType set to \"stereographic\"." << std::endl;
		}
		else if (value == "compressedPlaneA2B1") {
			warp_type = "compressedPlaneA2B1";
			std::cout << "WarpType set to \"compressedPlaneA2B1\"." << std::endl;
		}
		else if (value == "compressedPlaneA1.5B1") {
			warp_type = "compressedPlaneA1.5B1";
			std::cout << "WarpType set to \"compressedPlaneA1.5B1\"." << std::endl;
		}
		else if (value == "compressedPlanePortraitA2B1") {
			warp_type = "compressedPlanePortraitA2B1";
			std::cout << "WarpType set to \"compressedPlanePortraitA2B1\"." << std::endl;
		}
		else if (value == "compressedPlanePortraitA1.5B1") {
			warp_type = "compressedPlanePortraitA1.5B1";
			std::cout << "WarpType set to \"compressedPlanePortraitA1.5B1\"." << std::endl;
		}
		else if (value == "paniniA1.5B1") {
			warp_type = "paniniA1.5B1";
			std::cout << "WarpType set to \"paniniA1.5B1\"." << std::endl;
		}
		else if (value == "paniniPortraitA2B1") {
			warp_type = "paniniPortraitA2B1";
			std::cout << "WarpType set to \"paniniPortraitA2B1\"." << std::endl;
		}
		else if (value == "paniniPortraitA1.5B1") {
			warp_type = "paniniPortraitA1.5B1";
			std::cout << "WarpType set to \"paniniPortraitA1.5B1\"." << std::endl;
		}
		else if (value == "mercator") {
			warp_type = "mercator";
			std::cout << "WarpType set to \"mercator\"." << std::endl;
		}
		else if (value == "transverseMercator") {
			warp_type = "transverseMercator";
			std::cout << "WarpType set to \"transverseMercator\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for WarpType: " << value << ". Using default: paniniA2B1." << std::endl;
		}
	}
	else if (category == "ExpoCompensatorType") {
		if (value == "gainblocks") {
			exposure_compensator_type = cv::detail::ExposureCompensator::GAIN_BLOCKS;
			std::cout << "ExpoCompensatorType set to \"gainblocks\"." << std::endl;
		}
		else if (value == "no") {
			exposure_compensator_type = cv::detail::ExposureCompensator::NO;
			std::cout << "ExpoCompensatorType set to \"no\"." << std::endl;
		}
		else if (value == "gain") {
			exposure_compensator_type = cv::detail::ExposureCompensator::GAIN;
			std::cout << "ExpoCompensatorType set to \"gain\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for ExpoCompensatorType: " << value << ". Using default: gainblocks." << std::endl;
		}
	}
	else if (category == "MatchConfidence") {
		float temp = static_cast<float>(atof(value.c_str()));
		if (temp == 0) {
			std::cout << "Invalid value for MatchConfidence: " << value << ". Using default: 0.3." << std::endl;
		}
		else {
			std::cout << "Setting MatchConfidence to: " << value << "." << std::endl;
			match_confidence = temp;
		}
	}
	else if (category == "SeamEstimation") {
		if (value == "gccolor") {
			seam_find_type = "gc_color";
			std::cout << "SeamEstimation set to \"gccolor\"." << std::endl;
		}
		else if (value == "no") {
			seam_find_type = "no";
			std::cout << "SeamEstimation set to \"no\"." << std::endl;
		}
		else if (value == "voronoi") {
			seam_find_type = "voronoi";
			std::cout << "SeamEstimation set to \"voronoi\"." << std::endl;
		}
		else if (value == "gccolorgrad") {
			seam_find_type = "gc_colorgrad";
			std::cout << "SeamEstimation set to \"gccolorgrad\"." << std::endl;
		}
		else if (value == "dpcolor") {
			seam_find_type = "dp_color";
			std::cout << "SeamEstimation set to \"dpcolor\"." << std::endl;
		}
		else if (value == "dpcolorgrad") {
			seam_find_type = "dp_colorgrad";
			std::cout << "SeamEstimation set to \"dpcolorgrad\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for SeamEstimation: " << value << ". Using default: gccolor." << std::endl;
		}
	}
	else if (category == "BlendType") {
		if (value == "multiband") {
			blend_type = cv::detail::Blender::MULTI_BAND;
			std::cout << "BlendType set to \"multiband\"." << std::endl;
		}
		else if (value == "no") {
			blend_type = cv::detail::Blender::NO;
			std::cout << "BlendType set to \"no\"." << std::endl;
		}
		else if (value == "feather") {
			blend_type = cv::detail::Blender::FEATHER;
			std::cout << "BlendType set to \"feather\"." << std::endl;
		}
		else {
			std::cout << "Unknown value for BlendType: " << value << ". Using default: multiband." << std::endl;
		}
	}
	else if (category == "BlendStrength") {
		float temp = static_cast<float>(atof(value.c_str()));
		if (temp == 0) {
			std::cout << "Invalid value for BlendStrength: " << value << ". Using default: 5." << std::endl;
		}
		else {
			std::cout << "Setting BlendStrength to: " << value << "." << std::endl;
			blend_strength = temp;
		}
	}
	else if (category == "ShowStitchSeam") {
		if (value == "0") {
			showStitchSeam = false;
			std::cout << "ShowStitchSeam set to false." << std::endl;
		}
		else if (value == "1") {
			showStitchSeam = true;
			std::cout << "ShowStitchSeam set to true." << std::endl;
		}
		else {
			std::cout << "Unknown value for ShowStitchSeam: " << value << ". Using default: false." << std::endl;
		}
	}
	else if (category == "LengthOfVideo") {
		int temp = strtol(value.c_str(), NULL, 10);
		if (temp == 0) {
			lengthToStitch = 0;
			std::cout << "LengthOfVideo set to stitch entire video." << std::endl;
		}
		else {
			lengthToStitch = temp;
			std::cout << "LengthOfVideo set to stitch " << temp << " seconds." << std::endl;
		}
	}
	else if (category == "RedoStitching") {
		int temp = strtol(value.c_str(), NULL, 10);
		if (temp == 0) {
			redoStitching = 0;
			std::cout << "RedoStitching set to never re-execute stitching pipeline." << std::endl;
		}
		else {
			redoStitching = temp;
			std::cout << "RedoStitching set to re-execute stitching pipeline every " << temp << " seconds." << std::endl;
		}
	}
	else {
		std::cout << "Unknown category: " << category << ". Skipping." << std::endl;
	}
}

void joinAndStitch(int frameNumber, int noOfFrames) {
	for (int i = 0; i < no_of_active_threads; ++i) {
		threads[i].join();
	}

	for (int i = 0; i < start_index; ++i) {
		outputVideo << results[i];
	}

	no_of_active_threads = 0;
	start_index = 0;

	// print some progress to std::cout
	printf("Stitching progress: %i / %i = %3.2f%%\n", frameNumber, noOfFrames, frameNumber / (float)noOfFrames * 100);
}

int stitch(int argc, char* argv[]) {
	if (argc < 2) {
		std::cout << "Usage: \n"
			<< ".exe <video #1> ... <video #n>" << std::endl;
		return -1;
	}
	else if (argc == 2) {
		std::cout << "Need more than one video to stitch!" << std::endl;
		std::cout << "Usage: \n"
			<< ".exe <video #1> ... <video #n>" << std::endl;
		return -1;
	}

	no_of_videos = argc - 1;
	int frameNumber = 0;

	readSettings(); // Read settings from .ini file
	std::cout << std::endl;

	clock_gettime(CLOCK_MONOTONIC, &start);

	// ---------- Read videos ---------- //
	for (int i = 1; i < argc; ++i) {
		cv::VideoCapture video(argv[i]);

		if (!video.isOpened()) {
			std::cout << "Could not open video #" << i << "!" << std::endl;
			return -1;
		}

		videos.push_back(video);
	}

	// Determine the video that has the least amount of frames
	int minimum_frames = videos[0].get(CV_CAP_PROP_FRAME_COUNT);
	for (size_t i = 1; i < videos.size(); ++i) {
		if (minimum_frames > videos[i].get(CV_CAP_PROP_FRAME_COUNT)) {
			minimum_frames = videos[i].get(CV_CAP_PROP_FRAME_COUNT);
		}
	}

	// Determine number of frames to stitch
	int noOfFrames;
	if (lengthToStitch == 0) {
		noOfFrames = minimum_frames;
	}
	else {
		noOfFrames = (int)videos[0].get(CV_CAP_PROP_FPS) * lengthToStitch;
	}

	// Determine number of frames to re-execute stitching pipeline
	int redoFrames;
	if (redoStitching == 0) {
		redoFrames = minimum_frames;
	}
	else {
		redoFrames = (int)videos[0].get(CV_CAP_PROP_FPS) * redoStitching;
	}

	while (frameNumber < noOfFrames) {
		video_frames.clear();

		// ---------- Save video frames ---------- //
		for (size_t i = 0; i < videos.size(); ++i) {
			cv::Mat frame;
			videos[i] >> frame;

			if (frame.empty()) {
				frameEmpty = true;
				break;
			}

			video_frames.push_back(frame);
		}

		if (frameEmpty) {
			break;
		}

		// ---------- Set image scales ---------- //
		double work_scale = 1;
		double seam_work_aspect = 1;

		// ---------- FIND FEATURES ---------- //
		//std::cout << "Finding Features...";

		std::vector<cv::detail::ImageFeatures> features(no_of_videos);
		std::vector<cv::Mat> images(no_of_videos);
		std::vector<cv::Size> full_image_sizes(no_of_videos);

		FindFeatures find_features(features_type, work_megapix, seam_megapix); // Constructor

		find_features.find_features(video_frames,
			features,
			images,
			full_image_sizes,
			work_scale,
			seam_work_aspect);

		//std::cout << " Done." << std::endl;
		// ---------- FIND FEATURES DONE ---------- //

		// ---------- FEATURE MATCHING ---------- //
		//std::cout << "Feature Matching...";
		cv::vector<cv::detail::MatchesInfo> pairwise_matches;

		FeatureMatching feature_matching(try_gpu, match_confidence); // Constructor

		feature_matching.feature_matching(features, pairwise_matches);

		//std::cout << " Done." << std::endl;
		// ---------- FEATURE MATCHING DONE ---------- //

		// ---------- ESTIMATE HOMOGRAPHY ---------- //
		//std::cout << "Estimating Homography...";
		std::vector<cv::detail::CameraParams> cameraParameters;

		HomographyEstimator homography_estimator; // Constructor

		homography_estimator.estimate(features, pairwise_matches, cameraParameters);

		//std::cout << " Done." << std::endl;
		// ---------- ESTIMATE HOMOGRAPHY DONE ---------- //

		// ---------- BUNDLE ADJUSTMENT ---------- //
		//std::cout << "Bundle Adjustment...";

		BundleAdjustment bundle_adjustment(ba_cost_func,
			confidence_threshold,
			ba_refine_mask,
			do_wave_correct,
			wave_correct); // Constructor

		float warped_image_scale;
		bundle_adjustment.bundle_adjustment(features,
			pairwise_matches,
			cameraParameters,
			warped_image_scale);

		//std::cout << " Done." << std::endl;
		// ---------- BUNDLE ADJUSTMENT DONE ---------- //

		// ---------- WARP IMAGES ---------- //
		//std::cout << "Warping Images...";
		std::vector<cv::Point> corners(no_of_videos);
		std::vector<cv::Mat> masks_warped(no_of_videos);
		std::vector<cv::Mat> images_warped(no_of_videos);
		std::vector<cv::Size> sizes(no_of_videos);

		cv::Ptr<cv::WarperCreator> warper_creator;

		std::vector<cv::Mat> images_warped_temp(no_of_videos);

		WarpImages warp_images(no_of_videos,
			warp_type,
			warper_creator,
			warped_image_scale,
			seam_work_aspect); // Constructor

		warp_images.warp_images(corners,
			masks_warped,
			images_warped,
			sizes,
			images,
			cameraParameters,
			images_warped_temp);

		//std::cout << " Done." << std::endl;
		// ---------- WARP IMAGES DONE ---------- //

		// ---------- COMPENSATE SEAMS ---------- //
		//std::cout << "Compensating Seams...";
		cv::Ptr<cv::detail::ExposureCompensator> compensator;

		SeamCompensator seam_compensator(seam_find_type, compensator, exposure_compensator_type); // Constructor

		seam_compensator.seam_compensate(corners,
			images_warped,
			masks_warped,
			compensator,
			images_warped_temp);

		//std::cout << " Done." << std::endl;
		// ---------- COMPENSATE SEAMS DONE ---------- //

		// ---------- STITCH IMAGES TOGETHER ---------- //
		//std::cout << "Stitching Images Together...";

		cv::Mat result, result_mask; // Matrices to store final panoramic images

		Blender blender(work_scale,
			warped_image_scale,
			warper_creator,
			no_of_videos,
			cameraParameters,
			full_image_sizes,
			corners,
			sizes); // Constructor

		blender.blend(video_frames,
			cameraParameters,
			corners,
			sizes,
			compensator,
			masks_warped,
			blend_type,
			try_gpu,
			blend_strength,
			result,
			result_mask);

		//std::cout << " Done." << std::endl;

		if (result.rows == 1 && result.cols == 1) {
			std::cout << "Cannot stitch videos together." << std::endl;
			return -1;
		}

		if (!initialise) {
			outputVideo.open("stitchedVideo.mpeg", CV_FOURCC('M', 'P', '4', 'S'), videos[0].get(CV_CAP_PROP_FPS), cv::Size(result.cols, result.rows), true);

			if (!outputVideo.isOpened()) {
				std::cout << "Could not open output video for writing." << std::endl;
				return -1;
			}

			width = result.cols;
			height = result.rows;
		}

		// Check if result needs to be resized
		if (result.cols != width || result.rows != height) {
			cv::resize(result, result, cv::Size(width, height));
		}

		results[start_index] = result;
		++frameNumber;
		++start_index;

		// ---------- Print some information ---------- //
		if (!initialise) {
			std::cout << "----- Stitched Video Properties -----" << std::endl;
			std::cout << "Output frame resolution: Width = " << result.cols
				<< " Height = " << result.rows
				<< " of # of frames total: " << noOfFrames << std::endl;
			std::cout << "FPS: " << videos[0].get(CV_CAP_PROP_FPS) << std::endl;
			initialise = true;
		}

		// ---------- STITCH THE REST OF THE VIDEO ---------- //
		int frames_to_stitch = redoFrames - 1; // the number of frames to stitch using the same masks

		// check if end of video is reached
		if (frameNumber + frames_to_stitch >= noOfFrames) {
			frames_to_stitch = noOfFrames - frameNumber;
		}

		while (frames_to_stitch > 0) {
			if (frames_to_stitch <= 100) {
				if (no_of_active_threads == 10) {
					joinAndStitch(frameNumber, noOfFrames);
				}

				int frames_last_thread = frames_to_stitch;
				std::vector<cv::Mat> frames;

				for (int i = 0; i < frames_last_thread; ++i) {
					for (int j = 0; j < no_of_videos; ++j) {
						cv::Mat frame;
						videos[j] >> frame;

						if (showStitchSeam) {
							frame.convertTo(frame, -1, (float)(j) / (float)(videos.size()) + 0.5, j);
						}

						frames.push_back(frame);
					}
				}

				// create a thread
				threads.at(no_of_active_threads) = std::thread(blendThread,
						frames,
						cameraParameters,
						corners,
						sizes,
						compensator,
						masks_warped,
						blend_type,
						try_gpu,
						blend_strength,
						std::ref(results),
						start_index,
						work_scale,
						warped_image_scale,
						warper_creator,
						no_of_videos,
						full_image_sizes,
						width,
						height);

				++no_of_active_threads;
				start_index += frames_last_thread;
				frameNumber += frames_last_thread;
				frames_to_stitch -= frames_last_thread;
			} // end of less than 100 frames to stitch

			else if (frames_to_stitch <= 1000) {
				// each thread stitches a maximum of 100 frames
				int complete_threads = frames_to_stitch / 100;

				// check for more than 10 threads
				if (no_of_active_threads + complete_threads > 10) {
					joinAndStitch(frameNumber, noOfFrames);
				}

				// create threads
				for (int t = 0; t < complete_threads; ++t) {
					std::vector<cv::Mat> frames;

					for (int i = 0; i < 100; ++i) {
						for (int j = 0; j < no_of_videos; ++j) {
							cv::Mat frame;
							videos[j] >> frame;

							if (showStitchSeam) {
								frame.convertTo(frame, -1, (float)(j) / (float)(videos.size()) + 0.5, j);
							}

							frames.push_back(frame);
						}
					}

					threads.at(no_of_active_threads) = std::thread(blendThread,
							frames,
							cameraParameters,
							corners,
							sizes,
							compensator,
							masks_warped,
							blend_type,
							try_gpu,
							blend_strength,
							std::ref(results),
							start_index,
							work_scale,
							warped_image_scale,
							warper_creator,
							no_of_videos,
							full_image_sizes,
							width,
							height);

					++no_of_active_threads;
					start_index += 100;
					frameNumber += 100;
					frames_to_stitch -= 100;
				}
			} // end of less than 1000 frames to stitch

			else {
				// 10 full threads stitching 100 frames each can be created
				if (no_of_active_threads != 0) {
					joinAndStitch(frameNumber, noOfFrames);
				}

				// create threads
				for (int t = 0; t < 10; ++t) {
					std::vector<cv::Mat> frames;

					for (int i = 0; i < 100; ++i) {
						for (int j = 0; j < no_of_videos; ++j) {
							cv::Mat frame;
							videos[j] >> frame;

							if (showStitchSeam) {
								frame.convertTo(frame, -1, (float)(j) / (float)(videos.size()) + 0.5, j);
							}

							frames.push_back(frame);
						}
					}

					threads.at(no_of_active_threads) = std::thread(blendThread,
							frames,
							std::ref(cameraParameters),
							std::ref(corners),
							std::ref(sizes),
							std::ref(compensator),
							std::ref(masks_warped),
							blend_type,
							try_gpu,
							blend_strength,
							std::ref(results),
							start_index,
							work_scale,
							warped_image_scale,
							std::ref(warper_creator),
							no_of_videos,
							std::ref(full_image_sizes),
							width,
							height);

					++no_of_active_threads;
					start_index += 100;
					frameNumber += 100;
					frames_to_stitch -= 100;
				}
			} // end of more than 1000 frames to stitch
		}
	}

	// check for any remaining running threads
	if (no_of_active_threads != 0) {
		joinAndStitch(frameNumber, noOfFrames);
	}

	clock_gettime(CLOCK_MONOTONIC, &finish);

	elapsed = (finish.tv_sec - start.tv_sec);
	elapsed += (finish.tv_nsec - start.tv_nsec) / 1000000000.0;

	printf("Stitching took %f seconds.\n", elapsed);

	std::cout << "Done stitching." << std::endl;

	return 0;
}

void blendThread(std::vector<cv::Mat> frames,
		std::vector<cv::detail::CameraParams> cameraParameters,
		std::vector<cv::Point> corners,
		std::vector<cv::Size> sizes,
		cv::Ptr<cv::detail::ExposureCompensator> compensator,
		std::vector<cv::Mat> masks_warped,
		int blend_type,
		bool try_gpu,
		int blend_strength,
		std::vector<cv::Mat> &results,
		int begin,
		double work_scale,
		float warped_image_scale,
		cv::Ptr<cv::WarperCreator> warper_creator,
		int no_of_videos,
		std::vector<cv::Size> full_image_sizes,
		int width,
		int height) {

	Blender blender(work_scale,
			warped_image_scale,
			warper_creator,
			no_of_videos,
			cameraParameters,
			full_image_sizes,
			corners,
			sizes); // Constructor

	std::vector<cv::Mat> video_frames;

	size_t i = 0;

	while (i < frames.size()) {
		for (int j = 0; j < no_of_videos; ++j) {
			video_frames.push_back(frames[i]);
			++i;
		}

		cv::Mat result, result_mask;

		blender.blend(video_frames,
				cameraParameters,
				corners,
				sizes,
				compensator,
				masks_warped,
				blend_type,
				try_gpu,
				blend_strength,
				result,
				result_mask);

		// Check if result needs to be resized
		if (result.cols != width || result.rows != height) {
			cv::resize(result, result, cv::Size(width, height));
		}

		results[begin] = result;
		++begin;
		video_frames.clear();
	}
}
