source/render/RephotographyUtil.h (130 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 <opencv2/imgproc/imgproc.hpp>
#include <folly/dynamic.h>
#include "source/util/CvUtil.h"
namespace fb360_dep {
namespace rephoto_util {
// Based on paper "Image Quality Assessment: From Error Visibility to Structural
// Similarity", by Z. Wang et al. 2004
template <typename T>
inline cv::Mat_<T> blur(const cv::Mat_<T>& in, const int blurRadius) {
const float sigma = 1.5f; // original MSSIM implementation
return cv_util::gaussianBlur(in, blurRadius, sigma);
}
// Assuming x, y in [0, 1]
// SSIM = L^a * C^b + S^g, where a, b, g > 0 and
// L = (2 * muX * muY + c1) / (muX^2 + muY^2 + c1)
// C = (2 * sigmaXY + c2) / (sigmaX^2 + sigmaY^2 + c2)
// S = (sigmaXY + c3) / (sigmaX * sigmaY + c3)
//
// Note that NCC = SSIM wih a = 0, b = 0, g = 1
//
// NOTE: this will not work with non floating point images
template <typename T>
inline cv::Mat_<T> computeSSIM(
const cv::Mat_<T>& x,
const cv::Mat_<T>& y,
const int blurRadius,
const float alpha = 1.0f,
const float beta = 1.0f,
const float gamma = 1.0f) {
CHECK_EQ(x.size(), y.size());
CHECK_EQ(x.channels(), 3); // RGB
CHECK_EQ(x.channels(), y.channels());
CHECK_GT(blurRadius, 0);
CHECK_GE(alpha, 0.0f);
CHECK_GE(beta, 0.0f);
CHECK_GE(gamma, 0.0f);
const cv::Mat_<T> muX = blur(x, blurRadius);
const cv::Mat_<T> muY = blur(y, blurRadius);
const cv::Mat_<T> mu2X = muX.mul(muX);
const cv::Mat_<T> mu2Y = muY.mul(muY);
const cv::Mat_<T> muXY = muX.mul(muY);
const cv::Mat_<T> xMuX2 = (x - muX).mul(x - muX);
const cv::Mat_<T> sig2X = blur(xMuX2, blurRadius);
const cv::Mat_<T> yMuY2 = (y - muY).mul(y - muY);
const cv::Mat_<T> sig2Y = blur(yMuY2, blurRadius);
const cv::Mat_<T> xMuxYMuy = (x - muX).mul(y - muY);
const cv::Mat_<T> sigXY = blur(xMuxYMuy, blurRadius);
cv::Mat_<T> sigX;
cv::sqrt(sig2X, sigX);
cv::Mat_<T> sigY;
cv::sqrt(sig2Y, sigY);
// Default constants in the SSIM index formula: K = [0.01 0.03], L = 1
// c1 = (k * L)^2, k = 0.01, L = 1
// c2 = (k * L)^2, k = 0.03, L = 1
// c3 = c2 / 2
const cv::Scalar c1 = cv::Scalar::all(0.0001f);
const cv::Scalar c2 = cv::Scalar::all(0.0009f);
const cv::Scalar c3 = c2 / 2.0f;
cv::Mat_<T> luminance = (2 * muXY + c1).mul(1.0f / (mu2X + mu2Y + c1));
cv::pow(luminance, alpha, luminance);
cv::Mat_<T> contrast = (2 * sigX.mul(sigY) + c2).mul(1.0f / (sig2X + sig2Y + c2));
cv::pow(contrast, beta, contrast);
cv::Mat_<T> structure = (sigXY + c3).mul(1.0f / (sigX.mul(sigY) + c3));
cv::pow(structure, gamma, structure);
return contrast.mul(luminance).mul(structure);
}
inline cv::Scalar averageScore(const cv::Mat& scoreMap, const cv::Mat& mask = cv::Mat()) {
cv::Scalar result;
std::vector<cv::Mat> channels(scoreMap.channels());
cv::split(scoreMap, channels);
for (int i = 0; i < scoreMap.channels(); ++i) {
cv::Mat maskNan(mask.size(), mask.type());
mask.copyTo(maskNan);
for (int yy = 0; yy < channels[i].rows; ++yy) {
for (int xx = 0; xx < channels[i].cols; ++xx) {
const float v = channels[i].at<float>(yy, xx);
if (std::isnan(v)) {
maskNan.at<uint8_t>(yy, xx) = 0;
}
}
}
result[i] = cv::mean(channels[i], maskNan)[0];
}
return result;
}
template <typename T>
inline cv::Mat_<T> computeScoreMap(
const std::string& method,
const cv::Mat_<T>& x,
const cv::Mat_<T>& y,
const int blurRadius) {
if (method == "MSSIM") {
return computeSSIM(x, y, blurRadius, 1, 1, 1);
} else if (method == "NCC") {
return computeSSIM(x, y, blurRadius, 0, 0, 1);
} else {
CHECK(false) << "Invalid method " << method;
}
}
inline std::string formatResults(const cv::Scalar& scoreAvg) {
return folly::sformat(
"R {:.2f}%, G {:.2f}%, B {:.2f}%",
100 * scoreAvg.val[2],
100 * scoreAvg.val[1],
100 * scoreAvg.val[0]);
}
template <typename T, typename U>
inline cv::Mat_<cv::Vec3b> stackResults(
const std::vector<cv::Mat_<T>>& reference, // color (and depth)
const std::vector<cv::Mat_<T>>& rendered, // color (and depth)
const cv::Mat_<U>& ssim,
const cv::Scalar& mssim,
const cv::Mat& mask = cv::Mat()) {
CHECK_GE(reference.size(), 1); // at least color
CHECK_LE(reference.size(), 2); // at most color and depth
CHECK_EQ(reference.size(), rendered.size());
CHECK_EQ(ssim.channels(), 3);
// Stack all images
std::vector<cv::Mat_<cv::Vec3b>> images;
// Reference and rendered images are converted to 8 bit
for (int i = 0; i < int(reference.size()); ++i) {
cv::Mat_<cv::Vec3b> ref = cv_util::convertImage<cv::Vec3b>(reference[i]);
images.push_back(ref);
}
for (int i = 0; i < int(rendered.size()); ++i) {
cv::Mat_<cv::Vec3b> ren = cv_util::convertImage<cv::Vec3b>(rendered[i]);
if (!mask.empty()) {
ren.setTo(cv::Scalar::all(0), ~mask);
}
images.push_back(ren);
}
// SSIM is converted to 8 bit and has a color map applied
cv::Mat_<cv::Vec3b> ssim8 = cv_util::convertImage<cv::Vec3b>(ssim);
// Convert to heatmap
// JET makes [0, 255] -> [blue, red], but we want [0, 255] -> [red, blue]
ssim8 = cv::Scalar::all(255) - ssim8;
cv::applyColorMap(ssim8, ssim8, cv::COLORMAP_JET);
// Mask pixels outside mask
ssim8.setTo(cv::Scalar::all(0), ~mask);
images.push_back(ssim8);
cv::Mat_<cv::Vec3b> plot = cv_util::stackHorizontal(images);
// Add text to image showing MSSIM values
const std::string text = formatResults(mssim);
const int textFont = cv::FONT_HERSHEY_PLAIN;
const double textScale = 2;
const cv::Scalar textColor(0, 255, 0); // green
cv::putText(plot, text, cv::Point2f(20, 50), textFont, textScale, textColor);
return plot;
}
} // namespace rephoto_util
} // namespace fb360_dep