Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image stack refactors #188

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 108 additions & 44 deletions src/ImageStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
#include <x86intrin.h>
#endif

using namespace std;
using namespace hdrmerge;


Expand All @@ -41,6 +40,8 @@ int ImageStack::addImage(Image && i) {
height = i.getHeight();
}
images.push_back(std::move(i));

// sort image stack from brightest to darkest ([0] being the brightest)
int n = images.size() - 1;
while (n > 0 && images[n] < images[n - 1]) {
std::swap(images[n], images[n - 1]);
Expand All @@ -50,75 +51,104 @@ int ImageStack::addImage(Image && i) {
}


void ImageStack::calculateSaturationLevel(const RawParameters & params, bool useCustomWl) {
// Calculate max value of brightest image and assume it is saturated
Image& brightest = images.front();
void ImageStack::calculateSaturationLevel(const RawParameters & params, bool use_custom_white_level) {

/*
* This method will approximate a saturation threshold for the brightest
* image only. It is sufficient because the calculated threshold will
* be translated to the rest of the images in the stack in a relative
* manner thanks to each image's response function being pre-calculated.
*
* The saturation threshold has a major role in deciding on which pixels to
* use from which image. The better this value is calculated the better the
* results.
*
* Methodology Overwiew:
* 1. Generate the 4 histograms (green, blue, green, red) from the
* brightest image
* 2. Determine the brightest value per histogram but ignore the outliers
* using the threshold `occurance_threshold` that will discard bright
* pixels that don't have a significant frequency (occurance)
* 3. Use vorious attempts to enhance the `saturation_threshold`
*/

std::vector<std::vector<size_t>> histograms(4, std::vector<size_t>(brightest.getMax() + 1));
Image& brightest_image = images.front();

std::vector<std::vector<size_t>> histograms(4, std::vector<size_t>(brightest_image.getMax() + 1));
fanckush marked this conversation as resolved.
Show resolved Hide resolved

#pragma omp parallel
{
std::vector<std::vector<size_t>> histogramsThr(4, std::vector<size_t>(brightest.getMax() + 1));
std::vector<std::vector<size_t>> histograms_thr(4, std::vector<size_t>(brightest_image.getMax() + 1));
#pragma omp for schedule(dynamic,16) nowait
for (size_t y = 0; y < height; ++y) {
// get the color codes from x = 0 to 5, works for bayer and xtrans
uint16_t fcrow[6];
uint16_t fc_row[6];
for (size_t i = 0; i < 6; ++i) {
fcrow[i] = params.FC(i, y);
fc_row[i] = params.FC(i, y);
}
size_t x = 0;
for (; x < width - 5; x+=6) {
for(size_t j = 0; j < 6; ++j) {
uint16_t v = brightest(x + j, y);
++histogramsThr[fcrow[j]][v];
uint16_t luminance_val = brightest_image(x + j, y);
fanckush marked this conversation as resolved.
Show resolved Hide resolved
++histograms_thr[fc_row[j]][luminance_val];
}
}
// remaining pixels
for (size_t j = 0; x < width; ++x, ++j) {
uint16_t v = brightest(x, y);
++histogramsThr[fcrow[j]][v];
uint16_t luminance_val = brightest_image(x, y);
fanckush marked this conversation as resolved.
Show resolved Hide resolved
++histograms_thr[fc_row[j]][luminance_val];
}
}
#pragma omp critical
{
for (int c = 0; c < 4; ++c) {
for (std::vector<size_t>::size_type i = 0; i < histograms[c].size(); ++i) {
histograms[c][i] += histogramsThr[c][i];
for (int c_channel = 0; c_channel < 4; ++c_channel) {
std::vector<size_t>::size_type histogram_size, luminance_val;
histogram_size = histograms[c_channel].size();
for (luminance_val = 0; luminance_val < histogram_size; ++luminance_val) {
fanckush marked this conversation as resolved.
Show resolved Hide resolved
histograms[c_channel][luminance_val] += histograms_thr[c_channel][luminance_val];
}
}
}
}

const size_t threshold = width * height / 10000;
// used to ignore luminances with frequentcy less htan 0.0001
const size_t occurance_threshold = width * height / 10000;
fanckush marked this conversation as resolved.
Show resolved Hide resolved

uint16_t maxPerColor[4] = {0, 0, 0, 0};
// maximum "significant" luminance per color channel
uint16_t max_lum_per_color[4] = {0, 0, 0, 0};
fanckush marked this conversation as resolved.
Show resolved Hide resolved

for (int c = 0; c < 4; ++c) {
for (int i = histograms[c].size() - 1; i >= 0; --i) {
const size_t v = histograms[c][i];
if (v > threshold) {
maxPerColor[c] = i;
for (int c_channel = 0; c_channel < 4; ++c_channel) {
// start from the highest value in the histograms (brightest)
for (int luminance_val = histograms[c_channel].size() - 1; luminance_val >= 0; --luminance_val) {
const size_t frequency = histograms[c_channel][luminance_val];
fanckush marked this conversation as resolved.
Show resolved Hide resolved
// ignore if it has a low occurance (frequency) in the image
if (frequency > occurance_threshold) {
max_lum_per_color[c_channel] = luminance_val;
break;
}
}
}


uint16_t maxPerColors = std::max(maxPerColor[0], std::max(maxPerColor[1],std::max(maxPerColor[2], maxPerColor[3])));
satThreshold = params.max == 0 ? maxPerColors : params.max;
uint16_t max_luminance = std::max(max_lum_per_color[0], std::max(max_lum_per_color[1],std::max(
fanckush marked this conversation as resolved.
Show resolved Hide resolved
max_lum_per_color[2], max_lum_per_color[3])));
saturation_threshold = params.max == 0 ? max_luminance : params.max;

if(maxPerColors > 0) {
satThreshold = std::min(satThreshold, maxPerColors);
if(max_luminance > 0) {
fanckush marked this conversation as resolved.
Show resolved Hide resolved
saturation_threshold = std::min(saturation_threshold, max_luminance);
}

if (!useCustomWl) { // only scale when no custom white level was specified
satThreshold *= 0.99;
// only scale when no custom white level was specified
if (!use_custom_white_level) {
// The saturation threshold needs to go a little bit "before" the
// highest notable luminance so that it is considered oversaturated
saturation_threshold *= 0.99;
}

Log::debug( "Using white level ", satThreshold );
Log::debug( "Using white level ", saturation_threshold );

for (auto& i : images) {
i.setSaturationThreshold(satThreshold);
for (auto& img : images) {
fanckush marked this conversation as resolved.
Show resolved Hide resolved
img.setSaturationThreshold(saturation_threshold);
}
}

Expand Down Expand Up @@ -149,12 +179,12 @@ void ImageStack::align() {
void ImageStack::crop() {
int dx = 0, dy = 0;
for (auto & i : images) {
int newDx = max(dx, i.getDeltaX());
int bound = min(dx + width, i.getDeltaX() + i.getWidth());
int newDx = std::max(dx, i.getDeltaX());
int bound = std::min(dx + width, i.getDeltaX() + i.getWidth());
width = bound > newDx ? bound - newDx : 0;
dx = newDx;
int newDy = max(dy, i.getDeltaY());
bound = min(dy + height, i.getDeltaY() + i.getHeight());
int newDy = std::max(dy, i.getDeltaY());
bound = std::min(dy + height, i.getDeltaY() + i.getHeight());
height = bound > newDy ? bound - newDy : 0;
dy = newDy;
}
Expand All @@ -165,14 +195,42 @@ void ImageStack::crop() {


void ImageStack::computeResponseFunctions() {

/*
* Computes the repsonse function for every image (excluding the darkest).
* We compute the rosponse for an image img by comparing it to the next
* image in the stack. That is why there is no point to compute the
* response for the darkest image becuase there is no other image to
* compare it to.
*/

Timer t("Compute response functions");
for (int i = images.size() - 2; i >= 0; --i) {
// images[i] = the brighter image
// images[i + 1] = the darker image
images[i].computeResponseFunction(images[i + 1]);
}
}


void ImageStack::generateMask() {

/*
* This method generates the multi-colored mask in the GUI. The way the
* the mask is generated is by starting from the brightest image and
* checking every pixel if it or one of it's surrounding pixels are
* saturated, if so then test against the same pixel (x, y) but from the
* next image which is darker.
*
* Methodology: if possible get all the pixels from the brightest image
* because it has the least amount of noise. only clipped data will be
* taken from the darker images
*
* Result: we end up with a map (matrix) that dictates from which layer of
* the stack should each pixel be taken from for the HDR merging. Exapmle:
fanckush marked this conversation as resolved.
Show resolved Hide resolved
* map(154, 445) -> 2, 2 is the index of the image.
*/

Timer t("Generate mask");
mask.resize(width, height);
if(images.size() == 1) {
Expand All @@ -184,20 +242,26 @@ void ImageStack::generateMask() {
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
size_t i = 0;
while (i < images.size() - 1 &&
(!images[i].contains(x, y) ||
images[i].isSaturatedAround(x, y))) ++i;
while (i < images.size() - 1 && (!images[i].contains(x, y) ||
fanckush marked this conversation as resolved.
Show resolved Hide resolved
images[i].isSaturatedAround(x, y))) {
// skip if the pixel is not in image or clipped
++i;
}
mask(x, y) = i;
}
}
}
// The mask can be used in compose to get the information about saturated pixels
// The mask can be used in compose() to get the information about saturated pixels
// but the mask can be modified in gui, so we have to make a copy to represent the original state
origMask = mask;
original_mask = mask;
}


double ImageStack::value(size_t x, size_t y) const {
/*
fanckush marked this conversation as resolved.
Show resolved Hide resolved
* Get the exposure value (luminance) of the stack at any given pixel
* using the generated mask from generateMask()
*/
const Image & img = images[mask(x, y)];
return img.exposureAt(x, y);
}
Expand Down Expand Up @@ -406,7 +470,7 @@ Array2D<float> ImageStack::compose(const RawParameters & params, int featherRadi
dst.fillBorders(0.f);

float max = 0.0;
double saturatedRange = params.max - satThreshold;
double saturatedRange = params.max - saturation_threshold;
#pragma omp parallel
{
float maxthr = 0.0;
Expand All @@ -421,11 +485,11 @@ Array2D<float> ImageStack::compose(const RawParameters & params, int featherRadi
p = p - j;
v = images[j].exposureAt(x, y);
// Adjust false highlights
if (j < origMask(x,y)) { // SaturatedAround
if (j < original_mask(x,y)) { // SaturatedAround
v /= params.whiteMultAt(x, y);
if(p > 0.0001) {
uint16_t rawV = images[j].getMaxAround(x, y);
double k = (rawV - satThreshold) / saturatedRange;
double k = (rawV - saturation_threshold) / saturatedRange;
if (k > 1.0)
k = 1.0;
p += (1.0 - p) * k;
Expand All @@ -437,7 +501,7 @@ Array2D<float> ImageStack::compose(const RawParameters & params, int featherRadi
}
if (p > 0.0001 && j < imageMax && images[j + 1].contains(x, y)) {
vv = images[j + 1].exposureAt(x, y);
if (j + 1 < origMask(x,y)) { // SaturatedAround
if (j + 1 < original_mask(x,y)) { // SaturatedAround
vv /= params.whiteMultAt(x, y);
}
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/ImageStack.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ class ImageStack {

std::vector<Image> images; ///< Images, from most to least exposed
EditableMaskImpl mask;
Array2D<uint8_t> origMask;
Array2D<uint8_t> original_mask;
size_t width;
size_t height;
int flip;
uint16_t satThreshold;
uint16_t saturation_threshold;
};

} // namespace hdrmerge
Expand Down