source/isp/CameraIsp.h (1,068 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 <exception> #include <fstream> #include <iostream> #include <sstream> #include <thread> #include <boost/algorithm/string.hpp> #include <glog/logging.h> #include <folly/json.h> #include <folly/lang/Bits.h> #include "source/isp/ColorspaceConversion.h" #include "source/isp/Filter.h" #include "source/isp/MonotonicTable.h" #include "source/util/CvUtil.h" #include "source/util/MathUtil.h" #include "source/util/SystemUtil.h" namespace fb360_dep { enum class DemosaicFilter : unsigned int { BILINEAR = 0, FREQUENCY, EDGE_AWARE, CHROMA_SUPRESSED_BILINEAR, LAST }; const int kToneCurveLutSize = 4096; class CameraIsp { protected: std::string bayerPattern; bool isLittleEndian; bool isPlanar; std::string planeOrder; bool isRowMajor; int sensorWidth; int sensorHeight; int sensorBitsPerPixel; std::vector<cv::Point3f> compandingLut; cv::Point3f blackLevel; cv::Point3f clampMin; cv::Point3f clampMax; std::vector<cv::Point3f> vignetteRollOffH; std::vector<cv::Point3f> vignetteRollOffV; int stuckPixelThreshold; float stuckPixelDarknessThreshold; int stuckPixelRadius; cv::Point3f whiteBalanceGain; cv::Mat_<float> ccm; // 3x3 cv::Mat_<float> compositeCCM; float saturation; cv::Point3f gamma; cv::Point3f lowKeyBoost; cv::Point3f highKeyBoost; float contrast; cv::Point3f sharpening; float sharpeningSupport; float noiseCore; cv::Mat_<float> rawImage; bool redBayerPixel[2][2]; bool greenBayerPixel[2][2]; cv::Mat_<cv::Vec3f> demosaicedImage; uint32_t filters; DemosaicFilter demosaicFilter; int resize; bool disableToneCurve; bool toneCurveEnabled; std::vector<cv::Vec3f> toneCurveLut; math_util::BezierCurve<float, cv::Vec3f> vignetteCurveH; math_util::BezierCurve<float, cv::Vec3f> vignetteCurveV; const int width; const int height; const int maxDimension; const float maxD; // max diagonal distance const float sqrtMaxD; // max diagonal distance void demosaicBilinearFilter(cv::Mat_<float>& r, cv::Mat_<float>& g, cv::Mat_<float>& b) const { for (int i = 0; i < height; ++i) { const int i_1 = math_util::reflect(i - 1, height); const int i1 = math_util::reflect(i + 1, height); const bool redGreenRow = (redPixel(i, 0) && greenPixel(i, 1)) || (redPixel(i, 1) && greenPixel(i, 0)); for (int j = 0; j < width; ++j) { const int j_1 = math_util::reflect(j - 1, width); const int j1 = math_util::reflect(j + 1, width); if (redPixel(i, j)) { g(i, j) = cv_util::bilerp(g(i_1, j), g(i1, j), g(i, j_1), g(i, j1), 0.5f, 0.5f); b(i, j) = cv_util::bilerp(b(i_1, j_1), b(i1, j_1), b(i_1, j1), b(i1, j1), 0.5f, 0.5f); } else if (greenPixel(i, j)) { if (redGreenRow) { b(i, j) = (b(i_1, j) + b(i1, j)) / 2.0f; r(i, j) = (r(i, j_1) + r(i, j1)) / 2.0f; } else { r(i, j) = (r(i_1, j) + r(i1, j)) / 2.0f; b(i, j) = (b(i, j_1) + b(i, j1)) / 2.0f; } } else { g(i, j) = cv_util::bilerp(g(i_1, j), g(i1, j), g(i, j_1), g(i, j1), 0.5f, 0.5f); r(i, j) = cv_util::bilerp(r(i_1, j_1), r(i1, j_1), r(i_1, j1), r(i1, j1), 0.5f, 0.5f); } } } } void demosaicFrequencyFilter(cv::Mat_<float>& r, cv::Mat_<float>& g, cv::Mat_<float>& b) const { // Green/luma 4-th order Butterworth lp filter const math_util::Butterworth dFilter(0.0f, 2.0f, width + height, 1.0f, 4.0); // Chrome cross over filter const math_util::Butterworth dcFilter(0.0f, 2.0f, width + height, 1.0f, 2.0f); // Do a per pixel filtering in DCT space const int h2 = r.rows; const int w2 = r.cols; for (int i = 0; i < h2; ++i) { const float y = float(i) / float(h2 - 1); for (int j = 0; j < w2; ++j) { const float x = float(j) / float(w2 - 1); // Diagonal distance and half static const float kDScale = 1.2f; const float d = (x + y) * kDScale; const float kSharpen = d / 2.5f + 1.0f; const float gGain = 2.0f * dFilter(d) * kSharpen; const float rbGain = 4.0f * dFilter(d); g(i, j) *= gGain; const float kCrossoverCutoff = 3.0f; const float d2 = d * 2 * kCrossoverCutoff; // Cross over blend value const float alpha = dcFilter(d2); r(i, j) = math_util::lerp(g(i, j), r(i, j) * rbGain, alpha); b(i, j) = math_util::lerp(g(i, j), b(i, j) * rbGain, alpha); } } } void demosaicEdgeAware(cv::Mat_<float>& red, cv::Mat_<float>& green, cv::Mat_<float>& blue) const { // Horizontal and vertical green values cv::Mat_<float> gV(height, width); cv::Mat_<float> gH(height, width); // And their first and second order derivatives cv::Mat_<float> dV(height, width); cv::Mat_<float> dH(height, width); // Compute green gradients for (int i = 0; i < height; ++i) { const int i_1 = math_util::reflect(i - 1, height); const int i1 = math_util::reflect(i + 1, height); const int i_2 = math_util::reflect(i - 2, height); const int i2 = math_util::reflect(i + 2, height); for (int j = 0; j < width; ++j) { const int j_1 = math_util::reflect(j - 1, width); const int j1 = math_util::reflect(j + 1, width); const int j_2 = math_util::reflect(j - 2, width); const int j2 = math_util::reflect(j + 2, width); if (greenPixel(i, j)) { gV(i, j) = green(i, j); gH(i, j) = green(i, j); dV(i, j) = (fabsf(green(i2, j) - green(i, j)) + fabsf(green(i, j) - green(i_2, j))) / 2.0f; dH(i, j) = (fabsf(green(i, j2) - green(i, j)) + fabsf(green(i, j) - green(i, j_2))) / 2.0f; } else { gV(i, j) = (green(i_1, j) + green(i1, j)) / 2.0f; gH(i, j) = (green(i, j_1) + green(i, j1)) / 2.0f; dV(i, j) = (fabsf(green(i_1, j) - green(i1, j))) / 2.0f; dH(i, j) = (fabsf(green(i, j_1) - green(i, j1))) / 2.0f; cv::Mat_<float>& ch = redPixel(i, j) ? red : blue; gV(i, j) += (2.0f * ch(i, j) - ch(i_2, j) - ch(i2, j)) / 4.0f; gH(i, j) += (2.0f * ch(i, j) - ch(i, j_2) - ch(i, j2)) / 4.0f; dV(i, j) += fabsf(-2.0f * ch(i, j) + ch(i_2, j) + ch(i2, j)) / 2.0f; dH(i, j) += fabsf(-2.0f * ch(i, j) + ch(i, j_2) + ch(i, j2)) / 2.0f; } } } const int w = 4; const int diameter = 2 * w + 1; const int diameterSquared = math_util::square(diameter); for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { // Homogenity test int hCount = 0; for (int l = -w; l <= w; ++l) { const int il = math_util::reflect(i + l, height); for (int k = -w; k <= w; ++k) { const int jk = math_util::reflect(j + k, width); hCount += (dH(il, jk) <= dV(il, jk)); } } green(i, j) = math_util::lerp(gV(i, j), gH(i, j), float(hCount) / diameterSquared); } } demosaicChromaSuppressed(red, green, blue); } void demosaicGreenBilinear(cv::Mat_<float>& red, cv::Mat_<float>& green, cv::Mat_<float>& blue) const { for (int i = 0; i < height; ++i) { const int i_1 = math_util::reflect(i - 1, height); const int i1 = math_util::reflect(i + 1, height); for (int j = 0; j < width; ++j) { const int j_1 = math_util::reflect(j - 1, width); const int j1 = math_util::reflect(j + 1, width); if (redPixel(i, j)) { green(i, j) = cv_util::bilerp(green(i_1, j), green(i1, j), green(i, j_1), green(i, j1), 0.5f, 0.5f); } else if (!greenPixel(i, j)) { green(i, j) = cv_util::bilerp(green(i_1, j), green(i1, j), green(i, j_1), green(i, j1), 0.5f, 0.5f); } } } demosaicChromaSuppressed(red, green, blue); } void demosaicChromaSuppressed(cv::Mat_<float>& red, cv::Mat_<float>& green, cv::Mat_<float>& blue) const { // compute r-b cv::Mat_<float> redMinusGreen(height, width); cv::Mat_<float> blueMinusGreen(height, width); for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { if (redPixel(i, j)) { redMinusGreen(i, j) = red(i, j) - green(i, j); } else if (!greenPixel(i, j)) { blueMinusGreen(i, j) = blue(i, j) - green(i, j); } } } // Now use a constant hue based red/blue bilinear interpolation for (int i = 0; i < height; ++i) { const int i_1 = math_util::reflect(i - 1, height); const int i1 = math_util::reflect(i + 1, height); const int i_2 = math_util::reflect(i - 2, height); const int i2 = math_util::reflect(i + 2, height); const bool redGreenRow = (redPixel(i, 0) && greenPixel(i, 1)) || (redPixel(i, 1) && greenPixel(i, 0)); for (int j = 0; j < width; ++j) { const int j_1 = math_util::reflect(j - 1, width); const int j1 = math_util::reflect(j + 1, width); const int j_2 = math_util::reflect(j - 2, width); const int j2 = math_util::reflect(j + 2, width); if (redPixel(i, j)) { blue(i, j) = (blueMinusGreen(i_1, j_1) + blueMinusGreen(i1, j_1) + blueMinusGreen(i_1, j1) + blueMinusGreen(i1, j1)) / 4.0f + green(i, j); red(i, j) = (redMinusGreen(i, j) + redMinusGreen(i_2, j) + redMinusGreen(i2, j) + redMinusGreen(i, j_2) + redMinusGreen(i, j2)) / 5.0f + green(i, j); } else if (greenPixel(i, j)) { cv::Mat_<float>& diffCh1 = redGreenRow ? blueMinusGreen : redMinusGreen; cv::Mat_<float>& diffCh2 = redGreenRow ? redMinusGreen : blueMinusGreen; cv::Mat_<float>& ch1 = redGreenRow ? blue : red; cv::Mat_<float>& ch2 = redGreenRow ? red : blue; ch1(i, j) = (diffCh1(i_1, j_2) + diffCh1(i_1, j) + diffCh1(i_1, j2) + diffCh1(i1, j_2) + diffCh1(i1, j2) + diffCh1(i1, j2)) / 6.0f + green(i, j); ch2(i, j) = (diffCh2(i_2, j_1) + diffCh2(i, j_1) + diffCh2(i2, j_1) + diffCh2(i_2, j1) + diffCh2(i, j1) + diffCh2(i2, j1)) / 6.0f + green(i, j); } else { red(i, j) = (redMinusGreen(i_1, j_1) + redMinusGreen(i1, j_1) + redMinusGreen(i_1, j1) + redMinusGreen(i1, j1)) / 4.0f + green(i, j); blue(i, j) = (blueMinusGreen(i, j) + blueMinusGreen(i_2, j) + blueMinusGreen(i2, j) + blueMinusGreen(i, j_2) + blueMinusGreen(i, j2)) / 5.0f + green(i, j); } } } } template <class T> void resizeInput(const cv::Mat_<T>& inputImage) { int bitsPerPixel = 8 * sizeof(T); int maxPixelValue = (1 << bitsPerPixel) - 1; const float areaRecip = 1.0f / (maxPixelValue * float(math_util::square(resize))); const int r = resize > 1 ? 2 : 1; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { float sum = 0.0f; for (int k = 0; k < resize; ++k) { const int ip = i * resize + k * 2; const int ipp = math_util::reflect(ip + (i % r), inputImage.rows); for (int l = 0; l < resize; ++l) { const int jp = j * resize + l * 2; const int jpp = math_util::reflect(jp + (j % r), inputImage.cols); sum += float(inputImage(ipp, jpp)); } } rawImage(i, j) = sum * areaRecip; } } } std::array<int, 4> getPlaneOrderToBayerOrder() const { bool foundFirstG = false; std::array<int, 4> map; for (int i = 0; i < 4; ++i) { map[i] = foundFirstG ? planeOrder.rfind(bayerPattern[i]) : planeOrder.find(bayerPattern[i]); foundFirstG |= bayerPattern[i] == 'G'; } return map; } // Used to build up the low and high key parts of the tone curve inline float bezier(float a, float b, float c, float d, float t) { // Four point DeCasteljau's Algorithm return math_util::lerp( math_util::lerp(math_util::lerp(a, b, t), math_util::lerp(b, c, t), t), math_util::lerp(math_util::lerp(b, c, t), math_util::lerp(c, d, t), t), t); } inline float highKey(float highKeyBoost, float x) { const float a = 0.5f; const float b = math_util::clamp(0.6666f, 0.0f, 1.0f); const float c = math_util::clamp(0.8333f + highKeyBoost, 0.0f, 1.0f); const float d = 1.0f; return x > 0.5f ? bezier(a, b, c, d, (x - 0.5f) * 2.0f) : 0; } inline float lowKey(float lowKeyBoost, float x) { const float a = 0.0f; const float b = math_util::clamp(0.1666f + lowKeyBoost, 0.0f, 1.0f); const float c = math_util::clamp(0.3333f, 0.0f, 1.0f); const float d = 0.5f; return x <= 0.5f ? bezier(a, b, c, d, x * 2.0f) : 0; } // Build the composite tone curve map from [0,1]^3 to [0,1]^3 void buildToneCurveLut() { toneCurveLut.clear(); const float dx = 1.0f / float(kToneCurveLutSize - 1); // Contrast angle constants const float angle = M_PI * 0.25f * contrast; const float slope = tanf(angle); const float bias = 0.5f * (1.0f - slope); for (int i = 0; i < kToneCurveLutSize; ++i) { const float x = dx * i; if (!toneCurveEnabled) { // Just a linear ramp ==> no-op const float y = x; toneCurveLut.push_back(cv::Vec3f(y, y, y)); } else { // Apply gamma correction float r = powf(x, gamma.x); float g = powf(x, gamma.y); float b = powf(x, gamma.z); // Then low/high key boost r = lowKey(lowKeyBoost.x, r) + highKey(highKeyBoost.x, r); g = lowKey(lowKeyBoost.y, g) + highKey(highKeyBoost.y, g); b = lowKey(lowKeyBoost.z, b) + highKey(highKeyBoost.z, b); // Then contrast r = math_util::clamp((slope * r + bias), 0.0f, 1.0f); g = math_util::clamp((slope * g + bias), 0.0f, 1.0f); b = math_util::clamp((slope * b + bias), 0.0f, 1.0f); // Place it in the table. toneCurveLut.push_back(cv::Vec3f(r, g, b)); } } } void getPoint(folly::dynamic& f, cv::Point3f& p) const { if (f.isNull()) { return; } p = cv::Point3f(); if (f[0] != nullptr) { p.x = f[0].getDouble(); } else { VLOG(1) << "Bad or missing x point coordinate." << std::endl; } if (f[1] != nullptr) { p.y = f[1].getDouble(); } else { VLOG(1) << "Bad or missing y point coordinate." << std::endl; } if (f[2] != nullptr) { p.z = f[2].getDouble(); } else { VLOG(1) << "Bad or missing z point coordinate." << std::endl; } } void getCoordList(folly::dynamic& list, std::vector<cv::Point3f>& coords) const { if (list.isNull()) { return; } coords = std::vector<cv::Point3f>(); for (auto l : list) { cv::Point3f p; getPoint(l, p); coords.push_back(p); } } void getMatrix(folly::dynamic& list, cv::Mat_<float>& m) const { if (list.isNull()) { return; } int rows = 0; int cols = 0; for (auto i : list) { ++rows; cols = 0; for (auto j : i) { ++cols; } } m = cv::Mat_<float>(rows, cols); for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { m(i, j) = list[i][j].asDouble(); } } } public: CameraIsp(const std::string jsonInput) : demosaicFilter(DemosaicFilter::EDGE_AWARE), resize(1), toneCurveEnabled(true), width(0), height(0), maxDimension(0), maxD(0), sqrtMaxD(0) { // Set the default values and override them from the json file compandingLut.push_back(cv::Point3f(0.0f, 0.0f, 0.0f)); compandingLut.push_back(cv::Point3f(1.0f, 1.0f, 1.0f)); blackLevel = cv::Point3f(0.0f, 0.0f, 0.0f); clampMin = cv::Point3f(0.0f, 0.0f, 0.0f); clampMax = cv::Point3f(1.0f, 1.0f, 1.0f); stuckPixelThreshold = 0; stuckPixelDarknessThreshold = 0; stuckPixelRadius = 0; vignetteRollOffH.push_back(cv::Vec3f(1.0f, 1.0f, 1.0f)); vignetteRollOffV.push_back(cv::Vec3f(1.0f, 1.0f, 1.0f)); whiteBalanceGain = cv::Point3f(1.0f, 1.0f, 1.0f); ccm = cv::Mat::eye(3, 3, CV_32F); saturation = 1.0f; contrast = 1.0f; sharpening = cv::Point3f(0.0f, 0.0f, 0.0f); sharpeningSupport = 10.0f / 2048.0f; // Approx filter support is 10 pixels noiseCore = 1000.0f; gamma = cv::Point3f(1.0f, 1.0f, 1.0f); lowKeyBoost = cv::Point3f(0.0f, 0.0f, 0.0f); highKeyBoost = cv::Point3f(0.0f, 0.0f, 0.0f); bayerPattern = "GBRG"; sensorWidth = width; sensorHeight = height; sensorBitsPerPixel = 16; isPlanar = false; planeOrder = ""; isLittleEndian = false; isRowMajor = true; folly::dynamic config = folly::parseJson(jsonInput); if (config["CameraIsp"] != nullptr) { folly::dynamic cameraConfig = config["CameraIsp"]; sensorBitsPerPixel = cameraConfig.getDefault("bitsPerPixel", sensorBitsPerPixel).asInt(); sensorWidth = cameraConfig.getDefault("width", sensorWidth).asInt(); sensorHeight = cameraConfig.getDefault("height", sensorHeight).asInt(); isLittleEndian = cameraConfig.getDefault("isLittleEndian", isLittleEndian).asBool(); isRowMajor = cameraConfig.getDefault("isRowMajor", isRowMajor).asBool(); bayerPattern = cameraConfig.getDefault("bayerPattern", bayerPattern).asString(); boost::to_upper(bayerPattern); CHECK_EQ(bayerPattern.size(), 4); planeOrder = cameraConfig.getDefault("planeOrder", planeOrder).asString(); boost::to_upper(planeOrder); isPlanar = !planeOrder.empty(); CHECK(!isPlanar || planeOrder.size() == 4); getCoordList(cameraConfig["compandingLut"], compandingLut); getPoint(cameraConfig["blackLevel"], blackLevel); getPoint(cameraConfig["clampMin"], clampMin); getPoint(cameraConfig["clampMax"], clampMax); stuckPixelThreshold = cameraConfig.getDefault("stuckPixelThreshold", stuckPixelThreshold).asInt(); CHECK_GE(stuckPixelThreshold, 0); stuckPixelDarknessThreshold = cameraConfig.getDefault("stuckPixelDarknessThreshold", stuckPixelDarknessThreshold) .asDouble(); stuckPixelRadius = cameraConfig.getDefault("stuckPixelRadius", stuckPixelRadius).asInt(); getCoordList(cameraConfig["vignetteRollOffH"], vignetteRollOffH); getCoordList(cameraConfig["vignetteRollOffV"], vignetteRollOffV); getPoint(cameraConfig["whiteBalanceGain"], whiteBalanceGain); getMatrix(cameraConfig["ccm"], ccm); saturation = cameraConfig.getDefault("saturation", saturation).asDouble(); getPoint(cameraConfig["gamma"], gamma); getPoint(cameraConfig["lowKeyBoost"], lowKeyBoost); getPoint(cameraConfig["highKeyBoost"], highKeyBoost); contrast = cameraConfig.getDefault("contrast", contrast).asDouble(); getPoint(cameraConfig["sharpening"], sharpening); sharpeningSupport = cameraConfig.getDefault("sharpeningSupport", sharpeningSupport).asDouble(); noiseCore = cameraConfig.getDefault("noiseCore", noiseCore).asDouble(); } else { VLOG(1) << "Missing \"CameraIsp\" in config; using default values.\n"; } setup(); } virtual ~CameraIsp() {} void setup() { // Build the bayer pattern tables if (bayerPattern.find("RGGB") != std::string::npos) { filters = 0x94949494; redBayerPixel[0][0] = true; redBayerPixel[0][1] = false; redBayerPixel[1][0] = false; redBayerPixel[1][1] = false; greenBayerPixel[0][0] = false; greenBayerPixel[0][1] = true; greenBayerPixel[1][0] = true; greenBayerPixel[1][1] = false; } else if (bayerPattern.find("GRBG") != std::string::npos) { filters = 0x61616161; redBayerPixel[0][0] = false; redBayerPixel[0][1] = true; redBayerPixel[1][0] = false; redBayerPixel[1][1] = false; greenBayerPixel[0][0] = true; greenBayerPixel[0][1] = false; greenBayerPixel[1][0] = false; greenBayerPixel[1][1] = true; } else if (bayerPattern.find("GBRG") != std::string::npos) { filters = 0x49494949; redBayerPixel[0][0] = false; redBayerPixel[0][1] = false; redBayerPixel[1][0] = true; redBayerPixel[1][1] = false; greenBayerPixel[0][0] = true; greenBayerPixel[0][1] = false; greenBayerPixel[1][0] = false; greenBayerPixel[1][1] = true; } else if (bayerPattern.find("BGGR") != std::string::npos) { filters = 0x16161616; redBayerPixel[0][0] = false; redBayerPixel[0][1] = false; redBayerPixel[1][0] = false; redBayerPixel[1][1] = true; greenBayerPixel[0][0] = false; greenBayerPixel[0][1] = true; greenBayerPixel[1][0] = true; greenBayerPixel[1][1] = false; } vignetteCurveH.clearPoints(); for (auto p : vignetteRollOffH) { vignetteCurveH.addPoint(p); } vignetteCurveV.clearPoints(); for (auto p : vignetteRollOffV) { vignetteCurveV.addPoint(p); } // If saturation is unit this satMat will be the identity matrix. cv::Mat_<float> satMat = cv::Mat::zeros(3, 3, CV_32F); satMat(0, 0) = 1.0f; satMat(1, 1) = saturation; satMat(2, 2) = saturation; // Move into yuv scale by the saturation and move back satMat = color::yuv2rgb * satMat * color::rgb2yuv; transpose(ccm, compositeCCM); compositeCCM *= satMat; // The stage following the CCM maps tone curve lut to 12bits so we // scale the pixel by the lut size here once instead of doing it // for every pixel. compositeCCM *= float(kToneCurveLutSize - 1); // Build the tone curve table buildToneCurveLut(); } // Helper functions inline bool redPixel(const int i, const int j) const { return redBayerPixel[i % 2][j % 2]; } inline bool greenPixel(const int i, const int j) const { return greenBayerPixel[i % 2][j % 2]; } inline bool bluePixel(const int i, const int j) const { return !(greenBayerPixel[i % 2][j % 2] || redBayerPixel[i % 2][j % 2]); } inline int getChannelNumber(const int i, const int j) { return redPixel(i, j) ? 0 : greenPixel(i, j) ? 1 : 2; } inline cv::Vec3f curveHAtPixel(const int x) { return vignetteCurveH(float(x) / float(maxDimension)); } inline cv::Vec3f curveVAtPixel(const int x) { return vignetteCurveV(float(x) / float(maxDimension)); } void dumpConfigFile(const std::string configFileName) { std::ofstream ofs(configFileName.c_str(), std::ios::out); if (ofs) { ofs.precision(3); ofs << std::fixed; ofs << "{\n"; ofs << " \"CameraIsp\" : {\n"; ofs << " \"serial\" : 0,\n"; ofs << " \"name\" : \"RED Helium\",\n"; ofs << " \"compandingLut\" : ["; for (int i = 0; i < int(compandingLut.size()); ++i) { if (i > 0) { ofs << " "; } ofs << "[" << compandingLut[i].x << ", " << compandingLut[i].y << ", " << compandingLut[i].z << "]"; if (i < int(compandingLut.size()) - 1) { ofs << ",\n"; } } ofs << "],\n"; ofs << " \"blackLevel\" : [" << blackLevel.x << ", " << blackLevel.y << ", " << blackLevel.z << "],\n"; ofs << " \"clampMin\" : [" << clampMin.x << ", " << clampMin.y << ", " << clampMin.z << "],\n"; ofs << " \"clampMax\" : [" << clampMax.x << ", " << clampMax.y << ", " << clampMax.z << "],\n"; ofs << " \"vignetteRollOffH\" : ["; for (int i = 0; i < int(vignetteRollOffH.size()); ++i) { if (i > 0) { ofs << " "; } ofs << "[" << vignetteRollOffH[i].x << ", " << vignetteRollOffH[i].y << ", " << vignetteRollOffH[i].z << "]"; if (i < int(vignetteRollOffH.size()) - 1) { ofs << ",\n"; } } ofs << "],\n"; ofs << " \"vignetteRollOffV\" : ["; for (int i = 0; i < int(vignetteRollOffV.size()); ++i) { if (i > 0) { ofs << " "; } ofs << "[" << vignetteRollOffV[i].x << ", " << vignetteRollOffV[i].y << ", " << vignetteRollOffV[i].z << "]"; if (i < int(vignetteRollOffV.size()) - 1) { ofs << ",\n"; } } ofs << "],\n"; ofs << " \"whiteBalanceGain\" : [" << whiteBalanceGain.x << "," << whiteBalanceGain.y << "," << whiteBalanceGain.z << "],\n"; ofs << " \"stuckPixelThreshold\" : " << stuckPixelThreshold << ",\n"; ofs << " \"stuckPixelDarknessThreshold\" : " << stuckPixelDarknessThreshold << ",\n"; ofs << " \"stuckPixelRadius\" : " << stuckPixelRadius << ",\n"; ofs.precision(5); ofs << std::fixed; ofs << " \"ccm\" : [[" << ccm(0, 0) << ", " << ccm(0, 1) << ", " << ccm(0, 2) << "],\n"; ofs << " [" << ccm(1, 0) << ", " << ccm(1, 1) << ", " << ccm(1, 2) << "],\n"; ofs << " [" << ccm(2, 0) << ", " << ccm(2, 1) << ", " << ccm(2, 2) << "]],\n"; ofs.precision(3); ofs << std::fixed; ofs << " \"sharpening\" : [" << sharpening.x << ", " << sharpening.y << ", " << sharpening.z << "],\n"; ofs << " \"saturation\" : " << saturation << ",\n"; ofs << " \"contrast\" : " << contrast << ",\n"; ofs << " \"lowKeyBoost\" : [" << lowKeyBoost.x << ", " << lowKeyBoost.y << ", " << lowKeyBoost.z << "],\n"; ofs << " \"highKeyBoost\" : [" << highKeyBoost.x << ", " << highKeyBoost.y << ", " << highKeyBoost.z << "],\n"; ofs << " \"gamma\" : [" << gamma.x << ", " << gamma.y << ", " << gamma.z << "],\n"; ofs << " \"bayerPattern\" : \"" << bayerPattern << "\",\n"; ofs << " \"isLittleEndian\" : " << isLittleEndian << ",\n"; ofs << " \"isPlanar\" : " << isPlanar << ",\n"; ofs << " \"isRowMajor\" : " << isRowMajor << ",\n"; ofs << " \"width\" : " << sensorWidth << ",\n"; ofs << " \"height\" : " << sensorHeight << ",\n"; ofs << " \"bitsPerPixel\" : " << sensorBitsPerPixel << ",\n"; ofs << " \"planeOrder\" : \"" << planeOrder << "\"\n"; ofs << " }\n"; ofs << "}\n"; } else { CHECK(false) << "unable to open output ISP config file: " << configFileName; } } /* Load sensorWidth x sensorHeight image from raw data, interleaving, byte swapping, and re-indexing if necessary for host and OpenCV compatibility. */ template <typename T> void loadImageFromSensor(const std::vector<T>& inputImage) { // - sanity check config CHECK_NE(sensorWidth, 0); CHECK_NE(sensorHeight, 0); CHECK_EQ(sensorWidth % 2, 0); CHECK_EQ(sensorHeight % 2, 0); CHECK_EQ(inputImage.size(), sensorWidth * sensorHeight); CHECK_EQ(sensorBitsPerPixel, 8 * sizeof(T)); cv::Mat_<T> intermediateImage(sensorHeight, sensorWidth); // - load directly if there's nothing to do if (!isPlanar && isLittleEndian == folly::kIsLittleEndian && isRowMajor) { memcpy(intermediateImage.data, inputImage.data(), sensorWidth * sensorHeight * sizeof(T)); loadImage(intermediateImage); return; } // - setup problem dimensions and channel mappings int planeWidth = sensorWidth / 2; int planeHeight = sensorHeight / 2; int numPixelsPerPlane = planeWidth * planeHeight; std::array<int, 4> inputChannelOrderToBayerOrder = { {0, 1, 2, 3}}; // no-op for interleaved input if (isPlanar) { inputChannelOrderToBayerOrder = getPlaneOrderToBayerOrder(); } // - inner loop helpers to byte swap, interleave, and re-index as needed // - converts from linear index to a representation-independent (channel, row, column) // pixel index auto pixelIndexFromInputLinearIndex = [=](int linearIndex) { int channel, row, col; if (isPlanar) { channel = linearIndex / numPixelsPerPlane; int linearIndexInPlane = linearIndex - channel * numPixelsPerPlane; std::tie(row, col) = math_util::linearToMatrixIndex(linearIndexInPlane, planeHeight, planeWidth, isRowMajor); } else { std::tie(row, col) = math_util::linearToMatrixIndex(linearIndex, sensorHeight, sensorWidth, isRowMajor); // Bayer pattern is always specified in row major, so compute the channel index accordingly channel = math_util::matrixToLinearIndex(std::make_tuple(row % 2, col % 2), 2, 2); row /= 2; col /= 2; } return std::make_tuple(channel, row, col); }; // - converts representation-independent (channel, row, column) pixel index to (row, column) // matrix index in interleaved image auto outputInterleavedRawIndexFromPixelIndex = [=](std::tuple<int, int, int> pixelIndex) { int channel, row, col, bayerRow, bayerCol; std::tie(channel, row, col) = pixelIndex; channel = inputChannelOrderToBayerOrder[channel]; std::tie(bayerRow, bayerCol) = math_util::linearToMatrixIndex(channel, 2, 2); return std::make_pair(2 * row + bayerRow, 2 * col + bayerCol); }; auto byteSwap = isLittleEndian ? folly::Endian::little<T> : folly::Endian::big<T>; // - reorder and load image for (int k = 0; k < sensorWidth * sensorHeight; ++k) { auto pixelIndex = pixelIndexFromInputLinearIndex(k); auto rawIndex = outputInterleavedRawIndexFromPixelIndex(pixelIndex); intermediateImage(std::get<0>(rawIndex), std::get<1>(rawIndex)) = byteSwap(inputImage[k]); } loadImage(intermediateImage); } /* Load a native byte order, interleaved, and arbitrarily sized OpenCV raw image */ virtual void loadImage(const cv::Mat& inputImage) { setDimensions(inputImage.cols, inputImage.rows); rawImage = cv::Mat::zeros(height, width, CV_32F); // Copy and convert to float uint8_t depth = inputImage.type() & CV_MAT_DEPTH_MASK; // Match the input bits per pixel overriding what is in the config file. if (depth == CV_8U) { resizeInput<uint8_t>(inputImage); } else if (depth == CV_16U) { resizeInput<uint16_t>(inputImage); } else { CHECK(false) << "input is larger that 16 bits per pixel"; } } template <typename T> cv::Mat_<T> getRawImage() const { CHECK(!rawImage.empty()); return cv_util::convertTo<T>(rawImage); } void setDemosaicedImage(const cv::Mat_<cv::Vec3f>& demosaicedImage) { this->demosaicedImage = demosaicedImage; } cv::Mat_<cv::Vec3f> getDemosaicedImage() const { return demosaicedImage; } void addBlackLevelOffset(const int offset) { blackLevel.x += offset; blackLevel.y += offset; blackLevel.z += offset; } void setBlackLevel(const cv::Point3f& blackLevel) { this->blackLevel = blackLevel; } cv::Point3f getBlackLevel() const { return blackLevel; } void setClampMin(const cv::Point3f& clampMin) { this->clampMin = clampMin; } cv::Point3f getClampMin() const { return clampMin; } void setClampMax(const cv::Point3f& clampMax) { this->clampMax = clampMax; } cv::Point3f getClampMax() const { return clampMax; } void setVignetteRollOffH(const std::vector<cv::Point3f>& vignetteRollOffH) { this->vignetteRollOffH = vignetteRollOffH; } std::vector<cv::Point3f> getVignetteRollOffH() const { return vignetteRollOffH; } void setVignetteRollOffV(const std::vector<cv::Point3f>& vignetteRollOffV) { this->vignetteRollOffV = vignetteRollOffV; } std::vector<cv::Point3f> getVignetteRollOffV() const { return vignetteRollOffV; } void setCCM(const cv::Mat_<float>& ccm) { this->ccm = ccm; } cv::Mat_<float> getCCM() const { return ccm; } uint32_t getFilters() const { return filters; } void setWhiteBalance(const cv::Point3f whiteBalanceGain) { this->whiteBalanceGain = whiteBalanceGain; } cv::Point3f getWhiteBalanceGain() const { return whiteBalanceGain; } void setGamma(const cv::Point3f gamma) { this->gamma = gamma; } cv::Point3f getGamma() const { return gamma; } void setToneCurveEnabled(bool toneCurveEnabled) { if (this->toneCurveEnabled != toneCurveEnabled) { this->toneCurveEnabled = toneCurveEnabled; buildToneCurveLut(); } } void setDemosaicFilter(DemosaicFilter demosaicFilter) { CHECK(DemosaicFilter::BILINEAR <= demosaicFilter && demosaicFilter < DemosaicFilter::LAST) << "expecting Demosaic filter in [0," << int(DemosaicFilter::LAST) - 1 << "]"; this->demosaicFilter = demosaicFilter; } void setResize(const int resize) { CHECK(resize == 1 || resize == 2 || resize == 4 || resize == 8) << "expecting a resize value of 1, 2, 4, or 8. got " << resize; this->resize = resize; } // Set the white point of the camera/scene void whiteBalance(const bool clampOutput = true) { for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { if (redPixel(i, j)) { rawImage(i, j) *= whiteBalanceGain.x; } else if (greenPixel(i, j)) { rawImage(i, j) *= whiteBalanceGain.y; } else { rawImage(i, j) *= whiteBalanceGain.z; } if (clampOutput) { rawImage(i, j) = math_util::clamp(rawImage(i, j), 0.0f, 1.0f); } } } } void removeStuckPixels() { if (stuckPixelRadius > 0) { struct Pval { float val; int i; int j; Pval(float v) : val(v) {} Pval(const Pval& other) = default; bool operator<(const Pval& p) const { return val < p.val; } Pval& operator=(const Pval& p) { val = p.val; i = p.i; j = p.j; return *this; } }; std::vector<Pval> region; for (int i = 0; i < height; ++i) { // Traverse boustrophedonically const bool evenScanLine = (i % 2) == 0; const int jStart = evenScanLine ? 0 : width - 1; const int jEnd = evenScanLine ? width - 1 : 0; const int jStep = evenScanLine ? 1 : -1; for (int j = jStart; j != jEnd; j += jStep) { const bool thisPixelRed = redPixel(i, j); const bool thisPixelGreen = greenPixel(i, j); const bool thisPixelBlue = bluePixel(i, j); region.clear(); float mean = 0.0f; for (int y = -stuckPixelRadius; y <= stuckPixelRadius; y++) { const int ip = math_util::reflect(i + y, height); for (int x = -stuckPixelRadius; x <= stuckPixelRadius; x++) { const int jp = math_util::reflect(j + x, width); Pval p(rawImage(ip, jp)); p.i = ip; p.j = jp; if (redPixel(ip, jp) && thisPixelRed) { mean += p.val; region.push_back(p); } else if (greenPixel(ip, jp) && thisPixelGreen) { mean += p.val; region.push_back(p); } else if (bluePixel(ip, jp) && thisPixelBlue) { mean += p.val; region.push_back(p); } } } mean /= float(region.size()); // Only deal with dark regions if (mean < stuckPixelDarknessThreshold) { sort(region.begin(), region.end()); // See if the middle pixel is an outlier for (int k = int(region.size()) - 1; k >= int(region.size()) - stuckPixelThreshold; --k) { if (region[k].i == i && region[k].j == j) { // Replace the pixel pixel with the median of the region. rawImage(i, j) = region[region.size() / 2].val; goto l0; } } } l0: continue; } } } } void blackLevelAdjust() { const float br = blackLevel.x; const float bg = blackLevel.y; const float bb = blackLevel.z; const float sr = 1.0f / (1.0f - br); const float sg = 1.0f / (1.0f - bg); const float sb = 1.0f / (1.0f - bb); for (int i = 0; i < height; ++i) { for (int j = 0; j < width; j++) { if (rawImage(i, j) < 1.0f) { if (redPixel(i, j)) { rawImage(i, j) = (rawImage(i, j) - br) * sr; } else if (greenPixel(i, j)) { rawImage(i, j) = (rawImage(i, j) - bg) * sg; } else { rawImage(i, j) = (rawImage(i, j) - bb) * sb; } } } } } void clampAndStretch() { for (int i = 0; i < height; ++i) { for (int j = 0; j < width; j++) { const float clampMinVal = redPixel(i, j) ? clampMin.x : (greenPixel(i, j) ? clampMin.y : clampMin.z); const float clampMaxVal = redPixel(i, j) ? clampMax.x : (greenPixel(i, j) ? clampMax.y : clampMax.z); const float v = math_util::clamp(rawImage(i, j), clampMinVal, clampMaxVal); rawImage(i, j) = (v - clampMinVal) / (clampMaxVal - clampMinVal); } } } void antiVignette() { for (int i = 0; i < height; ++i) { const cv::Vec3f vV = curveVAtPixel(i); for (int j = 0; j < width; j++) { const cv::Vec3f vH = curveHAtPixel(j); int ch = getChannelNumber(i, j); rawImage(i, j) *= vH[ch] * vV[ch]; } } } uint32_t nextPowerOf2(const uint32_t i) { uint32_t p = 1; while (i > p) { p <<= 1; } return p; } void demosaic() { uint32_t h2 = demosaicFilter == DemosaicFilter::FREQUENCY ? nextPowerOf2(height) : height; uint32_t w2 = demosaicFilter == DemosaicFilter::FREQUENCY ? nextPowerOf2(width) : width; cv::Mat_<float> r(h2, w2, 0.0f); cv::Mat_<float> g(h2, w2, 0.0f); cv::Mat_<float> b(h2, w2, 0.0f); // Break out each plane into a separate image so we can demosaicFilter // them separately and then recombine them. for (int i = 0; i < height; ++i) { for (int j = 0; j < width; j++) { if (redPixel(i, j)) { r(i, j) = rawImage(i, j); } else if (greenPixel(i, j)) { g(i, j) = rawImage(i, j); } else { b(i, j) = rawImage(i, j); } } } if (demosaicFilter == DemosaicFilter::FREQUENCY) { // Move into the frequency domain std::thread t1([&] { dct(r, r); }); std::thread t2([&] { dct(g, g); }); std::thread t3([&] { dct(b, b); }); t1.join(); t2.join(); t3.join(); // Filter including sharpnning in the DCT domain demosaicFrequencyFilter(r, g, b); #undef DEBUG_DCT #ifndef DEBUG_DCT // Move back into the spatial domain std::thread t4([&] { idct(r, r); }); std::thread t5([&] { idct(g, g); }); std::thread t6([&] { idct(b, b); }); t4.join(); t5.join(); t6.join(); #endif } else if (demosaicFilter == DemosaicFilter::BILINEAR) { demosaicBilinearFilter(r, g, b); } else if (demosaicFilter == DemosaicFilter::CHROMA_SUPRESSED_BILINEAR) { demosaicGreenBilinear(r, g, b); } else { demosaicEdgeAware(r, g, b); } demosaicedImage = cv::Mat::zeros(height, width, CV_32F); for (int i = 0; i < height; ++i) { for (int j = 0; j < width; j++) { demosaicedImage(i, j)[0] = r(i, j); demosaicedImage(i, j)[1] = g(i, j); demosaicedImage(i, j)[2] = b(i, j); } } } void colorCorrect() { const float kToneCurveLutRange = kToneCurveLutSize - 1; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; j++) { cv::Vec3f p = demosaicedImage(i, j); #ifdef DEBUG_DCT cv::Vec3f v( logf(math_util::square(p[0]) + 1.0f) * 255.0f, logf(math_util::square(p[1]) + 1.0f) * 255.0f, logf(math_util::square(p[2]) + 1.0f) * 255.0f); #else cv::Vec3f v( toneCurveLut[math_util::clamp( compositeCCM(0, 0) * p[0] + compositeCCM(0, 1) * p[1] + compositeCCM(0, 2) * p[2], 0.0f, kToneCurveLutRange)][0], toneCurveLut[math_util::clamp( compositeCCM(1, 0) * p[0] + compositeCCM(1, 1) * p[1] + compositeCCM(1, 2) * p[2], 0.0f, kToneCurveLutRange)][1], toneCurveLut[math_util::clamp( compositeCCM(2, 0) * p[0] + compositeCCM(2, 1) * p[1] + compositeCCM(2, 2) * p[2], 0.0f, kToneCurveLutRange)][2]); #endif demosaicedImage(i, j) = v; } } } void sharpen() { if (sharpening.x != 0.0 && sharpening.y != 0.0 && sharpening.z != 0.0) { cv::Mat_<cv::Vec3f> lowPass(height, width); const isp::ReflectBoundary<int> reflectB; const float maxVal = 1.0f; isp::iirLowPass<isp::ReflectBoundary<int>, isp::ReflectBoundary<int>, cv::Vec3f>( demosaicedImage, sharpeningSupport, lowPass, reflectB, reflectB, maxVal); isp::sharpenWithIirLowPass<cv::Vec3f>( demosaicedImage, lowPass, 1.0f + sharpening.x, 1.0f + sharpening.y, 1.0f + sharpening.z, noiseCore, maxVal); } } protected: // Replacable pipeline virtual void executePipeline(const bool swizzle) { // Apply the pipeline blackLevelAdjust(); antiVignette(); whiteBalance(); clampAndStretch(); removeStuckPixels(); demosaic(); colorCorrect(); sharpen(); } void setDimensions(int inputWidth, int inputHeight) { const_cast<int&>(width) = inputWidth / resize; const_cast<int&>(height) = inputHeight / resize; const_cast<int&>(maxDimension) = std::max(width, height); const_cast<float&>(maxD) = math_util::square(width) + math_util::square(height); const_cast<float&>(sqrtMaxD) = sqrt(maxD); } public: int getOutputWidth() const { return width; } int getOutputHeight() const { return height; } int getSensorWidth() const { return sensorWidth; } int getSensorHeight() const { return sensorHeight; } int getSensorBitsPerPixel() const { return sensorBitsPerPixel; } template <typename T> cv::Mat_<cv::Vec<T, 3>> getImage(const bool swizzle = true) { cv::Mat_<cv::Vec<T, 3>> outputImage(getOutputHeight(), getOutputWidth()); getImage(outputImage, swizzle); return outputImage; } template <typename T> void getImage(cv::Mat_<cv::Vec<T, 3>>& outputImage, const bool swizzle = true) { CHECK_EQ(getOutputWidth(), outputImage.cols); CHECK_EQ(getOutputHeight(), outputImage.rows); executePipeline(swizzle); int outputBitsPerPixel = 8 * sizeof(T); float scale = (1 << outputBitsPerPixel) - 1; // Copy and convert to byte swizzling to BGR const int c0 = swizzle ? 2 : 0; const int c1 = swizzle ? 1 : 1; const int c2 = swizzle ? 0 : 2; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; j++) { cv::Vec3f dp = scale * demosaicedImage(i, j); outputImage(i, j)[c0] = dp[0]; outputImage(i, j)[c1] = dp[1]; outputImage(i, j)[c2] = dp[2]; } } } }; // class CameraIsp }; // namespace fb360_dep