// Copyright (c) 2012-2017 VideoStitch SAS // Copyright (c) 2018 stitchEm #include "epipolarCurvesAlgorithm.hpp" #include "parse/json.hpp" #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable : 4127) #endif #include "calibration/calibrationUtils.hpp" #ifdef _MSC_VER #pragma warning(pop) #endif #include "calibration/keypointExtractor.hpp" #include "calibration/keypointMatcher.hpp" #include "core/controllerInputFrames.hpp" #include "core/geoTransform.hpp" #include "util/pngutil.hpp" #include "util/registeredAlgo.hpp" #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable : 4190) #endif #include <opencv2/imgproc.hpp> #ifdef _MSC_VER #pragma warning(pop) #endif #include <random> #define POINT_PICKING_TARGET_N_PAIRS 10000 #define POINT_PICKING_MAX_TRIALS (1000 * POINT_PICKING_TARGET_N_PAIRS) #define MIN_HANDLED_DEPTH 0.001f #define MAX_HANDLED_DEPTH 100.0f namespace VideoStitch { namespace EpipolarCurves { namespace { Util::RegisteredAlgo<EpipolarCurvesAlgorithm> registered("epipolar"); } const char* EpipolarCurvesAlgorithm::docString = "An algorithm that shows epipolar curves in input pictures, based on provided points or automatically matched " "ones.\n"; static const cv::Scalar colors[] = {{0, 0, 255}, {0, 255, 0}, {255, 0, 0}, {0, 255, 255}, {255, 255, 0}, {255, 0, 255}}; EpipolarCurvesAlgorithm::EpipolarCurvesAlgorithm(const Ptv::Value* config) : epipolarCurvesConfig(config) {} EpipolarCurvesAlgorithm::~EpipolarCurvesAlgorithm() {} /* Writes OpenCV image to file */ static Status dumpImageFile(const cv::Mat& image, const std::string& filename) { bool (*writeImageFileFunction)(const char* filename, int64_t width, int64_t height, const void* data) = nullptr; switch (image.type()) { case CV_8UC4: writeImageFileFunction = Util::PngReader::writeRGBAToFile; break; case CV_8UC3: writeImageFileFunction = Util::PngReader::writeBGRToFile; break; case CV_8UC1: writeImageFileFunction = Util::PngReader::writeMonochromToFile; break; default: return {Origin::Output, ErrType::RuntimeError, "Invalid image format"}; } if (!writeImageFileFunction(filename.c_str(), image.cols, image.rows, image.data)) { return {Origin::Output, ErrType::RuntimeError, "Could not write output file to path: '" + filename + "'"}; } return Status::OK(); } // checks that a point can be correctly mapped // points scaled from (0, 0, -1) are singular, they cannot be mapped correctly static bool isMappable(const Core::SphericalCoords3& scaledPoint3d) { const float norm = std::sqrt(scaledPoint3d.x * scaledPoint3d.x + scaledPoint3d.y * scaledPoint3d.y + scaledPoint3d.z * scaledPoint3d.z); return (std::abs(scaledPoint3d.z + norm) > 1.0e-6f); } /* Computes the Euclidian distance between two 2D points */ template <class Point2DType1, class Point2DType2> float distance2D(const Point2DType1& pt1, const Point2DType2& pt2) { return std::sqrt((pt1.x - pt2.x) * (pt1.x - pt2.x) + (pt1.y - pt2.y) * (pt1.y - pt2.y)); } /* Computes the minimum stitching distance for a 3D unit point - returns false if not possible */ static bool computeMinimumStitchingDistanceFor3DPoint( const Core::SphericalCoords3& refPoint3d, const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano, float& stitchingDistance) { const float minDepth = MIN_HANDLED_DEPTH; const float maxDepth = MAX_HANDLED_DEPTH; auto isDepthFor3DPointWithinInputBounds = [&](const videoreaderid_t id, const float depth) -> bool { assert(depth >= 0.f); Core::SphericalCoords3 scaledPoint3d = refPoint3d; scaledPoint3d *= depth; if (!isMappable(scaledPoint3d)) { return false; } const Core::CenterCoords2 centerProjected = transforms[id]->mapRigSphericalToInput(pano->getVideoInput(id), scaledPoint3d, 0); const Core::TopLeftCoords2 topLeftProjected(centerProjected, inputCenters[id]); return transforms[id]->isWithinInputBounds(pano->getVideoInput(id), topLeftProjected); }; // Check if refPoint3d is visible by a number of cameras when scaled at maxDepth std::vector<videoreaderid_t> camerasIds; for (videoreaderid_t i = 0; i < pano->numVideoInputs(); ++i) { if (isDepthFor3DPointWithinInputBounds(i, maxDepth)) { camerasIds.push_back(i); } } // We need the point to be visible by at least two cameras, if not, pick another point if (camerasIds.size() < 2) { return false; } // Find the minimum depth that will make the point be visible by less than 2 cameras float depth_below = minDepth; float depth_above = maxDepth; while (depth_above - depth_below >= .001) { float mid_depth = (depth_above + depth_below) / 2; size_t numberOfCamerasWherePointIsVisible = 0; for (auto camId : camerasIds) { if (isDepthFor3DPointWithinInputBounds(camId, mid_depth)) { ++numberOfCamerasWherePointIsVisible; } } if (numberOfCamerasWherePointIsVisible < 2) { // stitching distance is too small, less than 2 cameras still see the point depth_below = mid_depth; } else { // stitching distance is is too large, more than 2 cameras see the point depth_above = mid_depth; } } stitchingDistance = (depth_above + depth_below) / 2; return true; } /* Computes the minimum stitching distance for every point of the output panorama */ static cv::Mat computeMinimumStitchingDistancePerPointInOutputPanorama( const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano, double imageMaxOutputDepth) { const int panoWidth = (int)pano->getWidth(); const int panoHeight = (int)pano->getHeight(); /* supporting equirectangular outputs only for the moment */ cv::Mat outputEquirectangularDepthImage(panoHeight, panoWidth, CV_32FC1, cv::Scalar::all(0.)); const float panoCenterX = panoWidth / 2.0f; const float panoCenterY = panoHeight / 2.0f; const Core::TransformStack::GeoTransform* transform0 = transforms[0].get(); /* Parallel execution using C++11 lambda. */ outputEquirectangularDepthImage.forEach<float>([&](float& pixel, const int position[]) -> void { const Core::CenterCoords2 panoCoords(position[1] - panoCenterX, position[0] - panoCenterY); const Core::SphericalCoords3 refPoint3d = transform0->mapPanoramaToRigSpherical(panoCoords); computeMinimumStitchingDistanceFor3DPoint(refPoint3d, inputCenters, transforms, pano, pixel); }); cv::Mat output8bits; if (imageMaxOutputDepth > 0) { outputEquirectangularDepthImage.convertTo(output8bits, CV_8UC1, 255. / imageMaxOutputDepth); } else { Logger::get(Logger::Error) << "Invalid max output depth " << imageMaxOutputDepth << " given to convert floating point depth to 8 bit gray-scale intensities" << std::endl; } return output8bits; } /* Computes the minimum stitching distance by randomly picking points on the sphere */ static float computeMinimumStitchingDistanceByRandomPoints( const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano) { float minStitchingDistance = std::numeric_limits<float>::max(); /* Needs translations between cameras to work */ if (pano->hasTranslations()) { float maxStitchingDistance = 0.0f; const int target_n_pairs = POINT_PICKING_TARGET_N_PAIRS; const int max_trials = POINT_PICKING_MAX_TRIALS; int n_pairs = 0; std::seed_seq seed{1, 2, 3, 4, 5, 6, 7, 8, 9}; std::mt19937 gen(seed); std::uniform_real_distribution<double> distribution_theta(0, 2 * M_PI); std::uniform_real_distribution<double> distribution_u(-1, 1); // Randomly generate 3D points to determine their minimum stitching distance (which is the distance where they will // no longer be seen by two cameras) for (int trial = 0; trial <= max_trials && n_pairs <= target_n_pairs; ++trial) { // Generate a point on the unit sphere using sphere point picking, see // http://mathworld.wolfram.com/SpherePointPicking.html double theta = distribution_theta(gen); double u = distribution_u(gen); const Core::SphericalCoords3 refPoint3d(static_cast<float>(std::sqrt(1. - u * u) * std::cos(theta)), static_cast<float>(std::sqrt(1. - u * u) * std::sin(theta)), static_cast<float>(u)); float stitchingDistance; if (computeMinimumStitchingDistanceFor3DPoint(refPoint3d, inputCenters, transforms, pano, stitchingDistance)) { // Get the max of and min of minimum stitching distance if (minStitchingDistance > stitchingDistance) { minStitchingDistance = stitchingDistance; } if (maxStitchingDistance < stitchingDistance) { maxStitchingDistance = stitchingDistance; } ++n_pairs; } } } return minStitchingDistance; } /* Load input images given an array of frame numbers */ static Status loadInputImages(std::map<frameid_t, std::vector<cv::Mat>>& inputImages, const Core::PanoDefinition* pano, const std::vector<frameid_t>& frameNumbers) { inputImages.clear(); auto container = Core::ControllerInputFrames<PixelFormat::RGBA, uint32_t>::create(pano); FAIL_RETURN(container.status()); for (auto& frameNumber : frameNumbers) { std::map<readerid_t, PotentialValue<GPU::HostBuffer<uint32_t>>> loadedFrames; FAIL_RETURN(container->seek(frameNumber)); container->load(loadedFrames); /*Load input images for this frame number*/ for (const auto& loadedFrame : loadedFrames) { readerid_t inputId = loadedFrame.first; auto potLoadedFrame = loadedFrame.second; FAIL_RETURN(potLoadedFrame.status()); GPU::HostBuffer<uint32_t> frame = potLoadedFrame.value(); /* Get the size of the current image */ const Core::InputDefinition& inputDef = pano->getInput(inputId); const int width = static_cast<int>(inputDef.getWidth()); const int height = static_cast<int>(inputDef.getHeight()); auto potHostFrame = GPU::HostBuffer<unsigned char>::allocate(frame.numElements() * 4, "EpipolarCurves frame loading"); FAIL_RETURN(potHostFrame.status()); GPU::HostBuffer<unsigned char> hostFrame = potHostFrame.value(); std::memcpy(hostFrame.hostPtr(), frame.hostPtr(), frame.byteSize()); cv::Mat bgrImage; cv::Mat rgbaImage(cv::Size(width, height), CV_8UC4, frame.hostPtr(), cv::Mat::AUTO_STEP); cv::cvtColor(rgbaImage, bgrImage, CV_RGBA2BGR); inputImages[frameNumber].push_back(bgrImage); hostFrame.release(); } } return Status::OK(); } static Status extractKeyPoints( std::map<std::pair<videoreaderid_t, videoreaderid_t>, Core::ControlPointList>& matchedPointsMap, std::map<videoreaderid_t, std::vector<Core::TopLeftCoords2>>& pointsMap, const std::vector<cv::Mat>& inputImages, const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano, const double decimationCellFactor) { /* Extraction and description of features on a list of images */ Calibration::KeypointExtractor kpExtractor(2, 2, 0.001); Calibration::KeypointMatcher kpMatcher(0.99); std::vector<Calibration::KPList> keypoints; std::vector<Calibration::DescriptorList> descriptors; keypoints.resize(inputImages.size()); descriptors.resize(inputImages.size()); /*Loop over cameras*/ for (videoreaderid_t camId = 0; camId < (videoreaderid_t)inputImages.size(); ++camId) { /*Do the real extraction part*/ FAIL_RETURN(kpExtractor.extract(inputImages[camId], keypoints[camId], descriptors[camId], cv::Mat())); } /*Perform matching for all pairs*/ for (videoreaderid_t camId1 = 0; camId1 < (videoreaderid_t)inputImages.size() - 1; ++camId1) { for (videoreaderid_t camId2 = camId1 + 1; camId2 < (videoreaderid_t)inputImages.size(); ++camId2) { /*Raw blind matching*/ Core::ControlPointList matched, validated, decimated; std::pair<videoreaderid_t, videoreaderid_t> pair{camId1, camId2}; FAIL_RETURN(kpMatcher.match(0, pair, keypoints[camId1], descriptors[camId1], keypoints[camId2], descriptors[camId2], matched)); /*Validate the matches by reprojecting them*/ for (Core::ControlPoint& cp : matched) { const Core::TopLeftCoords2 pointCam1(static_cast<float>(cp.x0), static_cast<float>(cp.y0)); const Core::TopLeftCoords2 pointCam2(static_cast<float>(cp.x1), static_cast<float>(cp.y1)); const Core::CenterCoords2 centeredPointCam1(pointCam1, inputCenters[camId1]); const Core::CenterCoords2 centeredPointCam2(pointCam2, inputCenters[camId2]); // Up-lifting points to spheres far away const Core::SphericalCoords3 spherePointCam1 = transforms[camId1]->mapInputToRigSpherical( pano->getVideoInput(camId1), centeredPointCam1, 0, MAX_HANDLED_DEPTH); const Core::CenterCoords2 centeredPointCam1InCam2 = transforms[camId2]->mapRigSphericalToInput(pano->getVideoInput(camId2), spherePointCam1, 0); const Core::TopLeftCoords2 pointCam1InCam2(centeredPointCam1InCam2, inputCenters[camId2]); /* Reject points if reprojection is too far */ if (distance2D(pointCam2, pointCam1InCam2) > 200.f) { continue; } cp.rx0 = pointCam1InCam2.x; cp.ry0 = pointCam1InCam2.y; validated.push_back(cp); } /*Sort and decimate the ControlPoints to limit their density*/ validated.sort(Core::ControlPointComparator()); Calibration::decimateSortedControlPoints(decimated, validated, pano->getVideoInput(camId1).getWidth(), pano->getVideoInput(camId1).getHeight(), decimationCellFactor); Logger::get(Logger::Verbose) << "Found " << matched.size() << " rough matched points between camera #" << camId1 << " and camera #" << camId2 << std::endl; Logger::get(Logger::Verbose) << "Validated " << validated.size() << " matched points between camera #" << camId1 << " and camera #" << camId2 << std::endl; Logger::get(Logger::Verbose) << "Decimated to " << decimated.size() << " points" << std::endl; /*Merging result*/ matchedPointsMap[pair].insert(matchedPointsMap[pair].end(), decimated.begin(), decimated.end()); /*Add matched points to single points map*/ for (const auto& it : decimated) { pointsMap[it.index0].push_back(Core::TopLeftCoords2(static_cast<float>(it.x0), static_cast<float>(it.y0))); pointsMap[it.index1].push_back(Core::TopLeftCoords2(static_cast<float>(it.x1), static_cast<float>(it.y1))); } } } return Status::OK(); } /* Returns a map of epipolar curves per camera, given a camera ID and 2D point */ static void computeEpipolarCurves(std::map<videoreaderid_t, std::vector<cv::Point>>& curveMap, const std::pair<videoreaderid_t, Core::TopLeftCoords2>& point, const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano) { const videoreaderid_t fromId = point.first; const Core::TopLeftCoords2 topLeftPoint = point.second; const Core::CenterCoords2 centerPoint(topLeftPoint, inputCenters[fromId]); curveMap.clear(); for (videoreaderid_t camId = 0; camId < pano->numVideoInputs(); camId++) { if (fromId == camId) { continue; } // minimum distance for which lifting points and reprojecting them is meaningful // (the sphere at this distance should contain both camera centers of projection) const float minDistance = std::max(transforms[fromId]->computeInputMinimumRigSphereRadius(pano->getVideoInput(fromId), 0), transforms[camId]->computeInputMinimumRigSphereRadius(pano->getVideoInput(camId), 0)); Core::TopLeftCoords2 lastAddedPoint; for (float distance = MAX_HANDLED_DEPTH; distance >= minDistance; (distance <= 3.f) ? distance -= .01f : distance -= .5f) { Core::SphericalCoords3 scaledPoint3d = transforms[fromId]->mapInputToRigSpherical(pano->getVideoInput(fromId), centerPoint, 0, distance); Core::CenterCoords2 centerProjected = transforms[camId]->mapRigSphericalToInput(pano->getVideoInput(camId), scaledPoint3d, 0); Core::TopLeftCoords2 topLeftProjected(centerProjected, inputCenters[camId]); if (transforms[camId]->isWithinInputBounds(pano->getVideoInput(camId), topLeftProjected)) { // add point only if there is at least a one pixel difference with the last added one if (curveMap[camId].empty() || distance2D(lastAddedPoint, topLeftProjected) >= 1.f) { lastAddedPoint = topLeftProjected; curveMap[camId].push_back(cv::Point(static_cast<int>(std::round(topLeftProjected.x)), static_cast<int>(std::round(topLeftProjected.y)))); } } } } } /* Draws the epipolar curves given a map of points in several cameras */ static void drawEpipolarCurves(std::vector<cv::Mat>& inputImages, const std::map<videoreaderid_t, std::vector<Core::TopLeftCoords2>>& pointsMap, const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano) { /* For each camera in map */ for (const auto& it : pointsMap) { videoreaderid_t refId = it.first; cv::Scalar color = colors[refId % (sizeof(colors) / sizeof(colors[0]))]; /* For each point for this camera */ for (const auto& topLeftPoint : it.second) { std::map<videoreaderid_t, std::vector<cv::Point>> curveMap; /* Compute epipolar curves in other cameras */ computeEpipolarCurves(curveMap, {refId, topLeftPoint}, inputCenters, transforms, pano); /* Do the actual drawing */ for (videoreaderid_t camId = 0; camId < pano->numVideoInputs(); camId++) { if (refId == camId) { cv::circle( inputImages[refId], cv::Point(static_cast<int>(std::round(topLeftPoint.x)), static_cast<int>(std::round(topLeftPoint.y))), 10, color, 2); } else { if (!curveMap[camId].empty()) { /* If curve has a single point, draw a cross, else the curve */ if (curveMap[camId].size() == 1) { cv::line(inputImages[camId], curveMap[camId][0] + cv::Point(-10, 0), curveMap[camId][0] + cv::Point(10, 0), color, 2); cv::line(inputImages[camId], curveMap[camId][0] + cv::Point(0, -10), curveMap[camId][0] + cv::Point(0, 10), color, 2); } else { cv::polylines(inputImages[camId], curveMap[camId], false, color, 2); } } } } } } } /* Computes and draws the depth for each point in map */ static void computeAndDrawDepths( std::vector<cv::Mat>& inputImages, const std::map<std::pair<videoreaderid_t, videoreaderid_t>, Core::ControlPointList>& matchedPointsMap, const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano) { // Compute depth of matched points by intersecting rays out of cameras for (const auto& matchedPoint : matchedPointsMap) { const videoreaderid_t camId1 = matchedPoint.first.first; const videoreaderid_t camId2 = matchedPoint.first.second; for (const Core::ControlPoint& cp : matchedPoint.second) { const Core::TopLeftCoords2 pointCam1(static_cast<float>(cp.x0), static_cast<float>(cp.y0)); const Core::TopLeftCoords2 pointCam2(static_cast<float>(cp.x1), static_cast<float>(cp.y1)); const Core::CenterCoords2 centeredPointCam1(pointCam1, inputCenters[camId1]); const Core::CenterCoords2 centeredPointCam2(pointCam2, inputCenters[camId2]); // Up-lifting points to spheres of radius 1.0 and 2.0 const Core::SphericalCoords3 firstScaledPointCam1 = transforms[camId1]->mapInputToScaledCameraSphereInRigBase( pano->getVideoInput(camId1), centeredPointCam1, 0, 1.0); const Core::SphericalCoords3 secondScaledPointCam1 = transforms[camId1]->mapInputToScaledCameraSphereInRigBase( pano->getVideoInput(camId1), centeredPointCam1, 0, 2.0); const Core::SphericalCoords3 firstScaledPointCam2 = transforms[camId2]->mapInputToScaledCameraSphereInRigBase( pano->getVideoInput(camId2), centeredPointCam2, 0, 1.0); const Core::SphericalCoords3 secondScaledPointCam2 = transforms[camId2]->mapInputToScaledCameraSphereInRigBase( pano->getVideoInput(camId2), centeredPointCam2, 0, 2.0); // Get distance of closest points between rays (or skew lines) // https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points // Reusing the notations of the Wikipedia article // Get vectors of rays out of cameras // Using cv::Vec3d for points to ease arithmetic operations on them const cv::Vec3d p1(firstScaledPointCam1.x, firstScaledPointCam1.y, firstScaledPointCam1.z); const cv::Vec3d p2(firstScaledPointCam2.x, firstScaledPointCam2.y, firstScaledPointCam2.z); // Ray unit vectors out of the cameras (note that they have unit norm) const cv::Vec3d d1(secondScaledPointCam1.x - firstScaledPointCam1.x, secondScaledPointCam1.y - firstScaledPointCam1.y, secondScaledPointCam1.z - firstScaledPointCam1.z); const cv::Vec3d d2(secondScaledPointCam2.x - firstScaledPointCam2.x, secondScaledPointCam2.y - firstScaledPointCam2.y, secondScaledPointCam2.z - firstScaledPointCam2.z); const double u = d1.dot(d2); if (std::abs(u - 1.) > std::numeric_limits<double>::epsilon()) { cv::Vec3d n = d1.cross(d2); cv::Vec3d n1 = d1.cross(n); cv::Vec3d n2 = d2.cross(n); const double t1 = (p2 - p1).dot(n2) / d1.dot(n2); const double t2 = (p1 - p2).dot(n1) / d2.dot(n1); // c1 and c2 form the shortest line segment joining skew lines from cam1 and cam2 cv::Vec3d c1 = p1 + t1 * d1; cv::Vec3d c2 = p2 + t2 * d2; /* Depth of closest points on skew lines in world space */ const double depthCam1 = std::sqrt(c1.dot(c1)); const double depthCam2 = std::sqrt(c2.dot(c2)); Logger::get(Logger::Debug) << "distance p1 p2 " << std::sqrt((c1 - c2).dot(c1 - c2)) << std::endl; // Reprojecting centeredPointCam1 onto Cam2, to know if we are within the input bounds const double scaleForPointCam1 = t1 + 1 /* because t1 is the distance starting at firstScaledPointCam1, already at camera sphere scale 1 */; const Core::SphericalCoords3 pointCam1ToD1 = transforms[camId1]->mapInputToScaledCameraSphereInRigBase( pano->getVideoInput(camId1), centeredPointCam1, 0, static_cast<float>(scaleForPointCam1)); const Core::CenterCoords2 pointCam1ToCam2 = transforms[camId2]->mapRigSphericalToInput(pano->getVideoInput(camId2), pointCam1ToD1, 0); const Core::TopLeftCoords2 reprojected(pointCam1ToCam2, inputCenters[camId2]); Logger::get(Logger::Debug) << "cam1 to cam2 " << reprojected.x << ", " << reprojected.y << ", cam2 " << cp.x1 << ", " << cp.y1 << ", distance " << distance2D(reprojected, pointCam2) << ", rx0, ry0 " << cp.rx0 << ", " << cp.ry0 << ", distance " << distance2D(cv::Point2f(static_cast<float>(cp.rx0), static_cast<float>(cp.ry0)), pointCam2) << std::endl; Logger::get(Logger::Debug) << "depth 1 " << depthCam1 << std::endl; Logger::get(Logger::Debug) << "depth 2 " << depthCam2 << std::endl; cv::Scalar colorCamId1 = colors[camId1 % (sizeof(colors) / sizeof(colors[0]))]; cv::Scalar colorCamId2 = colors[camId2 % (sizeof(colors) / sizeof(colors[0]))]; /* Draw the depth value in centimeters next to the points */ if (transforms[camId2]->isWithinInputBounds(pano->getVideoInput(camId2), reprojected)) { cv::putText(inputImages[camId1], std::to_string(int(std::round(depthCam1 * 100.f))), cv::Point(static_cast<int>(std::round(pointCam1.x + 20.f)), static_cast<int>(std::round(pointCam1.y - 10.f))), cv::FONT_HERSHEY_SIMPLEX, 1, colorCamId1, 2, cv::LINE_AA); cv::putText(inputImages[camId2], std::to_string(int(std::round(depthCam2 * 100.f))), cv::Point(static_cast<int>(std::round(pointCam2.x + 20.f)), static_cast<int>(std::round(pointCam2.y - 10.f))), cv::FONT_HERSHEY_SIMPLEX, 1, colorCamId2, 2, cv::LINE_AA); } else { cv::putText(inputImages[camId1], "unknown", cv::Point(static_cast<int>(std::round(pointCam1.x + 20.f)), static_cast<int>(std::round(pointCam1.y - 10.f))), cv::FONT_ITALIC, 1, colorCamId1, 2, cv::LINE_AA); cv::putText(inputImages[camId2], "unknown", cv::Point(static_cast<int>(std::round(pointCam2.x + 20.f)), static_cast<int>(std::round(pointCam2.y - 10.f))), cv::FONT_ITALIC, 1, colorCamId2, 2, cv::LINE_AA); } } } } } /* Computes and draws a spherical grid in the input pictures */ static void computeAndDrawSphericalGrid( std::vector<cv::Mat>& inputImages, const float sphereScale, const std::vector<Core::TopLeftCoords2>& inputCenters, const std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>>& transforms, const Core::PanoDefinition* pano) { float longitude, latitude; videoreaderid_t camId; std::vector<cv::Point> curve; auto drawAndFlushCurve = [&]() { const int npts = static_cast<int>(curve.size()); if (npts) { const cv::Point* pts = curve.data(); cv::polylines(inputImages[camId], &pts, &npts, 1, false, colors[camId]); curve.clear(); } }; /* Lambda function to add a point to a curve if it falls within input picture, and draw and flush the curve if it * falls outside */ auto addPointToCurve = [&]() { Core::SphericalCoords3 scaledPoint3d(sphereScale * std::cos(latitude) * std::sin(longitude), sphereScale * std::sin(latitude), sphereScale * std::cos(latitude) * std::cos(longitude)); if (!isMappable(scaledPoint3d)) { return; } Core::CenterCoords2 centerProjected = transforms[camId]->mapRigSphericalToInput(pano->getVideoInput(camId), scaledPoint3d, 0); Core::TopLeftCoords2 topLeftProjected(centerProjected, inputCenters[camId]); if (transforms[camId]->isWithinInputBounds(pano->getVideoInput(camId), topLeftProjected)) { curve.push_back(cv::Point(static_cast<int>(std::round(topLeftProjected.x)), static_cast<int>(std::round(topLeftProjected.y)))); } else { drawAndFlushCurve(); } }; const float F_PI = static_cast<float>(M_PI); for (camId = 0; camId < pano->numVideoInputs(); camId++) { /* Project 20 latitude lines in the camId input picture */ for (latitude = -F_PI / 2; latitude <= F_PI / 2; latitude += F_PI / 20) { for (longitude = -F_PI; longitude < F_PI; longitude += 2 * F_PI / 2000) { addPointToCurve(); } drawAndFlushCurve(); } /* Then project 20 longitude lines */ for (longitude = -F_PI; longitude < F_PI; longitude += 2 * F_PI / 20) { for (latitude = -F_PI / 2; latitude <= F_PI / 2; latitude += F_PI / 2000) { addPointToCurve(); } drawAndFlushCurve(); } } } Potential<Ptv::Value> EpipolarCurvesAlgorithm::apply(Core::PanoDefinition* pano, ProgressReporter*, Util::OpaquePtr**) const { if (!epipolarCurvesConfig.isValid()) { return {Origin::EpipolarCurvesAlgorithm, ErrType::InvalidConfiguration, "Invalid configuration for algorithm"}; } /* Prepare transforms and input centers coordinates */ std::vector<std::unique_ptr<Core::TransformStack::GeoTransform>> transforms; std::vector<Core::TopLeftCoords2> inputCenters; for (const auto& videoInputDef : pano->getVideoInputs()) { transforms.push_back(std::unique_ptr<Core::TransformStack::GeoTransform>( Core::TransformStack::GeoTransform::create(*pano, videoInputDef))); inputCenters.push_back(Core::TopLeftCoords2(static_cast<float>(videoInputDef.get().getWidth() / 2), static_cast<float>(videoInputDef.get().getHeight() / 2))); } /* Get minimum stitching distance by randomly picking points on the sphere */ const float minStitchingDistance = computeMinimumStitchingDistanceByRandomPoints(inputCenters, transforms, pano); Logger::get(Logger::Info) << "Minimum stitching distance computed from Pano geometry " << minStitchingDistance << std::endl; /* Get minimum stitching distance for every panorama output point and generate a picture */ const cv::Mat minStitchingDistanceInOutputPanoram = computeMinimumStitchingDistancePerPointInOutputPanorama( inputCenters, transforms, pano, epipolarCurvesConfig.getImageMaxOutputDepth()); FAIL_RETURN(dumpImageFile(minStitchingDistanceInOutputPanoram, "output_min_stitching_distance.png")); /* Draw depth for each point in map by triangulation, if we have translations between cameras */ /* Load input images */ std::vector<frameid_t> frameNumbers = epipolarCurvesConfig.getFrames(); std::map<frameid_t, std::vector<cv::Mat>> inputImagesMap; FAIL_RETURN(loadInputImages(inputImagesMap, pano, frameNumbers)); /* For each input frame number, extract keypoints, draw epipolar curves and depths */ for (auto& frameNumber : epipolarCurvesConfig.getFrames()) { assert(pano->numVideoInputs() == (videoreaderid_t)inputImagesMap[frameNumber].size()); std::map<videoreaderid_t, std::vector<Core::TopLeftCoords2>> pointsMap = epipolarCurvesConfig.getSinglePointsMap(); std::map<std::pair<videoreaderid_t, videoreaderid_t>, Core::ControlPointList> matchedPointsMap = epipolarCurvesConfig.getMatchedPointsMap(); if (epipolarCurvesConfig.getIsAutoPointMatching()) { FAIL_RETURN(extractKeyPoints(matchedPointsMap, pointsMap, inputImagesMap[frameNumber], inputCenters, transforms, pano, epipolarCurvesConfig.getDecimationCellFactor())); } /* Draw epipolar curves for each point in map */ drawEpipolarCurves(inputImagesMap[frameNumber], pointsMap, inputCenters, transforms, pano); /* Draw depth for each point in map, if we have translations between cameras */ if (pano->hasTranslations()) { computeAndDrawDepths(inputImagesMap[frameNumber], matchedPointsMap, inputCenters, transforms, pano); } /* Draw spherical grid */ if (epipolarCurvesConfig.getSphericalGridRadius() > 0) { computeAndDrawSphericalGrid(inputImagesMap[frameNumber], static_cast<float>(epipolarCurvesConfig.getSphericalGridRadius()), inputCenters, transforms, pano); } /* Save input images */ for (videoreaderid_t camid = 0; camid < (videoreaderid_t)inputImagesMap[frameNumber].size(); camid++) { FAIL_RETURN(dumpImageFile( inputImagesMap[frameNumber][camid], pano->getVideoInput(camid).getReaderConfig().asString() + "_frame_" + std::to_string(frameNumber) + ".png")) } } /*Create the result*/ Potential<Ptv::Value> ret(Ptv::Value::emptyObject()); ret->push("minStitchingDistance", new Parse::JsonValue(minStitchingDistance)); return ret; } } // namespace EpipolarCurves } // namespace VideoStitch