Skip to content
Closed
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
10 changes: 9 additions & 1 deletion mrd-viz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@

Lightweight MRD inspection and preview VS Code extension.

The current focus is direct inspection of existing `.mrd` files rather than reconstruction orchestration. See `docs/TECHNICAL_DESIGN.md` for detailed project context and implementation plan.
The current focus is direct inspection of existing `.mrd` files rather than reconstruction orchestration. See `docs/TECHNICAL_DESIGN.md` for detailed project context and implementation plan.

## Install from the latest GitHub Release

Use the stable latest-release page:

<https://github.com/ismrmrd/mrd/releases/latest>

Download the platform VSIX attached to that release, then install it with **Extensions → … → Install from VSIX…** or `code --install-extension <file>.vsix`.
11 changes: 11 additions & 0 deletions mrd-viz/docs/TECHNICAL_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ Out of scope for now:

## Product Surface

### Functionality and rollout table

| Track | Before | After | Functions / UX created | Rollout priority |
| --- | --- | --- | --- | --- |
| D1: backend setup | Installed users had to reason through local Python setup and backend discovery failures manually. | The extension first uses a bundled backend binary when present; when that fails or is unavailable, **Set Up Backend Automatically…** builds a managed virtual environment and stores the machine-scoped `mrdViz.backendPath`. | Backend candidate planning, detailed probe failures, managed `backend-venv`, automatic setup command, interpreter picker, cleanup of partial venvs, and backend-missing guidance in the webview. | First researcher bundle; removes the biggest onboarding blocker. |
| Release: GitHub Release VSIX | Users needed local build knowledge or development-host setup to try MRD Viz. | Tag pushes build platform-specific VSIX artifacts with staged backend binaries and attach them to a GitHub Release; users start from the stable `/releases/latest` page and pick the platform VSIX. | `mrd_viz_release.yml` build matrix, PyInstaller backend staging, VSIX artifacts, release publishing, icon/license packaging, and release docs. | Same PR/release train as D1 so reviewers approve one deployable handoff. |
| D3: richer metadata grouping | Metadata exists, but users still scan summary panels and raw stream/JSON details for common interpretation patterns. | Metadata is scaffolded into file, header, image, acquisition, and stream groups, with the Groups tab becoming the first metadata view. | Typed metadata group model, grouped webview cards/tables, and a shell regression test, all derived from the existing backend payload without changing the backend contract. | Included with D1/release because it improves first-run researcher comprehension without adding install risk. |
| D2: multi-file / comparison workflow | Users open one `.mrd` file at a time and compare files manually across tabs. | Users should be able to classify and compare related files or batches in one workflow. | Planned multi-file selector, batch classification, side-by-side summary state, and comparison-oriented webview surfaces. | After initial feedback clarifies the common comparison tasks. |
| Marketplace | Users install from GitHub Release VSIX files. | Users install/update from the VS Code Marketplace once publisher, credential, and compliance work are complete. | Guarded Marketplace publish job, publisher checklist, `VSCE_PAT` activation path, optional Open VSX follow-up. | Later; do not block researcher access through GitHub Releases. |
| F10: feedback / follow-up loop | Feedback is ad hoc after manual testing. | Researchers can use the D1/release bundle while follow-up UX work continues in parallel. | Release notes, known-issues tracking, focused follow-up PRs for onboarding failures and newly observed MRD variants. | Starts with the first researcher release. |

### Why a VS Code plugin

`mrd-viz` will be a VS Code plugin because the initial users are technical researchers and operators who already inspect files, notebooks, scripts, and reconstruction outputs inside VS Code. A plugin keeps MRD inspection inside that existing development loop instead of forcing users to switch to a separate command-line-only workflow.
Expand Down
2 changes: 2 additions & 0 deletions mrd-viz/extension/mrd-viz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ This extension contributes the following settings:
## Commands

- `MRD Viz: Open File` — open the selected or picked `.mrd` file in MRD Viz.
- `MRD Viz: Set Up Backend` — create or rebuild a managed backend environment when the bundled backend is unavailable.
- `MRD Viz: Select Python Interpreter` — point `mrdViz.backendPath` at an existing backend environment.

## Known Issues

Expand Down
9 changes: 9 additions & 0 deletions mrd-viz/extension/mrd-viz/media/viewer.css
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,15 @@ li + li { margin-top: 4px; }
.metadata-section { display: grid; gap: 8px; }
.metadata-section h3 { margin: 0; font-size: 12px; font-weight: 650; }
.metadata-note { color: var(--mrd-muted); font-size: 12px; line-height: 1.4; }
.metadata-group {
display: grid;
gap: 8px;
padding: 9px;
border: 1px solid var(--mrd-line);
border-radius: 6px;
background: var(--mrd-panel-strong);
}
.metadata-group h3 { margin: 0; font-size: 12px; font-weight: 650; }
.metadata-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.metadata-table th,
.metadata-table td { padding: 4px 6px; border-bottom: 1px solid var(--mrd-line); text-align: left; vertical-align: top; }
Expand Down
43 changes: 43 additions & 0 deletions mrd-viz/extension/mrd-viz/media/viewer/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { payload } from './state';
import { stat, notice, section, addField, valueOrUnknown, formatList } from './dom';
import { buildMetadataGroups } from './metadataGroups';

function metadata() {
return payload.metadata || {};
Expand Down Expand Up @@ -129,6 +130,7 @@ export function renderShell() {
}

function renderMetadata() {
renderGroupedMetadata();
renderSummaryMetadata();
renderOrganizationMetadata();
renderStreamMetadata();
Expand All @@ -149,6 +151,47 @@ function activateTab(name) {
});
}

function renderGroupedMetadata() {
const root = document.getElementById('metadata-groups');
root.textContent = '';
const groups = buildMetadataGroups(payload);
if (!groups.length) {
appendEmpty(root, 'No grouped metadata is available for this file.');
return;
}

groups.forEach(function (group) {
const article = document.createElement('article');
article.className = 'metadata-group';

const heading = document.createElement('h3');
heading.textContent = group.title;
article.appendChild(heading);

const description = document.createElement('div');
description.className = 'metadata-note';
description.textContent = group.description;
article.appendChild(description);

if (group.fields.length) {
article.appendChild(definitionList(group.fields.map(function (field) {
return [field.label, field.value];
})));
}

group.tables.forEach(function (metadataTable) {
if (!metadataTable.rows.length) {
return;
}
const tableSection = section(metadataTable.title);
tableSection.appendChild(table(metadataTable.headers, metadataTable.rows));
article.appendChild(tableSection);
});

root.appendChild(article);
});
}

function renderSummaryMetadata() {
const root = document.getElementById('metadata-summary');
root.textContent = '';
Expand Down
214 changes: 214 additions & 0 deletions mrd-viz/extension/mrd-viz/media/viewer/metadataGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// D3 metadata grouping scaffold: derive user-facing metadata sections from the existing backend
// payload so richer labels/search can evolve without changing the backend contract first.

import { formatList, valueOrUnknown } from './dom';

export interface MetadataField {
label: string;
value: string;
}

export interface MetadataTable {
title: string;
headers: string[];
rows: string[][];
}

export interface MetadataGroup {
id: string;
title: string;
description: string;
fields: MetadataField[];
tables: MetadataTable[];
}

export function buildMetadataGroups(payload): MetadataGroup[] {
return [
buildFileGroup(payload),
buildHeaderGroup(payload),
buildImageGroup(payload),
buildAcquisitionGroup(payload),
buildStreamGroup(payload),
].filter(group => group.fields.length > 0 || group.tables.some(table => table.rows.length > 0));
}

function buildFileGroup(payload): MetadataGroup {
return {
id: 'file',
title: 'File',
description: 'Open state and high-level classification.',
fields: [
field('class', payload.file_class),
field('classification reliable', payload.file_class_reliable),
field('display mode', payload.display_mode),
field('schema version', payload.schema_version),
field('file size bytes', payload.file_size_bytes),
],
tables: (payload.warnings || []).length ? [{
title: 'Warnings',
headers: ['warning'],
rows: payload.warnings.map(warning => [formatValue(warning)]),
}] : [],
};
}

function buildHeaderGroup(payload): MetadataGroup {
const summary = payload.summary || {};
return {
id: 'header',
title: 'Header',
description: 'Encoding and reconstructed-space summary from the MRD header.',
fields: [
field('encoding count', summary.encoding_count),
field('encoded matrix', summary.encoded_matrix),
field('recon matrix', summary.recon_matrix),
field('encoded FOV mm', summary.encoded_fov_mm),
field('recon FOV mm', summary.recon_fov_mm),
],
tables: [],
};
}

function buildImageGroup(payload): MetadataGroup {
const imageItems = images(payload);
return {
id: 'images',
title: 'Images',
description: 'Image-item organization for slice/type/series scanning.',
fields: [
field('images', imageItems.length),
field('unique slices', uniqueCount(imageItems.map(image => headValue(image, 'slice')))),
field('unique image types', uniqueCount(imageItems.map(image => headValue(image, 'image_type')))),
field('unique series', uniqueCount(imageItems.map(image => headValue(image, 'image_series_index')))),
field('unique shapes', uniqueCount(imageItems.map(shapeKey))),
field('unique dtypes', uniqueCount(imageItems.map(image => image.dtype))),
],
tables: [
distributionTable('Slice distribution', 'slice', imageItems.map(image => headValue(image, 'slice'))),
distributionTable('Image type distribution', 'image type', imageItems.map(image => headValue(image, 'image_type'))),
distributionTable('Shape / dtype consistency', 'shape / dtype', imageItems.map(image => shapeKey(image) + ' / ' + formatValue(image.dtype))),
],
};
}

function buildAcquisitionGroup(payload): MetadataGroup {
const acquisitionItems = acquisitions(payload);
const firstAcquisition = acquisitionItems[0] || {};
const idx = firstAcquisition.idx || {};
return {
id: 'acquisitions',
title: 'Acquisitions',
description: 'Raw acquisition examples and encoding counters returned by the backend.',
fields: [
field('examples returned', acquisitionItems.length),
field('first stream index', firstAcquisition.stream_index),
field('first shape', firstAcquisition.data_shape),
field('first dtype', firstAcquisition.dtype),
field('first flags', firstAcquisition.flags),
field('first scan counter', firstAcquisition.scan_counter),
field('first slice', idx.slice),
field('first k-space step 1', idx.kspace_encode_step_1),
field('first k-space step 2', idx.kspace_encode_step_2),
],
tables: [],
};
}

function buildStreamGroup(payload): MetadataGroup {
const stream = payload.stream || {};
const itemRows = Object.entries(stream.item_counts || {})
.sort(function (left, right) { return left[0].localeCompare(right[0]); })
.map(function (entry) { return [entry[0], formatValue(entry[1])]; });

return {
id: 'stream',
title: 'Stream',
description: 'Stream item counts and metadata sample coverage.',
fields: [
field('images', stream.image_count),
field('acquisitions', stream.acquisition_count),
field('waveforms', stream.waveform_count),
field('other items', stream.other_count),
field('partial stream read', stream.partial),
field('thumbnail payload truncated', payload.mosaic && payload.mosaic.truncated),
field('returned thumbnails', payload.mosaic && payload.mosaic.thumbnails && payload.mosaic.thumbnails.length),
field('image metadata entries', images(payload).length),
field('acquisition examples', acquisitions(payload).length),
field('waveform entries', waveforms(payload).length),
field('other item entries', otherItems(payload).length),
],
tables: itemRows.length ? [{
title: 'Stream item counts',
headers: ['item type', 'count'],
rows: itemRows,
}] : [],
};
}

function field(label: string, value): MetadataField {
return { label, value: formatValue(value) };
}

function metadata(payload) {
return payload.metadata || {};
}

function images(payload) {
return metadata(payload).images || [];
}

function acquisitions(payload) {
return metadata(payload).acquisitions || [];
}

function waveforms(payload) {
return metadata(payload).waveforms || [];
}

function otherItems(payload) {
return metadata(payload).other_items || [];
}

function headValue(image, key: string) {
return image && image.head ? image.head[key] : undefined;
}

function shapeKey(image) {
return formatList(image && image.data_shape) || 'unknown';
}

function distributionTable(title: string, label: string, values): MetadataTable {
return {
title,
headers: [label, 'count'],
rows: distribution(values),
};
}

function distribution(values): string[][] {
const counts = new Map<string, number>();
values.forEach(function (value) {
const key = formatValue(value);
counts.set(key, (counts.get(key) || 0) + 1);
});

return Array.from(counts.entries()).sort(function (left, right) {
return left[0].localeCompare(right[0], undefined, { numeric: true });
}).map(function (entry) {
return [entry[0], String(entry[1])];
});
}

function uniqueCount(values): number {
return distribution(values).length;
}

function formatValue(value): string {
if (Array.isArray(value)) {
return formatList(value);
}
if (value && typeof value === 'object') {
return JSON.stringify(value);
}
return valueOrUnknown(value);
}
Loading