Outlines
Introduction
Outlines
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.
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.
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
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.
Binary Code
Binary code is a system of representing information using only two symbols, typically 0 and 1 - Britannica
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.
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
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.
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!
If you're new to Google Colab or Python in general, don't worry! Here's an introductory video on Google Colab for Beginners 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:
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:
Left-Half Bits Mapping (LHB Map)
Think of this mapping as a way to extract the left-half bits values from the cover image within the merged image.Right-Half Bits Mapping (RHB Map)
This mapping aims to extract the left-half bits values of the hidden image from the merged image.Whole Bits Mapping (MB Map)
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.Possible Right-Half Bits Mapping (RRHB Map)
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
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
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
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.
.
├── 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:
-
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.
-
dict_to_nparray: Converting the dictionary from
get_bits_dict
into a numpy array facilitates faster lookup operations. -
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) ofi
and the left-half bits (hidden image) ofj
. -
dict_to_2darray: This works similarly to
dict_to_nparray
, but it reshapes the array into a 2D structure, particularly useful for image reconstruction. -
Respective function callings for later:
- 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.
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:
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 informat_ci
andformat_hi
variables.Validation: It checks whether both images have valid formats. If the formats don't match any in the class'
__format
attribute, it raises aTypeError
.Check Format Uniformity: It checks if both images have the same format. If not, it raises a
TypeError
.PNG Format Flag: Sets the
ispng
flag to True if the format is PNG; otherwise, sets it to False.Read Images: Reads the images into
original_cover_image
andoriginal_hidden_image
using theplt.imread()
function from Matplotlib.Invoke
read_and_adjust_images
: Calls the methodread_and_adjust_images()
to perform additional adjustments on the images.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.
Steps:
-
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.
Parameters:
-
pos
: Specifies where the hidden image will be located in relation to the cover image. Default is'upper_left'
.
Steps:
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 aValueError
.Get Left Half Bits: It calls
get_left_half_bits
to extract the left half-bits of the hidden image and stores them in theleft_half_bits_hidden_image
attribute.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:
Parameters:
-
img_arr
: The image array that you want to extract the left half-bits from.
Steps:
Lookup: It looks up the left half-bits of the image array
img_arr
using a pre-defined lookup table__lhb_lookup
.Return: It returns the left half-bits.
Explanation of the merge_two_half_bits
Method:
Parameters:
-
pos
: The position where the hidden image will be located in relation to the cover image.
Steps:
Copy Original Cover Image: It first creates a copy of the original cover image and stores it in
self.merged_image
.Get Slicing Range: It calls the
get_slicing
method to determine the slice where the hidden image will be placed.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).Type Casting: Finally, it casts the
merged_image
into 'uint8' type.
Explanation of the get_slicing
Method:
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:
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.
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_
).Error Handling: If an invalid
pos
value is provided, aValueError
is raised.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.
Parameters:
-
pos
: This parameter specifies where the hidden image was embedded within the cover image. The default is'upper_left'
.
Steps:
Validation: First, the method checks if
self.merged_image
isNone
. If it is, it raises aTypeError
. It also validates that the positionpos
is one of the acceptable positions. If not, aValueError
is raised.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:
Parameters:
-
pos
: Specifies the original position where the hidden image was embedded within the cover image.
Steps:
Slice Definition: Calls the
get_slicing
method to determine the slice range based on thepos
value, i.e., where exactly the hidden image is located in the merged image.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 toself.unmerged_left_half_bits_cover_image
.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 byget_slicing
.
Explanation of the get_right_half_bits
Method:
Parameters:
-
img_arr
: The image array that you want to extract the right half-bits from.
Steps:
Lookup: It looks up the right half-bits of the image array
img_arr
using a pre-defined lookup table__rhb_lookup
.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.
Steps:
Validation: The method checks that
self.unmerged_left_half_bits_cover_image
andself.unmerged_left_half_bits_hidden_image
are notNone
. If either of them isNone
, it raises aTypeError
, prompting you to run thedecode()
method first.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 inself.reconstructed_cover_image
.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 inself.reconstructed_hidden_image
.
Explanation of the reconstruct_right_half_bits
Method:
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:
Random Seed: The
np.random.seed
function is used to set the random seed to ensure that the random process is reproducible.Flattening Image: The
ravel()
method is used to flatten theim_arr
, essentially turning it into a one-dimensional array (flat_im_arr
).Random Indices: A random set of indices (
random_indices
) is generated usingnp.random.randint
, with the size of the array based on the shape offlat_im_arr
. This will simulate the random selection of right-half bits for each corresponding left-half bit.Reconstruction: The lookup table
__rrhb_lookup
is used in conjunction withrandom_indices
to get a set of right-half bits for each corresponding left-half bit inflat_im_arr
. The result (reconstructed_flat
) is a one-dimensional array containing the reconstructed right-half bits.Reshaping: The reconstructed one-dimensional array is reshaped back into its original shape (
shape
) to form the reconstructed image.Type Casting: Finally, the numpy array is cast to the type
'uint8'
.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:
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:
Next, we have the hidden image:
For a deeper understanding, let's examine the RGB plot of these images. Click on the image below to enlarge it.
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.
Here's the RGB plot for a more in-depth analysis:
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.
For a more analytical view, let's check the RGB plot:
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.
Take a look at the RGB breakdown for further insights:
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 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.
RGB Breakdown of Reconstructed Images
For a more analytical perspective, let's examine the RGB plots of these reconstructed images:
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:
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.
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.
Limited File Format Support: The code currently supports only PNG and JPEG formats, potentially excluding users with images in other formats.
Code Complexity: The codebase is somewhat intricate, which might make it less accessible for individuals who are not familiar with the project's specifics.
Error Handling: While some error-handling mechanisms exist, they may not be comprehensive enough to address all potential edge cases.
Hard-Coded Values: The presence of hard-coded values in the code reduces its flexibility and adaptability.
Google Colab Dependency: The code relies on Google Colab-specific libraries, making it less portable for users who may prefer other development environments.
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.
- References: Steganography: Hiding an image inside another
Top comments (8)
very nice!
Thank you hope it helps. Please do share it to your peers 😁
Substituting Msbs of the covert image in the Lsbs of the lesser image is simple and brilliant. Not quite sure how the smoothing then worked (to remove the slight ghosted appearance of the covert image) ; and the RGBs, to this untrained eye, all look fairly identical...
This is an impressive and comprehensive breakdown of the technique. I admit I haven't digested it all , but nicely done. Wow.
Thank you for your kind words! I'm glad you found the post comprehensive and helpful. Steganography is indeed a fascinating field. If you have any specific questions or areas you'd like me to delve deeper into in my next post, feel free to let me know! I'm here to help. 😊
In the future I want to explore it from different angles.
Very nice blog. stepwise breakdown with code is excellent!
glad to have learnt a new thing today😇
I'm thrilled to hear that you found the content valuable! If you believe others would benefit from this information on steganography as well, please feel free to share it. Spreading knowledge helps us all grow and learn. Remember, "Sharing is caring!" 😊
great buddy. Its aweome
Absolutely! It's wonderful to see that you found the information useful. Knowledge is meant to be shared, and by sharing this post, you're contributing to a broader understanding of steganography. So go ahead, share away! You never know who might find it fascinating or how it could inspire someone. 😊