Add SIGFM (SIFT-based) matching algorithm for small-area sensors

Add SIGFM, a SIFT-based fingerprint matching algorithm designed for
small-area sensors where NBIS minutiae detection is unreliable.

SIGFM uses OpenCV4 SIFT keypoint matching with geometric consistency
verification to compare fingerprint images directly, making it suitable
for sensors with capture areas as small as 112x88 pixels.

The algorithm is integrated as an optional dependency: when OpenCV4 is
available, SIGFM support and SIGFM-based drivers are built; otherwise,
the build proceeds without them. This follows the same pattern used for
PIXMAN and UDEV optional dependencies.

Based on work by Natalie Klestrup Röijezon (MR !418) and Tooniis
(MR !530).
This commit is contained in:
Sergey Subbotin 2026-03-12 10:30:55 +01:00
parent 2c7842c905
commit 910fcad2cf
36 changed files with 6694 additions and 102 deletions

View file

@ -850,7 +850,7 @@ fpi_device_aes1610_class_init (FpiDeviceAes1610Class *klass)
img_class->activate = dev_activate;
img_class->deactivate = dev_deactivate;
img_class->bz3_threshold = 20;
img_class->score_threshold = 20;
img_class->img_width = IMAGE_WIDTH;
img_class->img_height = -1;

View file

@ -80,7 +80,7 @@ fpi_device_aes1660_class_init (FpiDeviceAes1660Class *klass)
dev_class->id_table = id_table;
dev_class->scan_type = FP_SCAN_TYPE_SWIPE;
img_class->bz3_threshold = 20;
img_class->score_threshold = 20;
img_class->img_width = FRAME_WIDTH + FRAME_WIDTH / 2;
img_class->img_height = -1;

View file

@ -82,7 +82,7 @@ fpi_device_aes2660_class_init (FpiDeviceAes2660Class *klass)
dev_class->id_table = id_table;
dev_class->scan_type = FP_SCAN_TYPE_SWIPE;
img_class->bz3_threshold = 20;
img_class->score_threshold = 20;
img_class->img_width = FRAME_WIDTH + FRAME_WIDTH / 2;
img_class->img_height = -1;

View file

@ -266,7 +266,7 @@ fpi_device_aes3k_class_init (FpiDeviceAes3kClass *klass)
img_class->deactivate = aes3k_dev_deactivate;
/* Extremely low due to low image quality. */
img_class->bz3_threshold = 9;
img_class->score_threshold = 9;
/* Everything else is set by the subclasses. */
}

View file

@ -440,5 +440,5 @@ fpi_device_egis0570_class_init (FpDeviceEgis0570Class *klass)
img_class->img_width = EGIS0570_IMGWIDTH;
img_class->img_height = -1;
img_class->bz3_threshold = EGIS0570_BZ3_THRESHOLD; /* security issue */
img_class->score_threshold = EGIS0570_BZ3_THRESHOLD; /* security issue */
}

View file

@ -161,7 +161,7 @@ static unsigned char repeat_pkts[][EGIS0570_PKTSIZE] =
};
/*
* This sensor is small so I decided to reduce bz3_threshold from
* This sensor is small so I decided to reduce score_threshold from
* 40 to 10 to have more success to fail ratio
* Bozorth3 Algorithm seems not fine at the end
* foreget about security :))

View file

@ -1006,5 +1006,5 @@ fpi_device_elan_class_init (FpiDeviceElanClass *klass)
img_class->deactivate = dev_deactivate;
img_class->change_state = dev_change_state;
img_class->bz3_threshold = 24;
img_class->score_threshold = 24;
}

View file

@ -1701,7 +1701,7 @@ fpi_device_elanspi_class_init (FpiDeviceElanSpiClass *klass)
dev_class->scan_type = FP_SCAN_TYPE_SWIPE;
dev_class->nr_enroll_stages = 7; /* these sensors are very hit or miss, may as well record a few extras */
img_class->bz3_threshold = 24;
img_class->score_threshold = 24;
img_class->img_open = elanspi_open;
img_class->activate = elanspi_activate;
img_class->deactivate = elanspi_deactivate;

View file

@ -435,7 +435,7 @@ fpi_device_nb1010_class_init (FpiDeviceNb1010Class *klass)
img_class->img_height = FRAME_HEIGHT;
img_class->img_width = FRAME_WIDTH;
img_class->bz3_threshold = 24;
img_class->score_threshold = 24;
img_class->img_open = nb1010_dev_init;
img_class->img_close = nb1010_dev_deinit;

View file

@ -1540,7 +1540,7 @@ dev_init (FpImageDevice *dev)
self->assembling_ctx.line_width = IMG_WIDTH_1001;
/* The sensor resolution is too low for the normal threshold. */
fpi_image_device_set_bz3_threshold (dev, 25);
fpi_image_device_set_score_threshold (dev, 25);
break;
case UPEKSONLY_2016:

View file

@ -457,7 +457,7 @@ fpi_device_upektc_class_init (FpiDeviceUpektcClass *klass)
img_class->activate = dev_activate;
img_class->deactivate = dev_deactivate;
img_class->bz3_threshold = 30;
img_class->score_threshold = 30;
img_class->img_width = IMAGE_WIDTH;
img_class->img_height = IMAGE_HEIGHT;

View file

@ -777,7 +777,7 @@ fpi_device_upektc_img_class_init (FpiDeviceUpektcImgClass *klass)
img_class->activate = dev_activate;
img_class->deactivate = dev_deactivate;
img_class->bz3_threshold = 20;
img_class->score_threshold = 20;
img_class->img_width = -1;
img_class->img_height = -1;

View file

@ -766,7 +766,7 @@ fpi_device_vfs0050_class_init (FpDeviceVfs0050Class *klass)
img_class->activate = dev_activate;
img_class->deactivate = dev_deactivate;
img_class->bz3_threshold = 24;
img_class->score_threshold = 24;
img_class->img_width = VFS_IMAGE_WIDTH;
img_class->img_height = -1;

View file

@ -1363,7 +1363,7 @@ fpi_device_vfs101_class_init (FpDeviceVfs101Class *klass)
img_class->activate = dev_activate;
img_class->deactivate = dev_deactivate;
img_class->bz3_threshold = 24;
img_class->score_threshold = 24;
img_class->img_width = VFS_IMG_WIDTH;
img_class->img_height = -1;

View file

@ -264,7 +264,7 @@ fpi_device_vfs301_class_init (FpDeviceVfs301Class *klass)
img_class->deactivate = dev_deactivate;
img_class->change_state = dev_change_state;
img_class->bz3_threshold = 24;
img_class->score_threshold = 24;
img_class->img_width = VFS301_FP_WIDTH;
img_class->img_height = -1;

View file

@ -894,7 +894,7 @@ fpi_device_vfs5011_class_init (FpDeviceVfs5011Class *klass)
img_class->activate = dev_activate;
img_class->deactivate = dev_deactivate;
img_class->bz3_threshold = 20;
img_class->score_threshold = 20;
img_class->img_width = VFS5011_IMAGE_WIDTH;
img_class->img_height = -1;

View file

@ -1065,7 +1065,7 @@ fpi_device_vfs7552_class_init (FpDeviceVfs7552Class *klass)
img_class->activate = dev_activate;
img_class->img_open = dev_open;
img_class->bz3_threshold = 20;
img_class->score_threshold = 20;
img_class->img_width = VFS7552_IMAGE_WIDTH;
img_class->img_height = VFS7552_IMAGE_HEIGHT;

View file

@ -36,7 +36,8 @@ typedef struct
GError *action_error;
FpImage *capture_image;
gint bz3_threshold;
gint score_threshold;
FpiPrintType algorithm;
} FpImageDevicePrivate;

View file

@ -17,6 +17,7 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "fpi-print.h"
#define FP_COMPONENT "image_device"
#include "fpi-log.h"
@ -101,7 +102,6 @@ fp_image_device_start_capture_action (FpDevice *device)
FpImageDevice *self = FP_IMAGE_DEVICE (device);
FpImageDevicePrivate *priv = fp_image_device_get_instance_private (self);
FpiDeviceAction action;
FpiPrintType print_type;
/* There is just one action that we cannot support out
* of the box, which is a capture without first waiting
@ -125,9 +125,10 @@ fp_image_device_start_capture_action (FpDevice *device)
FpPrint *enroll_print = NULL;
fpi_device_get_enroll_data (device, &enroll_print);
FpiPrintType print_type;
g_object_get (enroll_print, "fpi-type", &print_type, NULL);
if (print_type != FPI_PRINT_NBIS)
fpi_print_set_type (enroll_print, FPI_PRINT_NBIS);
if (print_type != priv->algorithm)
fpi_print_set_type (enroll_print, priv->algorithm);
}
priv->enroll_stage = 0;
@ -194,9 +195,12 @@ fp_image_device_constructed (GObject *obj)
FpImageDeviceClass *cls = FP_IMAGE_DEVICE_GET_CLASS (self);
/* Set default threshold. */
priv->bz3_threshold = BOZORTH3_DEFAULT_THRESHOLD;
if (cls->bz3_threshold > 0)
priv->bz3_threshold = cls->bz3_threshold;
priv->score_threshold = BOZORTH3_DEFAULT_THRESHOLD;
if (cls->score_threshold > 0)
priv->score_threshold = cls->score_threshold;
priv->algorithm = FPI_PRINT_NBIS;
if (cls->algorithm > 0)
priv->algorithm = (FpiPrintType) cls->algorithm;
G_OBJECT_CLASS (fp_image_device_parent_class)->constructed (obj);
}

View file

@ -165,10 +165,21 @@ typedef struct
FpiImageFlags flags;
unsigned char *image;
gboolean image_changed;
} DetectMinutiaeNbisData;
} DetectMinutiaeData;
#ifdef HAVE_SIGFM
typedef struct
{
SigfmImgInfo * sigfm_info;
guchar * image;
gint width;
gint height;
GAsyncReadyCallback user_cb;
} ExtractSigfmData;
#endif
static void
fp_image_detect_minutiae_free (DetectMinutiaeNbisData *data)
fp_image_detect_minutiae_free (DetectMinutiaeData *data)
{
g_clear_pointer (&data->minutiae, free_minutiae);
g_clear_pointer (&data->binarized, g_free);
@ -179,15 +190,45 @@ fp_image_detect_minutiae_free (DetectMinutiaeNbisData *data)
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (DetectMinutiaeNbisData, fp_image_detect_minutiae_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (DetectMinutiaeData, fp_image_detect_minutiae_free)
#ifdef HAVE_SIGFM
static void
fp_image_sigfm_extract_free (ExtractSigfmData * data)
{
g_clear_pointer (&data->image, g_free);
g_clear_pointer (&data->sigfm_info, sigfm_free_info);
g_free (data);
}
static void
fp_image_sigfm_extract_cb (GObject * source_object, GAsyncResult * res,
gpointer user_data)
{
GTask * task = G_TASK (res);
FpImage * image;
ExtractSigfmData * data = g_task_get_task_data (task);
if (!g_task_had_error (task))
{
image = FP_IMAGE (source_object);
g_clear_pointer (&image->data, g_free);
image->data = g_steal_pointer (&data->image);
image->sigfm_info = g_steal_pointer (&data->sigfm_info);
}
if (data->user_cb)
data->user_cb (source_object, res, user_data);
}
#endif
static gboolean
fp_image_detect_minutiae_nbis_finish (FpImage *self,
GTask *task,
GError **error)
{
g_autoptr(DetectMinutiaeNbisData) data = NULL;
g_autoptr(DetectMinutiaeData) data = NULL;
data = g_task_propagate_pointer (task, error);
@ -270,14 +311,48 @@ invert_colors (guint8 *data, gint width, gint height)
data[i] = 0xff - data[i];
}
#ifdef HAVE_SIGFM
static void
fp_image_detect_minutiae_nbis_thread_func (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
fp_image_sigfm_extract_thread_func (GTask * task, void * src_obj,
void * task_data,
GCancellable * cancellable)
{
ExtractSigfmData * data = task_data;
GTimer * timer = g_timer_new ();
data->sigfm_info = sigfm_extract (data->image, data->width, data->height);
g_timer_stop (timer);
fp_dbg ("sigfm extract completed in %f secs", g_timer_elapsed (timer, NULL));
g_timer_destroy (timer);
if (!data->sigfm_info)
{
fp_err ("extract sigfm info failed");
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "SIGFM scan failed");
g_object_unref (task);
return;
}
if (sigfm_keypoints_count (data->sigfm_info) < 25)
{
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
"No enough keypoints found");
g_object_unref (task);
return;
}
g_task_return_boolean (task, TRUE);
g_object_unref (task);
}
#endif
static void
fp_image_detect_minutiae_thread_func (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
g_autoptr(GTimer) timer = NULL;
g_autoptr(DetectMinutiaeNbisData) ret_data = NULL;
g_autoptr(DetectMinutiaeData) ret_data = NULL;
g_autoptr(GTask) thread_task = g_steal_pointer (&task);
g_autofree gint *direction_map = NULL;
g_autofree gint *low_contrast_map = NULL;
@ -300,7 +375,7 @@ fp_image_detect_minutiae_nbis_thread_func (GTask *task,
if (minutiae_flags != FPI_IMAGE_NONE)
image = g_memdup2 (self->data, self->width * self->height);
ret_data = g_new0 (DetectMinutiaeNbisData, 1);
ret_data = g_new0 (DetectMinutiaeData, 1);
ret_data->flags = minutiae_flags;
ret_data->image = image;
ret_data->image_changed = image != self->data;
@ -448,6 +523,51 @@ fp_image_get_minutiae (FpImage *self)
return self->minutiae;
}
#ifdef HAVE_SIGFM
/**
* fp_image_get_sigfm_info:
* @self: A #FpImage
*
* Gets the SIGFM keypoints and descriptors for an image. This data must
* not be modified or freed. You need to first extract keypoints and
* descriptors using fp_image_extract_sigfm_info().
*
* Returns: (skip): SIGFM keypoints and descriptors
*/
SigfmImgInfo *
fp_image_get_sigfm_info (FpImage * self)
{
return self->sigfm_info;
}
/*
* fp_image_extract_sigfm_info:
*
* Extracts SIFT keypoints and descriptors from an image.
* Completion is handled via fp_image_detect_minutiae_finish().
*/
void
fp_image_extract_sigfm_info (FpImage * self, GCancellable * cancellable,
GAsyncReadyCallback callback, gpointer user_data)
{
GTask * task;
ExtractSigfmData * data = g_new0 (ExtractSigfmData, 1);
task = g_task_new (self, cancellable, fp_image_sigfm_extract_cb, user_data);
g_task_set_source_tag (task, fp_image_extract_sigfm_info);
data->image = g_malloc (self->width * self->height);
memcpy (data->image, self->data, self->width * self->height);
data->width = self->width;
data->height = self->height;
data->user_cb = callback;
g_task_set_task_data (task, data,
(GDestroyNotify) fp_image_sigfm_extract_free);
g_task_run_in_thread (task, fp_image_sigfm_extract_thread_func);
}
#endif
/**
* fp_image_detect_minutiae:
* @self: A #FpImage
@ -481,7 +601,7 @@ fp_image_detect_minutiae (FpImage *self,
}
g_task_run_in_thread (g_steal_pointer (&task),
fp_image_detect_minutiae_nbis_thread_func);
fp_image_detect_minutiae_thread_func);
}
/**
@ -504,6 +624,12 @@ fp_image_detect_minutiae_finish (FpImage *self,
g_return_val_if_fail (FP_IS_IMAGE (self), FALSE);
g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
#ifdef HAVE_SIGFM
if (g_task_get_source_tag (G_TASK (result)) == fp_image_extract_sigfm_info)
return g_task_propagate_boolean (G_TASK (result), error);
#endif
g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) ==
fp_image_detect_minutiae, FALSE);

View file

@ -43,6 +43,7 @@ void fp_image_detect_minutiae (FpImage *self,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
gboolean fp_image_detect_minutiae_finish (FpImage *self,
GAsyncResult *result,
GError **error);

View file

@ -18,6 +18,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <config.h>
#ifdef HAVE_SIGFM
#include "sigfm/sigfm.h"
#endif
#define FP_COMPONENT "print"
#include "fp-print-private.h"
@ -680,6 +684,8 @@ fp_print_serialize (FpPrint *print,
g_variant_builder_open (&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_close (&builder);
GPtrArray * to_free = NULL;
/* Insert NBIS print data for type NBIS, otherwise the GVariant directly */
if (print->type == FPI_PRINT_NBIS)
{
@ -714,6 +720,30 @@ fp_print_serialize (FpPrint *print,
g_variant_builder_close (&nested);
g_variant_builder_add (&builder, "v", g_variant_builder_end (&nested));
}
#ifdef HAVE_SIGFM
else if (print->type == FPI_PRINT_SIGFM)
{
to_free = g_ptr_array_new ();
g_ptr_array_set_free_func (to_free, free);
GVariantBuilder nested =
G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(a(ay))"));
g_variant_builder_open (&nested, G_VARIANT_TYPE ("a(ay)"));
for (int i = 0; i != print->prints->len; ++i)
{
g_variant_builder_open (&nested, G_VARIANT_TYPE ("(ay)"));
SigfmImgInfo * info = g_ptr_array_index (print->prints, i);
int slen;
unsigned char * serialized = sigfm_serialize_binary (info, &slen);
g_variant_builder_add_value (
&nested, g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE,
serialized, slen, 1));
g_ptr_array_add (to_free, serialized);
g_variant_builder_close (&nested);
}
g_variant_builder_close (&nested);
g_variant_builder_add (&builder, "v", g_variant_builder_end (&nested));
}
#endif
else
{
g_variant_builder_add (&builder, "v", g_variant_new_variant (print->data));
@ -741,6 +771,8 @@ fp_print_serialize (FpPrint *print,
g_variant_get_data (result);
g_variant_store (result, (*data) + 3);
if (to_free != NULL)
g_ptr_array_free (to_free, TRUE);
return TRUE;
}
@ -870,6 +902,37 @@ fp_print_deserialize (const guchar *data,
g_ptr_array_add (result->prints, g_steal_pointer (&xyt));
}
}
#ifdef HAVE_SIGFM
else if (type == FPI_PRINT_SIGFM)
{
g_autoptr(GVariant) prints = g_variant_get_child_value (print_data, 0);
guint i;
result = g_object_new (FP_TYPE_PRINT, "driver", driver, "device-id",
device_id, "device-stored", device_stored, NULL);
g_object_ref_sink (result);
fpi_print_set_type (result, FPI_PRINT_SIGFM);
for (i = 0; i < g_variant_n_children (prints); i++)
{
g_autoptr(GVariant) sigfm_data = NULL;
sigfm_data = g_variant_get_child_value (prints, i);
GVariant * child = g_variant_get_child_value (sigfm_data, 0);
gsize slen;
const unsigned char * serialized =
g_variant_get_fixed_array (child, &slen, sizeof (unsigned char));
g_variant_unref (child);
SigfmImgInfo * sigfm_info = sigfm_deserialize_binary (serialized, slen);
if (!sigfm_info)
goto invalid_format;
g_ptr_array_add (result->prints, g_steal_pointer (&sigfm_info));
}
}
#endif
else if (type == FPI_PRINT_RAW)
{
g_autoptr(GVariant) fp_data = g_variant_get_child_value (print_data, 0);

View file

@ -17,6 +17,9 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <config.h>
#include "fpi-print.h"
#include "fpi-image.h"
#define FP_COMPONENT "image_device"
#include "fpi-log.h"
@ -276,7 +279,7 @@ fpi_image_device_minutiae_detected (GObject *source_object, GAsyncResult *res, g
if (!error)
{
print = fp_print_new (device);
fpi_print_set_type (print, FPI_PRINT_NBIS);
fpi_print_set_type (print, priv->algorithm);
if (!fpi_print_add_from_image (print, image, &error))
{
g_clear_object (&print);
@ -319,13 +322,24 @@ fpi_image_device_minutiae_detected (GObject *source_object, GAsyncResult *res, g
else if (action == FPI_DEVICE_ACTION_VERIFY)
{
FpPrint *template;
FpiMatchResult result;
FpiMatchResult result = FPI_MATCH_ERROR;
fpi_device_get_verify_data (device, &template);
if (print)
result = fpi_print_bz3_match (template, print, priv->bz3_threshold, &error);
{
if (priv->algorithm == FPI_PRINT_NBIS)
result = fpi_print_bz3_match (template, print, priv->score_threshold,
&error);
#ifdef HAVE_SIGFM
else if (priv->algorithm == FPI_PRINT_SIGFM)
result = fpi_print_sigfm_match (template, print, priv->score_threshold,
&error);
#endif
}
else
result = FPI_MATCH_ERROR;
{
result = FPI_MATCH_ERROR;
}
if (!error || error->domain == FP_DEVICE_RETRY)
fpi_device_verify_report (device, result, g_steal_pointer (&print), g_steal_pointer (&error));
@ -343,7 +357,17 @@ fpi_image_device_minutiae_detected (GObject *source_object, GAsyncResult *res, g
{
FpPrint *template = g_ptr_array_index (templates, i);
if (fpi_print_bz3_match (template, print, priv->bz3_threshold, &error) == FPI_MATCH_SUCCESS)
int match_result = FPI_MATCH_ERROR;
if (priv->algorithm == FPI_PRINT_NBIS)
match_result = fpi_print_bz3_match (template, print,
priv->score_threshold, &error);
#ifdef HAVE_SIGFM
else if (priv->algorithm == FPI_PRINT_SIGFM)
match_result = fpi_print_sigfm_match (template, print,
priv->score_threshold, &error);
#endif
if (match_result == FPI_MATCH_SUCCESS)
{
result = template;
break;
@ -371,9 +395,9 @@ fpi_image_device_minutiae_detected (GObject *source_object, GAsyncResult *res, g
/* Private API */
/**
* fpi_image_device_set_bz3_threshold:
* fpi_image_device_set_score_threshold:
* @self: a #FpImageDevice imaging fingerprint device
* @bz3_threshold: BZ3 threshold to use
* @score_threshold: BZ3 threshold to use
*
* Dynamically adjust the bz3 threshold. This is only needed for drivers
* that support devices with different properties. It should generally be
@ -381,15 +405,15 @@ fpi_image_device_minutiae_detected (GObject *source_object, GAsyncResult *res, g
* callback.
*/
void
fpi_image_device_set_bz3_threshold (FpImageDevice *self,
gint bz3_threshold)
fpi_image_device_set_score_threshold (FpImageDevice *self,
gint score_threshold)
{
FpImageDevicePrivate *priv = fp_image_device_get_instance_private (self);
g_return_if_fail (FP_IS_IMAGE_DEVICE (self));
g_return_if_fail (bz3_threshold > 0);
g_return_if_fail (score_threshold > 0);
priv->bz3_threshold = bz3_threshold;
priv->score_threshold = score_threshold;
}
/**
@ -494,12 +518,22 @@ fpi_image_device_image_captured (FpImageDevice *self, FpImage *image)
priv->minutiae_scan_active = TRUE;
/* XXX: We also detect minutiae in capture mode, we solely do this
* to normalize the image which will happen as a by-product. */
fp_image_detect_minutiae (image,
fpi_device_get_cancellable (FP_DEVICE (self)),
fpi_image_device_minutiae_detected,
self);
#ifdef HAVE_SIGFM
if (priv->algorithm == FPI_PRINT_SIGFM)
{
fp_image_extract_sigfm_info (image,
fpi_device_get_cancellable (FP_DEVICE (self)),
fpi_image_device_minutiae_detected, self);
}
else
#endif
{
/* XXX: We also detect minutiae in capture mode, we solely do this
* to normalize the image which will happen as a by-product. */
fp_image_detect_minutiae (image,
fpi_device_get_cancellable (FP_DEVICE (self)),
fpi_image_device_minutiae_detected, self);
}
/* XXX: This is wrong if we add support for raw capture mode. */
fp_image_device_change_state (self, FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF);

View file

@ -19,8 +19,9 @@
#pragma once
#include "fpi-device.h"
#include "fp-image-device.h"
#include "fpi-device.h"
#include "fpi-print.h"
/**
* FpiImageDeviceState:
@ -70,9 +71,14 @@ typedef enum {
FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF,
} FpiImageDeviceState;
typedef enum {
FPI_DEVICE_ALGO_NBIS = FPI_PRINT_NBIS,
FPI_DEVICE_ALGO_SIGFM = FPI_PRINT_SIGFM,
} FpiImageDeviceAlgorithm;
/**
* FpImageDeviceClass:
* @bz3_threshold: Threshold to consider bozorth3 score a match, default: 40
* @score_threshold: Threshold to consider bozorth3 score a match, default: 40
* @img_width: Width of the image, only provide if constant
* @img_height: Height of the image, only provide if constant
* @img_open: Open the device and do basic initialization
@ -102,22 +108,23 @@ typedef enum {
*/
struct _FpImageDeviceClass
{
FpDeviceClass parent_class;
FpDeviceClass parent_class;
gint bz3_threshold;
gint img_width;
gint img_height;
gint score_threshold;
gint img_width;
gint img_height;
FpiImageDeviceAlgorithm algorithm;
void (*img_open) (FpImageDevice *dev);
void (*img_close) (FpImageDevice *dev);
void (*activate) (FpImageDevice *dev);
void (*change_state) (FpImageDevice *dev,
FpiImageDeviceState state);
void (*deactivate) (FpImageDevice *dev);
void (*img_open) (FpImageDevice *dev);
void (*img_close) (FpImageDevice *dev);
void (*activate) (FpImageDevice *dev);
void (*change_state) (FpImageDevice *dev,
FpiImageDeviceState state);
void (*deactivate) (FpImageDevice *dev);
};
void fpi_image_device_set_bz3_threshold (FpImageDevice *self,
gint bz3_threshold);
void fpi_image_device_set_score_threshold (FpImageDevice *self,
gint score_threshold);
void fpi_image_device_session_error (FpImageDevice *self,
GError *error);

View file

@ -21,6 +21,10 @@
#pragma once
#include "fp-image.h"
#include <config.h>
#ifdef HAVE_SIGFM
#include "sigfm/sigfm.h"
#endif
/**
* FpiImageFlags:
@ -64,12 +68,15 @@ struct _FpImage
FpiImageFlags flags;
/*< private >*/
guint8 *data;
guint8 *binarized;
guint8 *data;
guint8 *binarized;
GPtrArray *minutiae;
GPtrArray *minutiae;
#ifdef HAVE_SIGFM
SigfmImgInfo *sigfm_info;
#endif
gboolean detection_in_progress;
gboolean detection_in_progress;
};
gint fpi_std_sq_dev (const guint8 *buf,
@ -81,3 +88,11 @@ gint fpi_mean_sq_diff_norm (const guint8 *buf1,
FpImage *fpi_image_resize (FpImage *orig,
guint w_factor,
guint h_factor);
#ifdef HAVE_SIGFM
SigfmImgInfo * fp_image_get_sigfm_info (FpImage *self);
void fp_image_extract_sigfm_info (FpImage *self,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
#endif

View file

@ -18,6 +18,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "fpi-print.h"
#ifdef HAVE_SIGFM
#include "sigfm/sigfm.h"
#endif
#define FP_COMPONENT "print"
#include "fpi-log.h"
@ -39,18 +43,36 @@
* @print: A #FpPrint
* @add: Print to append to @print
*
* Appends the single #FPI_PRINT_NBIS print from @add to the collection of
* prints in @print. Both print objects need to be of type #FPI_PRINT_NBIS
* for this to work.
* Appends the single #FPI_PRINT_NBIS or #FPI_PRINT_SIGFM print from @add
* to the collection of prints in @print. Both print objects need to be of
* the same type for this to work.
*/
void
fpi_print_add_print (FpPrint *print, FpPrint *add)
{
g_return_if_fail (print->type == FPI_PRINT_NBIS);
g_return_if_fail (add->type == FPI_PRINT_NBIS);
g_return_if_fail (print->type == FPI_PRINT_NBIS
#ifdef HAVE_SIGFM
|| print->type == FPI_PRINT_SIGFM
#endif
);
g_return_if_fail (add->type == FPI_PRINT_NBIS
#ifdef HAVE_SIGFM
|| add->type == FPI_PRINT_SIGFM
#endif
);
g_return_if_fail (add->type == print->type);
g_return_if_fail (add->prints->len > 0);
g_assert (add->prints->len == 1);
g_ptr_array_add (print->prints, g_memdup2 (add->prints->pdata[0], sizeof (struct xyt_struct)));
#ifdef HAVE_SIGFM
void * to_add =
print->type == FPI_PRINT_NBIS ?
g_memdup2 (add->prints->pdata[0], sizeof (struct xyt_struct)) :
(void *) sigfm_copy_info (add->prints->pdata[0]);
#else
void * to_add = g_memdup2 (add->prints->pdata[0], sizeof (struct xyt_struct));
#endif
g_ptr_array_add (print->prints, to_add);
}
/**
@ -71,10 +93,20 @@ fpi_print_set_type (FpPrint *print,
g_return_if_fail (print->type == FPI_PRINT_UNDEFINED);
print->type = type;
if (print->type == FPI_PRINT_NBIS)
if (print->type == FPI_PRINT_NBIS
#ifdef HAVE_SIGFM
|| print->type == FPI_PRINT_SIGFM
#endif
)
{
g_assert_null (print->prints);
#ifdef HAVE_SIGFM
print->prints = g_ptr_array_new_with_free_func (
print->type == FPI_PRINT_NBIS ? g_free :
(void (*)(void *))(sigfm_free_info));
#else
print->prints = g_ptr_array_new_with_free_func (g_free);
#endif
}
g_object_notify (G_OBJECT (print), "fpi-type");
}
@ -144,7 +176,7 @@ minutiae_to_xyt (struct fp_minutiae *minutiae,
* @error: Return location for error
*
* Extracts the minutiae from the given image and adds it to @print of
* type #FPI_PRINT_NBIS.
* type #FPI_PRINT_NBIS or #FPI_PRINT_SIGFM.
*
* The @image will be kept so that API users can get retrieve it e.g.
* for debugging purposes.
@ -160,7 +192,11 @@ fpi_print_add_from_image (FpPrint *print,
struct fp_minutiae _minutiae;
struct xyt_struct *xyt;
if (print->type != FPI_PRINT_NBIS || !image)
if ((print->type != FPI_PRINT_NBIS
#ifdef HAVE_SIGFM
&& print->type != FPI_PRINT_SIGFM
#endif
) || !image)
{
g_set_error (error,
G_IO_ERROR,
@ -169,23 +205,31 @@ fpi_print_add_from_image (FpPrint *print,
return FALSE;
}
minutiae = fp_image_get_minutiae (image);
if (!minutiae || minutiae->len == 0)
if (print->type == FPI_PRINT_NBIS)
{
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"No minutiae found in image or not yet detected!");
return FALSE;
minutiae = fp_image_get_minutiae (image);
if (!minutiae || minutiae->len == 0)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"No minutiae found in image or not yet detected!");
return FALSE;
}
_minutiae.num = minutiae->len;
_minutiae.list = (struct fp_minutia **) minutiae->pdata;
_minutiae.alloc = minutiae->len;
xyt = g_new0 (struct xyt_struct, 1);
minutiae_to_xyt (&_minutiae, image->width, image->height, xyt);
g_ptr_array_add (print->prints, xyt);
}
_minutiae.num = minutiae->len;
_minutiae.list = (struct fp_minutia **) minutiae->pdata;
_minutiae.alloc = minutiae->len;
xyt = g_new0 (struct xyt_struct, 1);
minutiae_to_xyt (&_minutiae, image->width, image->height, xyt);
g_ptr_array_add (print->prints, xyt);
#ifdef HAVE_SIGFM
else if (print->type == FPI_PRINT_SIGFM)
{
SigfmImgInfo *info = fp_image_get_sigfm_info (image);
g_ptr_array_add (print->prints, info);
}
#endif
g_clear_object (&print->image);
print->image = g_object_ref (image);
@ -198,7 +242,7 @@ fpi_print_add_from_image (FpPrint *print,
* fpi_print_bz3_match:
* @template: A #FpPrint containing one or more prints
* @print: A newly scanned #FpPrint to test
* @bz3_threshold: The BZ3 match threshold
* @score_threshold: The BZ3 match threshold
* @error: Return location for error
*
* Match the newly scanned @print (containing exactly one print) against the
@ -210,14 +254,14 @@ fpi_print_add_from_image (FpPrint *print,
* Returns: Whether the prints match, @error will be set if #FPI_MATCH_ERROR is returned
*/
FpiMatchResult
fpi_print_bz3_match (FpPrint *template, FpPrint *print, gint bz3_threshold, GError **error)
fpi_print_bz3_match (FpPrint *template, FpPrint *print, gint score_threshold, GError **error)
{
struct xyt_struct *pstruct;
gint probe_len;
gint i;
/* XXX: Use a different error type? */
if (template->type != FPI_PRINT_NBIS || print->type != FPI_PRINT_NBIS)
if (template->type != FPI_PRINT_NBIS)
{
*error = fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
"It is only possible to match NBIS type print data");
@ -240,15 +284,62 @@ fpi_print_bz3_match (FpPrint *template, FpPrint *print, gint bz3_threshold, GErr
gint score;
gstruct = g_ptr_array_index (template->prints, i);
score = bozorth_to_gallery (probe_len, pstruct, gstruct);
fp_dbg ("score %d/%d", score, bz3_threshold);
fp_dbg ("score %d/%d", score, score_threshold);
if (score >= bz3_threshold)
if (score >= score_threshold)
return FPI_MATCH_SUCCESS;
}
return FPI_MATCH_FAIL;
}
#ifdef HAVE_SIGFM
/**
* fpi_print_sigfm_match:
* @template: A #FpPrint containing one or more prints
* @print: A newly scanned #FpPrint to test
* @score_threshold: The BZ3 match threshold
* @error: Return location for error
*
* Match the newly scanned @print (containing exactly one print) against the
* prints contained in @template which will have been stored during enrollment.
*
* Both @template and @print need to be of type #FPI_PRINT_SIGFM for this to
* work.
*
* Returns: Whether the prints match, @error will be set if #FPI_MATCH_ERROR is returned
*/
FpiMatchResult
fpi_print_sigfm_match (FpPrint * template, FpPrint * print,
gint score_threshold, GError ** error)
{
if (template->type != FPI_PRINT_SIGFM)
{
*error = fpi_device_error_new_msg (
FP_DEVICE_ERROR_NOT_SUPPORTED,
"Cannot call sigfm match with non-sigfm print data, type was %d",
template->type);
return FPI_MATCH_ERROR;
}
SigfmImgInfo * against = g_ptr_array_index (print->prints, 0);
for (int i = 0; i != template->prints->len; ++i)
{
SigfmImgInfo * pinfo = g_ptr_array_index (template->prints, i);
int score = sigfm_match_score (pinfo, against);
if (score < 0)
{
*error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID,
"error in sigfm_match_score");
return FPI_MATCH_ERROR;
}
fp_dbg ("sigfm score %d/%d", score, score_threshold);
if (score >= score_threshold)
return FPI_MATCH_SUCCESS;
}
return FPI_MATCH_FAIL;
}
#endif
/**
* fpi_print_generate_user_id:
* @print: #FpPrint to generate the ID for

View file

@ -3,6 +3,7 @@
#include "fpi-enums.h"
#include "fp-device.h"
#include "fp-print.h"
#include <config.h>
G_BEGIN_DECLS
@ -11,11 +12,13 @@ G_BEGIN_DECLS
* @FPI_PRINT_UNDEFINED: Undefined type, this happens prior to enrollment
* @FPI_PRINT_RAW: A raw print where the data is directly compared
* @FPI_PRINT_NBIS: NBIS minutiae comparison
* @FPI_PRINT_SIGFM: SIGFM minutiae comparison
*/
typedef enum {
FPI_PRINT_UNDEFINED = 0,
FPI_PRINT_RAW,
FPI_PRINT_NBIS,
FPI_PRINT_SIGFM,
} FpiPrintType;
/**
@ -44,11 +47,16 @@ gboolean fpi_print_add_from_image (FpPrint *print,
FpiMatchResult fpi_print_bz3_match (FpPrint *temp,
FpPrint *print,
gint bz3_threshold,
gint score_threshold,
GError **error);
#ifdef HAVE_SIGFM
FpiMatchResult fpi_print_sigfm_match (FpPrint * template, FpPrint * print,
gint score_threshold, GError * *error);
#endif
/* Helpers to encode metadata into user ID strings. */
gchar * fpi_print_generate_user_id (FpPrint *print);
gchar * fpi_print_generate_user_id (FpPrint * print);
gboolean fpi_print_fill_from_user_id (FpPrint *print,
const char *user_id);

View file

@ -149,6 +149,8 @@ driver_sources = {
[ 'drivers/goodixmoc/goodix.c', 'drivers/goodixmoc/goodix_proto.c' ],
'fpcmoc' :
[ 'drivers/fpcmoc/fpc.c' ],
'fpcmoh' :
[ 'drivers/fpcmoh/fpcmoh.c' ],
'realtek' :
[ 'drivers/realtek/realtek.c' ],
'focaltech_moc' :
@ -236,6 +238,13 @@ deps = [
mathlib_dep,
] + optional_deps
subdir('sigfm')
sigfm_link = []
if have_sigfm
sigfm_link = [libsigfm]
endif
# These are empty and only exist so that the include directories are created
# in the build tree. This silences a build time warning.
subdir('nbis/include')
@ -265,7 +274,7 @@ libfprint_private = static_library('fprint-private',
libfprint_private_sources,
],
dependencies: deps,
link_with: libnbis,
link_with: [libnbis] + sigfm_link,
install: false)
libfprint_drivers = static_library('fprint-drivers',
@ -309,6 +318,11 @@ libfprint_dep = declare_dependency(link_with: libfprint,
install_headers(['fprint.h'] + libfprint_public_headers,
subdir: versioned_libname
)
if have_sigfm
install_headers(['sigfm/sigfm.h'],
subdir: versioned_libname + '/sigfm'
)
endif
libfprint_private_dep = declare_dependency(
include_directories: include_directories('.'),

229
libfprint/sigfm/binary.hpp Normal file
View file

@ -0,0 +1,229 @@
// SIGFM algorithm for libfprint
// Copyright (C) 2022 Matthieu CHARETTE <matthieu.charette@gmail.com>
// Copyright (c) 2022 Natasha England-Elbro <natasha@natashaee.me>
// Copyright (c) 2022 Timur Mangliev <tigrmango@gmail.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later
//
#pragma once
#include "opencv2/core/mat.hpp"
#include <array>
#include <cstring>
#include <stdexcept>
#include <type_traits>
#include <vector>
namespace bin {
using byte = unsigned char;
class stream;
template<typename T, typename EnableIf = void>
struct serializer : public std::false_type {
void serialize(const T& m, stream& out);
};
template<typename T, typename EnableIf = void>
struct deserializer : public std::false_type {
T deserialize(stream& in);
};
class stream {
public:
stream() = default;
template<
typename Iter,
std::enable_if_t<std::is_same_v<typename std::iterator_traits<
std::decay_t<Iter>>::value_type,
byte>,
bool> = true>
stream(Iter begin, Iter end) : store_{begin, end}
{
}
template<typename T, std::enable_if_t<serializer<T>::value, bool> = true>
constexpr stream& operator<<(T v)
{
serializer<T>::serialize(v, *this);
return *this;
}
template<typename T, std::enable_if_t<deserializer<T>::value, bool> = true>
constexpr stream& operator>>(T& v)
{
v = deserializer<T>::deserialize(*this);
return *this;
}
template<
typename Iter,
std::enable_if_t<std::is_same_v<typename std::iterator_traits<
std::decay_t<Iter>>::value_type,
byte>,
bool> = true>
constexpr stream& write(Iter&& begin, Iter&& end)
{
std::copy(std::forward<Iter>(begin), std::forward<Iter>(end),
std::back_inserter(store_));
return *this;
}
template<typename T, std::enable_if_t<serializer<T>::value, bool> = true>
stream& serialize(const T& m, stream& out)
{
serializer<T>::serialize(m, out);
return out;
}
template<
typename Iter,
std::enable_if_t<std::is_same_v<typename std::iterator_traits<
std::decay_t<Iter>>::value_type,
byte>,
bool> = true>
constexpr stream& read(Iter&& begin, Iter&& end)
{
const auto dist = std::distance(begin, end);
return stream::read(begin, dist);
}
template<
typename Iter,
std::enable_if_t<std::is_same_v<typename std::iterator_traits<
std::decay_t<Iter>>::value_type,
byte>,
bool> = true>
constexpr stream& read(Iter&& begin, std::size_t dist)
{
if (dist > store_.size()) {
throw std::runtime_error{"trying to read too much from a stream. wanted: " + std::to_string(dist) + " available: " + std::to_string(store_.size())};
}
std::copy(store_.begin(), store_.begin() + dist, begin);
store_.erase(store_.begin(), store_.begin() + dist);
return *this;
}
byte* copy_buffer() const
{
byte* raw = static_cast<byte*>(malloc(store_.size()));
std::copy(store_.begin(), store_.end(), raw);
return raw;
}
std::size_t size() const { return store_.size(); }
private:
std::vector<byte> store_;
};
template<typename T>
struct serializer<T, std::enable_if_t<std::is_trivial_v<T>>> : public std::true_type {
static void serialize(T v, stream& out) {
using seg_store = std::array<byte, sizeof(T)>;
alignas(T) seg_store s = {};
std::memcpy(s.data(), &v, sizeof(T));
out.write(s.begin(), s.end());
}
};
template<typename T>
struct deserializer<T, std::enable_if_t<std::is_trivial_v<T>>> : public std::true_type {
static T deserialize(stream& in) {
alignas(T) std::array<byte, sizeof(T)> s = {};
in.read(s.begin(), s.size());
T v;
std::memcpy(&v, s.data(), s.size());
return v;
}
};
template<>
struct serializer<cv::Mat> : public std::true_type {
static void serialize(const cv::Mat& m, stream& out)
{
out << m.type() << m.rows << m.cols;
out.write(m.datastart, m.dataend);
}
};
template<>
struct deserializer<cv::Mat> : public std::true_type {
static cv::Mat deserialize(stream& in)
{
int rows, cols, type;
in >> type >> rows >> cols;
cv::Mat m;
m.create(rows, cols, type);
in.read(m.data, std::distance(m.datastart, m.dataend));
return m;
}
};
template<typename T>
struct deserializer<cv::Point_<T>> : public std::true_type {
static cv::Point2f deserialize(stream& in)
{
cv::Point_<T> p;
in >> p.x >> p.y;
return p;
}
};
template<typename T>
struct serializer<cv::Point_<T>> : public std::true_type {
static void serialize(const cv::Point_<T>& pt, stream& out)
{
out << pt.x << pt.y;
}
};
template<>
struct serializer<cv::KeyPoint> : public std::true_type {
static void serialize(const cv::KeyPoint& pt, stream& out)
{
out << pt.class_id << pt.angle << pt.octave << pt.response << pt.size
<< pt.pt;
}
};
template<>
struct deserializer<cv::KeyPoint> : public std::true_type {
static cv::KeyPoint deserialize(stream& in)
{
cv::KeyPoint pt;
in >> pt.class_id >> pt.angle >> pt.octave >> pt.response >> pt.size >>
pt.pt;
return pt;
}
};
template<typename T>
struct serializer<std::vector<T>, std::enable_if_t<serializer<T>::value>> : public std::true_type {
static void serialize(const std::vector<T>& vs, stream& out)
{
out << static_cast<std::size_t>(vs.size());
std::for_each(vs.begin(), vs.end(),
[&out](const auto& el) { out << el; });
}
};
template<typename T>
struct deserializer<std::vector<T>, std::enable_if_t<deserializer<T>::value>> : public std::true_type {
static std::vector<T> deserialize(stream& in)
{
std::size_t size;
in >> size;
std::vector<T> vs;
vs.reserve(size);
for (std::size_t n = 0; n != size; ++n) {
T v;
in >> v;
vs.emplace_back(std::move(v));
}
return vs;
}
};
} // namespace bin

View file

@ -0,0 +1,18 @@
// SIGFM algorithm for libfprint
// Copyright (C) 2022 Matthieu CHARETTE <matthieu.charette@gmail.com>
// Copyright (c) 2022 Natasha England-Elbro <natasha@natashaee.me>
// Copyright (c) 2022 Timur Mangliev <tigrmango@gmail.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later
//
#pragma once
#include <opencv2/core.hpp>
#include <vector>
struct SigfmImgInfo {
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
};

View file

@ -0,0 +1,20 @@
sigfm_sources = ['sigfm.cpp']
opencv = dependency('opencv4', required: false)
have_sigfm = opencv.found()
if have_sigfm
libsigfm = static_library('sigfm',
sigfm_sources,
dependencies: [opencv],
cpp_args: cpp.get_supported_arguments([
'-Wno-suggest-attribute=format',
]),
)
doctest = dependency('doctest', required: false)
if doctest.found()
sigfm_tests = executable('sigfm-tests', ['./tests.cpp'], dependencies: [doctest, opencv], link_with: [libsigfm])
endif
endif

208
libfprint/sigfm/sigfm.cpp Normal file
View file

@ -0,0 +1,208 @@
// SIGFM algorithm for libfprint
// Copyright (C) 2022 Matthieu CHARETTE <matthieu.charette@gmail.com>
// Copyright (c) 2022 Natasha England-Elbro <natasha@natashaee.me>
// Copyright (c) 2022 Timur Mangliev <tigrmango@gmail.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later
//
#include "sigfm.h"
#include "binary.hpp"
#include "img-info.hpp"
#include "opencv2/core/persistence.hpp"
#include "opencv2/core/types.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/imgcodecs.hpp"
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <sstream>
#include <string>
#include <opencv2/opencv.hpp>
#include <vector>
namespace bin {
template<>
struct serializer<SigfmImgInfo> : public std::true_type {
static void serialize(const SigfmImgInfo& info, stream& out)
{
out << info.keypoints << info.descriptors;
}
};
template<>
struct deserializer<SigfmImgInfo> : public std::true_type {
static SigfmImgInfo deserialize(stream& in)
{
SigfmImgInfo info;
in >> info.keypoints >> info.descriptors;
return info;
}
};
} // namespace bin
namespace {
constexpr auto distance_match = 0.75;
constexpr auto length_match = 0.05;
constexpr auto angle_match = 0.05;
constexpr auto min_match = 5;
struct match {
cv::Point2i p1;
cv::Point2i p2;
match(cv::Point2i ip1, cv::Point2i ip2) : p1{ip1}, p2{ip2} {}
match() : p1{cv::Point2i(0, 0)}, p2{cv::Point2i(0, 0)} {}
bool operator==(const match& right) const
{
return std::tie(this->p1, this->p2) == std::tie(right.p1, right.p2);
}
bool operator<(const match& right) const
{
return (this->p1.y < right.p1.y) ||
((this->p1.y < right.p1.y) && this->p1.x < right.p1.x);
}
};
struct angle {
double cos;
double sin;
match corr_matches[2];
angle(double cos_, double sin_, match m1, match m2)
: cos{cos_}, sin{sin_}, corr_matches{m1, m2}
{
}
};
} // namespace
SigfmImgInfo* sigfm_copy_info(SigfmImgInfo* info) { return new SigfmImgInfo{*info}; }
int sigfm_keypoints_count(SigfmImgInfo* info) { return info->keypoints.size(); }
unsigned char* sigfm_serialize_binary(SigfmImgInfo* info, int* outlen)
{
bin::stream s;
s << *info;
*outlen = s.size();
return s.copy_buffer();
}
SigfmImgInfo* sigfm_deserialize_binary(const unsigned char* bytes, int len)
{
try {
bin::stream s{bytes, bytes + len};
auto info = std::make_unique<SigfmImgInfo>();
s >> *info;
return info.release();
}
catch (const std::exception&) {
return nullptr;
}
}
SigfmImgInfo* sigfm_extract(const SigfmPix* pix, int width, int height)
{
try {
cv::Mat img;
img.create(height, width, CV_8UC1);
std::memcpy(img.data, pix, width * height);
const auto roi = cv::Mat::ones(cv::Size{img.size[1], img.size[0]}, CV_8UC1);
std::vector<cv::KeyPoint> pts;
cv::Mat descs;
cv::SIFT::create()->detectAndCompute(img, roi, pts, descs);
auto* info = new SigfmImgInfo{pts, descs};
return info;
} catch(...) {
return nullptr;
}
}
int sigfm_match_score(SigfmImgInfo* frame, SigfmImgInfo* enrolled)
{
try {
std::vector<std::vector<cv::DMatch>> points;
auto bfm = cv::BFMatcher::create();
bfm->knnMatch(frame->descriptors, enrolled->descriptors, points, 2);
std::set<match> matches_unique;
int nb_matched = 0;
for (const auto& pts : points) {
if (pts.size() < 2) {
continue;
}
const cv::DMatch& match_1 = pts.at(0);
if (match_1.distance < distance_match * pts.at(1).distance) {
matches_unique.emplace(
match{frame->keypoints.at(match_1.queryIdx).pt,
enrolled->keypoints.at(match_1.trainIdx).pt});
nb_matched++;
}
}
if (nb_matched < min_match) {
return 0;
}
std::vector<match> matches{matches_unique.begin(),
matches_unique.end()};
std::vector<angle> angles;
for (std::size_t j = 0; j < matches.size(); j++) {
match match_1 = matches[j];
for (std::size_t k = j + 1; k < matches.size(); k++) {
match match_2 = matches[k];
int vec_1[2] = {match_1.p1.x - match_2.p1.x,
match_1.p1.y - match_2.p1.y};
int vec_2[2] = {match_1.p2.x - match_2.p2.x,
match_1.p2.y - match_2.p2.y};
double length_1 = sqrt(pow(vec_1[0], 2) + pow(vec_1[1], 2));
double length_2 = sqrt(pow(vec_2[0], 2) + pow(vec_2[1], 2));
if (1 - std::min(length_1, length_2) /
std::max(length_1, length_2) <=
length_match) {
double product = length_1 * length_2;
angles.emplace_back(angle(
M_PI / 2 +
asin((vec_1[0] * vec_2[0] + vec_1[1] * vec_2[1]) /
product),
acos((vec_1[0] * vec_2[1] - vec_1[1] * vec_2[0]) /
product),
match_1, match_2));
}
}
}
if (angles.size() < min_match) {
return 0;
}
int count = 0;
for (std::size_t j = 0; j < angles.size(); j++) {
angle angle_1 = angles[j];
for (std::size_t k = j + 1; k < angles.size(); k++) {
angle angle_2 = angles[k];
if (1 - std::min(angle_1.sin, angle_2.sin) /
std::max(angle_1.sin, angle_2.sin) <=
angle_match &&
1 - std::min(angle_1.cos, angle_2.cos) /
std::max(angle_1.cos, angle_2.cos) <=
angle_match) {
count += 1;
}
}
}
return count;
}
catch (...) {
return -1;
}
}
void sigfm_free_info(SigfmImgInfo* info) { delete info; }

91
libfprint/sigfm/sigfm.h Normal file
View file

@ -0,0 +1,91 @@
// SIGFM algorithm for libfprint
// Copyright (C) 2022 Matthieu CHARETTE <matthieu.charette@gmail.com>
// Copyright (c) 2022 Natasha England-Elbro <natasha@natashaee.me>
// Copyright (c) 2022 Timur Mangliev <tigrmango@gmail.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later
//
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef unsigned char SigfmPix;
/**
* @brief Contains information used by the sigfm algorithm for matching
* @details Get one from sigfm_extract() and make sure to clean it up with sigfm_free_info()
* @struct SigfmImgInfo
*/
typedef struct SigfmImgInfo SigfmImgInfo;
/**
* @brief Extracts information from an image for later use sigfm_match_score
*
* @param pix Pixels of the image must be width * height in length
* @param width Width of the image
* @param height Height of the image
* @return SigfmImgInfo* Info that can be used with the API
*/
SigfmImgInfo * sigfm_extract (const SigfmPix * pix,
int width,
int height);
/**
* @brief Destroy an SigfmImgInfo
* @warning Call this instead of free() or you will get UB!
* @param info SigfmImgInfo to destroy
*/
void sigfm_free_info (SigfmImgInfo * info);
/**
* @brief Score how closely a frame matches another
*
* @param frame Print to be checked
* @param enrolled Canonical print to verify against
* @return int Score of how closely they match, values <0 indicate error, 0 means always reject
*/
int sigfm_match_score (SigfmImgInfo * frame,
SigfmImgInfo * enrolled);
/**
* @brief Serialize an image info for storage
*
* @param info SigfmImgInfo to store
* @param outlen output: Length of the returned byte array
* @return unsigned* char byte array for storage, should be free'd by the callee
*/
unsigned char * sigfm_serialize_binary (SigfmImgInfo * info,
int * outlen);
/**
* @brief Deserialize an SigfmImgInfo from storage
*
* @param bytes Byte array to deserialize from
* @param len Length of the byte array
* @return SigfmImgInfo* Deserialized info, or NULL if deserialization failed
*/
SigfmImgInfo * sigfm_deserialize_binary (const unsigned char * bytes,
int len);
/**
* @brief Keypoints for an image. Low keypoints generally means the image is
* low quality for matching
*
* @param info
* @return int
*/
int sigfm_keypoints_count (SigfmImgInfo * info);
/**
* @brief Copy an SigfmImgInfo
*
* @param info Source of copy
* @return SigfmImgInfo* Newly allocated and copied version of info
*/
SigfmImgInfo * sigfm_copy_info (SigfmImgInfo * info);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load diff

160
libfprint/sigfm/tests.cpp Normal file
View file

@ -0,0 +1,160 @@
// SIGFM algorithm for libfprint
// Copyright (C) 2022 Matthieu CHARETTE <matthieu.charette@gmail.com>
// Copyright (c) 2022 Natasha England-Elbro <natasha@natashaee.me>
// Copyright (c) 2022 Timur Mangliev <tigrmango@gmail.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later
//
#include "opencv2/core.hpp"
#include "opencv2/core/types.hpp"
#include "sigfm.h"
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
#include "binary.hpp"
#include "tests-embedded.hpp"
#include "img-info.hpp"
#include <opencv2/opencv.hpp>
#include<vector>
namespace cv {
bool operator==(const cv::KeyPoint& lhs, const cv::KeyPoint& rhs)
{
return lhs.angle == rhs.angle && lhs.class_id == rhs.class_id &&
lhs.octave == rhs.octave && lhs.size == rhs.size &&
lhs.response == rhs.response && lhs.pt == rhs.pt;
}
} // namespace cv
namespace {
bool comp_mats(const cv::Mat& lhs, const cv::Mat& rhs)
{
return std::equal(lhs.datastart, lhs.dataend, rhs.datastart, rhs.dataend);
}
std::string to_str(const cv::KeyPoint& k)
{
std::stringstream s;
s << "angle: " << k.angle << ", class_id: " << k.class_id
<< ", octave: " << k.octave << ", size: " << k.size
<< ", reponse: " << k.response << ", ptx: " << k.pt.x
<< ", pty: " << k.pt.y;
return s.str();
}
} // namespace
template<typename T>
void check_vec(const std::vector<T>& vs)
{
for (auto i : vs) {
bin::stream s;
s << i;
T iv;
s >> iv;
CHECK(i == iv);
}
}
TEST_SUITE("binary")
{
TEST_CASE("float can be stored and restored")
{
check_vec<float>({3, 2.4, 6.7});
}
TEST_CASE("size_t can be stored and restored")
{
check_vec<std::size_t>({2, 5, 803, 900});
}
TEST_CASE("number can be stored and restored")
{
check_vec<int>({5, 3, 10, 16, 24, 900});
}
TEST_CASE("image can be stored and restored")
{
cv::Mat input;
input.create(256, 256, CV_8UC1);
std::memcpy(input.data, embedded::capture_aes3500, 256 * 256);
bin::stream s;
s << input;
cv::Mat output;
s >> output;
CHECK(std::equal(input.datastart, input.dataend, output.datastart,
output.dataend));
}
TEST_CASE("taking more than giving to a stream will cause an exception") {
bin::stream s;
s << 5;
int v1;
s >> v1;
CHECK_THROWS(s >> v1);
}
TEST_CASE("vector of values can be stored and restored")
{
std::vector inputs = {3, 5, 1, 7};
bin::stream s;
s << inputs;
std::vector<int> outputs;
s >> outputs;
CHECK(outputs == inputs);
}
TEST_CASE("keypoints can be stored and restored")
{
cv::KeyPoint pt;
pt.angle = 20;
pt.octave = 3;
pt.response = 3;
pt.size = 40;
pt.pt = cv::Point2f{3, 1};
bin::stream s;
s << pt;
cv::KeyPoint ptout;
s >> ptout;
CHECK(to_str(pt) == to_str(ptout));
}
TEST_CASE("sigfm img info can be stored and restored")
{
constexpr auto img_w = 256;
constexpr auto img_h = 256;
constexpr auto img = embedded::capture_aes3500;
SigfmImgInfo* info = sigfm_extract(img, img_w, img_h);
REQUIRE(info != nullptr);
const auto inf1desc = info->descriptors;
cv::Mat descout;
bin::stream s;
s << inf1desc;
s >> descout;
CHECK(comp_mats(inf1desc, descout));
int slen;
const auto bin_data = sigfm_serialize_binary(info, &slen);
int slen2;
SigfmImgInfo* info2 = sigfm_deserialize_binary(bin_data, slen);
REQUIRE(info2);
const auto bin_data2 = sigfm_serialize_binary(info2, &slen2);
CHECK(slen == slen2);
CHECK(std::equal(bin_data, bin_data + slen, bin_data2,
bin_data2 + slen2));
REQUIRE(info->keypoints == info2->keypoints);
REQUIRE(std::equal(
info->descriptors.datastart, info->descriptors.dataend,
info2->descriptors.datastart, info2->descriptors.dataend));
sigfm_free_info(info);
sigfm_free_info(info2);
free(bin_data);
free(bin_data2);
}
}

View file

@ -154,6 +154,17 @@ if have_spi
default_drivers += spi_drivers
endif
# SIGFM-based drivers require OpenCV4 for SIFT keypoint matching
sigfm_drivers = [
'fpcmoh',
]
have_opencv = dependency('opencv4', required: false).found()
if have_opencv
default_drivers += sigfm_drivers
libfprint_conf.set10('HAVE_SIGFM', true)
endif
# FIXME: All the drivers should be fixed by adjusting the byte order.
# See https://gitlab.freedesktop.org/libfprint/libfprint/-/issues/236
endian_independent_drivers = virtual_drivers + [
@ -201,6 +212,17 @@ if enabled_spi_drivers.length() > 0 and not have_spi
error('SPI drivers @0@ are not supported'.format(enabled_spi_drivers))
endif
enabled_sigfm_drivers = []
foreach driver : sigfm_drivers
if driver in drivers
enabled_sigfm_drivers += driver
endif
endforeach
if enabled_sigfm_drivers.length() > 0 and not have_opencv
error('SIGFM drivers @0@ require opencv4'.format(enabled_sigfm_drivers))
endif
driver_helper_mapping = {
'aes1610' : [ 'aeslib' ],
'aes1660' : [ 'aeslib', 'aesx660' ],
@ -210,6 +232,7 @@ driver_helper_mapping = {
'aes3500' : [ 'aeslib', 'aes3k' ],
'aes4000' : [ 'aeslib', 'aes3k' ],
'uru4000' : [ 'openssl' ],
'fpcmoh' : [ 'openssl' ],
'elanspi' : [ 'udev' ],
'virtual_image' : [ 'virtual' ],
'virtual_device' : [ 'virtual' ],