Using PIL To Insert Greyscale Image Into RGB Image By Inserting Greyscale Values In RGB Tuple
Solution 1:
You're on the right track. That's how you manipulate pixels, though you can do it a little faster using pixel access objects like I've shown below.
It's all pretty straightforward except for extracting and setting the correct digits. In this example, I've done that by dividing by powers of 10 and by using the modulo operator, though there are other ways. Hopefully the comments explain it well enough.
from PIL import Image
def add_watermark(watermark_path, image_in_path, image_out_path):
# Load watermark and image and check sizes and modes
watermark = Image.open(watermark_path)
assert watermark.mode == 'L'
image = Image.open(image_in_path)
assert image.mode == 'RGB'
assert watermark.size == image.size
# Get pixel access objects
watermark_pixels = watermark.load()
image_pixels = image.load()
# Watermark each pixel
for x in range(image.size[0]):
for y in xrange(image.size[1]):
# Get the tuple of rgb values and convert to a list (mutable)
rgb = list(image_pixels[x, y])
for i, p in enumerate(rgb):
# Divide the watermark pixel by 100 (r), then 10 (g), then 1 (b)
# Then take it modulo 10 to get the last digit
watermark_digit = (watermark_pixels[x, y] / (10 ** (2 - i))) % 10
# Divide and multiply value by 10 to zero the last digit
# Then add the watermark digit
rgb[i] = (p / 10) * 10 + watermark_digit
# Convert back to a tuple and store in the image
image_pixels[x, y] = tuple(rgb)
# Save the image
image.save(image_out_path)
Solution 2:
If you are interested in watermarking images, you might want to take a look at steganography. As an example, Digital_Sight is a working demonstration of the concept and could be used as a basis for storing text used as a watermark. To study how modifying various pixel-bits in an image can alter its quality, you might want to play around with Color_Disruptor before deciding what data to overwrite.
Digital_Sight
import cStringIO
from PIL import Image
import bz2
import math
################################################################################
PIXELS_PER_BLOCK = 4
BYTES_PER_BLOCK = 3
MAX_DATA_BYTES = 16777215
################################################################################
class ByteWriter:
"ByteWriter(image) -> ByteWriter instance"
def __init__(self, image):
"Initalize the ByteWriter's internal variables."
self.__width, self.__height = image.size
self.__space = bytes_in_image(image)
self.__pixels = image.load()
def write(self, text):
"Compress and write the text to the image's pixels."
data = bz2.compress(text)
compressed_size = len(data)
if compressed_size > self.__space:
raise MemoryError('There is not enough space for the data!')
size_data = self.__encode_size(compressed_size)
tail = '\0' * ((3 - compressed_size) % 3)
buffer = size_data + data + tail
self.__write_buffer(buffer)
@staticmethod
def __encode_size(number):
"Convert number into a 3-byte block for writing."
data = ''
for _ in range(3):
number, lower = divmod(number, 256)
data = chr(lower) + data
return data
def __write_buffer(self, buffer):
"Write the buffer to the image in blocks."
addr_iter = self.__make_addr_iter()
data_iter = self.__make_data_iter(buffer)
for trio in data_iter:
self.__write_trio(trio, addr_iter.next())
def __make_addr_iter(self):
"Iterate over addresses of pixels to write to."
addr_group = []
for x in range(self.__width):
for y in range(self.__height):
addr_group.append((x, y))
if len(addr_group) == 4:
yield tuple(addr_group)
addr_group = []
@staticmethod
def __make_data_iter(buffer):
"Iterate over the buffer a block at a time."
if len(buffer) % 3 != 0:
raise ValueError('Buffer has a bad size!')
data = ''
for char in buffer:
data += char
if len(data) == 3:
yield data
data = ''
def __write_trio(self, trio, addrs):
"Write a 3-byte block to the pixels addresses given."
duo_iter = self.__make_duo_iter(trio)
tri_iter = self.__make_tri_iter(duo_iter)
for (r_duo, g_duo, b_duo), addr in zip(tri_iter, addrs):
r, g, b, a = self.__pixels[addr]
r = self.__set_two_bits(r, r_duo)
g = self.__set_two_bits(g, g_duo)
b = self.__set_two_bits(b, b_duo)
self.__pixels[addr] = r, g, b, a
@staticmethod
def __make_duo_iter(trio):
"Iterate over 2-bits that need to be written."
for char in trio:
byte = ord(char)
duos = []
for _ in range(4):
byte, duo = divmod(byte, 4)
duos.append(duo)
for duo in reversed(duos):
yield duo
@staticmethod
def __make_tri_iter(duo_iter):
"Group bits into their pixel units for writing."
group = []
for duo in duo_iter:
group.append(duo)
if len(group) == 3:
yield tuple(group)
group = []
@staticmethod
def __set_two_bits(byte, duo):
"Write a duo (2-bit) group to a pixel channel (RGB)."
if duo > 3:
raise ValueError('Duo bits has to high of a value!')
byte &= 252
byte |= duo
return byte
################################################################################
class ByteReader:
"ByteReader(image) -> ByteReader instance"
def __init__(self, image):
"Initalize the ByteReader's internal variables."
self.__width, self.__height = image.size
self.__pixels = image.load()
def read(self):
"Read data out of a picture, decompress the data, and return it."
compressed_data = ''
addr_iter = self.__make_addr_iter()
size_block = self.__read_blocks(addr_iter)
size_value = self.__block_to_number(size_block)
blocks_to_read = math.ceil(size_value / 3.0)
for _ in range(blocks_to_read):
compressed_data += self.__read_blocks(addr_iter)
if len(compressed_data) != blocks_to_read * 3:
raise ValueError('Blocks were not read correctly!')
if len(compressed_data) > size_value:
compressed_data = compressed_data[:size_value]
return bz2.decompress(compressed_data)
def __make_addr_iter(self):
"Iterate over the pixel addresses in the image."
addr_group = []
for x in range(self.__width):
for y in range(self.__height):
addr_group.append((x, y))
if len(addr_group) == 4:
yield tuple(addr_group)
addr_group = []
def __read_blocks(self, addr_iter):
"Read data a block at a time (4 pixels for 3 bytes)."
pixels = []
for addr in addr_iter.next():
pixels.append(self.__pixels[addr])
duos = self.__get_pixel_duos(pixels)
data = ''
buffer = []
for duo in duos:
buffer.append(duo)
if len(buffer) == 4:
value = 0
for duo in buffer:
value <<= 2
value |= duo
data += chr(value)
buffer = []
if len(data) != 3:
raise ValueError('Data was not decoded properly!')
return data
@classmethod
def __get_pixel_duos(cls, pixels):
"Extract bits from a given group of pixels."
duos = []
for pixel in pixels:
duos.extend(cls.__extract_duos(pixel))
return duos
@staticmethod
def __extract_duos(pixel):
"Retrieve the bits stored in a pixel."
r, g, b, a = pixel
return r & 3, g & 3, b & 3
@staticmethod
def __block_to_number(block):
"Convert a block into a number (size of data buffer)."
value = 0
for char in block:
value <<= 8
value |= ord(char)
return value
################################################################################
def main(picture, mode, text):
"Dispatch the various operations that can be requested."
image = Image.open(picture)
if image.mode != 'RGBA':
image = image.convert('RGBA')
if mode == 'Evaluate':
evaluate(image)
elif mode == 'Simulate':
simulate(image, text)
elif mode == 'Encode':
encode(image, text)
elif mode == 'Decode':
decode(image)
else:
raise ValueError('Mode %r was not recognized!' % mode)
################################################################################
def evaluate(image):
"Display the number of bytes available in the image."
print 'Usable bytes available =', bytes_in_image(image)
def bytes_in_image(image):
"Calculate the number of usable bytes in an image."
blocks = blocks_in_image(image)
usable_blocks = blocks - 1
usable_bytes = usable_blocks * BYTES_PER_BLOCK
return min(usable_bytes, MAX_DATA_BYTES)
def blocks_in_image(image):
"Find out how many blocks are in an image."
width, height = image.size
pixels = width * height
blocks = pixels / PIXELS_PER_BLOCK
return blocks
################################################################################
def simulate(image, text):
"Find out how much space the text takes in the image that was given."
compressed_data = bz2.compress(text)
compressed_size = len(compressed_data)
usable_bytes = bytes_in_image(image)
space_leftover = usable_bytes - compressed_size
if space_leftover > 0:
print 'You still have %s more bytes for storage.' % space_leftover
elif space_leftover < 0:
print 'You overfilled the image by %s bytes.' % -space_leftover
else:
print 'This is a perfect fit!'
################################################################################
def encode(image, text):
"Encodes text in image and returns picture to the browser."
mutator = ByteWriter(image)
mutator.write(text)
output = cStringIO.StringIO()
image.save(output, 'PNG')
output.seek(0)
print 'Content-Type: image/PNG'
print output.read()
################################################################################
def decode(image):
"Extract the original message and deliver it to the client."
accessor = ByteReader(image)
buffer = accessor.read()
print buffer
################################################################################
if __name__ == '__builtin__':
try:
main(cStringIO.StringIO(PICTURE), MODE, TEXT)
except SystemExit:
pass
Color_Disruptor
from cStringIO import StringIO
from PIL import Image
from random import randrange
def main(data, r_bits, g_bits, b_bits, a_bits):
image = Image.open(data)
if image.mode != 'RGBA':
image = image.convert('RGBA')
width, height = image.size
array = image.load()
data.close()
for x in range(width):
for y in range(height):
r, g, b, a = array[x, y]
r ^= randrange(r_bits)
g ^= randrange(g_bits)
b ^= randrange(b_bits)
a ^= randrange(a_bits)
array[x, y] = r, g, b, a
data = StringIO()
image.save(data, 'PNG')
print 'Content-Type: image/PNG'
print data.getvalue()
if __name__ == '__builtin__':
main(StringIO(DATA), *map(lambda bits: 1 << int(bits), (R, G, B, A)))
Post a Comment for "Using PIL To Insert Greyscale Image Into RGB Image By Inserting Greyscale Values In RGB Tuple"