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
8 changes: 8 additions & 0 deletions client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ interface PipeMetadata {
* it gets the first camera's list. Parsed from a `# Image List Keys:` header.
*/
imageListKeys?: string[];
/**
* KWIVER config keys (e.g. "depth_map:computer:ocv_stereo_disparity:calibration_file")
* that the dataset's stereo calibration file is bound to at run time. Parsed from a
* `# Calibration Keys: <k> [k...]` header. Pipes whose calibration consumer is not the
* conventional `measurer`/`calibration_reader` declare their own keys here; when unset
* the two conventional keys are used.
*/
calibrationKeys?: string[];
}

interface PipelineRuntimeParams {
Expand Down
15 changes: 14 additions & 1 deletion client/platform/desktop/backend/native/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ async function extractPipeMetadata(filePath: string): Promise<PipeMetadata> {
}

if (inDescription) {
if (/^#\s*$/.test(line) || /^#\s*=/.test(line) || /^#\s*(Input|Output|Requires\s+Calibration|Metadata\s+File|Image\s+List\s+Keys?):/i.test(line) || !line.startsWith('#')) {
if (/^#\s*$/.test(line) || /^#\s*=/.test(line) || /^#\s*(Input|Output|Requires\s+Calibration|Metadata\s+File|Image\s+List\s+Keys?|Calibration\s+Keys?):/i.test(line) || !line.startsWith('#')) {
inDescription = false;
} else {
fullDescription += ` ${line.replace(/^#\s*/, '').trim()}`;
Expand Down Expand Up @@ -273,6 +273,19 @@ async function extractPipeMetadata(filePath: string): Promise<PipeMetadata> {
metadata.imageListKeys = keys;
}
}

// `# Calibration Keys: <k> [k...]` binds the dataset's stereo calibration
// file to each listed KWIVER key. Needed because `$CONFIG{global:...}`
// indirection cannot receive `-s` overrides (macros expand at parse time,
// `-s` blocks are appended last), so a pipe must name the consuming process
// key directly.
const calibrationKeysMatch = line.match(/^#\s*Calibration\s+Keys?:\s*(.+)/i);
if (calibrationKeysMatch) {
const keys = calibrationKeysMatch[1].trim().split(/[\s,]+/).filter((k) => k);
if (keys.length) {
metadata.calibrationKeys = keys;
}
}
});
metadata.description = fullDescription.trim() || undefined;
} catch (error) {
Expand Down
10 changes: 8 additions & 2 deletions client/platform/desktop/backend/native/viame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,14 @@ async function runPipeline(
trackOutput = npath.join(jobWorkDir, outFiles[meta.multiCam.defaultDisplay]);

if (meta.multiCam.calibration) {
command.push(`-s measurer:calibration_file="${meta.multiCam.calibration}"`);
command.push(`-s calibration_reader:file="${meta.multiCam.calibration}"`);
// A pipe whose calibration consumer is not the conventional
// `measurer`/`calibration_reader` names its own keys via `# Calibration Keys:`.
const calibrationKeys = runPipelineArgs.pipeline.metadata?.calibrationKeys?.length
? runPipelineArgs.pipeline.metadata.calibrationKeys
: ['measurer:calibration_file', 'calibration_reader:file'];
calibrationKeys.forEach((key) => {
command.push(`-s ${key}="${meta.multiCam?.calibration}"`);
});
}
} else if (pipeline.type === stereoPipelineMarker) {
throw new Error('Attempting to run a multicam pipeline on non multicam data');
Expand Down
22 changes: 20 additions & 2 deletions server/dive_tasks/multicam_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,32 @@ def find_downloaded_calibration_file(directory: Path) -> Optional[Path]:
return sorted(matches, key=lambda p: (len(p.parts), str(p)))[0]


# Calibration consumers every stereo pipe is assumed to have unless it declares
# its own via a `# Calibration Keys:` header.
DEFAULT_CALIBRATION_KEYS = ('measurer:calibration_file', 'calibration_reader:file')


def stereo_calibration_keys(pipeline: Optional[PipelineDescription]) -> Tuple[str, ...]:
"""
KWIVER keys the dataset's calibration file binds to for this pipe.

A pipe opts out of the `measurer`/`calibration_reader` convention with a
`# Calibration Keys: <k> [k...]` header, naming the consuming process keys
directly (e.g. `depth_map:computer:ocv_stereo_disparity:calibration_file`).
"""
declared = ((pipeline or {}).get('metadata') or {}).get('calibrationKeys')
return tuple(declared) if declared else DEFAULT_CALIBRATION_KEYS


def append_stereo_calibration_kwiver_settings(
command: List[str],
calibration_path: Path,
pipeline: Optional[PipelineDescription] = None,
) -> None:
"""Append KWIVER settings used by desktop for stereoscopic calibration input."""
cal_path = shlex.quote(str(calibration_path))
command.append(f'-s measurer:calibration_file={cal_path}')
command.append(f'-s calibration_reader:file={cal_path}')
for key in stereo_calibration_keys(pipeline):
command.append(f'-s {shlex.quote(key)}={cal_path}')


def append_metadata_file_kwiver_settings(
Expand Down
19 changes: 18 additions & 1 deletion server/dive_tasks/pipeline_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def extract_pipe_metadata(file_path: Path) -> PipeMetadata:
or re.match(r'^#\s*=', line_raw)
or re.match(
r'^#\s*(Input|Output|Requires\s+Calibration|Metadata\s+File'
r'|Image\s+List\s+Keys?):',
r'|Image\s+List\s+Keys?|Calibration\s+Keys?):',
line_raw,
re.IGNORECASE,
)
Expand Down Expand Up @@ -261,6 +261,23 @@ def extract_pipe_metadata(file_path: Path) -> PipeMetadata:
if keys:
metadata["imageListKeys"] = keys

# `# Calibration Keys: <k> [k...]` binds the dataset's stereo
# calibration file to each listed KWIVER key. Needed because
# `$CONFIG{global:...}` indirection cannot receive `-s` overrides
# (macros expand at parse time, `-s` blocks are appended last), so a
# pipe must name the consuming process key directly.
calibration_keys_match = re.match(
r'^#\s*Calibration\s+Keys?:\s*(.+)', line_raw, re.IGNORECASE
)
if calibration_keys_match:
keys = [
k
for k in re.split(r'[\s,]+', calibration_keys_match.group(1).strip())
if k
]
if keys:
metadata["calibrationKeys"] = keys

if full_description_parts:
metadata["description"] = " ".join(full_description_parts)
else:
Expand Down
2 changes: 1 addition & 1 deletion server/dive_tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ def run_pipeline(self: Task, params: PipelineJob):
)
cal_path = find_downloaded_calibration_file(cal_dir)
if cal_path is not None:
append_stereo_calibration_kwiver_settings(command, cal_path)
append_stereo_calibration_kwiver_settings(command, cal_path, pipeline)
else:
manager.write(
f'Warning: calibration item {calibration_item_id} '
Expand Down
5 changes: 5 additions & 0 deletions server/dive_utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ class PipeMetadata(TypedDict):
# (one single-file list per camera). A `{cam}` placeholder is expanded per
# camera (1-based); a key without it gets the first camera's list.
imageListKeys: NotRequired[Optional[list[str]]]
# KWIVER config keys the dataset's stereo calibration file is bound to, parsed
# from `# Calibration Keys: <k> [k...]`. Pipes whose calibration consumer is
# not the conventional `measurer`/`calibration_reader` declare their own keys
# here; when unset the two conventional keys are used.
calibrationKeys: NotRequired[Optional[list[str]]]


class PipelineDescription(TypedDict):
Expand Down
30 changes: 30 additions & 0 deletions server/tests/test_multicam_pipeline.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from pathlib import Path

from dive_tasks.multicam_pipeline import (
DEFAULT_CALIBRATION_KEYS,
append_stereo_calibration_kwiver_settings,
build_multicam_kwiver_settings,
find_downloaded_calibration_file,
is_stereo_measurement_pipeline,
is_stereo_or_multicam_pipeline,
pipeline_requires_input,
stereo_calibration_keys,
)
from dive_utils import constants

Expand Down Expand Up @@ -54,6 +56,34 @@ def test_append_stereo_calibration_kwiver_settings():
]


def test_append_stereo_calibration_kwiver_settings_declared_keys():
"""A pipe's `# Calibration Keys:` header replaces the default keys."""
pipeline = {
'name': 'disparity',
'type': constants.StereoPipelineMarker,
'pipe': 'measurement_compute_rectified_disparity.pipe',
'metadata': {
'calibrationKeys': [
'depth_map:computer:ocv_stereo_disparity:calibration_file',
'stereo_pairing:cameras_directory',
]
},
}
command: list = []
append_stereo_calibration_kwiver_settings(command, Path('/work/cal.npz'), pipeline)
assert command == [
'-s depth_map:computer:ocv_stereo_disparity:calibration_file=/work/cal.npz',
'-s stereo_pairing:cameras_directory=/work/cal.npz',
]


def test_stereo_calibration_keys_defaults():
assert stereo_calibration_keys(None) == DEFAULT_CALIBRATION_KEYS
assert stereo_calibration_keys({'metadata': None}) == DEFAULT_CALIBRATION_KEYS
assert stereo_calibration_keys({'metadata': {'calibrationKeys': []}}) == DEFAULT_CALIBRATION_KEYS
assert stereo_calibration_keys({'metadata': {'calibrationKeys': ['a:b']}}) == ('a:b',)


def test_build_multicam_kwiver_settings_image_sequence(tmp_path: Path):
cameras = [
{'name': 'left', 'folder_id': 'l', 'media_type': constants.ImageSequenceType},
Expand Down
24 changes: 24 additions & 0 deletions server/tests/test_pipeline_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,27 @@ def test_extract_pipe_metadata_requires_calibration(tmp_path: Path):
assert metadata['description'] == 'stereo measurement'
assert metadata['inputType'] == 'TRACK'
assert metadata['outputType'] == 'TRACK'
assert metadata.get('calibrationKeys') is None


def test_extract_pipe_metadata_calibration_keys(tmp_path: Path):
pipe = tmp_path / 'measurement_disparity.pipe'
pipe.write_text(
'\n'.join(
[
'# Description: rectified disparity',
'# Requires Calibration: True',
'# Calibration Keys: depth_map:computer:ocv_stereo_disparity:calibration_file'
' stereo_pairing:cameras_directory',
]
)
)

metadata = extract_pipe_metadata(pipe)

assert metadata['calibrationKeys'] == [
'depth_map:computer:ocv_stereo_disparity:calibration_file',
'stereo_pairing:cameras_directory',
]
# The header must not bleed into the multi-line description.
assert metadata['description'] == 'rectified disparity'
Loading