Rearrange pixels in image so it can't be recognized and then get it back

85

36

Create a program which can rearrange pixels in image so it can't be recognized. However your program should able to convert it back to original image.

You can write two functions - for encoding and decoding, however one function which applied repeatedly gives original image (example in math - f(x) = 1 - x) is a bonus.

Also producing some pattern in output gives bonus too.

Image may be represented as 1D/2D array or image object if your language supports it. Note that you can only change order of pixels!

Will be logical to select as winner code which produces less recognizable image, however I don't know how to measure it exactly, all ways I can imagine can be cheated. Therefore I chose this question to be popularity contest - let users choose what the best answer!

Test image 1 (800 x 422 px): Test image 2 (800 x 480 px): Please provide with code output image.

Somnium

Posted 2014-07-24T06:43:35.237

Reputation: 2 537

The question is a very long-winded way of saying "Write an encryption algorithm for images, whose output is an image." – David Richerby – 2014-07-24T09:10:42.543

3@DavidRicherby … that uses the same pixels/colors. Five black pixels in the "plain image" -> five black pixels in the "cipher image". – Daniel Beck – 2014-07-24T09:34:30.993

What's the meaning of your "bonus"? – Martin Ender – 2014-07-24T10:54:56.157

@MartinBüttner I thought if two answers will have same votes count, the one with more bonuses will be chosen as accepted. Pretty rare situation I think)) – Somnium – 2014-07-24T11:08:57.253

2@user2992539 All right, in that case you might want to explicitly state that this is used as the tie-breaker. Otherwise, just saying it's a bonus isn't very meaningful. – Martin Ender – 2014-07-24T11:11:45.980

.jpg isn't helpful as a test image, as saving the output as .jpg again can lose some of the quality so that the pixels no longer match. Is it worth posting a .png version of your Test image 1? I know we could write programs to take account of that and write lossless .jpg files but that seems like a separate challenge. – trichoplax – 2014-07-25T00:03:15.907

3

This question made me think of Arnold's cat map. I don't think it's quite suitable for this purpose but it's interesting in the same way - repeating the map enough times gets you back to the original image.

– trichoplax – 2014-07-25T00:30:48.623

@githubphagocyte Oops seems like I have taken wrong image from my computer) It should be PNG). About Arnold's cat map, cool, didn't know about, however, I don't understand how much iterations do you need to reverse image to original. – Somnium – 2014-07-25T06:01:08.190

The total number of iterations to get back to the original image can be hundreds even for a small image with Arnold's cat map. You could perform half this many iterations, then repeating that would give you the original again. However, for some image sizes half way through just gives you the original image upside down, which isn't much use here... Also it's only for square images - you'd have to expand it to accept arbitrary rectangles to use it here. – trichoplax – 2014-07-25T08:08:27.857

@githubphagocyte Ok, likely it's too hard to apply this algorithm here, however, thanks anyway for mentioning it. – Somnium – 2014-07-25T08:15:01.160

Could you edit the question to replace the accidental jpg image with the png image you had originally intended? – trichoplax – 2014-07-27T09:20:07.787

@githubphagocyte I can't, CodeGolf converts it automatically to JPG. It seems first time it also was auto-converted. – Somnium – 2014-07-27T18:58:24.777

It wasn't you - I've realised in uploading my scrambled images that they are being automatically converted to jpg when uploading png, but only for the larger file sizes - I'm guessing anything over 500 KB. Test image 2 is more compressed and uploads as png. The jpg format did give some interesting distortion in my output images though... – trichoplax – 2014-07-27T20:44:15.527

Ah - I see you already worked this out... – trichoplax – 2014-07-27T20:44:41.107

A similar problem was exposed to me as a introductory student in an assignment and we used a method that completely obscured the image, as opposed to some of the solutions here. I can't post my solution, as it would violate our Honor Code, but I encourage you to take a look at how we did it. If you do use this method, please do not post any code, unless you significantly alter the technique. It's a really cool little assignment for beginners that I had a lot of fun with and I would hate to have it spoiled for anyone else by being able to access a solution online. http://www.cs.princeton.edu/co

– the_hands_that_thieve – 2014-07-28T12:18:08.460

4

Now elsewhere on the Stack Exchange network: Security.SE of all places

– Doorknob – 2014-07-29T06:34:33.323

I'm bit puzzled as why don't I see the FFT as a solution here. – PermanentGuest – 2014-07-29T08:34:26.973

@PermanentGuest a Fast Fourier Transform wouldn't be self inverse for the first bonus but I'd still like to see a solution using it, and what patterns emerge in the output for the second bonus... – trichoplax – 2014-07-29T11:05:33.840

@PermanentGuest a Hartley Transform would be self inverse but neither transform would maintain the original pixel colours so both would be invalid answers here.

– trichoplax – 2014-07-29T11:08:53.513

@githubphagocyte : Ah, I forgot about the color aspect for a moment. – PermanentGuest – 2014-07-29T12:34:07.527

Answers

58

Python 2.7 (with PIL) - No Pseudorandomness

I break the image into 2 by 2 blocks (ignoring the remainder) and rotate each block by 180 degrees, then I do the same with 3 by 3 blocks, then 4, etc. up to some parameter BLKSZ. Then I do the same for BLKSZ-1, then BLKSZ-2, all the way back down to 3, then 2. This method reverses itself exactly; the unscramble function is the scramble function.

The code:

from PIL import Image
import math

im = Image.open("ST1.png", "r")
arr = im.load() #pixel data stored in this 2D array

def rot(A, n, x1, y1): #this is the function which rotates a given block
    temple = []
    for i in range(n):
        temple.append([])
        for j in range(n):
            temple[i].append(arr[x1+i, y1+j])
    for i in range(n):
        for j in range(n):
            arr[x1+i,y1+j] = temple[n-1-i][n-1-j]


xres = 800
yres = 480
BLKSZ = 50 #blocksize
for i in range(2, BLKSZ+1):
    for j in range(int(math.floor(float(xres)/float(i)))):
        for k in range(int(math.floor(float(yres)/float(i)))):
            rot(arr, i, j*i, k*i)
for i in range(3, BLKSZ+1):
    for j in range(int(math.floor(float(xres)/float(BLKSZ+2-i)))):
        for k in range(int(math.floor(float(yres)/float(BLKSZ+2-i)))):
            rot(arr, BLKSZ+2-i, j*(BLKSZ+2-i), k*(BLKSZ+2-i))

im.save("ST1OUT "+str(BLKSZ)+".png")

print("Done!")

Depending on the blocksize, you can make the computation eradicate all resemblance to the original image: (BLKSZ = 50) enter image description here enter image description here

Or make the computation efficient: (BLKSZ = 10) enter image description here enter image description here

G. H. Faust

Posted 2014-07-24T06:43:35.237

Reputation: 696

6Sound like best results will be if BLKSZ will be around half of image size. Anyway, I like algorithm and for small BLKSZ it looks like a modern art! Cool! – Somnium – 2014-07-24T15:25:18.917

11I always upvote python. – qwr – 2014-07-25T02:06:54.637

Instead of scrambling for all values from 2 to 50, maybe you should use only prime numbers? – Neil – 2014-07-25T09:47:58.710

@Neil Probably then it will look more random and less artistic. – Somnium – 2014-07-25T10:14:53.457

The BLKSZ = 10 landscape is really cool! – wchargin – 2014-07-25T15:27:15.283

It would be a trivial change to find out whether the prime number version is more or less random/artistic. – trichoplax – 2014-07-25T15:47:06.613

52

C#, Winform

Edit Changing the way you fill the coordinates array you can have different patterns - see below

Do you like this kind of pattern?

LandScape

Abstract

Bonus:

Scream Scream Scrambled

Random swap exactly one time all pixels in upper half with all pixels in lower half. Repeat the same procedure for unscrambling (bonus).

Code

Scramble.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.IO;

namespace Palette
{
    public partial class Scramble : Form
    {
        public Scramble()
        {
            InitializeComponent();
        }

        public struct Coord
        {
            public int x, y;
        }

        private void Work(Bitmap srcb, Bitmap outb)
        {
            int w = srcb.Width, h = srcb.Height;
            Coord[] coord = new Coord[w * h];

            FastBitmap fsb = new FastBitmap(srcb);
            FastBitmap fob = new FastBitmap(outb);
            fsb.LockImage();
            fob.LockImage();
            ulong seed = 0;
            int numpix = 0;
            for (int y = 0; y < h; y++)
                for (int x = 0; x < w; numpix++, x++)
                {
                    coord[numpix].x = x;
                    coord[numpix].y = y;
                    uint color = fsb.GetPixel(x, y);
                    seed += color;
                    fob.SetPixel(x, y, color);
                }
            fsb.UnlockImage();
            fob.UnlockImage();
            pbOutput.Refresh();
            Application.DoEvents();

            int half = numpix / 2;
            int limit = half;
            XorShift rng = new XorShift(seed);
            progressBar.Visible = true;
            progressBar.Maximum = limit;

            fob.LockImage();
            while (limit > 0)
            {
                int p = (int)(rng.next() % (uint)limit);
                int q = (int)(rng.next() % (uint)limit);
                uint color = fob.GetPixel(coord[p].x, coord[p].y); 
                fob.SetPixel(coord[p].x, coord[p].y, fob.GetPixel(coord[half+q].x, coord[half+q].y)); 
                fob.SetPixel(coord[half+q].x, coord[half+q].y, color); 
                limit--;
                if (p < limit)
                {
                    coord[p]=coord[limit];
                }
                if (q < limit)
                {
                    coord[half+q]=coord[half+limit];
                }
                if ((limit & 0xfff) == 0)
                {
                    progressBar.Value = limit;
                    fob.UnlockImage();
                    pbOutput.Refresh();
                    fob.LockImage();
                }
            }
            fob.UnlockImage();
            pbOutput.Refresh();
            progressBar.Visible = false; 
        }

        void DupImage(PictureBox s, PictureBox d)
        {
            if (d.Image != null)
                d.Image.Dispose();
            d.Image = new Bitmap(s.Image.Width, s.Image.Height);  
        }

        void GetImagePB(PictureBox pb, string file)
        {
            Bitmap bms = new Bitmap(file, false);
            Bitmap bmp = bms.Clone(new Rectangle(0, 0, bms.Width, bms.Height), PixelFormat.Format32bppArgb);
            bms.Dispose(); 
            if (pb.Image != null)
                pb.Image.Dispose();
            pb.Image = bmp;
        }

        private void btnOpen_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();

            openFileDialog.InitialDirectory = "c:\\temp\\";
            openFileDialog.Filter = "Image Files(*.BMP;*.JPG;*.PNG)|*.BMP;*.JPG;*.PNG|All files (*.*)|*.*";
            openFileDialog.FilterIndex = 1;
            openFileDialog.RestoreDirectory = true;

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    string file = openFileDialog.FileName;
                    GetImagePB(pbInput, file);
                    pbInput.Tag = file;
                    DupImage(pbInput, pbOutput);
                    Work(pbInput.Image as Bitmap, pbOutput.Image as Bitmap);
                    file = Path.GetDirectoryName(file) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(file) + ".scr.png";
                    pbOutput.Image.Save(file);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
                }

            }
        }
    }

    //Adapted from Visual C# Kicks - http://www.vcskicks.com/
    unsafe public class FastBitmap
    {
        private Bitmap workingBitmap = null;
        private int width = 0;
        private BitmapData bitmapData = null;
        private Byte* pBase = null;

        public FastBitmap(Bitmap inputBitmap)
        {
            workingBitmap = inputBitmap;
        }

        public BitmapData LockImage()
        {
            Rectangle bounds = new Rectangle(Point.Empty, workingBitmap.Size);

            width = (int)(bounds.Width * 4 + 3) & ~3;

            //Lock Image
            bitmapData = workingBitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
            pBase = (Byte*)bitmapData.Scan0.ToPointer();
            return bitmapData;
        }

        private uint* pixelData = null;

        public uint GetPixel(int x, int y)
        {
            pixelData = (uint*)(pBase + y * width + x * 4);
            return *pixelData;
        }

        public uint GetNextPixel()
        {
            return *++pixelData;
        }

        public void GetPixelArray(int x, int y, uint[] Values, int offset, int count)
        {
            pixelData = (uint*)(pBase + y * width + x * 4);
            while (count-- > 0)
            {
                Values[offset++] = *pixelData++;
            }
        }

        public void SetPixel(int x, int y, uint color)
        {
            pixelData = (uint*)(pBase + y * width + x * 4);
            *pixelData = color;
        }

        public void SetNextPixel(uint color)
        {
            *++pixelData = color;
        }

        public void UnlockImage()
        {
            workingBitmap.UnlockBits(bitmapData);
            bitmapData = null;
            pBase = null;
        }
    }

    public class XorShift
    {
        private ulong x; /* The state must be seeded with a nonzero value. */

        public XorShift(ulong seed)
        {
            x = seed;
        }

        public ulong next()
        {
            x ^= x >> 12; // a
            x ^= x << 25; // b
            x ^= x >> 27; // c
            return x * 2685821657736338717L;
        }
    }
} 

Scramble.designer.cs

namespace Palette
{
    partial class Scramble
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.panel = new System.Windows.Forms.FlowLayoutPanel();
            this.pbInput = new System.Windows.Forms.PictureBox();
            this.pbOutput = new System.Windows.Forms.PictureBox();
            this.progressBar = new System.Windows.Forms.ProgressBar();
            this.btnOpen = new System.Windows.Forms.Button();
            this.panel.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)(this.pbInput)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.pbOutput)).BeginInit();
            this.SuspendLayout();
            // 
            // panel
            // 
            this.panel.AutoScroll = true;
            this.panel.AutoSize = true;
            this.panel.Controls.Add(this.pbInput);
            this.panel.Controls.Add(this.pbOutput);
            this.panel.Dock = System.Windows.Forms.DockStyle.Top;
            this.panel.Location = new System.Drawing.Point(0, 0);
            this.panel.Name = "panel";
            this.panel.Size = new System.Drawing.Size(748, 306);
            this.panel.TabIndex = 3;
            // 
            // pbInput
            // 
            this.pbInput.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
            this.pbInput.Location = new System.Drawing.Point(3, 3);
            this.pbInput.MinimumSize = new System.Drawing.Size(100, 100);
            this.pbInput.Name = "pbInput";
            this.pbInput.Size = new System.Drawing.Size(100, 300);
            this.pbInput.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;
            this.pbInput.TabIndex = 3;
            this.pbInput.TabStop = false;
            // 
            // pbOutput
            // 
            this.pbOutput.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
            this.pbOutput.Location = new System.Drawing.Point(109, 3);
            this.pbOutput.MinimumSize = new System.Drawing.Size(100, 100);
            this.pbOutput.Name = "pbOutput";
            this.pbOutput.Size = new System.Drawing.Size(100, 300);
            this.pbOutput.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;
            this.pbOutput.TabIndex = 4;
            this.pbOutput.TabStop = false;
            // 
            // progressBar
            // 
            this.progressBar.Dock = System.Windows.Forms.DockStyle.Bottom;
            this.progressBar.Location = new System.Drawing.Point(0, 465);
            this.progressBar.Name = "progressBar";
            this.progressBar.Size = new System.Drawing.Size(748, 16);
            this.progressBar.TabIndex = 5;
            // 
            // btnOpen
            // 
            this.btnOpen.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
            this.btnOpen.Location = new System.Drawing.Point(12, 429);
            this.btnOpen.Name = "btnOpen";
            this.btnOpen.Size = new System.Drawing.Size(53, 30);
            this.btnOpen.TabIndex = 6;
            this.btnOpen.Text = "Start";
            this.btnOpen.UseVisualStyleBackColor = true;
            this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click);
            // 
            // Scramble
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.BackColor = System.Drawing.SystemColors.ControlDark;
            this.ClientSize = new System.Drawing.Size(748, 481);
            this.Controls.Add(this.btnOpen);
            this.Controls.Add(this.progressBar);
            this.Controls.Add(this.panel);
            this.Name = "Scramble";
            this.Text = "Form1";
            this.panel.ResumeLayout(false);
            this.panel.PerformLayout();
            ((System.ComponentModel.ISupportInitialize)(this.pbInput)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.pbOutput)).EndInit();
            this.ResumeLayout(false);
            this.PerformLayout();

        }


        private System.Windows.Forms.FlowLayoutPanel panel;
        private System.Windows.Forms.PictureBox pbOutput;
        private System.Windows.Forms.ProgressBar progressBar;
        private System.Windows.Forms.PictureBox pbInput;
        private System.Windows.Forms.Button btnOpen;
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace Palette
{
  static class Program
  {
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Scramble());
    }
  }
}

Check 'Unsafe code' in project property to compile.

Complex pattern

Scramble

Change the first part of work function, up to Application.DoEvents:

        int w = srcb.Width, h = srcb.Height;
        string Msg = "Scramble";

        Graphics gr = Graphics.FromImage(outb);

        Font f = new Font("Arial", 100, FontStyle.Bold);
        var size = gr.MeasureString(Msg, f);
        f = new Font("Arial", w / size.Width * 110, FontStyle.Bold);
        size = gr.MeasureString(Msg, f);
        gr.DrawString(Msg, f, new SolidBrush(Color.White), (w - size.Width) / 2, (h - size.Height) / 2);

        gr.Dispose();

        Coord[] coord = new Coord[w * h];
        FastBitmap fsb = new FastBitmap(srcb);
        FastBitmap fob = new FastBitmap(outb);
        fsb.LockImage();
        fob.LockImage();
        ulong seed = 1;
        int numpix = h * w;
        int c1 = 0, c2 = numpix;
        int y2 = h / 2;

        int p2 = numpix/2;

        for (int p = 0; p < p2; p++)
        {
            for (int s = 1; s > -2; s -= 2)
            {
                int y = (p2+s*p) / w;
                int x = (p2+s*p) % w;

                uint d = fob.GetPixel(x, y);
                if (d != 0)
                {
                    c2--;
                    coord[c2].x = x;
                    coord[c2].y = y;
                }
                else
                {
                    coord[c1].x = x;
                    coord[c1].y = y;
                    c1++;
                }
                fob.SetPixel(x, y, fsb.GetPixel(x, y));
            }
        }
        fsb.UnlockImage();
        fob.UnlockImage();
        pbOutput.Refresh();
        Application.DoEvents();

edc65

Posted 2014-07-24T06:43:35.237

Reputation: 31 086

1Interesting) I'm wondering whether with similar approach is possible to make more complicated patterns in output. – Somnium – 2014-07-24T18:29:54.253

1Nice idea - what happens to the middle line when there's an odd number of lines? – flawr – 2014-07-24T20:51:32.200

1@flawr the split is per pixel. If there is an odd number of pixel the last one is left untouched. If there is an odd number of rows, the left half of the middle row is 'upper side' and the right half is 'lower side'. – edc65 – 2014-07-24T21:17:13.987

1@user2992539 I think you can subdivide more - even checkerboard. With more subdivisions, the image is more recognizable. – edc65 – 2014-07-24T21:19:24.197

7Like your "Scramble" version!) – Somnium – 2014-07-25T07:08:54.043

@edc65 if there will be more pattern in output (like "Scramble") people won't see old image because they are focusing on new pattern which is more visible. – Somnium – 2014-07-25T15:55:46.600

I always upvote C#. – Ray – 2014-07-27T22:27:56.180

43

C, arbitrary blurring, easily reversible

Late to the party. Here is my entry!

This method does a scrambling blur. I call it scramblur. It is extremely simple. In a loop, it chooses a random pixel and then swaps it with a randomly chosen nearby pixel in a toroidal canvas model. You specify the maximum distance defining what "nearby pixel" means (1 means always choose an adjacent pixel), the number of iterations, and optionally a random number seed. The larger the maximum distance and the larger the number of iterations, the blurrier the result.

It is reversible by specifying a negative number of iterations (this is simply a command-line interface convenience; there is actually no such thing as negative iterations). Internally, it uses a custom 64-bit LCPRNG (linear congruential pseudorandom number generator) and pre-generates a block of values. The table allows looping through the block either forward or reverse for scrambling or unscrambling, respectively.

Demo

For the first two images, as you scroll down, each image is blurred using a higher maximum offset: Topmost is the original image (e.g., 0-pixel offset), followed by 1, 2, 4, 8, 16, 32, 64, 128, and finally 256. The iteration count is 10⁶ = 1,000,000 for all images below.

For the second two images, each image is blurred using a progressively lower offset — e.g., most blurry to least blurry — from a maximum offset of 256 down to 0. Enjoy!

Landscape Abstract

And for these next two images, you can see the progressions full-size here and here:

Breaking Bad Simpsons

Code

I hacked this together in about an hour while waking up this morning and it contains almost no documentation. I might come back in a few days and add more documentation later if people request it.

//=============================================================================
// SCRAMBLUR
//
// This program is a image-processing competition entry which scrambles or
// descrambles an image based on a pseudorandom process.  For more details,
// information, see:
//
//    http://codegolf.stackexchange.com/questions/35005
//
// It is assumed that you have the NETPBM package of image-processing tools
// installed on your system.  This can be obtained from:
//
//    http://netpbm.sourceforge.net/
//
// or by using your system's package manager, e.g., yum, apt-get, port, etc.
//
// Input to the program is a 24-bit PNM image (type "P6").  Output is same.
// Example command-line invocation:
//
// pngtopnm original.png  | scramblur 100  1000000 | pnmtopng >scrambled.png
// pngtopnm scrambled.png | scramblur 100 -1000000 | pnmtopng >recovered.png
//
//
// Todd S. Lehman, July 2014

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

typedef uint8_t uint8;
typedef uint64_t uint64;

//-----------------------------------------------------------------------------
// PIXEL STRUCTURE

#pragma pack(push, 1)
typedef struct
{
  uint8 r, g, b;     // Red, green, and blue color components
}
Pixel;
#pragma pack(pop)

//-----------------------------------------------------------------------------
// IMAGE STRUCTURE

typedef struct
{
  int width;          // Width of image in pixels
  int height;         // Height of image in pixels
  int pixel_count;    // Total number of pixels in image (e.g., width * height)
  int maxval;         // Maximum pixel component value (e.g., 255)
  Pixel *data;        // One-dimensional array of pixels
}
Image;

//-----------------------------------------------------------------------------
// 64-BIT LCG TABLE

static const long lcg64_table_length = 1000000;  // 10⁶ entries => 8 Megabytes

static uint64 lcg64_table[lcg64_table_length];

//-----------------------------------------------------------------------------
// GET 64-BIT LCG VALUE FROM TABLE

uint64 lcg64_get(long const iteration)
{
  return lcg64_table[iteration % lcg64_table_length];
}

//-----------------------------------------------------------------------------
// INITIALIZE 64-BIT LCG TABLE

void lcg64_init(uint64 const seed)
{
  uint64 x = seed;
  for (long iteration = 0; iteration < lcg64_table_length; iteration++)
  {
    uint64 const a = UINT64_C(6364136223846793005);
    uint64 const c = UINT64_C(1442695040888963407);
    x = (x * a) + c;
    lcg64_table[iteration] = x;
  }
}

//-----------------------------------------------------------------------------
// READ BINARY PNM IMAGE

Image image_read(FILE *const file)
{
  Image image = { .data = NULL };

  char *line = NULL;
  size_t linecap = 0;

  // Read image type.  (Currently only P6 is supported here.)
  if (getline(&line, &linecap, file) < 0) goto failure;
  if (strcmp(line, "P6\n") != 0) goto failure;

  // Read width and height of image in pixels.
  {
    if (getline(&line, &linecap, file) < 0) goto failure;
    char *pwidth = &line[0];
    char *pheight = strchr(line, ' ');
    if (pheight != NULL) pheight++; else goto failure;
    image.width = atoi(pwidth);
    image.height = atoi(pheight);
    image.pixel_count = image.width * image.height;
  }

  // Read maximum color value.  (Currently only 255 is supported here.)
  {
    if (getline(&line, &linecap, file) < 0) goto failure;
    image.maxval = atoi(line);
    if (image.maxval != 255)
      goto failure;
  }

  // Allocate image buffer and read image data.
  if (!(image.data = calloc(image.pixel_count, sizeof(Pixel))))
    goto failure;

  if (fread(image.data, sizeof(Pixel), image.pixel_count, file) !=
      image.pixel_count)
    goto failure;

success:
  free(line);
  return image;

failure:
  free(line);
  free(image.data); image.data = NULL;
  return image;
}

//-----------------------------------------------------------------------------
// WRITE BINARY PNM IMAGE

void image_write(const Image image, FILE *const file)
{
  printf("P6\n");
  printf("%d %d\n", image.width, image.height);
  printf("%d\n", image.maxval);
  (void)fwrite(image.data, sizeof(Pixel), image.pixel_count, file);
}

//-----------------------------------------------------------------------------
// DISCARD IMAGE

void image_discard(Image image)
{
  free(image.data);
}

//-----------------------------------------------------------------------------
// SCRAMBLE OR UNSCRAMBLE IMAGE

void image_scramble(Image image,
                    int const max_delta,
                    long const iterations,
                    uint64 const lcg64_seed)
{
  if (max_delta == 0) return;

  int neighborhood1 = (2 * max_delta) + 1;
  int neighborhood2 = neighborhood1 * neighborhood1;

  lcg64_init(lcg64_seed);

  long iteration_start = (iterations >= 0)? 0 : -iterations;
  long iteration_end   = (iterations >= 0)? iterations : 0;
  long iteration_inc   = (iterations >= 0)? 1 : -1;

  for (long iteration = iteration_start;
       iteration != iteration_end;
       iteration += iteration_inc)
  {
    uint64 lcg64 = lcg64_get(iteration);

    // Choose random pixel.
    int pixel_index = (int)((lcg64 >> 0) % image.pixel_count);

    // Choose random pixel in the neighborhood.
    int d2 = (int)((lcg64 >> 8) % neighborhood2);
    int dx = (d2 % neighborhood1) - (neighborhood1 / 2);
    int dy = (d2 / neighborhood1) - (neighborhood1 / 2);
    int other_pixel_index = pixel_index + dx + (dy * image.width);
    while (other_pixel_index < 0)
      other_pixel_index += image.pixel_count;
    other_pixel_index %= image.pixel_count;

    // Swap pixels.
    Pixel t = image.data[pixel_index];
    image.data[pixel_index] = image.data[other_pixel_index];
    image.data[other_pixel_index] = t;
  }
}

//-----------------------------------------------------------------------------
int main(const int argc, char const *const argv[])
{
  int max_delta     = (argc > 1)? atoi(argv[1]) : 1;
  long iterations   = (argc > 2)? atol(argv[2]) : 1000000;
  uint64 lcg64_seed = (argc > 3)? (uint64)strtoull(argv[3], NULL, 10) : 0;

  Image image = image_read(stdin);
  if (!image.data) { fprintf(stderr, "Invalid input\n"), exit(1); }

  image_scramble(image, max_delta, iterations, lcg64_seed);

  image_write(image, stdout);

  image_discard(image);

  return 0;
}

Todd Lehman

Posted 2014-07-24T06:43:35.237

Reputation: 1 723

4Just scrolled past this answer, looks awesome – Thomas – 2014-07-26T02:48:01.743

1This answer is really tall. Do you think you could move the extra images (i.e. everything except the two test images, fully blurred) to an off-site gallery? – Tim S. – 2014-07-28T20:47:10.760

@TimS. — done! shrunk 'em down to tiny thumbnails. – Todd Lehman – 2014-07-28T21:03:57.753

42

Python 3.4

  • Bonus 1: Self inverse: repeating restores the original image.
  • Optional key image: original image can only be restored by using the same key image again.
  • Bonus 2: Producing pattern in the output: key image is approximated in the scrambled pixels.

When bonus 2 is achieved, by using an additional key image, bonus 1 is not lost. The program is still self inverse, provided it is run with the same key image again.

Standard usage

Test image 1:

Scrambled test image 1

Test image 2:

Scrambled test image 2

Running the program with a single image file as its argument saves an image file with the pixels scrambled evenly over the whole image. Running it again with the scrambled output saves an image file with the scrambling applied again, which restores the original since the scrambling process is its own inverse.

The scrambling process is self inverse because the list of all pixels is split into 2-cycles, so that every pixel is swapped with one and only one other pixel. Running it a second time swaps every pixel with the pixel it was first swapped with, putting everything back to how it started. If there are an odd number of pixels there will be one that does not move.

Thanks to mfvonh's answer as the first to suggest 2-cycles.

Usage with a key image

Scrambling Test image 1 with Test image 2 as the key image

Scramble test 1 with test 2

Scrambling Test image 2 with Test image 1 as the key image

Scramble test 2 with test 1

Running the program with a second image file argument (the key image) divides the original image into regions based on the key image. Each of these regions is divided into 2-cycles separately, so that all of the scrambling occurs within regions, and pixels are not moved from one region to another. This spreads out the pixels over each region and so the regions become a uniform speckled colour, but with a slightly different average colour for each region. This gives a very rough approximation of the key image, in the wrong colours.

Running again swaps the same pairs of pixels in each region, so each region is restored to its original state, and the image as a whole reappears.

Thanks to edc65's answer as the first to suggest dividing the image into regions. I wanted to expand on this to use arbitrary regions, but the approach of swapping everything in region 1 with everything in region 2 meant that the regions had to be of identical size. My solution is to keep the regions insulated from each other, and simply shuffle each region into itself. Since regions no longer need to be of similar size it becomes simpler to apply arbitrary shaped regions.

Code

import os.path
from PIL import Image   # Uses Pillow, a fork of PIL for Python 3
from random import randrange, seed


def scramble(input_image_filename, key_image_filename=None,
             number_of_regions=16777216):
    input_image_path = os.path.abspath(input_image_filename)
    input_image = Image.open(input_image_path)
    if input_image.size == (1, 1):
        raise ValueError("input image must contain more than 1 pixel")
    number_of_regions = min(int(number_of_regions),
                            number_of_colours(input_image))
    if key_image_filename:
        key_image_path = os.path.abspath(key_image_filename)
        key_image = Image.open(key_image_path)
    else:
        key_image = None
        number_of_regions = 1
    region_lists = create_region_lists(input_image, key_image,
                                       number_of_regions)
    seed(0)
    shuffle(region_lists)
    output_image = swap_pixels(input_image, region_lists)
    save_output_image(output_image, input_image_path)


def number_of_colours(image):
    return len(set(list(image.getdata())))


def create_region_lists(input_image, key_image, number_of_regions):
    template = create_template(input_image, key_image, number_of_regions)
    number_of_regions_created = len(set(template))
    region_lists = [[] for i in range(number_of_regions_created)]
    for i in range(len(template)):
        region = template[i]
        region_lists[region].append(i)
    odd_region_lists = [region_list for region_list in region_lists
                        if len(region_list) % 2]
    for i in range(len(odd_region_lists) - 1):
        odd_region_lists[i].append(odd_region_lists[i + 1].pop())
    return region_lists


def create_template(input_image, key_image, number_of_regions):
    if number_of_regions == 1:
        width, height = input_image.size
        return [0] * (width * height)
    else:
        resized_key_image = key_image.resize(input_image.size, Image.NEAREST)
        pixels = list(resized_key_image.getdata())
        pixel_measures = [measure(pixel) for pixel in pixels]
        distinct_values = list(set(pixel_measures))
        number_of_distinct_values = len(distinct_values)
        number_of_regions_created = min(number_of_regions,
                                        number_of_distinct_values)
        sorted_distinct_values = sorted(distinct_values)
        while True:
            values_per_region = (number_of_distinct_values /
                                 number_of_regions_created)
            value_to_region = {sorted_distinct_values[i]:
                               int(i // values_per_region)
                               for i in range(len(sorted_distinct_values))}
            pixel_regions = [value_to_region[pixel_measure]
                             for pixel_measure in pixel_measures]
            if no_small_pixel_regions(pixel_regions,
                                      number_of_regions_created):
                break
            else:
                number_of_regions_created //= 2
        return pixel_regions


def no_small_pixel_regions(pixel_regions, number_of_regions_created):
    counts = [0 for i in range(number_of_regions_created)]
    for value in pixel_regions:
        counts[value] += 1
    if all(counts[i] >= 256 for i in range(number_of_regions_created)):
        return True


def shuffle(region_lists):
    for region_list in region_lists:
        length = len(region_list)
        for i in range(length):
            j = randrange(length)
            region_list[i], region_list[j] = region_list[j], region_list[i]


def measure(pixel):
    '''Return a single value roughly measuring the brightness.

    Not intended as an accurate measure, simply uses primes to prevent two
    different colours from having the same measure, so that an image with
    different colours of similar brightness will still be divided into
    regions.
    '''
    if type(pixel) is int:
        return pixel
    else:
        r, g, b = pixel[:3]
        return r * 2999 + g * 5869 + b * 1151


def swap_pixels(input_image, region_lists):
    pixels = list(input_image.getdata())
    for region in region_lists:
        for i in range(0, len(region) - 1, 2):
            pixels[region[i]], pixels[region[i+1]] = (pixels[region[i+1]],
                                                      pixels[region[i]])
    scrambled_image = Image.new(input_image.mode, input_image.size)
    scrambled_image.putdata(pixels)
    return scrambled_image


def save_output_image(output_image, full_path):
    head, tail = os.path.split(full_path)
    if tail[:10] == 'scrambled_':
        augmented_tail = 'rescued_' + tail[10:]
    else:
        augmented_tail = 'scrambled_' + tail
    save_filename = os.path.join(head, augmented_tail)
    output_image.save(save_filename)


if __name__ == '__main__':
    import sys
    arguments = sys.argv[1:]
    if arguments:
        scramble(*arguments[:3])
    else:
        print('\n'
              'Arguments:\n'
              '    input image          (required)\n'
              '    key image            (optional, default None)\n'
              '    number of regions    '
              '(optional maximum - will be as high as practical otherwise)\n')

JPEG image burn

.jpg files are processed very quickly, but at the cost of running too hot. This leaves a burned in after image when the original is restored:

jpg burn

But seriously, a lossy format will result in some of the pixel colours being changed slightly, which in itself makes the output invalid. When a key image is used and the shuffling of pixels is restricted to regions, all of the distortion is kept within the region it happened in, and then spread out evenly over that region when the image is restored. The difference in average distortion between regions leaves a visible difference between them, so the regions used in the scrambling process are still visible in the restored image.

Converting to .png (or any non-lossy format) before scrambling ensures that the unscrambled image is identical to the original with no burn or distortion:

png with no burn

Little details

  • A minimum size of 256 pixels is imposed on regions. If the image were allowed to split into regions that are too small, then the original image would still be partially visible after scrambling.
  • If there's more than one region with an odd number of pixels then one pixel from the second region is reassigned to the first, and so on. This means there can only ever be one region with an odd number of pixels, and so only one pixel will remain unscrambled.
  • There is a third optional argument which restricts the number of regions. Setting this to 2 for example will give two tone scrambled images. This may look better or worse depending on the images involved. If a number is specified here, the image can only be restored using the same number again.
  • The number of distinct colours in the original image also limits the number of regions. If the original image is two tone then regardless of the key image or the third argument, there can only be a maximum of 2 regions.

trichoplax

Posted 2014-07-24T06:43:35.237

Reputation: 10 499

I keep getting a list index out of range error in the no_small_pixel_regions function on line counts[value] += 1. It's something to do with the key image, but I can't figure out why. – Status – 2015-10-04T03:13:16.367

@Status if you mention me with @trichoplax in [chat] and describe what you did leading up to the error then I'll have a look and do my best to find out what is going wrong. – trichoplax – 2015-10-04T20:59:56.413

2+1 Applause! I vaguely thought about this, but found it too difficult to implement. – edc65 – 2014-07-27T21:09:57.313

1This is brilliant. I've got an entry submitted, but I like yours better because of the key image feature. – Todd Lehman – 2014-07-28T03:31:38.353

I'd be curious what these two images look like keyed against each other: http://www.lardlad.com/assets/wallpaper/simpsons1920.jpg and http://blogs.nd.edu/oblation/files/2013/09/BreakingBad.jpg (downsized to 720x450 or whatever makes sense, and of course pre-converted to PNG to avoid the JPEG burn).

– Todd Lehman – 2014-07-28T03:36:50.077

@ToddLehman yours already got my vote - I love the fine control over how scrambled things get. It's great how popularity contests tend to throw up so many different approaches. – trichoplax – 2014-07-28T09:50:18.163

2

@ToddLehman my algorithm is limited by the need to be its own inverse. If you want to see some really interesting approaches to shuffling one image to resemble another, you should look at American Gothic in the palette of Mona Lisa. Some of those programs would do amazing things with the images you mention.

– trichoplax – 2014-07-28T10:05:54.863

Thanks @edc65 - and thanks for the inspiration to try this approach. – trichoplax – 2014-07-28T10:08:22.717

@ToddLehman you're free to try this program on any images you like - everything on stack exchange is licensed under Creative Commons CC BY-SA 3.0. On the full size 1920x1200 images it may take 2 or 3 minutes to run (depending on your machine). I can only post images that are compatible with that license though. Some search engines have an option to show only images which are free to share.

– trichoplax – 2014-07-28T10:34:28.660

@githubphagocyte — I get an error message "ImportError: No module named PIL". The only Python program I've ever written is "Hello, world." Is the missing PIL module likely to be something that's going to be a rabbit hole for me to get installed, or is it as simple as a CPAN type of installation? – Todd Lehman – 2014-07-28T11:08:49.467

@ToddLehman It uses Pillow, the fork of PIL for Python 3. It's in the Python Package Index so you can just download the relevant automatic installer for your system - or if you prefer to compile things yourself the source is there too.

– trichoplax – 2014-07-28T11:15:48.103

2The key image feature puts this head and shoulders above the rest. – Jack Aidley – 2014-07-29T14:19:37.273

33

Here is a non-random transform for a change

  1. Put all even columns on the left and all odd columns on the right.
  2. repeat nx times
  3. do the same for rows ny times

The transformation is almost self-inverse, repeating the transformation a total of size_x times (in x-direction) returns the original image. I didn't figure out the exact math, but using integer multiples of int(log_2(size_x)) produces the best shuffling with the smallest ghost images

shuffled mountains enter image description here

from numpy import *
from pylab import imread, imsave

def imshuffle(im, nx=0, ny=0):
    for i in range(nx):
        im = concatenate((im[:,0::2], im[:,1::2]), axis=1)
    for i in range(ny):
        im = concatenate((im[0::2,:], im[1::2,:]), axis=0)
    return im

im1 = imread('circles.png')
im2 = imread('mountain.jpg')

imsave('s_circles.png', imshuffle(im1, 7,7))
imsave('s_mountain.jpg', imshuffle(im2, 8,9))

This is how the first steps 20 iterations look like (nx=ny, note the effect of different resolutions) enter image description here

DenDenDo

Posted 2014-07-24T06:43:35.237

Reputation: 2 811

7That is a really cool algorithm. And you should totally get a bonus for using the Lena Söderberg pic. :) – Todd Lehman – 2014-07-26T03:13:21.467

Always upvote Lena – None – 2014-07-29T01:33:28.673

24

Mathematica

This is pretty straightforward. I pick 5 * nPixels random coordinate pairs and swap those two pixels (which completely obscures the picture). To unscramble it I do the same in reverse. Of course, I need to seed the PRNG to get the same coordinate pairs on both steps.

scramble[image_] := Module[
   {data, h, w, temp},
   data = ImageData@image;
   {h, w} = Most@Dimensions@data;
   SeedRandom[42];
   (
      temp = data[[#[[1]], #[[2]]]];
      data[[#[[1]], #[[2]]]] = data[[#2[[1]], #2[[2]]]];
      data[[#2[[1]], #2[[2]]]] = temp;
      ) & @@@
    Partition[
     Transpose@{RandomInteger[h - 1, 10*h*w] + 1, 
       RandomInteger[w - 1, 10*h*w] + 1}, 2];
   Image@data
   ];
unscramble[image_] := Module[
   {data, h, w, temp},
   data = ImageData@image;
   {h, w} = Most@Dimensions@data;
   SeedRandom[42];
   (
      temp = data[[#[[1]], #[[2]]]];
      data[[#[[1]], #[[2]]]] = data[[#2[[1]], #2[[2]]]];
      data[[#2[[1]], #2[[2]]]] = temp;
      ) & @@@
    Reverse@
     Partition[
      Transpose@{RandomInteger[h - 1, 10*h*w] + 1, 
        RandomInteger[w - 1, 10*h*w] + 1}, 2];
   Image@data
   ];

The only difference between the two functions is Reverse@ in unscramble. Both functions take an actual image object. You can use them as follows:

in = Import["D:\\Development\\CodeGolf\\image-scrambler\\circles.png"]
scr = scramble[im]
out = unscramble[scr]

out and in are identical. Here is what scr looks like:

enter image description here enter image description here

Martin Ender

Posted 2014-07-24T06:43:35.237

Reputation: 184 808

4Great! Only problem is that it is more safe to make PRNG yourself, because if after some time Mathematica thinks to change PRNG algorithm, this won't decode old encoded images! – Somnium – 2014-07-24T08:10:49.583

1Nice. You should be able to achieve the same result with Permute and FindPermutation. – DavidC – 2014-07-24T08:10:58.433

I'm not sure I understand. You can enter the precise permutation you want as a list of cycles. – DavidC – 2014-07-24T08:13:13.457

@DavidCarraher Hm, interesting. Wouldn't I have to remember the original permutation for using FindPermutation though? – Martin Ender – 2014-07-24T08:16:05.863

Or maybe something as {c, a, b}[[{2, 3, 1}]] can be used? – Somnium – 2014-07-24T08:16:45.113

22

C# (+ Bonus for Symmetric Algorithm)

This works by finding an x such that x^2 == 1 mod (number of pixels in image), and then multiplying each pixel's index by x in order to find its new location. This lets you use the exact same algorithm to scramble and unscramble an image.

using System.Drawing;
using System.IO;
using System.Numerics;

namespace RearrangePixels
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (var arg in args)
                ScrambleUnscramble(arg);
        }

        static void ScrambleUnscramble(string fileName)
        {
            using (var origImage = new Bitmap(fileName))
            using (var newImage = new Bitmap(origImage))
            {
                BigInteger totalPixels = origImage.Width * origImage.Height;
                BigInteger modSquare = GetSquareRootOf1(totalPixels);
                for (var x = 0; x < origImage.Width; x++)
                {
                    for (var y = 0; y < origImage.Height; y++)
                    {
                        var newNum = modSquare * GetPixelNumber(new Point(x, y), origImage.Size) % totalPixels;
                        var newPoint = GetPoint(newNum, origImage.Size);
                        newImage.SetPixel(newPoint.X, newPoint.Y, origImage.GetPixel(x, y));
                    }
                }
                newImage.Save("scrambled-" + Path.GetFileName(fileName));
            }
        }

        static BigInteger GetPixelNumber(Point point, Size totalSize)
        {
            return totalSize.Width * point.Y + point.X;
        }

        static Point GetPoint(BigInteger pixelNumber, Size totalSize)
        {
            return new Point((int)(pixelNumber % totalSize.Width), (int)(pixelNumber / totalSize.Width));
        }

        static BigInteger GetSquareRootOf1(BigInteger modulo)
        {
            for (var i = (BigInteger)2; i < modulo - 1; i++)
            {
                if ((i * i) % modulo == 1)
                    return i;
            }
            return modulo - 1;
        }
    }
}

first test image, scrambled

second test image, scrambled

Tim S.

Posted 2014-07-24T06:43:35.237

Reputation: 615

1Clever one) Will be there always solution to that congruence equation? – Somnium – 2014-07-25T12:18:22.680

1

@user2992539 There will always be the trivial solutions, 1 (original image) and modulo-1 (inverted/reversed image). Most numbers have non-trivial solutions, but there are some exceptions, it seems. (related to the prime factorization of modulo)

– Tim S. – 2014-07-25T12:26:28.920

As I understand trivial solutions lead to image similar to input one. – Somnium – 2014-07-25T15:36:45.720

Correct: 1 outputs the original image, and -1 outputs e.g. http://imgur.com/EiE6VW2

– Tim S. – 2014-07-25T15:52:38.203

19

C#, self-inverse, no randomness

If the original image has dimensions that are powers of two, then each row and column is exchanged with the row and column that has the reversed bit pattern, for example for a image of width 256 then row 0xB4 is exchanged with row 0x2D. Images of other sizes are split into the rectangles with sides of powers of 2.

namespace CodeGolf
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (var arg in args)
                Scramble(arg);
        }

        static void Scramble(string fileName)
        {
            using (var origImage = new System.Drawing.Bitmap(fileName))
            using (var tmpImage = new System.Drawing.Bitmap(origImage))
            {
                {
                    int x = origImage.Width;
                    while (x > 0) {
                       int xbit = x & -x;
                        do {
                            x--;
                            var xalt = BitReverse(x, xbit);
                            for (int y = 0; y < origImage.Height; y++)
                                tmpImage.SetPixel(xalt, y, origImage.GetPixel(x, y));
                        } while ((x & (xbit - 1)) != 0);
                    }
                }
                {
                    int y = origImage.Height;
                    while (y > 0) {
                        int ybit = y & -y;
                        do {
                            y--;
                            var yalt = BitReverse(y, ybit);
                            for (int x = 0; x < origImage.Width; x++)
                                origImage.SetPixel(x, yalt, tmpImage.GetPixel(x, y));
                        } while ((y & (ybit - 1)) != 0);
                    } 
                }
                origImage.Save(System.IO.Path.GetFileNameWithoutExtension(fileName) + "-scrambled.png");
            }
        }

        static int BitReverse(int n, int bit)
        {
            if (bit < 4)
                return n;
            int r = n & ~(bit - 1);
            int tmp = 1;
            while (bit > 1) {
                bit >>= 1;
                if ((n & bit) != 0)
                    r |= tmp;
                tmp <<= 1;
            }
            return r;
        }
    }
}

First image:

Scrambled first image

Second image:

Scrambled second image

Neil

Posted 2014-07-24T06:43:35.237

Reputation: 95 035

2I like the "plaid" output on this one. – Brian Rogers – 2014-07-26T04:49:41.173

14

C#

Same method for scrambling and unscrambling. I would appreciate suggestions on improving this.

using System;
using System.Drawing;
using System.Linq;

public class Program
{
    public static Bitmap Scramble(Bitmap bmp)
    {
        var res = new Bitmap(bmp);
        var r = new Random(1);

        // Making lists of even and odd numbers and shuffling them
        // They contain numbers between 0 and picture.Width (or picture.Height)
        var rX = Enumerable.Range(0, bmp.Width / 2).Select(x => x * 2).OrderBy(x => r.Next()).ToList();
        var rrX = rX.Select(x => x + 1).OrderBy(x => r.Next()).ToList();
        var rY = Enumerable.Range(0, bmp.Height / 2).Select(x => x * 2).OrderBy(x => r.Next()).ToList();
        var rrY = rY.Select(x => x + 1).OrderBy(x => r.Next()).ToList();

        for (int y = 0; y < bmp.Height; y++)
        {
            for (int x = 0; x < rX.Count; x++)
            {
                // Swapping pixels in a row using lists rX and rrX
                res.SetPixel(rrX[x], y, bmp.GetPixel(rX[x], y));
                res.SetPixel(rX[x], y, bmp.GetPixel(rrX[x], y));
            }
        }
        for (int x = 0; x < bmp.Width; x++)
        {
            for (int y = 0; y < rY.Count; y++)
            {
                // Swapping pixels in a column using sets rY and rrY
                var px = res.GetPixel(x, rrY[y]);
                res.SetPixel(x, rrY[y], res.GetPixel(x, rY[y]));
                res.SetPixel(x, rY[y], px);
            }
        }

        return res;
    }
}

Outputs results in psychedelic plaid First one Second one

Ray Poward

Posted 2014-07-24T06:43:35.237

Reputation: 281

Nice that this has some striped pattern) – Somnium – 2014-07-24T14:01:03.507

1Can you please swap the 2 images? In the question the mountains image is first. – A.L – 2014-07-24T18:33:12.127

1Could you include a brief explanation of the algorithm? – trichoplax – 2014-07-24T20:54:29.000

14

Python 2 (self-inverse, no randomness, context-sensitive)

This won't win any prizes for "least recognizable", but maybe it can score as "interesting". :-)

I wanted to make something context-sensitive, where the scrambling of the pixels actually depends on the image itself.

The idea is quite simple: Sort all pixels according to some arbitrary value derived from the pixel's colour and then swap the positions of the first pixel in that list with the last, the second with the second to last, and so on.

Unfortunately in this simple approach there is a problem with pixels of the same colour, so to still make it self-inverse my program grew a bit more complicated...

from PIL import Image

img = Image.open('1.png', 'r')
pixels = img.load()
size_x, size_y = img.size

def f(colour):
    r,g,b = colour[:3]
    return (abs(r-128)+abs(g-128)+abs(b-128))//128

pixel_list = [(x,y,f(pixels[x,y])) for x in xrange(size_x) for y in xrange(size_y)]
pixel_list.sort(key=lambda x: x[2])
print "sorted"

colours = {}
for p in pixel_list:
    if p[2] in colours:
        colours[p[2]] += 1
    else:
        colours[p[2]] = 1
print "counted"

for n in set(colours.itervalues()):
    pixel_group = [p for p in pixel_list if colours[p[2]]==n]
    N = len(temp_list)
    for p1, p2 in zip(pixel_group[:N//2], pixel_group[-1:-N//2:-1]):
        pixels[p1[0],p1[1]], pixels[p2[0],p2[1]] = pixels[p2[0],p2[1]], pixels[p1[0],p1[1]]
print "swapped"

img.save('1scrambled.png')
print "saved"

This is the result: (abs(r-128)+abs(g-128)+abs(b-128))//128 (abs(r-128)+abs(g-128)+abs(b-128))//128

You can achieve quite different results by changing the hash function f:

  • r-g-b:

    r-g-b

  • r+g/2.**8+b/2.**16:

    r+g/2.**8+b/2.**16

  • math.sin(r+g*2**8+b*2**16):

    math.sin(r+g*2**8+b*2**16)

  • (r+g+b)//600:

    (r+g+b)//600

  • 0:

    0

Emil

Posted 2014-07-24T06:43:35.237

Reputation: 1 438

3WOW! This one is great!!! Nice work! – Todd Lehman – 2014-08-03T07:59:50.907

1That's the most interesting one so far. Good work! – bebe – 2014-08-15T11:19:52.327

12

Mathematica (+ bonus)

This collapses the color channels and scrambles the image as one long list of data. The result is an even less recognizable scrambled version because it does not have the same color distribution as the original (since that data was scrambled too). This is most obvious in the second scrambled image, but if you look closely you will see the same effect in the first as well. The function is its own inverse.

There was a comment that this may not be valid because it scrambles per channel. I think it should be, but it's not a big deal. The only change necessary to scramble whole pixels (instead of per channel) would be to change Flatten @ x to Flatten[x, 1] :)

ClearAll @ f;

f @ x_ := 
  With[
    {r = SeedRandom[Times @@ Dimensions @ x], f = Flatten @ x},
    ArrayReshape[
      Permute[f, Cycles @ Partition[RandomSample @ Range @ Length @ f, 2]],
      Dimensions @ x]]

Explanation

Defines a function f that takes a 2-dimensional array x. The function uses the product of the image's dimensions as a random seed, and then flattens the array to a 1-dimensional list f (locally shadowed). Then it creates a list of the form {1, 2, ... n} where n is the length of f, randomly permutes that list, partitions it into segments of 2 (so, e.g., {{1, 2}, {3, 4}, ...} (dropping last number if the dimensions are both odd), and then permutes f by swapping the values at the positions indicated in each sublist just created, and finally it reshapes the permuted list back to the original dimensions of x. It scrambles per channel because in addition to collapsing the image dimensions the Flatten command also collapses the channel data in each pixel. The function is its own inverse because the cycles include only two pixels each.

Usage

img1=Import@"http://i.stack.imgur.com/2C2TY.jpg"//ImageData;
img2=Import@"http://i.stack.imgur.com/B5TbK.png"//ImageData;

f @ img1 // Image

enter image description here

f @ f @ img1 // Image

enter image description here

f @ img2 // Image

enter image description here

f @ f @ img2 // Image

enter image description here

Here's using Flatten[x, 1].

g@x_ := With[{r = SeedRandom[Times @@ Dimensions @ x], f = Flatten[x, 1]}, 
  ArrayReshape[
   Permute[f, Cycles@Partition[RandomSample@Range@Length@f, 2]], 
   Dimensions@x]]

g@img2 // Image

enter image description here

mfvonh

Posted 2014-07-24T06:43:35.237

Reputation: 300

1My guess is that this doesn't meet the criteria, since it swaps on a smaller than pixel scale. – trichoplax – 2014-07-24T21:04:09.370

1I don't think it's a valid answer, but I also really like it. It's a fascinating twist, so +1 anyway... – trichoplax – 2014-07-24T21:05:33.197

1@githubphagocyte See update :) – mfvonh – 2014-07-24T23:38:05.800

Great - I reached for the +1 again but of course I can't do it twice... – trichoplax – 2014-07-25T00:45:13.733

I take it this is its own inverse? I'm not familiar with Mathematica notation... – trichoplax – 2014-07-25T00:46:34.920

1@githubphagocyte Oh right, I forget the short syntax can be bizarre. Yes. f @ f @ img1 // Image is (in full syntax) Image[f[f[img1]]] – mfvonh – 2014-07-25T01:04:45.500

10

Mathematica-Use a permutation to scramble and its inverse to unscramble.

A jpg image is an three-dimensional array of {r,g,b} pixel colors. (The 3 dimensions structure the set of pixels by row, column, and color). It can be flattened into a list of {r,g,b} triples, then permuted according to a "known" cycle list, and finally re-assembled into an array of the original dimensions. The result is a scrambled image.

Unscrambling takes the scrambled image and processes it with the reverse of the cycle list. It outputs, yes, the original image.

So a single function (in the present case, scramble) serves for scrambling as well as unscrambling pixels in an image.

An image is input along with a seed number (to ensure that the Random number generator will be in the same state for scrambling and unscrambling). When the parameter, reverse, is False, the function will scramble. When it is True, the function will unscramble.


scramble

The pixels are flattened and a random list of cycles is generated. Permute uses cycles to switch positions of pixels in the flattened list.

unscramble

The same function, scramble is used for unscrambling. However the order of the cycle list is reversed.

scramble[img_,s_,reverse_:False,imgSize_:300]:=
  Module[{i=ImageData[img],input,r},input=Flatten[i,1];SeedRandom[s];
  r=RandomSample@Range[Length[input]];Image[ArrayReshape[Permute[input,
  Cycles[{Evaluate@If[reverse,Reverse@r,r]}]],Dimensions[i]],ImageSize->imgSize]]

Examples

The same seed (37) is used for scrambling and unscrambling.

This produces the scrambled image of the mountain. The picture below shows that the variable scrambledMount can be substituted by the actual image of the mountain scene.

scrambledMount=scramble[mountain, 37, True]

mount1


Now we run the inverse; scrambledMount is entered and the original picture is recuperated.

 scramble[scrambledMount, 37, True]

mount2


Same thing for the circles:

circles1


 scramble[scrambledCircles, 37, True]

circles2

DavidC

Posted 2014-07-24T06:43:35.237

Reputation: 24 524

I can't see how an image could be a 3 dimensional array. – edc65 – 2014-07-25T19:52:59.593

1@edc65, Rows x Columns x Colors. The mountain image is 422 rows by 800 columns by 3 colors. If the array is flattened, it yields 1012800 pieces of data as a one dimensional array, i.e. as a list. – DavidC – 2014-07-25T20:29:04.753

@edc65 I should add that Permute was used to rearrange the colors as rgb triples. I didn't flatten that any more because I wasn't interested in making any changes within the list of colors of any pixel. If you consider the rgb info, {r,g,b} as an element, then we are talking about a 2D array. Viewed this way, it makes complete sense to raise the question (How could an image be a 3 dimensional array?) that you raised. In fact, it may be more normal to regard an image as a 2D array, disregarding the fact that the rgb elements add another dimension. – DavidC – 2014-07-26T05:37:37.410

10

Matlab (+ bonus)

I basically switch the position of two pixels at random and tag each pixel that has been switched so it will not be switched again. The same script can be used again for the 'decryption' because I reset the random number generator each time. This is done untill almost all pixels are switched (thats why stepsize is greater than 2)

EDIT: Just saw that Martin Büttner used a similar approach - I did not intend to copy the idea - I began writing my code when there were no answers, so sorry for that. I still think my version uses some different ideas=) (And my algorithm is far more inefficient if you look at the bit where the two random coordinates get picked^^)

Images

1 2

Code

img = imread('shuffle_image2.bmp');
s = size(img)
rand('seed',0)
map = zeros(s(1),s(2));
for i = 1:2.1:s(1)*s(2) %can use much time if stepsize is 2 since then every pixel has to be exchanged
    while true %find two unswitched pixels
        a = floor(rand(1,2) .* [s(1),s(2)] + [1,1]);
        b = floor(rand(1,2) .* [s(1),s(2)] + [1,1]);
        if map(a(1),a(2)) == 0 && map(b(1),b(2)) == 0
            break
        end
    end
    %switch
    map(a(1),a(2)) = 1;
    map(b(1),b(2)) = 1;
    t = img(a(1),a(2),:);
    img(a(1),a(2),:) = img(b(1),b(2),:);
    img(b(1),b(2),:) = t;
end
image(img)
imwrite(img,'output2.png')

flawr

Posted 2014-07-24T06:43:35.237

Reputation: 40 560

I don't fully understand, does your code applied second time on encrypted image will decrypt it? – Somnium – 2014-07-24T08:46:58.737

2Exactly: Each time exactly two pixel get swapped, and will not get swapped again during the whole process. Because the 'random' numbers are both times exactly the same (due to the resetting of the random number generator), the pixel pairs will get swapped back. (The RNG relies always on the previous generated number for generating the next one, I hope this clear.) – flawr – 2014-07-24T08:50:17.960

That's cool, bonus achieved! – Somnium – 2014-07-24T08:51:56.397

Yay thanks!!! =) – flawr – 2014-07-24T08:53:02.930

I like your solution more than Martin Büttner's) Too bad I have't worked with Matlab, only Mathematica, but I think I got your idea. – Somnium – 2014-07-24T08:54:40.007

1Ha, that was actually my initial idea, but then I couldn't be bothered to make sure each pixel is swapped exactly once, because I had to get to work. :D +1! – Martin Ender – 2014-07-24T09:07:27.630

Haha, nice=) I hope you appreciate my elegant algorithm for findin the next pair of pixels that have not been swapped yet=) – flawr – 2014-07-24T09:38:10.467

3

@user2992539 Well check out Octave which is a nice opensource alternative for matlab, and you can run 99% of matlab code directly in octave.

– flawr – 2014-07-24T09:46:30.077

2I think if you squint really hard at your pictures you can still see some structure from the input (which is due to not moving all pixels). I guess if you changed your selection algorithm to run in O(1) instead of O(∞), you could fix that. ;) – Martin Ender – 2014-07-24T10:38:41.263

@MartinBüttner Thanks for noticing, I haven't seen that some parts of images are still visible. Maybe one should swap all pixels (except 1 pixel if area is odd)? – Somnium – 2014-07-24T10:53:46.440

10

Python

I like this puzzle, he seemed interesting and I came with a wraped and Movement function to apply on the image.

Wraped

I read the picture as a text (from left to right, up and down) and write it as a snail shell.

This function is cyclic: there is a n in N, f^(n)(x)=x for example, for a picture of 4*2, f(f(f(x)))=x

Movement

I take a random number and move each column and ligne from it

Code

# Opening and creating pictures
img = Image.open("/home/faquarl/Bureau/unnamed.png")
PM1 = img.load()
(w,h) = img.size
img2 = Image.new( 'RGBA', (w,h), "black") 
PM2 = img2.load()
img3 = Image.new( 'RGBA', (w,h), "black") 
PM3 = img3.load()

# Rotation
k = 0
_i=w-1
_j=h-1
_currentColMin = 0
_currentColMax = w-1
_currentLigMin = 0
_currentLigMax = h-1
_etat = 0
for i in range(w):
    for j in range(h):
        PM2[_i,_j]=PM1[i,j]
        if _etat==0:
            if _currentColMax == _currentColMin:
                _j -= 1
                _etat = 2
            else:
                _etat = 1
                _i -= 1
        elif _etat==1:
            _i -= 1
            if _j == _currentLigMax and _i == _currentColMin:
                _etat = 2
        elif _etat==2:
            _j -= 1
            _currentLigMax -= 1
            if _j == _currentLigMin and _i == _currentColMin:
                _etat = 5
            else:
                _etat = 3
        elif _etat==3:
            _j -= 1
            if _j == _currentLigMin and _i == _currentColMin:
                _etat = 4
        elif _etat==4:
            _i += 1
            _currentColMin += 1
            if _j == _currentLigMin and _i == _currentColMax:
                _etat = 7
            else:
                _etat = 5
        elif _etat==5:
            _i += 1
            if _j == _currentLigMin and _i == _currentColMax:
                _etat = 6
        elif _etat==6:
            _j += 1
            _currentLigMin += 1
            if _j == _currentLigMax and _i == _currentColMax:
                _etat = 1
            else:
                _etat = 7
        elif _etat==7:
            _j += 1
            if _j == _currentLigMax and _i == _currentColMax:
                _etat = 8
        elif _etat==8:
            _i -= 1
            _currentColMax -= 1
            if _j == _currentLigMax and _i == _currentColMin:
                _etat = 3
            else:
                _etat = 1
        k += 1
        if k == w * h:
            i = w
            j = h
# Movement
if w>h:z=w
else:z=h
rand.seed(z)
a=rand.randint(0,h)
for i in range(w):
  for j in range(h):
  if i%2==0:
    PM3[(i+a)%w,(j+a)%h]=PM2[i,j]
  else:
    PM3[(i-a)%w,(j-a)%h]=PM2[i,j]
# Rotate Again

Pictures

First rotation: enter image description here

then permutation: enter image description here

And with the last rotaion: enter image description here

As for the other example: enter image description here

Faquarl

Posted 2014-07-24T06:43:35.237

Reputation: 101

2How do you restore the original image? – trichoplax – 2014-07-31T13:37:42.940

If it's just rotation, I can do it a certain amount of time (depends of the size). However, if I had the permutations I'm not sure if it's cyclic so I just have a second function which only change is what PM2[_i,_j]=PM1[i,j] became PM2[i,j]=PM1[_i,_j] and PM3[(i+a)%w,(j+a)%h]=PM2[i,j] became PM3[(i-a)%w,(j-a)%h]=PM2[i,j]. I'm looking for a way to do it without chang these two lines – Faquarl – 2014-08-01T07:30:50.743

8

VB.NET (+ bonus)

This uses flawr's idea, thanks to him, however this uses different swapping and checking algorithm. Program encodes and decodes the same way.

Imports System

Module Module1

    Sub swap(ByVal b As Drawing.Bitmap, ByVal i As Integer, ByVal j As Integer)
        Dim c1 As Drawing.Color = b.GetPixel(i Mod b.Width, i \ b.Width)
        Dim c2 As Drawing.Color = b.GetPixel(j Mod b.Width, j \ b.Width)
        b.SetPixel(i Mod b.Width, i \ b.Width, c2)
        b.SetPixel(j Mod b.Width, j \ b.Width, c1)
    End Sub

    Sub Main(ByVal args() As String)
        For Each a In args
            Dim f As New IO.FileStream(a, IO.FileMode.Open)
            Dim b As New Drawing.Bitmap(f)
            f.Close()
            Dim sz As Integer = b.Width * b.Height - 1
            Dim w(sz) As Boolean
            Dim r As New Random(666)
            Dim u As Integer, j As Integer = 0
            Do While j < sz
                Do
                    u = r.Next(0, sz)
                Loop While w(u)
                ' swap
                swap(b, j, u)
                w(j) = True
                w(u) = True
                Do
                    j += 1
                Loop While j < sz AndAlso w(j)
            Loop
            b.Save(IO.Path.ChangeExtension(a, "png"), Drawing.Imaging.ImageFormat.Png)
            Console.WriteLine("Done!")
        Next
    End Sub

End Module

Output images:

Somnium

Posted 2014-07-24T06:43:35.237

Reputation: 2 537

8

After being reminded that this is about to swap pixels and not alter them, here is my solution for this:

Scrambled: enter image description here

Restored: enter image description here

This is done by randomizing the pixel order, but to be able to restore it, the randomization is fixed. This is done by using a pseudo-random with a fixed seed and generate a list of indexes that describe which pixels to swap. As the swap will be the same, the same list will restore the original image.

public class ImageScramble {

  public static void main(String[] args) throws IOException {
    if (args.length < 2) {
      System.err.println("Usage: ImageScramble <fileInput> <fileOutput>");
    } else {
      // load image
      final String extension = args[0].substring(args[0].lastIndexOf('.') + 1);
      final BufferedImage image = ImageIO.read(new File(args[0]));
      final int[] pixels = image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());

      // create randomized swap list
      final ArrayList<Integer> indexes = IntStream.iterate(0, i -> i + 1).limit(pixels.length).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
      Collections.shuffle(indexes, new Random(1337));

      // swap all pixels at index n with pixel at index n+1
      int tmp;
      for (int i = 0; i < indexes.size(); i += 2) {
        tmp = pixels[indexes.get(i)];
        pixels[indexes.get(i)] = pixels[indexes.get(i + 1)];
        pixels[indexes.get(i + 1)] = tmp;
      }

      // write image to disk
      final BufferedImage imageScrambled = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
      imageScrambled.setRGB(0, 0, imageScrambled.getWidth(), imageScrambled.getHeight(), pixels, 0, imageScrambled.getWidth());
      ImageIO.write(imageScrambled, extension, new File(args[1]));
    }
  }
}

Note that using this algorithm on a lossy compression format will not produce the same result, as the image format will alter the data. This should work fine with any loss-less codec like PNG.

TwoThe

Posted 2014-07-24T06:43:35.237

Reputation: 359

8

Mathematica

We define a helper function h and the scrambling function scramble as:

h[l_List, n_Integer, k_Integer: 1] := 
  With[{ m = Partition[l, n, n, 1, 0] }, 
    Flatten[
      Riffle[
        RotateLeft[ m[[ ;; , {1} ]] , k ],
        m[[ ;; , 2;; ]]
      ], 1
    ] [[ ;; Length[l] ]]
  ];

scramble[img_Image, k_Integer] :=
  Module[{ list , cNum = 5 },
    Which[
      k > 0,    list = Prime@Range[cNum],
      k < 0,    list = Reverse@Prime@Range[cNum],
      True,     list = {}
    ];
    Image[
      Transpose[
        Fold[ h[ #1, #2, k ] &, #, list ] & /@
        Transpose[
          Fold[ h[#1, #2, k] &, #, list ] & /@ ImageData[img]
        ]
      ]
    ]
  ];

After loading an image, you can call scramble[img, k] where k is any integer, to scramble the image. Calling again with -k will descramble. (If k is 0, then no change is made.) Typically k should be chosen to be something like 100, which gives a pretty scrambled image:

Example output 1

Example output 2

Frxstrem

Posted 2014-07-24T06:43:35.237

Reputation: 676

7

Matlab: Row and Column Scrambling based on row/column sum invariances

This seemed like a fun puzzle, so I had a think about it and came up with the following function. It is based on the invariance of row and column pixel-value sums during circular shifting: it shifts each row, then each column, by the total sum of the row/column's pixel values (assuming a uint8 for whole number in the shift-variable). This can then be reversed by shifting each column then row by their sum-value in the opposite direction.

It's not as pretty as the others, but I like that it is non-random and fully specified by the image - no choosing parameters.

I originally designed it to shift each color channel separately, but then I noticed the specification to only move full pixels.

function pic_scramble(input_filename)
i1=imread(input_filename);
figure;
subplot(1,3,1);imagesc(i1);title('Original','fontsize',20);

i2=i1;
for v=1:size(i1,1)
    i2(v,:,:)=circshift(i2(v,:,:),sum(sum(i2(v,:,:))),2);
end
for w=1:size(i2,2)
    i2(:,w,:)=circshift(i2(:,w,:),sum(sum(i2(:,w,:))),1);
end
subplot(1,3,2);imagesc(i2);title('Scrambled','fontsize',20);

i3=i2;
for w=1:size(i3,2)
    i3(:,w,:)=circshift(i3(:,w,:),-1*sum(sum(i3(:,w,:))),1);
end
for v=1:size(i3,1)
    i3(v,:,:)=circshift(i3(v,:,:),-1*sum(sum(i3(v,:,:))),2);
end
subplot(1,3,3);imagesc(i3);title('Recovered','fontsize',20);

First test image Secont test image

Hugh Nolan

Posted 2014-07-24T06:43:35.237

Reputation: 191

6

Java

This program randomly swaps pixels (creates pixel to pixel mapping), but instead of random function it uses Math.sin() (integer x). It's fully reversible. With test images it creates some patterns.

Parameters: integer number (number of passes, negative number to reverse, 0 does nothing), input mage and output image (can be the same). Output file should be in format that uses lossless compression.

1 pass: enter image description here enter image description here

100 passes (it takes a few minutes to do it): enter image description here enter image description here

Code:

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class Test{

public static void main(String... args) {
    String in = "image.png";
    String out = in;
    int passes = 0;
    if (args.length < 1) {
        System.out.println("no paramem encryptimg, 1 pass, reading and saving image.png");
        System.out.println("Usage: pass a number. Negative - n passes of decryption, positive - n passes of encryption, 0 - do nothing");
    } else {
        passes = Integer.parseInt(args[0]);
        if (args.length > 1) {
            in = args[1];
        }
        if(args.length > 2){
            out = args[2];
        }
    }
    boolean encrypt = passes > 0;
    passes = Math.abs(passes);
    for (int a = 0; a < passes; a++) {
        BufferedImage img = null;
        try {
            img = ImageIO.read(new File(a == 0 ? in : out));
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        int pixels[][] = new int[img.getWidth()][img.getHeight()];
        int[][] newPixels = new int[img.getWidth()][img.getHeight()];
        for (int x = 0; x < pixels.length; x++) {
            for (int y = 0; y < pixels[x].length; y++) {
                pixels[x][y] = img.getRGB(x, y);
            }
        }
        int amount = img.getWidth() * img.getHeight();
        int[] list = new int[amount];
        for (int i = 0; i < amount; i++) {
            list[i] = i;
        }
        int[] mapping = new int[amount];
        for (int i = amount - 1; i >= 0; i--) {
            int num = (Math.abs((int) (Math.sin(i) * amount))) % (i + 1);
            mapping[i] = list[num];
            list[num] = list[i];
        }
        for (int xz = 0; xz < amount; xz++) {
            int x = xz % img.getWidth();
            int z = xz / img.getWidth();
            int xzMap = mapping[xz];
            int newX = xzMap % img.getWidth();
            int newZ = xzMap / img.getWidth();
            if (encrypt) {
                newPixels[x][z] = pixels[newX][newZ];
            } else {
                newPixels[newX][newZ] = pixels[x][z];
            }
        }
        BufferedImage newImg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < pixels.length; x++) {
            for (int y = 0; y < pixels[x].length; y++) {
                newImg.setRGB(x, y, newPixels[x][y]);
            }
        }

        try {
            String[] s = out.split("\\.");
            ImageIO.write(newImg, s[s.length - 1],
                    new File(out));
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }
}
}

barteks2x

Posted 2014-07-24T06:43:35.237

Reputation: 281

6

Python 2.7 with PIL

A little late to the party, but I thought it would be fun to convert the images into plaids (and back of course). First we shift columns up or down by 4 times the columns number (even columns down, odd columns up). Then, we shift rows left or right by 4 times the row number (even columns left, odd columns right).

The result is quite Tartanish.

To reverse, we just do these in the opposite order and shift by the opposite amount.

Code

from PIL import Image

def slideColumn (pix, tpix, x, offset, height):
  for y in range(height):
    tpix[x,(offset+y)%height] = pix[x,y]

def slideRow (pix, tpix, y, offset, width):
  for x in range(width):
    tpix[(offset+x)%width,y] = pix[x,y]

def copyPixels (source, destination, width, height):
  for x in range(width):
    for y in range(height):
      destination[x,y]=source[x,y]

def shuffleHorizontal (img, tmpimg, factor, encoding):
  xsize,ysize = img.size
  pix = img.load()
  tpix = tmpimg.load()
  for y in range(ysize):
    offset = y*factor
    if y%2==0:
      offset = xsize-offset
    offset = (xsize + offset) % xsize
    if encoding:
      slideRow(pix,tpix,y,offset,xsize)
    else:
      slideRow(pix,tpix,y,-offset,xsize)
  copyPixels(tpix,pix,xsize,ysize)

def shuffleVertical (img, tmpimg, factor, encoding):
  xsize,ysize = img.size
  pix = img.load()
  tpix = tmpimg.load()
  for x in range(xsize):
    offset = x*factor
    if x%2==0:
      offset = ysize-offset
    offset = (ysize + offset) % ysize
    if encoding:
      slideColumn(pix,tpix,x,offset,ysize)
    else:
      slideColumn(pix,tpix,x,-offset,ysize)
  copyPixels(tpix,pix,xsize,ysize)


def plaidify (img):
  tmpimg = Image.new("RGB",img.size)
  shuffleVertical(img,tmpimg,4,True)
  shuffleHorizontal(img,tmpimg,4,True)

def deplaidify (img):
  tmpimg = Image.new("RGB",img.size)
  shuffleHorizontal(img,tmpimg,4,False)
  shuffleVertical(img,tmpimg,4,False)

Results

The plaid from image 1:

the plaidified 1.jpg

The plaid form image 2:

the plaidified 2.png

jrrl

Posted 2014-07-24T06:43:35.237

Reputation: 161

2Very nice! Is it possible to get the diagonals along a 45° angle? – Todd Lehman – 2014-08-13T07:03:32.270

2It is possible by changing the offset lines to:

`offset = x*xsize/ysize`

and

`offset = y*ysize/xsize`

But, it really doesn't hide the image as well, unfortunately. – jrrl – 2014-08-13T21:39:32.863

5

Python (+ bonus) - permutation of the pixels

In this method, every pixel will be placed on another position, with the constraint that the other pixel will be placed on the first position. Mathematically, it is a permutation with cycle length 2. As such, the method is it's own inverse.

In retrospect, it is very similar to mfvonh, but this submission is in Python and I had to construct that permutation myself.

def scramble(I):
    result = np.zeros_like(I)
    size = I.shape[0:2]
    nb_pixels = size[0]*size[1]
    #Build permutation
    np.random.seed(0)
    random_indices = np.random.permutation( range(nb_pixels) )
    random_indices1 = random_indices[0:int(nb_pixels/2)]
    random_indices2 = random_indices[-1:-1-int(nb_pixels/2):-1]
    for c in range(3):
        Ic = I[:,:,c].flatten()
        Ic[ random_indices2 ] = Ic[random_indices1]
        Ic[ random_indices1 ] = I[:,:,c].flatten()[random_indices2]
        result[:,:,c] = Ic.reshape(size)
    return result

First test image: First test image Second test image: Second test image

Dave

Posted 2014-07-24T06:43:35.237

Reputation: 151

5

Python 2.7 + PIL, Inspiration from the Sliding Puzzles

Just had another idea. Basically, this method divides an image into equal sized blocks and then shuffles their order. Since the new order is based on a fixed seed, it's possible to completely revert the process using the same seed. Besides, with the additional parameter called granularity, it's possible to achieve different and unrecognizable results.

Results:

Original

original

Granularity 16

16

Granularity 13

13

Granularity 10

10

Granularity 3

3

Granularity 2

2

Granularity 1

enter image description here

Original

original

Granularity 16

16

Granularity 13

13

Granularity 10

10

Granularity 3

3

Granularity 2

2

Granularity 1

1

Code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PIL import Image
import random

def scramble_blocks(im,granularity,password,nshuffle):
    set_seed(password)
    width=im.size[0]
    height=im.size[1]

    block_width=find_block_dim(granularity,width)       #find the possible block dimensions
    block_height=find_block_dim(granularity,height)

    grid_width_dim=width/block_width                #dimension of the grid
    grid_height_dim=height/block_height

    nblocks=grid_width_dim*grid_height_dim          #number of blocks

    print "nblocks: ",nblocks," block width: ",block_width," block height: ",block_height
    print "image width: ",width," image height: ",height
    print "getting all the blocks ..."
    blocks=[]
    for n in xrange(nblocks): #get all the image blocks
        blocks+=[get_block(im,n,block_width,block_height)]

    print "shuffling ..."
    #shuffle the order of the blocks
    new_order=range(nblocks)
    for n in xrange(nshuffle):
        random.shuffle(new_order)

    print "building final image ..."
    new_image=im.copy()
    for n in xrange(nblocks):
        #define the target box where to paste the new block
        i=(n%grid_width_dim)*block_width                #i,j -> upper left point of the target image
        j=(n/grid_width_dim)*block_height
        box = (i,j,i+block_width,j+block_height)    

        #paste it   
        new_image.paste(blocks[new_order[n]],box)

    return new_image



#find the dimension(height or width) according to the desired granularity (a lower granularity small blocks)
def find_block_dim(granularity,dim):
    assert(granularity>0)
    candidate=0
    block_dim=1
    counter=0
    while counter!=granularity:         #while we dont achive the desired granularity
        candidate+=1
        while((dim%candidate)!=0):      
            candidate+=1
            if candidate>dim:
                counter=granularity-1
                break

        if candidate<=dim:
            block_dim=candidate         #save the current feasible lenght

        counter+=1

    assert(dim%block_dim==0 and block_dim<=dim)
    return block_dim

def unscramble_blocks(im,granularity,password,nshuffle):
    set_seed(password)
    width=im.size[0]
    height=im.size[1]

    block_width=find_block_dim(granularity,width)       #find the possible block dimensions
    block_height=find_block_dim(granularity,height)

    grid_width_dim=width/block_width                #dimension of the grid
    grid_height_dim=height/block_height

    nblocks=grid_width_dim*grid_height_dim          #number of blocks

    print "nblocks: ",nblocks," block width: ",block_width," block height: ",block_height
    print "getting all the blocks ..."
    blocks=[]
    for n in xrange(nblocks): #get all the image blocks
        blocks+=[get_block(im,n,block_width,block_height)]

    print "shuffling ..."
    #shuffle the order of the blocks
    new_order=range(nblocks)
    for n in xrange(nshuffle):
        random.shuffle(new_order)

    print "building final image ..."
    new_image=im.copy()
    for n in xrange(nblocks):
        #define the target box where to paste the new block
        i=(new_order[n]%grid_width_dim)*block_width             #i,j -> upper left point of the target image
        j=(new_order[n]/grid_width_dim)*block_height
        box = (i,j,i+block_width,j+block_height)    

        #paste it   
        new_image.paste(blocks[n],box)

    return new_image

#get a block of the image
def get_block(im,n,block_width,block_height):

    width=im.size[0]

    grid_width_dim=width/block_width                        #dimension of the grid

    i=(n%grid_width_dim)*block_width                        #i,j -> upper left point of the target block
    j=(n/grid_width_dim)*block_height

    box = (i,j,i+block_width,j+block_height)
    block_im = im.crop(box)
    return block_im

#set random seed based on the given password
def set_seed(password):
    passValue=0
    for ch in password:                 
        passValue=passValue+ord(ch)
    random.seed(passValue)


if __name__ == '__main__':

    filename="0RT8s.jpg"
    # filename="B5TbK.png"
    password="yOs0ZaKpiS"
    nshuffle=1
    granularity=1

    im=Image.open(filename)

    new_image=scramble_blocks(im,granularity,password,nshuffle)
    new_image.show()
    new_image.save(filename.split(".")[0]+"_puzzled.png")

    new_image=unscramble_blocks(new_image,granularity,password,nshuffle)
    new_image.save(filename.split(".")[0]+"_unpuzzled.png")
    new_image.show()

AlexPnt

Posted 2014-07-24T06:43:35.237

Reputation: 371

5

47

94 lines. 47 for encoding, 47 for decoding.

require 'chunky_png'
require_relative 'codegolf-35005_ref.rb'


REF = {:png => ref, :w => 1280, :h => 720}
REF[:pix] = REF[:png].to_rgb_stream.unpack('C*').each_slice(3).to_a
SEVENTH_PRIME = 4*7 - 4-7 - (4&7)
FORTY_SEVEN   = 4*7 + 4+7 + (4&7) + (4^7) + 7/4
THRESHOLD     = FORTY_SEVEN * SEVENTH_PRIME


class RNG
    @@m = 2**32
    @@r = 0.5*(Math.sqrt(5.0) - 1.0)
    def initialize(n=0)
        @cur = FORTY_SEVEN + n
    end
    def hash(seed)
        (@@m*((seed*@@r)%1)).floor
    end
    def _next(max)
        hash(@cur+=1) % max
    end
    def _prev(max)
        hash(@cur-=1) % max
    end        
    def advance(n)
        @cur += n
    end
    def state
        @cur
    end
    alias_method :rand, :_next
end


def load_png(file, resample_w = nil, resample_h = nil)
    png  = ChunkyPNG::Image.from_file(file)
    w    = resample_w || png.width
    h    = resample_h || png.height
    png.resample_nearest_neighbor!(w,h) if resample_w || resample_h
    pix  = png.to_rgb_stream.unpack('C*').each_slice(3).to_a
    return {:png => png, :w => w, :h => h, :pix => pix}
end


def make_png(img)
    rgb_stream = img[:pix].flatten.pack('C*')
    img[:png] = ChunkyPNG::Canvas.from_rgb_stream(img[:w],img[:h],rgb_stream)
    return img
end


def difference(pix_a,pix_b)
    (pix_a[0]+pix_a[1]+pix_a[2]-pix_b[0]-pix_b[1]-pix_b[2]).abs
end


def code(img, img_ref, mode)
    img_in  = load_png(img)
    pix_in  = img_in[:pix]
    pix_ref = img_ref[:pix]
    s = img_in[:w] * img_in[:h] 
    rng = RNG.new(mode==:enc ? 0 : FORTY_SEVEN*s+1)
    rand = mode == :enc ? rng.method(:_next) : rng.method(:_prev)
    s.times do
        FORTY_SEVEN.times do
            j = rand.call(s)
            i = rng.state % s
            diff_val = difference(pix_ref[i],pix_ref[j])
            if diff_val > THRESHOLD
               pix_in[i], pix_in[j] = pix_in[j], pix_in[i]
            end
        end
    end
    make_png(img_in)
end


case ARGV.shift
when 'enc'
    org, cod = ARGV
    encoded_image = code(org,REF,:enc)
    encoded_image[:png].save(cod)
when 'dec'
    org, cod = ARGV
    decoded_image = code(cod,REF,:dec)
    decoded_image[:png].save(org)
else
    puts '<original> <coded>'
    puts 'specify either <enc> or <dec>'
    puts "ruby #{$0} enc codegolf-35005_inp.png codegolf-35005_enc.png"
end

codegolf-35005_ref.rb

enter image description here enter image description here

(converted to jpg)

enter image description here

(original downscaled)

enter image description here

blutorange

Posted 2014-07-24T06:43:35.237

Reputation: 1 205

3Some of original pattern is visible through that lines. However it resembles when you draw with a finger on misted window). – Somnium – 2014-08-16T16:34:09.050

2...+ 184426 bytes for codegolf-35005_ref.rb? – edc65 – 2014-08-16T17:18:44.097

5

Matlab with a pinch of Group Theory (+ bonus)

In this approach we assume that we have an even number of total pixels. (If not, we just ignore one pixel) So we need to choose half of the pixels to swap with the other half. For this, we index all the pixels from 0 up to 2N-1 and consider these indices as a cyclic group.

Among the primes we search for a number p that is not too small and not too big, and that is coprime to 2N, the order of our group. This means g generates our group or {k*g mod 2N | k=0,1,...,2N-1} = {0,1,...,2N-1}.

So we choose the first N multiples of g as one set, and all the remaining indeces as the other set, and just swap the corresponding set of pixels.

If p is chosen the right way, the first set is evenly distributed over the whole image.

The two test cases:

Slightly off topic but interesting:

During testing I noticed, that if you save it to a (lossy compressed) jpg (instead of a losslessly compressed png) and apply the transformation back and forth, you quite quickly see artefacts of the compression, this shows the results of two consecutive rearrangements:

As you can see, the jpg compression makes the result look almost black and white!

clc;clear;
inputname = 'codegolf_rearrange_pixels2.png';
inputname = 'codegolf_rearrange_pixels2_swapped.png';
outputname = 'codegolf_rearrange_pixels2_swapped.png';

%read image
src = imread(inputname);

%separate into channels
red = src(:,:,1);
green = src(:,:,2);
blue = src(:,:,3);

Ntotal = numel(red(:));  %number of pixels
Nswap = floor(Ntotal/2); %how many pairs we can swap

%find big enough generator
factors = unique(factor(Ntotal));
possible_gen = primes(max(size(red)));
eliminated = setdiff(possible_gen,factors);
if mod(numel(eliminated),2)==0 %make length odd for median
    eliminated = [1,eliminated];
end
generator = median(eliminated);

%set up the swapping vectors
swapindices1 = 1+mod((1:Nswap)*generator, Ntotal);
swapindices2 = setdiff(1:Ntotal,swapindices1);
swapindices2 = swapindices2(1:numel(swapindices1)); %make sure both have the same length

%swap the pixels
red([swapindices1,swapindices2]) = red([swapindices2,swapindices1]);
green([swapindices1,swapindices2]) = green([swapindices2,swapindices1]);
blue([swapindices1,swapindices2]) = blue([swapindices2,swapindices1]);

%write and display
output = cat(3,red,green,blue);
imwrite(output,outputname);
subplot(2,1,1);
imshow(src)
subplot(2,1,2);
imshow(output);

flawr

Posted 2014-07-24T06:43:35.237

Reputation: 40 560

4

JavaScript (+bonus) - pixel divide swap repeater

The function takes an image element and

  1. Divides the pixels by 8.
  2. Does a reversible swap of pixel groups.
  3. Recurses swapping if the pixel group >= 8.
function E(el){
    var V=document.createElement('canvas')
    var W=V.width=el.width,H=V.height=el.height,C=V.getContext('2d')
    C.drawImage(el,0,0)
    var id=C.getImageData(0,0,W,H),D=id.data,L=D.length,i=L/4,A=[]
    for(;--i;)A[i]=i
    function S(A){
        var L=A.length,x=L>>3,y,t,i=0,s=[]
        if(L<8)return A
        for(;i<L;i+=x)s[i/x]=S(A.slice(i,i+x))
        for(i=4;--i;)y=[6,4,7,5,1,3,0,2][i],t=s[i],s[i]=s[y],s[y]=t
        s=[].concat.apply([],s)
        return s
    }
    var N=C.createImageData(W,H),d=N.data,A=S(A)
    for(var i=0;i<L;i++)d[i]=D[(A[i>>2]*4)+(i%4)]
    C.putImageData(N,0,0)
    el.src=C.canvas.toDataURL()
}

Mountains Circles

wolfhammer

Posted 2014-07-24T06:43:35.237

Reputation: 1 219

4

Python 2.7 + PIL, Column/Row Scrambler

This method simply scrambles the rows and columns of the image. It's possible to scramble only one of the dimensions or both. Besides, the order of the new scrambled row/column is based on a password. Also, another possibility is scrambling the entire image array without considering the dimensions.

Results:

Scrambling the entire image:

enter image description here

enter image description here

Scrambling the columns:

enter image description here

enter image description here

Scrambling the rows:

enter image description here

enter image description here

Scrambling both columns and rows:

enter image description here

enter image description here

I also tried to apply several runs to the image, but the end results didn't differ much, only the difficulty to decrypt it.

Code:

from PIL import Image
import random,copy

def scramble(im,columns,rows):
    pixels =list(im.getdata())

    newOrder=range(columns*rows)        
    random.shuffle(newOrder)            #shuffle

    newpixels=copy.deepcopy(pixels)
    for i in xrange(len(pixels)):
        newpixels[i]=pixels[newOrder[i]]

    im.putdata(newpixels)

def unscramble(im,columns,rows):
    pixels =list(im.getdata())

    newOrder=range(columns*rows)        
    random.shuffle(newOrder)            #unshuffle

    newpixels=copy.deepcopy(pixels)
    for i in xrange(len(pixels)):
        newpixels[newOrder[i]]=pixels[i]

    im.putdata(newpixels)

def scramble_columns(im,columns,rows):
    pixels =list(im.getdata())

    newOrder=range(columns)     
    random.shuffle(newOrder)            #shuffle

    newpixels=[]
    for i in xrange(rows):
        for j in xrange(columns):
            newpixels+=[pixels[i*columns+newOrder[j]]]

    im.putdata(newpixels)

def unscramble_columns(im,columns,rows):
    pixels =list(im.getdata())

    newOrder=range(columns)     
    random.shuffle(newOrder)            #shuffle

    newpixels=copy.deepcopy(pixels)
    for i in xrange(rows):
        for j in xrange(columns):
            newpixels[i*columns+newOrder[j]]=pixels[i*columns+j]

    im.putdata(newpixels)

def scramble_rows(im,columns,rows):
    pixels =list(im.getdata())

    newOrder=range(rows)        
    random.shuffle(newOrder)            #shuffle the order of pixels

    newpixels=copy.deepcopy(pixels)
    for j in xrange(columns):
        for i in xrange(rows):
            newpixels[i*columns+j]=pixels[columns*newOrder[i]+j]

    im.putdata(newpixels)

def unscramble_rows(im,columns,rows):
    pixels =list(im.getdata())

    newOrder=range(rows)        
    random.shuffle(newOrder)            #shuffle the order of pixels

    newpixels=copy.deepcopy(pixels)
    for j in xrange(columns):
        for i in xrange(rows):
            newpixels[columns*newOrder[i]+j]=pixels[i*columns+j]

    im.putdata(newpixels)


#set random seed based on the given password
def set_seed(password):
    passValue=0
    for ch in password:                 
        passValue=passValue+ord(ch)
    random.seed(passValue)

def encrypt(im,columns,rows,password):
    set_seed(password)
    # scramble(im,columns,rows)
    scramble_columns(im,columns,rows)
    scramble_rows(im,columns,rows)

def decrypt(im,columns,rows,password):
    set_seed(password)
    # unscramble(im,columns,rows)
    unscramble_columns(im,columns,rows)
    unscramble_rows(im,columns,rows)

if __name__ == '__main__':
    passwords=["yOs0ZaKpiS","NA7N3v57og","Nwu2T802mZ","6B2ec75nwu","FP78XHYGmn"]
    iterations=1
    filename="0RT8s.jpg"
    im=Image.open(filename)
    size=im.size
    columns=size[0]
    rows=size[1]

    for i in range(iterations):
        encrypt(im,columns,rows,passwords[i])
    im.save(filename.split(".")[0]+"_encrypted.jpg")

    for i in range(iterations):
        decrypt(im,columns,rows,passwords[iterations-i-1])
    im.save(filename.split(".")[0]+"_decrypted.jpg")

AlexPnt

Posted 2014-07-24T06:43:35.237

Reputation: 371

3

C# Winforms

Image1: enter image description here

Image 2: enter image description here

Source code:

class Program
{
    public static void codec(String src, String trg, bool enc)
    {
        Bitmap bmp = new Bitmap(src);
        Bitmap dst = new Bitmap(bmp.Width, bmp.Height);

        List<Point> points = new List<Point>();
        for (int y = 0; y < bmp.Height; y++)
            for (int x = 0; x < bmp.Width; x++)
                points.Add(new Point(x, y));

        for (int y = 0; y < bmp.Height; y++)
        {
            for (int x = 0; x < bmp.Width; x++)
            {
                int py = Convert.ToInt32(y + 45 * Math.Sin(2.0 * Math.PI * x / 128.0));
                int px = Convert.ToInt32(x + 45 * Math.Sin(2.0 * Math.PI * y / 128.0));

                px = px < 0 ? 0 : px;
                py = py < 0 ? 0 : py;
                px = px >= bmp.Width ? bmp.Width - 1 : px;
                py = py >= bmp.Height ? bmp.Height - 1 : py;

                int srcIndex = x + y * bmp.Width;
                int dstIndex = px + py * bmp.Width;

                Point temp = points[srcIndex];
                points[srcIndex] = points[dstIndex];
                points[dstIndex] = temp;
            }
        }

        for (int y = 0; y < bmp.Height; y++)
        {
            for (int x = 0; x < bmp.Width; x++)
            {
                Point p = points[x + y * bmp.Width];
                if (enc)
                    dst.SetPixel(x, y, bmp.GetPixel(p.X, p.Y));
                else
                    dst.SetPixel(p.X, p.Y, bmp.GetPixel(x, y));
            }
        }

        dst.Save(trg);
    }


    static void Main(string[] args)
    {
        // encode
        codec(@"c:\shared\test.png", @"c:\shared\test_enc.png", true);

        // decode
        codec(@"c:\shared\test_enc.png", @"c:\shared\test_dec.png", false);
    }
}

Johan du Toit

Posted 2014-07-24T06:43:35.237

Reputation: 1 524

1

Python 3.6 + pypng

Riffle/Master Shuffle

#!/usr/bin/env python3.6

import argparse
import itertools

import png

def read_image(filename):
    img = png.Reader(filename)
    w, h, data, meta = img.asRGB8()
    return w, h, list(itertools.chain.from_iterable(
        [
            (row[i], row[i+1], row[i+2])
            for i in range(0, len(row), 3)
        ]
        for row in data
    ))

def riffle(img, n=2):
    l = len(img)
    base_size = l // n
    big_groups = l % n
    base_indices = [0]
    for i in range(1, n):
        base_indices.append(base_indices[-1] + base_size + int(i <= big_groups))
    result = []
    for i in range(0, base_size):
        for b in base_indices:
            result.append(img[b + i])
    for i in range(big_groups):
        result.append(img[base_indices[i] + base_size])
    return result

def master(img, n=2):
    parts = [[] for _ in range(n)]
    for i, pixel in enumerate(img):
        parts[i % n].append(pixel)
    return list(itertools.chain.from_iterable(parts))

def main():
    parser = argparse.ArgumentParser()

    parser.add_argument('infile')
    parser.add_argument('outfile')
    parser.add_argument('-r', '--reverse', action='store_true')
    parser.add_argument('-i', '--iterations', type=int, default=1)
    parser.add_argument('-n', '--groupsize', type=int, default=2)
    parser.add_argument('-c', '--complex', nargs='+', type=int)

    args = parser.parse_args()

    w, h, img = read_image(args.infile)

    if args.complex:
        if any(-1 <= n <= 1 for n in args.complex):
            parser.error("Complex keys must use group sizes of at least 2")
        if args.reverse:
            args.complex = [
                -n for n in reversed(args.complex)
            ]
        for n in args.complex:
            if n > 1:
                img = riffle(img, n)
            elif n < -1:
                img = master(img, -n)
    elif args.reverse:
        for _ in range(args.iterations):
            img = master(img, args.groupsize)
    else:
        for _ in range(args.iterations):
            img = riffle(img, args.groupsize)

    writer = png.Writer(w, h)
    with open(args.outfile, 'wb') as f:
        writer.write_array(f, list(itertools.chain.from_iterable(img)))


if __name__ == '__main__':
    main()

My algorithm applies the riffle shuffle in one direction and a master shuffle in the other (since the two are inverses of one another), several iterations each, but each is generalized to split into any number of subgroups instead of just two. The effect is that you could make a multi-iteration permutation key since the image won't be restored without knowing the exact sequence of riffle and master shuffles. A sequence can be specified with a series of integers, with positive numbers representing riffles and negative numbers representing masters.

I shuffled the landscape with the key [3, -5, 2, 13, -7]:

Landscape 3 -5 2 13 -7

Interestingly enough, some interesting things happen from [3, -5], where some artifacts from the original image are left over:

Landscape 3 -5

Here's the abstract pattern shuffled with the key [2, 3, 5, 7, -11, 13, -17]:

Circles 2 3 5 7 -11 13 -17

If just one parameter is wrong in the key, the unshuffle won't restore the image:

Bad Unshuffle

Beefster

Posted 2014-07-24T06:43:35.237

Reputation: 6 651