update v1
Browse files- app.py +102 -0
- requirement.txt +4 -0
- steganography.py +127 -0
- utils.py +25 -0
app.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from steganography import Steganography
|
| 3 |
+
from utils import draw_multiple_line_text, generate_qr_code
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
TITLE = """<h2 align="center"> ✍️ Invisible Watermark </h2>"""
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def apply_watermark(radio_button, input_image, watermark_image, watermark_text, watermark_url):
|
| 10 |
+
input_image = input_image.convert('RGB')
|
| 11 |
+
|
| 12 |
+
if radio_button == "Image":
|
| 13 |
+
watermark_image = watermark_image.resize((input_image.width, input_image.height)).convert('L').convert('RGB')
|
| 14 |
+
return Steganography().merge(input_image, watermark_image, digit=7)
|
| 15 |
+
elif radio_button == "Text":
|
| 16 |
+
watermark_image = draw_multiple_line_text(input_image.size, watermark_text)
|
| 17 |
+
return Steganography().merge(input_image, watermark_image, digit=7)
|
| 18 |
+
else:
|
| 19 |
+
size = min(input_image.width, input_image.height)
|
| 20 |
+
watermark_image = generate_qr_code(watermark_url).resize((size, size)).convert('RGB')
|
| 21 |
+
return Steganography().merge(input_image, watermark_image, digit=7)
|
| 22 |
+
|
| 23 |
+
def extract_watermark(input_image_to_extract):
|
| 24 |
+
return Steganography().unmerge(input_image_to_extract.convert('RGB'), digit=7).convert('RGBA')
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
with gr.Blocks() as demo:
|
| 28 |
+
gr.HTML(TITLE)
|
| 29 |
+
with gr.Tab("Add watermark"):
|
| 30 |
+
with gr.Row():
|
| 31 |
+
with gr.Column():
|
| 32 |
+
gr.Markdown("### Image to apply watermark")
|
| 33 |
+
input_image = gr.Image(type='pil')
|
| 34 |
+
with gr.Blocks():
|
| 35 |
+
gr.Markdown("### Which type of watermark you want to apply?")
|
| 36 |
+
radio_button = gr.Radio(
|
| 37 |
+
choices=["QRCode", "Text", "Image"],
|
| 38 |
+
label="Watermark type",
|
| 39 |
+
value="QRCode",
|
| 40 |
+
# info="Which type of watermark you want to apply?"
|
| 41 |
+
)
|
| 42 |
+
watermark_url = gr.Textbox(
|
| 43 |
+
placeholder="URL to generate QR code",
|
| 44 |
+
visible=True
|
| 45 |
+
)
|
| 46 |
+
watermark_text = gr.Textbox(
|
| 47 |
+
placeholder="What text you want to use as watermark?",
|
| 48 |
+
visible=False
|
| 49 |
+
)
|
| 50 |
+
watermark_image = gr.Image(
|
| 51 |
+
type='pil',
|
| 52 |
+
visible=False
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def update_visability(radio_value):
|
| 56 |
+
return {
|
| 57 |
+
watermark_image:
|
| 58 |
+
{
|
| 59 |
+
"visible":radio_value == "Image",
|
| 60 |
+
"__type__": "update"
|
| 61 |
+
},
|
| 62 |
+
watermark_text:
|
| 63 |
+
{
|
| 64 |
+
"visible":radio_value == "Text",
|
| 65 |
+
"__type__": "update"
|
| 66 |
+
},
|
| 67 |
+
watermark_url:
|
| 68 |
+
{
|
| 69 |
+
"visible":radio_value == "QRCode",
|
| 70 |
+
"__type__": "update"
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
with gr.Column():
|
| 75 |
+
gr.Markdown("### Appied watermark image")
|
| 76 |
+
output_image = gr.Image(show_label=False)
|
| 77 |
+
with gr.Row():
|
| 78 |
+
apply_button =gr.Button("Apply")
|
| 79 |
+
|
| 80 |
+
with gr.Tab("Extract watermark"):
|
| 81 |
+
with gr.Row():
|
| 82 |
+
with gr.Column():
|
| 83 |
+
gr.Markdown("### Image to extract watermark")
|
| 84 |
+
input_image_to_extract = gr.Image(type='pil')
|
| 85 |
+
with gr.Column():
|
| 86 |
+
gr.Markdown("### Extracted watermark")
|
| 87 |
+
extracted_watermark = gr.Image(type='pil')
|
| 88 |
+
extract_button = gr.Button("Extract")
|
| 89 |
+
|
| 90 |
+
radio_button.change(
|
| 91 |
+
fn=update_visability,
|
| 92 |
+
inputs=radio_button,
|
| 93 |
+
outputs=[watermark_image, watermark_text, watermark_url]
|
| 94 |
+
)
|
| 95 |
+
apply_button.click(
|
| 96 |
+
fn=apply_watermark,
|
| 97 |
+
inputs=[radio_button, input_image, watermark_image, watermark_text, watermark_url],
|
| 98 |
+
outputs=[output_image]
|
| 99 |
+
)
|
| 100 |
+
extract_button.click(fn=extract_watermark, inputs=[input_image_to_extract], outputs=[extracted_watermark])
|
| 101 |
+
|
| 102 |
+
demo.launch()
|
requirement.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Pillow
|
| 2 |
+
click
|
| 3 |
+
gradio
|
| 4 |
+
qrcode
|
steganography.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
|
| 3 |
+
from PIL import Image
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Steganography:
|
| 7 |
+
|
| 8 |
+
BLACK_PIXEL = (0, 0, 0)
|
| 9 |
+
|
| 10 |
+
def _int_to_bin(self, rgb):
|
| 11 |
+
"""Convert an integer tuple to a binary (string) tuple.
|
| 12 |
+
|
| 13 |
+
:param rgb: An integer tuple like (220, 110, 96)
|
| 14 |
+
:return: A string tuple like ("00101010", "11101011", "00010110")
|
| 15 |
+
"""
|
| 16 |
+
r, g, b = rgb
|
| 17 |
+
return f'{r:08b}', f'{g:08b}', f'{b:08b}'
|
| 18 |
+
|
| 19 |
+
def _bin_to_int(self, rgb):
|
| 20 |
+
"""Convert a binary (string) tuple to an integer tuple.
|
| 21 |
+
|
| 22 |
+
:param rgb: A string tuple like ("00101010", "11101011", "00010110")
|
| 23 |
+
:return: Return an int tuple like (220, 110, 96)
|
| 24 |
+
"""
|
| 25 |
+
r, g, b = rgb
|
| 26 |
+
return int(r, 2), int(g, 2), int(b, 2)
|
| 27 |
+
|
| 28 |
+
def _merge_rgb(self, rgb1, rgb2, digit):
|
| 29 |
+
"""Merge two RGB tuples.
|
| 30 |
+
|
| 31 |
+
:param rgb1: An integer tuple like (220, 110, 96)
|
| 32 |
+
:param rgb2: An integer tuple like (240, 95, 105)
|
| 33 |
+
:return: An integer tuple with the two RGB values merged.
|
| 34 |
+
"""
|
| 35 |
+
r1, g1, b1 = self._int_to_bin(rgb1)
|
| 36 |
+
r2, g2, b2 = self._int_to_bin(rgb2)
|
| 37 |
+
rgb = r1[:digit] + r2[:8-digit], g1[:digit] + g2[:8-digit], b1[:digit] + b2[:8-digit]
|
| 38 |
+
return self._bin_to_int(rgb)
|
| 39 |
+
|
| 40 |
+
def _unmerge_rgb(self, rgb, digit):
|
| 41 |
+
"""Unmerge RGB.
|
| 42 |
+
|
| 43 |
+
:param rgb: An integer tuple like (220, 110, 96)
|
| 44 |
+
:return: An integer tuple with the two RGB values merged.
|
| 45 |
+
"""
|
| 46 |
+
r, g, b = self._int_to_bin(rgb)
|
| 47 |
+
# Extract the last 4 bits (corresponding to the hidden image)
|
| 48 |
+
# Concatenate 4 zero bits because we are working with 8 bit
|
| 49 |
+
new_rgb = r[digit:] + '0'*digit, g[digit:] + '0'*digit, b[digit:] + '0'*digit
|
| 50 |
+
return self._bin_to_int(new_rgb)
|
| 51 |
+
|
| 52 |
+
def merge(self, image1, image2, digit=4):
|
| 53 |
+
"""Merge image2 into image1.
|
| 54 |
+
|
| 55 |
+
:param image1: First image
|
| 56 |
+
:param image2: Second image
|
| 57 |
+
:return: A new merged image.
|
| 58 |
+
"""
|
| 59 |
+
# Check the images dimensions
|
| 60 |
+
if image2.size[0] > image1.size[0] or image2.size[1] > image1.size[1]:
|
| 61 |
+
raise ValueError('Image 2 should be smaller than Image 1!')
|
| 62 |
+
|
| 63 |
+
# Get the pixel map of the two images
|
| 64 |
+
map1 = image1.load()
|
| 65 |
+
map2 = image2.load()
|
| 66 |
+
|
| 67 |
+
new_image = Image.new(image1.mode, image1.size)
|
| 68 |
+
new_map = new_image.load()
|
| 69 |
+
|
| 70 |
+
for i in range(image1.size[0]):
|
| 71 |
+
for j in range(image1.size[1]):
|
| 72 |
+
is_valid = lambda: i < image2.size[0] and j < image2.size[1]
|
| 73 |
+
rgb1 = map1[i ,j]
|
| 74 |
+
rgb2 = map2[i, j] if is_valid() else self.BLACK_PIXEL
|
| 75 |
+
new_map[i, j] = self._merge_rgb(rgb1, rgb2, digit)
|
| 76 |
+
|
| 77 |
+
return new_image
|
| 78 |
+
|
| 79 |
+
def unmerge(self, image, digit=4, binarization=True):
|
| 80 |
+
"""Unmerge an image.
|
| 81 |
+
|
| 82 |
+
:param image: The input image.
|
| 83 |
+
:return: The unmerged/extracted image.
|
| 84 |
+
"""
|
| 85 |
+
pixel_map = image.load()
|
| 86 |
+
|
| 87 |
+
# Create the new image and load the pixel map
|
| 88 |
+
new_image = Image.new(image.mode, image.size)
|
| 89 |
+
new_map = new_image.load()
|
| 90 |
+
|
| 91 |
+
for i in range(image.size[0]):
|
| 92 |
+
for j in range(image.size[1]):
|
| 93 |
+
r, g, b = self._unmerge_rgb(pixel_map[i, j], digit)
|
| 94 |
+
r = 255 if r >= 128 else 0
|
| 95 |
+
g = 255 if g >= 128 else 0
|
| 96 |
+
b = 255 if b >= 128 else 0
|
| 97 |
+
new_map[i, j] = r, g, b
|
| 98 |
+
|
| 99 |
+
return new_image
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
parser = argparse.ArgumentParser(description='Steganography')
|
| 104 |
+
subparser = parser.add_subparsers(dest='command')
|
| 105 |
+
|
| 106 |
+
merge = subparser.add_parser('merge')
|
| 107 |
+
merge.add_argument('--image1', required=True, help='Image1 path')
|
| 108 |
+
merge.add_argument('--image2', required=True, help='Image2 path')
|
| 109 |
+
merge.add_argument('--output', required=True, help='Output path')
|
| 110 |
+
|
| 111 |
+
unmerge = subparser.add_parser('unmerge')
|
| 112 |
+
unmerge.add_argument('--image', required=True, help='Image path')
|
| 113 |
+
unmerge.add_argument('--output', required=True, help='Output path')
|
| 114 |
+
|
| 115 |
+
args = parser.parse_args()
|
| 116 |
+
|
| 117 |
+
if args.command == 'merge':
|
| 118 |
+
image1 = Image.open(args.image1)
|
| 119 |
+
image2 = Image.open(args.image2)
|
| 120 |
+
Steganography().merge(image1, image2).save(args.output)
|
| 121 |
+
elif args.command == 'unmerge':
|
| 122 |
+
image = Image.open(args.image)
|
| 123 |
+
Steganography().unmerge(image).save(args.output)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
if __name__ == '__main__':
|
| 127 |
+
main()
|
utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 2 |
+
import qrcode
|
| 3 |
+
|
| 4 |
+
def generate_qr_code(url):
|
| 5 |
+
return qrcode.make(url)
|
| 6 |
+
|
| 7 |
+
def draw_multiple_line_text(input_image_size, text, font=None, text_color=(255, 255, 255)):
|
| 8 |
+
if font is None:
|
| 9 |
+
font = ImageFont.load_default()
|
| 10 |
+
watermark_image = Image.new("RGB", input_image_size, (0, 0, 0))
|
| 11 |
+
output_image = watermark_image.copy()
|
| 12 |
+
draw = ImageDraw.Draw(watermark_image)
|
| 13 |
+
image_width, image_height = input_image_size
|
| 14 |
+
line_width, line_height = font.getsize(text)
|
| 15 |
+
draw.text(
|
| 16 |
+
((image_width - line_width)/2, (image_height - line_height)/2),
|
| 17 |
+
text,
|
| 18 |
+
font=font,
|
| 19 |
+
fill=text_color
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
scale = min(image_width / line_width, image_height / line_height)
|
| 23 |
+
watermark_image = watermark_image.resize((int(watermark_image.width * scale), int(watermark_image.height*scale)))
|
| 24 |
+
output_image.paste(watermark_image, (int((image_width-watermark_image.width)/2), int((image_height-watermark_image.height)/2)))
|
| 25 |
+
return output_image
|