35

I have an NxM matrix in MATLAB that I would like to reorder in similar fashion to the way JPEG reorders its subblock pixels:

zigzag layout pattern (image from Wikipedia)

I would like the algorithm to be generic such that I can pass in a 2D matrix with any dimensions. I am a C++ programmer by trade and am very tempted to write an old school loop to accomplish this, but I suspect there is a better way to do it in MATLAB.

I'd be rather want an algorithm that worked on an NxN matrix and go from there.

Example:

1 2 3
4 5 6  -->  1 2 4 7 5 3 6 8 9
7 8 9
double-beep
  • 5,031
  • 17
  • 33
  • 41
fbrereto
  • 35,429
  • 19
  • 126
  • 178
  • Is there a generic formula for the index (e.g. 1,1 ->1, 1,2->2, 2,1 -> 3) etc. idx = f(i,j,m,n)> – Marc Jun 11 '10 at 17:51
  • +1, I had to write generic zigzag myself as a school assignment 3 weeks ago. I'm also curious if it's possible to do this without loops. – avakar Jun 11 '10 at 18:47

6 Answers6

26

Consider the code:

M = randi(100, [3 4]);                      %# input matrix

ind = reshape(1:numel(M), size(M));         %# indices of elements
ind = fliplr( spdiags( fliplr(ind) ) );     %# get the anti-diagonals
ind(:,1:2:end) = flipud( ind(:,1:2:end) );  %# reverse order of odd columns
ind(ind==0) = [];                           %# keep non-zero indices

M(ind)                                      %# get elements in zigzag order

An example with a 4x4 matrix:

» M
M =
    17    35    26    96
    12    59    51    55
    50    23    70    14
    96    76    90    15

» M(ind)
ans =
    17  35  12  50  59  26  96  51  23  96  76  70  55  14  90  15

and an example with a non-square matrix:

M =
    69     9    16   100
    75    23    83     8
    46    92    54    45
ans =
    69     9    75    46    23    16   100    83    92    54     8    45
Amro
  • 123,847
  • 25
  • 243
  • 454
  • Both yours and gnovice's were correct, so I gave it to the smaller of the two. – fbrereto Jun 11 '10 at 22:49
  • 1
    The most efficient implementation I have seen to this date. Thanks Amro. +1. – rayryeng Feb 06 '15 at 16:40
  • 1
    @rayryeng I was about to post my answer when your question was closed as duplicate. It turns out to be quite fast, or so it seems from my rough (tic/toc) estimations on Matlab R2014b with large matrices. So I've posted it here – Luis Mendo Feb 06 '15 at 22:24
9

This approach is pretty fast:

X = randn(500,2000); %// example input matrix
[r, c] = size(X);
M = bsxfun(@plus, (1:r).', 0:c-1);
M = M + bsxfun(@times, (1:r).'/(r+c), (-1).^M);
[~, ind] = sort(M(:));
y = X(ind).'; %'// output row vector

Benchmarking

The following code compares running time with that of Amro's excellent answer, using timeit. It tests different combinations of matrix size (number of entries) and matrix shape (number of rows to number of columns ratio).

%// Amro's approach
function y = zigzag_Amro(M)
ind = reshape(1:numel(M), size(M));
ind = fliplr( spdiags( fliplr(ind) ) );     
ind(:,1:2:end) = flipud( ind(:,1:2:end) );
ind(ind==0) = [];
y = M(ind);

%// Luis' approach
function y = zigzag_Luis(X)
[r, c] = size(X);
M = bsxfun(@plus, (1:r).', 0:c-1);
M = M + bsxfun(@times, (1:r).'/(r+c), (-1).^M);
[~, ind] = sort(M(:));
y = X(ind).';

%// Benchmarking code:
S = [10 30 100 300 1000 3000]; %// reference to generate matrix size
f = [1 1]; %// number of cols is S*f(1); number of rows is S*f(2)
%// f = [0.5 2]; %// plotted with '--'
%// f = [2 0.5]; %// plotted with ':'
t_Amro = NaN(size(S));
t_Luis = NaN(size(S));
for n = 1:numel(S)
    X = rand(f(1)*S(n), f(2)*S(n));
    f_Amro = @() zigzag_Amro(X);
    f_Luis = @() zigzag_Luis(X);
    t_Amro(n) = timeit(f_Amro);
    t_Luis(n) = timeit(f_Luis);
end
loglog(S.^2*prod(f), t_Amro, '.b-');
hold on
loglog(S.^2*prod(f), t_Luis, '.r-');
xlabel('number of matrix entries')
ylabel('time')

The figure below has been obtained with Matlab R2014b on Windows 7 64 bits. Results in R2010b are very similar. It is seen that the new approach reduces running time by a factor between 2.5 (for small matrices) and 1.4 (for large matrices). Results are seen to be almost insensitive to matrix shape, given a total number of entries.

enter image description here

Community
  • 1
  • 1
Luis Mendo
  • 110,752
  • 13
  • 76
  • 147
  • 1
    Very elegant. Comparable to Amro. Thanks so much! Pretty self explanatory! Btw, I'd like to give you a bounty. I've never done it before and it seems that the interface is not intuitive. Do you know how I'd do it? – rayryeng Feb 06 '15 at 23:03
  • 1
    @rayryeng I've given bounties before in other Stack Exchange sites, but not in the modality "reward a good existing answer". My bounties were of the type "this question has not received enough attention". In any case, I don't think my answer really deserves a bounty :-) Thanks a lot for your appreciation of it! Perhaps I should do some real (`timeit`) benchmarking... – Luis Mendo Feb 06 '15 at 23:11
  • 1
    @rayryeng I've added some benchmarking with different matrix sizes and shapes. My approach turns out to be faster by a factor on the order of 2. I might be getting closer to a bounty :-P Now seriously, I don't want to take rep away from you! – Luis Mendo Feb 07 '15 at 01:03
  • 1
    I've reached my 20K limit so I have a bit to spare! BTW, very nice benchmarking. Factor of 2 is certainly something to brag about! – rayryeng Feb 07 '15 at 08:57
  • OK, I've started a bounty. It says I can manually award it after 24 hours. When that's up, I will grant you the bounty. Thanks for this awesome answer! – rayryeng Feb 07 '15 at 23:51
  • 1
    @rayryeng Exactly! The most feared bounty hunter in the _far, far away galaxy_, hehehe – Luis Mendo Feb 08 '15 at 03:06
  • hehe :D. Are you excited for the new movie coming out this December? – rayryeng Feb 08 '15 at 05:39
  • 1
    I confess I have more fear than excitement. The Disney touch, the "funny" round rolling robot in the teaser trailer... I hope they don't ruin it! We had enough with Jar-Jar Binks, didn't we? :-D – Luis Mendo Feb 08 '15 at 13:54
8

Here's a non-loop solution zig_zag.m. It looks ugly but it works!:

function [M,index] = zig_zag(M)
  [r,c] = size(M);
  checker = rem(hankel(1:r,r-1+(1:c)),2);
  [rEven,cEven] = find(checker);
  [cOdd,rOdd] = find(~checker.'); %'#
  rTotal = [rEven; rOdd];
  cTotal = [cEven; cOdd];
  [junk,sortIndex] = sort(rTotal+cTotal);
  rSort = rTotal(sortIndex);
  cSort = cTotal(sortIndex);
  index = sub2ind([r c],rSort,cSort);
  M = M(index);
end

And a test matrix:

>> M = [magic(4) zeros(4,1)];

M =

    16     2     3    13     0
     5    11    10     8     0
     9     7     6    12     0
     4    14    15     1     0

>> newM = zig_zag(M)    %# Zig-zag sampled elements

newM =

    16
     2
     5
     9
    11
     3
    13
    10
     7
     4
    14
     6
     8
     0
     0
    12
    15
     1
     0
     0
gnovice
  • 125,304
  • 15
  • 256
  • 359
  • Thanks for the answer. I ran your code and the indices I'm getting for a 3x3 matrix is [1 4 2 3 5 7 8 6 9], whereas the solution I'd expect would be [1 2 4 7 5 3 6 8 9] - slightly different; am I missing something? – fbrereto Jun 11 '10 at 19:44
  • @fbrereto: The function returns *indices*, not the reordered matrix, so you have to then do `M(index)` to get the reordered elements. I think I'll update the function so it actually returns the reordered elements *and* the indices as an additional output (if they are needed). – gnovice Jun 11 '10 at 19:47
  • @gnovice: Right, as I said in my comment the indices I'm getting back don't seem to jive with the indices I'd expect. (Correct me if I'm wrong.) – fbrereto Jun 11 '10 at 19:50
  • I think I see the difference; in Matlab the indices are row-major and I'm assuming they're column major, is that right? – fbrereto Jun 11 '10 at 19:52
  • @fbrereto: Oh, I see what you are asking about. Linear indices in MATLAB start at the top left element, then traverse down the first column, then the second, etc.. So, the element indexed by `4` is in row 1 and column 2 (which is the value `2`). – gnovice Jun 11 '10 at 19:54
5

Here's a way how to do this. Basically, your array is a hankel matrix plus vectors of 1:m, where m is the number of elements in each diagonal. Maybe someone else has a neat idea on how to create the diagonal arrays that have to be added to the flipped hankel array without a loop.

I think this should be generalizeable to a non-square array.

% for a 3x3 array 
n=3;

numElementsPerDiagonal = [1:n,n-1:-1:1];
hadaRC = cumsum([0,numElementsPerDiagonal(1:end-1)]);
array2add = fliplr(hankel(hadaRC(1:n),hadaRC(end-n+1:n)));

% loop through the hankel array and add numbers counting either up or down
% if they are even or odd
for d = 1:(2*n-1)
   if floor(d/2)==d/2
      % even, count down
      array2add = array2add + diag(1:numElementsPerDiagonal(d),d-n);
   else
      % odd, count up
      array2add = array2add + diag(numElementsPerDiagonal(d):-1:1,d-n);
   end
end

% now flip to get the result
indexMatrix = fliplr(array2add)

result =
     1     2     6
     3     5     7
     4     8     9

Afterward, you just call reshape(image(indexMatrix),[],1) to get the vector of reordered elements.

EDIT

Ok, from your comment it looks like you need to use sort like Marc suggested.

indexMatrixT = indexMatrix';   % ' SO formatting
[dummy,sortedIdx] = sort(indexMatrixT(:));

sortedIdx =
     1     2     4     7     5     3     6     8     9

Note that you'd need to transpose your input matrix first before you index, because Matlab counts first down, then right.

Jonas
  • 74,690
  • 10
  • 137
  • 177
4

Assuming X to be the input 2D matrix and that is square or landscape-shaped, this seems to be pretty efficient -

[m,n] = size(X);
nlim = m*n;
n = n+mod(n-m,2);
mask = bsxfun(@le,[1:m]',[n:-1:1]);
start_vec = m:m-1:m*(m-1)+1;
a = bsxfun(@plus,start_vec',[0:n-1]*m);
offset_startcol = 2- mod(m+1,2);
[~,idx] = min(mask,[],1);
idx = idx - 1;
idx(idx==0) = m;
end_ind = a([0:n-1]*m + idx);

offsets = a(1,offset_startcol:2:end) + end_ind(offset_startcol:2:end);
a(:,offset_startcol:2:end) = bsxfun(@minus,offsets,a(:,offset_startcol:2:end));
out = a(mask);
out2 = m*n+1 - out(end:-1:1+m*(n-m+1));
result = X([out2 ; out(out<=nlim)]);

Quick runtime tests against Luis's approach -

Datasize: 500 x 2000
------------------------------------- With Proposed Approach
Elapsed time is 0.037145 seconds.
------------------------------------- With Luis Approach
Elapsed time is 0.045900 seconds.


Datasize: 5000 x 20000
------------------------------------- With Proposed Approach
Elapsed time is 3.947325 seconds.
------------------------------------- With Luis Approach
Elapsed time is 6.370463 seconds.
Community
  • 1
  • 1
Divakar
  • 218,885
  • 19
  • 262
  • 358
0

Let's assume for a moment that you have a 2-D matrix that's the same size as your image specifying the correct index. Call this array idx; then the matlab commands to reorder your image would be

[~,I] = sort (idx(:)); %sort the 1D indices of the image into ascending order according to idx
reorderedim = im(I);

I don't see an obvious solution to generate idx without using for loops or recursion, but I'll think some more.

Marc
  • 5,315
  • 5
  • 30
  • 36