Search code examples
javascriptbinaryhexdecimalsteganography

By what calculation are the rgb values changing?


Here I have some code which creates an image that hides another image in it. I print out some rgb values to see the change (after implementing function chopToHide and function shift, but I don't understand by what calculation the numbers have changed by). This bit of code is part of a larger program to hide one image inside another (steganography).
This is the environment where I 'm working in: http://www.dukelearntoprogram.com/course1/example/index.php

var start = new SimpleImage("astrachan.jpg");
var hide = new SimpleImage("duvall.jpg");

print (start);
print(hide);
print ("width and height of astrachan picture");
print (start.getHeight(), start.getWidth());
print ("width and height of duvall picture")
print (hide.getHeight(), hide.getWidth());


var cropWidth = start.getWidth();
if (hide.getWidth() < cropWidth) {
    cropWidth = hide.getWidth();
 }
var cropHeight = start.getHeight();
if (hide.getHeight() < cropHeight) {
    cropHeight = hide.getHeight();
}
start = crop(start,cropWidth, cropHeight);
hide = crop(hide,cropWidth, cropHeight);
print(start);
print(hide);

function crop(image, width, height){
     var n = new SimpleImage(width,height);
     for(var p of image.values()){
       var x = p.getX();
       var y = p.getY();
       if (x < width && y < height){
     var np = n.getPixel(x,y);
     np.setRed(p.getRed());
     np.setBlue(p.getBlue());
     np.setGreen(p.getGreen()); 
}
     }
     return n;
}

//print (start);
//print(hide);
print ("cropped width and height of astrachan picture");
print (start.getHeight(), start.getWidth());
print ("cropped width and height of duvall picture")
print (hide.getHeight(), hide.getWidth());



function chopToHide(image){ 
    for(var px of image.values()){ 
        px.setRed(pixchange(px.getRed())); 
        px.setGreen(pixchange(px.getGreen())); 
        px.setBlue(pixchange(px.getBlue())); 
    } 
    return image; 
}

function pixchange(pixval){ 
    var x = Math.floor(pixval/16) * 16; 
    return x; 
} 

 function shift(oldImage){
    var newImage = new SimpleImage(oldImage.getWidth(), oldImage.getHeight());
    for(var oldPixel of oldImage.values()){
        var x = oldPixel.getX();
        var y = oldPixel.getY();
        var newPixel = newImage.getPixel(x, y);
        newPixel.setRed( Math.floor(oldPixel.getRed()/16) );
        newPixel.setGreen( Math.floor(oldPixel.getGreen()/16) );
        newPixel.setBlue( Math.floor(oldPixel.getBlue()/16) );
    }
    return newImage;
}   









print("before applying the chopToHide function to the image start:");
     for(i = 1; i <= 100; i+=20 ){
    var pixel = start.getPixel(i, i+5);
    print("pixel at (" + pixel.getX() + "," + pixel.getY() + ")-> R= " +    
pixel.getRed() + " : G= " + pixel.getGreen() + " : B= " + pixel.getBlue() );
}
//print (start);
start = chopToHide(start);

 print("After applying the chopToHide function to the image start:");
    for(i = 1; i <= 100; i+=20 ){
    var pixel = start.getPixel(i, i+5);
    print("pixel at (" + pixel.getX() + "," + pixel.getY() + ")-> R= " +   
pixel.getRed() + " : G= " + pixel.getGreen() + " : B= " + pixel.getBlue() );
} 
//print (start);
//print (hide);

print("Before applying the shift function to the image hide:");
for(i = 1; i <= 100; i+=20 ){
    var pixel = hide.getPixel(i, i+5);
    print("pixel at (" + pixel.getX() + "," + pixel.getY() + ")-> R= " +    
pixel.getRed() + " : G= " + pixel.getGreen() + " : B= " + pixel.getBlue() );
}
hide = shift(hide);

print("After applying the shift function to the image hide:");
    for(i = 1; i <= 100; i+=20 ){
        var pixel = hide.getPixel(i, i+5);
        print("pixel at (" + pixel.getX() + "," + pixel.getY() + ")-> R= " 
+ pixel.getRed() + " : G= " + pixel.getGreen() + " : B= " +   
pixel.getBlue() );
}

Solution

  • The idea this code is working towards is to reduce the colour range from 0..255 (8 bits, a "byte") per colour channel to 0..15 (4 bits, a "nybble") per colour channel on both pictures, then combine the two nybbles into one byte. The picture whose colours are in the high nybble will give the perceptible colour change (in increments of 16); the picture hiding in the low nybble will shade the colour of the main picture's pixels imperceptibly.

    The formula should be:

    steganoImageColour = (overtImageColour & 240) | (covertImageColour >> 4)
    

    or equivalently (but not as fast):

    steganoImageColour = (Math.floor(overtImageColour / 16) * 16) +
                         Math.floor(covertImageColour / 16)
    

    To extract the covert image, you just take the low nybble and promote it to high:

    reconstructedCovertImageColour = (steganoImageColour & 15) << 4
    

    or equivalently,

    reconstructedCovertImageColour = (steganoImageColour % 16) * 16
    

    For example, if you have a green-yellow pixel in the overt image (173 255 47) and a light sky blue pixel in the covert image (135 206 250), the resulting pixel is (168 252 47), which is still very close to the original green-yellow, but one can reconstruct (128 192 240) from it, which is very close to the original light sky blue.

    <div style="background-color: rgb(173,255,47)">overt (green-yellow)</div>
    <div style="background-color: rgb(135,206,250)">covert (light sky blue)</div>
    <div style="background-color: rgb(168,252,47)">stegano (close enough to green-yellow)</div>
    <div style="background-color: rgb(128,192,240)">reconstructed covert (close enough to light sky blue)</div>