Added a few DB sanity tests for BLOB values between JS and Python

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2023-01-16 17:06:55 -08:00
parent 743d7cc5ea
commit 1ed1b1d1ea
7 changed files with 346 additions and 81 deletions

View File

@ -21,12 +21,60 @@ div {
.Identities {
display: flex;
flex-grow: 1;
overflow-y: scroll;
flex-direction: column;
border: 1px solid green;
}
.Identity {
display: flex;
flex-direction: column;
position: relative;
margin: 0.125rem;
border: 1px solid transparent;
}
.Identity:hover {
border: 1px solid yellow;
cursor: pointer;
}
.Identity .Title {
position: absolute;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
padding: 0.125rem;
font-size: 0.6rem;
color: white;
}
.Identity .Face {
width: 8rem;
height: 8rem;
background-size: contain !important;
background-repeat: no-repeat no-repeat !important;;
background-position: 50% 50% !important;;
}
.Cluster {
display: flex;
flex-direction: column;
overflow-y: scroll;
flex-grow: 1;
border: 1px solid red;
padding: 0.5rem;
}
.Cluster .Info {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.Cluster .Faces {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
}

View File

@ -3,10 +3,68 @@ import React, { useState, useMemo, useEffect } from 'react';
import { useApi } from './useApi';
import './App.css';
const Cluster = () => {
return (
<div className='Cluster'>cluster</div>
type ClusterProps = {
id: number
};
const Cluster = ({ id }: ClusterProps) => {
const [identity, setIdentity] = useState<Identity | undefined>(undefined);
const { loading, data } = useApi(
`../api/v1/identities/${id}`
);
useEffect(() => {
if (data) {
if (Array.isArray(data) && data.length > 0) {
setIdentity(data[0] as Identity);
} else {
setIdentity(data as Identity);
}
}
}, [data]);
const relatedFacesJSX = useMemo(() => {
if (identity === undefined) {
return <></>;
}
return identity.relatedFaces.map((face) => {
const idPath = String(face.faceId % 100).padStart(2, '0');
return (
<div key={face.faceId}
className='Identity'>
<div className='Title'>
{face.distance}
</div>
<div className='Face'
style={{
background: `url("/faces/${idPath}/${face.faceId}.jpg")`,
}} />
</div>
);
});
}, [identity]);
return (
<div className='Cluster'>
{ loading && `Loading ${id}`}
{ identity !== undefined && <div className="Info">
<div>{identity.lastName}</div>
<div>{identity.firstName}</div>
<div>{identity.middleName}</div>
<div>{identity.displayName}</div>
<div>Faces: {identity.relatedFaces.length}</div>
</div> }
{ identity !== undefined && <div className="Faces">
{ relatedFacesJSX }
</div> }
</div>
);
};
type Face = {
distance: number,
faceId: number,
photoId: number
};
type Identity = {
@ -16,22 +74,41 @@ type Identity = {
descriptors: number[],
id: number
displayName: string,
relatedFaces: Face[]
};
interface IdentitiesProps {
setIdentity?(id: number): void,
identities: Identity[]
};
const Identities = ({ identities } : IdentitiesProps) => {
const identitiesJSX = useMemo(() =>
identities.map((identity) => {
const idPath = String(identity.id % 100).padStart(2, '0');
return (<img
key={identity.id}
alt={identity.id.toString()}
src={`/faces/${idPath}/${identity.id}.jpg`}/>);
}
), [ identities ]);
const Identities = ({ identities, setIdentity } : IdentitiesProps) => {
const identitiesJSX = useMemo(() => {
const loadIdentity = (id: number): void => {
if (setIdentity) {
setIdentity(id)
}
};
return identities.map((identity) => {
const face = identity.relatedFaces[0];
const idPath = String(face.faceId % 100).padStart(2, '0');
return (
<div key={face.faceId}
onClick={() => loadIdentity(identity.id)}
className='Identity'>
<div className='Title'>
{identity.displayName}
</div>
<div className='Face'
style={{
background: `url("/faces/${idPath}/${face.faceId}.jpg")`,
}}/>
</div>
);
});
}, [ setIdentity, identities ]);
return (
<div className='Identities'>
@ -42,8 +119,9 @@ const Identities = ({ identities } : IdentitiesProps) => {
const App = () => {
const [identities, setIdentities] = useState<Identity[]>([]);
const [identity, setIdentity] = useState<number>(0);
const { loading, data } = useApi(
'../api/v1/faces'
'../api/v1/identities'
);
useEffect(() => {
@ -56,9 +134,12 @@ const App = () => {
<div className="App">
<div className="Worksheet">
{ loading && <div>Loading...</div> }
{ !loading && identity !== 0 && <Cluster id={identity} />}
{ !loading && identity === 0 && <div className="Cluster">
Select identity to edit
</div> }
{ !loading && <>
<Cluster/>
<Identities identities={identities}/>
<Identities {... {identities, setIdentity }}/>
</> }
</div>
</div>

View File

@ -8,12 +8,17 @@ type UseApi = {
};
const useApi = (_url: string, _options?: {}) : UseApi => {
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [data, setData] = useState(undefined);
const [error, setError] = useState<any>(undefined);
useEffect(() => {
if (_url === '' || loading) {
return;
}
const fetchApi = async () => {
console.log(`Fetching ${_url}...`);
setLoading(true);
try {
const res = await window.fetch(_url, _options);
const data = await res.json();
@ -27,7 +32,7 @@ const useApi = (_url: string, _options?: {}) : UseApi => {
};
fetchApi();
}, [_url, _options]);
}, [_url, _options, loading]);
return { loading, data, error };
};

55
ketrface/db-test.py Normal file
View File

@ -0,0 +1,55 @@
import functools
from ketrface.util import *
from ketrface.dbscan import *
from ketrface.db import *
from ketrface.config import *
config = read_config()
html_path = merge_config_path(config['path'], 'frontend')
pictures_path = merge_config_path(config['path'], config['picturesPath'])
faces_path = merge_config_path(config['path'], config['facesPath'])
db_path = merge_config_path(config['path'], config["db"]["photos"]["host"])
html_base = config['basePath']
if html_base == "/":
html_base = "."
print(f'Connecting to database: {db_path}')
conn = create_connection(db_path)
with conn:
cur = conn.cursor()
res = cur.execute('''
SELECT identities.descriptors,
GROUP_CONCAT(faces.id) AS relatedFaceIds,
GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds,
GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds
FROM identities
INNER JOIN faces ON identities.id=faces.identityId
WHERE identities.id=7
GROUP BY identities.id
''')
for identity in res.fetchall():
relatedFaceDescriptorIds = identity[2].split(',')
res2 = cur.execute(
'SELECT descriptors FROM facedescriptors WHERE id IN (%s)' %
','.join('?'*len(relatedFaceDescriptorIds)), relatedFaceDescriptorIds)
descriptors = []
for row2 in res2.fetchall():
descriptors.append(np.frombuffer(row2[0]))
distances = []
relatedFaceIds = identity[2].split(',')
for i, face in enumerate(relatedFaceIds):
distance = findEuclideanDistance(
descriptors[i],
np.frombuffer(identity[0])
)
distances.append(distance)
distances.sort()
print(distances)

View File

@ -1,10 +1,13 @@
#!/bin/bash
pid=$(ps aux |
grep '[0-9] node app.js' |
grep -E '[0-9] (/usr/bin/)?node .*server/app.js' |
while read user pid rest; do
echo $pid;
done)
if [[ "$pid" != "" ]]; then
echo "Killing ${pid}"
kill $pid
else
echo "No node server found"
fi

98
server/db-test.js Normal file
View File

@ -0,0 +1,98 @@
"use strict";
const Promise = require("bluebird");
let photoDB;
function bufferToFloat32Array(buffer) {
return new Float64Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float64Array.BYTES_PER_ELEMENT);
}
function euclideanDistance(a, b) {
let A = bufferToFloat32Array(a);
let B = bufferToFloat32Array(b);
console.log(A.length, B.length);
let sum = 0;
for (let i = 0; i < A.length; i++) {
let delta = A[i] - B[i];
sum += delta * delta;
}
return Math.sqrt(sum);
}
require("./db/photos").then(function(db) {
photoDB = db;
})
.then(async () => {
const id = 7;
const filter = ` WHERE identities.id=:id `;
const identities = await photoDB.sequelize.query("SELECT " +
"identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " +
"FROM identities " +
"INNER JOIN faces ON identities.id=faces.identityId " +
filter +
"GROUP BY identities.id", {
replacements: { id },
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
await Promise.map(identities, async (identity) => {
[ 'firstName', 'middleName', 'lastName' ].forEach(key => {
if (!identity[key]) {
identity[key] = '';
}
});
const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
let descriptors = await photoDB.sequelize.query(
`SELECT descriptors FROM facedescriptors WHERE id in (:ids)`, {
replacements: {
ids: identity.relatedFaceDescriptorIds.split(',')
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}
);
descriptors = descriptors.map(entry => entry.descriptors);
identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance(
descriptors[index],
identity.descriptors
);
console.log(index, distance);
return {
faceId,
photoId: relatedFacePhotos[index],
distance
};
});
identity
.relatedFaces
.sort((A, B) => {
return A.distance - B.distance;
});
/* If no filter was specified, only return the best face for
* the identity */
if (!filter) {
identity.relatedFaces = [ identity.relatedFaces[0] ];
}
delete identity.descriptors;
delete identity.relatedFaceIds;
delete identity.relatedFacePhotoIds;
delete identity.relatedIdentityDescriptors;
}, {
concurrency: 10
});
});

View File

@ -90,7 +90,7 @@ router.post("/", (req, res) => {
});
function bufferToFloat32Array(buffer) {
return new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float32Array.BYTES_PER_ELEMENT);
return new Float64Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float64Array.BYTES_PER_ELEMENT);
}
function euclideanDistance(a, b) {
@ -105,6 +105,8 @@ function euclideanDistance(a, b) {
}
router.get("/:id?", async (req, res) => {
console.log(`GET ${req.url}`);
let id;
if (req.params.id) {
@ -119,10 +121,9 @@ router.get("/:id?", async (req, res) => {
const identities = await photoDB.sequelize.query("SELECT " +
"identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," +
"GROUP_CONCAT(facedescriptors.descriptors) AS relatedIdentityDescriptors " +
"GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " +
"FROM identities " +
"INNER JOIN facedescriptors ON facedescriptors.id=faces.descriptorId " +
"INNER JOIN faces ON identities.id=faces.identityId " +
filter +
"GROUP BY identities.id", {
@ -131,7 +132,7 @@ router.get("/:id?", async (req, res) => {
raw: true
});
identities.forEach((identity) => {
await Promise.map(identities, async (identity) => {
[ 'firstName', 'middleName', 'lastName' ].forEach(key => {
if (!identity[key]) {
identity[key] = '';
@ -139,15 +140,26 @@ router.get("/:id?", async (req, res) => {
});
const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(","),
relatedIdentityDescriptors =
identity.relatedIdentityDescriptors.split(",");
relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
let descriptors = await photoDB.sequelize.query(
`SELECT descriptors FROM facedescriptors WHERE id in (:ids)`, {
replacements: {
ids: identity.relatedFaceDescriptorIds.split(',')
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}
);
descriptors = descriptors.map(entry => entry.descriptors);
identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance(
relatedIdentityDescriptors[index],
descriptors[index],
identity.descriptors
);
return {
faceId,
photoId: relatedFacePhotos[index],
@ -155,61 +167,24 @@ router.get("/:id?", async (req, res) => {
};
});
identity
.relatedFaces
.sort((A, B) => {
return A.distance - B.distance;
});
/* If no filter was specified, only return the best face for
* the identity */
if (!filter) {
identity.relatedFaces = [ identity.relatedFaces[0] ];
}
delete identity.descriptors;
delete identity.relatedFaceIds;
delete identity.relatedFacePhotoIds;
delete identity.relatedIdentityDescriptors;
});
//if (!req.query.withScore) {
console.log("No score request.");
return res.status(200).json(identities);
//}
// THe rest of this routine needs to be reworked -- I don't
// recall what it was doing; maybe getting a list of all identities
// sorted with distance to this faceId?
console.log("Looking up score against: " + req.query.withScore);
await Promise.map(identities, async (identity) => {
const descriptors = photoDB.sequelize.query(
"SELECT id FROM facedescriptors " +
"WHERE descriptorId " +
"IN (:id,:descriptorIds)", {
replacements: {
id: parseInt(req.query.withScore),
descriptorIds: identity.relatedFaces.map(
face => parseInt(face.faceId))
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
let target;
for (let i = 0; i < descriptors.length; i++) {
if (descriptors[i].descriptorId == req.query.withScore) {
target = descriptors[i].descriptors;
break;
}
}
if (!target) {
console.warn("Could not find descriptor for requested face: " + req.query.withScore);
return;
}
/* For each face's descriptor returned for this identity, compute the distance between the
* requested photo and that face descriptor */
descriptors.forEach((descriptor) => {
for (let i = 0; i < identity.relatedFaces.length; i++) {
if (identity.relatedFaces[i].faceId == descriptor.faceId) {
identity.relatedFaces[i].distance = euclideanDistance(target, descriptor.descriptors);
identity.relatedFaces[i].descriptors = descriptor.descriptors;
return;
}
}
});
}, {
concurrency: 5
});
return res.status(200).json(identities);
});