⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
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
5 changes: 3 additions & 2 deletions client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,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 @@ -155,7 +156,7 @@ interface DatasetMeta extends DatasetMetaMutable {

interface Api {
getPipelineList(): Promise<Pipelines>;
runPipeline(itemId: string, pipeline: Pipe): Promise<unknown>;
runPipeline(itemId: string, pipeline: Pipe, frameRange?: [number, number] | null): Promise<unknown>;
deleteTrainedPipeline(pipeline: Pipe): Promise<void>;
exportTrainedPipeline(path: string, pipeline: Pipe): Promise<unknown>;

Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/AnnotationVisibilityMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,10 @@ export default defineComponent({
>
<v-menu
v-if="button.id === 'text'"
:key="`${button.id}-view`"
open-on-hover
bottom
offset-y
:key="`${button.id}-view`"
:close-on-content-click="false"
>
<template #activator="{ on, attrs }">
Expand Down
8 changes: 7 additions & 1 deletion client/dive-common/components/RunPipelineMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,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 @@ -149,8 +154,9 @@ export default defineComponent({
datasetIds = props.selectedDatasetIds.map((item) => item.substring(0, item.lastIndexOf('/')));
}
selectedPipe.value = pipeline;
const frameRange = props.timeFilter;
await _runPipelineRequest(() => Promise.all(
datasetIds.map((id) => runPipeline(id, pipeline)),
datasetIds.map((id) => runPipeline(id, pipeline, frameRange)),
));
}

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 @@ -433,6 +433,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 @@ -458,6 +459,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 @@ -577,6 +584,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 @@ -882,6 +890,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 @@ -569,6 +569,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
81 changes: 79 additions & 2 deletions client/platform/desktop/backend/native/viame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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
trainingExe: string; // name of training binary on PATH
Expand All @@ -40,7 +88,7 @@ async function runPipeline(
viameConstants: ViameConstants,
forceTranscodedVideo?: boolean,
): Promise<DesktopJob> {
const { datasetId, pipeline } = runPipelineArgs;
const { datasetId, pipeline, frameRange } = runPipelineArgs;

const isValid = await validateViamePath(settings);
if (isValid !== true) {
Expand Down Expand Up @@ -100,6 +148,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 @@ -113,6 +172,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 @@ -194,7 +257,21 @@ async function runPipeline(
job.on('exit', async (code) => {
if (code === 0) {
try {
const { meta: newMeta } = await common.ingestDataFiles(settings, datasetId, [detectorOutput, trackOutput], multiOutFiles);
// Determine which output files to use
let finalDetectorOutput = detectorOutput;
let finalTrackOutput = trackOutput;

// Filter output CSV by frame range for videos
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
1 change: 1 addition & 0 deletions client/platform/desktop/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export interface RunPipeline extends JobArgs {
type: JobType.RunPipeline;
datasetId: string;
pipeline: Pipe;
frameRange?: [number, number] | null; // (start_frame, end_frame) or null for full video
}

export interface ExportTrainedPipeline extends JobArgs {
Expand Down
3 changes: 2 additions & 1 deletion client/platform/desktop/frontend/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ async function getTrainingConfigurations(): Promise<TrainingConfigs> {
return ipcRenderer.invoke('get-training-configs');
}

async function runPipeline(itemId: string, pipeline: Pipe): Promise<void> {
async function runPipeline(itemId: string, pipeline: Pipe, frameRange?: [number, number] | null): Promise<void> {
const args: RunPipeline = {
type: JobType.RunPipeline,
pipeline,
datasetId: itemId,
frameRange: frameRange || undefined,
};
gpuJobQueue.enqueue(args);
}
Expand Down
15 changes: 14 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, watchEffect,
} 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,17 @@ export default defineComponent({
return props.id;
});
const readOnlyMode = computed(() => settings.value?.readonlyMode || false);
const timeFilter: Ref<[number, number] | null> = ref(null);

// Watch the viewer's trackFilters.timeFilters and sync to local ref
watchEffect(() => {
if (viewerRef.value?.trackFilters?.timeFilters?.value) {
timeFilter.value = viewerRef.value.trackFilters.timeFilters.value;
} else {
timeFilter.value = null;
}
});

const runningPipelines = computed(() => {
const results: string[] = [];
// Check if any running job contains the root props.id
Expand Down Expand Up @@ -120,6 +131,7 @@ export default defineComponent({
readOnlyMode,
runningPipelines,
largeImageWarning,
timeFilter,
};
},
});
Expand Down Expand Up @@ -159,6 +171,7 @@ export default defineComponent({
:camera-numbers="camNumbers"
:running-pipelines="runningPipelines"
:read-only-mode="readOnlyMode"
:time-filter="timeFilter"
v-bind="{ buttonOptions, menuOptions }"
/>
<ImportAnnotations
Expand Down
18 changes: 11 additions & 7 deletions client/platform/web-girder/api/rpc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ function postProcess(folderId: string, skipJobs = false, skipTranscoding = false
});
}

function runPipeline(itemId: string, pipeline: Pipe) {
return girderRest.post('dive_rpc/pipeline', null, {
params: {
folderId: itemId,
pipeline,
},
});
function runPipeline(itemId: string, pipeline: Pipe, frameRange?: [number, number] | null) {
const params: Record<string, unknown> = {
folderId: itemId,
pipeline,
};
if (frameRange) {
const [startFrame, endFrame] = frameRange;
params.startFrame = startFrame;
params.endFrame = endFrame;
}
return girderRest.post('dive_rpc/pipeline', null, { params });
}

function runTraining(
Expand Down
13 changes: 13 additions & 0 deletions client/platform/web-girder/views/ViewerLoader.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import {
computed, defineComponent, onBeforeUnmount, onMounted, ref, toRef, watch, Ref, PropType,
watchEffect,
} from 'vue';

import Viewer from 'dive-common/components/Viewer.vue';
Expand Down Expand Up @@ -106,12 +107,22 @@ export default defineComponent({
const currentJob = computed(() => getters['Jobs/datasetCompleteJobs'](props.id));

const typeList: Ref<string[]> = ref([]);
const timeFilter: Ref<[number, number] | null> = ref(null);

const findType = async () => {
const meta = await loadMetadata(props.id);
typeList.value = [meta.type];
};
findType();

// Watch the viewer's trackFilters.timeFilters and sync to local ref
watchEffect(() => {
if (viewerRef.value?.trackFilters?.timeFilters?.value) {
timeFilter.value = viewerRef.value.trackFilters.timeFilters.value;
} else {
timeFilter.value = null;
}
});
const runningPipelines = computed(() => {
const results: string[] = [];
if (getters['Jobs/datasetRunningState'](props.id)) {
Expand Down Expand Up @@ -226,6 +237,7 @@ export default defineComponent({
routeSet,
largeImageWarning,
typeList,
timeFilter,
};
},
});
Expand Down Expand Up @@ -265,6 +277,7 @@ export default defineComponent({
:selected-dataset-ids="[id]"
:running-pipelines="runningPipelines"
:read-only-mode="revisionNum !== undefined"
:time-filter="timeFilter"
/>
<ImportAnnotations
:button-options="buttonOptions"
Expand Down
Loading
Loading