Skip to content

Commit e0b517c

Browse files
authored
FPS Clamping DIVE (#722)
* FPS Clamping DIVE * Implement on worker * Fix parseint * Update clamping * WIP again * Fix tests * Finish clamping, parse FPS from CSV * Fix video stream discovery
1 parent 6f865ec commit e0b517c

19 files changed

Lines changed: 285 additions & 69 deletions

File tree

client/dive-common/components/Viewer.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import HeadTail from 'dive-common/recipes/headtail';
3030
import EditorMenu from 'dive-common/components/EditorMenu.vue';
3131
import ConfidenceFilter from 'dive-common/components/ConfidenceFilter.vue';
3232
import UserGuideButton from 'dive-common/components/UserGuideButton.vue';
33-
import RunPipelineMenu from 'dive-common/components/RunPipelineMenu.vue';
3433
import DeleteControls from 'dive-common/components/DeleteControls.vue';
3534
import ControlsContainer from 'dive-common/components/ControlsContainer.vue';
3635
import Sidebar from 'dive-common/components/Sidebar.vue';
@@ -51,7 +50,6 @@ export default defineComponent({
5150
VideoAnnotator,
5251
ImageAnnotator,
5352
ConfidenceFilter,
54-
RunPipelineMenu,
5553
UserGuideButton,
5654
EditorMenu,
5755
},

client/dive-common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const MediaTypes = {
88
};
99

1010
const DefaultVideoFPS = 10;
11+
const FPSOptions = [1, 5, 10, 15, 24, 25, 30, 50, 60];
1112

1213
const websafeVideoTypes = [
1314
'video/mp4',
@@ -79,6 +80,7 @@ export {
7980
ImageSequenceType,
8081
VideoType,
8182
MediaTypes,
83+
FPSOptions,
8284
calibrationFileTypes,
8385
fileVideoTypes,
8486
otherImageTypes,

client/platform/desktop/backend/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ function updater(update: DesktopJobUpdate) {
4747
}
4848

4949
async function parseViameFile(file: string) {
50-
const tracks = await parseFile(file);
51-
stdout.write(JSON.stringify(tracks));
50+
const data = await parseFile(file);
51+
stdout.write(JSON.stringify(data));
5252
}
5353

5454
async function parseJsonFile(filepath: string, metapath: string) {

client/platform/desktop/backend/native/common.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ const urlMapper = (a: string) => `http://localhost:8888/api/media?path=${a}`;
7777
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7878
const updater = (update: DesktopJobUpdate) => undefined;
7979
// eslint-disable-next-line @typescript-eslint/no-unused-vars
80-
const checkMedia = async (settingsVal: Settings, file: string) => file.includes('mp4');
80+
const checkMedia = async (settingsVal: Settings, file: string) => ({
81+
websafe: file.includes('mp4'),
82+
originalFps: 30,
83+
originalFpsString: '30/1',
84+
});
8185
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8286
const convertMedia = async (settingsVal: Settings, args: ConversionArgs,
8387
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -120,6 +124,11 @@ mockfs({
120124
notanimage: '',
121125
'notanimage.txt': '',
122126
},
127+
imageSuccessWithAnnotations: {
128+
'foo.png': '',
129+
'bar.png': '',
130+
'file1.csv': '# comment line\n# metadata,fps: 32,"whateever"\n#comment line',
131+
},
123132
videoSuccess: {
124133
'video1.avi': '',
125134
'video1.mp4': '',
@@ -298,6 +307,7 @@ describe('native.common', () => {
298307
);
299308
expect(data.videoUrl).toBe(`http://localhost:8888/api/media?path=${videoPath}`);
300309
});
310+
301311
it('loadJsonMetadata type multi without multiCam', async () => {
302312
await expect(common.loadMetadata(settings, 'projectid5missingMultiCam', urlMapper))
303313
.rejects.toThrow('Dataset: missingMulti is of type multiCam or stereo but contains no multiCam data');
@@ -310,12 +320,15 @@ describe('native.common', () => {
310320
version: 1,
311321
type: 'image-sequence',
312322
fps: 100,
323+
originalFps: 0,
313324
name: 'myproject1_name',
314325
createdAt: (new Date()).toString(),
315326
originalBasePath: '/foo/bar/baz',
316327
id: 'myproject1',
317328
originalImageFiles: [],
329+
transcodedImageFiles: [],
318330
originalVideoFile: '',
331+
transcodedVideoFile: '',
319332
};
320333
const result = await common.createKwiverRunWorkingDir(settings, [jsonMeta], 'mypipeline.pipe');
321334
const stat = fs.statSync(result);
@@ -333,12 +346,34 @@ describe('native.common', () => {
333346
expect(payload.jsonMeta.originalBasePath).toBe('/home/user/data/imageSuccess');
334347
});
335348

349+
it('import with CSV annotations', async () => {
350+
const payload = await common.beginMediaImport(settings, '/home/user/data/imageSuccessWithAnnotations', checkMedia);
351+
payload.jsonMeta.fps = 12; // simulate user specify FPS action
352+
await common.finalizeMediaImport(settings, payload, updater, convertMedia);
353+
const meta = await common.loadMetadata(settings, payload.jsonMeta.id, urlMapper);
354+
expect(meta.fps).toBe(32);
355+
});
356+
357+
it('import with user selected FPS > originalFPS', async () => {
358+
const payload = await common.beginMediaImport(settings, '/home/user/data/videoSuccess/video1.mp4', checkMedia);
359+
payload.jsonMeta.fps = 50; // above 30
360+
await common.finalizeMediaImport(settings, payload, updater, convertMedia);
361+
const meta1 = await common.loadMetadata(settings, payload.jsonMeta.id, urlMapper);
362+
expect(meta1.fps).toBe(30);
363+
364+
payload.jsonMeta.fps = -1; // above 30
365+
await common.finalizeMediaImport(settings, payload, updater, convertMedia);
366+
const meta2 = await common.loadMetadata(settings, payload.jsonMeta.id, urlMapper);
367+
expect(meta2.fps).toBe(1);
368+
});
369+
336370
it('importMedia video success', async () => {
337371
const payload = await common.beginMediaImport(settings, '/home/user/data/videoSuccess/video1.mp4', checkMedia);
338372
expect(payload.jsonMeta.name).toBe('video1');
339373
expect(payload.jsonMeta.originalImageFiles.length).toBe(0);
340374
expect(payload.jsonMeta.originalVideoFile).toBe('video1.mp4');
341375
expect(payload.jsonMeta.originalBasePath).toBe('/home/user/data/videoSuccess');
376+
expect(payload.jsonMeta.fps).toBe(5); // 5 is still the default
342377
});
343378

344379
it('importMedia empty json file success', async () => {

client/platform/desktop/backend/native/common.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import {
1818
} from 'dive-common/constants';
1919
import {
2020
JsonMeta, Settings, JsonMetaCurrentVersion, DesktopMetadata, DesktopJobUpdater,
21-
ConvertMedia, RunTraining, ExportDatasetArgs, MediaImportPayload,
21+
ConvertMedia, RunTraining, ExportDatasetArgs, MediaImportPayload, CheckMediaResults,
2222
} from 'platform/desktop/constants';
2323
import {
2424
cleanString, filterByGlob, makeid, strNumericCompare,
2525
} from 'platform/desktop/sharedUtils';
2626
import { Attribute, Attributes } from 'vue-media-annotator/use/useAttributes';
2727
import processTrackAttributes from './attributeProcessor';
28+
import { upgrade } from './migrations';
2829
import { getMultiCamUrls, transcodeMultiCam } from './multiCamUtils';
2930

3031
const ProjectsFolderName = 'DIVE_Projects';
@@ -148,13 +149,7 @@ async function loadJsonMetadata(metaAbsPath: string): Promise<JsonMeta> {
148149
throw new Error(`Unable to parse ${metaAbsPath}: ${err}`);
149150
}
150151
/* check if this file meets the current schema version */
151-
if ('version' in metaJson) {
152-
const { version } = metaJson;
153-
if (version !== JsonMetaCurrentVersion) {
154-
// TODO: schema migration for older schema versions
155-
throw new Error('outdated meta schema version found, migration not implemented');
156-
}
157-
}
152+
upgrade(metaJson);
158153
return metaJson as JsonMeta;
159154
}
160155

@@ -406,7 +401,7 @@ async function _saveAsJson(absPath: string, data: unknown) {
406401
}
407402

408403
async function saveMetadata(settings: Settings, datasetId: string,
409-
args: DatasetMetaMutable & { attributes?: Record<string, Attribute>}) {
404+
args: DatasetMetaMutable & { attributes?: Record<string, Attribute> }) {
410405
const projectDirInfo = await getValidatedProjectDir(settings, datasetId);
411406
const release = await _acquireLock(projectDirInfo.basePath, projectDirInfo.metaFileAbsPath, 'meta');
412407
const existing = await loadJsonMetadata(projectDirInfo.metaFileAbsPath);
@@ -460,7 +455,7 @@ async function processOtherAnnotationFiles(
460455
datasetId: string,
461456
absPaths: string[],
462457
): Promise<{ fps?: number; processedFiles: string[]; attributes?: Attributes }> {
463-
const fps = undefined;
458+
let fps: number | undefined;
464459
const processedFiles = []; // which files were processed to generate the detections
465460
let attributes: Attributes = {};
466461

@@ -474,8 +469,9 @@ async function processOtherAnnotationFiles(
474469
// Attempt to process the file
475470
try {
476471
// eslint-disable-next-line no-await-in-loop
477-
const tracks = await viameSerializers.parseFile(path);
478-
const processed = processTrackAttributes(tracks);
472+
const data = await viameSerializers.parseFile(path);
473+
const processed = processTrackAttributes(data.tracks);
474+
fps = fps || data.fps;
479475
attributes = processed.attributes;
480476
// eslint-disable-next-line no-await-in-loop
481477
await _saveSerialized(settings, datasetId, processed.data, true);
@@ -547,7 +543,7 @@ async function _initializeProjectDir(settings: Settings, jsonMeta: JsonMeta): Pr
547543
async function beginMediaImport(
548544
settings: Settings,
549545
path: string,
550-
checkMedia: (settings: Settings, path: string) => Promise<boolean>,
546+
checkMedia: (settings: Settings, path: string) => Promise<CheckMediaResults>,
551547
): Promise<MediaImportPayload> {
552548
let datasetType: DatasetType;
553549

@@ -568,11 +564,13 @@ async function beginMediaImport(
568564
const dsName = npath.parse(path).name;
569565
const dsId = `${cleanString(dsName).substr(0, 20)}_${makeid(10)}`;
570566

567+
const _defaultFps = datasetType === 'video' ? 5 : 1;
571568
const jsonMeta: JsonMeta = {
572569
version: JsonMetaCurrentVersion,
573570
type: datasetType,
574571
id: dsId,
575-
fps: 5, // TODO
572+
fps: _defaultFps, // adjusted below
573+
originalFps: _defaultFps, // adjusted below
576574
originalBasePath: path,
577575
originalVideoFile: '',
578576
createdAt: (new Date()).toString(),
@@ -599,10 +597,16 @@ async function beginMediaImport(
599597
if (websafeImageTypes.includes(mimetype) || otherImageTypes.includes(mimetype)) {
600598
throw new Error('User chose image file for video import option');
601599
} else if (websafeVideoTypes.includes(mimetype) || otherVideoTypes.includes(mimetype)) {
602-
const webSafeVideo = await checkMedia(settings, path);
603-
if (!webSafeVideo || otherVideoTypes.includes(mimetype)) {
600+
const checkMediaResult = await checkMedia(settings, path);
601+
if (!checkMediaResult.websafe || otherVideoTypes.includes(mimetype)) {
604602
mediaConvertList.push(path);
605603
}
604+
const newAnnotationFps = Math.floor(
605+
// Prevent FPS smaller than 1
606+
Math.max(1, Math.min(jsonMeta.fps, checkMediaResult.originalFps)),
607+
);
608+
jsonMeta.originalFps = checkMediaResult.originalFps;
609+
jsonMeta.fps = newAnnotationFps;
606610
} else {
607611
throw new Error(`unsupported MIME type for video ${mimetype}`);
608612
}
@@ -652,6 +656,12 @@ async function finalizeMediaImport(
652656
mediaConvertList = found.mediaConvertList;
653657
}
654658

659+
// Verify that the user didn't choose an FPS value higher than originalFPS
660+
// This shouldn't be possible in the UI, but we should still prevent it here.
661+
jsonMeta.fps = Math.floor(
662+
Math.max(1, Math.min(jsonMeta.fps, jsonMeta.originalFps)),
663+
);
664+
655665
//Now we will kick off any conversions that are necessary
656666
let jobBase = null;
657667
if (mediaConvertList.length) {

client/platform/desktop/backend/native/linux.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
RunTraining,
1515
DesktopJobUpdater,
1616
ConversionArgs,
17+
CheckMediaResults,
1718
} from 'platform/desktop/constants';
1819
import { observeChild } from 'platform/desktop/backend/native/processManager';
1920
import * as viame from './viame';
@@ -208,7 +209,7 @@ async function ffmpegCommand(settings: Settings) {
208209
* Checs the video file for the codec type and
209210
* returns true if it is x264, if not will return false for media conversion
210211
*/
211-
async function checkMedia(settings: Settings, file: string): Promise<boolean> {
212+
async function checkMedia(settings: Settings, file: string): Promise<CheckMediaResults> {
212213
await ffmpegCommand(settings);
213214
return viame.checkMedia({
214215
...ViameLinuxConstants,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* eslint-disable no-param-reassign */
2+
import { JsonMeta, JsonMetaCurrentVersion } from 'platform/desktop/constants';
3+
4+
function upgrade(meta: JsonMeta) {
5+
if (meta.version === JsonMetaCurrentVersion) {
6+
/* Perform soft upgrades of backward-compatible properties */
7+
if (meta.originalFps === undefined) {
8+
// This will be an incorrect value.
9+
meta.originalFps = meta.fps;
10+
}
11+
} else if (meta.version < JsonMetaCurrentVersion) {
12+
/* Perform major version upgrade */
13+
console.error('Impossible schema', meta);
14+
throw new Error('Impossible schema revision detected. This is not a valid DIVE project metadata file. Check the console for details.');
15+
} else if (meta.version > JsonMetaCurrentVersion) {
16+
throw new Error('You are trying to open a newer version of the project schema. Please upgrade DIVE to the latest version');
17+
}
18+
meta.version = JsonMetaCurrentVersion;
19+
}
20+
21+
// eslint-disable-next-line import/prefer-default-export
22+
export { upgrade };

client/platform/desktop/backend/native/multiCam.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ const settings: Settings = {
2626
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2727
const updater = (update: DesktopJobUpdate) => undefined;
2828
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29-
const checkMedia = async (settingsVal: Settings, file: string) => file.includes('mp4');
29+
const checkMedia = async (settingsVal: Settings, file: string) => ({
30+
websafe: file.includes('mp4'),
31+
originalFpsString: '30/1',
32+
originalFps: 30,
33+
});
3034
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3135
const convertMedia = async (settingsVal: Settings, args: ConversionArgs,
3236
// eslint-disable-next-line @typescript-eslint/no-unused-vars

client/platform/desktop/backend/native/multiCamImport.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import npath from 'path';
22
import fs from 'fs-extra';
33
import mime from 'mime-types';
4-
import {
5-
DatasetType, MultiCamImportFolderArgs,
6-
MultiCamImportKeywordArgs, MultiCamImportArgs,
7-
} from 'dive-common/apispec';
4+
import { DatasetType } from 'dive-common/apispec';
85
import {
96
websafeImageTypes, websafeVideoTypes, otherImageTypes, otherVideoTypes,
107
} from 'dive-common/constants';
118
import {
129
JsonMeta, Settings, JsonMetaCurrentVersion,
10+
MultiCamImportFolderArgs,
11+
MultiCamImportKeywordArgs,
12+
MultiCamImportArgs,
13+
CheckMediaResults,
1314
MediaImportPayload,
1415
} from 'platform/desktop/constants';
1516
import { cleanString, makeid } from 'platform/desktop/sharedUtils';
@@ -43,7 +44,7 @@ async function asyncForEach(array: any[], callback: Function) {
4344
async function beginMultiCamImport(
4445
settings: Settings,
4546
args: MultiCamImportArgs,
46-
checkMedia: (settings: Settings, path: string) => Promise<boolean>,
47+
checkMedia: (settings: Settings, path: string) => Promise<CheckMediaResults>,
4748
): Promise<MediaImportPayload> {
4849
const datasetType: DatasetType | 'multi' = 'multi';
4950

@@ -99,6 +100,7 @@ async function beginMultiCamImport(
99100
type: datasetType,
100101
id: dsId,
101102
fps: 5, // TODO
103+
originalFps: 5,
102104
originalBasePath: mainFolder,
103105
originalVideoFile: '',
104106
createdAt: (new Date()).toString(),
@@ -128,13 +130,21 @@ async function beginMultiCamImport(
128130
if (websafeImageTypes.includes(mimetype) || otherImageTypes.includes(mimetype)) {
129131
throw new Error('User chose image file for video import option');
130132
} else if (websafeVideoTypes.includes(mimetype) || otherVideoTypes.includes(mimetype)) {
131-
const webSafeVideo = await checkMedia(settings, video);
132-
if (!webSafeVideo || otherVideoTypes.includes(mimetype)) {
133+
const checkMediaResult = await checkMedia(settings, video);
134+
if (!checkMediaResult.websafe || otherVideoTypes.includes(mimetype)) {
133135
mediaConvertList.push(video);
134136
}
135137
if (jsonMeta.multiCam && jsonMeta.multiCam.cameras[key] !== undefined) {
136138
jsonMeta.multiCam.cameras[key].originalVideoFile = npath.basename(video);
137139
}
140+
const newAnnotationFps = Math.floor(
141+
Math.min(jsonMeta.fps, checkMediaResult.originalFps),
142+
);
143+
if (newAnnotationFps <= 0) {
144+
throw new Error('fps < 1 unsupported');
145+
}
146+
jsonMeta.originalFps = checkMediaResult.originalFps;
147+
jsonMeta.fps = newAnnotationFps;
138148
} else {
139149
throw new Error(`unsupported MIME type for video ${mimetype}`);
140150
}

0 commit comments

Comments
 (0)