lardon3d/src/acquisition_pairing.cpp

608 lines
25 KiB
C++

#include <lardon3d/acquisition_pairing.h>
#include <libexif/exif-data.h>
#include <libexif/exif-utils.h>
#include <libraw/libraw.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <new>
namespace {
bool has_field(const Lardon3DAcquisitionMetadata &metadata, uint32_t field) {
return (metadata.present_fields & field) != 0u;
}
struct BoundedText {
const char *data;
size_t length;
bool available;
};
BoundedText bounded_text(const char *text, size_t capacity) {
const void *terminator = std::memchr(text, '\0', capacity);
if (terminator == nullptr || terminator == text) {
return {text, 0u, false};
}
const size_t length = static_cast<size_t>(static_cast<const char *>(terminator) - text);
return {text, length, true};
}
bool same_text(const char *left, size_t left_capacity, const char *right,
size_t right_capacity) {
const BoundedText left_text = bounded_text(left, left_capacity);
const BoundedText right_text = bounded_text(right, right_capacity);
return left_text.available && right_text.available &&
left_text.length == right_text.length &&
std::memcmp(left_text.data, right_text.data, left_text.length) == 0;
}
bool different_text(const char *left, size_t left_capacity, const char *right,
size_t right_capacity) {
const BoundedText left_text = bounded_text(left, left_capacity);
const BoundedText right_text = bounded_text(right, right_capacity);
return left_text.available && right_text.available &&
(left_text.length != right_text.length ||
std::memcmp(left_text.data, right_text.data, left_text.length) != 0);
}
bool copy_padded_text(char *destination, size_t capacity, const unsigned char *source,
size_t source_size) {
size_t length = source_size;
while (length > 0u && (source[length - 1u] == '\0' || source[length - 1u] == ' ')) {
--length;
}
if (length >= capacity) {
destination[0] = '\0';
return false;
}
if (length > 0u) {
std::memcpy(destination, source, length);
}
destination[length] = '\0';
return length != 0u;
}
void extract_raw_fixed_text(const libraw_data_t &raw,
Lardon3DAcquisitionMetadata &metadata) {
if (copy_padded_text(metadata.make, sizeof(metadata.make),
reinterpret_cast<const unsigned char *>(raw.idata.make),
sizeof(raw.idata.make))) {
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_MAKE;
}
if (copy_padded_text(metadata.model, sizeof(metadata.model),
reinterpret_cast<const unsigned char *>(raw.idata.model),
sizeof(raw.idata.model))) {
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_MODEL;
}
if (copy_padded_text(metadata.body_serial, sizeof(metadata.body_serial),
reinterpret_cast<const unsigned char *>(raw.shootinginfo.BodySerial),
sizeof(raw.shootinginfo.BodySerial))) {
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_BODY_SERIAL;
}
}
void extract_raw_image_unique_id(const libraw_data_t &raw,
Lardon3DAcquisitionMetadata &metadata) {
if (copy_padded_text(metadata.image_unique_id, sizeof(metadata.image_unique_id),
reinterpret_cast<const unsigned char *>(raw.color.ImageUniqueID),
sizeof(raw.color.ImageUniqueID))) {
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID;
}
}
ExifEntry *find_entry(ExifData *data, ExifTag tag) {
for (unsigned int index = 0u; index < EXIF_IFD_COUNT; ++index) {
ExifEntry *entry = exif_content_get_entry(data->ifd[index], tag);
if (entry != nullptr) {
return entry;
}
}
return nullptr;
}
void extract_ascii(ExifData *data, ExifTag tag, char *destination, size_t capacity,
uint32_t field, Lardon3DAcquisitionMetadata &metadata) {
ExifEntry *entry = find_entry(data, tag);
if (entry == nullptr || entry->data == nullptr || entry->size == 0u ||
entry->format != EXIF_FORMAT_ASCII) {
return;
}
if (copy_padded_text(destination, capacity, entry->data, entry->size)) {
metadata.present_fields |= field;
}
}
bool extract_unsigned(ExifData *data, ExifTag tag, uint32_t &value) {
ExifEntry *entry = find_entry(data, tag);
if (entry == nullptr || entry->data == nullptr) {
return false;
}
const ExifByteOrder order = exif_data_get_byte_order(data);
if (entry->format == EXIF_FORMAT_SHORT && entry->size >= 2u) {
value = exif_get_short(entry->data, order);
return true;
}
if (entry->format == EXIF_FORMAT_LONG && entry->size >= 4u) {
value = exif_get_long(entry->data, order);
return true;
}
return false;
}
enum class JpegValidationResult {
valid,
corrupt,
io_error,
};
constexpr size_t JPEG_MAX_CONTAINER_IMAGES = 8u;
// Structural validation deliberately parses JPEG framing without decoding
// pixels, so metadata acceptance does not depend on decoder permissiveness.
bool read_byte(FILE *file, unsigned char &value) {
const int byte = std::fgetc(file);
if (byte == EOF) {
return false;
}
value = static_cast<unsigned char>(byte);
return true;
}
bool skip_bytes(FILE *file, uint16_t count) {
unsigned char byte = 0u;
for (uint16_t index = 0u; index < count; ++index) {
if (!read_byte(file, byte)) {
return false;
}
}
return true;
}
bool marker_has_length(unsigned char marker) {
return (marker >= 0xc0u && marker <= 0xc7u) ||
(marker >= 0xc9u && marker <= 0xcfu) ||
(marker >= 0xdau && marker <= 0xdfu) ||
(marker >= 0xe0u && marker <= 0xfeu);
}
bool read_marker(FILE *file, unsigned char &marker) {
unsigned char byte = 0u;
if (!read_byte(file, byte) || byte != 0xffu) {
return false;
}
do {
if (!read_byte(file, marker)) {
return false;
}
} while (marker == 0xffu);
return marker != 0x00u;
}
bool skip_marker_payload(FILE *file, unsigned char marker, bool &has_mpf_app2) {
unsigned char high = 0u;
unsigned char low = 0u;
if (!read_byte(file, high) || !read_byte(file, low)) {
return false;
}
const uint16_t length = static_cast<uint16_t>((static_cast<uint16_t>(high) << 8u) |
static_cast<uint16_t>(low));
if (marker == 0xdau) {
unsigned char component_count = 0u;
if (length < 3u || !read_byte(file, component_count) || component_count == 0u ||
component_count > 4u) {
return false;
}
const uint16_t expected_length =
static_cast<uint16_t>(6u + 2u * static_cast<uint16_t>(component_count));
return length == expected_length &&
skip_bytes(file, static_cast<uint16_t>(length - 3u));
}
if (marker == 0xdcu) {
unsigned char line_count_high = 0u;
unsigned char line_count_low = 0u;
return length == 4u && read_byte(file, line_count_high) &&
read_byte(file, line_count_low) &&
(line_count_high != 0u || line_count_low != 0u);
}
if (marker == 0xe2u && length >= 6u) {
unsigned char identifier[4] = {};
for (unsigned char &byte : identifier) {
if (!read_byte(file, byte)) {
return false;
}
}
if (std::memcmp(identifier, "MPF\0", sizeof(identifier)) == 0) {
has_mpf_app2 = true;
}
return skip_bytes(file, static_cast<uint16_t>(length - 6u));
}
return length >= 2u && skip_bytes(file, static_cast<uint16_t>(length - 2u));
}
JpegValidationResult validate_jpeg_image(FILE *file, bool soi_already_consumed,
bool &has_mpf_app2) {
has_mpf_app2 = false;
if (!soi_already_consumed) {
unsigned char first = 0u;
unsigned char second = 0u;
if (!read_byte(file, first) || !read_byte(file, second) || first != 0xffu ||
second != 0xd8u) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
}
// Entropy bytes are not marker payload: FF 00 is stuffing and RSTn stays
// within the scan. Only a real following marker resumes marker parsing.
bool in_entropy = false;
for (;;) {
unsigned char marker = 0u;
bool marker_from_entropy = false;
if (in_entropy) {
unsigned char byte = 0u;
do {
if (!read_byte(file, byte)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
} while (byte != 0xffu);
do {
if (!read_byte(file, marker)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
} while (marker == 0xffu);
if (marker == 0x00u || (marker >= 0xd0u && marker <= 0xd7u)) {
continue;
}
marker_from_entropy = true;
in_entropy = false;
} else if (!read_marker(file, marker)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
if (marker == 0xd9u) {
return JpegValidationResult::valid;
}
if (marker == 0xd8u) {
return JpegValidationResult::corrupt;
}
if (marker == 0x01u || (marker >= 0xd0u && marker <= 0xd7u)) {
continue;
}
if (!marker_has_length(marker) ||
!skip_marker_payload(file, marker, has_mpf_app2)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
if (marker == 0xdau || (marker == 0xdcu && marker_from_entropy)) {
in_entropy = true;
}
}
}
JpegValidationResult validate_jpeg_structure(FILE *file) {
// Secondary JPEGs are allowed only in the bounded private container form:
// primary APP2 contains MPF\0, every image reaches EOI structurally, and
// inter-image/final padding is zero only. No image pixels are decoded.
bool primary_has_mpf = false;
for (size_t image_index = 0u; image_index < JPEG_MAX_CONTAINER_IMAGES; ++image_index) {
bool image_has_mpf = false;
const JpegValidationResult image_result =
validate_jpeg_image(file, image_index != 0u, image_has_mpf);
if (image_result != JpegValidationResult::valid) {
return image_result;
}
if (image_index == 0u) {
primary_has_mpf = image_has_mpf;
}
int byte = EOF;
do {
byte = std::fgetc(file);
} while (byte == 0);
if (byte == EOF) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::valid;
}
if (!primary_has_mpf || byte != 0xff || std::fgetc(file) != 0xd8) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
}
return JpegValidationResult::corrupt;
}
Lardon3DAcquisitionResult extract_jpeg(const char *path,
Lardon3DAcquisitionMetadata &metadata) {
FILE *file = std::fopen(path, "rb");
if (file == nullptr) {
return errno == ENOENT || errno == EACCES ? LARDON3D_ACQUISITION_IO_ERROR
: LARDON3D_ACQUISITION_IO_ERROR;
}
const JpegValidationResult validation = validate_jpeg_structure(file);
const bool close_failed = std::fclose(file) != 0;
if (validation == JpegValidationResult::io_error || close_failed) {
return LARDON3D_ACQUISITION_IO_ERROR;
}
if (validation != JpegValidationResult::valid) {
return LARDON3D_ACQUISITION_CORRUPT_SOURCE;
}
ExifData *data = exif_data_new_from_file(path);
if (data == nullptr) {
return LARDON3D_ACQUISITION_METADATA_UNAVAILABLE;
}
metadata.source_kind = LARDON3D_ACQUISITION_SOURCE_JPEG;
extract_ascii(data, EXIF_TAG_MAKE, metadata.make, sizeof(metadata.make),
LARDON3D_ACQUISITION_FIELD_MAKE, metadata);
extract_ascii(data, EXIF_TAG_MODEL, metadata.model, sizeof(metadata.model),
LARDON3D_ACQUISITION_FIELD_MODEL, metadata);
extract_ascii(data, EXIF_TAG_BODY_SERIAL_NUMBER, metadata.body_serial,
sizeof(metadata.body_serial), LARDON3D_ACQUISITION_FIELD_BODY_SERIAL,
metadata);
extract_ascii(data, EXIF_TAG_DATE_TIME_ORIGINAL, metadata.datetime_original,
sizeof(metadata.datetime_original),
LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL, metadata);
extract_ascii(data, EXIF_TAG_SUB_SEC_TIME_ORIGINAL, metadata.subsec_original,
sizeof(metadata.subsec_original), LARDON3D_ACQUISITION_FIELD_SUBSEC_ORIGINAL,
metadata);
extract_ascii(data, EXIF_TAG_OFFSET_TIME_ORIGINAL, metadata.offset_original,
sizeof(metadata.offset_original), LARDON3D_ACQUISITION_FIELD_OFFSET_ORIGINAL,
metadata);
extract_ascii(data, EXIF_TAG_IMAGE_UNIQUE_ID, metadata.image_unique_id,
sizeof(metadata.image_unique_id), LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID,
metadata);
uint32_t width = 0u;
uint32_t height = 0u;
if (extract_unsigned(data, EXIF_TAG_PIXEL_X_DIMENSION, width) &&
extract_unsigned(data, EXIF_TAG_PIXEL_Y_DIMENSION, height) && width != 0u &&
height != 0u) {
metadata.width = width;
metadata.height = height;
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_DIMENSIONS;
}
uint32_t orientation = 0u;
if (extract_unsigned(data, EXIF_TAG_ORIENTATION, orientation) && orientation <= UINT16_MAX) {
metadata.orientation = static_cast<uint16_t>(orientation);
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_ORIENTATION;
}
exif_data_unref(data);
return LARDON3D_ACQUISITION_OK;
}
Lardon3DAcquisitionResult extract_raw(const char *path,
Lardon3DAcquisitionMetadata &metadata) {
libraw_data_t *raw = libraw_init(0u);
if (raw == nullptr) {
return LARDON3D_ACQUISITION_INTERNAL_ERROR;
}
const int status = libraw_open_file(raw, path);
if (status != LIBRAW_SUCCESS) {
libraw_close(raw);
if (status == LIBRAW_FILE_UNSUPPORTED || status == LIBRAW_DATA_ERROR ||
status == LIBRAW_IO_ERROR) {
return LARDON3D_ACQUISITION_UNSUPPORTED_FORMAT;
}
return LARDON3D_ACQUISITION_CORRUPT_SOURCE;
}
metadata.source_kind = LARDON3D_ACQUISITION_SOURCE_RAW;
extract_raw_fixed_text(*raw, metadata);
extract_raw_image_unique_id(*raw, metadata);
if (raw->other.timestamp > 0) {
std::tm time_parts = {};
if (gmtime_r(&raw->other.timestamp, &time_parts) != nullptr &&
std::strftime(metadata.datetime_original, sizeof(metadata.datetime_original),
"%Y:%m:%d %H:%M:%S", &time_parts) != 0u) {
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL;
}
}
if (raw->sizes.width != 0u && raw->sizes.height != 0u) {
metadata.width = raw->sizes.width;
metadata.height = raw->sizes.height;
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_DIMENSIONS;
}
if (raw->sizes.flip >= 0 && raw->sizes.flip <= UINT16_MAX) {
metadata.orientation = static_cast<uint16_t>(raw->sizes.flip);
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_ORIENTATION;
}
libraw_close(raw);
return LARDON3D_ACQUISITION_OK;
}
void record_matching_text(const Lardon3DAcquisitionMetadata &left,
const Lardon3DAcquisitionMetadata &right, uint32_t field,
const char *left_text, size_t left_capacity,
const char *right_text, size_t right_capacity, uint32_t evidence,
Lardon3DAcquisitionPairResult &result) {
if (has_field(left, field) && has_field(right, field) &&
same_text(left_text, left_capacity, right_text, right_capacity)) {
result.evidence |= evidence;
}
}
unsigned evidence_count(uint32_t value) {
unsigned count = 0u;
while (value != 0u) {
count += value & 1u;
value >>= 1u;
}
return count;
}
} // namespace
#ifdef LARDON3D_ACQUISITION_PAIRING_TESTING
extern "C" void lardon3d_acquisition_test_extract_raw_image_unique_id(
const libraw_data_t *raw, Lardon3DAcquisitionMetadata *metadata) {
if (raw != nullptr && metadata != nullptr) {
extract_raw_image_unique_id(*raw, *metadata);
}
}
extern "C" void lardon3d_acquisition_test_extract_raw_fixed_text(
const libraw_data_t *raw, Lardon3DAcquisitionMetadata *metadata) {
if (raw != nullptr && metadata != nullptr) {
extract_raw_fixed_text(*raw, *metadata);
}
}
#endif
extern "C" Lardon3DAcquisitionResult lardon3d_acquisition_extract_metadata(
const char *path, Lardon3DAcquisitionMetadata *metadata) {
if (path == nullptr || path[0] == '\0' || metadata == nullptr) {
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
}
try {
*metadata = {};
metadata->policy_version = LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION;
FILE *file = std::fopen(path, "rb");
if (file == nullptr) {
return LARDON3D_ACQUISITION_IO_ERROR;
}
unsigned char signature[2] = {};
const size_t bytes_read = std::fread(signature, 1u, sizeof(signature), file);
const bool read_error = std::ferror(file) != 0;
std::fclose(file);
if (read_error) {
return LARDON3D_ACQUISITION_IO_ERROR;
}
if (bytes_read == sizeof(signature) && signature[0] == 0xffu && signature[1] == 0xd8u) {
return extract_jpeg(path, *metadata);
}
return extract_raw(path, *metadata);
} catch (const std::bad_alloc &) {
return LARDON3D_ACQUISITION_INTERNAL_ERROR;
} catch (...) {
return LARDON3D_ACQUISITION_INTERNAL_ERROR;
}
}
extern "C" Lardon3DAcquisitionResult lardon3d_acquisition_compare(
const Lardon3DAcquisitionMetadata *left, const Lardon3DAcquisitionMetadata *right,
int same_asset, int basename_hint, Lardon3DAcquisitionPairResult *result) {
if (left == nullptr || right == nullptr || result == nullptr ||
left->policy_version != LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION ||
right->policy_version != LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION) {
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
}
*result = {};
result->decision = LARDON3D_ACQUISITION_INSUFFICIENT;
if (basename_hint != 0) {
result->evidence |= LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT;
}
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID,
left->image_unique_id, sizeof(left->image_unique_id),
right->image_unique_id, sizeof(right->image_unique_id),
LARDON3D_ACQUISITION_EVIDENCE_IMAGE_UNIQUE_ID, *result);
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL,
left->body_serial, sizeof(left->body_serial), right->body_serial,
sizeof(right->body_serial),
LARDON3D_ACQUISITION_EVIDENCE_BODY_SERIAL, *result);
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_MAKE, left->make,
sizeof(left->make), right->make, sizeof(right->make),
LARDON3D_ACQUISITION_EVIDENCE_MAKE, *result);
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_MODEL, left->model,
sizeof(left->model), right->model, sizeof(right->model),
LARDON3D_ACQUISITION_EVIDENCE_MODEL, *result);
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL,
left->datetime_original, sizeof(left->datetime_original),
right->datetime_original, sizeof(right->datetime_original),
LARDON3D_ACQUISITION_EVIDENCE_DATETIME_SECOND, *result);
if (has_field(*left, LARDON3D_ACQUISITION_FIELD_DIMENSIONS) &&
has_field(*right, LARDON3D_ACQUISITION_FIELD_DIMENSIONS) &&
left->width == right->width && left->height == right->height) {
result->evidence |= LARDON3D_ACQUISITION_EVIDENCE_DIMENSIONS;
}
if (has_field(*left, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID) &&
has_field(*right, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID) &&
different_text(left->image_unique_id, sizeof(left->image_unique_id),
right->image_unique_id, sizeof(right->image_unique_id))) {
result->contradictions |= LARDON3D_ACQUISITION_CONTRADICTION_IMAGE_UNIQUE_ID;
}
if (has_field(*left, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL) &&
has_field(*right, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL) &&
different_text(left->body_serial, sizeof(left->body_serial), right->body_serial,
sizeof(right->body_serial))) {
result->contradictions |= LARDON3D_ACQUISITION_CONTRADICTION_BODY_SERIAL;
}
if (same_asset != 0) {
result->strength = 0u;
return LARDON3D_ACQUISITION_OK;
}
if (result->contradictions != 0u) {
result->decision = LARDON3D_ACQUISITION_DIFFERENT;
return LARDON3D_ACQUISITION_OK;
}
if ((result->evidence & LARDON3D_ACQUISITION_EVIDENCE_IMAGE_UNIQUE_ID) != 0u) {
result->decision = LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG;
result->strength = 100u;
return LARDON3D_ACQUISITION_OK;
}
const uint32_t metadata_evidence =
result->evidence & ~LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT;
if (metadata_evidence != 0u) {
result->decision = LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE;
result->strength = evidence_count(metadata_evidence) +
((result->evidence & LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT) != 0u
? 1u
: 0u);
}
return LARDON3D_ACQUISITION_OK;
}
extern "C" Lardon3DAcquisitionResult lardon3d_acquisition_select_candidate(
const Lardon3DAcquisitionMetadata *source,
const Lardon3DAcquisitionCandidate *candidates, size_t candidate_count,
Lardon3DAcquisitionSelection *selection) {
if (source == nullptr || selection == nullptr ||
(candidate_count != 0u && candidates == nullptr)) {
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
}
if (candidate_count > LARDON3D_ACQUISITION_MAX_CANDIDATES) {
return LARDON3D_ACQUISITION_LIMIT_EXCEEDED;
}
*selection = {};
selection->decision = LARDON3D_ACQUISITION_INSUFFICIENT;
uint32_t top_strength = 0u;
for (size_t index = 0u; index < candidate_count; ++index) {
if (candidates[index].metadata == nullptr) {
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
}
Lardon3DAcquisitionPairResult pair = {};
const Lardon3DAcquisitionResult status = lardon3d_acquisition_compare(
source, candidates[index].metadata, candidates[index].same_asset,
candidates[index].basename_hint, &pair);
if (status != LARDON3D_ACQUISITION_OK) {
return status;
}
if (pair.decision != LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG &&
pair.decision != LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE) {
continue;
}
if (pair.strength > top_strength) {
top_strength = pair.strength;
selection->selected_candidate_id = candidates[index].candidate_id;
selection->selected_pair = pair;
selection->top_candidate_count = 1u;
} else if (pair.strength == top_strength) {
++selection->top_candidate_count;
}
}
if (selection->top_candidate_count == 0u) {
return LARDON3D_ACQUISITION_OK;
}
if (selection->top_candidate_count > 1u) {
selection->decision = LARDON3D_ACQUISITION_AMBIGUOUS;
selection->selected_candidate_id = 0u;
return LARDON3D_ACQUISITION_OK;
}
selection->decision = selection->selected_pair.decision;
return LARDON3D_ACQUISITION_OK;
}