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
5 changes: 4 additions & 1 deletion +labkit/+biosignal/private/detectEcgPeaksImpl.m
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,10 @@ function validateSignal(signal)
return;
end
med = median(x, 'omitnan');
sigma = 1.4826 * median(abs(x - med), 'omitnan');
% Constant: 1.4826 makes median absolute deviation consistent with the
% standard deviation of a Gaussian distribution.
gaussianMadScale = 1.4826;
sigma = gaussianMadScale * median(abs(x - med), 'omitnan');
if ~isfinite(sigma) || sigma <= 0
sigma = std(x, 'omitnan');
end
Expand Down
22 changes: 16 additions & 6 deletions +labkit/+biosignal/private/inferTableTime.m
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@
end

function scale = unitScale(explicitUnit, name)
% Constant: SI prefixes convert milliseconds, microseconds, and
% nanoseconds to the facade's canonical seconds unit.
millisecondsToSeconds = 1e-3;
microsecondsToSeconds = 1e-6;
nanosecondsToSeconds = 1e-9;
if ~isempty(explicitUnit)
unit = lower(string(explicitUnit));
elseif strcmpi(char(name), 'I0')
Expand All @@ -201,11 +206,11 @@
case {"s", "sec", "secs", "second", "seconds"}
scale = 1;
case {"ms", "msec", "millisecond", "milliseconds"}
scale = 1e-3;
scale = millisecondsToSeconds;
case {"us", "usec", "microsecond", "microseconds"}
scale = 1e-6;
scale = microsecondsToSeconds;
case {"ns", "nsec", "nanosecond", "nanoseconds"}
scale = 1e-9;
scale = nanosecondsToSeconds;
case {"samples", "sample", "index", "sample_index"}
scale = 1;
otherwise
Expand All @@ -229,13 +234,18 @@
end

function label = unitLabel(scale)
% Constant: SI prefixes identify the source time unit after conversion
% to the facade's canonical seconds unit.
millisecondsToSeconds = 1e-3;
microsecondsToSeconds = 1e-6;
nanosecondsToSeconds = 1e-9;
if scale == 1
label = "seconds";
elseif scale == 1e-3
elseif scale == millisecondsToSeconds
label = "milliseconds";
elseif scale == 1e-6
elseif scale == microsecondsToSeconds
label = "microseconds";
elseif scale == 1e-9
elseif scale == nanosecondsToSeconds
label = "nanoseconds";
else
label = "custom";
Expand Down
2 changes: 1 addition & 1 deletion +labkit/+biosignal/version.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
% compatible contract ranges implemented by this code, contract status,
% and a short maintainer note.

info = labkit.contract.versionInfo("biosignal", "1.0.0", ">=1.0 <2", ...
info = labkit.contract.versionInfo("biosignal", "1.0.1", ">=1.0 <2", ...
"stable", "Biosignal recording, filtering, event, segmentation, and ECG facade contract.");
end
6 changes: 5 additions & 1 deletion +labkit/+dta/private/pulsesFromCurrent.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
ok = false;

Iabs = abs(Im);
thr = max(1e-12, 0.25 * max(Iabs));
% Constant: the 1e-12 A floor avoids a zero detector threshold for
% numerically quiet traces; 25 percent selects dominant pulse segments.
currentFloorA = 1e-12;
dominantPulseFraction = 0.25;
thr = max(currentFloorA, dominantPulseFraction * max(Iabs));
cathMask = Im <= -thr;
anodMask = Im >= thr;

Expand Down
2 changes: 1 addition & 1 deletion +labkit/+dta/version.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
% compatible contract ranges implemented by this code, contract status,
% and a short maintainer note.

info = labkit.contract.versionInfo("dta", "2.0.0", ">=2.0 <3", ...
info = labkit.contract.versionInfo("dta", "2.0.1", ">=2.0 <3", ...
"stable", "DTA parser, file item, pulse, and curve facade contract.");
end
5 changes: 3 additions & 2 deletions +labkit/+image/adjustBrightnessContrast.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
% imageOut = labkit.image.adjustBrightnessContrast(imageIn, brightnessPct, contrastPct)
%
% Inputs:
% imageIn - numeric image data, normalized internally with toRgbDouble.
% imageIn - numeric image data, normalized internally to RGB double.
% brightnessPct - brightness offset in percent of full scale.
% contrastPct - contrast scale delta in percent around midpoint 0.5.
%
% Outputs:
% imageOut - RGB double image clamped to [0, 1].

imageIn = labkit.image.toRgbDouble(imageIn);
imageIn = labkit.image.ensureRgb(labkit.image.im2double(imageIn));
imageIn = min(max(imageIn, 0), 1);
brightness = double(brightnessPct) / 100;
contrastScale = max(0, 1 + double(contrastPct) / 100);
imageOut = (imageIn - 0.5) .* contrastScale + 0.5 + brightness;
Expand Down
6 changes: 4 additions & 2 deletions +labkit/+image/adjustHueSaturation.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
% imageOut = labkit.image.adjustHueSaturation(imageIn, hueDeg, saturationPct)
%
% Inputs:
% imageIn - numeric image data, normalized internally with toRgbDouble.
% imageIn - numeric image data, normalized internally to RGB double.
% hueDeg - hue rotation in degrees.
% saturationPct - saturation scale delta in percent.
%
% Outputs:
% imageOut - RGB double image clamped to [0, 1].

hsvImage = rgb2hsv(labkit.image.toRgbDouble(imageIn));
imageIn = labkit.image.ensureRgb(labkit.image.im2double(imageIn));
imageIn = min(max(imageIn, 0), 1);
hsvImage = rgb2hsv(imageIn);
hsvImage(:, :, 1) = mod(hsvImage(:, :, 1) + double(hueDeg) / 360, 1);
hsvImage(:, :, 2) = hsvImage(:, :, 2) .* (1 + double(saturationPct) / 100);
hsvImage(:, :, 2) = min(max(hsvImage(:, :, 2), 0), 1);
Expand Down
31 changes: 31 additions & 0 deletions +labkit/+image/ensureRgb.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function rgb = ensureRgb(imageData)
%ENSURERGB Return image data with exactly three color channels.
%
% App-facing contract:
% rgb = labkit.image.ensureRgb(imageData)
%
% Inputs:
% imageData - numeric or logical M-by-N grayscale, M-by-N-by-1 grayscale,
% M-by-N-by-3 RGB, or M-by-N-by-C data with C greater than three.
%
% Outputs:
% rgb - data with the same class and first two dimensions as imageData.
% Grayscale is replicated across three channels and channels after the
% first three are dropped. Values and numeric class are not changed.

narginchk(1, 1);
validateattributes(imageData, {'numeric', 'logical'}, {'nonsparse'}, ...
mfilename, 'imageData');
if isempty(imageData)
rgb = imageData;
return;
end
if ismatrix(imageData) || size(imageData, 3) == 1
rgb = repmat(imageData(:, :, 1), 1, 1, 3);
elseif size(imageData, 3) >= 3
rgb = imageData(:, :, 1:3);
else
error('labkit:image:ensureRgb:InvalidChannelCount', ...
'Image data must have one, three, or more than three channels.');
end
end
5 changes: 3 additions & 2 deletions +labkit/+image/grayWorldWhiteBalance.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
% imageOut = labkit.image.grayWorldWhiteBalance(imageIn, strengthPct, temperaturePct)
%
% Inputs:
% imageIn - numeric image data, normalized internally with toRgbDouble.
% imageIn - numeric image data, normalized internally to RGB double.
% strengthPct - blend strength from 0 to 100.
% temperaturePct - optional warm/cool red-blue offset in percent.
%
% Outputs:
% imageOut - RGB double image clamped to [0, 1].

imageIn = labkit.image.toRgbDouble(imageIn);
imageIn = labkit.image.ensureRgb(labkit.image.im2double(imageIn));
imageIn = min(max(imageIn, 0), 1);
strength = min(max(double(strengthPct) / 100, 0), 1);
temperature = double(temperaturePct) / 100;
channelMean = squeeze(mean(imageIn, [1 2]));
Expand Down
47 changes: 47 additions & 0 deletions +labkit/+image/im2double.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function imageOut = im2double(imageIn, imageType)
%IM2DOUBLE Convert image data to double using MATLAB's im2double contract.
%
% App-facing contract:
% imageOut = labkit.image.im2double(imageIn)
% imageOut = labkit.image.im2double(indexedImage, "indexed")
%
% Inputs:
% imageIn - logical, uint8, uint16, int16, single, or double image data.
% imageType - optional "indexed" flag. For uint8 and uint16 indexed data,
% one is added after conversion to preserve MATLAB's one-based double
% colormap indices.
%
% Outputs:
% imageOut - double image data with the same class scaling and indexed-image
% offset as MATLAB im2double. Empty input returns an empty double array.

narginchk(1, 2);
indexed = false;
if nargin == 2
indexed = strcmpi(string(imageType), "indexed");
if ~isscalar(indexed) || ~indexed
error('labkit:image:im2double:InvalidImageType', ...
'The optional image type must be "indexed".');
end
end

if isa(imageIn, 'double')
imageOut = imageIn;
elseif islogical(imageIn) || isfloat(imageIn)
imageOut = double(imageIn);
elseif isa(imageIn, 'uint8')
imageOut = double(imageIn) ./ double(intmax('uint8'));
elseif isa(imageIn, 'uint16')
imageOut = double(imageIn) ./ double(intmax('uint16'));
elseif isa(imageIn, 'int16')
imageOut = (double(imageIn) - double(intmin('int16'))) ./ ...
(double(intmax('int16')) - double(intmin('int16')));
else
error('labkit:image:im2double:UnsupportedClass', ...
'Unsupported image class: %s.', class(imageIn));
end

if indexed && (isa(imageIn, 'uint8') || isa(imageIn, 'uint16'))
imageOut = imageOut .* double(intmax(class(imageIn))) + 1;
end
end
5 changes: 3 additions & 2 deletions +labkit/+image/localContrast.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
% imageOut = labkit.image.localContrast(imageIn, amountPct, radiusPx)
%
% Inputs:
% imageIn - numeric image data, normalized internally with toRgbDouble.
% imageIn - numeric image data, normalized internally to RGB double.
% amountPct - nonnegative effect strength in percent.
% radiusPx - local neighborhood radius in pixels.
%
% Outputs:
% imageOut - RGB double image clamped to [0, 1].

imageIn = labkit.image.toRgbDouble(imageIn);
imageIn = labkit.image.ensureRgb(labkit.image.im2double(imageIn));
imageIn = min(max(imageIn, 0), 1);
amount = max(0, double(amountPct)) / 100;
radius = max(1, round(double(radiusPx)));
hsvImage = rgb2hsv(imageIn);
Expand Down
10 changes: 8 additions & 2 deletions +labkit/+image/previewBudget.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

opts = parseOptions(varargin);
validateImageData(imageData);
maxPixels = positiveScalar(opts.MaxPixels, 1.2e6);
maxPixels = positiveScalar(opts.MaxPixels, defaultMaxPixels());
expansion = positiveScalar(opts.Expansion, 1);

estimatedPixels = double(size(imageData, 1)) * ...
Expand All @@ -39,7 +39,7 @@
end

function opts = parseOptions(args)
opts = struct('MaxPixels', 1.2e6, 'Expansion', 1);
opts = struct('MaxPixels', defaultMaxPixels(), 'Expansion', 1);
if isempty(args)
return;
end
Expand All @@ -57,6 +57,12 @@
end
end

function value = defaultMaxPixels()
% Constant: 1.2 megapixels balances interactive preview responsiveness
% with enough spatial detail for image measurement workflows.
value = 1.2e6;
end

function validateImageData(imageData)
if isempty(imageData) || ~(isnumeric(imageData) || islogical(imageData)) || ndims(imageData) > 3
error('labkit:image:InvalidImageData', ...
Expand Down
3 changes: 2 additions & 1 deletion +labkit/+image/readFiles.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
reportProgress(opts.progressFcn, "beforeRead", k, numel(paths), path);
imageData = imread(char(path));
if opts.Normalize
imageData = labkit.image.toRgbDouble(imageData);
imageData = labkit.image.ensureRgb(labkit.image.im2double(imageData));
imageData = min(max(imageData, 0), 1);
end
records(k) = struct( ...
'path', path, ...
Expand Down
38 changes: 38 additions & 0 deletions +labkit/+image/rgb2gray.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
function gray = rgb2gray(rgb)
%RGB2GRAY Convert RGB data using MATLAB's rgb2gray call contract.
%
% App-facing contract:
% grayImage = labkit.image.rgb2gray(rgbImage)
% grayMap = labkit.image.rgb2gray(rgbColorMap)
%
% Inputs:
% rgb - M-by-N-by-3 RGB image or M-by-3 colormap of class uint8, uint16,
% int16, single, or double.
%
% Outputs:
% gray - grayscale image or colormap with the same numeric class as rgb.
% Conversion uses the Rec.601 luma weights used by MATLAB rgb2gray.

narginchk(1, 1);
validateattributes(rgb, {'uint8', 'uint16', 'int16', 'single', 'double'}, ...
{'real', 'nonsparse'}, mfilename, 'rgb');
isColorMap = ismatrix(rgb) && size(rgb, 2) == 3;
isRgbImage = ndims(rgb) == 3 && size(rgb, 3) == 3;
if ~(isColorMap || isRgbImage)
error('labkit:image:rgb2gray:InvalidShape', ...
'Input must be an M-by-N-by-3 RGB image or an M-by-3 colormap.');
end

% Constant: ITU-R BT.601 luma coefficients are the documented MATLAB rgb2gray
% transform; naming the source keeps these scientific constants auditable.
rec601LumaWeights = [0.2989 0.5870 0.1140];
if isColorMap
converted = double(rgb) * rec601LumaWeights(:);
converted = repmat(converted, 1, 3);
else
converted = rec601LumaWeights(1) .* double(rgb(:, :, 1)) + ...
rec601LumaWeights(2) .* double(rgb(:, :, 2)) + ...
rec601LumaWeights(3) .* double(rgb(:, :, 3));
end
gray = cast(converted, 'like', rgb);
end
5 changes: 3 additions & 2 deletions +labkit/+image/sharpen.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
% imageOut = labkit.image.sharpen(imageIn, amountPct, radiusPx)
%
% Inputs:
% imageIn - numeric image data, normalized internally with toRgbDouble.
% imageIn - numeric image data, normalized internally to RGB double.
% amountPct - nonnegative effect strength in percent.
% radiusPx - blur radius in pixels.
%
% Outputs:
% imageOut - RGB double image clamped to [0, 1].

imageIn = labkit.image.toRgbDouble(imageIn);
imageIn = labkit.image.ensureRgb(labkit.image.im2double(imageIn));
imageIn = min(max(imageIn, 0), 1);
amount = max(0, double(amountPct)) / 100;
radius = max(0.5, double(radiusPx));
windowSize = max(3, 2 * round(radius) + 1);
Expand Down
31 changes: 0 additions & 31 deletions +labkit/+image/toDouble.m

This file was deleted.

Loading
Loading