lardon3d/src/photo_quality.cpp

373 lines
14 KiB
C++

#include <lardon3d/photo_quality.h>
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
namespace {
/* Metrics v1 is a deterministic engineering recommendation policy calibrated
* with synthetic fixtures and real Sony A6000 / Samsung S21 captures. These
* cutoffs are not universal scientific validity criteria. Any value or mapping
* change requires a new metrics/policy version and corresponding cache or
* fingerprint invalidation rather than silently reinterpreting stored rows. */
constexpr double kSharpnessVeryLow = 0.00018; // REJECT: severe normalized blur.
constexpr double kSharpnessLow = 0.00045; // SUSPECT: review normalized blur.
constexpr unsigned char kBlackSampleMaximum = 5; // Prototype-compatible black sample.
constexpr unsigned char kWhiteSampleMinimum = 250; // Prototype-compatible white sample.
constexpr double kWhiteClippingSuspect = 0.08; // SUSPECT above 8% white samples.
constexpr double kWhiteClippingSevere = 0.20; // REJECT above 20% white samples.
constexpr double kBlackClippingSuspect = 0.15; // SUSPECT above 15% black samples.
constexpr double kBlackClippingSevere = 0.35; // REJECT above 35% black samples.
constexpr double kContrastLow = 0.025; // SUSPECT below normalized standard deviation.
constexpr double kLowTextureFraction = 0.985; // SUSPECT above low-gradient fraction.
constexpr double kSobelLowTextureMagnitude = 0.035; // Low-gradient normalized cutoff.
/* Structural validation is deliberately bounded independently of JPEG pixel
* dimensions. This is parser resource admission, not a scientific image-size
* limit: every byte read or skipped consumes the same finite work budget. */
constexpr uint64_t kJpegStructuralByteLimit = 64ULL * 1024ULL * 1024ULL;
constexpr size_t kJpegMaximumContainerImages = 8;
bool jpeg_dimensions(const char *path, uint32_t &width, uint32_t &height) {
std::FILE *file = std::fopen(path, "rb");
if (!file)
return false;
const auto close = [&file]() { std::fclose(file); };
uint64_t structural_bytes = 0;
const auto read_byte = [&file, &structural_bytes]() {
if (structural_bytes == kJpegStructuralByteLimit)
return EOF;
const int value = std::fgetc(file);
if (value != EOF)
++structural_bytes;
return value;
};
const auto read_exact = [&file, &structural_bytes](void *destination, size_t count) {
if (count > kJpegStructuralByteLimit - structural_bytes)
return false;
const size_t actual = std::fread(destination, 1, count, file);
structural_bytes += actual;
return actual == count;
};
const auto skip = [&file, &structural_bytes](uint64_t count) {
if (count > kJpegStructuralByteLimit - structural_bytes || count > LONG_MAX)
return false;
if (std::fseek(file, static_cast<long>(count), SEEK_CUR) != 0)
return false;
structural_bytes += count;
return true;
};
bool have_dimensions = false;
bool primary_has_mpf = false;
bool soi_consumed = false;
for (size_t image_index = 0; image_index < kJpegMaximumContainerImages; ++image_index) {
if (!soi_consumed && (read_byte() != 0xff || read_byte() != 0xd8)) {
close();
return false;
}
soi_consumed = false;
bool image_has_dimensions = false;
bool image_has_mpf = false;
bool in_entropy = false;
int marker = -1;
for (;;) {
if (marker < 0) {
int prefix = read_byte();
if (in_entropy) {
while (prefix != EOF) {
if (prefix != 0xff) {
prefix = read_byte();
continue;
}
marker = read_byte();
while (marker == 0xff)
marker = read_byte();
if (marker == 0x00 || (marker >= 0xd0 && marker <= 0xd7)) {
prefix = read_byte();
continue;
}
break;
}
} else {
if (prefix != 0xff) {
close();
return false;
}
marker = read_byte();
}
}
while (marker == 0xff)
marker = read_byte();
if (marker == EOF || marker == 0x00 || marker == 0xd8 ||
(!in_entropy && marker >= 0xd0 && marker <= 0xd7)) {
close();
return false;
}
in_entropy = false;
if (marker == 0xd9)
break;
if (marker == 0x01) {
marker = -1;
continue;
}
const int high = read_byte();
const int low = read_byte();
if (high == EOF || low == EOF) {
close();
return false;
}
const unsigned length = (static_cast<unsigned>(high) << 8) |
static_cast<unsigned>(low);
if (length < 2) {
close();
return false;
}
if (marker == 0xe2 && length >= 6) {
unsigned char identifier[4];
if (!read_exact(identifier, sizeof(identifier))) {
close();
return false;
}
image_has_mpf = image_has_mpf ||
std::memcmp(identifier, "MPF\0", sizeof(identifier)) == 0;
if (!skip(length - 6)) {
close();
return false;
}
} else {
const bool sof = (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 &&
marker != 0xc8 && marker != 0xcc);
if (sof) {
unsigned char header[6];
if (length < 8 || !read_exact(header, sizeof(header))) {
close();
return false;
}
const uint32_t image_height = (static_cast<uint32_t>(header[1]) << 8) | header[2];
const uint32_t image_width = (static_cast<uint32_t>(header[3]) << 8) | header[4];
if (image_width == 0 || image_height == 0 || image_has_dimensions ||
!skip(length - 8)) {
close();
return false;
}
if (image_index == 0) {
width = image_width;
height = image_height;
have_dimensions = true;
}
image_has_dimensions = true;
} else if (!skip(length - 2)) {
close();
return false;
}
}
if (marker == 0xda) {
if (!image_has_dimensions) {
close();
return false;
}
in_entropy = true;
}
marker = -1;
}
if (image_index == 0)
primary_has_mpf = image_has_mpf;
/* The frozen MPF contract treats physical bytes after primary EOI as a
* bounded container, never as pixels: only zero gaps, structurally valid
* secondary JPEGs, and a zero-only trailer are accepted. */
int trailing = read_byte();
while (trailing == 0)
trailing = read_byte();
if (trailing == EOF) {
const bool valid = have_dimensions && std::ferror(file) == 0;
close();
return valid;
}
if (!primary_has_mpf || trailing != 0xff) {
close();
return false;
}
const int soi = read_byte();
if (soi != 0xd8) {
close();
return false;
}
soi_consumed = true;
}
close();
return false;
}
int reduced_grayscale_flag(uint32_t maximum_dimension) {
if (maximum_dimension > 4u * LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION)
return cv::IMREAD_REDUCED_GRAYSCALE_8;
if (maximum_dimension > 2u * LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION)
return cv::IMREAD_REDUCED_GRAYSCALE_4;
if (maximum_dimension > LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION)
return cv::IMREAD_REDUCED_GRAYSCALE_2;
return cv::IMREAD_GRAYSCALE;
}
void initialize(Lardon3DPhotoQualityMetrics *output) {
std::memset(output, 0, sizeof(*output));
output->metrics_version = LARDON3D_PHOTO_QUALITY_METRICS_VERSION;
output->status = LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT;
output->recommendation = LARDON3D_PHOTO_QUALITY_REJECT;
}
void append_reason(char reasons[LARDON3D_PHOTO_QUALITY_REASON_CAPACITY], const char *reason) {
const size_t used = std::strlen(reasons);
if (used >= LARDON3D_PHOTO_QUALITY_REASON_CAPACITY - 1)
return;
(void)std::snprintf(reasons + used, LARDON3D_PHOTO_QUALITY_REASON_CAPACITY - used,
"%s%s", used ? ";" : "", reason);
}
} // namespace
extern "C" Lardon3DPhotoQualityMetricStatus lardon3d_photo_quality_analyze_jpeg(
const char *path, Lardon3DPhotoQualityMetrics *output) {
if (!output)
return LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT;
initialize(output);
if (!path || !path[0])
return output->status;
try {
uint32_t source_width = 0;
uint32_t source_height = 0;
if (!jpeg_dimensions(path, source_width, source_height)) {
output->status = LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR;
append_reason(output->reasons, "JPEG_DECODE_ERROR");
return output->status;
}
const uint32_t source_maximum = std::max(source_width, source_height);
output->decoded_width = source_width;
output->decoded_height = source_height;
if (source_maximum > LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION) {
/* This is an operational admission ceiling, not malformed input and not
* a scientific rejection. Preserve a pending SUSPECT result so a proxy,
* policy change, or explicit human override can resolve selection. */
output->status = LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE;
output->recommendation = LARDON3D_PHOTO_QUALITY_SUSPECT;
append_reason(output->reasons, "JPEG_DIMENSIONS_EXCEED_OPERATIONAL_LIMIT");
return output->status;
}
/* Complete structural validation through EOI precedes allocation. This is
* what distinguishes an operationally oversized proxy from a truncated
* file that merely contains a syntactically sufficient oversized SOF. */
cv::Mat decoded = cv::imread(path, reduced_grayscale_flag(source_maximum));
if (decoded.empty() || decoded.cols <= 0 || decoded.rows <= 0) {
output->status = LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR;
append_reason(output->reasons, "JPEG_DECODE_ERROR");
return output->status;
}
const double scale = static_cast<double>(LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION) /
static_cast<double>(std::max(decoded.cols, decoded.rows));
const int width = std::max(1, static_cast<int>(std::lround(decoded.cols * scale)));
const int height = std::max(1, static_cast<int>(std::lround(decoded.rows * scale)));
cv::Mat analysis;
cv::resize(decoded, analysis, cv::Size(width, height), 0.0, 0.0,
scale < 1.0 ? cv::INTER_AREA : cv::INTER_NEAREST);
decoded.release();
output->analysis_width = static_cast<uint32_t>(analysis.cols);
output->analysis_height = static_cast<uint32_t>(analysis.rows);
cv::Mat normalized;
analysis.convertTo(normalized, CV_32F, 1.0 / 255.0);
cv::Scalar mean;
cv::Scalar deviation;
cv::meanStdDev(normalized, mean, deviation);
output->contrast_raw = deviation[0] * 255.0;
output->contrast_normalized = deviation[0];
cv::Mat laplacian;
cv::Laplacian(normalized, laplacian, CV_32F, 3);
cv::Scalar lap_mean;
cv::Scalar lap_deviation;
cv::meanStdDev(laplacian, lap_mean, lap_deviation);
output->sharpness_raw = lap_deviation[0] * lap_deviation[0] * 255.0 * 255.0;
output->sharpness_normalized = lap_deviation[0] * lap_deviation[0];
laplacian.release();
cv::Mat gradient_x;
cv::Mat gradient_y;
cv::Sobel(normalized, gradient_x, CV_32F, 1, 0, 3);
cv::Sobel(normalized, gradient_y, CV_32F, 0, 1, 3);
cv::Mat magnitude;
cv::magnitude(gradient_x, gradient_y, magnitude);
output->low_texture_fraction =
static_cast<double>(cv::countNonZero(magnitude < kSobelLowTextureMagnitude)) /
static_cast<double>(magnitude.total());
output->clipped_black_fraction =
static_cast<double>(cv::countNonZero(analysis <= kBlackSampleMaximum)) /
static_cast<double>(analysis.total());
output->clipped_white_fraction =
static_cast<double>(cv::countNonZero(analysis >= kWhiteSampleMinimum)) /
static_cast<double>(analysis.total());
bool severe = false;
bool suspect = false;
if (output->sharpness_normalized < kSharpnessVeryLow) {
append_reason(output->reasons, "SHARPNESS_VERY_LOW");
severe = true;
} else if (output->sharpness_normalized < kSharpnessLow) {
append_reason(output->reasons, "SHARPNESS_LOW");
suspect = true;
}
if (output->clipped_white_fraction > kWhiteClippingSevere ||
output->clipped_black_fraction > kBlackClippingSevere) {
append_reason(output->reasons, "EXPOSURE_CLIPPING_SEVERE");
severe = true;
} else if (output->clipped_white_fraction > kWhiteClippingSuspect ||
output->clipped_black_fraction > kBlackClippingSuspect) {
append_reason(output->reasons, "EXPOSURE_CLIPPING");
suspect = true;
}
if (output->contrast_normalized < kContrastLow ||
output->low_texture_fraction > kLowTextureFraction) {
append_reason(output->reasons, "LOW_TEXTURE_OR_CONTRAST");
suspect = true;
}
output->status = LARDON3D_PHOTO_QUALITY_METRIC_OK;
output->recommendation = severe ? LARDON3D_PHOTO_QUALITY_REJECT
: suspect ? LARDON3D_PHOTO_QUALITY_SUSPECT
: LARDON3D_PHOTO_QUALITY_GOOD;
if (!output->reasons[0])
append_reason(output->reasons, "OK");
return output->status;
} catch (...) {
// C++ and OpenCV exceptions are contained at the public C17 ABI boundary.
initialize(output);
output->status = LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR;
append_reason(output->reasons, "JPEG_DECODE_ERROR");
return output->status;
}
}
extern "C" void lardon3d_photo_quality_raw_only(Lardon3DPhotoQualityMetrics *output) {
if (!output)
return;
initialize(output);
output->status = LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE;
output->recommendation = LARDON3D_PHOTO_QUALITY_SUSPECT;
append_reason(output->reasons, "METRIC_UNAVAILABLE_REQUIRES_JPEG_PROXY");
}
extern "C" int lardon3d_photo_quality_effective_include(
Lardon3DPhotoQualityRecommendation recommendation,
Lardon3DPhotoQualityOverride override_value) {
if (override_value == LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE)
return 1;
if (override_value == LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE)
return 0;
return recommendation == LARDON3D_PHOTO_QUALITY_GOOD ? 1 : 0;
}