LSB Image Steganography in MATLAB
A focused guide to LSB image steganography in MATLAB: how the algorithm works, format constraints, source code for embedding and extraction, and what LSB hiding does and does not protect against.
What the LSB method does
LSB image steganography hides a payload in the least significant bit of selected pixel values. In an 8-bit image, each channel value runs from 0 to 255. Changing only the final bit of that value alters the displayed brightness or color by at most one level, so the cover image and stego image usually remain visually similar. The method is not encryption: it only conceals the presence of a message inside the image.
For MATLAB work, the practical input is typically an uncompressed raster represented as a numeric array. Because the algorithm treats pixels as a sequence of values, the image format matters mainly at the point where the image is read or written. Formats that store the decoded pixel array faithfully, such as BMP or PNG, preserve the altered bits. Formats that recompress the image after hiding, such as first-generation JPEG export, can overwrite the hidden bits and make extraction fail.
A minimal LSB embedder
The core operation is to clear the target LSB in each cover value and replace it with one bit from the payload. The following function hides a uint8 message bit stream in the first available pixels of a uint8 cover image.
function stego = lsb_embed(cover, message_bits)
% cover: uint8 image array
% message_bits: logical or double column with values 0/1
message_bits = uint8(reshape(message_bits, [], 1));
pixels = cover(:);
if numel(message_bits) > numel(pixels)
error('Message is too large for the cover image.');
end
% Keep the seven most significant bits, then write one message bit.
pixels(1:numel(message_bits)) = bitand(pixels(1:numel(message_bits)), 254);
pixels(1:numel(message_bits)) = pixels(1:numel(message_bits)) + message_bits;
stego = reshape(pixels, size(cover));
stego = uint8(stego);
end
The expression bitand(value, 254) clears the least significant bit of each value, and adding the message bit writes a zero or one into that position. This assumes the payload bits are already available as zeros and ones. A text string first needs to be converted to bytes and then to bits.
Preparing text for hiding
function bits = text_to_bits(message)
bytes = uint8(message);
bit_matrix = de2bi(bytes, 8, 'left-msb');
bits = reshape(bit_matrix', [], 1);
end
Each character becomes an 8-bit row, with the most significant bit on the left. The transpose and reshape order the bytes consecutively into one bit column. The exact form of the payload bit stream is a convention shared by the embedder and extractor; it does not by itself improve security.
Extracting the payload
Extraction reads the least significant bit back from the same pixel positions. If the hiding order and payload length are known, the recovery is deterministic.
function message_bits = lsb_extract(stego, bit_count)
% stego: uint8 image array
% bit_count: number of message bits to read
pixels = stego(:);
message_bits = bitand(pixels(1:bit_count), 1);
end
Converting the recovered bits back to text requires grouping them into bytes. The following routine assumes the bit stream was produced by text_to_bits.
function text = bits_to_text(bits)
bit_matrix = reshape(bits, 8, []).';
bytes = bi2de(bit_matrix, 'left-msb');
text = native2unicode(uint8(bytes), 'UTF-8');
end
Both functions must agree on bit order, grouping and length. A payload that does not fill a whole number of bytes is a protocol error, so a practical implementation should embed the message length or use a fixed-length area before the payload.
Image-format constraints in MATLAB
MATLAB reads an image into an array with imread, and the LSB functions operate on that array. The safest workflow is to avoid any lossy step between hiding and extraction.
- Read a PNG or BMP cover image with imread.
- Hide the payload in the decoded pixel array.
- Write the stego image with imwrite using PNG or BMP output.
- Read the stego image again and extract the payload.
Saving as JPEG before extraction is not a reliable LSB carrier because JPEG quantization can change many pixel values, including their least significant bits. Likewise, resizing, cropping, color-space conversion, filtering, or saving through a service that recompresses the image can destroy the hidden data even when the image looks nearly identical.
Capacity and visual effect
With one payload bit per pixel value, a W-by-H RGB image can hold at most W * H * 3 bits before every channel is used. In practice, a shorter or explicit length field should reserve space so that the extractor knows where the message ends. The change per value is one gray level or one channel level at most, which is normally invisible on natural images but can be more noticeable in flat synthetic regions.
This capacity calculation follows directly from the one-bit-per-value scheme; it is not a measured guarantee for a particular image or transform chain.
What LSB hiding does not provide
Visual similarity is not the same as resistance to steganalysis. Simple sequential LSB embedding changes the statistical distribution of the least significant bits. An analyst or detector may still identify likely stego content even though the image appears unchanged. Hiding a payload should not be mistaken for encrypting it, and a clean look after export should not be mistaken for preservation of the embedded data.
If confidentiality of the message contents is required, the payload would need a separate encryption step before embedding. If the image may pass through an unknown conversion pipeline, traditional LSB replacement is the wrong tool because those conversions are likely to destroy the hidden bits.
A reliable MATLAB workflow
cover = imread('cover.png');
message = 'secret payload';
bits = text_to_bits(message);
stego = lsb_embed(cover, bits);
imwrite(stego, 'stego.png');
stego_read = imread('stego.png');
recovered_bits = lsb_extract(stego_read, numel(bits));
recovered = bits_to_text(recovered_bits);
When the cover is RGB, the functions operate on all three channels in column-major order: red, green and blue values for the first pixel, then the next pixel, and so on. The extractor must use the same convention. For grayscale or indexed images, the image should be converted to the intended numeric form first, because writing bits into an index array can change palette references rather than a direct color value.