Find The Length of a Coastline - Programming - Part 2

Sat, 12/16/2017 - 10:03

About a week ago I wrote an article where I hypothesized that I could find the length of a coast-line using an algorithm based on a map or satellite image.

This is part 2 of the programming part of that series.

color reduce failThis section of the code both worked perfectly...and, not quite as expected. The program reduces the image down to the two most common colors clearly showing the difference between land and sea however, it looks as though despite this image being a Windows bitmap screenshot of the map, which is a vector image, there are some compression distortions. This makes sense I suppose. It's likely that either Google Chrome or Google maps is actually rendering the image as a JPEG. The source code suggest that the map is drawn to a canvas tag. I am not sure if the canvas tag is pure vectors or if it draws as a JPEG or other bitmap image.

Anyways, the end result is that post reduction there is now a land-bridge between Ireland and Great Britain...Something I don't think either side would be very happy about.

I originally though that perhaps the previous step in my code was the culprit because it modifies the image and makes some guesses to remove the labels. But, when I run the code without the text removal part the issue is still there.

So, I think I have two possible solutions:

  1. Reduce the colors setting the major color as one being "close" to the most common color of blue.
  2. Reduce the image in two passes. First down to 16 colors, then a second pass down to 2 colors.

NOTE: I goofed...it turns out that my code wasn't working because I swapped the x,y keys when repopulating the pixel array after drawing the secondary color.

It was a simple error. In order to loop the array I start with the y axis then move to the x axis. This is counter to how I might normally write things out.

 p[x][y][0] = maxb.c[0];
 p[x][y][1] = maxb.c[1];
 p[x][y][2] = maxb.c[2];

Should have been:

 p[y][x][0] = maxb.c[0];
 p[y][x][1] = maxb.c[1];
 p[y][x][2] = maxb.c[2];

The loop was writing to the opposite side of the row. Then when the loop rolled passed the newly written section it said, "this is not blue, so it should be grey". Effectively mirroring the map on the opposite side.

It's obvious looking at the result that I nned to work harder on step one to effectively remove the labels.

The current source for reducing colors is posted below and the resulting image before and after below that.

And here is a live demo.

 

 

 const   W = 807,
                H = 807
        window.onload = function() {

            'use strict';
            
            var i = $w.add_object_single(
                1,
                Map,{
                    src:'1pixel2miles.bmp',
                    w:W,
                    h:H
                },
                document.getElementById('target'),
                W,H
            );
        }
        /**
         * Object
         * 
         * @param {Object}
         * @returns {Void}
         * */
        var Map = function(o){
            // SET WATER AS A CONSTANT
            this.water = [202,232,255,255];
            
            // set the width / height of the image
            this.w = o.w;
            this.h = o.h;
            // get the reference to the canvas
            this.i = o.i;
            // create an image object
            var img = new Image();
            img.src = o.src;
            // 
            var $this = this;
            // when the image has been loaded
            img.onload = function() {
                // get the canvas context
                var ctx = $w.canvas.get(o.i,'ctx');
                // draw the image
                ctx.drawImage(img, 0, 0);
                // get the image pixel colors as a two-dimensional array
                var p = $this.getData(ctx);
                // loop the pixels and act on them
                p = $this.loopPixels(p,ctx);
                p = $this.twoColors(p,ctx);
            }
            
        }
        /**
         * get the image data from the canvas
         * @param {Object}
         * @returns {Array}
         * */
        Map.prototype.getData = function(ctx){
            // Create an ImageData object.
            var imgd = ctx.getImageData(0,0,this.w,this.h);
            var pix = imgd.data;
            var x = 0, y = 0;
            var pixels = [[]];
            // Loop over each pixel and set a transparent red.
            for (var i = 0; n = pix.length, i < n; i += 4) {
                pixels[y][x] = [pix[i],pix[i+1],pix[i+2],pix[i+3]];
                x++;
                if (x == this.w) {
                    x = 0;
                    y++;
                    pixels[y] = [];
                }
            }   
            return pixels;
        }
        /**
         * loop all the pixels and run action as necessary
         * @param {Array}
         * @returns {Array}
         * */
        Map.prototype.loopPixels = function(p,ctx) {
            for (var y = 0; y<this.h;y++) {
                for (var x = 0; x<this.w;x++) {
                    // if color close to black
                    // change it to the nearest color
                    if(p[y][x][0] < 144 && p[y][x][1] < 144 && p[y][x][2] < 144) {
                        p[y][x] = this.nearestPixel(p,y,x,147,162,173);
                        ctx.fillStyle=$w.color.rgbToHex(p[y][x][0],p[y][x][1],p[y][x][2]);
                        ctx.fillRect(x,y,1,1);
                    }
                    // lets do a second pass
                    if(p[y][x][0] < 163 && p[y][x][1] < 163 && p[y][x][2] < 163) {
                        p[y][x] = this.nearestPixel(p,y,x,147,162,173,250);
                        ctx.fillStyle=$w.color.rgbToHex(p[y][x][0],p[y][x][1],p[y][x][2]);
                        ctx.fillRect(x,y,1,1);
                    }
                }   
            }
            return p;
        }
        /**
         * reduce the image to two colors
         * @param {Array}
         * @returns {Array}
         * */
        Map.prototype.twoColors = function(p,ctx) {
            // @param {Object}
            var maxa = {
                i:0,
                c:null
            },
            // @param {Object}
            maxb = {
                i:0,
                c:null
            },
            // @param {Object}
            c = {}
            // loop all the pixels
            for (var y = 0; y<this.h;y++) {
                for (var x = 0; x<this.w;x++) {
                    // count all the colors on the stage
                    if (undefined === c[p[y][x][0]+p[y][x][1]+p[y][x][2]]) {
                        c[p[y][x][0]+p[y][x][1]+p[y][x][2]] = {
                            i:1,
                            c:p[y][x]
                        }
                    }else{
                        c[p[y][x][0]+p[y][x][1]+p[y][x][2]].i++;
                    }
                }   
            }
            // loop all the found colors
            for(var i in c) {
                if (c.hasOwnProperty(i)) {
                    // if this color was found more often than max then replace it
                    if(c[i].i > maxa.i) {
                        maxa.i = c[i].i
                        maxa.c = c[i].c;
                    }
                    // if this color was found more often than max - 1 replace it
                    if(c[i].i > maxb.i && c[i].i < maxa.i) {
                        maxb.i = c[i].i
                        maxb.c = c[i].c;
                    } 
                }
            }
            // looop the pixels again
            for (var y = 0; y<this.h;y++) {
                for (var x = 0; x<this.w;x++) {
                    // if the current color is NOT the MAX color
                    if (p[y][x][0] != maxa.c[0] && p[y][x][1] != maxa.c[1] && p[y][x][2] != maxa.c[2]) {
                        // then set it to the next mst common color
                        ctx.fillStyle=$w.color.rgbToHex(maxb.c[0],maxb.c[1],maxb.c[2]);
                        ctx.fillRect(x,y,1,1);
                        p[y][x][0] = maxb.c[0];
                        p[y][x][1] = maxb.c[1];
                        p[y][x][2] = maxb.c[2];
                    }
                }   
            }
            
            return p;
        }
        /**
         * get the most common color within a range of nine pixels
         * @param {Array}
         * @param {Number}
         * @param {Number}
         * @param {Number} 0-255
         * @param {Number} 0-255
         * @param {Number} 0-255
         * @returns {Array}
         * */
        Map.prototype.nearestPixel = function(p,y,x,r,g,b){
                // @param {Object}
            var c = {},
                // @param {Number} how many surrounding pixels to look
                l = 5;
            /**
             * look at the adjoining pixels to get the most common pixel color
             *
             * #####
             * ##X##
             * #####
             *
             * */
            for(var yy=y-l;yy<y+l;yy++) {
                for(var xx=x-l;xx<x+l;xx++){
                    // if this is not black
                    if(p[yy][xx][0] > r && p[yy][xx][1] > g && p[yy][xx][2] > b) {
                        var q = p[yy][xx][0]+p[yy][xx][1]+p[yy][xx][2];
                        // increment a count of the color of this pixel
                        if (typeof c[q] === 'undefined') {
                            c[q] = {
                                c:p[yy][xx],
                                i:1
                            };
                        }else{
                            c[q].i++;
                        }
                    }
                }
            }
            // @param {Object}
            var max = {
                i:0,
                c:null
            }
            // loop the colors to see which is the most common
            for(var i in c) {
                if (c.hasOwnProperty(i)) {
                    // if this color was found more often than max then replace it
                    if(c[i].i > max.i) {
                        max.i = c[i].i
                        max.c = c[i].c;
                    }
                }
            }
            return max.c;
        }
Image
reduce map to 2 colors