メインコンテンツ

Reconstruct 3-D Scene from Geometrically Refined Pair of Initial Views

Since R2026a

This example shows how to initialize a reconstruction from the view graph refined in the Refine View Graph Using Geometric Verification example by estimating the relative pose between the two views, triangulating 3-D points and then validating the pair based using multi-view geometry criteria commonly used in Structure from Motion (SfM).

The reconstruction begins with a carefully chosen two-view initialization [1]. The quality of this pair strongly affects robustness, accuracy, and runtime of the subsequent stages. Errors introduced at this step can propagate and become difficult to correct later. A strong initial pair typically comes from a densely connected region of a refined view graph, where both images share substantial overlap in the scene. This redundancy stabilizes relative pose estimation and triangulation. A good initial pair generally:

  • Has a large number of geometrically verified feature matches that are uniform over the spatial extent of the image.

  • Exhibits a sufficiently wide baseline (i.e., larger triangulation angles) to enable stable depth estimation.

  • Produces many triangulated 3-D points with low reprojection error.

Load Data

First, ensure that the images are available on the path.

% Check if the image datastore exists. If not, load images.
if ~exist("imds", "var")
    downloadFolder = tempdir;
    imageFolder = fullfile(downloadFolder, "sfmTrainingDataTUMRGBD", "images");
    imds = imageDatastore(imageFolder);
end

Load the view graph refined from the Refine View Graph Using Geometric Verification example.

% Check if a view graph exists in workspace. If not, load it from MAT file.
if ~exist("viewGraph", "var")
    viewGraphFile = fullfile(downloadFolder, "sfmTrainingDataTUMRGBD", "refinedViewGraph.mat");
    viewGraphData = load(viewGraphFile);
    viewGraph = viewGraphData.viewGraph;
end

Load camera intrinsic parameters, which will be used later in estimation of the essential matrix.

% Load camera intrinsics
intrinsicsFile = fullfile(downloadFolder, "sfmTrainingDataTUMRGBD", "cameraInfo.mat");
data = load(intrinsicsFile);
intrinsics = data.intrinsics;

Select Strongly-connected View Pairs in View Graph

Select edges in the view graph that have a large number of geometrically verified matches. A common threshold is 100–200 matches, depending on image resolution. For 480-by-640 images in this example, we use 100. More inliers generally make the relative pose estimate more robust.

minNumMatchesInitial = 100;
isStrongEdge = cellfun(@(x)(size(x,1)>minNumMatchesInitial), viewGraph.Connections.Matches);

numSelectedConn = nnz(isStrongEdge);
selectedConnIdx = find(isStrongEdge); % convert to linear indices

disp(numSelectedConn + " of " + viewGraph.NumConnections +...
" edges have more than " + minNumMatchesInitial + " feature matches.");
349 of 429 edges have more than 100 feature matches.

Select View Pairs with Uniformly Distributed Matched Feature Points

Select image pairs whose inlier features are uniformly spread across images. This prevents degenerate conditions that can cause triangulation to fail.

numImages = numel(imds.Files);

% Display the selected connection
idx = 151;
connId = selectedConnIdx(idx);
matchedPairs = viewGraph.Connections.Matches{connId};

% View indexes for the two views being considered
viewIds1 = viewGraph.Connections.ViewId1;
viewIds2 = viewGraph.Connections.ViewId2;

viewId1 = viewIds1(connId);
viewId2 = viewIds2(connId);
view1   = findView(viewGraph, viewId1);
view2   = findView(viewGraph, viewId2);
points1 = view1.Points{:};
points2 = view2.Points{:};

% Get the matched SIFT points
matchedPoints1 = points1(matchedPairs(:,1));
matchedPoints2 = points2(matchedPairs(:,2));

% Read image pair
I1 = readimage(imds, viewId1);
I2 = readimage(imds, viewId2);

% Create a 4-by-4 grid on the image and plot each line
numCols = 4;
numRows = 4;
[H, W] = size(I1, [1 2]);
[rowIdx, colIdx, lines] = helperImageGridIndices(H, W, numRows,numCols);

% Count the number of grids that contain at least one feature point
gridCounts1 = helperCountKeypointsPerGrid(rowIdx, colIdx, matchedPoints1);
gridCounts2 = helperCountKeypointsPerGrid(rowIdx, colIdx, matchedPoints2);

% Display the selected images
figure
subplot(1,2,1)
imshow(I1);
hold on

% Plot the feature point locations
plot(matchedPoints1, ShowScale=false);
xCoords = [lines(:,1), lines(:,3)]' - 0.5;
yCoords = [lines(:,2), lines(:,4)]' - 0.5;
plot(xCoords, yCoords, "w-", LineWidth=2);
title("Image: " + viewId1 + ", Non-empty grids = " + nnz(gridCounts1));

subplot(1,2,2)
imshow(I2);

% Plot the feature point locations
hold on
plot(matchedPoints2, ShowScale=false);
xCoords = [lines(:,1), lines(:,3)]' - 0.5;
yCoords = [lines(:,2), lines(:,4)]' - 0.5;
plot(xCoords, yCoords, "w-", LineWidth=2);

title("Image: " + viewId2 + ", Non-empty grids = " + nnz(gridCounts2));

Figure contains 2 axes objects. Hidden axes object 1 with title Image: 38, Non-empty grids = 11 contains 8 objects of type image, line. One or more of the lines displays its values using only markers Hidden axes object 2 with title Image: 39, Non-empty grids = 15 contains 8 objects of type image, line. One or more of the lines displays its values using only markers

Apply this coverage test to every selected edge. If fewer than 50% of the grid cells are non-empty, exclude that edge from initialization.

uniformConnIdx = [];
minGridCount = 8; % 8 out of 16 grid cells should have at least 1 inlier keypoint
for idx = 1:numSelectedConn
    connId = selectedConnIdx(idx);
    matchedPairs = viewGraph.Connections.Matches{connId};

    % View indexes for the two views being considered
    viewId1 = viewIds1(connId);
    viewId2 = viewIds2(connId);
    view1   = findView(viewGraph, viewId1);
    view2   = findView(viewGraph, viewId2);
    points1 = view1.Points{:};
    points2 = view2.Points{:};

    % Get the matched SIFT points
    matchedPoints1 = points1(matchedPairs(:,1));
    matchedPoints2 = points2(matchedPairs(:,2));

    % Read image pair
    I1 = readimage(imds, viewId1);
    I2 = readimage(imds, viewId2);

    gridCounts1 = helperCountKeypointsPerGrid(rowIdx, colIdx, matchedPoints1);
    gridCounts2 = helperCountKeypointsPerGrid(rowIdx, colIdx, matchedPoints2);

    if nnz(gridCounts1) > minGridCount && nnz(gridCounts2) > minGridCount
        uniformConnIdx = [uniformConnIdx; connId]; %#ok<*AGROW>
    end
end
numUniformConns = numel(uniformConnIdx);
disp(numUniformConns + " of " + numSelectedConn + " are selected after applying the grid threshold.")
324 of 349 are selected after applying the grid threshold.

Initialize Two-View Reconstruction for an Image Pair

After identifying inlier feature matches between two views, evaluate the geometric relationship using two transformation models: homography and essential matrix. The transform that results in a smaller reprojection error is selected to estimate the relative rotation and translation using estrelpose. Because the images are captured with a monocular camera, depth information is not directly available, and the estimated translation is only recoverable up to scale. Additionally, filter out configurations with near-pure forward motion, as they tend to produce low parallax which lead to inaccurate depth estimation and unreliable triangulation.

% Select an edge connecting two views from the remaining set of edges
idx = 90;
connId  = uniformConnIdx(idx);
viewId1 = viewIds1(connId);
viewId2 = viewIds2(connId);
matchedPairs = viewGraph.Connections.Matches{connId};

% Get the keypoints for that view
view1   = findView(viewGraph, viewId1);
view2   = findView(viewGraph, viewId2);
points1 = view1.Points{:};
points2 = view2.Points{:};

% Get the matched SIFT points
matchedPoints1 = points1(matchedPairs(:,1));
matchedPoints2 = points2(matchedPairs(:,2));

% Suppress innocuous warnings related to RANSAC
warning("off","vision:ransac:maxTrialsReached");
warning("off","images:geotrans:transformationMatrixBadlyConditioned");

% Compute homography and evaluate reconstruction
[tformH, scoreH, inliersIndexH] = helperComputeHomography(matchedPoints1, matchedPoints2);

% Compute essential matrix and evaluate reconstruction
[tformE, scoreE, inliersIndexE] = helperComputeEssentialMatrix(matchedPoints1, matchedPoints2, intrinsics, intrinsics);

% Select the transform based on a heuristic
ratio = scoreH/(scoreH + scoreE);
ratioThreshold = 0.45;
if ratio > ratioThreshold
    inlierTformIdx = inliersIndexH;
    tform       = tformH;
else
    inlierTformIdx = inliersIndexE;
    tform       = tformE;
end

% Compute relative pose between the two views
inlierPoints1 = matchedPoints1(inlierTformIdx);
inlierPoints2 = matchedPoints2(inlierTformIdx);
matchedPairs  = matchedPairs(inlierTformIdx, :);
[relPose, validFraction] = estrelpose(tform, intrinsics, intrinsics, inlierPoints1, inlierPoints2);

% Compute forward motion ratio which quantifies how much the camera moved
% along its optical axis (viewing direction) relative to the baseline between
% two camera positions.
forwardMotionRatio = abs(dot(relPose.Translation, [0 0 1]));

% Discard if not enough inliers are found or forward motion ratio is high
minValidFraction     = 0.5;
maxFowardMotionRatio = 0.95;
isPoseValid = validFraction > minValidFraction && isscalar(relPose) &&...
    forwardMotionRatio < maxFowardMotionRatio;

if ~isPoseValid
    disp("The connection between view " + viewId1 + " and " + viewId2 + " is invalid. Select a different connection.");
    return
end

Given the relative camera pose and the matched feature points in the two images, determine the 3-D locations of the matched points using triangulate function. A triangulated 3-D point is considered valid if it meets these geometric criteria:

  • It lies in front of both cameras, indicating positive depth values.

  • It has a low reprojection error, ensuring consistency between the observed and projected image points.

  • The triangulation angle—the angle between the viewing rays from the two cameras—is sufficiently large, which improves depth accuracy and stability.

To ensure robustness against matching outliers, the median triangulation angle across all triangulated points should also be large. This helps maintain reliable depth estimation and prevents degenerate configurations caused by matching outliers.

% Thresholds for selecting the initial pair of views
% minMedianTriAngle - Minimum acceptable median triangulation angle across 3-D points from the two views
% minTriAnglePoint  - Minimum triangulation angle required for a 3-D point to be considered valid
% maxReprojError    - Maximum allowable reprojection error for a triangulated 3-D point to be valid
minMedianTriAngle = 16;  % in degrees
minTriAnglePoint  = 1.5; % in degrees
maxReprojError    = 4;   % in pixels

% Triangulate the two views to obtain 3-D world points
pose1 = rigidtform3d();
pose2 = relPose;

% Get camera projection matrix
camMatrix1 = cameraProjection(intrinsics, pose2extr(pose1));
camMatrix2 = cameraProjection(intrinsics, pose2extr(pose2));

% Triangulate two views to obtain 3-D map points
[xyzPoints, reprojectionErrors, isInFront] = triangulate(inlierPoints1, inlierPoints2, camMatrix1, camMatrix2);

% Compute triangulation angles in degrees for each triangulate 3-D point
ray1       = xyzPoints - pose1.Translation;
ray2       = xyzPoints - pose2.Translation;
triAngles  = acosd(sum(ray1 .* ray2, 2) ./ (vecnorm(ray1, 2, 2) .* vecnorm(ray2, 2, 2)));

% Filter points by view direction and reprojection error
inlierTriangulationIdx  = isInFront & reprojectionErrors < maxReprojError & triAngles > minTriAnglePoint;
xyzPoints  = xyzPoints(inlierTriangulationIdx ,:);
matchedPairs = matchedPairs(inlierTriangulationIdx, :);

% Check parallax in terms of median triangulation angle
medianAngle = median(triAngles);
isValid = medianAngle > minMedianTriAngle;

Visualize the images and the triangulated 3-D points.

% Load the image for the specified view ID
I1 = readimage(imds, viewId1);
I2 = readimage(imds, viewId2);

figure
imshowpair(I1, I2, "montage");
title("Images: " + viewId1 + " and " + viewId2);
truesize

Figure contains an axes object. The hidden axes object with title Images: 12 and 97 contains an object of type image.

figure
% Get color of 3-D points from the two images
colors = helperGetPointColorsFromTwoViews(I1, I2, viewId1, viewId2, viewGraph, matchedPairs);

% Plot 3-D points
pcshow(xyzPoints, colors, MarkerSize=30);
xlabel("X")
ylabel("Y")
zlabel("Z")
hold on

% Plot cameras
plotCamera([pose1, pose2], Label={num2str(viewId1), num2str(viewId2)}, Size=0.1);
title("Triangulated points");

Figure contains an axes object. The axes object with title Triangulated points, xlabel X, ylabel Y contains 21 objects of type line, text, patch, scatter.

Apply the two-view reconstruction procedure to all image pairs in the refined view graph using the helper function helperReconstructTwoView, which encapsulates the validation steps described in the previous section. For each pair, estimate the relative pose, triangulate 3-D points, and validate the reconstruction based on geometric criteria. After processing all pairs, select the pair that yields the highest number of valid triangulated 3-D points. If no pair meets the required criteria gradually relax the median triangulation angle threshold to allow for a viable initialization while maintaining geometric robustness.

% Select initial view pair
initConnId        = [];
initTriAngle      = [];
initXYZPoints     = [];
initRelPose       = [];
initMatches       = [];
initInlierIndices = [];

rng(0); % For reproducibility

for idx = 1:numUniformConns
    connId  = uniformConnIdx(idx);
    viewId1 = viewIds1(connId);
    viewId2 = viewIds2(connId);
    matchedPairs = viewGraph.Connections.Matches{connId};

    % Get the keypoints for that view
    view1   = findView(viewGraph, viewId1);
    view2   = findView(viewGraph, viewId2);
    points1 = view1.Points{:};
    points2 = view2.Points{:};

    [isValid, xyzWorldPoints, inlierMatchIndices, medianTriAngle, relPose] = ...
        helperReconstructTwoView(points1, points2, matchedPairs, intrinsics,...
        MaxReprojectionError        = maxReprojError, ...
        MinTriangulationAnglePoint  = minTriAnglePoint, ...
        MinMedianTriangulationAngle = minMedianTriAngle, ...
        MinValidFraction            = minValidFraction, ...
        MaxFowardMotionRatio        = maxFowardMotionRatio);

    if isValid
        % Pick the pair with most high-quality triangulated points.
        if nnz(inlierMatchIndices) > nnz(initInlierIndices)
            initConnId        = connId;
            initTriAngle      = medianTriAngle;
            initXYZPoints     = xyzWorldPoints;
            initRelPose       = relPose;
            initMatches       = matchedPairs;
            initInlierIndices = inlierMatchIndices;
        end
    end
end

if isempty(initConnId)
    disp("No pair can be found. Reduce the value of minMedianTriAngle.");
end

% Restore warnings after the loop
warning("on","vision:ransac:maxTrialsReached");
warning("on","images:geotrans:transformationMatrixBadlyConditioned");

Visualize the initial image pair and sparse reconstruction.

viewId1 = viewIds1(initConnId);
viewId2 = viewIds2(initConnId);

% Load the image for the specified view ID
I1 = readimage(imds, viewId1);
I2 = readimage(imds, viewId2);

figure
imshowpair(I1, I2, "montage");
title("Images: " + viewId1 + " and " + viewId2);
truesize

Figure contains an axes object. The hidden axes object with title Images: 95 and 10 contains an object of type image.

figure
% Get color of 3-D points from the two images
matchedPairs = initMatches(initInlierIndices, :);
colors = helperGetPointColorsFromTwoViews(I1, I2, viewId1, viewId2, viewGraph, matchedPairs);

% Plot 3-D points
pcshow(initXYZPoints, colors, MarkerSize=30);
xlabel("X")
ylabel("Y")
zlabel("Z")
hold on

% Plot cameras
plotCamera([pose1, initRelPose], Label=string([viewId1, viewId2]), Size=0.1);
xlim([-2, 6]);
zlim([-1, 5]);
title("Median triangulation angle = " + initTriAngle);

Figure contains an axes object. The axes object with title Median triangulation angle = 21.163, xlabel X, ylabel Y contains 21 objects of type line, text, patch, scatter.

Refine Initial Reconstruction Using Bundle Adjustment

After triangulating from the initial pair, update the view graph with the estimated relative pose and add the 3-D points and their image correspondences to a worldpointset object. Then refine the initial reconstruction using bundleAdjustment, which optimizes both camera poses and world points to minimize the overall reprojection errors.

% Update the pose of the second view. The pose of the first view is assumed
% identify transform.
viewGraph = updateView(viewGraph, viewId2, initRelPose);

% Update relative pose between the initial two view and feature matching
viewGraph = updateConnection(viewGraph, viewId1, viewId2, initRelPose, Matches=matchedPairs);

% Add 3-D points
wpSet = worldpointset();
[wpSet, pointIndices] = addWorldPoints(wpSet, initXYZPoints);

% Add 3-D to 2-D correspondences
wpSet = addCorrespondences(wpSet, viewId1, pointIndices, matchedPairs(:,1));
wpSet = addCorrespondences(wpSet, viewId2, pointIndices, matchedPairs(:,2));

% Perform bundle adjustment to refine the poses of first two views and the triangulated
% 3-D point locations
[wpSet, viewGraph, mapPointIdx, reprojectionErrors] = bundleAdjustment(...
    wpSet, viewGraph, [viewId1, viewId2], intrinsics, FixedViewIDs=viewId1, ...
    PointsUndistorted=true, AbsoluteTolerance=1e-7,...
    RelativeTolerance=1e-16, Solver="preconditioned-conjugate-gradient");

After bundle adjustment, remove 3-D points with large reprojection errors or insufficient triangulation angle.

% Get refined poses of the two views
camPosesTable = poses(viewGraph, [viewId1, viewId2]);

% Compute triangulation angle in degrees of each refined 3-D point
xyzPoints = wpSet.WorldPoints(mapPointIdx,:);
ray1      = xyzPoints - camPosesTable.AbsolutePose(1).Translation;
ray2      = xyzPoints - camPosesTable.AbsolutePose(2).Translation;
triAngles = acosd(sum(ray1 .* ray2, 2) ./ (vecnorm(ray1, 2, 2) .* vecnorm(ray2, 2, 2)));

% Remove 3-D points with large reprojection error or smaller triangulation angle
isOutlier = reprojectionErrors > maxReprojError | triAngles < minTriAnglePoint;
wpSet = removeWorldPoints(wpSet, mapPointIdx(isOutlier));

Display the reconstruction after bundle adjustment.

figure
% Plot 3-D points
pcshow(wpSet.WorldPoints, colors(~isOutlier, :), MarkerSize=30);
xlabel("X")
ylabel("Y")
zlabel("Z")
hold on

% Plot cameras
plotCamera(camPosesTable, Size=0.1);
xlim([-2, 6]);
zlim([-1, 5]);
title("Median triangulation angle = " + median(triAngles(~isOutlier)));

Figure contains an axes object. The axes object with title Median triangulation angle = 20.063, xlabel X, ylabel Y contains 21 objects of type line, text, patch, scatter.

After the initial construction, process the remaining views in the view graph to expand the reconstructed 3-D scene. See the Reconstruct Complete 3-D Scene Using Incremental Structure from Motion example for the next steps.

Supporting Files

helperImageGridIndices divide an image into grids.

function [rowIdx,colIdx,lines] = helperImageGridIndices(H,W,numRows,numCols)

% Compute row edges
rowEdges = round(linspace(1, H+1, numRows+1));
% Compute col edges
colEdges = round(linspace(1, W+1, numCols+1));

% Assemble start and end indices for rows
rowIdx = [rowEdges(1:end-1)', rowEdges(2:end)'-1];

% Assemble start and end indices for columns
colIdx = [colEdges(1:end-1)', colEdges(2:end)'-1];

% Reconstruct edges for drawing grid lines
rowEdgesForLines = rowIdx(:,1);
colEdgesForLines = colIdx(:,1);
rowLinesY = rowEdgesForLines(2:end);
colLinesX = colEdgesForLines(2:end);

% Grid lines: [[x1 y1 x2 y2]; ....]
lines = [];
for y = rowLinesY'
    lines = [lines; 1 y W y];
end

for x = colLinesX'
    lines = [lines; x 1 x H];
end
end

helperCountKeypointsPerGrid count the number of keypoint located in each grid.

function gridCounts = helperCountKeypointsPerGrid(rowIdx, colIdx, points)

% Generate row and col edges from rowIdx and colIdx
% These edges cover the full extent
rowEdges = [rowIdx(:,1); rowIdx(end,2)+1];
colEdges = [colIdx(:,1); colIdx(end,2)+1];

% Extract x and y
x = points.Location(:,1);
y = points.Location(:,2);

% Bin the y coordinates to row bins
[rowBin,~] = discretize(y, rowEdges);

% Bin the x coordinates to column bins
[colBin,~] = discretize(x, colEdges);

% Remove rows with NaN bin indices
valid = ~isnan(rowBin) & ~isnan(colBin);

% Create subscripts
subs = [rowBin(valid), colBin(valid)];

% Count occurrences within each grid bin
gridCounts = accumarray(subs, 1, [size(rowIdx,1), size(colIdx,1)]);
end

helperComputeEssentialMatrix compute essential matrix and evaluate reconstruction.

function [E, score, inliersIndex] = helperComputeEssentialMatrix(matchedPoints1, matchedPoints2, intrinsics1, intrinsics2)

outlierThreshold = 4;

[E, inliersIndex, status] = estimateEssentialMatrix( ...
    matchedPoints1, matchedPoints2, intrinsics1, intrinsics2, MaxNumTrials=1e3, MaxDistance=outlierThreshold);

if status % No valid essential matrix can be found
    score = outlierThreshold*size(matchedPoints1,1);
    return
end

F = intrinsics1.K'\ E /intrinsics2.K;

inlierPoints1 = matchedPoints1(inliersIndex);
inlierPoints2 = matchedPoints2(inliersIndex);

locations1    = inlierPoints1.Location;
locations2    = inlierPoints2.Location;

% Distance from points to epipolar line
lineIn1   = epipolarLine(F', locations2);
error2in1 = (sum([locations1, ones(size(locations1, 1),1)].* lineIn1, 2)).^2 ...
    ./ sum(lineIn1(:,1:2).^2, 2);
lineIn2   = epipolarLine(F, locations1);
error1in2 = (sum([locations2, ones(size(locations2, 1),1)].* lineIn2, 2)).^2 ...
    ./ sum(lineIn2(:,1:2).^2, 2);

score = sum(max(outlierThreshold-error1in2, 0)) + ...
    sum(max(outlierThreshold-error2in1, 0));
end

helperComputeHomography compute homography and evaluate reconstruction.

function [H, score, inliersIndex] = helperComputeHomography(matchedPoints1, matchedPoints2)

outlierThreshold = 6;

[H, inliersIndex] = estgeotform2d( ...
    matchedPoints1, matchedPoints2, "projective", ...
    MaxNumTrials=1e3, MaxDistance=outlierThreshold);

inlierPoints1 = matchedPoints1(inliersIndex);
inlierPoints2 = matchedPoints2(inliersIndex);

locations1 = inlierPoints1.Location;
locations2 = inlierPoints2.Location;
xy1In2     = transformPointsForward(H, locations1);
xy2In1     = transformPointsInverse(H, locations2);
error1in2  = sum((locations2 - xy1In2).^2, 2);
error2in1  = sum((locations1 - xy2In1).^2, 2);

score = sum(max(outlierThreshold-error1in2, 0)) + ...
    sum(max(outlierThreshold-error2in1, 0));
end

helperReconstructTwoView perform 3-D reconstruction from two-views.

function [isValid, xyzPoints, inlierMatchIndices, medianTriAngle, relPose] = ...
    helperReconstructTwoView(points1,points2,matchedPairs,intrinsics,args)
arguments
    points1
    points2
    matchedPairs
    intrinsics
    args.MaxReprojectionError = 4
    args.MinTriangulationAnglePoint  = 1.5
    args.MinMedianTriangulationAngle = 16
    args.MinValidFraction            = 0.5
    args.MaxFowardMotionRatio        = 0.95
end

% Get the matched SIFT points
matchedPoints1 = points1(matchedPairs(:,1));
matchedPoints2 = points2(matchedPairs(:,2));

% Suppress innocuous warnings related to RANSAC
warning("off","vision:ransac:maxTrialsReached");
warning("off","images:geotrans:transformationMatrixBadlyConditioned");

% Compute homography and evaluate reconstruction
[tformH, scoreH, inliersIndexH] = helperComputeHomography(matchedPoints1, matchedPoints2);

% Compute essential matrix and evaluate reconstruction
[tformE, scoreE, inliersIndexE] = helperComputeEssentialMatrix(matchedPoints1, matchedPoints2, intrinsics, intrinsics);

% Select the model based on a heuristic
ratio = scoreH/(scoreH + scoreE);
ratioThreshold = 0.45;
if ratio > ratioThreshold
    inlierTformIdx = inliersIndexH;
    tform       = tformH;
else
    inlierTformIdx = inliersIndexE;
    tform       = tformE;
end

% Compute relative pose between the two views
inlierPoints1 = matchedPoints1(inlierTformIdx);
inlierPoints2 = matchedPoints2(inlierTformIdx);
[relPose, validFraction] = estrelpose(tform, intrinsics, intrinsics, inlierPoints1, inlierPoints2);

xyzPoints=[];
inlierMatchIndices=[];
medianTriAngle=[];

if ~isscalar(relPose)
    isValid = false;
    return
end

% Compute forward motion ratio which quantifies how much the camera moved
% along its optical axis (viewing direction) relative to the baseline between
% two camera positions.
forwardMotionRatio = abs(dot(relPose.Translation, [0 0 1]));

% Discard if not enough inliers are found or forward motion ratio is high
isPoseValid = validFraction > args.MinValidFraction && isscalar(relPose) &&...
    forwardMotionRatio < args.MaxFowardMotionRatio;

if ~isPoseValid
    isValid = false;
    return
end

% Triangulate the two views to obtain 3-D world points
pose1 = rigidtform3d();
pose2 = relPose;

% Get camera projection matrix
camMatrix1 = cameraProjection(intrinsics, pose2extr(pose1));
camMatrix2 = cameraProjection(intrinsics, pose2extr(pose2));

% Triangulate two views to obtain 3-D map points
[xyzPoints, reprojectionErrors, isInFront] = triangulate(inlierPoints1, inlierPoints2, camMatrix1, camMatrix2);

% Compute triangulation angles in degrees for each triangulate 3-D point
ray1       = xyzPoints - pose1.Translation;
ray2       = xyzPoints - pose2.Translation;
cosAngle   = acosd(sum(ray1 .* ray2, 2) ./ (vecnorm(ray1, 2, 2) .* vecnorm(ray2, 2, 2)));

% Filter points by view direction and reprojection error
inlierTriangulationIdx  = isInFront & reprojectionErrors < args.MaxReprojectionError & cosAngle > args.MinTriangulationAnglePoint;
xyzPoints  = xyzPoints(inlierTriangulationIdx ,:);

% Check parallax in terms of median triangulation angle
medianTriAngle = median(cosAngle);
isValid = medianTriAngle > args.MinMedianTriangulationAngle;

inlierTformIdxLinearIdx = find(inlierTformIdx);
inlierMatchIndices = inlierTformIdxLinearIdx(inlierTriangulationIdx);
end

helperGetPointColorsFromTwoViews compute color of 3-D points as the average of two images.

function colors = helperGetPointColorsFromTwoViews(I1, I2, viewId1, viewId2, viewGraph, matchedPairs)
colors = zeros(size(matchedPairs, 1), 3, "uint8");
points1 = findView(viewGraph, viewId1).Points{:};
points2 = findView(viewGraph, viewId2).Points{:};
xy1 = round(points1.Location(matchedPairs(:, 1), :));
xy2 = round(points2.Location(matchedPairs(:, 2), :));
for i = 1:size(colors, 1)
    colors(i, :) = (I1(xy1(i, 2), xy1(i, 1), :) + I2(xy2(i, 2), xy2(i, 1), :))/2 ;
end
end

References

[1] Schonberger, Johannes L., and Jan-Michael Frahm. "Structure-from-motion revisited." In Proceedings of the IEEE conference on computer vision and pattern recognition, pp. 4104-4113. 2016.

See Also

| | |

Topics