Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions client/platform/desktop/backend/native/attributeProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Attributes } from 'platform/desktop/constants';
import { MultiTrackRecord } from 'viame-web-common/apispec';
import { StringKeyObject, TrackData } from 'vue-media-annotator/track';


/**
* Processes a list of tracks and returns a MultiTrackRecord and an attributes object to be used
* @param tracks list of tracks to process for the attributes
*/
function processTrackAttributes(tracks: TrackData[]):
{data: MultiTrackRecord; attributes: Attributes} {
const attributeObj: Attributes = {};
const trackMap: MultiTrackRecord = {};
const testVals: Record<string, Record<string, number>> = {};

function processAttributes(attributes: StringKeyObject, type: 'track' | 'detection') {
Object.entries(attributes).forEach(([key, val]) => {
const valstring = `${val}`;
if (attributeObj[`${type}_${key}`] === undefined) {
attributeObj[`${type}_${key}`] = {
belongs: type,
datatype: 'text',
name: key,
_id: `${type}_${key}`,
};
testVals[`${type}_${key}`] = { };
testVals[`${type}_${key}`][valstring] = 1;
} else if (attributeObj[`${type}_${key}`] && testVals[`${type}_${key}`]) {
if (testVals[`${type}_${key}`][valstring]) {
testVals[`${type}_${key}`][valstring] += 1;
}
}
});
// Now we attempt to process the attributes to infer the type.
// Cascading based on the attempting to convert and values
Object.keys(attributeObj).forEach((attributeKey) => {
if (testVals[attributeKey]) {
let attributeType: ('number' | 'boolean' | 'text') = 'number';
let lowCount = 1;
const values: string[] = [];
Object.entries(testVals[attributeKey]).forEach(([key, val]) => {
if (val <= lowCount) {
lowCount = val;
}
values.push(key);
if (attributeType === 'number' && Number.isNaN(parseFloat(key))) {
attributeType = 'boolean';
}
if (attributeType === 'boolean' && key !== 'true' && key !== 'false') {
attributeType = 'text';
}
});
// If all items are used 2 or more times it has discrete set Values otherwise
if (lowCount >= 2 && attributeType.indexOf('text') !== -1) {
attributeObj[attributeKey].values = values;
}
// eslint-disable-next-line no-param-reassign
attributeObj[attributeKey].datatype = attributeType;
}
});
}

function processTrackforAttributes(track: TrackData) {
if (track.attributes) {
processAttributes(track.attributes, 'track');
}
if (track.features) {
track.features.forEach((item) => {
if (item.attributes) {
processAttributes(track.attributes, 'detection');
}
});
}
}

tracks.forEach((t) => {
trackMap[t.trackId.toString()] = t;
processTrackforAttributes(t);
});

return { data: trackMap, attributes: attributeObj };
}

export default processTrackAttributes;
101 changes: 101 additions & 0 deletions client/platform/desktop/backend/native/common.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
DesktopJobUpdate, DesktopJobUpdater, JsonMeta, Settings,
} from 'platform/desktop/constants';

import { Attribute } from 'viame-web-common/apispec';
import * as common from './common';

const pipelines = {
Expand Down Expand Up @@ -114,6 +115,12 @@ mockfs({
notanimage: '',
'notanimage.txt': '',
},
metaAttributesID: {
'foo.png': '',
'bar.png': '',
notanimage: '',
'notanimage.txt': '',
},
videoSuccess: {
'video1.avi': '',
'video1.mp4': '',
Expand Down Expand Up @@ -174,6 +181,40 @@ mockfs({
'result_2.json': '',
auxiliary: {},
},
metaAttributesID: {
'meta.json': JSON.stringify({
version: 1,
id: 'metaAttributesID',
type: 'image-sequence',
fps: 5,
originalBasePath: '/home/user/media/metaAttributesID',
originalImageFiles: [
'foo.png',
'bar.png',
],
attributes: {
// eslint-disable-next-line @typescript-eslint/camelcase
track_attribute1: {
belongs: 'track',
datatype: 'text',
values: ['value1', 'value2', 'value3'],
name: 'attribute1',
_id: 'track_attribute1',
},
// eslint-disable-next-line @typescript-eslint/camelcase
detection_attribute1: {
belongs: 'detection',
datatype: 'number',
name: 'attribute1',
_id: 'detection_attribute1',
},
},
}),
'result_whatever.json': JSON.stringify({}),
auxiliary: {},


},
},
},
});
Expand Down Expand Up @@ -280,6 +321,66 @@ describe('native.common', () => {
expect(meta.transcodingJobKey).toBe('jobKey');
expect(meta.type).toBe('video');
});


it('getAtributes', async () => {
const meta = await common.getAttributes(settings, 'metaAttributesID');
//Should return an array of data items
expect(meta.length).toBe(2);
expect(meta[0].values).toEqual(['value1', 'value2', 'value3']);
expect(meta[1].datatype).toBe('number');
});

it('addAttribute', async () => {
const templateAttribute: Attribute = {
name: 'newAttribute', datatype: 'boolean', belongs: 'track', _id: '',
};
await common.setAttribute(settings, 'metaAttributesID', {
data: templateAttribute,
});
//Should return an array of data items
const meta = await common.getAttributes(settings, 'metaAttributesID');
const newAttribute = meta.find((item) => item.name === 'newAttribute');
expect(meta.length).toBe(3);
expect(newAttribute).toEqual(templateAttribute);
});

it('updateAttribute', async () => {
const templateAttribute: Attribute = {
name: 'newAttributeName', datatype: 'boolean', belongs: 'track', _id: 'track_attribute1',
};
await common.setAttribute(settings, 'metaAttributesID', {
data: templateAttribute,
});
//Should return an array of data items
const meta = await common.getAttributes(settings, 'metaAttributesID');
const updatedAttribute = meta.find((item) => item._id === 'track_attribute1');
expect(meta.length).toBe(3);
expect(updatedAttribute).toEqual(templateAttribute);
});

it('deleteAttribute', async () => {
const deleteAttribute: Attribute = {
name: 'attribute1', datatype: 'text', belongs: 'track', _id: 'track_attribute1',
};
await common.deleteAttribute(settings, 'metaAttributesID', { data: deleteAttribute });
const meta = await common.getAttributes(settings, 'metaAttributesID');
//Should return an array of data items
expect(meta.length).toBe(2);
expect(meta[0].datatype).toBe('number');
});

it('initial attribute creation', async () => {
const templateAttribute: Attribute = {
name: 'newAttribute', datatype: 'boolean', belongs: 'track', _id: '',
};
await common.setAttribute(settings, 'projectid1VideoGood', {
data: templateAttribute,
});
const meta = await common.getAttributes(settings, 'projectid1VideoGood');
expect(meta.length).toBe(1);
expect(meta[0]).toEqual(templateAttribute);
});
});

afterAll(() => {
Expand Down
73 changes: 64 additions & 9 deletions client/platform/desktop/backend/native/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import moment from 'moment';
import lockfile from 'proper-lockfile';
import {
DatasetType, MultiTrackRecord, Pipelines, SaveDetectionsArgs,
FrameImage, DatasetMetaMutable, TrainingConfigs,
FrameImage, DatasetMetaMutable, TrainingConfigs, Attribute,
} from 'viame-web-common/apispec';
import * as viameSerializers from 'platform/desktop/backend/serializers/viame';

import {
websafeImageTypes, websafeVideoTypes, otherImageTypes, otherVideoTypes,
JsonMeta, Settings, JsonMetaCurrentVersion, DesktopMetadata, DesktopJobUpdater, ConvertMedia,
JsonMeta, Settings, JsonMetaCurrentVersion, DesktopMetadata,
DesktopJobUpdater, ConvertMedia, Attributes,
} from 'platform/desktop/constants';
import processTrackAttributes from './attributeProcessor';
import { cleanString, makeid } from './utils';

const ProjectsFolderName = 'DIVE_Projects';
Expand Down Expand Up @@ -321,10 +323,52 @@ async function saveMetadata(settings: Settings, datasetId: string, args: Dataset
if (args.customTypeStyling) {
existing.customTypeStyling = args.customTypeStyling;
}
if (args.attributes) {
existing.attributes = args.attributes;
}
await _saveAsJson(projectDirInfo.metaFileAbsPath, existing);
await release();
}

async function getAttributes(settings: Settings, datasetId: string):
Comment thread
subdavis marked this conversation as resolved.
Promise<Attribute[]> {
const projectDirData = await getValidatedProjectDir(settings, datasetId);
const projectMetaData = await loadJsonMetadata(projectDirData.metaFileAbsPath);
if (projectMetaData.attributes) {
return Object.values(projectMetaData.attributes);
}
return [];
}

async function setAttribute(settings: Settings, datasetId: string, { data }:
{data: Attribute }) {
const projectDirData = await getValidatedProjectDir(settings, datasetId);
const projectMetaData = await loadJsonMetadata(projectDirData.metaFileAbsPath);
if (!projectMetaData.attributes) {
projectMetaData.attributes = {};
}
// Reassign _id based on name if it is a new item
if (data._id === '') {
// eslint-disable-next-line no-param-reassign
data._id = `${data.belongs}_${data.name}`;
}
projectMetaData.attributes[data._id] = data;
await saveMetadata(settings, datasetId, projectMetaData);
}

async function deleteAttribute(settings: Settings, datasetId: string, { data }:
{ data: Attribute }) {
const projectDirData = await getValidatedProjectDir(settings, datasetId);
const projectMetaData = await loadJsonMetadata(projectDirData.metaFileAbsPath);
if (!projectMetaData.attributes) {
return;
}
if (projectMetaData.attributes[data._id]) {
delete projectMetaData.attributes[data._id];
}
await saveMetadata(settings, datasetId, projectMetaData);
}

/**
* processOtherAnnotationFiles imports data from external annotation formats
* given a list of candidate file paths.
Expand All @@ -339,9 +383,10 @@ async function processOtherAnnotationFiles(
settings: Settings,
datasetId: string,
absPaths: string[],
): Promise<{ fps?: number; processedFiles: string[] }> {
): Promise<{ fps?: number; processedFiles: string[]; attributes?: Attributes }> {
const fps = undefined;
const processedFiles = []; // which files were processed to generate the detections
let attributes: Attributes = {};

for (let i = 0; i < absPaths.length; i += 1) {
const path = absPaths[i];
Expand All @@ -354,10 +399,10 @@ async function processOtherAnnotationFiles(
try {
// eslint-disable-next-line no-await-in-loop
const tracks = await viameSerializers.parseFile(path);
const data: MultiTrackRecord = {};
tracks.forEach((t) => { data[t.trackId.toString()] = t; });
const processed = processTrackAttributes(tracks);
attributes = processed.attributes;
// eslint-disable-next-line no-await-in-loop
await _saveSerialized(settings, datasetId, data, true);
await _saveSerialized(settings, datasetId, processed.data, true);
processedFiles.push(path);
break; // Exit on first successful detection load
} catch (err) {
Expand All @@ -366,7 +411,7 @@ async function processOtherAnnotationFiles(
}
}
}
return { fps, processedFiles };
return { fps, processedFiles, attributes };
}

async function _initializeAppDataDir(settings: Settings) {
Expand Down Expand Up @@ -521,7 +566,6 @@ async function importMedia(
jsonMeta.transcodingJobKey = jobBase.key;
}

await _saveAsJson(npath.join(projectDirAbsPath, JsonMetaFileName), jsonMeta);

let foundDetections = false;

Expand All @@ -533,6 +577,10 @@ async function importMedia(
trackFileAbsPath,
npath.join(projectDirAbsPath, npath.basename(trackFileAbsPath)),
);
//Load tracks to generate attributes
const tracks = await loadJsonTracks(trackFileAbsPath);
const { attributes } = processTrackAttributes(Object.values(tracks));
if (attributes) jsonMeta.attributes = attributes;
foundDetections = true;
}
/* Look for other types of annotation files as a second priority */
Expand All @@ -543,12 +591,16 @@ async function importMedia(
if (csvFileCandidates.length > 1) {
throw new Error(`too many CSV files found in ${jsonMeta.originalBasePath}, expected at most 1`);
}
const { fps, processedFiles } = await processOtherAnnotationFiles(
const { fps, processedFiles, attributes } = await processOtherAnnotationFiles(
settings, dsId, csvFileCandidates,
);
if (fps) jsonMeta.fps = fps;
if (attributes) jsonMeta.attributes = attributes;
foundDetections = processedFiles.length > 0;
}

await _saveAsJson(npath.join(projectDirAbsPath, JsonMetaFileName), jsonMeta);

/* Finally create an empty file as fallback */
if (!foundDetections) {
await _saveSerialized(settings, dsId, {}, true);
Expand Down Expand Up @@ -593,4 +645,7 @@ export {
saveDetections,
saveMetadata,
completeConversion,
getAttributes,
setAttribute,
deleteAttribute,
};
Loading