diff --git a/README.md b/README.md new file mode 100644 index 0000000..f14e533 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# Steganography Tool + +## Overview + +This tool provides functionality to hide and extract images within other images using steganographic techniques. It uses Least Significant Bit (LSB) modification to embed and extract hidden images while maintaining the visual integrity of the carrier image. + +## Features + +- **Merge Images**: Embed one image into another. +- **Extract Hidden Image**: Retrieve a hidden image from a merged image. + +## Installation + +To get started with the Steganography Tool, you'll need Python and some libraries. Follow these steps: + +1. **Clone the Repository**: + + ```bash + git clone https://github.com/K11E3R/steganography-tool.git + cd steganography-tool + ``` + +2. **Create a Virtual Environment (optional but recommended)**: + + ```bash + python -m venv venv + source venv/bin/activate # On Windows use: venv\Scripts\activate + ``` + +3. **Install Dependencies**: + + Use the provided `requirements.txt` to install the necessary Python libraries: + + ```bash + pip install -r requirements.txt + ``` + +## Usage + +### Command Line Interface + +The tool is designed to be used from the command line. Below are instructions for each command. + +#### Merging Images + +To merge one image into another, use: + +```bash +python steganography.py merge --image1 img/base_image.png --image2 img/image_to_hide.png --output path/to/output_image.png +``` + +- `--image1`: Path to the base image where the second image will be hidden. +- `--image2`: Path to the image to be hidden. +- `--output`: Path to save the merged image. + +#### Extracting Hidden Image + +To extract the hidden image from a merged image, use: + +```bash +python steganography.py unmerge --image path/to/merged_image.png --output path/to/extracted_image.png +``` + +- `--image`: Path to the merged image from which the hidden image will be extracted. +- `--output`: Path to save the extracted image. + +## Code Explanation + +### Loading Spinner + +A loading spinner is used to indicate progress during long operations. + +- **`loading_spinner()`**: Runs a spinner in a loop to show activity. +- **`show_loading_spinner()`**: Starts the spinner in a separate thread. +- **`stop_loading_spinner(spinner_thread)`**: Stops the spinner and prints "Done!". + +### Steganography Class + +This class handles the embedding and extraction of images. + +- **`_merge_rgb(self, rgb1, rgb2)`**: Combines two RGB color values by merging the lower 4 bits of `rgb2` with the higher 4 bits of `rgb1`. + + ```python + def _merge_rgb(self, rgb1, rgb2): + return tuple((c1 & 0xF0) | (c2 >> 4) for c1, c2 in zip(rgb1, rgb2)) + ``` + +- **`_unmerge_rgb(self, rgb)`**: Extracts the hidden color values from a merged RGB tuple. + + ```python + def _unmerge_rgb(self, rgb): + return tuple(((c & 0x0F) << 4) | ((c & 0xF0) >> 4) for c in rgb) + ``` + +- **`merge(self, image1, image2, output)`**: Embeds `image2` into `image1` and saves the result. + + ```python + def merge(self, image1, image2, output): + if image2.size[0] > image1.size[0] or image2.size[1] > image1.size[1]: + raise ValueError('Image 2 must be smaller than or equal to Image 1 in both dimensions!') + + if image1.mode != 'RGB' or image2.mode != 'RGB': + raise ValueError('Both images must be in RGB mode!') + + print("Merging images...") + spinner_thread = show_loading_spinner() + try: + img1_array = np.array(image1) + img2_array = np.array(image2) + height, width = img2_array.shape[:2] + merged_array = np.copy(img1_array) + + merged_array[:height, :width] = np.array([self._merge_rgb(tuple(c1), tuple(c2)) + for c1, c2 in zip(img1_array[:height, :width], img2_array)]) + + new_image = Image.fromarray(merged_array, mode='RGB') + new_image.save(output) + finally: + stop_loading_spinner(spinner_thread) + print(f"Image merged and saved as '{output}'") + ``` + +- **`unmerge(self, image, output)`**: Extracts the hidden image from `image` and saves it. + + ```python + def unmerge(self, image, output): + if image.mode != 'RGB': + raise ValueError('The image must be in RGB mode!') + + print("Extracting hidden image...") + spinner_thread = show_loading_spinner() + try: + img_array = np.array(image) + unmerged_array = np.array([self._unmerge_rgb(tuple(c)) + for c in img_array]) + + new_image = Image.fromarray(unmerged_array, mode='RGB') + new_image.save(output) + finally: + stop_loading_spinner(spinner_thread) + print(f"Hidden image extracted and saved as '{output}'") + ``` + +### Main Function + +The `main()` function handles command-line arguments and calls the appropriate steganography methods. + +```python +def main(): + parser = argparse.ArgumentParser(description='Steganography Tool') + subparser = parser.add_subparsers(dest='command') + + merge = subparser.add_parser('merge', help='Merge one image into another') + merge.add_argument('--image1', required=True, help='Path to the base image') + merge.add_argument('--image2', required=True, help='Path to the image to be hidden') + merge.add_argument('--output', required=True, help='Path to save the merged image') + + unmerge = subparser.add_parser('unmerge', help='Extract hidden image from a merged image') + unmerge.add_argument('--image', required=True, help='Path to the merged image') + unmerge.add_argument('--output', required=True, help='Path to save the extracted image') + + args = parser.parse_args() + + try: + stego = Steganography() + if args.command == 'merge': + with Image.open(args.image1) as image1, Image.open(args.image2) as image2: + stego.merge(image1, image2, args.output) + elif args.command == 'unmerge': + with Image.open(args.image) as image: + stego.unmerge(image, args.output) + else: + parser.print_help() + except FileNotFoundError as e: + print(f"File not found: {e.filename}") + except ValueError as e: + print(f"Value error: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") +``` + +## Technical Details + +### Steganographic Technique + +- **Merging Logic**: The tool hides an image (`image2`) within another image (`image1`) by combining their RGB values. Specifically, the lower 4 bits of `image2` are merged with the higher 4 bits of `image1`. + +- **Unmerging Logic**: Extracts the hidden image by reversing the merging process, recovering the lower 4 bits from the combined RGB values. + +### Capacity and Limits + +- **Capacity**: The amount of data you can hide depends on the carrier image’s resolution and bit depth. For a 24-bit RGB image, each pixel can theoretically store up to 3 bits of hidden data per color channel. + +- **Quality Considerations**: Hiding more data can reduce the quality of the carrier image. The hidden image's size and the amount of data being hidden affect the visual integrity of the carrier image. + +## Contributing + +If you would like to contribute to this project, please follow these steps: + +1. **Fork the Repository**: Create a copy of the repository under your GitHub account. +2. **Create a Feature Branch**: Create a new branch for your feature or bug fix. +3. **Make Your Changes**: Implement your changes and test thoroughly. +4. **Submit a Pull Request**: Push your changes to your fork and submit a pull request for review. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/advanced_test/base_image.png b/advanced_test/base_image.png new file mode 100644 index 0000000..d49f5a5 Binary files /dev/null and b/advanced_test/base_image.png differ diff --git a/advanced_test/hidden_image.png b/advanced_test/hidden_image.png new file mode 100644 index 0000000..b4fda19 Binary files /dev/null and b/advanced_test/hidden_image.png differ diff --git a/img/base_image.png b/img/base_image.png new file mode 100644 index 0000000..90c5bf6 Binary files /dev/null and b/img/base_image.png differ diff --git a/img/hidden_image.png b/img/hidden_image.png new file mode 100644 index 0000000..b9c71a9 Binary files /dev/null and b/img/hidden_image.png differ diff --git a/img_generator.py b/img_generator.py new file mode 100644 index 0000000..b43991d --- /dev/null +++ b/img_generator.py @@ -0,0 +1,24 @@ +from PIL import Image, ImageDraw + +def generate_image(width, height, color, file_path): + """Solid color image Generator """ + img = Image.new('RGB', (width, height), color) + img.save(file_path) + +def generate_test_images(): + """Image Generator """ + base_image_width, base_image_height = 200, 200 + hidden_image_width, hidden_image_height = 50, 50 + + generate_image(base_image_width, base_image_height, 'blue', 'img/base_image.png') + + generate_image(hidden_image_width, hidden_image_height, 'red', 'img/hidden_image.png') + hidden_img = Image.open('img/hidden_image.png') + draw = ImageDraw.Draw(hidden_img) + draw.rectangle([(10, 10), (40, 40)], outline='black', width=3) + hidden_img.save('img/hidden_image.png') + + print("Test images generating...\nbase_image.png ✅\nhidden_image.png ✅\nboth generated in the 'img' directory") + +if __name__ == '__main__': + generate_test_images() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d76c731 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +numpy==1.24.0 +Pillow==9.5.0 diff --git a/result_advanced_test/extracted_image.png b/result_advanced_test/extracted_image.png new file mode 100644 index 0000000..10d33d3 Binary files /dev/null and b/result_advanced_test/extracted_image.png differ diff --git a/result_advanced_test/merged_image.png b/result_advanced_test/merged_image.png new file mode 100644 index 0000000..e062df0 Binary files /dev/null and b/result_advanced_test/merged_image.png differ diff --git a/results/extracted_image.png b/results/extracted_image.png new file mode 100644 index 0000000..36607ce Binary files /dev/null and b/results/extracted_image.png differ diff --git a/results/merged_image.png b/results/merged_image.png new file mode 100644 index 0000000..663e073 Binary files /dev/null and b/results/merged_image.png differ diff --git a/steganography.py b/steganography.py new file mode 100644 index 0000000..76cc5e9 --- /dev/null +++ b/steganography.py @@ -0,0 +1,122 @@ +import argparse +import numpy as np +from PIL import Image +import itertools +import threading +import time +import sys + +stop_spinner = False + +def loading_spinner(): + """Display a loading spinner.""" + spinner = itertools.cycle(['-', '\\', '|', '/']) + while not stop_spinner: + sys.stdout.write(next(spinner)) + sys.stdout.flush() + sys.stdout.write('\b') + time.sleep(0.1) + +def show_loading_spinner(): + """Start the loading spinner in a separate thread.""" + global stop_spinner + stop_spinner = False + spinner_thread = threading.Thread(target=loading_spinner, daemon=True) + spinner_thread.start() + return spinner_thread + +def stop_loading_spinner(spinner_thread): + """Stop the loading spinner.""" + global stop_spinner + stop_spinner = True + spinner_thread.join() + sys.stdout.write('Done! ✅\n') + sys.stdout.flush() + +class Steganography: + def _merge_rgb(self, rgb1, rgb2): + """Merge two RGB tuples by combining their lower 4 bits.""" + return tuple((c1 & 0xF0) | (c2 >> 4) for c1, c2 in zip(rgb1, rgb2)) + + def _unmerge_rgb(self, rgb): + """Extract the hidden RGB value from a merged RGB tuple.""" + return tuple(((c & 0x0F) << 4) | ((c & 0xF0) >> 4) for c in rgb) + + def merge(self, image1, image2, output): + """Merge image2 into image1 and save to output path.""" + if image2.size[0] > image1.size[0] or image2.size[1] > image1.size[1]: + raise ValueError('Image 2 must be smaller than or equal to Image 1 in both dimensions!') + + if image1.mode != 'RGB' or image2.mode != 'RGB': + raise ValueError('Both images must be in RGB mode!') + + print("Merging images...") + spinner_thread = show_loading_spinner() + try: + img1_array = np.array(image1) + img2_array = np.array(image2) + height, width = img2_array.shape[:2] + merged_array = np.copy(img1_array) + + merged_array[:height, :width] = np.array([self._merge_rgb(tuple(c1), tuple(c2)) + for c1, c2 in zip(img1_array[:height, :width], img2_array)]) + + new_image = Image.fromarray(merged_array, mode='RGB') + new_image.save(output) + finally: + stop_loading_spinner(spinner_thread) + print(f"Image merged and saved as '{output}'") + + def unmerge(self, image, output): + """Unmerge an image to extract the hidden image and save to output path.""" + if image.mode != 'RGB': + raise ValueError('The image must be in RGB mode!') + + print("Extracting hidden image...") + spinner_thread = show_loading_spinner() + try: + img_array = np.array(image) + unmerged_array = np.array([self._unmerge_rgb(tuple(c)) + for c in img_array]) + + new_image = Image.fromarray(unmerged_array, mode='RGB') + new_image.save(output) + finally: + stop_loading_spinner(spinner_thread) + print(f"Hidden image extracted and saved as '{output}'") + +def main(): + """Main function to handle command-line arguments and execute steganography tasks.""" + parser = argparse.ArgumentParser(description='Steganography Tool') + subparser = parser.add_subparsers(dest='command') + + merge = subparser.add_parser('merge', help='Merge one image into another') + merge.add_argument('--image1', required=True, help='Path to the base image') + merge.add_argument('--image2', required=True, help='Path to the image to be hidden') + merge.add_argument('--output', required=True, help='Path to save the merged image') + + unmerge = subparser.add_parser('unmerge', help='Extract hidden image from a merged image') + unmerge.add_argument('--image', required=True, help='Path to the merged image') + unmerge.add_argument('--output', required=True, help='Path to save the extracted image') + + args = parser.parse_args() + + try: + stego = Steganography() + if args.command == 'merge': + with Image.open(args.image1) as image1, Image.open(args.image2) as image2: + stego.merge(image1, image2, args.output) + elif args.command == 'unmerge': + with Image.open(args.image) as image: + stego.unmerge(image, args.output) + else: + parser.print_help() + except FileNotFoundError as e: + print(f"File not found: {e.filename}") + except ValueError as e: + print(f"Value error: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + +if __name__ == '__main__': + main()