Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
406 views
in Technique[技术] by (71.8m points)

python - Alternative method to compare original image with edited one that may not need the original

A while back I did a python script to store data in images, however it has a small problem and I'm just wondering if anyone might be able to think of an alternative method.

A very basic idea is it'll pickle something, then with the first version, it directly wrote the ASCII numbers as pixels (since everything is between 0 and 255). That'll result in an image that looks a little like TV noise.

When writing into an actual image, it'll detect the minimum bits per pixel it needs to adjust so it'll not be noticeable to the human eye, and it'll split the data and add or subtract a few bits from each pixel, with the very first pixel storing the method it's using. I then store the URL as a file in the image, and can reverse it by comparing the original image from the URL with the current image using the rules given in the first pixel.

A bit of python pseudocode in case I didn't explain that well:

original_image = (200, 200, 200, 100, 210, 255...)
stuff_to_store = "test"
#Convert anything into a list of bytes
data_numbers = [bin(ord(x)) for x in cPickle.dumps(stuff_to_store)]

#This is calculated by the code, but for now it's 2
bytes_per_pixel = 2
store_mode = 'subtract'

#Join the bytes and split them every 2nd character
new_bytes = "".join(data_bytes)
new_bytes_split = [new_bytes[i:i+bytes_per_pixel] for i in range(0, len(new_bytes), bytes_per_pixel)]

#Edit the pixels (by subtraction in this case)
pixel_data = []
for i in range(len(original_image)):
    pixel_data = original_image[i] - int(new_bytes_split[i])

However, since the whole point of the script is to store things by modifying the pixels, storing the original image URL as a file feels a bit of a cheaty way to do it. I thought of storing the URL as the first few pixels, but it'd end up with a noticeable line whenever the image wasn't grey. Also, this way is incredibly inefficient as it needs two images to work properly, so it'd be great if anyone had an idea of how to avoid doing this.

The original code is here if anyone is interested, I did it before I learned about writing the documentation so it's a bit hard to figure out, just asking this now as I'm planning on rewriting it and would like to do it better.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Here's one way to embed data into the least significant bit of each colour channel of the pixels in a 8 bit per channel RGB image file, using PIL to do the image handling.

The code below illustrates bit stream handling in Python. It's reasonably efficient (as far as such operations can be efficient in Python), but it sacrifices efficiency for readability & simplicity of use when necessary. :)

#! /usr/bin/env python

''' Steganography with PIL (really Pillow)

    Encodes / decodes bits of a binary data file into the LSB of each color 
    value of each pixel of a non-palette-mapped image.

    Written by PM 2Ring 2015.02.03
'''

import sys
import getopt
import struct
from PIL import Image


def readbits(bytes):
    ''' Generate single bits from bytearray '''
    r = range(7, -1, -1)
    for n in bytes:
        for m in r:
            yield (n>>m) & 1

def encode(image_bytes, mode, size, dname, oname):
    print 'Encoding...'
    with open(dname, 'rb') as dfile:
        payload = bytearray(dfile.read())

    #Prepend encoded data length to payload
    datalen = len(payload)
    print 'Data length:', datalen

    #datalen = bytearray.fromhex(u'%06x' % datalen)
    datalen = bytearray(struct.pack('>L', datalen)[1:])
    payload = datalen + payload

    databits = readbits(payload)
    for i, b in enumerate(databits):
        image_bytes[i] = (image_bytes[i] & 0xfe) | b

    img = Image.frombytes(mode, size, str(image_bytes))
    img.save(oname)


def bin8(i): 
    return bin(i)[2:].zfill(8)

bit_dict = dict((tuple(int(c) for c in bin8(i)), i) for i in xrange(256))

def decode_bytes(data):
    return [bit_dict[t] for t in zip(*[iter(c&1 for c in data)] * 8)]

def decode(image_bytes, dname):
    print 'Decoding...'
    t = decode_bytes(image_bytes[:24])
    datalen = (t[0] << 16) | (t[1] << 8) | t[2]
    print 'Data length:', datalen

    t = decode_bytes(image_bytes[24:24 + 8*datalen])

    with open(dname, 'wb') as dfile:
        dfile.write(str(bytearray(t)))


def process(iname, dname, oname):
    with Image.open(iname) as img:
        mode = img.mode
        if mode == 'P':
            raise ValueError, '%s is a palette-mapped image' % fname
        size = img.size
        image_bytes = bytearray(img.tobytes())
    #del img

    print 'Data capacity:', len(image_bytes) // 8 - 24

    if oname:
        encode(image_bytes, mode, size, dname, oname)
    elif dname:
        decode(image_bytes, dname)


def main():
    #input image filename
    iname = None
    #data filename
    dname = None
    #output image filename
    oname = None

    def usage(msg=None):
        s = msg + '

' if msg else ''
        s += '''Embed data into or extract data from the low-order bits of an image file.

Usage:

%s [-h] -i input_image [-d data_file] [-o output_image]

To encode, you must specify all 3 file names.
To decode, just specify the input image and the data file names.
If only the the input image is given, its capacity will be printed,
i.e., the maximum size (in bytes) of data that it can hold.

Uses PIL (Pillow) to read and write the image data.
Do NOT use lossy image formats for output, eg JPEG, or the data WILL get scrambled.
The program will abort if the input image is palette-mapped, as such images
are not suitable.
'''
        print >>sys.stderr, s % sys.argv[0]
        raise SystemExit, msg!=None

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hi:d:o:")
    except getopt.GetoptError, e:
        usage(e.msg)

    for o, a in opts:
        if o == '-h': usage(None)
        elif o == '-i': iname = a
        elif o == '-d': dname = a
        elif o == '-o': oname = a

    if iname:
        print 'Input image:', iname
    else:
        usage('No input image specified!')

    if dname:
        print 'Data file:', dname

    if oname:
        print 'Output image:', oname

    process(iname, dname, oname)


if __name__ == '__main__':
    main()

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...