source/render/ReprojectionTable.h (174 lines of code) (raw):
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include "source/util/Camera.h"
namespace fb360_dep {
// a reprojection table tells you - for each pixel in dst - where in src to look
// i.e. it implements
// f(xy, disparity) = src.pixel(dst.rig(xy, 1 / disparity))
// the underlying implementation is a 3D piecewise linear table with enough
// points to stay within tolerance of the correct answer
// the reprojection table is a 3D table with dimensions shape. the first entry
// represents
// (0 - margin.x, 0 - margin.y, min disparity)
// the last entry represents
// (1 + margin.x, 1 + margin.y, max disparity)
// the reprojection table is meant to be used as a 3D texture. textures are
// addressed with texture coordinates that range from 0 at the *outside* corner
// of the first texel to 1 at the outside corner of the last texel
// that means that the value represented by the first entry must be remapped to
// the texture coordinate of the center of the first texel and vice versa:
// 0 + texel / 2 ... 1 - texel / 2
// scale and offset perform this remapping:
// texture coordinates = input * scale + offset
// there is another similar coordinate system used internally while building the
// table, called normalized coordinates. they are 0 at the center of the first
// texel and 1 at the center of the last texel. so similar to texture coors but
// without the half-texel adjustment
class ReprojectionTable {
public:
ReprojectionTable(
const Camera& dst,
const Camera& src,
const Camera::Vector2& tolerance,
const Camera::Vector2& margin = {0, 0})
: margin(margin) {
CHECK(dst.isNormalized());
if (src.overlap(dst) == 0) {
shape.setOnes();
values = {{-1, -1}}; // outside
return;
}
// compute the resolution required in each dimension
for (int dim = 0; dim < shape.size(); ++dim) {
// start by checking error at kN^3 cells, increasing end[dim] as needed
static const int kN = 10;
static const float kFactor = 1.2f;
for (IndexType end = IndexType::Constant(kN);; end[dim] *= kFactor) {
if (isWithinTolerance(dst, src, end, dim, tolerance, margin)) {
shape[dim] = end[dim] + 1;
break;
}
}
}
// now create table with the computed shape
values.resize(shape.prod());
for (IndexType i(0, 0, 0); good(i, shape); increment(i, shape)) {
const Camera::Vector3 normalized = divide(i, shape - 1);
values[flatten(i, shape)] = compute(dst, src, normalized, margin);
}
}
using Entry = Eigen::Vector2f; // not 16B, ok to put in vector
using IndexType = Eigen::Array3i;
IndexType shape;
const Camera::Vector2 margin;
std::vector<Entry> values;
Eigen::Array3f getScale() const {
// input range
Eigen::Array3f input(1 + 2 * margin.x(), 1 + 2 * margin.y(), maxDisparity() - minDisparity());
// output range
Eigen::Array3f output = 1 - 1 / shape.cast<float>();
return output / input;
}
Eigen::Array3f getOffset() const {
// input
Eigen::Array3f input(-margin.x(), -margin.y(), minDisparity());
// output
Eigen::Array3f output = 0.5f / shape.cast<float>();
// output = input * scale + offset <=>
return output - input * getScale();
}
// do a trilinear lookup in the table
// note: this is slow. use a GPU, that is what they are for
Entry lookup(const Entry& xy, const float disparity) const {
Eigen::Array3f texcoor = getScale() * Eigen::Array3f(xy.x(), xy.y(), disparity) + getOffset();
Eigen::Array3f unnorm = texcoor * shape.cast<float>();
// accumulate entries from a 2^3 cube beginning with floor(unnorm - 0.5)
IndexType begin = floor(unnorm - 0.5).cast<int>();
CHECK((0 <= begin && begin < shape - 1).all()) << begin << "out of range";
Entry result = {0, 0};
for (int z = 0; z < 2; ++z) {
for (int y = 0; y < 2; ++y) {
for (int x = 0; x < 2; ++x) {
IndexType index = begin + IndexType(x, y, z);
Eigen::Array3f diff = (index.cast<float>() + 0.5f - unnorm).abs();
float weight = (1 - diff).prod();
CHECK(0 <= weight && weight <= 1) << weight;
result += weight * values[flatten(index, shape)];
}
}
}
return result;
}
std::string toString(const int kRes = 10) const {
std::string result;
result += "[";
for (float z = 0; z < kRes; ++z) {
result += "[";
const float d = unnormalizeDisparity((z + 0.5f) / kRes);
for (float y = 0; y < kRes + 1; ++y) {
result += "[";
for (float x = 0; x < kRes + 1; ++x) {
result += "[";
ReprojectionTable::Entry p = lookup({x / kRes, y / kRes}, d);
result += std::to_string(p.x()) + "," + std::to_string(p.y());
result += "],";
}
result += "],";
}
result += "],";
}
result += "],";
return result;
}
static float maxDisparity() {
return 1.0f;
}
static float minDisparity() {
return 1.0f / Camera::kNearInfinity;
}
static float normalizeDisparity(float disparity) {
// remap disparity from minDisparity() ... maxDisparity() to 0 ... 1
return (disparity - minDisparity()) / (maxDisparity() - minDisparity());
}
static float unnormalizeDisparity(float normalized) {
// remap normalized from 0 ... 1 to minDisparity() ... maxDisparity()
CHECK(0 <= normalized && normalized <= 1);
return (1 - normalized) * minDisparity() + normalized * maxDisparity();
}
private:
static Camera::Vector2 unnormalizeXY(
const Camera::Vector3& normalized,
const Camera::Vector2& margin) {
// remap normalized.xy from 0 ... 1 to -margin ... 1 + margin
CHECK(0 <= normalized.x() && normalized.x() <= 1);
CHECK(0 <= normalized.y() && normalized.y() <= 1);
const Camera::Vector2 nxy = normalized.head<2>();
return nxy.array() * (1 + 2 * margin.array()) - margin.array();
}
static int flatten(const IndexType& index, const IndexType& shape) {
CHECK((index < shape).all());
return (index[2] * shape[1] + index[1]) * shape[0] + index[0];
}
static bool good(const IndexType& index, const IndexType& shape) {
return index[2] < shape[2];
}
static void increment(IndexType& index, const IndexType& shape) {
if (++index[0] == shape[0]) {
index[0] = 0;
if (++index[1] == shape[1]) {
index[1] = 0;
++index[2];
}
}
}
static Camera::Vector3
divide(const IndexType& num, const IndexType& den, const Camera::Real offset = 0) {
return (num.cast<Camera::Real>() + offset) / den.cast<Camera::Real>();
}
static bool isWithinTolerance(
const Camera& dst,
const Camera& src,
const IndexType& end,
int dim,
const Camera::Vector2& tolerance,
const Camera::Vector2& margin) {
const Eigen::Array2f tol = tolerance.array().cast<float>();
for (IndexType i(0, 0, 0); good(i, end); increment(i, end)) {
// compute values in center of cell
Camera::Vector3 normalized = divide(i, end, 0.5);
const Camera::Vector2 xy = unnormalizeXY(normalized, margin);
if (!dst.isOutsideImageCircle(xy)) {
const float disparity = unnormalizeDisparity(normalized.z());
Camera::Vector2 exact;
if (src.sees(dst.rig(xy, 1 / disparity), exact)) {
// compute sample on either side of normalized along dimension dim
normalized[dim] -= 0.5 / end[dim];
Entry lo = compute(dst, src, normalized, margin);
normalized[dim] += 1.0 / end[dim];
Entry hi = compute(dst, src, normalized, margin);
// does sub-texel precision error exceed tolerance?
const float kSubtexelPrecision = 1.0f / 512;
Eigen::Array2f sub = (hi - lo).array() * kSubtexelPrecision;
if ((abs(sub.array()) > tol).any()) {
return false; // error exceeds tolerance
}
// does linear approximation error exceed tolerance?
Entry lin = (lo + hi) / 2 - exact.cast<float>();
if ((abs(lin.array()) > tol).any()) {
return false; // error exceeds tolerance
}
}
}
}
return true;
}
static Entry compute(
const Camera& dst,
const Camera& src,
const Camera::Vector3& normalized,
const Camera::Vector2& margin) {
const Camera::Vector2 xy = unnormalizeXY(normalized, margin);
const float disparity = unnormalizeDisparity(normalized.z());
return src.pixel(dst.rig(xy, 1 / disparity)).cast<float>();
}
};
} // namespace fb360_dep