Contents

Kaleidoscopic Reflections

Inspired by: https://www.csh.rit.edu/~pat/hack/quickies/kaleid/

function kaleidoscope_demo(k, iters, doImg)
    % kaleidoscope options
    if nargin < 1, k = 4; end      % n-folds (n=4*k)
    if nargin < 2, iters = 1; end  % recursive level: 1, 2, ..
    if nargin < 3, doImg = true; end
    validateattributes(k, {'numeric'}, {'scalar', 'integer', '>=',1, '<=',4}, 1);
    validateattributes(iters, {'numeric'}, {'scalar', 'integer', 'positive'}, 2);
    validateattributes(doImg, {'numeric', 'logical'}, {'scalar', 'binary'}, 3);
    kaleidoscope = @(img) kaleidoscope_wrapper(img, k, iters);

    % run demo
    if doImg
        %img = imread(which('peppers.png'));
        %img = imread(fullfile(mexopencv.root(),'test','lena.jpg'));
        img = imread(fullfile(mexopencv.root(),'test','fruits.jpg'));
        demo_image(kaleidoscope, img);
    else
        cap = cv.VideoCapture(0);
        assert(cap.isOpened());
        demo_video(kaleidoscope, cap);
        cap.release();
    end
end

function demo_image(kaleidoscope, img)
    % input image must be square-sized
    sz = 400;
    img = cv.resize(img, [sz sz]);

    % animation by repeating a number of times
    hImg = imshow(img);
    for i=1:81  % sz/(10/2)+1
        % each time offset the input image with wrap-around padding
        of = (i-1)*10;
        if true
            im = cv.copyMakeBorder(img, [of 0 0 of], 'BorderType','Reflect101');
            im = im(1:end-of,of+1:end,:);
        else
            im = circshift(img, [of -of]);
        end

        % show kaleidoscope effect
        out = kaleidoscope(im);
        set(hImg, 'CData',out)
        drawnow
    end
end

function demo_video(kaleidoscope, cap)
    % video stream
    sz = 400;
    img = cap.read();
    assert(~isempty(img));
    img = cv.resize(img, [sz sz]);

    hImg = imshow(img);
    while ishghandle(hImg)
        % grab new frame
        img = cap.read();
        if isempty(img), break; end
        img = cv.resize(img, [sz sz]);

        % show kaleidoscope effect
        out = kaleidoscope(img);
        set(hImg, 'CData',out)
        drawnow
    end
end

Kaleidoscope functions

function out = kaleidoscope_4folds(img, flag)
    % take full image (pattern fills a square)
    out = img;

    % tile (mirror and repeat) this pattern 4 times to get 4-fold symmetry
    if nargin < 2 || flag, out = cv.rotate(out, '180'); end
    out = image_tile(out);
end

function out = kaleidoscope_8folds(img, flag)
    % extract wedge (divide square into 2 folds, take top one)
    sz = size(img, 1);  % assumes a square image
    if true
        mask = zeros(sz, 'uint8');
        mask = cv.fillConvexPoly(mask, [0 0; sz sz; sz 0], 'Color',255);
    else
        mask = triu(ones(sz, 'uint8') * 255);
    end
    q1 = image_crop(img, mask);

    % create other wedge (transpose and h-flip)
    q2 = cv.flip(cv.rotate(q1, '90CW'), 1);

    % merge the two wedges (pattern fills a square)
    out = image_merge(q1, q2);

    % tile (mirror and repeat) this pattern 4 times to get 8-fold symmetry
    if nargin < 2 || flag, out = cv.rotate(out, '180'); end
    out = image_tile(out);
end

function out = kaleidoscope_12folds(img, flag)
    % extract wedge (divide square into 3 folds, take middle one)
    sz = size(img, 1);  % assumes a square image
    s = round(sz * tand(30));
    mask = zeros(sz, 'uint8');
    mask = cv.fillConvexPoly(mask, [0 0; s sz; sz sz; sz s], 'Color',255);
    q1 = image_crop(img, mask);

    % create other two wedges (transpose, h-flip, and rotate +/- 30 degrees)
    q = cv.flip(cv.rotate(q1, '90CW'), 1);
    q2 = image_rotate(q, -30);
    q3 = image_rotate(q, +30);

    % merge the three wedges (pattern fills a square)
    out = image_merge(image_merge(q1, q2), q3);

    % tile (mirror and repeat) this pattern 4 times to get 12-fold symmetry
    if nargin < 2 || flag, out = cv.rotate(out, '180'); end
    out = image_tile(out);
end

function out = kaleidoscope_16folds(img, flag)
    % extract wedge (divide square into 4 folds, take middle-top one)
    sz = size(img, 1);  % assumes a square image
    s = round(sz * tand(22.5));
    mask = zeros(sz, 'uint8');
    mask = cv.fillConvexPoly(mask, [0 0; sz sz; sz s], 'Color',255);
    q1 = image_crop(img, mask);

    % create bottom wedge (rotate 45 degrees), and merge
    q2 = image_rotate(q1, -45);
    q12 = image_merge(q1, q2);

    % create other two wedges (transpose and h-flip), and merge
    q34 = cv.flip(cv.rotate(q12, '90CW'), 1);
    out = image_merge(q12, q34);

    % tile (mirror and repeat) this pattern 4 times to get 16-fold symmetry
    if nargin < 2 || flag, out = cv.rotate(out, '180'); end
    out = image_tile(out);
end

Helper functions

function out = kaleidoscope_wrapper(img, k, iters)
    switch k
        case 1
            kaleidoscope_nfolds = @kaleidoscope_4folds;
        case 2
            kaleidoscope_nfolds = @kaleidoscope_8folds;
        case 3
            kaleidoscope_nfolds = @kaleidoscope_12folds;
        case 4
            kaleidoscope_nfolds = @kaleidoscope_16folds;
    end

    % build kaleidoscope
    out = kaleidoscope_nfolds(img);
    for n=2:iters
        % apply recursively for more fun
        out = kaleidoscope_nfolds(out, false);
    end
end

function out = image_crop(img, mask)
    if true
        out = cv.copyTo(img, 'Mask',mask);
    elseif true
        out = cv.bitwise_and(img, uint8(255), 'Mask',mask);
    else
        out = bsxfun(@times, img, uint8(mask~=0));
    end
end

function out = image_rotate(img, angl)
    T = cv.getRotationMatrix2D([0 0], angl, 1);
    out = cv.warpAffine(img, T);
end

function out = image_merge(img1, img2)
    if true
        out = cv.bitwise_or(img1, img2);
    elseif true
        out = cv.addWeighted(img1,1, img2,1, 0);
    else
        out = img1 + img2;
    end
end

function out = image_tile(img)
    % downsample first so that output has same size as input
    out = cv.resize(img, 0.5, 0.5, 'Interpolation','Area');
    out = [out, cv.flip(out, 1)];
    out = [out; cv.flip(out, 0)];
end