Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

MiniPorcupine

4.63/5 (8 votes)
23 Nov 2010CPOL5 min read 1   288  
Invisible, LSB based, image watermarking for Symbian.

MiniPorcupine Screenshot

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.

ANDing 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.

Full method

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:

C++
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");
        //or ..... ui->ImagePreviewLabel->setPixmap(QPixmap::fromImage(*image));
        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.

C++
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.

C++
int extrabytes=0;         // how many extra bytes are needed to be multiple of 8
while (length%8!=0) // for the Blowfish encryption
{
  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:

Index color changing example

C++
image888= new QImage;
*image888 = image->convertToFormat(QImage::Format_RGB888);
        // convert to 24-bit image,
        // 8 bits per pixel
delete image;

numx = image888->width();    //width in pixels probably
numy = image888->height();   //height

Now the image is ready to be watermarked. Before doing that though, the message must be encrypted.

C++
if ((7*(length*8)+8)>numx*numy)                          //if more, it doesn't fit
{
    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);
    // Blowfish encryption

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.

C++
/**********************************************************************
/******************************1st byte is the watermark length*******/

char waterlength = length;
int lengthbits [8];

if ( (waterlength & 128) == 128)
  lengthbits[0]  = 1;
else
  lengthbits[0]  = 0;

if ( (waterlength & 64) == 64)         //2.
  lengthbits[1] = 1;
else
  lengthbits[1] = 0;

if ( (waterlength & 32) == 32)         //3.
  lengthbits[2] = 1;
else
  lengthbits[2] = 0;

if ( (waterlength & 16) == 16)         //4.
  lengthbits[3] = 1;
else
  lengthbits[3] = 0;

if ( (waterlength & 8) == 8)           //5.
  lengthbits[4] = 1;
else
  lengthbits[4] = 0;

if ( (waterlength & 4) == 4)           //6.
  lengthbits[5] = 1;
else
  lengthbits[5] = 0;

if ( (waterlength & 2) == 2)           //7.
  lengthbits[6] = 1;
else
  lengthbits[6] = 0;

if ( (waterlength & 1) == 1)           //8. LSB
  lengthbits[7] = 1;
else
  lengthbits[7] = 0;

/*********************************************************************/

int *binary=new int[length*8]; //this will hold the binary data

int a=0;                       //counter

for (int i=0;i<length;i++)
{
  if ( (bytes[i] & 128) == 128)        //1. MSB
      binary[a]  = 1;
  else
      binary[a]  = 0;

  if ( (bytes[i] & 64) == 64)         //2.
      binary[a+1] = 1;
  else
      binary[a+1] = 0;

  if ( (bytes[i] & 32) == 32)         //3.
      binary[a+2] = 1;
  else
      binary[a+2] = 0;

  if ( (bytes[i] & 16) == 16)         //4.
      binary[a+3] = 1;
  else
      binary[a+3] = 0;

  if ( (bytes[i] & 8) == 8)           //5.
      binary[a+4] = 1;
  else
      binary[a+4] = 0;

  if ( (bytes[i] & 4) == 4)           //6.
      binary[a+5] = 1;
  else
      binary[a+5] = 0;

  if ( (bytes[i] & 2) == 2)           //7.
      binary[a+6] = 1;
  else
      binary[a+6] = 0;

  if ( (bytes[i] & 1) == 1)           //8. LSB
      binary[a+7] = 1;
  else
      binary[a+7] = 0;

  a=a+8;
}

int x,y=0;
int g=3;
QRgb value;

int cnt2=0;         // store the length of the watermark

for (int q=0;q<8;q++)
{
  x=7*g;            //number generator (every 7 pixels)

  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;   //ANDing pixels with 254->"11111110"
  blue+=lengthbits[cnt2];  //adding information
  value = qRgb( red, green, blue);

  image888->setPixel(x, y, value);

  cnt2++;
  g++;
}

int cnt=0;                  //store actual watermark

for (int s=0;s<length*8;s++)
{
  x=7*g;                    //number generator (every 7 pixels)

  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;   //ANDing pixels with 254->"11111110"
  blue+=binary[cnt];       //adding information
  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.

Watermark detection

C++
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();  //width in pixels probably
            numy = image->height();

            int x,y=0;
            int g=3;

            QRgb value;

            int lengthbits [8];

            /*************************************************************/
            /**************************Detecting watermark length*********/

            for (int j=0;j<8;j++)
            {
            x=7*g;            //number generator (every 7 pixels)

                if (x>numx)
                {
                    y = x / numx;
                    x = x % numx;
                }

            value = image->pixel(x,y);
            int blue = qBlue (value);
            blue = blue & 1;    //ANDing pixels with 1->"00000001"

            if (blue==1)
            lengthbits[j]=1;
            else
            lengthbits[j]=0;    //reading the LSB of the image pixels

            g++;
            }

            char waterlength;   //holding the byte length of the watermark

            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];    //binary data

            for (int i=0;i<length*8;i++)
            {
                x=7*g;            //number generator (every 7 pixels)

                if (x>numx)
                {
                    y = x / numx;
                    x = x % numx;
                }

                value = image->pixel(x,y);
                int blue = qBlue (value);
                blue = blue & 1;  //ANDing pixels with 1->"00000001"

                if (blue==1)
                    binary[i]=1;
                else
                    binary[i]=0;  //reading the LSB of the image pixels

                g++;
                if (7*g>numx*numy)
                    break;
            }

            int p=0;
            char *message=new char[length];    //The extracted message
            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);
            // Blowfish decryption

            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:

1Java Runtime
2Active Perl
3Nokia Ovi Suite
4S60 SDKEach 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.
5Open 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.
6Application TRKChoose the one for your phone; for example, for Nokia 5800, download and install the s60 5_0 app.
7Qt for Symbian SDKOnce you install it, go to the installation folder and install the file qt_demos.sis on your phone.
8Qt creator IDE

* Or you can use the Nokia Qt SDK.

History

  • 20 March, 2010: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)