88
\$\begingroup\$

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.

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

28 Answers 28

58
\$\begingroup\$

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

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

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

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;
}
\$\endgroup\$
3
  • 4
    \$\begingroup\$ Just scrolled past this answer, looks awesome \$\endgroup\$
    – Thomas
    Jul 26, 2014 at 2:48
  • 1
    \$\begingroup\$ 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? \$\endgroup\$
    – Tim S.
    Jul 28, 2014 at 20:47
  • \$\begingroup\$ @TimS. — done! shrunk 'em down to tiny thumbnails. \$\endgroup\$ Jul 28, 2014 at 21:03
44
\$\begingroup\$

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.
\$\endgroup\$
12
  • 2
    \$\begingroup\$ +1 Applause! I vaguely thought about this, but found it too difficult to implement. \$\endgroup\$
    – edc65
    Jul 27, 2014 at 21:09
  • 1
    \$\begingroup\$ This is brilliant. I've got an entry submitted, but I like yours better because of the key image feature. \$\endgroup\$ Jul 28, 2014 at 3:31
  • \$\begingroup\$ 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). \$\endgroup\$ Jul 28, 2014 at 3:36
  • 2
    \$\begingroup\$ @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. \$\endgroup\$ Jul 28, 2014 at 10:05
  • 2
    \$\begingroup\$ The key image feature puts this head and shoulders above the rest. \$\endgroup\$ Jul 29, 2014 at 14:19
33
\$\begingroup\$

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

\$\endgroup\$
3
  • 7
    \$\begingroup\$ That is a really cool algorithm. And you should totally get a bonus for using the Lena Söderberg pic. :) \$\endgroup\$ Jul 26, 2014 at 3:13
  • \$\begingroup\$ Always upvote Lena \$\endgroup\$
    – user8777
    Jul 29, 2014 at 1:33
  • \$\begingroup\$ Reminds me of the cat map, which can distort images and be reversed. \$\endgroup\$
    – user92753
    Feb 26, 2021 at 0:31
23
\$\begingroup\$

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

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

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

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

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

\$\endgroup\$
1
  • 2
    \$\begingroup\$ I like the "plaid" output on this one. \$\endgroup\$ Jul 26, 2014 at 4:49
17
\$\begingroup\$

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

\$\endgroup\$
2
  • 3
    \$\begingroup\$ WOW! This one is great!!! Nice work! \$\endgroup\$ Aug 3, 2014 at 7:59
  • 1
    \$\begingroup\$ That's the most interesting one so far. Good work! \$\endgroup\$
    – bebe
    Aug 15, 2014 at 11:19
14
\$\begingroup\$

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

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

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@"https://i.stack.imgur.com/2C2TY.jpg"//ImageData;
img2=Import@"https://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

\$\endgroup\$
6
  • 1
    \$\begingroup\$ My guess is that this doesn't meet the criteria, since it swaps on a smaller than pixel scale. \$\endgroup\$ Jul 24, 2014 at 21:04
  • 1
    \$\begingroup\$ I don't think it's a valid answer, but I also really like it. It's a fascinating twist, so +1 anyway... \$\endgroup\$ Jul 24, 2014 at 21:05
  • 1
    \$\begingroup\$ @githubphagocyte See update :) \$\endgroup\$
    – mfvonh
    Jul 24, 2014 at 23:38
  • \$\begingroup\$ Great - I reached for the +1 again but of course I can't do it twice... \$\endgroup\$ Jul 25, 2014 at 0:45
  • 1
    \$\begingroup\$ @githubphagocyte Oh right, I forget the short syntax can be bizarre. Yes. f @ f @ img1 // Image is (in full syntax) Image[f[f[img1]]] \$\endgroup\$
    – mfvonh
    Jul 25, 2014 at 1:04
10
\$\begingroup\$

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')
\$\endgroup\$
9
  • \$\begingroup\$ I don't fully understand, does your code applied second time on encrypted image will decrypt it? \$\endgroup\$
    – Somnium
    Jul 24, 2014 at 8:46
  • 2
    \$\begingroup\$ 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.) \$\endgroup\$
    – flawr
    Jul 24, 2014 at 8:50
  • 1
    \$\begingroup\$ 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! \$\endgroup\$ Jul 24, 2014 at 9:07
  • 3
    \$\begingroup\$ @user2992539 Well check out Octave which is a nice opensource alternative for matlab, and you can run 99% of matlab code directly in octave. \$\endgroup\$
    – flawr
    Jul 24, 2014 at 9:46
  • 2
    \$\begingroup\$ 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. ;) \$\endgroup\$ Jul 24, 2014 at 10:38
10
\$\begingroup\$

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

\$\endgroup\$
3
  • \$\begingroup\$ I can't see how an image could be a 3 dimensional array. \$\endgroup\$
    – edc65
    Jul 25, 2014 at 19:52
  • 1
    \$\begingroup\$ @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. \$\endgroup\$
    – DavidC
    Jul 25, 2014 at 20:29
  • \$\begingroup\$ @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. \$\endgroup\$
    – DavidC
    Jul 26, 2014 at 5:37
10
\$\begingroup\$

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

\$\endgroup\$
2
  • 3
    \$\begingroup\$ How do you restore the original image? \$\endgroup\$ Jul 31, 2014 at 13:37
  • \$\begingroup\$ 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 \$\endgroup\$
    – Faquarl
    Aug 1, 2014 at 7:30
8
\$\begingroup\$

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:

\$\endgroup\$
8
\$\begingroup\$

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.

\$\endgroup\$
8
\$\begingroup\$

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

\$\endgroup\$
7
\$\begingroup\$

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

\$\endgroup\$
6
\$\begingroup\$

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;
        }
    }
}
}
\$\endgroup\$
6
\$\begingroup\$

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

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Very nice! Is it possible to get the diagonals along a 45° angle? \$\endgroup\$ Aug 13, 2014 at 7:03
  • 2
    \$\begingroup\$ 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. \$\endgroup\$
    – jrrl
    Aug 13, 2014 at 21:39
6
\$\begingroup\$

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

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

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

\$\endgroup\$
5
\$\begingroup\$

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()
\$\endgroup\$
5
\$\begingroup\$

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);
\$\endgroup\$
4
\$\begingroup\$

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

\$\endgroup\$
4
\$\begingroup\$

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")
\$\endgroup\$
4
\$\begingroup\$

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);
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Those are beautiful! Nice work!! \$\endgroup\$ Jan 5, 2021 at 19:02
1
\$\begingroup\$

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

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.