Android中能否为图片添加隐形图像水印?求具体实现代码
Absolutely! You can absolutely add invisible watermarks to images on Android—there are a few reliable approaches, with LSB (Least Significant Bit) steganography being the most straightforward for mobile scenarios. It modifies the least noticeable bits of image pixels to hide data, so the watermark is completely invisible to the human eye while preserving the image’s quality. For more robustness against edits (like resizing or compression), you could also use DCT-based methods (similar to how JPEG compression works), but let’s focus on the LSB method first since it’s easier to implement and understand.
Core Concept of LSB Steganography
Every RGB pixel in a bitmap has 3 color channels (Red, Green, Blue), each represented by 8 bits. The least significant bit (the 8th bit) of each channel contributes almost nothing to the pixel’s color. By replacing these bits with the bits of our watermark text, we can embed data without altering the image’s appearance.
Step-by-Step Implementation
1. Prepare the Bitmap
First, we need to convert the target image into a mutable Bitmap so we can modify its pixel data.
2. Encode the Watermark
- Convert the watermark text into a byte array.
- First embed the length of the watermark (as 4 bytes, 32 bits) so we know how much data to extract later.
- Iterate through each bit of the watermark data and replace the LSB of each color channel in the bitmap’s pixels.
3. Generate the Watermarked Image
After modifying the pixel array, create a new bitmap from the updated pixels.
4. Extract the Watermark (Optional)
To verify the watermark works, we can reverse the process: read the LSBs from the watermarked image to reconstruct the original text.
Full Code Example (Kotlin)
Encoding Function (Embed Watermark)
fun embedWatermark(originalBitmap: Bitmap, watermarkText: String): Bitmap? { // Create a mutable copy of the original bitmap to modify pixels val mutableBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true) val pixelCount = mutableBitmap.width * mutableBitmap.height val pixels = IntArray(pixelCount) mutableBitmap.getPixels(pixels, 0, mutableBitmap.width, 0, 0, mutableBitmap.width, mutableBitmap.height) // Convert watermark to bytes, plus prepend the length (4 bytes to store size) val watermarkBytes = watermarkText.toByteArray(Charsets.UTF_8) val lengthBuffer = ByteBuffer.allocate(4).putInt(watermarkBytes.size) val fullData = lengthBuffer.array() + watermarkBytes var currentBitIndex = 0 val totalBitsNeeded = fullData.size * 8 // Check if there are enough pixels to hold the watermark if (totalBitsNeeded > pixelCount * 3) { // 3 channels per pixel return null // Not enough space in the image } for (byte in fullData) { for (bitPosition in 0..7) { val pixelIndex = currentBitIndex / 3 val channelIndex = currentBitIndex % 3 // 0=Red, 1=Green, 2=Blue val currentPixel = pixels[pixelIndex] // Extract the current channel's value var channelValue = when (channelIndex) { 0 -> (currentPixel shr 16) and 0xFF 1 -> (currentPixel shr 8) and 0xFF else -> currentPixel and 0xFF } // Set the LSB to the current bit of the watermark byte val bitToEmbed = (byte shr (7 - bitPosition)) and 0x01 channelValue = (channelValue and 0xFE) or bitToEmbed // Update the pixel with the modified channel value val updatedPixel = when (channelIndex) { 0 -> (currentPixel and 0x00FFFF) or (channelValue shl 16) 1 -> (currentPixel and 0xFF00FF) or (channelValue shl 8) else -> (currentPixel and 0xFFFF00) or channelValue } pixels[pixelIndex] = updatedPixel currentBitIndex++ } } // Apply the modified pixels back to the bitmap mutableBitmap.setPixels(pixels, 0, mutableBitmap.width, 0, 0, mutableBitmap.width, mutableBitmap.height) return mutableBitmap }
Decoding Function (Extract Watermark)
fun extractWatermark(watermarkedBitmap: Bitmap): String? { val pixelCount = watermarkedBitmap.width * watermarkedBitmap.height val pixels = IntArray(pixelCount) watermarkedBitmap.getPixels(pixels, 0, watermarkedBitmap.width, 0, 0, watermarkedBitmap.width, watermarkedBitmap.height) var currentBitIndex = 0 // First, extract the length of the watermark (4 bytes = 32 bits) val lengthBytes = ByteArray(4) for (byteIndex in lengthBytes.indices) { var byte = 0 for (bitPosition in 0..7) { if (currentBitIndex >= pixelCount * 3) { return null // Invalid watermark or corrupted image } val pixelIndex = currentBitIndex / 3 val channelIndex = currentBitIndex % 3 val currentPixel = pixels[pixelIndex] val channelValue = when (channelIndex) { 0 -> (currentPixel shr 16) and 0xFF 1 -> (currentPixel shr 8) and 0xFF else -> currentPixel and 0xFF } // Get the LSB from the channel val extractedBit = channelValue and 0x01 byte = byte or (extractedBit shl (7 - bitPosition)) currentBitIndex++ } lengthBytes[byteIndex] = byte } val watermarkLength = ByteBuffer.wrap(lengthBytes).int // Check if the length is reasonable if (watermarkLength <= 0 || watermarkLength * 8 > (pixelCount * 3) - 32) { return null } // Now extract the actual watermark bytes val watermarkBytes = ByteArray(watermarkLength) for (byteIndex in watermarkBytes.indices) { var byte = 0 for (bitPosition in 0..7) { if (currentBitIndex >= pixelCount * 3) { return null } val pixelIndex = currentBitIndex / 3 val channelIndex = currentBitIndex % 3 val currentPixel = pixels[pixelIndex] val channelValue = when (channelIndex) { 0 -> (currentPixel shr 16) and 0xFF 1 -> (currentPixel shr 8) and 0xFF else -> currentPixel and 0xFF } val extractedBit = channelValue and 0x01 byte = byte or (extractedBit shl (7 - bitPosition)) currentBitIndex++ } watermarkBytes[byteIndex] = byte } return String(watermarkBytes, Charsets.UTF_8) }
Key Notes & Limitations
- Robustness: LSB watermarks are fragile—if you save the image as a compressed format (like JPEG) or edit it (resize, filter), the watermark may be lost. For more resilience, look into DCT-based watermarking (which works in the frequency domain of the image).
- Bitmap Config: Always use
ARGB_8888for the bitmap, as it provides 8 bits per color channel. Lower configs likeRGB_565don’t have enough bits to support LSB modifications. - Permissions: If you’re loading/saving images from external storage, make sure to request the necessary permissions (for Android 13+, use
READ_MEDIA_IMAGESandWRITE_MEDIA_IMAGES).
内容的提问来源于stack exchange,提问作者Raja Ixrar Khan

