Exploring Steganography in the Wild - Part 1
Tags: python, security, cybersecurity, steganography
Published: 2023-09-12 07:36:19

## {% details Outlines %}
- [Introduction](#introduction)
  - [Steganography](#steganography)
  - [Digital Images and Pixels](#digital-images-and-pixels)
  - [Color Models](#color-models)
  - [Binary Code](#binary-code)
  - [What To Expect](#what-to-expect)
- [Project Setup](#project-setup)
- [Core Concepts](#core-concepts)
  - [Mapping Techniques](#mapping-techniques)
  - [Bit Manipulation for Hiding Images](#bit-manipulation-for-hiding-images)
  - [Bit Manipulation for Revealing Images](#bit-manipulation-for-revealing-images)
  - [Bit Manipulation for Reconstructing Images](#bit-manipulation-for-reconstructing-images)
- [Implementation Details](#implementation-details)
  - [Supported Image Formats](#supported-image-formats)
  - [Code Structure](#code-structure)
  - [Explanation of Helper Functions](#explanation-of-helper-functions)
  - [Explanation of Steganograph Class](#explanation-of-steganograph-class)
- [Results and Visualization](#results-and-visualization)
  - [Visual Examples of Pre-Encoded Images](#visual-examples-of-preencoded-images)
  - [Visual Examples of Encoded Images](#visual-examples-of-encoded-images)
  - [Visual Examples of Decoded Images](#visual-examples-of-decoded-images)
  - [Visual Examples of Reconstructed Images](#visual-examples-of-reconstructed-images)
  - [Evaluation Metrics](#evaluation-metrics)
- [Limitations and Future Work](#limitations-and-future-work)
- [Conclusion](#conclusion)
  - [Summing Up the Journey](#summing-up-the-journey)
  - [Technical Achievements](#technical-achievements)
  - [Key Takeaways](#key-takeaways)
- [Additional Sections](#additional-sections)
{% enddetails %}

## Introduction

Welcome to a fascinating exploration of steganography and its applications. Steganography is a technique often cloaked in mystery, yet it serves practical purposes that extend from cybersecurity to digital watermarking. In this blog post, we'll delve into the nitty-gritty details of how steganography is applied to digital images.

### Steganography

> Steganography is the practice of representing information within another message or physical object, in such a manner that the presence of the information is not evident to human inspection - [Wikipedia](https://en.wikipedia.org/wiki/Steganography).

In simpler terms, steganography is akin to embedding a covert message within another overt message—in our case, hiding one image within another. 

### Digital Images and Pixels

> A digital image is an image composed of picture elements, also known as pixels, each with finite, discrete quantities of numeric representation for its intensity or gray level that is an output from its two-dimensional functions fed as input by its spatial coordinates denoted with x, y on the x-axis and y-axis, respectively - [Wikipedia](https://en.wikipedia.org/wiki/Digital_image).

Imagine a digital image as an intricate mosaic composed of minuscule tiles, known as pixels. Each pixel contributes to the overall image by adding its own splash of color. The more pixels you have, the more vivid and detailed your image becomes.

### Color Models

> Color models are tools central to color theory that define the color spectrum based on presence or absence of a few primary colors - [onlinelibrary.wiley.com](https://onlinelibrary.wiley.com/doi/epdf/10.1002/rth2.12308)

Think of color models as the recipe books for digital artists. Just like mixing primary colors in art class, color models help us understand how to blend different amounts of red, green, and blue light to produce various hues. In this discussion, we'll focus on the RGB (Red-Green-Blue) color model, which is the cornerstone for reproducing a wide array of colors in digital media.

> The name of the model comes from the initials of the three additive primary colors, red, green, and blue. The main purpose of the RGB color model is for the sensing, representation, and display of images in electronic systems, such as televisions and computers - [Wikipedia](https://en.wikipedia.org/wiki/RGB_color_model).

### Binary Code

> Binary code is a system of representing information using only two symbols, typically 0 and 1 - [Britannica](https://www.britannica.com/technology/binary-code)

In the realm of digital images, each pixel's color information is represented by 8-bit binary digits. This means each pixel can display one of \(2^8\) or 256 possible colors.

![8 bit binary digit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q41hsl3rg4dgdfbk98lo.png)

> In computing, the most significant bit (MSb) represents the highest-order place of a binary integer. Similarly, the least significant bit (LSb) is the bit position in a binary integer representing the binary 1s place of the integer - [Wikipedia](https://en.wikipedia.org/wiki/Bit_numbering)

The concept of most significant bit (MSb) and least significant bit (LSb) is crucial here. Altering the MSb can have a profound impact on a pixel's color, while changes to the LSb are usually subtle.

The **leftmost bit** is the **most significant bit**. We change this bit and it will affect tremendously on the value. For example, we flip the leftmost bit of the binary value of 165 from `1` to `0` (from 10100101 into 00100101) it will change the decimal value from 165 into 37.

On the other hand, the **rightmost bit** is the **least significant bit**. We change this bit and it won't affect tremendously on the value. For example, we flip the rightmost bit of the binary value of 165 from `1` to `0` (from `10100101` into `10100100`) it will change the decimal value from 165 into 164.

![pixels](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/75wnkg7oqf56ltb5zl4u.png)

So, let's tie it all together: each pixel in a digital image comprises three color values (RGB), each represented by an 8-bit binary digit. We'll exploit the "insignificance" of the least significant bits to embed one image into another without causing noticeable changes to the host image. This is the magic key to image-based steganography. We'll change the **less significant bits** of one image to include the **most significant bits** of another.

### What To Expect

In this technical dev blog post, you'll find:

- A deep dive into **steganography**, particularly the art of concealing one image within another.
- A focus on **LSB steganography**, which involves manipulating the least significant bits of pixels in a cover image to hide another image.
- Practical **code examples** and vivid **visualizations** to help you understand the process of hiding and retrieving images.
- A discussion on the **challenges** and **limitations** one might encounter, such as the quality of the concealed image.

So, buckle up for an exciting journey through the world of steganography!

## Project Setup

Before diving into the technicalities of steganography, it's essential to set up your workspace properly. This ensures that you can effortlessly follow along with the examples and get the most out of this exploration. Whether you're a seasoned coder or just dipping your toes into the Python ecosystem, I've got you covered.

I've created a Google Colab-compatible Jupyter Notebook to make this process seamless. The notebook is designed with a user-friendly UI/UX and hides the raw code, so you can focus on learning. And the best part? The code is pre-validated to run without hitches on the first try!

{% cta https://colab.research.google.com/github/ranggakd/steganography/blob/main/Exploring_Steganography_In_The_Wild_Part_1.ipynb %} Open in Colab {% endcta %}

If you're new to Google Colab or Python in general, don't worry! Here's an introductory [video on Google Colab for Beginners](https://www.youtube.com/watch?v=RLYoEyIHL6A) to help you get started.

For those who are more experienced and might prefer to run the project locally, here are the Python package requirements to set up your environment:

```txt
Python 3.7.12
matplotlib==3.2.2
matplotlib-inline==0.1.3
matplotlib-venn==0.11.6
numpy==1.21.5
Pillow==7.1.2
scikit-learn==1.0.2
```

Once you have your environment set up, you'll be ready to delve into the world of steganography with me!

## Core Concepts

### Mapping Techniques

Before diving into the nitty-gritty details, it's crucial to understand the four types of binary value mappings that we'll be using:

1. **Left-Half Bits Mapping (LHB Map)**
   ![left half bits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z8waj507ym60iv90kkmn.png)
   Think of this mapping as a way to extract the **left-half bits** values from the **cover image** within the **merged image**.

2. **Right-Half Bits Mapping (RHB Map)**
   ![right half bits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fdvnm9ww1skc8m87ddly.png)
   This mapping aims to extract the **left-half bits** values of the **hidden image** from the **merged image**.

3. **Whole Bits Mapping (MB Map)**
   ![merged bits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/olbs0eakha61k7js5mn2.png)
   This mapping is utilized to construct the **whole bits** values of the **merged image**, using the **left-half bits** values from both the **cover** and **hidden images**.

4. **Possible Right-Half Bits Mapping (RRHB Map)**
   ![reconstruction bits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6cgi1flq2ia5u17wn8g.png)
   In this case, we'll **randomly** pick possible **half bits values** as we don't store the information about the half bits beforehand. The aim here is to construct the **right-half bits** values of the **unmerged left-half bits** image for **both the unmerged cover** and **hidden images**.

### Bit Manipulation for Hiding Images

Now that we've understood the mapping types, let's look at how these mappings play a crucial role in hiding—or encoding—one image within another.

#### Encoding Process

![encode flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m31fxuw51jexpya6d7av.png)

Here's a summarized flow of the encoding process:
- **Choose Images**: Initially, you have to pick two images:
   - A "Cover Image," which will serve as the shell where your secret image will reside.
   - A "Hidden Image," which is the secret image you wish to hide within the cover image.
- **Extract and Map LHB (Left-Half Bits)**: The "Hidden Image" undergoes LHB (Left-Half Bits) Mapping. This extracts the left-half bits from the hidden image and maps them to create a new image called "Left-half bits Hidden Image."
- **Position the New Image**: The "Left-half bits Hidden Image" is then positioned within the "Cover Image" according to specified criteria (e.g., upper left corner, lower right corner, etc.).
- **Merge**: Finally, the "Cover Image" and the positioned "Left-half bits Hidden Image" are merged together. This forms the "Merged Image," which looks like your original cover image but has the hidden image embedded within it.

### Bit Manipulation for Revealing Images

Let's shift our focus to how we can extract—or decode—the hidden image from the merged image.

#### Decoding Process

![decode flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ew4ma9azc97n0a6kqezy.png)

Here's a quick rundown of the decoding process:
- **Use Merged Image**: The initial step in decoding involves taking the "Merged Image," which contains both the "Cover Image" and the "Hidden Image."
- **Extract Bits**: The "Merged Image" is then divided into its component "left-half bits" (LHB) and "right-half bits" (RHB). These are different portions of the image's pixel data.
- **Retrieve Images**: The "left-half bits" are used to reconstruct an "Unmerged Left-half bits Cover Image," effectively extracting what used to be the cover image. Note that the right-half bits for this image are still zero. The "right-half bits" are used to reconstruct a "Unmerged Left-half bits Hidden Image," effectively extracting what used to be the hidden image, also with zero right-half bits. The position of this hidden image within the merged image is accounted for during this step.

### Bit Manipulation for Reconstructing Images

Finally, let's look at how we can reconstruct the original images from their half-bit versions.

#### Reconstruction Process

![reconstruction flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u5zdbnnx5ecxm361s44v.png)

Here's how the reconstruction process works:
- **Use Unmerged Bits**: Use the unmerged left-half bits of both the cover and hidden images.
- **Map Right-Half Bits**: Use the "RRHB Mapping" to generate possible right-half bits values randomly.
- **Reconstruct Images**: The result is a reconstructed cover and hidden image.

I hope these explanations provide a clearer understanding of the steganography techniques involved in hiding, revealing, and reconstructing images.

## Implementation Details

### Supported Image Formats

In this exploration, we'll limit our focus to only two image formats: PNG and JPEG. This limitation helps keep our exploration straightforward.

### Code Structure

Before diving into the code's functionality, it's essential to get a grasp of its structure. Below is a snippet that outlines the main components of the code.

```bash
.
├── get_bits_dict(start: int, end: int, recon: bool = False) -> Dict[int, int]
├── get_merged_bits_array() -> np.ndarray
├── dict_to_nparray(d: dict) -> np.ndarray
├── dict_to_2darray(d: dict) -> np.ndarray
└── Steganograph
    ├── Object Attributes
    │     ├── ispng: bool
    │     ├── original_cover_image: numpy array
    │     ├── original_hidden_image: numpy array
    │     ├── left_half_bits_hidden_image: numpy array, default None
    │     ├── merged_image: numpy array, default None
    │     ├── unmerged_left_half_bits_cover_image: numpy array, default None
    │     ├── unmerged_left_half_bits_hidden_image: numpy array, default None
    │     ├── reconstructed_cover_image: numpy array, default None
    │     └── reconstructed_hidden_image: numpy array, default None
    ├──── Methods
    │     ├── encode(pos: str = 'upper_left')
    │     ├── decode(pos: str = 'upper_left')
    │     ├── encode_decode(pos: str = 'upper_left')
    │     ├── reconstruct()
    │     ├── encode_decode_recon(pos: str = 'upper_left')
    │     ├── is_two_images_identical(opt: int = 0) -> bool
    │     ├── save_image(opt: int = 0)
    │     ├── plot_original()
    │     ├── plot_left_half_bits()
    │     ├── plot_merged_image()
    │     ├── plot_unmerged_left_half_bits()
    │     └── plot_recon()
    └──── Class Attributes
          ├── __lhb_lookup: lhb #Left half bits array lookup
          ├── __rhb_lookup: rhb #Right half bits array lookup
          ├── __mb_lookup: mb #Merged bits 2D array lookup
          ├── __rrhb_lookup: rrhb #Reconstruction right half bits dict lookup
          ├── __format: tuple ('jpeg', 'png')
          ├── __rgb: tuple ('RGB', 'Red', 'Green', 'Blue')
          └── __pos: tuple ('upper_left', 'upper_right', 'lower_left', 'lower_right')
```

### Explanation of Helper Functions

The code incorporates several helper functions tailored for the art of steganography. Here's a brief rundown:

1. **get_bits_dict**: This function produces a dictionary that maps an integer (ranging from 0 to 255) to another integer. The mapping varies based on whether the process is for reconstruction. In the standard case, it extracts the **left-half bits**, whereas for reconstruction, it extracts the **right-half bits**.
   {% embed https://gist.github.com/ranggakd/e88d671aaf66917aea0f096c62d18600 %}

2. **dict_to_nparray**: Converting the dictionary from `get_bits_dict` into a numpy array facilitates faster lookup operations.
   {% embed https://gist.github.com/ranggakd/962b803fbab76253a1d6982eff359f7d %}

3. **get_merged_bits_array**: This function constructs a 2D array where each cell at index `[i][j]` holds the merged **left-half bits (cover image)** of `i` and **the left-half bits (hidden image)** of `j`.
   {% embed https://gist.github.com/ranggakd/1debb81914a05f680f2501d9ce405f28 %}

4. **dict_to_2darray**: This works similarly to `dict_to_nparray`, but it reshapes the array into a 2D structure, particularly useful for image reconstruction.
   {% embed https://gist.github.com/ranggakd/d51e57e149a31d7536b829f829ace61b %}

5. **Respective function callings for later**:
   {% embed https://gist.github.com/ranggakd/d64f5d7b1014f39c262be485e9595931 %}
   - Generates a numpy array for quick look-up of the left-half bits of a given number.
   - Creates a numpy array for speedy retrieval of the right-half bits of a number.
   - Constructs a 2D numpy array for efficient look-up of the merged bits of two numbers.
   - Builds a 2D numpy array for quick reference of the right-half bits of a number during the reconstruction phase.

### Explanation of Steganograph Class

#### Explanation of the `__init__` Method:

The `__init__` method initializes an instance of the `Steganograph` class.

{% embed https://gist.github.com/ranggakd/288450b4122226c8f372adbab2fd04f5 %}

##### Parameters:

- `cover_image_filepath`: File path for the cover image (supports only JPEG or PNG formats).
- `hidden_image_filepath`: File path for the hidden image (supports only JPEG or PNG formats).

##### Steps:

1. **Determine Image Formats**: The method first determines the format (JPEG or PNG) of both the cover image and the hidden image using the `imghdr.what()` function. The formats are stored in `format_ci` and `format_hi` variables.
   
2. **Validation**: It checks whether both images have valid formats. If the formats don't match any in the class' `__format` attribute, it raises a `TypeError`.

3. **Check Format Uniformity**: It checks if both images have the same format. If not, it raises a `TypeError`.

4. **PNG Format Flag**: Sets the `ispng` flag to True if the format is PNG; otherwise, sets it to False.

5. **Read Images**: Reads the images into `original_cover_image` and `original_hidden_image` using the `plt.imread()` function from Matplotlib.

6. **Invoke `read_and_adjust_images`**: Calls the method `read_and_adjust_images()` to perform additional adjustments on the images.

7. **Set Initial States**: Initializes the other attributes to None, as these would be populated by other methods later.

#### Explanation of the `read_and_adjust_images` Method:

This method adjusts the read images for further processing, especially for PNG images. By the end of these methods, the class instance is well-prepared with loaded and adjusted images, and is ready for encoding and decoding operations.

{% embed https://gist.github.com/ranggakd/82949b46bd0f92c5dcfc777c946b6e86 %}

##### Steps:

1. **Check if PNG**: If the images are in PNG format (`ispng` is True):
    - **Ignore Alpha Channel**: If the images have an alpha (A) channel, it ignores it for now.
    - **Normalization**: PNG images are often read in a normalized format where the pixel values are floats between 0 and 1. This method multiplies them by 255 and rounds off to convert them to integers.
    - **Type Casting**: It then casts the numpy arrays to 'uint8' type, ensuring that they are 8-bit unsigned integers.

#### Explanation of the `encode` Method:

The `encode` method is responsible for creating a simple steganograph—merging a hidden image into a cover image in a way that it can later be extracted.

{% embed https://gist.github.com/ranggakd/99b416f53ba7d823d2181850664c451f %}

##### Parameters:

- `pos`: Specifies where the hidden image will be located in relation to the cover image. Default is `'upper_left'`.

##### Steps:

1. **Position Validation**: It validates whether the given position `pos` is one of the four pre-defined positions (`upper_left`, `upper_right`, `lower_left`, `lower_right`). If not, it raises a `ValueError`.

2. **Get Left Half Bits**: It calls `get_left_half_bits` to extract the left half-bits of the hidden image and stores them in the `left_half_bits_hidden_image` attribute.

3. **Merge Half Bits**: It then calls `merge_two_half_bits` to merge the modified hidden image with the cover image.

##### Explanation of the `get_left_half_bits` Method:

{% embed https://gist.github.com/ranggakd/5d6d689e39eb9eaab064199abac8dea0 %}

###### Parameters:

- `img_arr`: The image array that you want to extract the left half-bits from.

###### Steps:

1. **Lookup**: It looks up the left half-bits of the image array `img_arr` using a pre-defined lookup table `__lhb_lookup`.

2. **Return**: It returns the left half-bits.

##### Explanation of the `merge_two_half_bits` Method:

{% embed https://gist.github.com/ranggakd/064a750f8675595edee52a46a9748e90 %}

###### Parameters:

- `pos`: The position where the hidden image will be located in relation to the cover image.

###### Steps:

1. **Copy Original Cover Image**: It first creates a copy of the original cover image and stores it in `self.merged_image`.

2. **Get Slicing Range**: It calls the `get_slicing` method to determine the slice where the hidden image will be placed.

3. **Merge the Half Bits**: It merges the cover image and the modified hidden image together to create the final merged image (steganograph). It does this by using a pre-defined 2D lookup table `__mb_lookup` for each channel (RGB).

4. **Type Casting**: Finally, it casts the `merged_image` into 'uint8' type.

##### Explanation of the `get_slicing` Method:

{% embed https://gist.github.com/ranggakd/8bc99d69895b6f2a7e94d7357c36ae8c %}

###### Parameters:

- `large_shape`: A tuple representing the shape of the larger array, which is the cover image in this case.
- `small_shape`: A tuple representing the shape of the smaller array, which is the hidden image.
- `pos`: The position where the hidden image will be located in relation to the cover image. Valid options are 'upper_left', 'upper_right', 'lower_left', and 'lower_right'.

###### Steps:

1. **Extract Shape Information**: The method first extracts the shape information of both the larger and smaller arrays. This information includes the number of rows and columns in each array.

2. **Calculate Slicing Range**: Based on the `pos` value, the function calculates the range in the larger array where the smaller array will be placed. This is done using numpy slice notation (`np.s_`).

3. **Error Handling**: If an invalid `pos` value is provided, a `ValueError` is raised.

4. **Return**: The function returns the calculated slicing range, which can be used as a slice object for numpy arrays. This slice object will then be used to place the hidden image in the specified position within the cover image.

By the end of the `encode` method, you'll have a steganograph where the hidden image is merged into the cover image at the specified position.

#### Explanation of the `decode` Method:

The `decode` method aims to break down the steganograph created by the `encode` method to recover the original hidden and cover images.

{% embed https://gist.github.com/ranggakd/e8579fd240d6b60da27626e62d284dce %}

##### Parameters:

- `pos`: This parameter specifies where the hidden image was embedded within the cover image. The default is `'upper_left'`.

##### Steps:

1. **Validation**: First, the method checks if `self.merged_image` is `None`. If it is, it raises a `TypeError`. It also validates that the position `pos` is one of the acceptable positions. If not, a `ValueError` is raised.

2. **Unmerge**: It calls the `unmerge_two_half_bits` method to separate the cover and hidden images from the merged image.

##### Explanation of the `unmerge_two_half_bits` Method:

{% embed https://gist.github.com/ranggakd/e83a0832b8203d7c4b71f21cdb3b0896 %}

###### Parameters:

- `pos`: Specifies the original position where the hidden image was embedded within the cover image.

###### Steps:

1. **Slice Definition**: Calls the `get_slicing` method to determine the slice range based on the `pos` value, i.e., where exactly the hidden image is located in the merged image.

2. **Extract Left Half Bits of Cover Image**: Uses the `get_left_half_bits` method to extract the left half bits of the merged image and assigns them to `self.unmerged_left_half_bits_cover_image`.

3. **Extract Right Half Bits of Hidden Image**: Similarly, it calls the `get_right_half_bits` method to extract the right half bits of the merged image. However, it does so only for the area specified by the slicing range returned by `get_slicing`.

##### Explanation of the `get_right_half_bits` Method:

{% embed https://gist.github.com/ranggakd/6f47ec48cf1cb37475ecab83e5d79d38 %}

###### Parameters:

- `img_arr`: The image array that you want to extract the right half-bits from.

###### Steps:

1. **Lookup**: It looks up the right half-bits of the image array `img_arr` using a pre-defined lookup table `__rhb_lookup`.

2. **Return**: It returns the right half-bits.

In summary, the `decode` method and its supporting methods work together to reverse the steganographic process, extracting the hidden image and cover image from the merged image.

#### Explanation of the `reconstruct` Method:

The `reconstruct` method aims to restore the original cover and hidden images from their left-half bits images, which were previously separated during the decoding process.

{% embed https://gist.github.com/ranggakd/50266f02dd13f9c09ce6304f8c0739f9 %}

##### Steps:

1. **Validation**: The method checks that `self.unmerged_left_half_bits_cover_image` and `self.unmerged_left_half_bits_hidden_image` are not `None`. If either of them is `None`, it raises a `TypeError`, prompting you to run the `decode()` method first.

2. **Reconstruct Cover Image**: The `reconstruct_right_half_bits` method is called to reconstruct the right-half bits of the cover image. The reconstructed image is stored in `self.reconstructed_cover_image`.

3. **Reconstruct Hidden Image**: Similarly, the `reconstruct_right_half_bits` method is used to reconstruct the right-half bits of the hidden image. The reconstructed image is stored in `self.reconstructed_hidden_image`.

##### Explanation of the `reconstruct_right_half_bits` Method:

{% embed https://gist.github.com/ranggakd/b2eb64ef330d2c70e25bde2c0ecc9c79 %}

###### Parameters:

- `im_arr`: The left-half bits of an image as a numpy array.
- `seed`: A seed for the random number generator, defaulting to 1999.

###### Steps:

1. **Random Seed**: The `np.random.seed` function is used to set the random seed to ensure that the random process is reproducible.

2. **Flattening Image**: The `ravel()` method is used to flatten the `im_arr`, essentially turning it into a one-dimensional array (`flat_im_arr`).

3. **Random Indices**: A random set of indices (`random_indices`) is generated using `np.random.randint`, with the size of the array based on the shape of `flat_im_arr`. This will simulate the random selection of right-half bits for each corresponding left-half bit.

4. **Reconstruction**: The lookup table `__rrhb_lookup` is used in conjunction with `random_indices` to get a set of right-half bits for each corresponding left-half bit in `flat_im_arr`. The result (`reconstructed_flat`) is a one-dimensional array containing the reconstructed right-half bits.

5. **Reshaping**: The reconstructed one-dimensional array is reshaped back into its original shape (`shape`) to form the reconstructed image.

6. **Type Casting**: Finally, the numpy array is cast to the type `'uint8'`.

7. **Return**: The reconstructed image, now having both left and right half-bits, is returned.

In summary, the `reconstruct` method and its sub-method `reconstruct_right_half_bits` work together to restore the original cover and hidden images from their left-half bits versions. This final step completes the round-trip process of encoding, decoding, and reconstructing the images.

For the complete code and a more detailed walkthrough, check out the Jupyter Notebook linked below:

{% cta https://colab.research.google.com/github/ranggakd/steganography/blob/main/Exploring_Steganography_In_The_Wild_Part_1.ipynb %} Open in Colab {% endcta %}

## Results and Visualization

Having delved into the theory and implementation details, it's time to see our steganography technique in action. We'll examine the visual changes and quality metrics at each stage of the process—encoding, decoding, and reconstruction.

### Visual Examples of Pre-Encoded Images

First, let's consider our "Cover Image" and "Hidden Image." The image below serves as our cover:

![Attack Titan.jpeg as cover image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6pthkxaqi3s6lm09c0h.jpeg)

Next, we have the hidden image:

![Eren.jpeg as hidden image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6girqpd6r1kh2uwjqrr.jpeg)

For a deeper understanding, let's examine the RGB plot of these images. Click on the image below to enlarge it.

[![RGB plot of cover and hidden images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xuvhejsjvn1rrn4pyel2.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xuvhejsjvn1rrn4pyel2.png)

### Visual Examples of Encoded Images

After encoding, we obtain two new images: the "Left Half Bits Hidden Image" and the "Merged Image."

#### Left Half Bits Hidden Image

The encoded "Left Half Bits Hidden Image" noticeably loses some quality compared to the original hidden image. For instance, the gradients do not blend as smoothly.

![LHB of hidden image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezx3qelhemiy83ykzp2q.jpeg)

Here's the RGB plot for a more in-depth analysis:

[![RGB plot of left half bits hidden image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7llj143tqsg7cphdrhlq.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7llj143tqsg7cphdrhlq.png)

#### Merged Image

At first glance, the "Merged Image" looks almost identical to the original cover image. However, a keen eye might spot a subtle face formation in the upper-left corner.

![Merged image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6symdv85lqgs0yeeyei0.jpeg)

For a more analytical view, let's check the RGB plot:

[![RGB plot of merged image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejj0u216s9j05kpir4m6.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejj0u216s9j05kpir4m6.png)

### Visual Examples of Decoded Images

Next, we decode the merged image to obtain the "Unmerged Left Half Bits Cover Image" and the "Unmerged Left Half Bits Hidden Image." Both of these images show a loss in quality and coherence, similar to what we observed with the left half bits hidden image.

![Unmerged left half bits cover image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/re8cawehxdod4hbhh9nr.jpeg)

![Unmerged left half bits hidden image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hu47pp54m6ye6d7jdpdh.jpeg)

Take a look at the RGB breakdown for further insights:

[![RGB plot of unmerged images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axxptrt2csnz4ohlwave.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axxptrt2csnz4ohlwave.png)

### Visual Examples of Reconstructed Images

Finally, we move on to the reconstruction stage, where we aim to approximate the original images as closely as possible.

#### Reconstructed Cover Image

When compared to the unmerged version, the reconstructed cover image appears slightly brighter. However, it's essential to note that the image quality is still not on par with the original. Gradients, for example, do not blend as smoothly as they should.

![Reconstructed cover image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghx71nxua4yozr1evvcc.jpeg)

#### Reconstructed Hidden Image

A similar trend is observed in the reconstructed hidden image. Though somewhat brighter than the unmerged version, the quality still falls short of the original.

![Reconstructed hidden image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g3wndidfw1mvuq6lgex1.jpeg)

#### RGB Breakdown of Reconstructed Images

For a more analytical perspective, let's examine the RGB plots of these reconstructed images:

[![RGB plot of reconstructed images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gokpzyk4gq71zkh6iwvy.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gokpzyk4gq71zkh6iwvy.png)

### Evaluation Metrics

For the evaluation, I utilized the Root Mean Square Error (RMSE) and Mean Absolute Error (MAE) metrics. These metrics help to quantify the differences between the images at different stages of the steganography process. While I can't speak to their application in formal steganalysis, they do offer a way to gauge the effectiveness of the encoding and decoding steps. If you have insights or recommendations on other metrics that might be more suitable, especially from a steganalysis point of view, feel free to share.

For instance, comparing the "Left Half Bits Hidden Image" with the original hidden image yielded the following results:

```
RMSE	: 8.868079206151936
MAE		: 7.579809053497942
```

This indicates that the two images are indeed different. However, when comparing the "Left Half Bits Hidden Image" with the "Unmerged Left Half Bits Hidden Image," the result was:

```
RMSE	: 0.0
MAE		: 0.0
```

This confirms that the encoding and decoding processes were executed correctly, as the two images are logically the same.

Once again, for the complete code and walkthrough, just run the Jupyter Notebook linked below:

{% cta https://colab.research.google.com/github/ranggakd/steganography/blob/main/Exploring_Steganography_In_The_Wild_Part_1.ipynb %} Open in Colab {% endcta %}

This concludes the "Results and Visualization" section, where we've explored the visual aspects and quality metrics of images at every stage—encoding, decoding, and reconstruction. These visual examples and metrics serve as a comprehensive evaluation of our steganography technique.

## Limitations and Future Work

In this section, we delve into the limitations of the current implementation and discuss opportunities for future enhancements.

1. **Image Channel Handling**: The code is designed to work specifically with RGB color spaces. This makes it less versatile when dealing with other color spaces or grayscale images.

2. **Limited File Format Support**: The code currently supports only PNG and JPEG formats, potentially excluding users with images in other formats.

3. **Code Complexity**: The codebase is somewhat intricate, which might make it less accessible for individuals who are not familiar with the project's specifics.

4. **Error Handling**: While some error-handling mechanisms exist, they may not be comprehensive enough to address all potential edge cases.

5. **Hard-Coded Values**: The presence of hard-coded values in the code reduces its flexibility and adaptability.

6. **Google Colab Dependency**: The code relies on Google Colab-specific libraries, making it less portable for users who may prefer other development environments.

7. **Lack of Modularization**: The code is not modular, which could complicate future efforts to extend its functionality or maintain it.

## Conclusion

### Summing Up the Journey

- **Enlightening and Challenging**: Our exploration into steganography has been a revealing journey, showcasing both the potential and the challenges of this field.
  
- **Practical Applications**: We demonstrated how one image could be hidden within another, a technique with various real-world applications such as digital watermarking and secure communications.

- **Identified Limitations**: Like any scientific endeavor, our work has its constraints, from the limitations in color channel handling to dependencies on specific platforms like Google Colab. These offer avenues for future research and refinement.

### Technical Achievements

- **Bit Manipulation**: One of the technical milestones of this project is the successful implementation of bit manipulation techniques to encode and decode images.

- **Visual Proof**: We provided visual examples to demonstrate our techniques in action. While the hidden image is not perfectly concealed in our experiments, we believe that in scenarios with more detailed and crowded cover images, the concealment could be more effective.

- **Quantitative Analysis**: We've used metrics such as RMSE and MAE to measure differences between original and manipulated images. While these metrics offer insights, they may not be specifically tailored for steganalysis. If you have expertise in steganalysis, your feedback in the comments would be highly valuable.

### Key Takeaways

#### The Complexity of Simplicity
What seemed like a straightforward task—hiding one image within another—unfolded into a labyrinth of complexities. The project has underscored the intricacies of image manipulation and bit-level operations.

#### Robustness and Flexibility are Paramount
The limitations we've faced have highlighted the importance of robust and flexible code. As we continue to refine this project, the lessons learned here will be invaluable.

#### Continuous Learning and Improvement
The world of steganography is vast, and there's so much more to explore. Your feedback and contributions are not just welcome but are essential for the continuous improvement of this project.

#### The Future is Open
The avenues for future work are promising. They range from refining the current code to make it more versatile to exploring more advanced steganographic techniques. With ongoing research and collaboration, the sky is the limit for what can be achieved.

## Additional Sections
- **Code Repository**: You can find the complete code and notebooks on [GitHub](https://github.com/ranggakd/steganography/).

{% embed https://github.com/ranggakd/steganography/ %}

- **References**: [Steganography: Hiding an image inside another](https://towardsdatascience.com/steganography-hiding-an-image-inside-another-77ca66b2acb1)