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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Do not duplicate long policy text across human docs. Human docs may explain arch

Every public library function under `+labkit/+ui`, `+labkit/+dta`, and `+labkit/+biosignal` must document its app-facing call contract immediately after the function declaration. Include inputs, outputs, options/spec fields, defaults, legal values, and examples where useful.

Private package helpers must include concise top-of-file implementation contracts: expected caller, input/output shapes, side effects, and non-obvious assumptions.
Private and app-owned package helpers must include concise top-of-file implementation contracts: expected caller, input/output shapes, side effects, and non-obvious assumptions.

## Sensitive Sample Data

Expand Down
18 changes: 11 additions & 7 deletions apps/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ Apps are first-class deliverables. Do not treat them as examples for a hidden pl
- Image apps with custom preview scroll, drawing, ROI, scale-bar, or other axes interaction should create a `labkit.ui.tool.createRuntime` and pass that runtime into reusable tools. Do not set image-tool `WindowScrollWheelFcn`, `WindowButtonMotionFcn`, `WindowButtonUpFcn`, or axes `ButtonDownFcn` directly in app code.
- DTA-backed apps use `labkit.dta.*` for discovery, loading, sessions, pulse detection, and parsed curve/table access.
- Biosignal-backed apps use `labkit.biosignal.*` for recording loading, channel extraction, waveform processing, events, segments, measurements, and group comparisons.
- Do not create app-specific public helper packages to make local workflow code look reusable.
- App-owned private helpers are acceptable only when they stay under the owning app tree and do not become public reusable APIs.
- Callback-heavy migrated apps may use an app-private runner to keep the public
launcher small, but the runner remains app-owned production code and must not
become a reusable facade.
- When a public app file grows large, prefer moving GUI-free app-owned calculations, export builders, formatting utilities, and deterministic image/signal transforms into `apps/<family>/<app_slug>/private/`.
- Use `apps/<family>/private/` only for helpers that are genuinely shared by multiple apps in that family.
- Do not create app-specific helper packages outside the owning app tree, and do not move app-specific helper code into `+labkit`.
- When an app needs extracted helpers, prefer an app-owned package under the app folder. The package name should match the app folder slug, such as `apps/image_measurement/batch_crop/+batch_crop/`.
- New extracted app helper code should use component packages such as `+ui`,
`+state`, `+ops`, `+view`, `+export`, and `+io` as needed. Do not use a fixed
`+app` namespace; the app folder already provides ownership context, while a
shared `+app` package name creates MATLAB package-resolution ambiguity.
- Callback-heavy migrated apps should move app-owned production code into these
package components instead of adding new `private/` runners or string-dispatch
workflow adapters.
- When a public app file grows large, prefer moving GUI-free app-owned calculations, export builders, formatting utilities, deterministic image/signal transforms, and focused control construction into `apps/<family>/<app_slug>/+<app_slug>/...`.
- Do not add new `apps/<family>/private/` helpers unless the helper is genuinely shared by multiple apps in that family and the user approves that family-level boundary.
- Keep the public app entry point responsible for GUI state, callbacks, user alerts, app workflow order, debug launch routing, and user-facing log wording.

## Documentation Sync
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
% App-owned export manifest helper. Expected caller: batch-crop app export
% callback and workflow tests. Input is a result struct vector. Output is a
% callback and package tests. Input is a result struct vector. Output is a
% table suitable for CSV export and has no file side effects.
function T = buildBatchCropManifest(results)
%BUILDBATCHCROPMANIFEST Build a per-image crop/export manifest table.
% Expected caller: labkit_BatchImageCrop_app and batchImageCropWorkflow.
% Input is a struct vector returned by batchCropImage or writeBatchCropOutputs.
function T = buildManifest(results)
%BUILDMANIFEST Build a per-image crop/export manifest table.
% Expected caller: labkit_BatchImageCrop_app and batch_crop package tests.
% Input is a struct vector returned by cropImage or writeOutputs.
% Output columns describe source/output files, crop geometry, and status.

if isempty(results)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
% App-owned batch crop export helper. Expected caller: batch-crop app export
% callback and workflow tests. Inputs are crop items and export options. This
% callback and package tests. Inputs are crop items and export options. This
% helper writes cropped images and a CSV manifest.
function payload = writeBatchCropOutputs(items, opts)
%WRITEBATCHCROPOUTPUTS Write cropped images and a manifest CSV.
% Expected caller: labkit_BatchImageCrop_app and batchImageCropWorkflow. Items
function payload = writeOutputs(items, opts)
%WRITEOUTPUTS Write cropped images and a manifest CSV.
% Expected caller: labkit_BatchImageCrop_app and batch_crop package tests. Items
% must contain path, image, angleDeg, and centerXY fields. Options contain
% outputFolder, format, cropWidth, cropHeight, and fillMode/fillValue.

Expand All @@ -22,16 +22,16 @@
end

outputFormat = normalizeOutputFormat(optionValue(opts, 'format', 'PNG'));
results = repmat(emptyBatchCropResult(), numel(items), 1);
results = repmat(batch_crop.state.emptyResult(), numel(items), 1);
reservedPaths = strings(0, 1);
for k = 1:numel(items)
result = emptyBatchCropResult();
result = batch_crop.state.emptyResult();
result.sourcePath = string(items(k).path);
try
cropOpts = opts;
cropOpts.angleDeg = items(k).angleDeg;
cropOpts.centerXY = items(k).centerXY;
crop = batchCropImage(items(k).image, cropOpts);
crop = batch_crop.ops.cropImage(items(k).image, cropOpts);
outputPath = uniqueBatchCropOutputPath(outputFolder, ...
string(items(k).path), outputFormat.extension, reservedPaths, "_crop");
reservedPaths(end+1, 1) = outputPath; %#ok<AGROW>
Expand All @@ -50,7 +50,7 @@
results(k) = result;
end

manifest = buildBatchCropManifest(results);
manifest = batch_crop.export.buildManifest(results);
manifestPath = uniqueBatchCropOutputPath(outputFolder, ...
"batch_crop_manifest.csv", ".csv", reservedPaths, "");
writetable(manifest, char(manifestPath));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
% App-owned image dialog filter helper. Expected caller: batch-crop app open
% callback. Output is a uigetfile filter cell array and has no side effects.
function filter = batchCropImageDialogFilter()
%BATCHCROPIMAGEDIALOGFILTER Return supported image file dialog filters.
function filter = imageDialogFilter()
%IMAGEDIALOGFILTER Return supported image file dialog filters.

filter = {'*.png;*.jpg;*.jpeg;*.tif;*.tiff;*.bmp', ...
'Image files (*.png, *.jpg, *.jpeg, *.tif, *.tiff, *.bmp)'};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
% App-owned selected-file normalization helper. Expected caller:
% labkit_BatchImageCrop_app and batchImageCropWorkflow. Inputs are raw
% labkit_BatchImageCrop_app and batch_crop package tests. Inputs are raw
% uigetfile values. Output is a sorted string column and has no file effects.
function paths = selectedBatchCropImagePaths(files, folder)
%SELECTEDBATCHCROPIMAGEPATHS Normalize manually selected image paths.
% Expected caller: labkit_BatchImageCrop_app and batchImageCropWorkflow. Inputs
function paths = selectedImagePaths(files, folder)
%SELECTEDIMAGEPATHS Normalize manually selected image paths.
% Expected caller: labkit_BatchImageCrop_app and batch_crop package tests. Inputs
% are raw uigetfile file/folder values. Output validates image extensions and
% sorts by display filename.

Expand Down Expand Up @@ -36,7 +36,7 @@
function paths = sortBatchCropPathsByName(paths)
names = strings(numel(paths), 1);
for k = 1:numel(paths)
names(k) = lower(string(displayNameFromPath(paths(k))));
names(k) = lower(string(batch_crop.view.displayNameFromPath(paths(k))));
end
[~, order] = sort(names);
paths = paths(order);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
% App-owned fixed-size crop helper. Expected caller: batchCropImage. Inputs
% App-owned fixed-size crop helper. Expected caller: cropImage. Inputs
% are a rotated canvas, center coordinates in canvas pixels, crop size, and
% scalar fill value. Output preserves class and pads out-of-bounds regions.
function cropped = cropCanvasFixedSize(canvas, centerXY, cropSize, fillValue)
%CROPCANVASFIXEDSIZE Crop a fixed pixel rectangle from a canvas.
% Expected caller: batchCropImage. Inputs are a 2-D or 3-D image canvas,
% Expected caller: cropImage. Inputs are a 2-D or 3-D image canvas,
% centerXY as [x y], cropSize as [width height], and a scalar fill value.
% Output is exactly height-by-width pixels and has no file side effects.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
% App-owned microscope image crop helper. Expected caller: batch-crop app
% callbacks and workflow tests. Inputs are an image array and crop options.
% callbacks and package tests. Inputs are an image array and crop options.
% Output is a result struct with the cropped image and crop metadata.
function result = batchCropImage(imageData, opts)
%BATCHCROPIMAGE Rotate an image canvas and crop a fixed pixel rectangle.
% Expected caller: labkit_BatchImageCrop_app and batchImageCropWorkflow.
function result = cropImage(imageData, opts)
%CROPIMAGE Rotate an image canvas and crop a fixed pixel rectangle.
% Expected caller: labkit_BatchImageCrop_app and batch_crop package tests.
% Inputs are an image array and opts with cropWidth, cropHeight, angleDeg,
% centerXY, fillMode, or fillValue. Output preserves image class and returns
% exactly cropHeight-by-cropWidth pixels, padding when the crop crosses canvas
Expand All @@ -19,17 +19,17 @@
angleDeg = double(optionValue(opts, 'angleDeg', 0));
fillValue = fillValueForImage(imageData, opts);

[canvas, mask] = rotateImageCanvas(imageData, angleDeg, fillValue);
[canvas, mask] = batch_crop.ops.rotateCanvas(imageData, angleDeg, fillValue);
centerXY = optionValue(opts, 'centerXY', []);
if isempty(centerXY) || numel(centerXY) ~= 2 || any(~isfinite(double(centerXY)))
centerXY = [(size(canvas, 2) + 1) / 2, (size(canvas, 1) + 1) / 2];
else
centerXY = double(centerXY(:)).';
end

cropped = cropCanvasFixedSize(canvas, centerXY, [cropWidth, cropHeight], fillValue);
cropped = batch_crop.ops.cropCanvasFixedSize(canvas, centerXY, [cropWidth, cropHeight], fillValue);

result = emptyBatchCropResult();
result = batch_crop.state.emptyResult();
result.ok = true;
result.status = "cropped";
result.image = cropped;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
% App-owned image rotation helper. Expected caller: batchCropImage. Inputs are
% App-owned image rotation helper. Expected caller: cropImage. Inputs are
% image data, angle in degrees, and fill value. Output is a loose rotated
% canvas with background filled consistently for grayscale/RGB images.
function [canvas, mask] = rotateImageCanvas(imageData, angleDeg, fillValue)
function [canvas, mask] = rotateCanvas(imageData, angleDeg, fillValue)
%ROTATEIMAGECANVAS Rotate an image without resizing its pixel scale.
% Expected caller: batchCropImage. The output canvas may be larger than the
% Expected caller: cropImage. The output canvas may be larger than the
% input when angleDeg is nonzero. The implementation uses base MATLAB
% interpolation so CI does not require Image Processing Toolbox.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
% App-owned state factory. Expected caller: batch-crop app startup and reset
% callbacks. Output is a scalar item struct for one loaded image and has no
% side effects.
function item = emptyBatchCropItem()
%EMPTYBATCHCROPITEM Return an empty loaded-image crop item.
function item = emptyItem()
%EMPTYITEM Return an empty loaded-image crop item.

item = struct( ...
'path', "", ...
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
% App-owned result factory. Expected caller: batch-crop calculation/export
% helpers. Output is a scalar result struct with stable fields and no side
% effects.
function result = emptyBatchCropResult()
%EMPTYBATCHCROPRESULT Return an empty batch crop result struct.
% Expected caller: batchCropImage and writeBatchCropOutputs. Output fields are
function result = emptyResult()
%EMPTYRESULT Return an empty batch crop result struct.
% Expected caller: cropImage and writeOutputs. Output fields are
% stable so result arrays can be preallocated before crop/export work.

result = struct( ...
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
% App-owned image loading helper. Expected caller: batch-crop app open-files
% callback. Input is a string vector of image paths. Output is an item struct
% vector with images loaded through imread.
function items = readBatchCropItems(paths)
%READBATCHCROPITEMS Load selected image paths into crop item structs.
function items = readItems(paths)
%READITEMS Load selected image paths into crop item structs.

items = repmat(emptyBatchCropItem(), numel(paths), 1);
items = repmat(batch_crop.state.emptyItem(), numel(paths), 1);
for k = 1:numel(paths)
img = imread(paths(k));
items(k).path = string(paths(k));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
% labkit_BatchImageCrop_app during startup. Inputs are shell tab grids,
% initial output folder, and callback handles. Output is a struct of UI
% handles. This helper creates controls only and has no file side effects.
function controls = createBatchCropControls(layFA, laySR, layLog, initialOutputFolder, callbacks)
%CREATEBATCHCROPCONTROLS Create controls for the batch image crop app.
function controls = createControls(layFA, laySR, layLog, initialOutputFolder, callbacks)
%CREATECONTROLS Create controls for the batch image crop app.

filePanel = labkit.ui.view.section(layFA, 'Images', 1, [5 2], ...
struct('rowHeight', {{'fit', 'fit', 105, 'fit', 'fit'}}, ...
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
% App-owned detail view helper. Expected caller: batch-crop app refreshSummary.
% Inputs are app state, current index, crop size, and fill mode. Output is a
% cell vector of text lines and has no side effects.
function lines = batchCropDetailLines(state, currentIndex, cropWidth, cropHeight, fillMode)
%BATCHCROPDETAILLINES Build detail text for the selected crop item.
function lines = detailLines(state, currentIndex, cropWidth, cropHeight, fillMode)
%DETAILLINES Build detail text for the selected crop item.

if isempty(state.items) || currentIndex < 1 || currentIndex > numel(state.items)
lines = {'No images loaded.'};
Expand All @@ -13,7 +13,7 @@
stateText = ternary(item.centerSet, 'confirmed', 'needs confirmation');
lines = { ...
sprintf('Image %d of %d: %s', currentIndex, numel(state.items), ...
displayNameFromPath(item.path)), ...
batch_crop.view.displayNameFromPath(item.path)), ...
sprintf('Crop center: x %.1f, y %.1f (%s)', ...
item.centerXY(1), item.centerXY(2), stateText), ...
sprintf('Output size: %d x %d px; rotation: %.3g deg; fill: %s', ...
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
% App-owned list display helper. Expected caller: batch-crop app refreshList.
% Input is an item struct vector. Output is listbox display text and has no
% side effects.
function items = batchCropListboxItems(cropItems)
%BATCHCROPLISTBOXITEMS Build listbox labels for loaded crop items.
function items = listboxItems(cropItems)
%LISTBOXITEMS Build listbox labels for loaded crop items.

items = cell(numel(cropItems), 1);
for k = 1:numel(cropItems)
marker = ternary(cropItems(k).centerSet, 'set', 'needs center');
items{k} = sprintf('%02d %s [%s]', k, ...
displayNameFromPath(cropItems(k).path), marker);
batch_crop.view.displayNameFromPath(cropItems(k).path), marker);
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
% App-owned preview fill helper. Expected caller: batch-crop app preview
% rendering. Inputs are image data and fill mode. Output is a scalar fill
% value compatible with rotateImageCanvas.
function value = batchCropPreviewFillValue(imageData, fillMode)
%BATCHCROPPREVIEWFILLVALUE Resolve black/white preview fill value.
% value compatible with rotateCanvas.
function value = previewFillValue(imageData, fillMode)
%PREVIEWFILLVALUE Resolve black/white preview fill value.

if strcmp(char(string(fillMode)), 'White')
if islogical(imageData)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
% App-owned overlay geometry helper. Expected caller: batch-crop app preview
% rendering. Inputs are center and crop size in pixels. Output is a MATLAB
% rectangle Position vector and has no side effects.
function position = batchCropRectanglePosition(centerXY, cropWidth, cropHeight)
%BATCHCROPRECTANGLEPOSITION Return rectangle overlay position for a crop box.
function position = rectanglePosition(centerXY, cropWidth, cropHeight)
%RECTANGLEPOSITION Return rectangle overlay position for a crop box.

colStart = round(centerXY(1) - (cropWidth - 1) / 2);
rowStart = round(centerXY(2) - (cropHeight - 1) / 2);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
% App-owned summary view helper. Expected caller: batch-crop app refreshSummary.
% Inputs are app state, current index, canvas size, crop size, and output format.
% Output is metric/value cell data and has no side effects.
function data = batchCropSummaryTableData(state, currentIndex, canvasSize, cropWidth, cropHeight, outputFormat)
%BATCHCROPSUMMARYTABLEDATA Build the batch crop summary table cell data.
function data = summaryTableData(state, currentIndex, canvasSize, cropWidth, cropHeight, outputFormat)
%SUMMARYTABLEDATA Build the batch crop summary table cell data.

if isempty(state.items) || currentIndex < 1 || currentIndex > numel(state.items)
data = { ...
Expand All @@ -17,7 +17,7 @@
item = state.items(currentIndex);
data = { ...
'Images loaded', sprintf('%d', numel(state.items)); ...
'Current image', displayNameFromPath(item.path); ...
'Current image', batch_crop.view.displayNameFromPath(item.path); ...
'Crop size', sprintf('%d x %d px', cropWidth, cropHeight); ...
'Aspect ratio', aspectRatioText(cropWidth, cropHeight); ...
'Rotation', sprintf('%.3g deg', item.angleDeg); ...
Expand Down
22 changes: 0 additions & 22 deletions apps/image_measurement/batch_crop/batchImageCropWorkflow.m

This file was deleted.

Loading
Loading