You send a friend a photo of the Nyatapola, the five-roofed temple in Bhaktapur. It looks like any other photo of the temple: the roofs, the stone steps, people crossing the square. But your friend knows where to look, and finds a snow leopard inside it.
The leopard is in the numbers of the photo. Every pixel’s colors were changed a tiny amount, so tiny that no eye can see it, and those tiny changes are a whole second picture. Hiding a message so that nobody even knows it is there is called steganography (STEG-uh-NOG-ruh-fee), from the Greek for “covered writing”.
Here is that photo. Slide Bit to the right, one step at a time.
A whole picture of a snow leopard is hidden in the photo. How much did the photo’s colors have to change to hold it?
Pick an answer to see why.
You need: binary and how a color is stored as numbers, from Grade 7. For the blocks, variables and the remainder, from Grade 7. For the Python, loops and lists, from Grade 8.
Try it
Play with the photo above.
- a.Start at bit 8 and slide to the right, one bit at a time. At which bit does the temple stop being easy to see?
- b.At bit 1, what do you see? Now press Without. What changes in the right picture, and what changes in the photo on the left?
- c.Look at the readouts with the secret in. About how many of the photo’s colors were changed to hide the leopard?
How it works
A color is a byte
In Grade 7 you met colors from numbers: every pixel is three lights, red, green and blue, and each light’s brightness is a number from 0 to 255. That number is one byte: eight bits, worth 128, 64, 32, 16, 8, 4, 2 and 1. Here is a red of 202:
| Worth | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
|---|---|---|---|---|---|---|---|---|
| Bit | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
| 202 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 |
128 + 64 + 8 + 2 = 202. The bits are not equally important. If bit 8 flips, the color jumps by 128, from a bright red to a dark one. If bit 1 flips, it moves by 1, from 202 to 203. In Grade 7 you saw that a screen’s 16.7 million colors are “enough that the steps between neighboring colors are too small for your eyes to see”. One step is exactly what a change in bit 1 makes. Bit 8 is called the most significant bit, and bit 1 the least significant bit.
A pixel’s green is 77. Which change would you see most easily?
Pick an answer to see why.
Hiding one bit
The last bit of a number is the remainder when you divide it by 2: 0 for an even number, 1 for an odd one. To hide a secret bit in a color, make the color even or odd to match:
- take away the remainder, which makes it even: 203 → 202, and 202 stays 202;
- then add the secret bit: 0 keeps it even, 1 makes it odd.
As one rule: new color = color − (remainder of color ÷ 2) + secret bit.
| Color | Secret bit | Color − remainder | New color | Change |
|---|---|---|---|---|
| 202 | 1 | 202 | 203 | +1 |
| 202 | 0 | 202 | 202 | none |
| 77 | 0 | 76 | 76 | −1 |
| 255 | 1 | 254 | 255 | none |
Every color moves by 1 at most, and about half don’t move at all: their last bit already matched. And nothing ever goes below 0 or above 255.
To get the secret back, the person who receives the photo only needs the remainder of each color ÷ 2. That is the whole trick.
The leopard in the widget is a black-and-white picture of 160 × 120 squares, each square doubled to cover 2 × 2 of the photo’s pixels. A white square is a 1, a black square a 0. Each bit is hidden in all three colors of its pixel, red, green and blue, so the right picture shows the leopard in plain black and white.
Program it
With blocks
Here is the rule as a block program. It asks for a color and a secret bit, and says the new color.
A photo is millions of colors: a program in a loop does them all in a moment.
In Python (from Grade 8): the bits of a number
// divides and throws away the remainder, and % gives the remainder. So (color // worth) % 2 is the bit worth worth: 202 // 8 is 25, and 25 % 2 is 1.
color = 202worth = 128for place in range(8): bit = (color // worth) % 2 print('worth', worth, ':', bit) worth = worth // 2Output
worth 128 : 1worth 64 : 1worth 32 : 0worth 16 : 0worth 8 : 1worth 4 : 0worth 2 : 1worth 1 : 0Reading down: 11001010, the table above.
A photo as a list of rows
The programs below use a small gray photo of the same temple, made from the same photo as the widget’s: 30 rows of 40 gray values, from 0 (black) to 255 (white), in the file nyatapola.py. A gray photo has one number a pixel instead of three, which keeps the programs short. They also use flag.py, our secret: Nepal’s flag drawn in 30 rows of 40 squares, '#' for the flag and '.' for the rest.
This program draws the photo, then three of its bit planes: one bit of every pixel, white where the bit is 1 and black where it is 0. A loop inside a loop goes through every pixel: the outer one takes each row, the inner one each value in it.
from nyatapola import photofrom pixels import show_grid def bit_plane(picture, worth): plane = [] for row in picture: new_row = [] for color in row: new_row.append((color // worth) % 2 * 255) plane.append(new_row) return plane show_grid(photo)show_grid(bit_plane(photo, 128))show_grid(bit_plane(photo, 8))show_grid(bit_plane(photo, 1))Picture
Even in a photo this small, bit 8 is the temple and bit 1 is noise.
Hiding the flag
Now hide the flag in bit 1. hide() makes a new photo, pixel by pixel, with the rule from above. Then the program counts how many pixels changed, and by how much.
from nyatapola import photofrom flag import flagfrom pixels import show_grid def hide(photo, picture): new_photo = [] for r in range(len(photo)): new_row = [] for c in range(len(photo[r])): color = photo[r][c] if picture[r][c] == '#': bit = 1 else: bit = 0 new_row.append(color - color % 2 + bit) new_photo.append(new_row) return new_photo secret_photo = hide(photo, flag) changed = 0biggest = 0for r in range(30): for c in range(40): change = abs(secret_photo[r][c] - photo[r][c]) if change > 0: changed = changed + 1 biggest = max(biggest, change)print(changed, 'of 1200 pixels changed')print('The biggest change:', biggest)show_grid(photo)show_grid(secret_photo)Output
562 of 1200 pixels changedThe biggest change: 1Picture
Put your finger between the two pictures. Can you find one square that is different?
Getting it back
The person who receives the photo needs only the remainders. This program hides the flag and then finds it again, using a palette: 0 is drawn white and 1 crimson, the flag’s color.
from nyatapola import photofrom flag import flagfrom pixels import show_grid def hide(photo, picture): new_photo = [] for r in range(len(photo)): new_row = [] for c in range(len(photo[r])): color = photo[r][c] if picture[r][c] == '#': bit = 1 else: bit = 0 new_row.append(color - color % 2 + bit) new_photo.append(new_row) return new_photo def reveal(photo): found = [] for row in photo: new_row = [] for color in row: new_row.append(color % 2) found.append(new_row) return found secret_photo = hide(photo, flag)show_grid(reveal(photo), palette=['white', 'crimson'])show_grid(reveal(secret_photo), palette=['white', 'crimson'])Picture
The photo’s own last bits are noise; the second picture is the flag, back exactly.
What your program shows
- 562 of the 1,200 pixels changed, and none by more than 1. The other 638 already had the right last bit. The photo with the flag in it looks exactly like the photo without it.
- The flag came back perfectly, every square of it, from nothing but the remainders.
- The last bit of a photo is noise, so a secret there doesn’t stand out in the photo.
It works because the bits of a number are not equal: the top bit carries half of a color’s brightness, the last bit 1 part in 255.
Now try the opposite. Change one line of hide() so the flag goes into bit 8:
from nyatapola import photofrom flag import flagfrom pixels import show_grid def hide_on_top(photo, picture): new_photo = [] for r in range(len(photo)): new_row = [] for c in range(len(photo[r])): color = photo[r][c] if picture[r][c] == '#': bit = 1 else: bit = 0 new_row.append(color % 128 + bit * 128) new_photo.append(new_row) return new_photo show_grid(hide_on_top(photo, flag))Picture
color % 128 keeps bits 7 to 1 and drops bit 8; bit * 128 puts the secret there. Same flag, same photo, still one bit a color, but now the flag shouts. Where you hide a bit matters more than how many you hide.
How much could a real photo hold? A phone photo of 12 million pixels has three colors a pixel:
pixels = 12000000colors = pixels * 3letters = colors // 8print(colors, 'last bits to hide in')print(letters, 'letters, at 8 bits a letter')Output
36000000 last bits to hide in4500000 letters, at 8 bits a letterFour and a half million letters: more than a whole thick book, inside one holiday photo.
Going further still
Hide more, see more. Hide a secret in the last two bits instead of one. Each pixel can then keep a number from 0 to 3: four shades of gray, enough for a recognizable photo, not just black and white. How big is the biggest change now? Challenge ★★ asks you to write it. With four bits, you can hide a whole gray photo inside another; at what bit does the cover photo start to look spoiled?
Text in a photo. A letter is a number (Grade 7’s ASCII: H is 72), and a number is 8 bits, so 8 pixels can hold one letter. Challenge ★★★ reads a message hidden this way.
A secret is easy to break. We tried saving the widget’s photo, with the leopard in it, as a JPEG file at quality 90, the usual way a photo file is squeezed smaller (Grade 8). The JPEG keeps the photo looking the same, but it changes most colors by a few steps, and 48 out of every 100 hidden bits came back wrong. That is no better than tossing a coin: the leopard was gone. Saved as a PNG file, which keeps every number exactly, not one bit changed. So a hidden picture must travel as a file that is never squeezed.
Challenges
★ Read a hidden letter (blocks)
Eight color numbers are typed, one at a time. Their last bits, the first worth 128 and the last worth 1, make one letter’s code. Finish the program so it says the code. For 200, 151, 88, 64, 203, 90, 40, 18 it says 72, the letter H.
★★ Hide in two bits (Python, from Grade 8)
Write hide_two(photo, picture). photo is a list of rows of numbers from 0 to 255, and picture a list of rows of the same size whose numbers are 0, 1, 2 or 3. Give back a new photo with each number’s last two bits replaced by the picture’s number.
def hide_two(photo, picture): # each picture number is 0 to 3 return photo print(hide_two([[200, 201, 202]], [[3, 0, 1]]))★★★ Read the hidden message (Python, from Grade 8)
A message is hidden in a list of color numbers: each letter is 8 last bits in a row, the first worth 128, and the message ends where 8 bits make 0 (or where the numbers run out). Write read_message(colors) that gives back the message. chr(72) turns the code 72 into 'H'.
def read_message(colors): # 8 last bits a letter; the code 0 ends the message return '' print(read_message([200, 151, 88, 64, 203, 90, 40, 18]))Questions
Which bit of a color can you change without anyone seeing the difference?
Pick an answer to see why.
To hide the bit 0 in the color 151, what does the color become?
Pick an answer to see why.
Explain the difference between hiding a message (steganography) and scrambling it so it can’t be read (encryption).
Answer
A photo with a picture hidden in its last bits is sent through a messaging app that squeezes it into a smaller JPEG. The friend finds only noise. Why?
Answer
A gray photo is 800 × 600 pixels. How many letters could be hidden in it, at one bit a pixel and 8 bits a letter?
Answer
Name one good use and one harmful use of hiding data in pictures or printouts.
Answer
Where to go next
- Grade 7, How Computers Store Everything: colors as three numbers from 0 to 255, and binary.
- Grade 8, Photos, Sound and Video: pixels, file sizes and why JPEG files are small.
- Grade 8, Data, Privacy and You: the other data that travels inside your photos.
- Grade 9, Lists, Strings and Dictionaries: more programs on the same gray photo of the Nyatapola.
- Which Card Was Flipped?: extra bits that find and fix a mistake, instead of hiding a message.
