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
|