From b43c28a4ec2ffa1dc9a23e7092b2e4b92bcd6137 Mon Sep 17 00:00:00 2001 From: MarshallXu Date: Sun, 17 May 2026 17:29:37 +1000 Subject: [PATCH 1/3] Enhance `patch.export_raw` command to export selected patch arrays and metadata to a compressed `.npz` archive with detailed contents and conditions for annotation and segmentation inclusion. --- src/mipview/control/README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/mipview/control/README.md b/src/mipview/control/README.md index 692e0e1..ff67789 100644 --- a/src/mipview/control/README.md +++ b/src/mipview/control/README.md @@ -274,7 +274,10 @@ mipview-ctl patch select ### `patch.export_raw` -Function: Export selected patch arrays and metadata, including annotation or segmentation patch arrays when available. +Function: Export selected patch arrays and metadata to a compressed `.npz` +archive. When an active annotation mask or file-backed active segmentation is +available, their patch arrays are exported with the same selected patch bounds as +the image patch. CLI usage: @@ -294,6 +297,32 @@ Parameters: | --- | --- | --- | | `path` | `str` | Output `.npz` path. The parent directory must exist and be writable. | +The `.npz` archive always includes: + +| Key | Description | +| --- | --- | +| `image_patch` | Raw selected image patch array. | +| `bounds` | Half-open source voxel bounds as `[[x0, x1], [y0, y1], [z0, z1]]`. | +| `patch_size` | Current requested patch size as `[sx, sy, sz]`. | +| `patch_center` | Current patch center voxel as `[x, y, z]`, when available. | +| `affine` | Patch affine. | +| `voxel_spacing` | Patch voxel spacing. | +| `source_image_path` | Loaded source image path, or an empty string. | +| `viewer_state_json` | JSON string containing the exported viewer state. | + +The archive conditionally includes: + +| Key | Included when | +| --- | --- | +| `annotation_patch` | An active annotation mask exists. The patch is extracted in voxel space using the same bounds as `image_patch`. | +| `segmentation_patch` | The active segmentation is a loaded file segmentation. Annotation-backed segmentations are not duplicated here. | + +The command response includes `annotation_included` and `segmentation_included` +booleans so clients can tell which optional arrays were written. + +This command exports selected patch arrays only. It does not provide standalone +full-volume annotation or segmentation `.npz` export commands. + Example: ```bash From ce1f18f3e1fa3694be966fd23529607aa1edc5a7 Mon Sep 17 00:00:00 2001 From: MarshallXu Date: Sun, 17 May 2026 17:36:18 +1000 Subject: [PATCH 2/3] Enhance CLI argument parser with detailed help descriptions and structured command options for improved usability. --- src/mipview/control/cli.py | 333 +++++++++++++++++++++++++++++++------ 1 file changed, 279 insertions(+), 54 deletions(-) diff --git a/src/mipview/control/cli.py b/src/mipview/control/cli.py index 923148a..bd084c9 100644 --- a/src/mipview/control/cli.py +++ b/src/mipview/control/cli.py @@ -11,6 +11,13 @@ from mipview.control.ipc_server import default_socket_path +class _HelpFormatter( + argparse.ArgumentDefaultsHelpFormatter, + argparse.RawDescriptionHelpFormatter, +): + """Keep examples readable while still showing defaults.""" + + def main(argv: list[str] | None = None) -> int: parser = _build_parser() args = parser.parse_args(argv) @@ -35,84 +42,302 @@ def main(argv: list[str] | None = None) -> int: def _build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(prog="mipview-ctl") + parser = argparse.ArgumentParser( + prog="mipview-ctl", + description=( + "Control an already-running MipView GUI through its local IPC socket. " + "Commands return structured JSON on stdout." + ), + epilog=( + "Typical workflow:\n" + " mipview-ctl status\n" + " mipview-ctl patch size 64 64 10\n" + " mipview-ctl patch center 120 80 45\n" + " mipview-ctl patch export-raw ./patch_raw.npz\n" + " mipview-ctl projection mode minip\n" + " mipview-ctl projection save axial ./patch_axial.png --annotation-preview\n" + " mipview-ctl annotation save ./annotation_mask.nii.gz\n\n" + "Use 'mipview-ctl GROUP --help' or 'mipview-ctl GROUP COMMAND --help' " + "for command details." + ), + formatter_class=_HelpFormatter, + ) parser.add_argument( "--socket", + metavar="SOCKET", default=str(default_socket_path()), - help="Path to the MipView Unix domain socket.", + help="Path to the running MipView IPC Unix socket.", + ) + subparsers = parser.add_subparsers( + dest="group", + required=True, + metavar="COMMAND_GROUP", ) - subparsers = parser.add_subparsers(dest="group", required=True) - subparsers.add_parser("status") + subparsers.add_parser( + "status", + help="Show loaded image, cursor, patch, annotation, and segmentation state.", + description="Show a concise JSON summary of the running MipView session.", + formatter_class=_HelpFormatter, + ) - send_parser = subparsers.add_parser("send") - send_parser.add_argument("command") - send_parser.add_argument("--args-json", default="{}") + send_parser = subparsers.add_parser( + "send", + help="Advanced: send a raw command name with JSON arguments.", + description=( + "Advanced direct IPC command dispatch. Use this for commands that do " + "not have a dedicated CLI wrapper." + ), + formatter_class=_HelpFormatter, + ) + send_parser.add_argument( + "command", + metavar="COMMAND", + help="Registered command name, e.g. cursor.move.", + ) + send_parser.add_argument( + "--args-json", + metavar="JSON", + default="{}", + help="JSON object containing command arguments.", + ) - viewer_parser = subparsers.add_parser("viewer") + viewer_parser = subparsers.add_parser( + "viewer", + help="Save screenshots or structured viewer state.", + description="Viewer export commands for screenshots and machine-readable state.", + formatter_class=_HelpFormatter, + ) viewer_subparsers = viewer_parser.add_subparsers(dest="viewer_command", required=True) - viewer_screenshot = viewer_subparsers.add_parser("screenshot") - viewer_screenshot.add_argument("path") - viewer_state = viewer_subparsers.add_parser("state") - viewer_state.add_argument("path") + viewer_screenshot = viewer_subparsers.add_parser( + "screenshot", + help="Save a full-window screenshot.", + description="Save a full-window screenshot and return viewer state metadata.", + formatter_class=_HelpFormatter, + ) + viewer_screenshot.add_argument("path", metavar="PATH", help="Output image path.") + viewer_state = viewer_subparsers.add_parser( + "state", + help="Write structured viewer state JSON.", + description="Write the returned viewer state data object to a JSON file.", + formatter_class=_HelpFormatter, + ) + viewer_state.add_argument("path", metavar="PATH", help="Output JSON path.") - cursor_parser = subparsers.add_parser("cursor") + cursor_parser = subparsers.add_parser( + "cursor", + help="Move the cursor by voxel coordinates.", + description="Cursor commands use source voxel coordinates as X Y Z integers.", + formatter_class=_HelpFormatter, + ) cursor_subparsers = cursor_parser.add_subparsers(dest="cursor_command", required=True) - cursor_move = cursor_subparsers.add_parser("move") - cursor_move.add_argument("x", type=int) - cursor_move.add_argument("y", type=int) - cursor_move.add_argument("z", type=int) - - patch_parser = subparsers.add_parser("patch") + cursor_move = cursor_subparsers.add_parser( + "move", + help="Move cursor to an in-bounds voxel.", + description="Move the cursor to an in-bounds source voxel coordinate.", + formatter_class=_HelpFormatter, + ) + cursor_move.add_argument("x", metavar="X", type=int, help="X voxel coordinate.") + cursor_move.add_argument("y", metavar="Y", type=int, help="Y voxel coordinate.") + cursor_move.add_argument("z", metavar="Z", type=int, help="Z voxel coordinate.") + + patch_parser = subparsers.add_parser( + "patch", + help="Select, save, and export current image patches.", + description=( + "Patch commands operate on the fixed-size patch centered at the current " + "patch center or cursor." + ), + formatter_class=_HelpFormatter, + ) patch_subparsers = patch_parser.add_subparsers(dest="patch_command", required=True) - patch_size = patch_subparsers.add_parser("size") - patch_size.add_argument("sx", type=int) - patch_size.add_argument("sy", type=int) - patch_size.add_argument("sz", type=int) - patch_center = patch_subparsers.add_parser("center") - patch_center.add_argument("x", type=int) - patch_center.add_argument("y", type=int) - patch_center.add_argument("z", type=int) - patch_subparsers.add_parser("select") - patch_export_raw = patch_subparsers.add_parser("export-raw") - patch_export_raw.add_argument("path") - patch_save = patch_subparsers.add_parser("save") - patch_save.add_argument("path") - - projection_parser = subparsers.add_parser("projection") + patch_size = patch_subparsers.add_parser( + "size", + help="Set patch size in voxels.", + description="Set the fixed patch size in voxel units; all values must be positive.", + formatter_class=_HelpFormatter, + ) + patch_size.add_argument("sx", metavar="SX", type=int, help="Patch size along X.") + patch_size.add_argument("sy", metavar="SY", type=int, help="Patch size along Y.") + patch_size.add_argument("sz", metavar="SZ", type=int, help="Patch size along Z.") + patch_center = patch_subparsers.add_parser( + "center", + help="Set patch center voxel.", + description="Set the patch center to an in-bounds source voxel coordinate.", + formatter_class=_HelpFormatter, + ) + patch_center.add_argument("x", metavar="X", type=int, help="X voxel coordinate.") + patch_center.add_argument("y", metavar="Y", type=int, help="Y voxel coordinate.") + patch_center.add_argument("z", metavar="Z", type=int, help="Z voxel coordinate.") + patch_subparsers.add_parser( + "select", + help="Extract and store the current patch.", + description="Extract and store the current patch using current patch bounds.", + formatter_class=_HelpFormatter, + ) + patch_export_raw = patch_subparsers.add_parser( + "export-raw", + help="Export patch arrays and metadata to .npz.", + description=( + "Export a compressed .npz with image_patch, bounds, patch_size, " + "patch_center, affine, voxel_spacing, source_image_path, and " + "viewer_state_json. Includes annotation_patch when an active annotation " + "exists and segmentation_patch when the active segmentation is file-backed." + ), + formatter_class=_HelpFormatter, + ) + patch_export_raw.add_argument("path", metavar="PATH", help="Output .npz path.") + patch_save = patch_subparsers.add_parser( + "save", + help="Save selected image patch as NIfTI.", + description="Save the selected image patch as .nii or .nii.gz.", + formatter_class=_HelpFormatter, + ) + patch_save.add_argument("path", metavar="PATH", help="Output NIfTI path.") + + projection_parser = subparsers.add_parser( + "projection", + help="Set MIP/MinIP mode and save patch projections.", + description="Projection commands operate on the selected patch.", + formatter_class=_HelpFormatter, + ) projection_subparsers = projection_parser.add_subparsers( dest="projection_command", required=True, ) - projection_mode = projection_subparsers.add_parser("mode") - projection_mode.add_argument("mode") - projection_save = projection_subparsers.add_parser("save") - projection_save.add_argument("view") - projection_save.add_argument("path") + projection_mode = projection_subparsers.add_parser( + "mode", + help="Set image projection mode.", + description="Set image projection mode for later projection saves.", + formatter_class=_HelpFormatter, + ) + projection_mode.add_argument( + "mode", + metavar="MODE", + help="Image projection mode: mip or minip.", + ) + projection_save = projection_subparsers.add_parser( + "save", + help="Save one patch projection image.", + description=( + "Save one grayscale patch projection for VIEW. The image projection " + "uses the current mode. With --annotation-preview, overlay the active " + "annotation MIP; annotation projection is always MIP." + ), + formatter_class=_HelpFormatter, + ) + projection_save.add_argument( + "view", + metavar="VIEW", + help="Projection view: axial, coronal, or sagittal.", + ) + projection_save.add_argument( + "path", + metavar="PATH", + help="Output .png, .jpg, or .jpeg path.", + ) projection_save.add_argument( "--annotation-preview", action="store_true", - help="Overlay the active annotation MIP onto the exported projection.", + help="Overlay active annotation MIP onto the image projection.", ) - annotation_parser = subparsers.add_parser("annotation") + annotation_parser = subparsers.add_parser( + "annotation", + help="Create, edit, and save voxel-space annotations.", + description=( + "Annotation commands modify the active voxel-space annotation mask. " + "Stroke points are voxel coordinates, not screen pixels." + ), + formatter_class=_HelpFormatter, + ) annotation_subparsers = annotation_parser.add_subparsers( dest="annotation_command", required=True, ) - annotation_create = annotation_subparsers.add_parser("create") - annotation_create.add_argument("--label", type=int, default=1) - annotation_paint = annotation_subparsers.add_parser("paint-stroke") - annotation_paint.add_argument("--label", type=int, required=True) - annotation_paint.add_argument("--radius", type=int, required=True) - annotation_paint.add_argument("--view", required=True) - annotation_paint.add_argument("--points", required=True) - annotation_erase = annotation_subparsers.add_parser("erase-stroke") - annotation_erase.add_argument("--radius", type=int, required=True) - annotation_erase.add_argument("--view", required=True) - annotation_erase.add_argument("--points", required=True) - annotation_save = annotation_subparsers.add_parser("save") - annotation_save.add_argument("path") + annotation_create = annotation_subparsers.add_parser( + "create", + help="Create or enable active annotation mask.", + description="Create or enable the active annotation mask and set the active label.", + formatter_class=_HelpFormatter, + ) + annotation_create.add_argument( + "--label", + metavar="LABEL", + type=int, + default=1, + help="Active label to paint.", + ) + annotation_paint = annotation_subparsers.add_parser( + "paint-stroke", + help="Paint a voxel-space stroke.", + description=( + "Paint a stroke into the active annotation mask. --points is a JSON " + "file containing voxel-space points like [[x,y,z], [x,y,z]]." + ), + formatter_class=_HelpFormatter, + ) + annotation_paint.add_argument( + "--label", + metavar="LABEL", + type=int, + required=True, + help="Positive label value to paint.", + ) + annotation_paint.add_argument( + "--radius", + metavar="RADIUS", + type=int, + required=True, + help="Brush radius in voxels.", + ) + annotation_paint.add_argument( + "--view", + metavar="VIEW", + required=True, + help="Stroke disk plane: axial, coronal, or sagittal.", + ) + annotation_paint.add_argument( + "--points", + metavar="POINTS_JSON", + required=True, + help="Path to JSON file of voxel-space stroke points.", + ) + annotation_erase = annotation_subparsers.add_parser( + "erase-stroke", + help="Erase a voxel-space stroke.", + description=( + "Erase a stroke from the active annotation mask. --points is a JSON " + "file containing voxel-space points like [[x,y,z], [x,y,z]]." + ), + formatter_class=_HelpFormatter, + ) + annotation_erase.add_argument( + "--radius", + metavar="RADIUS", + type=int, + required=True, + help="Brush radius in voxels.", + ) + annotation_erase.add_argument( + "--view", + metavar="VIEW", + required=True, + help="Stroke disk plane: axial, coronal, or sagittal.", + ) + annotation_erase.add_argument( + "--points", + metavar="POINTS_JSON", + required=True, + help="Path to JSON file of voxel-space stroke points.", + ) + annotation_save = annotation_subparsers.add_parser( + "save", + help="Save active annotation mask as NIfTI.", + description="Save the active full-volume annotation mask as .nii or .nii.gz.", + formatter_class=_HelpFormatter, + ) + annotation_save.add_argument("path", metavar="PATH", help="Output NIfTI path.") return parser From 23309aca9316769cef5c996bb66588bfeadb04c1 Mon Sep 17 00:00:00 2001 From: MarshallXu Date: Sun, 17 May 2026 17:36:33 +1000 Subject: [PATCH 3/3] bump version to 0.4.3 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 97aa4cd..b3e7607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mipview" -version = "0.4.2" +version = "0.4.3" description = "A lightweight Linux-first NIfTI viewer for patch-based inspection workflows." readme = "README.md" requires-python = ">=3.11"