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
16 changes: 14 additions & 2 deletions client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ interface PipeMetadata {
diveParams?: DiveParam[];
}

interface PipelineRuntimeParams {
frameRange?: [number, number] | null;
}

interface PipelineParams {
kwiverParams?: Record<string, string>;
runtimeParams?: PipelineRuntimeParams;
}

interface Pipe {
name: string;
pipe: string;
Expand Down Expand Up @@ -161,12 +170,13 @@ interface DatasetMetaMutable {
customTypeStyling?: Record<string, CustomStyle>;
customGroupStyling?: Record<string, CustomStyle>;
confidenceFilters?: Record<string, number>;
timeFilters?: [number, number] | null;
imageEnhancements?: ImageEnhancements;
attributes?: Readonly<Record<string, Attribute>>;
attributeTrackFilters?: Readonly<Record<string, AttributeTrackFilter>>;
error?: string;
}
const DatasetMetaMutableKeys = ['attributes', 'confidenceFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters'];
const DatasetMetaMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters'];

interface DatasetMeta extends DatasetMetaMutable {
id: Readonly<string>;
Expand All @@ -183,7 +193,7 @@ interface DatasetMeta extends DatasetMetaMutable {

interface Api {
getPipelineList(): Promise<Pipelines>;
runPipeline(itemId: string, pipeline: Pipe, additionalConfig?: Record<string, string>): Promise<unknown>;
runPipeline(itemId: string, pipeline: Pipe, pipelineParams?: PipelineParams): Promise<unknown>;
deleteTrainedPipeline(pipeline: Pipe): Promise<void>;
exportTrainedPipeline(path: string, pipeline: Pipe): Promise<unknown>;

Expand Down Expand Up @@ -257,6 +267,8 @@ export {
MultiTrackRecord,
MultiGroupRecord,
Pipe,
PipelineParams,
PipelineRuntimeParams,
PipeMetadata,
Pipelines,
SaveDetectionsArgs,
Expand Down
11 changes: 10 additions & 1 deletion client/dive-common/components/RunPipelineMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export default defineComponent({
type: Boolean,
default: false,
},
/* Time filter range from the viewer - [startFrame, endFrame] or null */
timeFilter: {
type: Array as unknown as PropType<[number, number] | null>,
default: null,
},
},

setup(props) {
Expand Down Expand Up @@ -200,10 +205,14 @@ export default defineComponent({
datasetIds = props.selectedDatasetIds.map((item) => item.substring(0, item.lastIndexOf('/')));
}
selectedPipeline.value = pipeline;
const frameRange = props.timeFilter;
await _runPipelineRequest(() => Promise.all(
datasetIds.map((id) => {
const additionalConfig = additionalConfigById ? additionalConfigById[id] : undefined;
return runPipeline(id, pipeline, additionalConfig);
return runPipeline(id, pipeline, {
kwiverParams: additionalConfig,
runtimeParams: frameRange ? { frameRange } : undefined,
});
}),
));
}
Expand Down
9 changes: 9 additions & 0 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ export default defineComponent({
customTypeStyling: trackStyleManager.getTypeStyles(trackFilters.allTypes),
customGroupStyling: groupStyleManager.getTypeStyles(groupFilters.allTypes),
confidenceFilters: trackFilters.confidenceFilters.value,
timeFilters: trackFilters.timeFilters.value,
imageEnhancements: imageEnhancements.value,
// TODO Group confidence filters are not yet supported.
}, saveSet);
Expand All @@ -542,6 +543,12 @@ export default defineComponent({
});
}

function saveTimeFilter() {
saveMetadata(datasetId.value, {
timeFilters: trackFilters.timeFilters.value,
});
}

function saveImageEnhancements() {
saveMetadata(datasetId.value, {
imageEnhancements: imageEnhancements.value,
Expand Down Expand Up @@ -744,6 +751,7 @@ export default defineComponent({
loadAttributes(meta.attributes);
}
trackFilters.setConfidenceFilters(meta.confidenceFilters);
trackFilters.setTimeFilters(meta.timeFilters ?? null);
if (meta.imageEnhancements) {
setImageEnhancements(meta.imageEnhancements);
}
Expand Down Expand Up @@ -1223,6 +1231,7 @@ export default defineComponent({
handler: globalHandler,
save,
saveThreshold,
saveTimeFilter,
updateTime,
// multicam
multiCamList,
Expand Down
3 changes: 3 additions & 0 deletions client/platform/desktop/backend/native/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,9 @@ async function saveMetadata(settings: Settings, datasetId: string, args: Dataset
if (args.attributes) {
existing.attributes = args.attributes;
}
if (args.timeFilters !== undefined) {
existing.timeFilters = args.timeFilters;
}
if (args.error) {
existing.error = args.error;
}
Expand Down
84 changes: 80 additions & 4 deletions client/platform/desktop/backend/native/viame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,54 @@ import {
const PipelineRelativeDir = 'configs/pipelines';
const DiveJobManifestName = 'dive_job_manifest.json';

/**
* Filter an image list to only include images within frame range.
* @param imageList List of image file paths
* @param frameRange Tuple of (start_frame, end_frame) inclusive (0-indexed)
* @returns Filtered list of image file paths
*/
function filterImageListByFrameRange(
imageList: string[],
frameRange: [number, number],
): string[] {
const [startFrame, endFrame] = frameRange;
// Ensure we don't go out of bounds
const safeStart = Math.max(0, startFrame);
const safeEnd = Math.min(endFrame, imageList.length - 1);
return imageList.slice(safeStart, safeEnd + 1);
}

/**
* Filter VIAME CSV to only include detections within frame range.
* @param csvPath Path to the input CSV file
* @param frameRange Tuple of (start_frame, end_frame) inclusive
* @returns Path to the filtered CSV file
*/
async function filterCsvByFrameRange(
csvPath: string,
frameRange: [number, number],
): Promise<string> {
const [startFrame, endFrame] = frameRange;
const filteredPath = csvPath.replace('.csv', '_filtered.csv');

const content = await fs.readFile(csvPath, 'utf-8');
const lines = content.split('\n');
const filteredLines = lines.filter((line) => {
if (line.startsWith('#') || line.trim() === '') {
return true;
}
const parts = line.split(',');
if (parts.length >= 3) {
const frame = parseInt(parts[2], 10);
return !Number.isNaN(frame) && frame >= startFrame && frame <= endFrame;
}
return false;
});

await fs.writeFile(filteredPath, filteredLines.join('\n'));
return filteredPath;
}

export interface ViameConstants {
setupScriptAbs: string; // abs path setup comman
/** Basename of unified VIAME CLI in `bin/` (e.g. `viame` or `viame.exe`). */
Expand Down Expand Up @@ -109,6 +157,7 @@ async function runPipeline(
forceTranscodedVideo?: boolean,
): Promise<DesktopJob> {
const { datasetId, pipeline } = runPipelineArgs;
const frameRange = runPipelineArgs.pipelineParams?.runtimeParams?.frameRange ?? undefined;

const isValid = await validateViamePath(settings);
if (isValid !== true) {
Expand Down Expand Up @@ -186,6 +235,17 @@ async function runPipeline(
`-p "${pipelinePath}"`,
`-s downsampler:target_frame_rate=${meta.fps}`,
];
if (frameRange) {
command.push(`-s downsampler:start_frame=${frameRange[0]}`);
command.push(`-s downsampler:end_frame=${frameRange[1]}`);
const isNative = !meta.originalFps || meta.fps >= meta.originalFps;
command.push(`-s downsampler:frame_range_is_native=${isNative}`);
// Transcode/filter pipes: output frames renumbered relative to new range
// All other pipes: output frames relative to original video
const renumber = pipeline.type === 'transcode' || pipeline.type === 'filter';
command.push(`-s downsampler:renumber_frames=${renumber}`);
command.push(`-s downsampler:adjust_timestamps=${renumber}`);
}
if (!stereoOrMultiCam) {
command.push(`-s input:video_filename="${videoAbsPath}"`);
command.push(`-s detector_writer:file_name="${detectorOutput}"`);
Expand All @@ -199,6 +259,10 @@ async function runPipeline(
if (meta.type === MultiType) {
imageList = getMultiCamImageFiles(meta);
}
// Filter image list by frame range if specified
if (frameRange) {
imageList = filterImageListByFrameRange(imageList, frameRange);
}
const fileData = imageList
.map((f) => npath.join(meta.originalBasePath, f))
.join('\n');
Expand Down Expand Up @@ -254,9 +318,10 @@ async function runPipeline(
}

// Add any custom pipeline parameters
if (runPipelineArgs.pipelineParams) {
const kwiverParams = runPipelineArgs.pipelineParams?.kwiverParams;
if (kwiverParams) {
const escapeValue = (val: string) => val.replace(/["$]/g, '\\$&');
Object.entries(runPipelineArgs.pipelineParams).forEach(([key, value]) => {
Object.entries(kwiverParams).forEach(([key, value]) => {
command.push(`-s ${key}="${escapeValue(value)}"`);
});
}
Expand Down Expand Up @@ -293,8 +358,19 @@ async function runPipeline(
if (code === 0) {
try {
if (!pipelineCreatesDatasetMarkers.includes(runPipelineArgs.pipeline.type)) {
// Filter and transcode pipelines should ensure that detector/track output files are located in the new dataset directory
const { meta: newMeta } = await common.ingestDataFiles(settings, datasetId, [detectorOutput, trackOutput], multiOutFiles);
let finalDetectorOutput = detectorOutput;
let finalTrackOutput = trackOutput;

if (frameRange && metaType === 'video') {
if (await fs.pathExists(trackOutput)) {
finalTrackOutput = await filterCsvByFrameRange(trackOutput, frameRange);
}
if (await fs.pathExists(detectorOutput)) {
finalDetectorOutput = await filterCsvByFrameRange(detectorOutput, frameRange);
}
}

const { meta: newMeta } = await common.ingestDataFiles(settings, datasetId, [finalDetectorOutput, finalTrackOutput], multiOutFiles);
if (newMeta) {
meta.attributes = newMeta.attributes;
await common.saveMetadata(settings, datasetId, meta);
Expand Down
5 changes: 2 additions & 3 deletions client/platform/desktop/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {
DatasetMeta, DatasetMetaMutable, DatasetType,
Pipe, SubType, MediaImportResponse,
Pipe, SubType, MediaImportResponse, PipelineParams,
} from 'dive-common/apispec';
import { Attribute } from 'vue-media-annotator/use/AttributeTypes';
import { AttributeTrackFilter } from 'vue-media-annotator/AttributeTrackFilterControls';
Expand Down Expand Up @@ -175,8 +175,7 @@ export interface RunPipeline extends JobArgs {
type: JobType.RunPipeline;
datasetId: string;
pipeline: Pipe;
/** Optional parameters to pass to the pipeline via -s flags */
pipelineParams?: Record<string, string>;
pipelineParams?: PipelineParams;
outputDatasetName?: string;
}

Expand Down
6 changes: 3 additions & 3 deletions client/platform/desktop/frontend/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios, { AxiosInstance } from 'axios';

import type {
DatasetMetaMutable, DatasetType, MultiCamImportArgs,
Pipe, Pipelines, SaveAttributeArgs,
Pipe, Pipelines, PipelineParams, SaveAttributeArgs,
SaveAttributeTrackFilterArgs, SaveDetectionsArgs, TrainingConfigs,
} from 'dive-common/apispec';

Expand Down Expand Up @@ -118,12 +118,12 @@ async function getTrainingConfigurations(): Promise<TrainingConfigs> {
return window.diveDesktop.invoke('get-training-configs');
}

async function runPipeline(itemId: string, pipeline: Pipe, additionalConfig?: Record<string, string>): Promise<void> {
async function runPipeline(itemId: string, pipeline: Pipe, pipelineParams?: PipelineParams): Promise<void> {
const args: RunPipeline = {
type: JobType.RunPipeline,
pipeline,
datasetId: itemId,
pipelineParams: additionalConfig,
pipelineParams,
};
gpuJobQueue.enqueue(args);
}
Expand Down
14 changes: 13 additions & 1 deletion client/platform/desktop/frontend/components/ViewerLoader.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import {
computed, defineComponent, ref, watch,
computed, defineComponent, ref, watch, Ref,
} from 'vue';
import Viewer from 'dive-common/components/Viewer.vue';
import RunPipelineMenu from 'dive-common/components/RunPipelineMenu.vue';
Expand Down Expand Up @@ -85,6 +85,16 @@ export default defineComponent({
return props.id;
});
const readOnlyMode = computed(() => settings.value?.readonlyMode || false);
const timeFilter: Ref<[number, number] | null> = ref(null);

watch(
() => viewerRef.value?.trackFilters?.timeFilters?.value,
(value) => {
timeFilter.value = value ?? null;
},
{ immediate: true },
);

const runningPipelines = computed(() => {
const results: string[] = [];
// Check if any running job contains the root props.id
Expand Down Expand Up @@ -120,6 +130,7 @@ export default defineComponent({
readOnlyMode,
runningPipelines,
largeImageWarning,
timeFilter,
};
},
});
Expand Down Expand Up @@ -159,6 +170,7 @@ export default defineComponent({
:camera-numbers="camNumbers"
:running-pipelines="runningPipelines"
:read-only-mode="readOnlyMode"
:time-filter="timeFilter"
v-bind="{ buttonOptions, menuOptions }"
/>
<ImportAnnotations
Expand Down
19 changes: 10 additions & 9 deletions client/platform/web-girder/api/rpc.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import girderRest from 'platform/web-girder/plugins/girder';
import type { GirderModel } from '@girder/components/src';
import { Pipe } from 'dive-common/apispec';
import type { Pipe, PipelineParams } from 'dive-common/apispec';

function postProcess(folderId: string, skipJobs = false, skipTranscoding = false, additive = false, additivePrepend = '', set: string | undefined = undefined) {
return girderRest.post<{folder: GirderModel, warnings: string[], job_ids: string[]}>(`dive_rpc/postprocess/${folderId}`, null, {
Expand All @@ -10,14 +10,15 @@ function postProcess(folderId: string, skipJobs = false, skipTranscoding = false
});
}

function runPipeline(itemId: string, pipeline: Pipe, additionalConfig?: Record<string, string>) {
return girderRest.post('dive_rpc/pipeline', null, {
params: {
folderId: itemId,
pipeline,
pipelineParams: additionalConfig,
},
});
function runPipeline(itemId: string, pipeline: Pipe, pipelineParams?: PipelineParams) {
const params: Record<string, unknown> = {
folderId: itemId,
pipeline,
};
if (pipelineParams) {
params.pipelineParams = pipelineParams;
}
return girderRest.post('dive_rpc/pipeline', null, { params });
}

function runTraining(
Expand Down
Loading
Loading