feat(quality): score every face on sharpness and alignment before it is evidence
Every embedding now carries the quality of the input it came from. Both axes fall out of the AR-005 warp for free: crop_sharpness() is the normalised Laplacian variance over the aligned 112x112, so contrast and size cannot leak into it, and the alignment residual is the part of the landmark deformation a similarity transform cannot explain, so in-plane roll reads as zero and foreshortening does not. Carried, not consumed. Nothing discounts or thresholds on either number yet -- that is AR-030 and VR-012, and the knee has to be located against recorded data before a gate is chosen. What this change buys is that the data exists to locate it with. No face is admitted unscored: the -1 sentinel is preserved rather than clamped, and a degenerate landmark fit is counted rather than silently dropped. Takes the VR-001 dump to schema_version 2. The bump is not for readers, which check for the datasets by name and replay a v1 dump unchanged; it is so a consumer can tell "never scored" from "scored zero", which is not recoverable from the arrays afterwards. TRACES: AR-028, AR-029, AR-030 | VR-001 | SR-002
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
#include <nanobind/stl/map.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
namespace nb = nanobind;
|
||||
@@ -69,6 +70,22 @@ template<> struct PythonConverter<EmbeddedSceneFrame> {
|
||||
auto conf = nb::cast<nb::ndarray<float, nb::shape<-1>, nb::c_contig>>(d["confidence"]);
|
||||
auto emb = nb::cast<nb::ndarray<float, nb::shape<-1, 512>, nb::c_contig>>(d["embeddings"]);
|
||||
|
||||
// AR-028 quality vector. Optional because a v1 dump predates it — absent
|
||||
// leaves the DetectedFace sentinels at -1, which reads as *unscored*, not
|
||||
// as a bad face. There is no live aligner on this path to recompute it:
|
||||
// the replay starts at the embedded-frame channel, so what the dump does
|
||||
// not carry is genuinely gone.
|
||||
//
|
||||
// Held in named locals, like the four above, because the ndarray owns the
|
||||
// reference that keeps the buffer alive — reading .data() off a temporary
|
||||
// would leave the pointer dangling at the end of the statement.
|
||||
using FloatCol = nb::ndarray<float, nb::shape<-1>, nb::c_contig>;
|
||||
std::optional<FloatCol> sharp_col, resid_col;
|
||||
if (d.contains("sharpness")) sharp_col = nb::cast<FloatCol>(d["sharpness"]);
|
||||
if (d.contains("alignment_residual")) resid_col = nb::cast<FloatCol>(d["alignment_residual"]);
|
||||
const float* sp = sharp_col ? sharp_col->data() : nullptr;
|
||||
const float* rp = resid_col ? resid_col->data() : nullptr;
|
||||
|
||||
const size_t n = bbox.shape(0);
|
||||
ef.faces.reserve(n);
|
||||
ef.embeddings.reserve(n);
|
||||
@@ -82,6 +99,8 @@ template<> struct PythonConverter<EmbeddedSceneFrame> {
|
||||
for (int k = 0; k < 5; ++k)
|
||||
f.landmarks[k] = cv::Point2f(lp[i*10 + k*2], lp[i*10 + k*2 + 1]);
|
||||
f.confidence = cp[i];
|
||||
if (sp) f.sharpness = sp[i];
|
||||
if (rp) f.alignment_residual = rp[i];
|
||||
ef.faces.push_back(f);
|
||||
|
||||
Embedding e;
|
||||
|
||||
Reference in New Issue
Block a user