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 { .Identities {
display: flex; display: flex;
flex-grow: 1; overflow-y: scroll;
flex-direction: column;
border: 1px solid green; 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 { .Cluster {
display: flex; display: flex;
flex-direction: column;
overflow-y: scroll;
flex-grow: 1; flex-grow: 1;
border: 1px solid red; 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 { useApi } from './useApi';
import './App.css'; import './App.css';
const Cluster = () => { type ClusterProps = {
return ( id: number
<div className='Cluster'>cluster</div> };
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 = { type Identity = {
@ -16,22 +74,41 @@ type Identity = {
descriptors: number[], descriptors: number[],
id: number id: number
displayName: string, displayName: string,
relatedFaces: Face[]
}; };
interface IdentitiesProps { interface IdentitiesProps {
setIdentity?(id: number): void,
identities: Identity[] identities: Identity[]
}; };
const Identities = ({ identities } : IdentitiesProps) => { const Identities = ({ identities, setIdentity } : IdentitiesProps) => {
const identitiesJSX = useMemo(() =>
identities.map((identity) => {
const idPath = String(identity.id % 100).padStart(2, '0'); const identitiesJSX = useMemo(() => {
return (<img const loadIdentity = (id: number): void => {
key={identity.id} if (setIdentity) {
alt={identity.id.toString()} setIdentity(id)
src={`/faces/${idPath}/${identity.id}.jpg`}/>); }
} };
), [ identities ]); 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 ( return (
<div className='Identities'> <div className='Identities'>
@ -42,8 +119,9 @@ const Identities = ({ identities } : IdentitiesProps) => {
const App = () => { const App = () => {
const [identities, setIdentities] = useState<Identity[]>([]); const [identities, setIdentities] = useState<Identity[]>([]);
const [identity, setIdentity] = useState<number>(0);
const { loading, data } = useApi( const { loading, data } = useApi(
'../api/v1/faces' '../api/v1/identities'
); );
useEffect(() => { useEffect(() => {
@ -56,9 +134,12 @@ const App = () => {
<div className="App"> <div className="App">
<div className="Worksheet"> <div className="Worksheet">
{ loading && <div>Loading...</div> } { loading && <div>Loading...</div> }
{ !loading && identity !== 0 && <Cluster id={identity} />}
{ !loading && identity === 0 && <div className="Cluster">
Select identity to edit
</div> }
{ !loading && <> { !loading && <>
<Cluster/> <Identities {... {identities, setIdentity }}/>
<Identities identities={identities}/>
</> } </> }
</div> </div>
</div> </div>

View File

@ -8,12 +8,17 @@ type UseApi = {
}; };
const useApi = (_url: string, _options?: {}) : UseApi => { const useApi = (_url: string, _options?: {}) : UseApi => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(false);
const [data, setData] = useState(undefined); const [data, setData] = useState(undefined);
const [error, setError] = useState<any>(undefined); const [error, setError] = useState<any>(undefined);
useEffect(() => { useEffect(() => {
if (_url === '' || loading) {
return;
}
const fetchApi = async () => { const fetchApi = async () => {
console.log(`Fetching ${_url}...`);
setLoading(true);
try { try {
const res = await window.fetch(_url, _options); const res = await window.fetch(_url, _options);
const data = await res.json(); const data = await res.json();
@ -27,7 +32,7 @@ const useApi = (_url: string, _options?: {}) : UseApi => {
}; };
fetchApi(); fetchApi();
}, [_url, _options]); }, [_url, _options, loading]);
return { loading, data, error }; 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 #!/bin/bash
pid=$(ps aux | 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 while read user pid rest; do
echo $pid; echo $pid;
done) done)
if [[ "$pid" != "" ]]; then if [[ "$pid" != "" ]]; then
echo "Killing ${pid}"
kill $pid kill $pid
else
echo "No node server found"
fi 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) { 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) { function euclideanDistance(a, b) {
@ -105,6 +105,8 @@ function euclideanDistance(a, b) {
} }
router.get("/:id?", async (req, res) => { router.get("/:id?", async (req, res) => {
console.log(`GET ${req.url}`);
let id; let id;
if (req.params.id) { if (req.params.id) {
@ -119,10 +121,9 @@ router.get("/:id?", async (req, res) => {
const identities = await photoDB.sequelize.query("SELECT " + const identities = await photoDB.sequelize.query("SELECT " +
"identities.*," + "identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," + "GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," + "GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," +
"GROUP_CONCAT(facedescriptors.descriptors) AS relatedIdentityDescriptors " + "GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " +
"FROM identities " + "FROM identities " +
"INNER JOIN facedescriptors ON facedescriptors.id=faces.descriptorId " +
"INNER JOIN faces ON identities.id=faces.identityId " + "INNER JOIN faces ON identities.id=faces.identityId " +
filter + filter +
"GROUP BY identities.id", { "GROUP BY identities.id", {
@ -131,7 +132,7 @@ router.get("/:id?", async (req, res) => {
raw: true raw: true
}); });
identities.forEach((identity) => { await Promise.map(identities, async (identity) => {
[ 'firstName', 'middleName', 'lastName' ].forEach(key => { [ 'firstName', 'middleName', 'lastName' ].forEach(key => {
if (!identity[key]) { if (!identity[key]) {
identity[key] = ''; identity[key] = '';
@ -139,15 +140,26 @@ router.get("/:id?", async (req, res) => {
}); });
const relatedFaces = identity.relatedFaceIds.split(","), const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(","), relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
relatedIdentityDescriptors =
identity.relatedIdentityDescriptors.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) => { identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance( const distance = euclideanDistance(
relatedIdentityDescriptors[index], descriptors[index],
identity.descriptors identity.descriptors
); );
return { return {
faceId, faceId,
photoId: relatedFacePhotos[index], 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.relatedFaceIds;
delete identity.relatedFacePhotoIds; delete identity.relatedFacePhotoIds;
delete identity.relatedIdentityDescriptors; 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); return res.status(200).json(identities);
}); });