Visual Cryptography and Bit-Plane Complexity Segmentation

Bit-Plane Complexity Segmentation lets you embed large amounts of data in images.


September 05, 2007
URL:http://www.drdobbs.com/security/visual-cryptography-and-bit-plane-comple/201804177

Daniel is a senior programmer for the Aareal Bank in Germany, and a doctoral student in applied cryptography at the Military Technical Academy in Romania. He can be contacted at [email protected].


Steganography is defined as "the art and science of writing hidden messages such that no one apart from the intended recipient knows of the existence of the message" (en.wikipedia.org/wiki/Steganography). In implementing steganography applications, you can embed information in both the image domain itself and in one of the image's transformed domains—frequency, cosine, or wavelet. In this article, I describe a steganography technique based on Bit-Plane Complexity Segmentation (BPCS) and visual cryptography. To test its viability, I implemented the technique in Python.

Bit-Plane Complexity Segmentation, a lossy image-compression technique first proposed at the Kyushu Institute of Technology, lets you embed large amounts of data in images. But to do so, you need visual cryptography to decompose a message in two shares with a highly random character.

Generally, visual cryptography is considered a visual form of secret sharing; see Doug Stinson's article "Visual Cryptography and Threshold Schemes" (www.ddj.com/184410530) and my article "Extended Visual Cryptography Schemes" (www.ddj.com/dept/architect/184406280). In its simplest form, a (2,2) visual cryptography scheme "splits" the original image into two "shadow images" called "shares." Every pixel in the original image is expanded to a 2x2 pixel matrix with a different version in any of the two shares. Any share contains uniformly distributed random black-and-white pixels. By analyzing only a single share, you can't obtain information about the original image, no matter how much computing power or analysis method is used. The whole point of visual cryptography is that in the decryption process, the original image has to be visually reconstructed. Each share is printed on a separate transparency and passed to a participant at the scheme. When the two participants come together, the secret can simply (and theoretically instantaneously) be reconstructed by stacking the two transparencies.

To build the shares, the visual cryptography scheme my application uses only considers diagonal matrices—see Versions 1 and 2 in Table 1. Figure 1 is an example of the visual cryptography scheme implemented by the application.

[Click image to view at full size]

Figure 1: (a) Secret message; (b) recomposed message; (c) first share; (d) second share.

[Click image to view at full size]

Table 1: A (2,2) visual cryptography scheme.

Bit-Plane Complexity Segmentation

Bit-Plane Complexity Segmentation (BPCS) relies on the fact that the human visual system is sensitive to patterns, but unable to identify random white noise. Therefore, if you want to implement a BCPS algorithm, you divide the image into regions and calculate the complexity of these regions. Any region with complexity above a certain threshold can be replaced with embedded data. This technique works on 24-bit true-color or 8-bit grayscale images. It doesn't work on paletted images; small changes in a pixel value might have drastic effects on the color of the pixel in the paletted image.

Kawaguchy and Eason (citeseer.ist.psu.edu/kawaguchi98principle.html) suggest that the bit-planes of natural images display monotonic increasing complexity from the Most Significant Bit-plane (MSB) to the Least Significant Bit-plane (LSB). Most of the LSBs just look like random noise (Figure 2).

[Click image to view at full size]

Figure 2: (a) Venice, original image; (b) MSB; (c) 4th bit-plane; (d) LSB .

Following the separation of the image in bit-planes, every bit-plane is decomposed in 8x8 square regions and the complexity of the regions is calculated. There is no general definition for the complexity of a binary image. Nevertheless, there's a simple way to calculate the complexity of a region in the bit-plane—just count the number of color changes in every row and column of the region. To define a coherent scale of complexities, you normalize this figure such that one plain color has a complexity of 0 and the checkerboard pattern (the most complex possible region) has a complexity of 1.

Any region in any bit-plane with a complexity above a chosen threshold is considered random noise and replaced by 8 bytes of data.

Modified BPCS Steganography

It is still possible that an embedded block will not have a complexity above the threshold value. In this case, the conjugate of the block must be taken. The conjugate of a binary image is obtained by XORing the image with the checkerboard pattern. Obviously, the original data can be remade by XORing the new image with the checkerboard pattern again. If necessary, the conjugate is calculated,and you need a "flag pixel" to mark the region as "conjugated."

In the decryption process (as in a puzzle), you have to detect any piece of embedded data and put it back in place. To properly identify every square, I mark each with a sequence number. The sequence number is an integer number binary representation.

Because the proposed embedding scheme is "blind" (that is, no prior knowledge of the image or other information is needed to extract the embedded message), the additional information consisting of the conjugate flag and sequence number has to be written back into the image, together with the embedding data.

To win space for the supplementary information that mustaccompany every 8x8 square, you can border any square with a row and a column; the information could be organized as in Figure 3.

[Click image to view at full size]

Figure 3: Organizing the information in an extended 959 pixel matrix.

The algorithm doesn't work if you embed more squares than can be marked with a sequence number. That is, the total number of embedded squares the model permits is of 216=65,536 squares. From this perspective, the maximum amount of information that can be embedded is 524,228 bytes, or 512 KB of data.

My steganography scheme writes data from the MSB to the LSB in a spiral from the middle of the image. This is because the lossy compression algorithms are likely to discard the LSB and the most important details are usually in the middle of the image.

A 24-bit true-color image consists of red, green, and blue (RGB). The human visual system seems sensitive to green variations and less sensitive to blue ones. Therefore, I adopt the following order in embedding data: I start with the MSB of every constituent color, then move to the next plane until all the embedding information is written. The order of the color components is blue, red, green.

Though the embedding scheme is known for all images, it does not have to be communicated to the receiving party. The receiver only has to divide the image into color components, then bit-planes and regions. Afterwards, all regions having a complexity above the predefined threshold are checked for data.

If a region has errors and is not identified by its complexity (or if the sequence number was altered), the receiver misses the block. But it is possible to guess which region should contain data (see Listing Seven, available at http://www.ddj.com/code/).

The Model

A possible scenario for the proposed model involves a "dealer" and "participants" to the scheme. The dealer chooses a secret message represented as a binary black-and-white image and applies a (2,2) visual-cryptography scheme on the secret message, obtaining the two corresponding shares. Every share is individually embedded into an 8-bit grayscale image (traditionally called a "vessel") using the modified BPCS scheme. Finally, the dealer electronically sends the images with embedded data to the participants. Participants process the received image (also based on the modified BPCS scheme), obtain the embedded share as a binary image, and print the binary image on a transparency. As soon as the participants come together, they can visually reconstruct the secret message by carefully superimposing the two transparencies.

This model follows the block diagram in Figure 4. Though separated in practice, for simplicity, Figure 4 illustrates the embed/extract process together: only the steganographic method for an 8-bit grayscale image is treated. Figure 5 presents a general view considering the modality the data is embedded into the vessels.

[Click image to view at full size]

Figure 4: The applied model.

[Click image to view at full size]

Figure 5: (a) Venice, original image; (b) roses, original image; (c) Venice, with embedded information; (d) roses, with embedded information; (e) difference between (a) and (c); (f) difference between (b) and (d).

The Implementation

To illustrate this model, I wrote an application using Python, the Python Imaging Library (PIL) (www.pythonware.com/products/pil/), and the numarray package for Python (www.stsci.edu/resources/software_hardware/numarray). PIL adds image-processing capabilities to Python and processes a range of image formats. numarray lets Python efficiently manipulate large numeric arrays, similar to Matlab, IDL, or Octave.

There is a common convention in processing binary (black-and-white) images: If any black pixel in the image is denoted by 1 and any white pixel is 0, you can "transform" any binary black-and-white image into a binary matrix. This is the approach used in my application.

In the embedding process, you select 858 squares from a larger matrix (a share in the model) and write back 9x9 squares into some other matrix (normally a bit-plane); see Listings One and Two, respectively. The algorithm to calculate the complexity of a region in a bit-plane can be implemented as in Listing Three.

def square(matrix, n, d):
  # returns a square slice of dim d from a matrix
  # n - square number
  maxX = len(matrix[0])
  maxY = len(matrix[:,0])
  xsquares = maxX/d
  ysquares = maxY/d
  dy, dx = divmod(n, xsquares)
  xslc = dx * d
  yslc = dy * d
  return matrix[xslc:xslc+d, yslc:yslc+d]
Listing One

def writesquare(matrix, square, n):
  # writes a square of dim d into a bigger matrix
  # n - square number
  # returns the matrix with the overwritten sqaure
  maxX = len(matrix[0])
  maxY = len(matrix[:,0])
  sqX = len(square[0])
  sqY = len(square[:,0])
  xsquares = maxX/sqX
  ysquares = maxY/sqY
  dy, dx = divmod(n, xsquares)
  xslc = dx * sqX
  yslc = dy * sqY
  matrix[xslc:xslc+sqX, yslc:yslc+sqY] = square
  return matrix
Listing Two

def complexity(matrix):
  maxX = len(matrix[0])
  maxY = len(matrix[:,0])
  norm = maxX*maxY
  cmp = 0.
  first = matrix[0][0]
  for x in range(maxX):
    for y in range(maxY):
        if first != matrix[x][y]:
            cmp += 1
            first = matrix[x][y]
  if cmp != 0:
      cmp /= norm
  
  return cmp
Listing Three

To ensure the "blind" character of the embedding scheme, you have to provide additional information (like sequence number and the flag for the conjugated areas) to any 8x8 square read from a share. The additional information is written back into the vessel, together with the 8x8 square of embedding data. The required space is obtained by bordering the square with one row and one column. Though generally defined, the functions in Listings Four(a) and (b) can perform the bordering as well.

(a)
def insert_line(where, line, matrix):
        maxX = len(matrix[0])
        maxY = len(matrix[:,0])+1
        if len(line) != maxX:
            print "[ERR] Unable to append line."
            sys.exit(1)
        getback = zeros((maxY, maxX))
        for y in range(maxY):
            if y == where:
                getback[y] = line
            else:
                if y < where:
                    getback[y] = matrix[y]
                else:
                    getback[y] = matrix[y-1]
        return getback

(b)
def insert_column(where, column, matrix):
        tmatrix = transpose(matrix)
        tcolumn = transpose(column)
        tmpback = insert_line(where, tcolumn[0], tmatrix)
        return transpose(tmpback)
Listing Four

The program also provides other tools for tasks such as generating a random binary matrix or checkerboard pattern, initializing log files, defining some standard directories, and the like. All these are implemented in a utils package.

The application has compression, embedding, and extraction modules. All components work on 8-bit gray-level or 24-bit true-color images. I focus on the 8-bit images because the processing of the true-color images only involves this extension: PIL lets you switch between different pixel representations of an image using the convert function. In terms of converted images, there are only two accepted formats: L (8-bit grayscale) and/or RGB (24-bit true color). This lets you convert input images in RGB format like this:


try:
  image = __original_image.convert("RGB")
except IOError:
  print "Cannot convert image: "      + image_name

After a successful conversion, the image object is in RGB format. For such objects, PIL offers a method that returns a tuple containing the individual image components ("bands" in the PIL documentation). This means you can say:


red, green, blue = image.split()

The red, green, blue objects this time are 8-bit grayscale images. All three modules of the application are implemented in a separate class called image.

Compression, which is based on BPCS, only determines image regions with a complexity above a determined threshold. These regions are overwritten with white color. The embedding module uses the modified BPCS scheme. The module performs the bordering of the 8x8 initial square, correspondingly inserting the sequence numbers and the "conjugated flag" if needed (see Listing Six, available at www.ddj.com/code/).

The function starts with a given sequence number and returns the last sequence number it used. This is because of the embedding scenario itself: During the process, you have to move from one bit-plane to another, and, in the case of true-color images, from one color component to another. So the function has to "know" what the current sequence number is and return the sequence number it finished with for the next iteration.

The extraction module checks the complexity of any region in the image and searches for embedded data. The application also tries to determine if the complete chain of sequence numbers was extracted; if not, it reports the missing squares.

As in Table 1, the only 2x2 matrices that should exist in a share are the diagonal ones. The extraction module also uses an error-detecting mechanism and informs you if some region of the image was altered—a "wrong" 2x2 matrix was found. As the embedding function, the extraction function starts with an initial sequence number and returns the next sequence number when finished (see Listing Seven, available at www.ddj.com/code/).

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.