Project #8: Image Steganography with DataFrames

In this DISCOVERY Data Science Project, you will do real data science in less than an hour and you will earn this project's card to your collection when you fully complete this project! 🎉

Overview

Steganography describes the technique of hiding data within secondary, usually ordinary, data to avoid detection. For example, an ordinary PNG image might look like a picture to us -- but, hidden inside of the image, is a special encoding that reveals hidden data that otherwise goes undetected.

In this project, you will explore steganography by decoding a message secretly hidden in an image just for you. Let's nerd out!

Background Knowledge

To finish this project, we assume you already know:

  • All topics covered in DISCOVERY Module 1: Basics of Data Science with Python (review the module here)
  • How to add columns to DataFrames -- we'll be adding a lot of new columns to a DataFrame in this project!

Let's get started! :)

Part 1: Using the DISCOVERY Library for Loading Images into a DataFrame

The DISCOVERY library provided as part of this project will load an image file into a DataFrame. Every image is represented by a grid of colored pixels, and a DataFrame that represents that same image will have the pixel's location (x and y) and the pixel's color (r, g, and b). The DataFrame will have a number of rows equal to the number of pixels in the image.

A computer represents each color channel (r, g and b) with a value between 0 and 255. A pixel with a bright blue color will have the color values (r=0, g=0, b=255) since it contains no red or green and the brightest blue possible (255).

The "Block I" image is included in this project and is just an ordinary Illini "Block I" (displayed on the right). The following code imports the DISCOVERY library and loads the "Block I" image called i.png:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 1: Using the DISCOVERY Library

Part 2: Steganography

In the Journal of Vision Research (June 2000), Andrew Stockman and Lindsay T. Sharpe's "The spectral sensitivities of the middle- and long-wavelength-sensitive cones derived from measurements in observers of known genotype" presents an incredibly detailed study of the sensitivity that the three types of cones have to various wavelengths of light.

Their work quantifies that the human eye is far more sensitive to green and red light than to blue light. Given this finding, this project will explore the hypothesis that we may be able hide data by slightly changing the blue components of pixels in an image without those changes being detected visually.

Our Algorithm: "Blue Steganography"

Based on the background research and hypothesis above, we will implement a way to detect "Blue Steganography" in an image. A few basic foundations:

  • To minimize any visual impact, we never want to change the blue (b) value of any pixel by more than +/- 1 (ex: if the original value b value is 153, our final value should be 152, 153, or 154 -- at most only adding or subtracting one to 153). Larger changes than this would almost certainly be easily detectable, but small ones may not be detectable.

  • To store information in this way, we will hide a message by checking if the b value is an even number or an odd number. Specifically:

  1. We will consider all columns from left to right but only the first 27 pixels of each vertical column of the image (y=0 to y=26).
  2. Exactly one b value in the first 27 pixels of each vertical column will be an even value. All other b values in the first 27 pixels will be an odd value.
  3. The index number of where the pixel was found in the vertical column (y-value) in which the even value was found will represent the letter in our secret, hidden message.
    • If the even b value is in the first pixel of the column, that is at y=0 and 0 represents an A,
    • If the even b value is at y=1 then we have a B,
    • If the even b value is at y=2 then we have a C,
    • ...and so on, until...
    • If the even b value is at y=25 then we have a Z, and
    • If the even b value is at y=26 then we will consider that to be a space ()

(This is only one possible steganography algorithm among many we could have used, but it should succeed in being an unnoticeable change to an image -- and it should be simple to process!)

Part 2.1: Creating the Steganography Subset

Using the image df loaded earlier, first filter the DataFrame to only the first 27 pixels of each column (y=0 to y=26) while making sure to keep all the columns (do not filter on the x values):

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 2.1: Finding the First 27 Rows

Part 2.2: Label the Pixels with an Even b Value

Now that you have a DataFrame with only the rows we're interested in, we need to find the one row in each column that has an even value. To detect if a value is even, we can use the "modulo" operation. The "modulo" operation -- denoted by the percentage operator (%) -- is the integer remainder after doing long division.

For example:

  • $7 / 4$ has us divide $7$ into $4$. The result of long division is "$1$ remainder $3$". Therefore, $7 % 4 = 3$ (the remainder).
  • Similarly, $43 / 11$ is "$3$ remainder $10$". Therefore, $43 % 11 = 10$ (since 10 is the remainder).
  • Finally, $11 / 2$ is "$5$ remainder $1$". Therefore, $11 % 2 = 1$ (since 1 is the remainder).

When we "modulo" by 2, there's a very special property to determine if we have an even or odd number:

  • Even numbers ALWAYS have a remainder of 0.
  • Odd numbers ALWAYS have a remainder of 1.

Create a new column called mod2 that stores the result of the operation df['b'] % 2 to find if the blue value of each pixel is even or odd:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 2.2: Label the Pixels with an Even `b` Value

Part 2.3: Filter to Include ONLY Pixels with Even-Valued Blue Components

Our steganography algorithm hides our message in the cells with even values for the blue component. Using your mod2 column you just created and your knowledge of the result of the mod2 operation, filter your DataFrame to only contain the pixels with an even value for the blue component:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 2.3: Filter to Include ONLY Pixels with Even-Valued Blue Components

Part 3: Uncovering the Hidden Message

You now have a DataFrame where you have found exactly one row for every column and that row tells us the letter in our hidden message. Let's now decode the message!

Part 3.1: Creating a Character from a Number

In Python, the chr function will translate a UTF-8 character code into a letter. The UTF-8 is an international standard and defines the translation from binary data into letters. For example, UTF-8 defines that the letter "A" is encoded by the number 65, "B" is encoded by the number 66, and so on.

We can see this in action by running the following cells:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)

Let's use the fact this is already built into Python (and all other programming languages) to do the work for us! To set up for using the chr function, add a new column to your DataFrame called charCode.

The charCode column can be created by adding 65 to the y value of each row so that y=0 has a charCode of 65, y=1 has a charCode of 66, and so on. Create this new column below:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 3.1: Creating a Character from a Number

Part 3.2: Apply the chr Function to Every Row

Occasionally, we will need to interface between standard Python and DataFrames. The chr function is an Python function, and not a DataFrame operation, so we will need to "apply" this function to our DataFrame.

To do this, we can use the DataFrame apply operation to apply a function to every row. To use the chr function on every value in the charCode column, the following syntax will do exactly that:

# Run the `chr` function for each row, using the value stored in the column `charCode`:
df["charCode"].apply(chr)

Store the result of the code above in a new column called character:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 3.2: Apply the `chr` Function to Every Row

Part 3.3: Concatenate the Entire String

We're almost done!! If you look at your DataFrame, and read down the character column, you will see the hidden message -- but we can do better -- we want to combine everything together into one string!

To combine all string columns together across an entire DataFrame, the str.cat() function will concatenate all the strings together into one large string. For example:

message = df.columnName.str.cat()
message    # combines all `columnName` strings together across the full DataFrame

Create a variable message that combines all of the characters together across the full DataFrame:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)

Part 3.4: Fix the Spaces

Oh no, you may have saw this coming! The space character is not correct -- it looks like it was mapped to the wrong character!

Use message.replace("[", " ") to replace all "[" characters with a space " " character:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 3.4: Concatenate the Entire String and Fix the Spaces

Part 4: Make a Reuseable Function

Often, programmers will do a task once to understand it and then combine the steps together into a function that can be used with different inputs.

Complete the following function, decodeHiddenMessage, that takes in a file name (like i.png) and returns the hidden message contained within in. (This function should simply combine all the steps in the previous parts into a single function, but with the provided file name fileName instead of i.png.)

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)

Let's first make sure your function works with the same input we've worked with already:

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
⚙️ Test Case: Part 4: Reuseable Function

Part 5: A Hidden Message??

Hmmm, that DISCOVERY logo is positions here just like the "Block I" at the top of the page. Strange.

The URL for the image is (it's quite long, make sure to get the whole thing -- on most devices you can usually triple click the text to highlight the entire string):

https://waf-server-01.cs.illinois.edu/static/discovery-projects/image-steganography/discovery.png

Can you check it out?

Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)
Reset Code Run All to Here Python Output:
(Run your code to see your code result's here.)

We can use this idea to hide messages all over in plain sight! Who knows where else you might find hidden messages in images! :)

Validate and Complete This Project!

Congratulations on finishing this DISCOVERY Data Science Project! 🎉🎉

To validate your entire project, your entire code will run from top-to-bottom on this page and each test case will be validated one final time. If everything looks good, you'll earn the card for completing this project: