Introduction
MiniPorcupine is a small variant of the Porcupine project. It only deals with invisible image watermarking, and utilizes the LSB manipulation method. It is created using the Nokia Qt Framework, and was originally targeted for Symbian devices. Nevertheless, since it's a cross platform framework, it can be deployed, using the same code, on the following platforms:
LSB Based Image Watermarking
Applied in the spatial domain, this method of watermarking is the simplest method there is for hiding actual information inside an image. The information to be watermarked (simple text in this case) is converted to 'bits' [0-1]. Then "random" chosen pixels are ANDed with the number 254 ["00000001"] so the LSB is turned to 0. Finally, the information is inserted by adding the watermark info bits to the luminance value of the chosen pixels.
As in Porcupine, a certain equation is used for "random" pixel choice. This solves problems like knowing when to stop giving random numbers, or if the watermark data is small enough to fit the image. But in contrast with Porcupine, a different, simpler, method is used for converting the watermark data into bits, plus, in addition to the watermark info, the watermark size is embedded to the image. This way, the detector will know how much data it needs to show, preventing it from showing "rubbish", useless data scanned. So sadly, MiniPorcupine and Porcupine are not compatible.
The message is encrypted using the Blowfish encryption before embedding it to the image.
This method is very fragile. It will fail holding the watermark even with the slightest manipulation of the image (don't save it as JPEG). This is why this method can be used for image authenticity. If the watermark is there, it's the original image, owned by the key holder. It is also a symmetric method, which means detection requires the same key as insertion.
Source Code Analysis
Using the code is very simple and self-explained. There are only two functions, one for embedding and one for detecting the watermark.
Loading the image:
void MainWindow::on_LoadImageButton_clicked()
{
filename = QFileDialog::getOpenFileName(this,
tr("Pick an image :)"), QDir::currentPath(),
tr("Images (*.png *.bmp *.jpg *.gif *.tiff)"));
if (!filename.isEmpty())
{
imageloaded=true;
ui->WatermarkTextEdit->setPlainText("");
ui->WatermarkTextLabel->setText("Message to be watermarked:");
image = new QImage(filename);
if (image->isNull())
{
QMessageBox::information(this, tr("Oh damn!"),
tr("Cannot load %1.").arg(filename));
delete image;
imageloaded=false;
return;
}
ui->ImagePreviewLabel->setText("Yep, Image Loaded");
ui->InfoLabel->setText(filename);
}
}
So, if the image is loaded and a message accompanied with a key for encrypting it are filled, the insertion process will start.
if (imageloaded==false)
{
ui->WatermarkTextEdit->setPlainText("Pick an image first!");
}
else if ( ui->WatermarkTextEdit->toPlainText().size() == 0 ||
ui->BlowfishLineEdit->text().size() ==0 )
{
QMessageBox msgBox;
msgBox.setText("Watermark text\nand a key are required");
msgBox.exec();
}
else
{
QString watermarkdata = ui->WatermarkTextEdit->toPlainText();
int length = watermarkdata.size();
Blowfish encryption requires that the message to be encrypted has to be a multiple of 8 in length. We make it so.
int extrabytes=0; while (length%8!=0) {
length++;
extrabytes++;
}
for (int f=0;f<extrabytes;f++)
{
watermarkdata+=" ";
}
It's best that we work with a true color image. This way, we can manipulate the LSB of a pixel-color without the human eye understanding the difference. Specifically, the blue color of pixels is chosen because it's the most unnoticed for the human eye. Converting to true color also has a more important reason. If the image was indexed, messing with a pixel bit (even the LSB one) could have a major change in the actual color, even change it completely. Here is an exaggerated example of 2 bits indexing to 4 colors:
image888= new QImage;
*image888 = image->convertToFormat(QImage::Format_RGB888);
delete image;
numx = image888->width(); numy = image888->height();
Now the image is ready to be watermarked. Before doing that though, the message must be encrypted.
if ((7*(length*8)+8)>numx*numy) {
QMessageBox msgBox;
msgBox.setText("Watermark text is\ntoo big for this image.");
msgBox.exec();
}
else
{
QString pass = ui->BlowfishLineEdit->text();
int passlength = pass.size();
QByteArray keyarr = pass.toAscii();
char *key = keyarr.data();
QByteArray bytesarr = watermarkdata.toAscii();
char *bytes = bytesarr.data();
CBlowFish blow( reinterpret_cast<unsigned>(key), passlength);
blow.Encrypt(reinterpret_cast<unsigned>(bytes),length);
The insertion process first adds the size of the watermark. This will let the detector know exactly how much it has to read for the full hidden message. Otherwise, the entire image should be read, showing unwanted characters next to the detected text. After inserting the watermark length, the actual message is stripped into bits (as the message length-number did) and it is inserted to the image as well.
char waterlength = length;
int lengthbits [8];
if ( (waterlength & 128) == 128)
lengthbits[0] = 1;
else
lengthbits[0] = 0;
if ( (waterlength & 64) == 64) lengthbits[1] = 1;
else
lengthbits[1] = 0;
if ( (waterlength & 32) == 32) lengthbits[2] = 1;
else
lengthbits[2] = 0;
if ( (waterlength & 16) == 16) lengthbits[3] = 1;
else
lengthbits[3] = 0;
if ( (waterlength & 8) == 8) lengthbits[4] = 1;
else
lengthbits[4] = 0;
if ( (waterlength & 4) == 4) lengthbits[5] = 1;
else
lengthbits[5] = 0;
if ( (waterlength & 2) == 2) lengthbits[6] = 1;
else
lengthbits[6] = 0;
if ( (waterlength & 1) == 1) lengthbits[7] = 1;
else
lengthbits[7] = 0;
int *binary=new int[length*8];
int a=0;
for (int i=0;i<length;i++)
{
if ( (bytes[i] & 128) == 128) binary[a] = 1;
else
binary[a] = 0;
if ( (bytes[i] & 64) == 64) binary[a+1] = 1;
else
binary[a+1] = 0;
if ( (bytes[i] & 32) == 32) binary[a+2] = 1;
else
binary[a+2] = 0;
if ( (bytes[i] & 16) == 16) binary[a+3] = 1;
else
binary[a+3] = 0;
if ( (bytes[i] & 8) == 8) binary[a+4] = 1;
else
binary[a+4] = 0;
if ( (bytes[i] & 4) == 4) binary[a+5] = 1;
else
binary[a+5] = 0;
if ( (bytes[i] & 2) == 2) binary[a+6] = 1;
else
binary[a+6] = 0;
if ( (bytes[i] & 1) == 1) binary[a+7] = 1;
else
binary[a+7] = 0;
a=a+8;
}
int x,y=0;
int g=3;
QRgb value;
int cnt2=0;
for (int q=0;q<8;q++)
{
x=7*g;
if (x>numx)
{
y = x / numx;
x = x % numx;
}
value = image888->pixel(x,y);
int red = qRed(value);
int green = qGreen(value);
int blue = qBlue (value);
blue = blue & 254; blue+=lengthbits[cnt2]; value = qRgb( red, green, blue);
image888->setPixel(x, y, value);
cnt2++;
g++;
}
int cnt=0;
for (int s=0;s<length*8;s++)
{
x=7*g;
if (x>numx)
{
y = x / numx;
x = x % numx;
}
value = image888->pixel(x,y);
int red = qRed(value);
int green = qGreen(value);
int blue = qBlue (value);
blue = blue & 254; blue+=binary[cnt]; value = qRgb( red, green, blue);
image888->setPixel(x, y, value);
cnt++;
g++;
}
delete [] binary;
If you're wondering about the number 7 on the equation, the watermark is inserted every 7 pixels. Why not in every pixel? You could, but this way, you can add a second watermark (and a 3rd, and a 4th, etc.) at the same time using the pixels in between (not scripted here yet). And the insertion process is complete. The new image is saved as a bitmap format (.bmp) which saves the data as it is, without any compression whatsoever.
The detection process is the exact opposite.
void MainWindow::on_DetectWatermarkButton_clicked()
{
if (imageloaded==false)
{
ui->WatermarkTextEdit->setPlainText("Pick an image first!");
}
else if ( ui->BlowfishLineEdit->text().size() ==0 )
{
QMessageBox msgBox;
msgBox.setText("A key is required\nfor watermark decryption");
msgBox.exec();
}
else
{
if (image->isGrayscale())
{
ui->WatermarkTextLabel->setText("Wrong image:");
ui->WatermarkTextEdit->setPlainText(
"This image is not watermarked. It's grayscale!");
ui->ImagePreviewLabel->clear();
imageloaded=false;
delete image;
}
else
{
numx = image->width(); numy = image->height();
int x,y=0;
int g=3;
QRgb value;
int lengthbits [8];
for (int j=0;j<8;j++)
{
x=7*g;
if (x>numx)
{
y = x / numx;
x = x % numx;
}
value = image->pixel(x,y);
int blue = qBlue (value);
blue = blue & 1;
if (blue==1)
lengthbits[j]=1;
else
lengthbits[j]=0;
g++;
}
char waterlength;
waterlength=128*lengthbits[0]+64*lengthbits[1]+32*lengthbits[2]+
16*lengthbits[3]+8*lengthbits[4]+4*lengthbits[5]+
2*lengthbits[6]+1*lengthbits[7];
int length = waterlength;
if (length%8!=0)
{
QMessageBox msgBox;
msgBox.setText("This image is not watermarked.");
msgBox.exec();
}
else
{
int *binary=new int[length*8];
for (int i=0;i<length*8;i++)
{
x=7*g;
if (x>numx)
{
y = x / numx;
x = x % numx;
}
value = image->pixel(x,y);
int blue = qBlue (value);
blue = blue & 1;
if (blue==1)
binary[i]=1;
else
binary[i]=0;
g++;
if (7*g>numx*numy)
break;
}
int p=0;
char *message=new char[length]; for (int j=0;j<length*8;j=j+8)
{
message[p]=128*binary[j]+64*binary[j+1]+32*binary[j+2]+
16*binary[j+3]+8*binary[j+4]+4*binary[j+5]+
2*binary[j+6]+1*binary[j+7];
p++;
}
QString pass = ui->BlowfishLineEdit->text();
int passlength = pass.size();
QByteArray keyarr = pass.toAscii();
char *key = keyarr.data();
CBlowFish blow( reinterpret_cast<unsigned char*>(key), passlength);
blow.Decrypt(reinterpret_cast<unsigned char*> (message),length);
delete [] binary;
ui->WatermarkTextLabel->setText("Hidden Watermark:");
ui->WatermarkTextEdit->setPlainText(message);
ui->ImagePreviewLabel->setText("Image buffer emptied.");
imageloaded=false;
ui->InfoLabel->setText("MiniPorcupine rocks!");
delete image;
}
}
}
}
About the Blowfish Encryption Algorithm
Blowfish is a symmetric block cipher that can be used as a drop-in replacement for DES or IDEA. It takes a variable-length key, from 32 bits to 448 bits, making it ideal for both domestic and exportable use. Blowfish was designed in 1993 by Bruce Schneier as a fast, free alternative to existing encryption algorithms. Since then it has been analyzed considerably, and it is slowly gaining acceptance as a strong encryption algorithm. Blowfish is unpatented and license-free, and is available free for all uses.
System Requirements
The system requirements for Qt for Symbian development are the same as for Symbian development.
Windows PC specifications |
1. Microsoft Windows XP SP2 or newer, or Microsoft Vista (32 bit). |
2. Minimum 1.8 GHz Pentium + 1GB RAM + 2 GB free disk space + 1,024 x 768 16-bit color screen. |
3. Administrator rights on the PC. |
Other software requirements for deploying this to the Symbian platform:
1 | Java Runtime |
2 | Active Perl |
3 | Nokia Ovi Suite |
4 | S60 SDK | Each SDK includes documentation, headers, and libraries for the S60 platform as well as the tool chain for building C++ based S60 applications. An S60 device emulator that lets you test applications on the PC before deploying them to a device is also included. |
5 | Open C/C++ | This is required for all Qt-based S60 development. Install this to all S60 SDKs you plan to use Qt with. This will install the development libraries and headers. |
6 | Application TRK | Choose the one for your phone; for example, for Nokia 5800, download and install the s60 5_0 app. |
7 | Qt for Symbian SDK | Once you install it, go to the installation folder and install the file qt_demos.sis on your phone. |
8 | Qt creator IDE |
* Or you can use the Nokia Qt SDK.
History
- 20 March, 2010: Initial version