Pixel Crypt: Encoding Secrets Within Images using Python CustomTkinter In 10 Steps

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Imagine if you could hide a secret message within a photo, and no one could tell by just looking at it.

This is the magic of steganography—a powerful technique that allows us to embed secret information into ordinary media files like images, audio, or video.

The concept has existed for centuries, but modern digital steganography makes it possible to hide data within digital files in ways that are virtually undetectable.

In this article, we’ll explore image-based steganography by creating a simple, beginner-friendly program in Python.

This program will allow you to encode and decode hidden messages in images by tweaking pixel values, demonstrating how steganography works at a practical level.

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

What you’ll build:

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Before we start building let’s understand the concept.

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

What is Steganography?

Steganography, in the simplest terms, is the art of concealing a message within another medium so that it remains invisible to an unsuspecting observer.

Unlike encryption, which scrambles data to make it unreadable, steganography keeps the data “in plain sight” by hiding it inside other data.

Example Use Cases of Steganography:

  • Private Communication: Send secret messages without drawing attention.
  • Watermarking: Embed a hidden watermark in images to protect intellectual property.
  • Forensics and Cybersecurity: Track data usage or embed secret security codes.

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

How Image Steganography Works

In digital images, each pixel is composed of three color channels: Red, Green, and Blue (RGB).

Each channel can hold a value between 0 and 255, which represents its intensity. By making tiny changes to these RGB values, we can hide binary data within the image without noticeably altering its appearance.

➡️➡️Click Here for a Step-by-Step Guide to Encoding a Message (e.g., “Hi”) in an Image

Let’s see how encoding works by hiding the message “Hi” within the pixels of an image.

Step 1: Convert the Message to Binary

To store our message in binary form, we first need to convert each character to its ASCII value, and then to binary.

For “Hi”:

  • Character ‘H’: ASCII value is 72, binary equivalent is 01001000.
  • Character ‘i’: ASCII value is 105, binary equivalent is 01101001.

So, “Hi” becomes:

01001000 01101001

Each character is represented by 8 bits, so the message “Hi” has 16 bits total.

Step 2: Select Pixels to Store the Data

To hide our binary data, we’ll modify the Least Significant Bit (LSB) of each color component in the selected pixels.

The LSB is the last bit in a binary number (e.g., in 11000010, the last bit is 0). Changing this bit from 0 to 1 or vice versa only slightly alters the color, which remains visually indistinguishable to the human eye.

Example Pixels: Let’s say we have the following three pixels:

Pixel 1: (27, 64, 164)
Pixel 2: (248, 244, 194)
Pixel 3: (174, 246, 250)

Step 3: Embed the Data in Pixels

Now, we’ll embed our binary data into these pixels by modifying the LSB of each color value to match our binary message. For example:

Binary for ‘H’: 01001000

  • First Pixel: RGB values are (27, 64, 164)
    • 27 (binary: 00011011) → Change LSB to 0 for 0, making it 00011010 (26).
    • 64 (binary: 01000000) → Change LSB to 1 for 1, making it 01000001 (63).
    • 164 (binary: 10100100) → Keep LSB as 0 for 0.
  • Modified Pixel 1: (26, 63, 164)

Binary for ‘i’: 01101001

  • Second Pixel: RGB values are (248, 244, 194)
    • 248 (binary: 11111000) → Keep LSB as 0.
    • 244 (binary: 11110100) → Change LSB to 1, making it 11110101 (243).
    • 194 (binary: 11000010) → Keep LSB as 0.
  • Modified Pixel 2: (248, 243, 194)

After encoding, our modified pixels look like this:

Original Pixels       -> Modified Pixels
(27, 64, 164)         -> (26, 63, 164)
(248, 244, 194)       -> (248, 243, 194)

Step 4: Finalize with a “Stop” Signal

To signal the end of the message, we can use a pixel with a specific pattern, like setting the LSBs to a known value.

Let’s now start building our encoder and decoder.

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Step 1: Import Necessary Libraries

➡️Click For Code
import customtkinter as ctk
from PIL import Image
from typing import List, Iterator, Tuple

  • customtkinter as ctk: This imports the customtkinter library as ctk, a custom version of the tkinter library, often used for creating GUIs.
  • from PIL import Image: This imports the Image class from the Python Imaging Library (PIL), allowing us to open, modify, and save images.
  • from typing import List, Iterator, Tuple: These imports define specific data types (List, Iterator, Tuple) to make the code more readable and understandable.

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Step 2: Creating the SteganographyApp Class

➡️Click For Code
class SteganographyApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Pixel Crypt")
        self.geometry("700x400")
        self.build()

  • SteganographyApp(ctk.CTk): This creates a class named SteganographyApp, which inherits from ctk.CTk, making it a GUI window.
  • init(): The initializer method runs as soon as an instance of this class is created.It sets up the app title, window size, and then calls the build method to lay out the GUI components.

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Step 3: Building the GUI Layout

➡️Click For Code
def build(self) -> None:
        """Lays out the GUI elements for encoding and decoding operations."""
        
        # Encoding Section
        self.encode_label = ctk.CTkLabel(self, text="Encode Message into Image")
        self.encode_label.grid(row=0, column=0, padx=20, pady=10)
        
        self.image_path_entry = ctk.CTkEntry(self, placeholder_text="Enter image path")
        self.image_path_entry.grid(row=1, column=0, padx=20, pady=5)
        
        self.message_entry = ctk.CTkEntry(self, placeholder_text="Enter message to encode")
        self.message_entry.grid(row=2, column=0, padx=20, pady=5)
        
        self.resulting_image_entry = ctk.CTkEntry(self, placeholder_text="Enter resulting image path")
        self.resulting_image_entry.grid(row=3, column=0, padx=20, pady=5)
        
        self.encode_button = ctk.CTkButton(self, text="Encode", command=self.encode_image)
        self.encode_button.grid(row=4, column=0, padx=20, pady=10)
        
        self.encode_result_label = ctk.CTkLabel(self, text="")
        self.encode_result_label.grid(row=5, column=0, padx=20, pady=5)

        # Decoding Section
        self.decode_label = ctk.CTkLabel(self, text="Decode Message from Image")
        self.decode_label.grid(row=0, column=1, padx=20, pady=10)
        
        self.decode_image_path_entry = ctk.CTkEntry(self, placeholder_text="Enter image path to decode")
        self.decode_image_path_entry.grid(row=1, column=1, padx=20, pady=5)
        
        self.decode_button = ctk.CTkButton(self, text="Decode", command=self.decode_image)
        self.decode_button.grid(row=2, column=1, padx=20, pady=10)
        
        self.decode_result_label = ctk.CTkLabel(self, text="")
        self.decode_result_label.grid(row=3, column=1, padx=20, pady=5)

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

➡️Click For Explanation

The build method adds all elements to the GUI:

  1. Encode Section:
    • self.encode_label: Label for the encoding section.
    • self.image_path_entry: Entry box for the user to enter the path of the image they want to encode a message into.
    • self.message_entry: Entry box for entering the message to encode.
    • self.resulting_image_entry: Entry box for entering the path where the resulting encoded image will be saved.
    • self.encode_button: Button that starts the encoding process when clicked, calling the encode_image method.
    • self.encode_result_label: Label to display the result of the encoding process (success or error message).
  2. Decode Section:
    • self.decode_label: Label for the decoding section.
    • self.decode_image_path_entry: Entry box for entering the path of the image to decode.
    • self.decode_button: Button that starts the decoding process, calling the decode_image method.
    • self.decode_result_label: Label to display the decoded message or any error.

Step 4: Converting Data to Binary with genData

➡️Click For Code
def genData(self, data: str) -> List[str]:
        """
        Convert encoding data into 8-bit binary form using ASCII values of characters.
        """
        return [format(ord(i), '08b') for i in data]

Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

genData: Converts each character in the message into an 8-bit binary string (using ASCII values). This is important for encoding the message into image pixels.

Step 5: Modifying Pixels with modPix

The modPix method is where the pixel data of an image is modified to hide the binary representation of the message within the image.

This is the essence of steganography in this application. Let’s break down how it works:

➡️Click For Code
def modPix(self, pix: Iterator[Tuple[int, int, int]], data: str) -> Iterator[Tuple[int, int, int]]:
        """
        Modify pixels according to the 8-bit binary data of the message.
        
        Parameters:
            pix (Iterator[Tuple[int, int, int]]): An iterator of pixel tuples from the image.
            data (str): The message to be encoded.
            
        Yields:
            Iterator[Tuple[int, int, int]]: Modified pixels as tuples.
        """
        datalist = self.genData(data)
        lendata = len(datalist)
        imdata = iter(pix)
        
        for i in range(lendata):
            pix = [value for value in imdata.__next__()[:3] +
                                    imdata.__next__()[:3] +
                                    imdata.__next__()[:3]]
            
            for j in range(0, 8):
                if (datalist[i][j] == '0' and pix[j] % 2 != 0):
                    pix[j] -= 1
                elif (datalist[i][j] == '1' and pix[j] % 2 == 0):
                    pix[j] = pix[j] - 1 if pix[j] != 0 else pix[j] + 1

            if (i == lendata - 1):
                if (pix[-1] % 2 == 0):
                    pix[-1] = pix[-1] - 1 if pix[-1] != 0 else pix[-1] + 1
            else:
                if (pix[-1] % 2 != 0):
                    pix[-1] -= 1
            
            pix = tuple(pix)
            yield pix[0:3]
            yield pix[3:6]
            yield pix[6:9]

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

  1. Generate Binary Data: First, it uses genData(data) to convert the message (text) into a list of binary strings. Each character in the message is converted to an 8-bit binary representation.
  2. Pixel Iterator Setup: imdata = iter(pix) creates an iterator from the image’s pixel data, which allows us to go through each pixel one by one.
  3. Loop Through Message Bits: The loop for i in range(lendata) iterates through each character’s binary representation in the message.
  4. Read 9 Pixels at a Time: The code pix = [value for value in imdata.__next__()[:3] + imdata.__next__()[:3] + imdata.__next__()[:3]] reads 9 pixel values (27 color channels in total). These pixels will be modified to store a single character from the message.
  5. Modify Pixels Based on Message:
    • For each bit in the binary string, the pixel values are modified to represent the bit (0 or 1).
    • If the bit is '0', it ensures that the pixel’s color value is even (even values encode 0).
    • If the bit is '1', it makes the color value odd (odd values encode 1).
  6. Handle End of Message: If it’s the last character (end of the message), the last pixel in this 9-pixel set is adjusted to make its value odd. This odd pixel signals the end of the message during decoding. If it’s not the end, this pixel is kept even.
  7. Yield Modified Pixels: Each modified set of 9 pixels is then yielded back to be encoded into the image.

Step 6: Encoding Data into the Image with encode_enc

The encode_enc method writes the modified pixels (from modPix) into a copy of the original image to create a new, encoded image.

➡️Click For Code
def encode_enc(self, newimg: Image.Image, data: str) -> None:
        """
        Encode the data into the image by modifying its pixels.
        
        Parameters:
            newimg (Image.Image): The image where data will be encoded.
            data (str): The message to be encoded.
        """
        w = newimg.size[0]
        (x, y) = (0, 0)

        for pixel in self.modPix(newimg.getdata(), data):
            newimg.putpixel((x, y), pixel)
            if x == w - 1:
                x = 0
                y += 1
            else:
                x += 1

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

  1. Get Image Dimensions: It retrieves the width of the image (how many pixels per row) to keep track of where to place modified pixels as it moves across rows.
  2. Initialize Coordinates: (x, y) = (0, 0) starts encoding at the top-left corner.
  3. Loop Through Modified Pixels: The for pixel in self.modPix(newimg.getdata(), data) loop goes through each modified pixel set (from modPix) and places them in the new image.
    • newimg.putpixel((x, y), pixel) sets each modified pixel at the current (x, y) coordinates in the new image.
  4. Coordinate Update: It moves to the next pixel in the row until reaching the end, at which point it resets x to 0 and moves down to the next row (y += 1).

This completes the encoding process by filling the new image with pixels modified to hide the message.

Step 7: Encoding Button Functionality with encode_image

The encode_image method is triggered when the user clicks the “Encode” button. This method handles user inputs, calls encoding functions, and updates the GUI.

➡️Click For Code
def encode_image(self) -> None:
        """Handles the encoding process, saves the resulting image, and updates the GUI with a success message."""
        try:
            img_path = self.image_path_entry.get()
            message = self.message_entry.get()
            result_img_path = self.resulting_image_entry.get()

            image = Image.open(img_path, 'r')
            if len(message) == 0:
                raise ValueError("Message cannot be empty")

            new_img = image.copy()
            self.encode_enc(new_img, message)
            new_img.save(result_img_path)
            
            self.encode_result_label.configure(text="Message encoded successfully!")
        except Exception as e:
            self.encode_result_label.configure(text=f"Error: {e}")

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

  1. Get Inputs from GUI:
    • Retrieves the image path from self.image_path_entry.get().
    • Gets the message to be encoded and the path for saving the encoded image.
  2. Load the Image: Opens the image file at img_path.
  3. Check Message: If the message is empty, it raises an error since encoding an empty message isn’t allowed.
  4. Create and Encode New Image: It makes a copy of the image to avoid altering the original and passes it to self.encode_enc(new_img, message), which encodes the message into new_img.
  5. Save and Display Result: The new, encoded image is saved, and if everything goes well, the GUI is updated to display “Message encoded successfully!”. If an error occurs (e.g., if the image path is invalid), it shows the error message in the GUI.

Step 8: Decoding Button Functionality with decode_image

This method is activated when the user clicks the “Decode” button to extract a hidden message from an image.

➡️Click For Code
def decode_image(self) -> None:
        """Handles the decoding process and updates the GUI with the decoded message."""
        try:
            img_path = self.decode_image_path_entry.get()
            image = Image.open(img_path, 'r')
            decoded_message = self.decode_data(image)
            self.decode_result_label.configure(text=f"Decoded Message: {decoded_message}")
        except Exception as e:
            self.decode_result_label.configure(text=f"Error: {e}")

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

  1. Get Image Path: Fetches the path of the image to decode from self.decode_image_path_entry.get().
  2. Open Image: Loads the specified image file.
  3. Decode Data: It calls self.decode_data(image), which will return the hidden message from the image.
  4. Display Result: Updates the GUI with the decoded message. If there’s an error (e.g., if the file path is incorrect), it shows the error message instead.

Step 9: Decoding the Message with decode_data

The decode_data method extracts the hidden message from the pixels of an encoded image.

➡️Click For Code
def decode_data(self, image: Image.Image) -> str:
        """
        Decode a hidden message from an image file.
        
        Parameters:
            image (Image.Image): The image containing the hidden message.
        
        Returns:
            str: The decoded message from the image.
        """
        data = ''
        imgdata = iter(image.getdata())

        while True:
            pixels = [value for value in imgdata.__next__()[:3] +
                                    imgdata.__next__()[:3] +
                                    imgdata.__next__()[:3]]
            
            binstr = ''.join(['0' if i % 2 == 0 else '1' for i in pixels[:8]])

            data += chr(int(binstr, 2))
            if pixels[-1] % 2 != 0:
                return data

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

  1. Initialize an Empty Message: It starts with an empty string data where it will build up the decoded message.
  2. Loop Through Pixels:
    • Uses imgdata = iter(image.getdata()) to get an iterator over the pixel data of the image.
  3. Read 9 Pixels at a Time: Like in encoding, it reads 9 pixels at a time, using the first 8 pixels to interpret binary bits for each character and the 9th pixel as an end-of-message marker.
  4. Extract Binary Bits:
    • For each color value in the first 8 pixels, it reads whether the value is even or odd.
    • Builds up an 8-bit binary string (binstr) where 0 represents even and 1 represents odd.
  5. Convert Binary to Character:
    • Converts binstr to an integer and then to its ASCII character form, adding this character to data.
  6. End of Message Check: The last pixel in each 9-pixel set acts as a flag. If its color value is odd, the message has ended, so it returns data. If not, it continues reading pixels.

This method completes the process by returning the full hidden message extracted from the image.

Step 10: Running the Application

➡️Click For Code
if __name__ == "__main__":
    app = SteganographyApp()
    app.mainloop()

Pixel Crypt Encoding Secrets Within Images using Python CustomTkinter

Main Loop: If this file is executed directly, this block creates an instance of SteganographyApp and starts the GUI, allowing the user to interact with the encoding and decoding functions.

Ending Challenge

Now that you’ve built a basic steganography tool, it’s time to take it up a notch! Try adding one or more of these advanced features to your project:

  1. Encrypt Your Message: Add an encryption layer to your encoding process so that even if someone decodes the image, they would still need a password to reveal the message. Look into simple encryption methods like Caesar Cipher or advanced ones like AES encryption.
  2. Increase Encoding Capacity: Explore ways to encode larger messages by hiding data in more image channels (e.g., alpha channel for images with transparency) or by reducing the color depth to free up bits for data.
  3. Detect Manipulation: Add functionality to check if the image has been tampered with (e.g., compressed or resized) before attempting to decode. This would prevent errors or issues in retrieving the message.

Full Source Code

Once you’ve nailed the challenge, share your updated version here in the project’s repository GitHub! Let’s grow the project together and learn as a community.

The complete source code is in the GitHub link provided above.

Leave a Reply