Willze Fortner
Willze Fortner

Reputation: 141

Canvas image zooming using Nearest Neighbor Algorithm

I'm using nearest neighbor algorithm to zoom the image on canvas. But, when I move the scaling bar higher, the image have white line that create a square array

Original Image

enter image description here

After I move the scale bar

enter image description here

The zoom is work but the problem is only the white lines. For the source code I will provide in bottom

1.html

 <!DOCTYPE HTML>
 <html>
 <head>
 <title>Prototype PC</title>
 </head>
 <body>
 <canvas id='canvas1'></canvas>
 <hr>
 <button id='read'>READ IMAGE</button>
 <hr>
      Scale <input type='range' value='1' min='1' max='5' step='0.25' id='scale'>
      <br><button id='default2'>Default Scalling</button>
 <hr/>
 </body>
 <style>
 body{
      background : rgba(255,255,255,1);
 }
 </style>
 <script src='imagine.js'></script>
 <script>

 var canvas = document.getElementById('canvas1')
 var obj = new pc(canvas)
 obj.image2canvas("565043_553561101348179_1714194038_a.jpg")

 var tes = new Array()
 document.getElementById('read').addEventListener('click',function(){
    tes = obj.image2read()
 })

 document.getElementById('scale').addEventListener('change',function(){
    var scaleval = this.value
    var xpos = 0
    var ypos = 0
    var xnow = 0
    var ynow = 0
    var objW = obj.width
    var objH = obj.height

    tesbackup = new Array()
    for(var c=0; c<tes.length; c++){
        temp = new Array()
        for(var d=0; d<4; d++){
            temp.push(255)
        }
        tesbackup.push(temp)
    }
    //end of copy
    for(var i=0; i<tes.length; i++){
        xpos = obj.i2x(i)
        ypos = obj.i2y(i)
        xnow = Math.round(xpos) * scaleval)
        ynow = Math.round(ypos) * scaleval)
        if (xnow < objW && ynow < objH) {
            for (var j=0; j<scaleval; j++) {
                for (var k=0; k<scaleval; k++) {
                    var idxnow = obj.xy2i(xnow,ynow)
                    tesbackup[idxnow][0] = tes[i][0]
                    tesbackup[idxnow][1] = tes[i][1]
                    tesbackup[idxnow][2] = tes[i][2]
                }
              }
           }
        }
        obj.array2canvas(tesbackup)
    })
</script>

and, for imagine.js

    function info(text){
        console.info(text)
    }

    function pc(canvas){
        this.canvas = canvas
        this.context = this.canvas.getContext('2d')
        this.width = 0
        this.height = 0
        this.imgsrc = ""
        this.image2read = function(){
            this.originalLakeImageData = this.context.getImageData(0,0, this.width, this.height)
            this.resultArr = new Array()
            this.tempArr = new Array()
            this.tempCount = 0
            for(var i=0; i<this.originalLakeImageData.data.length; i++){
                this.tempCount++
                this.tempArr.push(this.originalLakeImageData.data[i])
                if(this.tempCount == 4){
                    this.resultArr.push(this.tempArr)
                    this.tempArr = []
                    this.tempCount = 0
                }
            }
            info('image2read Success ('+this.imgsrc+') : '+this.width+'x'+this.height)
            return this.resultArr
        }

        this.image2canvas = function(imgsrc){
            var imageObj = new Image()
            var parent = this
            imageObj.onload = function() {
                parent.canvas.width = imageObj.width
                parent.canvas.height = imageObj.height
                parent.context.drawImage(imageObj, 0, 0)
                parent.width = imageObj.width
                parent.height = imageObj.height
                info('image2canvas Success ('+imgsrc+')')
            }   
            imageObj.src = imgsrc
            this.imgsrc = imgsrc
        }

        this.array2canvas = function(arr){
            this.imageData = this.context.getImageData(0,0, this.width, this.height)
            if(this.imageData.data.length != arr.length*4) {
                return false
            }
            for(var i = 0; i < arr.length; i++){
                this.imageData.data[(i*4)] = arr[i][0]
                this.imageData.data[(i*4)+1] = arr[i][1]
                this.imageData.data[(i*4)+2] = arr[i][2]
                this.imageData.data[(i*4)+3] = arr[i][3]
            }
            this.context.clearRect(0, 0, this.width, this.height)
            this.context.putImageData(this.imageData, 0, 0)
            info('Array2Canvas Success ('+this.imgsrc+')')
        }

        this.i2x = function(i){
            return (i % this.width)
        }

        this.i2y = function(i){
            return ((i - (i % this.width))/ this.width)
        }

        this.xy2i = function(x,y){
            return (y * this.width) + (x)
        }

    }

Thanks in advance for a solution of this problem

Upvotes: 1

Views: 932

Answers (1)

Blindman67
Blindman67

Reputation: 54089

Rounding out pixels

Nearest pixel will result in some zoomed pixels being larger than otheres

It is a problem with the value of scaleval. It has a step of 0.25 and when you calculate each zoomed pixels address you use (and I am guessing as your code has syntax errors) Math.round(xpos * scaleval) but then you draw the pixel using only the fractional size eg 2.75 not the integer size eg 3.0

The size of each pixel is var xSize = Math.round((xpos + 1) * scaleval)-Math.round(xpos * scaleval) same for y. That way when the pixel zoom is not an integer value every so many zoomed pixels will be one pixel wider and higher.

The following is a fix of your code but as you had a number of syntax errors and bugs I have had to guess some of your intentions.

xpos = obj.i2x(i)
ypos = obj.i2y(i)
xnow = Math.round(xpos * scaleval)
ynow = Math.round(ypos * scaleval)
// pixel width and height
var pw = Math.round((xpos + 1) * scaleval) - xnow;
var ph = Math.round((ypos + 1) * scaleval) - ynow;
if (xnow < objW && ynow < objH) {
    for (var y = 0; y < ph; y++) {
        for (var x  =0; x < pw; x++) {
            var idxnow = obj.xy2i(xnow + x, ynow + y)
            tesbackup[idxnow][0] = tes[i][0]
            tesbackup[idxnow][1] = tes[i][1]
            tesbackup[idxnow][2] = tes[i][2]
        }
      }
   }
}

But you are not really doing a nearest neighbor algorithm. For that you iterate each of the destination pixels finding the nearest pixel and using its colour. That allows you to easily apply a transform to the zoom but still get every pixel and not skip pixels due to rounding errors.

Nearest neighbor

Example of using nearest neighbor lookup for a scale rotated and translated image

var scaleFac = 2.3; // scale 1> zoom in 
var panX = 10;  // scaled image pan
var panY = 10; 
var ang = 1;
var w = ctx.canvas.width;  // source image
var h = ctx.canvas.height;
var wd = ctx1.canvas.width;  // destination image
var hd = ctx1.canvas.height;
// use 32bit ints as we are not interested in the channels
var src = ctx.getImageData(0, 0, w, h);
var data = new Uint32Array(src.data.buffer);// source
var dest = ctx1.createImageData(wd, hd);
var zoomData = new Uint32Array(dest.data.buffer);// destination
var xdx = Math.cos(ang) * scaleFac;  // xAxis vector x
var xdy = Math.sin(ang) * scaleFac;  // xAxis vector y
var ind = 0;
var xx,yy;
for(var y = 0; y < hd; y ++){
    for(var x = 0; x < wd; x ++){
        // transform point
        xx = (x * xdx - y * xdy + panX);
        yy = (x * xdy + y * xdx + panY);
        // is the lookup pixel in bounds
        if(xx >= 0 && xx < w && yy >= 0 && yy < h){                
             // use the nearest pixel to set the new pixel
             zoomData[ind++] = data[(xx | 0) + (yy | 0) * w]; // set the pixel
        }else{
             zoomData[ind++] = 0; // pixels outside bound are transparent
        }
    }
}

ctx1.putImageData(dest, 0, 0); // put the pixels onto the destination canvas


    
    

Upvotes: 1

Related Questions