画像に透かしを入れることに興味がある場合は、ステガノグラフィーをご覧になることをお勧めします。例として、Digital_Sightは概念の実用的なデモンストレーションであり、透かしとして使用されるテキストを格納するための基礎として使用できます。画像内のさまざまなピクセル ビットを変更するとその品質がどのように変化するかを調べるには、どのデータを上書きするかを決定する前に、 Color_Disruptorを試してみることをお勧めします。
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)))