Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT Add image overlay converter #507

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d6ba5b5
created image_overlay_converter.py
u7702792 Oct 15, 2024
947ef15
update __init__.py to add the new converter
u7702792 Oct 15, 2024
7a7bfad
minor change on orders
u7702792 Oct 15, 2024
47f58e0
Merge branch 'Azure:main' into main
u7702792 Oct 18, 2024
d210747
update on image_overlay_converter.py, to make the whole class more co…
u7702792 Oct 18, 2024
b14883e
Merge branch 'Azure:main' into main
u7702792 Oct 20, 2024
53bd55c
Merge branch 'Azure:main' into feat/add_image_overlay_converter
u7702792 Oct 20, 2024
21138d9
Merge branch 'main' of https://github.com/u7702792/PyRIT into feat/ad…
u7702792 Oct 20, 2024
c42d9e4
Merge remote-tracking branch 'origin/feat/add_image_overlay_converter…
u7702792 Oct 20, 2024
09efd5a
add some test cases of image_overlay_converter.py to git
u7702792 Oct 20, 2024
ec3b110
update the image_overlay_converter.py
u7702792 Oct 20, 2024
3af29ea
update
u7702792 Oct 27, 2024
8b080be
Merge branch 'Azure:main' into main
u7702792 Oct 27, 2024
9cd8f13
Merge branch 'main' of https://github.com/u7702792/PyRIT into feat/ad…
u7702792 Oct 27, 2024
2929d5e
Merge branch 'Azure:main' into feat/add_image_overlay_converter
u7702792 Oct 27, 2024
0ffe26a
Merge branch 'Azure:main' into feat/add_image_overlay_converter
u7702792 Nov 11, 2024
e09e20a
update on image_overlay_converter.py to fix the issue from PR comments
u7702792 Nov 11, 2024
46de42f
Merge remote-tracking branch 'origin/feat/add_image_overlay_converter…
u7702792 Nov 11, 2024
615e7fb
update on test_image_overlay_converter.py to fix the issue from PR co…
u7702792 Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyrit/prompt_converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
FuzzerSimilarConverter,
)
from pyrit.prompt_converter.human_in_the_loop_converter import HumanInTheLoopConverter
from pyrit.prompt_converter.image_overlay_converter import ImageOverlayConverter
from pyrit.prompt_converter.leetspeak_converter import LeetspeakConverter
from pyrit.prompt_converter.morse_converter import MorseConverter
from pyrit.prompt_converter.malicious_question_generator_converter import MaliciousQuestionGeneratorConverter
Expand All @@ -48,6 +49,7 @@
from pyrit.prompt_converter.variation_converter import VariationConverter



__all__ = [
"AddImageTextConverter",
"AddTextImageConverter",
Expand All @@ -70,6 +72,7 @@
"FuzzerShortenConverter",
"FuzzerSimilarConverter",
"HumanInTheLoopConverter",
"ImageOverlayConverter",
"LeetspeakConverter",
"LLMGenericTextConverter",
"MaliciousQuestionGeneratorConverter",
Expand Down
103 changes: 103 additions & 0 deletions pyrit/prompt_converter/image_overlay_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import base64
from typing import Optional

from PIL import Image
from io import BytesIO
from pathlib import Path

from pyrit.models import data_serializer_factory
from pyrit.models import PromptDataType
from pyrit.prompt_converter import PromptConverter, ConverterResult
from pyrit.memory import MemoryInterface, DuckDBMemory


class ImageOverlayConverter(PromptConverter):
"""
A converter that takes in a base image, and a secondary image to embed within the main image.

Args:
base_image_path (str): File path of the base image
x_pos (int, optional): X coordinate to place second image on the base image (0 is left most). Defaults to 0.
y_pos (int, optional): Y coordinate to place second image on the base image (0 is upper most). Defaults to 0.
memory: (memory, optional): Memory to store the chat messages. DuckDBMemory will be used by default.
"""

def __init__(
self,
base_image_path: str,
x_pos: Optional[int] = 0,
y_pos: Optional[int] = 0,
memory: Optional[MemoryInterface] = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#527 is removing the memory arg so you won't need to have this arg, but when you need to use memory you can simply do CentralMemory.get_memory_instance()

Let me know if this doesn't work or make sense.

):
if not base_image_path:
raise ValueError("Please provide valid image path")

file_from_path = Path(base_image_path)
if not file_from_path.is_file():
raise ValueError("File does not exist")

if x_pos < 0 or y_pos < 0:
raise ValueError("Position is out of boundary ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would split this up into two checks and have a more specific error message like "x_pos is less than 0 and therefore out of bounds.


self._base_image_path = base_image_path
self._x_pos = x_pos
self._y_pos = y_pos
self._memory = memory or DuckDBMemory()

def _add_overlay_image(self, overlay_image_path: str) -> Image.Image:
"""
Embed the second image onto the base image

Args:
overlay_image_path(str): The Path of second image

Returns:
Image.Image: The combined image with overlay.
"""
if not overlay_image_path:
raise ValueError("Please provide a valid image path")
# Open the images
with Image.open(self._base_image_path) as base_image, Image.open(overlay_image_path) as overlay_image:
# Paste the second image onto the base image
# And make a copy of the result, so it is accessible after "with" close
base_image.paste(overlay_image, (self._x_pos, self._y_pos), overlay_image)
result_image = base_image.copy()
return result_image

async def convert_async(self, *, prompt: str, input_type: PromptDataType = "image_path") -> ConverterResult:
"""
Converter the base image to embed the second image onto it.

Args:
prompt (str): The filename of the second image
input_type (PromptDataType): type of data, should be image_path

Returns:
ConverterResult: converted image with file path
"""
if not self.input_supported(input_type):
raise ValueError("Input type not supported")

second_img_from_path = Path(prompt)
if not second_img_from_path.is_file():
raise ValueError("Overlay Image File does not exist")

# Add overlay to the base image
updated_img = self._add_overlay_image(prompt)

# Create a new data serializer to save the images
updated_image_serializer = data_serializer_factory(
data_type="image_path",
extension="png" # you can change the file type as you want
)

# Save the result to the path generated by the serializer
updated_img.save(updated_image_serializer.get_data_filename())

return ConverterResult(output_text=str(updated_image_serializer.value), output_type="image_path")

def input_supported(self, input_type: PromptDataType) -> bool:
return input_type == "image_path"
86 changes: 86 additions & 0 deletions tests/converter/test_image_overlay_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock

import pytest
from PIL import Image

from pyrit.prompt_converter import ImageOverlayConverter

from io import BytesIO


@pytest.fixture
def base_image_path():
# Create a temporary file with a unique name
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
# Generate a simple image and save it to the temporary file path
img = Image.new("RGB", (100, 100), color=(255, 255, 255))
img.save(tmp.name)
temp_path = tmp.name # Store the temporary file path

yield temp_path # Provide the path to the test

# Cleanup after the test
os.remove(temp_path)


@pytest.fixture
def overlay_image_path():
# Create a temporary file with a unique name for overlay image
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
# Generate a simple image and save it to the temporary file path
img = Image.new("RGB", (10, 10), color=(100, 100, 100))
img.save(tmp.name)
temp_path = tmp.name # Store the temporary file path

yield temp_path # Provide the path to the test

# Cleanup after the test
os.remove(temp_path)


def test_image_overlay_converter_initialization(base_image_path):
# use MagicMock for memory
memory_mock = MagicMock()

converter = ImageOverlayConverter(
base_image_path=base_image_path, x_pos=10, y_pos=15, memory=memory_mock
)
assert converter._base_image_path == base_image_path, " Base image path should be initialized"
assert converter._x_pos == 10, "X position should be 10"
assert converter._y_pos == 15, "Y position should be 15"


def test_image_overlay_converter_invalid_image():
with pytest.raises(ValueError):
ImageOverlayConverter(base_image_path="")


@pytest.mark.asyncio
async def test_image_overlay_converter_convert_async(base_image_path, overlay_image_path):
# Initialize the converter with the base image path
converter = ImageOverlayConverter(base_image_path=base_image_path)

# Call the async `convert_async` method with the overlay image path as the prompt
result = await converter.convert_async(prompt=overlay_image_path, input_type="image_path")

# Verify that the result contains a valid file path in `output_text`
assert isinstance(result.output_text, str), "The result should contain a file path as output_text."
output_path = Path(result.output_text)

# Check that the output path exists and is a file
assert output_path.is_file(), "The output image file should exist."

# Open the result image and verify its properties
with Image.open(output_path) as img:
# Ensure the output image dimensions match the base image
assert img.size == Image.open(base_image_path).size, "The output image size should match the base image size."
# Check if the image mode is appropriate
assert img.mode in ["RGB", "RGBA"], "The output image mode should be RGB or RGBA."

# Cleanup after test
output_path.unlink()