Setup
Register for the lab using grinch
: https://grinch.caltech.edu/register and clone the repository as explained in the setup instructions.
Goals and Outcomes
One tool in Computer Science that we’ve seen over and over again this term is abstraction.
There are often many different ways of representing the same thing–each of which is useful in its own place. You deal with this regularly; for example
png
and jpg
are different file formats for images, but they both “look” like images. Today, we will explore different representations of colors,
images, and text. Along the way, we will explore a data-hiding technique called least-bit steganography which is one way to hide text in images.
By the end of this lab, you will…
- understand several representations of colors, images, and text
- see a cool application of bits and bytes
- use 2D arrays
- hide secret messages in text to send your friends and decode
- have fun!
Color Representation and Visual Perception
There are actually a surprisingly large number of ways to represent colors. Among the most common is a representation called “argb” which is used in png
s
and stands for “alpha red green blue”. A single color can be described as four separate values that determine different aspects of the color:
- an “alpha” value (which specifies the level of transparency of the color)
- a “red” value (which specifies ``how much red’ is in the color)
- a “green” value (which specifies ``how much green’ is in the color)
- a “blue” value (which specifies ``how much blue’ is in the color)
In this lab, we will keep the “alpha” constant and only worry about the red, green, and blue values. This is an unnecessary simplification, but it makes the idea easier to explain. Assuming “alpha” is specified at “fully opaque”, the rgb values completely determine which color we’re talking about.
Consider the two following colors:
- I am the color R=50, G=50, B=230.
- I am the color R=51, G=50, B=230.
These two colors look indistinguishable to humans because of deficiencies in our visual perception systems, but to a computer, they really are different colors.
The “computer science” problem here is “how can we abuse this?”. The most common way this can be abused is in what’s called “lossy image compression”–we can
use slightly different colors than in an original image to make it more “uniform” and thus easier to compress. We will explore a different usage of this
phenomenon: transmitting secret data!
To do this, we’ll need to understand a bit about how ARGB values, images, and text are stored on computers.
Pixels and Images
A pixel is conceptually a tiny square inside an image, and it has a single ARGB value. Pixels on a screen or in a photo line up in rows and columns. For example if a photo has a black border, it might have black pixels in the top and bottom few rows as well as the columns on the left and right edges of the image.
More formally, an image is a rectangular matrix of pixels which we can manipulate with all kinds of algorithms. In Java, we represent matrices as two-dimensional arrays. A two-dimensional array is represented as an array-of-arrays. Consider the following pictorial example of matrix and Java representations:
{
{x[0][0], x[0][1], x[0][2], ..., x[0][n]},
{x[1][0], x[1][1], x[1][2], ..., x[1][n]},
{ ... , ... , ... , ..., ... },
{x[d][0], x[d][1], x[d][2], ..., x[d][n]}
}
The syntax for two-dimensional arrays is nearly identical to the syntax for one-dimensional arrays:
// To declare a two-dimensional int array with $$a$$ rows and $$b$$ columns:
int[][] array = new int[a][b];
// To loop through our array, we use two nested for loops:
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[0].length; j++) {
System.out.println(array[i][j]);
}
}
Image Transposition
To explore 2D arrays, your first task is to write a “transpose” method in our Image
class. Do not worry about the starter code which works with the internal Java
implementation of png
images. You only have to work with the two-dimensional array representation. This is, once again, an example of abstraction.
public Image transpose()
Returns a new Image
which is a transpose (i.e., exchanges the rows and columns) of the original image. Does not modify the original Image
.
You should loop through all of the pixels and put them into a new 2D Array.
Task 0.
Implement transpose
in Image.java
.
It’s All Bits and Bytes!
ARGB Values as Bits
Each “channel” (single color value) of the ARGB pixel is represented as a number between 0 and 255. Normally, we write numbers in base 10, where the \(i\)th digit from the right represents how many \(10^i\)’s there are (hence the names “1’s digit”, “10’s digit”, etc.). Because computers are ultimately combinations of on/off switches, it is useful to look at binary representations of numbers. In particular, if we have a number \(N = (b_nb_{n-1}\cdots b_0)_2\), where \(b_i \in \{0, 1\}\), then \(\displaystyle N = \sum_{i=0}^{n}{b_i2^i}\). We call the individual “digits” bits; \(b_0\) is called the least-siginificant bit, because it contributes the smallest amount to the number’s value.
Text as Bits
All letters and symbols have numerical representations called ASCII codes. These are defined via a standard that is assumed to be used across
all computers. We can use ASCII to convert letters from our messages into numbers. For example,
'A' == 65
, 'a' == 97
, and '!' == 33
. You do not need (nor should you!) learn any of these actual values. The key idea here
is that char
s and int
s are interchangable via casting. For example, this code snippet prints A
:
char c = (char)65;
System.out.println(c);
We can also convert from a char
to a string by adding it to an empty string, like so:
String s = "" + j;
Putting it Together
Because pixels are so small, the difference in color between \(x\) and \(x+1\) is trivial enough to not be visible. Thus, this gives us the ability to store one bit of information per channel per pixel. To simplify our code, we will only hide data in the red channel. So, we can hide a single bit in a single pixel. This means that we can store a single ASCII value (which takes 8 bits) in 8 pixels.
If we iterate through the array going from the left to right, top to bottom, collecting the least significant bits of every red channel, it will result in a series of bytes which we can interpret as an ASCII string (our hidden message!)
public String decodeText()
Returns the String
hidden in the Image
in the red channel LSBs of this image.
Any recovered character with the value zero should be omitted.
Loop through all of the pixels and get the hidden bit using the
getLowestBitOfR
method in the Pixel
class. Then, use sets of 8 bits to reconstruct bytes. Make sure to start filling in the bits from the right of the byte.
That is, if \(h_0, h_1, h_2, ...\) are the bits recovered, then the first byte recovered should be \((h_7h_6h_5h_4h_3h_2h_1h_0)_2\).
Each byte should give you a number which can cast into a char
to get the ASCII value.
Accumulate the char
s, and return the final message.
Task 1.
Implement decodeText
in Image.java
.
public Image hideText(String text)
Takes in a message to be hidden, and then returns a new Image
with the red channel LSBs manipulated to hold the message. Zeroes out all
the red channel LSBs after the message is complete.
This method is the inverse of decodeText
, and, as such, it will look very similar.
You will find the fixLowestBitOfR
method in the Pixel
class and the
charAt
method in the String
class useful.
Task 2.
Implement hideText
in Image.java
.
Checking the Results on gitlab
Task 3. Check to make sure you are passing all the tests on gitlab.