have a working edition of the fingerprint visuals

This commit is contained in:
Michael Zanetti 2018-10-03 00:45:10 +02:00
parent 798b6879fd
commit b39fa30e15
9 changed files with 364 additions and 36 deletions

View File

@ -257,5 +257,11 @@
<file>ui/devicepages/FingerprintReaderDevicePage.qml</file>
<file>ui/images/account.svg</file>
<file>ui/images/contact-new.svg</file>
<file>ui/components/SegmentedImage.qml</file>
<file>ui/components/SegmentRenderer.qml</file>
<file>ui/components/SegmentBoundingBoxes.qml</file>
<file>ui/components/FingerprintVisual.qml</file>
<file>ui/images/fingerprint/fingerprint_boxes.json</file>
<file>ui/images/fingerprint/fingerprint_segmented.png</file>
</qresource>
</RCC>

View File

@ -0,0 +1,54 @@
import QtQuick 2.9
SegmentedImage {
id: segmentedImage
property var masks: []
property bool debug: false
// http://stackoverflow.com/a/1830844/538866
function isNumeric (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
function getMasksToEnroll () {
var outMasks = [];
if (masks && masks.length) {
masks.forEach(function (mask, i) {
// Format is "<source>/[x1,y1,w1,h1],,[xn,yn,wn,hn]"
// If any value is non-numeric, we drop the mask.
if (!isNumeric(mask.x) || !isNumeric(mask.y) || !isNumeric(mask.width)
|| !isNumeric(mask.height))
return;
// Translate the box so as to mirror the mask
mask.x = (1 - (mask.x + mask.width));
outMasks.push(mask);
});
}
return outMasks;
}
onMasksChanged: segmentedImage.enrollMasks(getMasksToEnroll())
textureSource: "../images/fingerprint/fingerprint_segmented.png"
boxesSource: "../images/fingerprint/fingerprint_boxes.json"
Repeater {
model: segmentedImage.masks
Rectangle {
visible: segmentedImage.debug
color: "red"
opacity: 0.25
x: modelData.x * segmentedImage.implicitWidth
y: modelData.y * segmentedImage.implicitHeight
width: modelData.width * segmentedImage.implicitWidth
height: modelData.height * segmentedImage.implicitHeight
Component.onCompleted: console.log('Scanner mask (x, y, w, h):', x, y, width, height)
}
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2016 Canonical Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Authored by Florian Boucault <florian.boucault@canonical.com>
*/
import QtQuick 2.4
QtObject {
id: segmentBoundingBoxes
property string source
onSourceChanged: parseBoundingBoxes(source)
property var boundingBoxes: []
property real width
property real height
property int count: boundingBoxes.length
// The API cannot be used reliably before this signal has been emitted.
signal ready()
function parseBoundingBoxes(source) {
var xhr = new XMLHttpRequest;
xhr.open("GET", source);
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
var b = [];
var json = JSON.parse(xhr.responseText);
boundingBoxes = json["boxes"];
width = json["width"];
height = json["height"];
ready();
}
}
xhr.send();
}
function intersects(box1, box2) {
// TODO: optimize
var x11 = box1[0];
var y11 = box1[1];
var x12 = box1[0] + box1[2];
var y12 = box1[1] + box1[3];
var x21 = box2[0];
var y21 = box2[1];
var x22 = box2[0] + box2[2];
var y22 = box2[1] + box2[3];
var x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
var y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
return (x_overlap / Math.min(box1[2], box2[2]) > 0.25
&& y_overlap / Math.min(box1[3], box2[3]) > 0.25);
}
function computeIntersections(hitBox) {
var absoluteHitBox = [hitBox[0] * width, hitBox[1] * height,
hitBox[2] * width, hitBox[3] * height];
var intersections = [];
for (var i in boundingBoxes) {
var boundingBox = boundingBoxes[i];
if (intersects(absoluteHitBox, boundingBox)) {
intersections.push(i);
}
}
return intersections;
}
}

View File

@ -0,0 +1,76 @@
import QtQuick 2.9
ShaderEffect {
id: segmentRenderer
implicitWidth: texture.width
implicitHeight: texture.height
function animate(segments) {
for (var i in segments) {
var progressPixel = progressTexture.children[segments[i]];
if (progressPixel.progress == 0.0) {
progressPixel.animation.start();
}
}
}
property string source
property int segmentsCount
property color backgroundColor: app.foregroundColor
property color fillColor: app.accentColor
property Image texture: Image {
source: segmentRenderer.source
}
property var progressTexture: progressTexture
property int progressTextureSize: progressTexture.size
fragmentShader: "
varying mediump vec2 qt_TexCoord0;
uniform lowp float qt_Opacity;
uniform lowp vec4 backgroundColor;
uniform lowp vec4 fillColor;
uniform lowp sampler2D texture;
uniform lowp sampler2D progressTexture;
uniform lowp int progressTextureSize;
void main() {
lowp vec4 p = texture2D(texture, qt_TexCoord0);
lowp float segment = p.r * 255.0;
lowp vec4 segmentProgress = step(0.9, segment) * texture2D(progressTexture, vec2((segment - 1.0 + 0.5) / float(progressTextureSize), 0.5));
lowp vec4 color = mix(fillColor, backgroundColor, step(segmentProgress.r, p.g));
gl_FragColor = vec4(color.rgb, 1.0) * p.b * qt_Opacity;
}
"
// TODO: not the most efficient; could be replaced with an image provider
Row {
id: progressTexture
property int size: 128
layer.enabled: true
layer.sourceRect: Qt.rect(0, 0, size, 1)
layer.textureSize: Qt.size(size, 1)
layer.wrapMode: ShaderEffectSource.ClampToEdge
visible: false
Repeater {
model: segmentRenderer.segmentsCount
Rectangle {
id: progressPixel
width: 1
height: 1
color: Qt.rgba(progress, progress, progress, 1.0)
property real progress
property NumberAnimation animation: NumberAnimation {
target: progressPixel
property: "progress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.InOutQuad
}
}
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2016 Canonical Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Authored by Florian Boucault <florian.boucault@canonical.com>
*/
import QtQuick 2.4
Item {
id: segImg
property alias textureSource: segmentRenderer.source
property alias boxesSource: segmentBoundingBoxes.source
implicitWidth: segmentRenderer.implicitWidth
implicitHeight: segmentRenderer.implicitHeight
// Ready to enroll.
signal ready()
function enrollMasks(masks) {
if (masks && masks.length) {
var segments = [];
masks.forEach(function (mask, i) {
var hitBox = [mask.x, mask.y, mask.width, mask.height];
segments = segments.concat(segmentBoundingBoxes.computeIntersections(hitBox));
});
segmentRenderer.animate(segments);
}
}
SegmentRenderer {
id: segmentRenderer
segmentsCount: segmentBoundingBoxes.count
}
SegmentBoundingBoxes {
id: segmentBoundingBoxes
onReady: segImg.ready()
}
}

View File

@ -15,6 +15,8 @@ Item {
property alias delegate: listView.delegate
property bool autoscroll: true
ColumnLayout {
anchors.fill: parent
@ -40,7 +42,11 @@ Item {
Layout.fillHeight: true
model: logsModel
clip: true
// onCountChanged: positionViewAtEnd()
onCountChanged: {
if (root.autoscroll) {
positionViewAtEnd()
}
}
onContentYChanged: {
if (!logsModel.busy && contentY - originY < 5 * height) {

View File

@ -17,39 +17,9 @@ DevicePageBase {
ColumnLayout {
anchors.fill: parent
// Item {
// Layout.fillWidth: true
// Layout.preferredHeight: root.inputVisible ? inputColumn.implicitHeight : 0
// Behavior on Layout.preferredHeight { NumberAnimation { duration: 130; easing.type: Easing.InOutQuad } }
// ColumnLayout {
// id: inputColumn
// anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
// TextField {
// id: titleTextField
// Layout.fillWidth: true
// Layout.topMargin: app.margins
// Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
// placeholderText: qsTr("Title")
// }
// TextArea {
// id: bodyTextField
// Layout.fillWidth: true
// Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
// placeholderText: qsTr("Text")
// wrapMode: Text.WordWrap
// }
// }
// }
Button {
Layout.fillWidth: true
Layout.margins: app.margins
Layout.leftMargin: app.margins; Layout.topMargin: app.margins; Layout.rightMargin: app.margins
text: qsTr("Manage access")
onClicked: {
pageStack.push(manageUsersComponent)
@ -69,12 +39,12 @@ DevicePageBase {
live: true
Component.onCompleted: update()
typeIds: [root.accessGrantedEventType.id, root.accessDeniedEventType.id];
}
delegate: MeaListItemDelegate {
width: parent.width
iconName: "../images/notification.svg"
iconName: model.typeId === root.accessGrantedEventType.id ? "../images/tick.svg" : "../images/dialog-error-symbolic.svg"
iconColor: model.typeId === root.accessGrantedEventType.id ? "green" : "red"
text: model.typeId === root.accessGrantedEventType.id ? qsTr("Access granted for user %1").arg(model.value) : qsTr("Access denied")
subText: Qt.formatDateTime(model.timestamp)
progressive: false
@ -134,6 +104,28 @@ DevicePageBase {
engine.deviceManager.executeAction(root.device.id, actionType.id, params)
}
}
ColumnLayout {
anchors.centerIn: parent
width: 200
spacing: app.margins * 2
visible: parent.count === 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: width
FingerprintVisual {
id: fingerprintVisual
anchors.centerIn: parent
scale: parent.height / implicitHeight
}
}
Button {
text: qsTr("Add a fingerprint")
onClicked: pageStack.push(addUserComponent)
Layout.fillWidth: true
}
}
}
}
}
@ -153,8 +145,8 @@ DevicePageBase {
Connections {
target: engine.deviceManager
onExecuteActionReply: {
addUserPage.error = params["deviceError"] !== DeviceManager.DeviceErrorNoError
print("Execute action reply:", params);
addUserPage.error = params["deviceError"] !== "DeviceErrorNoError"
print("Execute action reply:", params["deviceError"]);
addUserSwipeView.currentIndex++
}
}
@ -164,6 +156,7 @@ DevicePageBase {
SwipeView {
id: addUserSwipeView
Layout.fillWidth: true
Layout.topMargin: app.margins * 2
Layout.preferredHeight: 200
Layout.alignment: Qt.AlignTop
Item {
@ -182,6 +175,27 @@ DevicePageBase {
id: userIdTextField
Layout.fillWidth: true
}
Label {
Layout.fillWidth: true
text: qsTr("Finger")
}
ComboBox {
id: fingerComboBox
Layout.fillWidth: true
model: ListModel {
ListElement { modelData: qsTr("Left thumb"); enumValue: "ThumbLeft" }
ListElement { modelData: qsTr("Left index finger"); enumValue: "IndexFingerLeft" }
ListElement { modelData: qsTr("Left middle finger"); enumValue: "MiddleFingerLeft" }
ListElement { modelData: qsTr("Left ring finger"); enumValue: "RingFingerLeft" }
ListElement { modelData: qsTr("Left pinky finger"); enumValue: "PinkyLeft" }
ListElement { modelData: qsTr("Right thumb"); enumValue: "ThumbRight" }
ListElement { modelData: qsTr("Right index finger"); enumValue: "IndexFingerRight" }
ListElement { modelData: qsTr("Right middle finger"); enumValue: "MiddleFingerRight" }
ListElement { modelData: qsTr("Right ring finger"); enumValue: "RingFingerRight" }
ListElement { modelData: qsTr("Right pinky finger"); enumValue: "PinkyRight" }
}
}
Button {
text: qsTr("Add user")
Layout.fillWidth: true
@ -192,6 +206,10 @@ DevicePageBase {
titleParam["paramTypeId"] = actionType.paramTypes.findByName("userId").id
titleParam["value"] = userIdTextField.displayText
params.push(titleParam)
var fingerParam = {}
fingerParam["paramTypeId"] = actionType.paramTypes.findByName("finger").id
fingerParam["value"] = fingerComboBox.model.get(fingerComboBox.currentIndex).enumValue
params.push(fingerParam)
engine.deviceManager.executeAction(root.device.id, actionType.id, params)
addUserSwipeView.currentIndex++
}
@ -206,9 +224,43 @@ DevicePageBase {
ColumnLayout {
anchors.fill: parent
anchors.margins: app.margins
spacing: app.margins * 2
Label {
text: qsTr("Please scan the fingerprint now")
Layout.fillWidth: true
font.pixelSize: app.largeFont
color: app.accentColor
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.preferredWidth: 200
Layout.preferredHeight: 200
Layout.alignment: Qt.AlignCenter
FingerprintVisual {
id: fingerprintVisual
scale: parent.height / implicitHeight
anchors.centerIn: parent
Timer {
interval: 500
property real position: 0
running: addUserSwipeView.currentIndex == 1
repeat: true
onTriggered: {
var masks = [];
masks.push({x: 0, y: 0, width: 1, height: position})
position += 0.1
if (position < 1.1) {
fingerprintVisual.masks = masks
} else {
position = 0
fingerprintVisual.masks = []
}
}
}
}
}
}
}

View File

@ -0,0 +1 @@
{"width": 451, "boxes": [[291, 515, 104, 50], [269, 510, 96, 34], [227, 506, 62, 16], [198, 505, 72, 60], [144, 498, 46, 67], [200, 467, 133, 32], [214, 449, 87, 28], [67, 432, 25, 104], [396, 420, 31, 89], [177, 395, 22, 103], [133, 390, 15, 96], [266, 375, 8, 31], [1, 356, 7, 42], [336, 355, 27, 114], [221, 350, 95, 105], [243, 323, 17, 106], [155, 317, 68, 248], [110, 317, 43, 243], [22, 314, 12, 155], [199, 310, 13, 127], [302, 296, 83, 223], [276, 295, 20, 136], [311, 275, 30, 189], [177, 266, 8, 114], [88, 263, 34, 287], [221, 249, 9, 86], [441, 238, 9, 169], [110, 231, 9, 70], [44, 226, 16, 280], [310, 225, 8, 181], [199, 223, 53, 86], [370, 217, 37, 288], [249, 208, 25, 153], [177, 201, 61, 49], [281, 195, 15, 85], [133, 183, 16, 196], [177, 178, 96, 29], [155, 167, 35, 134], [345, 166, 18, 173], [200, 156, 81, 31], [1, 149, 17, 192], [386, 145, 18, 60], [305, 142, 36, 118], [66, 139, 26, 277], [149, 134, 168, 77], [111, 132, 45, 85], [88, 130, 36, 120], [350, 118, 35, 162], [165, 111, 134, 28], [262, 92, 85, 64], [414, 91, 35, 133], [129, 89, 119, 36], [22, 88, 42, 211], [92, 67, 256, 62], [47, 64, 75, 149], [349, 47, 80, 360], [132, 44, 257, 90], [13, 41, 56, 95], [66, 22, 273, 60], [198, 1, 215, 76], [74, 1, 109, 39]], "height": 566}

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB