From c1e6c93dd817553fa74726d8d4538bc630455c9a Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 14 May 2026 19:00:47 +0200 Subject: [PATCH 1/2] refactor: replace GPL-derived imsplit with clean-room helpers Replace imsplit.m (whose inner helpers were copied from NoRMCorre, GPLv2) with two clean-room MIT-compatible functions in +stack/+reshape: - splitImage(IM, numRows, numCols, overlap) - mergeImageBlocks(blocks, outputSize, overlap) Removes the 3-D/z-splitting and configurable-per-dimension overlap that imsplit supported but no caller used. The two call sites (globalCorrelation, localCorrelation) only ever split along H/W and preserve the time dimension intact inside each block. Also delete the unused duplicate at code/+nansen/+tools/+image/+resize/imsplit.m. Co-Authored-By: Claude Opus 4.7 --- code/+nansen/+tools/+image/+resize/imsplit.m | 120 ------------------ code/algorithms/+stack/+reshape/imsplit.m | 120 ------------------ .../+stack/+reshape/mergeImageBlocks.m | 41 ++++++ code/algorithms/+stack/+reshape/splitImage.m | 42 ++++++ .../+stack/+zproject/globalCorrelation.m | 6 +- .../+stack/+zproject/localCorrelation.m | 5 +- 6 files changed, 88 insertions(+), 246 deletions(-) delete mode 100644 code/+nansen/+tools/+image/+resize/imsplit.m delete mode 100644 code/algorithms/+stack/+reshape/imsplit.m create mode 100644 code/algorithms/+stack/+reshape/mergeImageBlocks.m create mode 100644 code/algorithms/+stack/+reshape/splitImage.m diff --git a/code/+nansen/+tools/+image/+resize/imsplit.m b/code/+nansen/+tools/+image/+resize/imsplit.m deleted file mode 100644 index dbf9cddf4..000000000 --- a/code/+nansen/+tools/+image/+resize/imsplit.m +++ /dev/null @@ -1,120 +0,0 @@ -function imOut = getChunkedData(IM, chunk, sz, varargin) - -param = struct('numRows', 4, 'numCols', 4); -param = utility.parsenvpairs(param, [], varargin); - -if nargin < 2; chunk = true; end - - if ~chunk - d1 = sz(1); d2 = sz(2); d3 = sz(3); - else - [d1,d2,d3] = size(IM); - end - - gridSize = [d1/param.numRows, d2/param.numCols, d3]; - gridSize = ceil(gridSize); - - [xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,xx_us,xx_uf,yy_us,yy_uf,zz_us,zz_uf] = ... - construct_grid(gridSize, [1,1,1] , d1,d2,d3, [32,32,16] ); - - if chunk - imOut = mat2cell_ov(IM,xx_s,xx_f,yy_s,yy_f,zz_s,zz_f, [1,1,0],[d1,d2,d3]); - else %unchunk - imOut = cell2mat_ov(IM,xx_us,xx_uf,yy_us,yy_uf,zz_us,zz_uf, [1,1,0], [d1,d2,d3]); - end -end - -function I = mat2cell_ov(X,xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,overlap,sz) - -% converts a matrix into a cell array with overlapping elements -% INPUTS: -% X: Input matrix -% grid_size: size of each element without overlap -% overlap: amount of overlap -% sz: spatial size of X - -% OUTPUT: -% I: output cell array - -% Written by Eftychios A. Pnevmatikakis, Simons Foundation, 2016 - -I = cell(length(xx_s),length(yy_s),length(zz_s)); -nd = length(sz); -if nd == 2; sz(3) = 1; end -for i = 1:length(xx_s) - for j = 1:length(yy_s) - for k = 1:length(zz_s) - extended_grid = [max(xx_s(i)-overlap(1),1),min(xx_f(i)+overlap(1),sz(1)),max(yy_s(j)-overlap(2),1),min(yy_f(j)+overlap(2),sz(2)),max(zz_s(k)-overlap(3),1),min(zz_f(k)+overlap(3),sz(3))]; - if nd == 2 - I{i,j} = X(extended_grid(1):extended_grid(2),extended_grid(3):extended_grid(4),:); - else - I{i,j,k} = X(extended_grid(1):extended_grid(2),extended_grid(3):extended_grid(4),extended_grid(5):extended_grid(6),:); - end - end - end -end -end - -function X = cell2mat_ov(I,xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,overlap,sz) - -% converts a cell array to a matrix when the cell elements overlap -% INPUTS: -% I: cell array -% grid_size: true size of each element -% overlap: amount of overlap in each direction -% d1: number of rows of matrix -% d2: number of columns of matrix - -% OUTPUT: -% X: output matrix - -% Written by Eftychios A. Pnevmatikakis, Simons Foundation, 2016 - -X = NaN([sz,size(I{1,1},length(sz)+1)]); -if length(sz) == 2; sz(3) = 1; end - -for i = 1:length(xx_f) - for j = 1:length(yy_f) - for k = 1:length(zz_f) - extended_grid = [max(xx_s(i)-overlap(1),1),min(xx_f(i)+overlap(1),sz(1)),max(yy_s(j)-overlap(2),1),min(yy_f(j)+overlap(2),sz(2)),max(zz_s(k)-overlap(3),1),min(zz_f(k)+overlap(3),sz(3))]; - X(xx_s(i):xx_f(i),yy_s(j):yy_f(j),zz_s(k):zz_f(k)) = ... - I{i,j,k}(1+(xx_s(i)-extended_grid(1)):end-(extended_grid(2)-xx_f(i)),1+(yy_s(j)-extended_grid(3)):end-(extended_grid(4)-yy_f(j)),1+(zz_s(k)-extended_grid(5)):end-(extended_grid(6)-zz_f(k))); - end - end -end -end - -function [xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,xx_us,xx_uf,yy_us,yy_uf,zz_us,zz_uf] = construct_grid(grid_size,mot_uf,d1,d2,d3,min_patch_size) - -xx_s = 1:grid_size(1):d1; -yy_s = 1:grid_size(2):d2; -zz_s = 1:grid_size(3):d3; - -xx_f = [xx_s(2:end)-1,d1]; -yy_f = [yy_s(2:end)-1,d2]; -zz_f = [zz_s(2:end)-1,d3]; - -if xx_f(end)-xx_s(end) + 1 < min_patch_size(1) && length(xx_s) > 1; xx_s(end) = []; xx_f(end-1) = []; end -if yy_f(end)-yy_s(end) + 1 < min_patch_size(2) && length(yy_s) > 1; yy_s(end) = []; yy_f(end-1) = []; end -if zz_f(end)-zz_s(end) + 1 < min_patch_size(3) && length(zz_s) > 1; zz_s(end) = []; zz_f(end-1) = []; end - -grid_size_us = floor(grid_size./mot_uf); -if mot_uf(1) > 1 - xx_us = 1:grid_size_us(1):d1; - xx_uf = [xx_us(2:end)-1,d1]; -else - xx_us = xx_s; xx_uf = xx_f; -end -if mot_uf(2) > 1 - yy_us = 1:grid_size_us(2):d2; - yy_uf = [yy_us(2:end)-1,d2]; -else - yy_us = yy_s; yy_uf = yy_f; -end -if mot_uf(3) > 1 - zz_us = 1:grid_size_us(3):d3; - zz_uf = [zz_us(2:end)-1,d3]; -else - zz_us = zz_s; zz_uf = zz_f; -end -end diff --git a/code/algorithms/+stack/+reshape/imsplit.m b/code/algorithms/+stack/+reshape/imsplit.m deleted file mode 100644 index dbf9cddf4..000000000 --- a/code/algorithms/+stack/+reshape/imsplit.m +++ /dev/null @@ -1,120 +0,0 @@ -function imOut = getChunkedData(IM, chunk, sz, varargin) - -param = struct('numRows', 4, 'numCols', 4); -param = utility.parsenvpairs(param, [], varargin); - -if nargin < 2; chunk = true; end - - if ~chunk - d1 = sz(1); d2 = sz(2); d3 = sz(3); - else - [d1,d2,d3] = size(IM); - end - - gridSize = [d1/param.numRows, d2/param.numCols, d3]; - gridSize = ceil(gridSize); - - [xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,xx_us,xx_uf,yy_us,yy_uf,zz_us,zz_uf] = ... - construct_grid(gridSize, [1,1,1] , d1,d2,d3, [32,32,16] ); - - if chunk - imOut = mat2cell_ov(IM,xx_s,xx_f,yy_s,yy_f,zz_s,zz_f, [1,1,0],[d1,d2,d3]); - else %unchunk - imOut = cell2mat_ov(IM,xx_us,xx_uf,yy_us,yy_uf,zz_us,zz_uf, [1,1,0], [d1,d2,d3]); - end -end - -function I = mat2cell_ov(X,xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,overlap,sz) - -% converts a matrix into a cell array with overlapping elements -% INPUTS: -% X: Input matrix -% grid_size: size of each element without overlap -% overlap: amount of overlap -% sz: spatial size of X - -% OUTPUT: -% I: output cell array - -% Written by Eftychios A. Pnevmatikakis, Simons Foundation, 2016 - -I = cell(length(xx_s),length(yy_s),length(zz_s)); -nd = length(sz); -if nd == 2; sz(3) = 1; end -for i = 1:length(xx_s) - for j = 1:length(yy_s) - for k = 1:length(zz_s) - extended_grid = [max(xx_s(i)-overlap(1),1),min(xx_f(i)+overlap(1),sz(1)),max(yy_s(j)-overlap(2),1),min(yy_f(j)+overlap(2),sz(2)),max(zz_s(k)-overlap(3),1),min(zz_f(k)+overlap(3),sz(3))]; - if nd == 2 - I{i,j} = X(extended_grid(1):extended_grid(2),extended_grid(3):extended_grid(4),:); - else - I{i,j,k} = X(extended_grid(1):extended_grid(2),extended_grid(3):extended_grid(4),extended_grid(5):extended_grid(6),:); - end - end - end -end -end - -function X = cell2mat_ov(I,xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,overlap,sz) - -% converts a cell array to a matrix when the cell elements overlap -% INPUTS: -% I: cell array -% grid_size: true size of each element -% overlap: amount of overlap in each direction -% d1: number of rows of matrix -% d2: number of columns of matrix - -% OUTPUT: -% X: output matrix - -% Written by Eftychios A. Pnevmatikakis, Simons Foundation, 2016 - -X = NaN([sz,size(I{1,1},length(sz)+1)]); -if length(sz) == 2; sz(3) = 1; end - -for i = 1:length(xx_f) - for j = 1:length(yy_f) - for k = 1:length(zz_f) - extended_grid = [max(xx_s(i)-overlap(1),1),min(xx_f(i)+overlap(1),sz(1)),max(yy_s(j)-overlap(2),1),min(yy_f(j)+overlap(2),sz(2)),max(zz_s(k)-overlap(3),1),min(zz_f(k)+overlap(3),sz(3))]; - X(xx_s(i):xx_f(i),yy_s(j):yy_f(j),zz_s(k):zz_f(k)) = ... - I{i,j,k}(1+(xx_s(i)-extended_grid(1)):end-(extended_grid(2)-xx_f(i)),1+(yy_s(j)-extended_grid(3)):end-(extended_grid(4)-yy_f(j)),1+(zz_s(k)-extended_grid(5)):end-(extended_grid(6)-zz_f(k))); - end - end -end -end - -function [xx_s,xx_f,yy_s,yy_f,zz_s,zz_f,xx_us,xx_uf,yy_us,yy_uf,zz_us,zz_uf] = construct_grid(grid_size,mot_uf,d1,d2,d3,min_patch_size) - -xx_s = 1:grid_size(1):d1; -yy_s = 1:grid_size(2):d2; -zz_s = 1:grid_size(3):d3; - -xx_f = [xx_s(2:end)-1,d1]; -yy_f = [yy_s(2:end)-1,d2]; -zz_f = [zz_s(2:end)-1,d3]; - -if xx_f(end)-xx_s(end) + 1 < min_patch_size(1) && length(xx_s) > 1; xx_s(end) = []; xx_f(end-1) = []; end -if yy_f(end)-yy_s(end) + 1 < min_patch_size(2) && length(yy_s) > 1; yy_s(end) = []; yy_f(end-1) = []; end -if zz_f(end)-zz_s(end) + 1 < min_patch_size(3) && length(zz_s) > 1; zz_s(end) = []; zz_f(end-1) = []; end - -grid_size_us = floor(grid_size./mot_uf); -if mot_uf(1) > 1 - xx_us = 1:grid_size_us(1):d1; - xx_uf = [xx_us(2:end)-1,d1]; -else - xx_us = xx_s; xx_uf = xx_f; -end -if mot_uf(2) > 1 - yy_us = 1:grid_size_us(2):d2; - yy_uf = [yy_us(2:end)-1,d2]; -else - yy_us = yy_s; yy_uf = yy_f; -end -if mot_uf(3) > 1 - zz_us = 1:grid_size_us(3):d3; - zz_uf = [zz_us(2:end)-1,d3]; -else - zz_us = zz_s; zz_uf = zz_f; -end -end diff --git a/code/algorithms/+stack/+reshape/mergeImageBlocks.m b/code/algorithms/+stack/+reshape/mergeImageBlocks.m new file mode 100644 index 000000000..a8f42fd27 --- /dev/null +++ b/code/algorithms/+stack/+reshape/mergeImageBlocks.m @@ -0,0 +1,41 @@ +function IM = mergeImageBlocks(blocks, outputSize, overlap) +%MERGEIMAGEBLOCKS Reassemble a cell grid of overlapping 2-D blocks. +% +% IM = stack.reshape.mergeImageBlocks(blocks, outputSize) merges the +% (numRows x numCols) cell array of overlapping 2-D image blocks +% produced by stack.reshape.splitImage into a single outputSize image, +% trimming the 1-pixel overlap on shared edges back to a clean tiling. +% +% IM = stack.reshape.mergeImageBlocks(blocks, outputSize, overlap) +% uses a custom overlap (in pixels; default 1). Must match the value +% used when the blocks were created. +% +% Syntax: +% IM = stack.reshape.mergeImageBlocks(blocks, outputSize) +% IM = stack.reshape.mergeImageBlocks(blocks, outputSize, overlap) + + arguments + blocks cell + outputSize (1,2) double {mustBePositive, mustBeInteger} + overlap (1,1) double {mustBeNonnegative, mustBeInteger} = 1 + end + + [numRows, numCols] = size(blocks); + h = outputSize(1); + w = outputSize(2); + rowEdges = floor(linspace(0, h, numRows + 1)); + colEdges = floor(linspace(0, w, numCols + 1)); + + IM = zeros(h, w, 'like', blocks{1,1}); + for i = 1:numRows + rowDst = (rowEdges(i)+1):rowEdges(i+1); + srcR0 = rowEdges(i) + 1 - max(rowEdges(i) + 1 - overlap, 1) + 1; + for j = 1:numCols + colDst = (colEdges(j)+1):colEdges(j+1); + srcC0 = colEdges(j) + 1 - max(colEdges(j) + 1 - overlap, 1) + 1; + IM(rowDst, colDst) = blocks{i,j}( ... + srcR0 : srcR0 + numel(rowDst) - 1, ... + srcC0 : srcC0 + numel(colDst) - 1); + end + end +end diff --git a/code/algorithms/+stack/+reshape/splitImage.m b/code/algorithms/+stack/+reshape/splitImage.m new file mode 100644 index 000000000..71b09566c --- /dev/null +++ b/code/algorithms/+stack/+reshape/splitImage.m @@ -0,0 +1,42 @@ +function blocks = splitImage(IM, numRows, numCols, overlap) +%SPLITIMAGE Split an H-by-W(-by-T) array into a (numRows x numCols) cell grid. +% +% blocks = stack.reshape.splitImage(IM, numRows, numCols) splits the +% first two dimensions of IM into a numRows-by-numCols cell array of +% sub-arrays. Each block extends one pixel into its neighbours on each +% side (clamped at the image borders) so that per-pixel computations +% needing a 1-pixel neighbourhood can be performed block-wise. The third +% dimension (e.g. time) is preserved in full inside each block. +% +% blocks = stack.reshape.splitImage(IM, numRows, numCols, overlap) uses +% a custom overlap (in pixels; default 1). +% +% Use stack.reshape.mergeImageBlocks to reassemble the blocks; the +% overlap region is trimmed during reassembly. +% +% Syntax: +% blocks = stack.reshape.splitImage(IM, numRows, numCols) +% blocks = stack.reshape.splitImage(IM, numRows, numCols, overlap) + + arguments + IM + numRows (1,1) double {mustBePositive, mustBeInteger} + numCols (1,1) double {mustBePositive, mustBeInteger} + overlap (1,1) double {mustBeNonnegative, mustBeInteger} = 1 + end + + [h, w, ~] = size(IM); + rowEdges = floor(linspace(0, h, numRows + 1)); + colEdges = floor(linspace(0, w, numCols + 1)); + + blocks = cell(numRows, numCols); + for i = 1:numRows + r0 = max(rowEdges(i) + 1 - overlap, 1); + r1 = min(rowEdges(i+1) + overlap, h); + for j = 1:numCols + c0 = max(colEdges(j) + 1 - overlap, 1); + c1 = min(colEdges(j+1) + overlap, w); + blocks{i,j} = IM(r0:r1, c0:c1, :); + end + end +end diff --git a/code/algorithms/+stack/+zproject/globalCorrelation.m b/code/algorithms/+stack/+zproject/globalCorrelation.m index 4c01c314f..d97bf1a50 100644 --- a/code/algorithms/+stack/+zproject/globalCorrelation.m +++ b/code/algorithms/+stack/+zproject/globalCorrelation.m @@ -24,7 +24,7 @@ imageSignal = single( squeeze(imageSignal) ); % Divide data into chunks to use less memory during calculation -tmpIm = stack.reshape.imsplit(IM, true, [], 'numRows', numRows, 'numCols', numCols); +tmpIm = stack.reshape.splitImage(IM, numRows, numCols); tmpC = cell(size(tmpIm)); for i = 1:size(tmpIm,1) @@ -43,9 +43,9 @@ end end -% Unchunk data +% Reassemble blocks into a single image [d1,d2,~] = size(IM); -cIm = stack.reshape.imsplit(tmpC, false, [d1,d2,1], 'numRows', numRows, 'numCols', numCols); +cIm = stack.reshape.mergeImageBlocks(tmpC, [d1,d2]); % cast... cIm = cIm .* nansen.util.range(P) + P(1); diff --git a/code/algorithms/+stack/+zproject/localCorrelation.m b/code/algorithms/+stack/+zproject/localCorrelation.m index e3ba04efc..f00e9589b 100644 --- a/code/algorithms/+stack/+zproject/localCorrelation.m +++ b/code/algorithms/+stack/+zproject/localCorrelation.m @@ -16,8 +16,7 @@ showWaitbar = false; - getChunkedData = @stack.reshape.imsplit; - tmpIm = getChunkedData(imArray, true, [], 'numRows', numRows, 'numCols', numCols); + tmpIm = stack.reshape.splitImage(imArray, numRows, numCols); tmpC = cell(size(tmpIm)); @@ -45,7 +44,7 @@ end [d1,d2,~] = size(imArray); - cIm = getChunkedData(tmpC, false, [d1,d2,1], 'numRows', numRows, 'numCols', numCols); + cIm = stack.reshape.mergeImageBlocks(tmpC, [d1,d2]); %cIm = stack.makeuint8(cIm); cIm = cIm .* nansen.util.range(P) + P(1); From 63d98aeb8859a4b094fa753850512ce593d0b18b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 19:12:15 +0000 Subject: [PATCH 2/2] Update GitHub badges [skip ci] --- .github/badges/code_issues.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg index ef1396f28..cd05e9181 100644 --- a/.github/badges/code_issues.svg +++ b/.github/badges/code_issues.svg @@ -1 +1 @@ -code issuescode issues17061706 \ No newline at end of file +code issuescode issues17041704 \ No newline at end of file