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
6 changes: 2 additions & 4 deletions +labkit/+ui/+app/createShell.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,9 @@
function tabs = standardTabs()
tabs = [ ...
labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [3 1], ...
{260, 'fit', 'fit'}, ...
struct('resizeRows', [1 2])), ...
{260, 'fit', 'fit'}), ...
labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ...
{'fit', '1x'}, ...
struct('resizeRows', 1)), ...
{'fit', '1x'}), ...
labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})];
end

Expand Down
23 changes: 21 additions & 2 deletions +labkit/+ui/+app/private/createTabbedWorkbenchShell.m
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,29 @@ function enableScrollableGrid(grid)

function rows = validResizeRows(spec, logicalRows)
rows = [];
if ~isfield(spec, 'resizeRows') || isempty(spec.resizeRows)
if isfield(spec, 'resizeRows') && ~isempty(spec.resizeRows)
rows = unique(spec.resizeRows(:).');
rows = rows(rows >= 1 & rows < logicalRows & isfinite(rows));
return;
end
rows = unique(spec.resizeRows(:).');

mode = optionValue(spec, 'resize', 'betweenRows');
if islogical(mode)
if mode
rows = 1:max(logicalRows - 1, 0);
end
return;
end
mode = lower(char(string(mode)));
switch mode
case {'betweenrows', 'auto', 'all'}
rows = 1:max(logicalRows - 1, 0);
case {'none', 'off', 'false'}
rows = [];
otherwise
error('labkit:ui:InvalidTabResizeMode', ...
'Unsupported tab resize mode "%s".', char(string(mode)));
end
rows = rows(rows >= 1 & rows < logicalRows & isfinite(rows));
end

Expand Down
12 changes: 10 additions & 2 deletions +labkit/+ui/+app/tab.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
%
% Usage:
% spec = labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', ...
% [4 1], {240, 220, 280, 160}, struct('resizeRows', [1 2 3]));
% [4 1], {240, 220, 280, 160});
%
% Inputs:
% key - valid field-name style identifier used in the returned ui struct.
Expand All @@ -14,7 +14,8 @@
%
% Options:
% columnWidth - cell row of column widths, default all {'1x'}.
% resizeRows - numeric logical-row boundaries after which drag handles are added.
% resize - row-resize behavior: 'betweenRows' default, or 'none'.
% resizeRows - legacy numeric logical-row boundaries. Prefer resize.
% resizeOptions - struct passed to row-resize handle creation.
% padding, rowSpacing, columnSpacing - grid layout properties.
%
Expand All @@ -27,20 +28,27 @@
if nargin < 5
opts = struct();
end
optsHasResize = isfield(opts, 'resize');
optsHasResizeRows = isfield(opts, 'resizeRows');

spec = struct( ...
'key', char(key), ...
'title', char(titleText), ...
'gridSize', gridSize, ...
'rowHeight', {asCellRow(rowHeight)}, ...
'columnWidth', {repmat({'1x'}, 1, gridSize(2))}, ...
'resize', 'betweenRows', ...
'resizeRows', [], ...
'resizeOptions', struct());

fields = fieldnames(opts);
for k = 1:numel(fields)
spec.(fields{k}) = opts.(fields{k});
end

if optsHasResizeRows && isempty(opts.resizeRows) && ~optsHasResize
spec.resize = 'none';
end
end

function value = asCellRow(value)
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
[![MATLAB](https://img.shields.io/badge/MATLAB-apps-orange.svg)](https://www.mathworks.com/products/matlab.html)

Focused MATLAB GUI apps for lab workflows in electrochemistry, DIC, image
measurement, microscopy focus stacking, batch image cropping, and wearable
biosignal review.
measurement, microscopy focus stacking, image enhancement, batch image
cropping, and wearable biosignal review.

LabKit MATLAB Workbench is an app-first research workbench. Each workflow keeps
its own launch command, app-owned calculations, plots, summaries, and exports.
Expand All @@ -21,7 +21,7 @@ processing.
| --- | --- |
| App-first workflows | Independent MATLAB GUI apps for daily lab tasks instead of one monolithic analysis launcher. |
| Electrochemistry support | Gamry DTA loading, chrono overlays, CIC, CSC, VT resistance, EIS plotting, pulse handling, and CSV export paths. |
| Image and DIC workflows | DIC preprocessing/postprocessing, curve measurement, calibrated scale bars, focus-stack fusion, and batch microscope image crops. |
| Image and DIC workflows | DIC preprocessing/postprocessing, curve measurement, calibrated scale bars, focus-stack fusion, paper image enhancement, and batch microscope image crops. |
| Wearable biosignals | ECG/table import, filtering, peak detection, event segments, templates, and SNR-style measurement summaries. |
| Reusable foundation | Layered `labkit.ui`, GUI-free `labkit.dta`, and GUI-free `labkit.biosignal` facades. |
| Guarded behavior | MATLAB build tasks, synthetic fixtures, architecture guardrails, and GitHub Actions CI. |
Expand Down Expand Up @@ -53,6 +53,8 @@ labkit_DICPostprocess_app
% Image measurement and microscopy utilities
labkit_CurvatureMeasurement_app
labkit_FocusStack_app
labkit_ImageEnhance_app
labkit_ImageMatch_app
labkit_BatchImageCrop_app

% Wearable biosignal review
Expand All @@ -75,6 +77,8 @@ options, and export outputs when the selected app provides an export action.
| `labkit_DICPostprocess_app` | Ncorr strain overlays, ROI summary, and colorbar export | Ncorr MAT, reference image, ROI mask | EXX/EYY overlays, summary CSV, colorbar files |
| `labkit_CurvatureMeasurement_app` | Editable curve tracing, calibrated scale, length, and circle-fit curvature | Image files | Overlay PNG and curvature/length CSV |
| `labkit_FocusStack_app` | Microscope focus-stack fusion into an all-in-focus image | Focus image folder or selected image files | Fused PNG, focus map PNG, summary CSV |
| `labkit_ImageEnhance_app` | Stepwise brightness, contrast, clarity, color, and white-balance enhancement for figures | Image files | Enhanced images and processing manifest CSV |
| `labkit_ImageMatch_app` | Reference-based white-balance, tone, and color-style matching for figure images | Image files | Matched images and processing manifest CSV |
| `labkit_BatchImageCrop_app` | Fixed-size batch microscope crops with per-image center and rotation | Microscope image files | Cropped images and crop manifest CSV |
| `labkit_ECGPrint_app` | ECG waveform preview, filtering, peak/segment SNR, and SNR-over-time display | MAT timetable, CSV, or TSV recordings | Segment SNR CSV and waveform PNG |

Expand Down
6 changes: 2 additions & 4 deletions apps/dic/dic_postprocess/labkit_DICPostprocess_app.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@
workbenchOpts.tabs = [ ...
labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ...
{240, 230, 260, 120}, ...
struct('resizeRows', [1 2 3], ...
'resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 90))), ...
struct('resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 90))), ...
labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ...
{210, '1x'}, ...
struct('resizeRows', 1, ...
'resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 90))), ...
struct('resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 90))), ...
labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})];
ui = labkit.ui.app.createShell(struct( ...
'title', 'DIC Strain Postprocess', ...
Expand Down
6 changes: 2 additions & 4 deletions apps/dic/dic_preprocess/+dic_preprocess/+ui/createLayout.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@
workbenchOpts.tabs = [ ...
labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ...
{240, 210, 330, 170}, ...
struct('resizeRows', [1 2 3], ...
'resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 80))), ...
struct('resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 80))), ...
labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ...
{150, '1x'}, ...
struct('resizeRows', 1, ...
'resizeOptions', struct('minTopHeight', 90, 'minBottomHeight', 90))), ...
struct('resizeOptions', struct('minTopHeight', 90, 'minBottomHeight', 90))), ...
labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})];
ui = labkit.ui.app.createShell(struct( ...
'title', 'DIC Image Preprocess', ...
Expand Down
6 changes: 2 additions & 4 deletions apps/image_measurement/batch_crop/labkit_BatchImageCrop_app.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@
'rightRowHeight', {{'1x'}});
workbenchOpts.tabs = [ ...
labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [3 1], ...
{250, 260, 145}, ...
struct('resizeRows', [1 2])), ...
{250, 260, 145}), ...
labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ...
{240, '1x'}, ...
struct('resizeRows', 1)), ...
{240, '1x'}), ...
labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})];

ui = labkit.ui.app.createShell(struct( ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
opts.tabs = [ ...
labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [5 1], ...
{140, 105, 355, 225, 160}, ...
struct('resizeRows', [1 2 3 4], ...
'resizeOptions', struct('minTopHeight', 140, 'minBottomHeight', 90))), ...
struct('resizeOptions', struct('minTopHeight', 140, 'minBottomHeight', 90))), ...
labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ...
{170, '1x'}, ...
struct('resizeRows', 1)), ...
{170, '1x'}), ...
labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})];
end
6 changes: 2 additions & 4 deletions apps/image_measurement/focus_stack/labkit_FocusStack_app.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@
workbenchOpts.tabs = [ ...
labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ...
{250, 235, 185, 170}, ...
struct('resizeRows', [1 2 3], ...
'resizeOptions', struct('minTopHeight', 130, 'minBottomHeight', 90))), ...
struct('resizeOptions', struct('minTopHeight', 130, 'minBottomHeight', 90))), ...
labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ...
{220, '1x'}, ...
struct('resizeRows', 1)), ...
{220, '1x'}), ...
labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})];

ui = labkit.ui.app.createShell(struct( ...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
% Expected caller: image_enhance.export.writeOutputs and tests. Input is a
% result struct array from batch export. Output is the stable CSV manifest table.
function T = buildManifest(results)

results = results(:);
sourceImage = strings(numel(results), 1);
outputImage = strings(numel(results), 1);
status = strings(numel(results), 1);
widthPx = zeros(numel(results), 1);
heightPx = zeros(numel(results), 1);
stepCount = zeros(numel(results), 1);
message = strings(numel(results), 1);

for k = 1:numel(results)
sourceImage(k) = results(k).sourcePath;
outputImage(k) = results(k).outputPath;
status(k) = results(k).status;
widthPx(k) = results(k).widthPx;
heightPx(k) = results(k).heightPx;
stepCount(k) = results(k).stepCount;
message(k) = results(k).message;
end

T = table(sourceImage, outputImage, status, widthPx, heightPx, ...
stepCount, message, 'VariableNames', {'SourceImage', 'OutputImage', ...
'Status', 'Width_px', 'Height_px', 'StepCount', 'Message'});
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
% Expected caller: labkit_ImageEnhance_app and image_enhance export tests.
% Inputs are loaded image items, ordered enhancement steps, and export options.
% Output includes per-image result structs and the manifest CSV path.
function payload = writeOutputs(items, steps, opts)

if isempty(items)
error('labkit_ImageEnhance_app:NoImagesLoaded', ...
'Load images before exporting enhanced outputs.');
end
if nargin < 3 || isempty(opts)
opts = struct();
end
outputFolder = optionValue(opts, 'outputFolder', string(pwd));
outputFormat = optionValue(opts, 'format', 'PNG');

if exist(outputFolder, 'dir') ~= 7
mkdir(outputFolder);
end

images = cell(numel(items), 1);
for k = 1:numel(items)
images{k} = items(k).image;
end
processed = image_enhance.ops.applyPipeline(images, steps);

resultTemplate = emptyResult();
results = repmat(resultTemplate, numel(items), 1);
for k = 1:numel(items)
result = resultTemplate;
result.sourcePath = items(k).path;
result.stepCount = numel(steps);
result.widthPx = size(processed{k}, 2);
result.heightPx = size(processed{k}, 1);

outputPath = uniqueOutputPath(outputFolder, items(k).path, outputFormat);
result.outputPath = outputPath;
try
imwrite(processed{k}, outputPath);
result.status = "saved";
result.message = "Saved";
catch ME
result.status = "failed";
result.message = string(ME.message);
end
results(k) = result;
end

manifestPath = uniquePath(fullfile(char(outputFolder), ...
'image_enhance_manifest.csv'));
writetable(image_enhance.export.buildManifest(results), manifestPath);

payload = struct();
payload.results = results;
payload.manifestPath = string(manifestPath);
end

function value = optionValue(opts, name, defaultValue)
value = defaultValue;
if isfield(opts, name) && ~isempty(opts.(name))
value = opts.(name);
end
end

function result = emptyResult()
result = struct( ...
'sourcePath', "", ...
'outputPath', "", ...
'status', "pending", ...
'widthPx', 0, ...
'heightPx', 0, ...
'stepCount', 0, ...
'message', "");
end

function outputPath = uniqueOutputPath(outputFolder, sourcePath, formatName)
[~, base, ~] = fileparts(char(sourcePath));
extension = formatExtension(formatName);
outputPath = uniquePath(fullfile(char(outputFolder), ...
sprintf('%s_enhanced%s', base, extension)));
outputPath = string(outputPath);
end

function extension = formatExtension(formatName)
switch upper(string(formatName))
case "TIFF"
extension = '.tif';
case "JPEG"
extension = '.jpg';
otherwise
extension = '.png';
end
end

function path = uniquePath(path)
[folder, base, ext] = fileparts(path);
candidate = fullfile(folder, [base ext]);
index = 1;
while isfile(candidate)
candidate = fullfile(folder, sprintf('%s_%03d%s', base, index, ext));
index = index + 1;
end
path = candidate;
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
% Expected caller: labkit_ImageEnhance_app file dialogs. Output is a uigetfile
% filter spec for image formats supported by the enhancement workflow.
function filterSpec = imageDialogFilter()

filterSpec = { ...
'*.png;*.jpg;*.jpeg;*.tif;*.tiff;*.bmp', ...
'Image files (*.png, *.jpg, *.jpeg, *.tif, *.tiff, *.bmp)'; ...
'*.*', 'All files (*.*)'};
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
% Expected caller: labkit_ImageEnhance_app and batch export tests. Input is a
% string vector of image paths. Output is an item struct array with RGB double
% images normalized to [0, 1]. Alpha channels are ignored.
function items = readImages(paths)

paths = string(paths(:));
template = image_enhance.state.emptyItem();
items = repmat(template, numel(paths), 1);

for k = 1:numel(paths)
imageData = imread(paths(k));
items(k) = template;
items(k).path = paths(k);
items(k).name = displayName(paths(k));
items(k).image = normalizeImage(imageData);
end
end

function name = displayName(path)
[~, base, ext] = fileparts(char(path));
name = string([base ext]);
end

function imageData = normalizeImage(imageData)
if ndims(imageData) == 2
imageData = repmat(imageData, 1, 1, 3);
elseif size(imageData, 3) > 3
imageData = imageData(:, :, 1:3);
end

imageData = im2double(imageData);
imageData = min(max(imageData, 0), 1);
end
Loading
Loading