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

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.

• The question is a very long-winded way of saying "Write an encryption algorithm for images, whose output is an image." – David Richerby Jul 24 '14 at 9:10
• @DavidRicherby … that uses the same pixels/colors. Five black pixels in the "plain image" -> five black pixels in the "cipher image". – Daniel Beck Jul 24 '14 at 9:34
• @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 Jul 24 '14 at 11:11
• 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 Jul 25 '14 at 0:30
• Now elsewhere on the Stack Exchange network: Security.SE of all places – Doorknob Jul 29 '14 at 6:34

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)

Or make the computation efficient: (BLKSZ = 10)

• Sound 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 Jul 24 '14 at 15:25
• I always upvote python. – qwr Jul 25 '14 at 2:06
• Instead of scrambling for all values from 2 to 50, maybe you should use only prime numbers? – Neil Jul 25 '14 at 9:47
• @Neil Probably then it will look more random and less artistic. – Somnium Jul 25 '14 at 10:14
• The BLKSZ = 10 landscape is really cool! – wchargin Jul 25 '14 at 15:27

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?

Bonus:

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
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.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.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
{
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Scramble());
}
}
}


Check 'Unsafe code' in project property to compile.

Complex pattern

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();

• Interesting) I'm wondering whether with similar approach is possible to make more complicated patterns in output. – Somnium Jul 24 '14 at 18:29
• Nice idea - what happens to the middle line when there's an odd number of lines? – flawr Jul 24 '14 at 20:51
• @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 Jul 24 '14 at 21:17
• @user2992539 I think you can subdivide more - even checkerboard. With more subdivisions, the image is more recognizable. – edc65 Jul 24 '14 at 21:19
• Like your "Scramble" version!) – Somnium Jul 25 '14 at 7:08

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!

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

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;
}
}

//-----------------------------------------------------------------------------

{
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);
}

//-----------------------------------------------------------------------------

{
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;

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

image_scramble(image, max_delta, iterations, lcg64_seed);

image_write(image, stdout);

return 0;
}

• Just scrolled past this answer, looks awesome – Thomas Jul 26 '14 at 2:48
• This 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. Jul 28 '14 at 20:47
• @TimS. — done! shrunk 'em down to tiny thumbnails. – Todd Lehman Jul 28 '14 at 21:03

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 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 2 with Test image 1 as the key image

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):
if tail[:10] == 'scrambled_':
augmented_tail = 'rescued_' + tail[10:]
else:
augmented_tail = 'scrambled_' + 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:

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:

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.
• +1 Applause! I vaguely thought about this, but found it too difficult to implement. – edc65 Jul 27 '14 at 21:09
• This is brilliant. I've got an entry submitted, but I like yours better because of the key image feature. – Todd Lehman Jul 28 '14 at 3:31
• I'd be curious what these two images look like keyed against each other: lardlad.com/assets/wallpaper/simpsons1920.jpg and 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 Jul 28 '14 at 3:36
• @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 Jul 28 '14 at 10:05
• The key image feature puts this head and shoulders above the rest. – Jack Aidley Jul 29 '14 at 14:19

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

from numpy import *

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

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)

• That is a really cool algorithm. And you should totally get a bonus for using the Lena Söderberg pic. :) – Todd Lehman Jul 26 '14 at 3:13
• Always upvote Lena – user8777 Jul 29 '14 at 1:33

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:

• Great! 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 Jul 24 '14 at 8:10
• Nice. You should be able to achieve the same result with Permute and FindPermutation. – DavidC Jul 24 '14 at 8:10
• I'm not sure I understand. You can enter the precise permutation you want as a list of cycles. – DavidC Jul 24 '14 at 8:13
• @DavidCarraher Hm, interesting. Wouldn't I have to remember the original permutation for using FindPermutation though? – Martin Ender Jul 24 '14 at 8:16
• Or maybe something as {c, a, b}[[{2, 3, 1}]] can be used? – Somnium Jul 24 '14 at 8:16

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)
{
}

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;
}
}
}


• Clever one) Will be there always solution to that congruence equation? – Somnium Jul 25 '14 at 12:18
• @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. Jul 25 '14 at 12:26
• As I understand trivial solutions lead to image similar to input one. – Somnium Jul 25 '14 at 15:36
• Correct: 1 outputs the original image, and -1 outputs e.g. imgur.com/EiE6VW2 – Tim S. Jul 25 '14 at 15:52

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:

Second image:

• I like the "plaid" output on this one. – Brian Rogers Jul 26 '14 at 4:49

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

• Nice that this has some striped pattern) – Somnium Jul 24 '14 at 14:01
• Can you please swap the 2 images? In the question the mountains image is first. – A.L Jul 24 '14 at 18:33
• Could you include a brief explanation of the algorithm? – trichoplax Jul 24 '14 at 20:54

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')
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:

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

• r-g-b:

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

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

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

• 0:

• WOW! This one is great!!! Nice work! – Todd Lehman Aug 3 '14 at 7:59
• That's the most interesting one so far. Good work! – bebe Aug 15 '14 at 11:19

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


f @ f @ img1 // Image


f @ img2 // Image


f @ f @ img2 // Image


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


• My guess is that this doesn't meet the criteria, since it swaps on a smaller than pixel scale. – trichoplax Jul 24 '14 at 21:04
• I don't think it's a valid answer, but I also really like it. It's a fascinating twist, so +1 anyway... – trichoplax Jul 24 '14 at 21:05
• @githubphagocyte See update :) – mfvonh Jul 24 '14 at 23:38
• Great - I reached for the +1 again but of course I can't do it twice... – trichoplax Jul 25 '14 at 0:45
• @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 Jul 25 '14 at 1:04

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^^)

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')

• I don't fully understand, does your code applied second time on encrypted image will decrypt it? – Somnium Jul 24 '14 at 8:46
• Exactly: 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 Jul 24 '14 at 8:50
• Ha, 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 Jul 24 '14 at 9:07
• @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 Jul 24 '14 at 9:46
• I 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 Jul 24 '14 at 10:38

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]


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

 scramble[scrambledMount, 37, True]


Same thing for the circles:

 scramble[scrambledCircles, 37, True]


• I can't see how an image could be a 3 dimensional array. – edc65 Jul 25 '14 at 19:52
• @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 Jul 25 '14 at 20:29
• @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 Jul 26 '14 at 5:37

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")
(w,h) = img.size
img2 = Image.new( 'RGBA', (w,h), "black")
img3 = Image.new( 'RGBA', (w,h), "black")

# 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:

then permutation:

And with the last rotaion:

As for the other example:

• How do you restore the original image? – trichoplax Jul 31 '14 at 13:37
• 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 Aug 1 '14 at 7:30

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:

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

Scrambled:

Restored:

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 {
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.

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:

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)
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);


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:

100 passes (it takes a few minutes to do it):

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;
}
}
}
}


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
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
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 plaid form image 2:

• Very nice! Is it possible to get the diagonals along a 45° angle? – Todd Lehman Aug 13 '14 at 7:03
• It 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 Aug 13 '14 at 21:39

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: Second test image:

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.

Code:

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

from PIL import Image
import random

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

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
passValue=0
passValue=passValue+ord(ch)
random.seed(passValue)

if __name__ == '__main__':

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

im=Image.open(filename)

new_image.show()
new_image.save(filename.split(".")[0]+"_puzzled.png")

new_image.save(filename.split(".")[0]+"_unpuzzled.png")
new_image.show()


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
@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)
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

(converted to jpg)

(original downscaled)

• Some of original pattern is visible through that lines. However it resembles when you draw with a finger on misted window). – Somnium Aug 16 '14 at 16:34
• ...+ 184426 bytes for codegolf-35005_ref.rb? – edc65 Aug 16 '14 at 17:18

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';

%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);


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()
}


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 both columns and rows:

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
passValue=0
passValue=passValue+ord(ch)
random.seed(passValue)

# scramble(im,columns,rows)
scramble_columns(im,columns,rows)
scramble_rows(im,columns,rows)

# unscramble(im,columns,rows)
unscramble_columns(im,columns,rows)
unscramble_rows(im,columns,rows)

if __name__ == '__main__':
iterations=1
filename="0RT8s.jpg"
im=Image.open(filename)
size=im.size
columns=size[0]
rows=size[1]

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

for i in range(iterations):
im.save(filename.split(".")[0]+"_decrypted.jpg")


C# Winforms

Image1:

Image 2:

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++)

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);
}
}


Python 3.6 + pypng

Riffle/Master Shuffle

#!/usr/bin/env python3.6

import argparse
import itertools

import png

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()

args = parser.parse_args()

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]:

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

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

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