LSB-DCT based Image steganography

JPEG steganography

If you want to save your image to jpeg, you have to follow the jpeg encoding process. Unfortunately, papers most I’ve read say don’t do it justice. The complete process is the following (wiki summary of a 182-page specifications book):

  1. RGB to YCbCr conversion (optional),
  2. subsampling of the chroma channels (optional),
  3. 8×8 block splitting,
  4. pixel value recentering,
  5. DCT,
  6. quantisation based on compression ratio/quality,
  7. order the coefficients in a zigzag pattern, and
  8. entropy encoding; most frequently involving Huffman coding and run-length encoding (RLE).

There are actually a lot more details involved, such as headers, section markers, specifics of how to store the DC and AC coefficients, etc. Then, there are aspects that the standard has only loosely defined and their implementation can vary between codecs, e.g., subsampling algorithm, quantisation tables and entropy encoding. That said, most pieces of software abide by the general JFIF standard and can be read by various software. If you want your jpeg file to do the same, be prepared to write hundreds (to about a thousand) lines of code just for an encoder. You’re better off borrowing an encoder that has already been published on the internet than writing your own. You can start by looking into libjpeg which is written in C and forms the basis of many other jpeg codecs, its C# implementation or even a Java version inspired by it.

In some pseudocode, the encoding/decoding process can be described as follows.

function saveToJpeg(pixels, fileout) {
    // pixels is a 2D or 3D array containing your raw pixel values
    // blocks is a list of 2D arrays of size 8x8 each, containing pixel values
    blocks = splitBlocks(pixels);
    // a list similar to blocks, but for the DCT coefficients
    coeffs = dct(blocks);
    saveCoefficients(coeffs, fileout);
}

function loadJpeg(filein) {
    coeffs = readCoefficients(filein);
    blocks = idct(coeffs);
    pixels = combineBlocks(blocks);
    return pixels;
}

For steganography, you’d modify it as follows.

function embedSecretToJpeg(pixels, secret, fileout) {
    blocks = splitBlocks(pixels);
    coeffs = dct(blocks);
    modified_coeffs = embedSecret(coeffs, secret);
    saveCoefficients(modified_coeffs, fileout);
}

function extractSecretFromJpeg(filein) {
    coeffs = readCoefficients(filein);
    secret = extractSecret(coeffs);
    return secret;
}

If your cover image is already in jpeg, there is no need to load it with a decoder to pixels and then pass it to an encoder to embed your message. You can do this instead.

function embedSecretToJpeg(pixels, secret, filein, fileout) {
    coeffs = readCoefficients(filein);
    modified_coeffs = embedSecret(coeffs, secret);
    saveCoefficients(modified_coeffs, fileout);
}

As far as your questions are concerned, 1, 2, 3 and 5 should be taken care of by the encoder/decoder unless you’re writing one yourself.

Question 1: Generally, you want to pad the image with the necessary number of rows/columns so that both the width and height are divisible by 8. Internally, the encoder will keep track of the padded rows/columns, so that the decoder will discard them after reconstruction. The choice of pixel value for these dummy rows/columns is up to you, but you’re advised against using a constant value because it will result to ringing artifacts which has to do with the fact that the Fourier transform of a square wave being the sinc function.

Question 2: While you’ll modify only a few blocks, the encoding process requires you to transform them all so they can be stored to a file.

Question 3: You have to quantise the float DCT coefficients as that’s what’s stored losslessly to a file. You can modify them to your heart’s content after the quantisation step.

Question 4: Nobody prevents you from modifying any coefficient, but you have to remember each coefficient affects all 64 pixels in a block. The DC coefficient and the low frequency AC ones introduce the biggest distortions, so you might want to stay away from them. More specifically, because of the way the DC coefficients are stored, modifying one would propage the distortion to all following blocks.

Since most high frequency coefficients are 0, they are efficiently compressed with RLE. Modifying a 0 coefficient may flip it to a 1 (if you’re doing basic LSB substitution), which disrupts this efficient compression.

Lastly, some algorithms store their secret in any non-zero coefficients and will skip any 0s. However, if you attempted to modify a 1, it might flip to a 0 and in the extraction process you’d blindly skip reading it. Therefore, such algorithms don’t go near any coefficients with the value of 1 or 0.

Question 5: In decoding you just multiply the coefficient with the respective quantisation table value. For example, the DC coefficient is 309.443 and quantisation gives you round(309.443 / 16) = 19. The rounding off bit is the lossy part here, which doesn’t allow you to reconstruct 309.433. So the reverse is simply 19 * 16 = 304.

Other uses of DCT in steganography

Frequency transforms, such as DCT and DWT can be used in steganography to embed the secret in the frequency domain but not necessarily store the stego image to jpeg. This process is pixels -> DCT -> coefficients -> modify coefficients -> IDCT -> pixels, which is what you send to the receiver. As such, the choice of format matters here. If you decide to save your pixels to jpeg, your secret in the DCT coefficients may be disturbed by another layer of quantisation from the jpeg encoding.

Leave a Comment