New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

contours.ts

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

contours.ts - npm Package Compare versions

Comparing version 0.1.3 to 0.1.4

66

dist/contours.d.ts
import { ImageDataLike } from './types/ImageDataLike';
import { Point } from './types/Point';
import { Circle, Rectangle } from './types/ShapeType';
interface ContourFinderOptions {
blur: boolean;
threshold: number;
}
/**

@@ -7,5 +12,9 @@ * Contour tracing on a black and white image

export declare class ContourFinder {
private readonly data;
private static readonly THRESHOLD;
private data;
private readonly width;
private readonly height;
private readonly channels;
readonly contours: Point[][];
private isSimplified;
/**

@@ -15,3 +24,3 @@ * Caches information about visited points

private visited;
constructor(imageData: ImageDataLike);
constructor(imageData: ImageDataLike, options?: ContourFinderOptions);
/**

@@ -21,4 +30,12 @@ * Converts x,y to index postion of pixel

*/
pointToIndex(point: Point): number;
private pointToIndex;
/**
* Blurs the image
*/
private blur;
/**
* Threshold image data
*/
private toBitData;
/**
* Returns next clockwise pixel based on current position and direction

@@ -38,34 +55,4 @@ * @param previous previous point

*
* Moore-Neighbor tracing algorithm:
* Moore-Neighbor tracing algorithm: https://en.wikipedia.org/wiki/Moore_neighborhood
*
* Input: A square tessellation, T, containing a connected component P of black cells.
*
* Output: A sequence B (b1, b2 ,..., bk) of boundary pixels i.e. the contour.
*
* Define M(a) to be the Moore neighborhood of pixel a.
* Let p denote the current boundary pixel.
* Let c denote the current pixel under consideration i.e. c is in M(p).
*
* Begin
*
* Set B to be empty.
* From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
* Insert s in B.
* Set the current boundary point p to s i.e. p=s
* Backtrack i.e. move to the pixel from which s was entered.
* Set c to be the next clockwise pixel in M(p).
*
* While c not equal to s do
*
* If c is black
* insert c in B
* set p=c
* backtrack (move the current pixel c to the pixel from which p was entered)
* else
* advance the current pixel c to the next clockwise pixel in M(p)
*
* end While
*
* End
*
* @param first first black pixel found

@@ -79,7 +66,12 @@ * @param firstPrevious Previous pixel for first

*/
extract(): Point[][];
private extract;
/**
* Applies threshold to image data
* Simplifies contours using Ramer–Douglas–Peucker algorithm
*/
threshold(): ContourFinder;
simplify(epsilon?: number): ContourFinder;
/**
* Approximate contours to shapes
*/
approximate(): Array<Rectangle | Circle | Point[]>;
}
export {};

@@ -5,6 +5,2 @@ 'use strict';

function approximate() {
console.log('yay');
}
function _extends() {

@@ -28,5 +24,89 @@ _extends = Object.assign || function (target) {

function distance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function perpendicularDistance(point, start, end) {
if (start.x === end.x && start.y === end.y) {
return distance(point, start);
}
return Math.abs((start.y - end.y) * point.x + (end.x - start.x) * point.y + start.x * end.y - end.x * start.y) / distance(start, end);
}
/*
function DouglasPeucker(PointList[], epsilon)
// Find the point with the maximum distance
dmax = 0
index = 0
end = length(PointList)
for i = 2 to (end - 1) {
d = perpendicularDistance(PointList[i], Line(PointList[1], PointList[end]))
if (d > dmax) {
index = i
dmax = d
}
}
ResultList[] = empty;
// If max distance is greater than epsilon, recursively simplify
if (dmax > epsilon) {
// Recursive call
recResults1[] = DouglasPeucker(PointList[1...index], epsilon)
recResults2[] = DouglasPeucker(PointList[index...end], epsilon)
// Build the result list
ResultList[] = {recResults1[1...length(recResults1) - 1], recResults2[1...length(recResults2)]}
} else {
ResultList[] = {PointList[1], PointList[end]}
}
// Return the result
return ResultList[]
end
*/
/**
*
* Ramer–Douglas–Peucker algorithm: https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
*
* @param contour array of polygon/contour points
* @param epsilon
*/
function RDP(contour, epsilon) {
if (epsilon === void 0) {
epsilon = 1;
}
var endIndex = contour.length - 1;
var collection = [];
var maxDistance = 0;
var index = 0; // no need to simplify 2 points... duh!
if (contour.length <= 2) return contour;
for (var i = 1; i < endIndex; i += 1) {
var _distance = perpendicularDistance(contour[i], contour[0], contour[endIndex]);
if (_distance > maxDistance) {
index = i;
maxDistance = _distance;
}
}
if (maxDistance > epsilon) {
collection = [].concat(RDP(contour.slice(0, index), epsilon), RDP(contour.slice(index, endIndex), epsilon));
} else {
collection = [contour[0], contour[endIndex]];
}
return collection;
}
/**
* Moore neighborhood
*/
var clockwiseOffset = {

@@ -71,6 +151,16 @@ '1,0': {

var ContourFinder = /*#__PURE__*/function () {
function ContourFinder(imageData) {
function ContourFinder(imageData, options) {
if (options === void 0) {
options = {
blur: false,
threshold: 85
};
}
this.contours = [];
this.isSimplified = false;
/**
* Caches information about visited points
*/
this.visited = {};

@@ -80,2 +170,13 @@ this.data = imageData.data;

this.height = imageData.height;
this.channels = this.data.length / (this.width * this.height); // preprocess image if multi channel
if (this.channels > 1) {
// blurs the image for better edge detection
if (options.blur) this.blur(); // threshold image to get bit data
if (options.threshold > 0) this.toBitData(options.threshold);
} // perform contour detection
this.extract();
}

@@ -94,2 +195,56 @@ /**

/**
* Blurs the image
*/
;
_proto.blur = function blur() {
var _this = this;
var _loop = function _loop(x) {
var _loop2 = function _loop2(y) {
var i = _this.pointToIndex({
x: x,
y: y
}); // average each channel of the pixel
var _loop3 = function _loop3(c) {
_this.data[i + c] = Object.values(clockwiseOffset).reduce(function (prev, curr) {
prev += _this.data[_this.pointToIndex({
x: x + curr.x,
y: y + curr.y
})] + c;
return prev;
}, _this.data[i + c]) / 9;
};
for (var c = 0; c < _this.channels; c += 1) {
_loop3(c);
}
};
for (var y = 0; y < _this.height; y += 1) {
_loop2(y);
}
};
for (var x = 0; x < this.width; x += 1) {
_loop(x);
}
}
/**
* Threshold image data
*/
;
_proto.toBitData = function toBitData(threshold) {
var bitData = []; // pixel average
for (var i = 0; i < this.data.length; i += this.channels) {
bitData.push(0.2126 * this.data[i] + 0.7152 * this.data[i + 1] + 0.0722 * this.data[i + 2] >= threshold ? 255 : 0);
}
this.data = bitData;
}
/**
* Returns next clockwise pixel based on current position and direction

@@ -143,36 +298,36 @@ * @param previous previous point

}
/*
Input: A square tessellation, T, containing a connected component P of black cells.
Output: A sequence B (b1, b2, ..., bk) of boundary pixels i.e. the contour.
Define M(a) to be the Moore neighborhood of pixel a.
Let p denote the current boundary pixel.
Let c denote the current pixel under consideration i.e. c is in M(p).
Let b denote the backtrack of c (i.e. neighbor pixel of p that was previously tested)
Begin
Set B to be empty.
From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
Insert s in B.
Set the current boundary point p to s i.e. p=s
Let b = the pixel from which s was entered during the image scan.
Set c to be the next clockwise pixel (from b) in M(p).
While c not equal to s do
If c is black
insert c in B
Let b = p
Let p = c
(backtrack: move the current pixel c to the pixel from which p was entered)
Let c = next clockwise pixel (from b) in M(p).
else
(advance the current pixel c to the next clockwise pixel in M(p) and update backtrack)
Let b = c
Let c = next clockwise pixel (from b) in M(p).
end If
end While
End
*/
/**
*
* Moore-Neighbor tracing algorithm:
* Moore-Neighbor tracing algorithm: https://en.wikipedia.org/wiki/Moore_neighborhood
*
* Input: A square tessellation, T, containing a connected component P of black cells.
*
* Output: A sequence B (b1, b2 ,..., bk) of boundary pixels i.e. the contour.
*
* Define M(a) to be the Moore neighborhood of pixel a.
* Let p denote the current boundary pixel.
* Let c denote the current pixel under consideration i.e. c is in M(p).
*
* Begin
*
* Set B to be empty.
* From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
* Insert s in B.
* Set the current boundary point p to s i.e. p=s
* Backtrack i.e. move to the pixel from which s was entered.
* Set c to be the next clockwise pixel in M(p).
*
* While c not equal to s do
*
* If c is black
* insert c in B
* set p=c
* backtrack (move the current pixel c to the pixel from which p was entered)
* else
* advance the current pixel c to the next clockwise pixel in M(p)
*
* end While
*
* End
*
* @param first first black pixel found

@@ -225,3 +380,2 @@ * @param firstPrevious Previous pixel for first

_proto.extract = function extract() {
var contours = [];
var skipping = false; // find first black pixel from top-left to bottom-right

@@ -250,3 +404,3 @@

contours.push(this.traceContour({
this.contours.push(this.traceContour({
x: x,

@@ -257,12 +411,30 @@ y: y

}
}
/**
* Simplifies contours using Ramer–Douglas–Peucker algorithm
*/
;
return contours;
_proto.simplify = function simplify(epsilon) {
var _this2 = this;
if (epsilon === void 0) {
epsilon = 1;
}
this.contours.forEach(function (contour, index) {
_this2.contours[index] = RDP(contour, epsilon);
});
this.isSimplified = true;
return this;
}
/**
* Applies threshold to image data
* Approximate contours to shapes
*/
;
_proto.threshold = function threshold() {
return this;
_proto.approximate = function approximate() {
if (!this.isSimplified) this.simplify(); // TODO: approximate contours
return [];
};

@@ -272,30 +444,5 @@

}();
ContourFinder.THRESHOLD = 85;
// https://rosettacode.org/wiki/Ramer-Douglas-Peucker_line_simplification#JavaScript
function RDP(contour, eps) {
var lastIndex = contour.length - 1;
var firstPoint = contour[0];
var lastPoint = contour[lastIndex];
var x21 = lastPoint.x - firstPoint.x;
var y21 = lastPoint.y - firstPoint.y;
var _contour$slice$map$re = contour.slice(1, lastIndex).map(function (p) {
return Math.abs(y21 * p.x - x21 * p.y + lastPoint.x * firstPoint.y - lastPoint.y * firstPoint.x);
}).reduce(function (p, c, i) {
var v = Math.max(p[0], c);
return [v, v === p[0] ? p[1] : i + 1];
}, [-1, 0]),
dMax = _contour$slice$map$re[0],
x = _contour$slice$map$re[1];
if (dMax > eps) {
return [].concat(RDP(contour.slice(0, x + 1), eps), RDP(contour.slice(x), eps).slice(1));
}
return [contour[0], contour[lastIndex]];
}
exports.ContourFinder = ContourFinder;
exports.RDP = RDP;
exports.approximate = approximate;
//# sourceMappingURL=contours.ts.cjs.development.js.map

@@ -1,2 +0,2 @@

"use strict";function t(){return(t=Object.assign||function(t){for(var i=1;i<arguments.length;i++){var r=arguments[i];for(var e in r)Object.prototype.hasOwnProperty.call(r,e)&&(t[e]=r[e])}return t}).apply(this,arguments)}Object.defineProperty(exports,"__esModule",{value:!0});var i={"1,0":{x:1,y:1},"1,1":{x:0,y:1},"0,1":{x:-1,y:1},"-1,1":{x:-1,y:0},"-1,0":{x:-1,y:-1},"-1,-1":{x:0,y:-1},"0,-1":{x:1,y:-1},"1,-1":{x:1,y:0}};exports.ContourFinder=function(){function r(t){this.visited={},this.data=t.data,this.width=t.width,this.height=t.height}var e=r.prototype;return e.pointToIndex=function(t){return t.y*this.width+t.x},e.nextClockwise=function(t){var r=t.previous,e=t.boundary,n=t.start,o=void 0===n?r:n,s=i[r.x-e.x+","+(r.y-e.y)],a={x:e.x+s.x,y:e.y+s.y};return a.x<0||a.y<0||a.x>=this.width||a.y>=this.height?this.nextClockwise({previous:a,boundary:e,start:o}):a.x===o.x&&a.y===o.y?{previous:a,boundary:e}:0===this.data[this.pointToIndex(a)]?{previous:r,boundary:a}:this.nextClockwise({previous:a,boundary:e,start:o})},e.traceContour=function(i){var r=[t({},i)],e={x:i.x,y:i.y+1},n=t({},i),o=t({},e);do{var s=this.nextClockwise({previous:o,boundary:n});o=s.previous;var a=this.pointToIndex(n=s.boundary);this.visited[a]||(r.push(n),this.visited[a]=!0)}while(o.x!==e.x||o.y!==e.y||n.x!==i.x||n.y!==i.y);return r},e.extract=function(){for(var t=[],i=!1,r=0;r<this.width;r+=1)for(var e=this.height-1;e>=0;e-=1){var n=this.pointToIndex({x:r,y:e});0===this.data[n]?this.visited[n]||i?i=!0:(this.visited[n]=!0,t.push(this.traceContour({x:r,y:e}))):i=!1}return t},e.threshold=function(){return this},r}(),exports.RDP=function t(i,r){var e=i.length-1,n=i[0],o=i[e],s=o.x-n.x,a=o.y-n.y,h=i.slice(1,e).map((function(t){return Math.abs(a*t.x-s*t.y+o.x*n.y-o.y*n.x)})).reduce((function(t,i,r){var e=Math.max(t[0],i);return[e,e===t[0]?t[1]:r+1]}),[-1,0]),u=h[1];return h[0]>r?[].concat(t(i.slice(0,u+1),r),t(i.slice(u),r).slice(1)):[i[0],i[e]]},exports.approximate=function(){console.log("yay")};
"use strict";function t(){return(t=Object.assign||function(t){for(var i=1;i<arguments.length;i++){var r=arguments[i];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(t[n]=r[n])}return t}).apply(this,arguments)}function i(t,i){return Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2))}Object.defineProperty(exports,"__esModule",{value:!0});var r={"1,0":{x:1,y:1},"1,1":{x:0,y:1},"0,1":{x:-1,y:1},"-1,1":{x:-1,y:0},"-1,0":{x:-1,y:-1},"-1,-1":{x:0,y:-1},"0,-1":{x:1,y:-1},"1,-1":{x:1,y:0}},n=function(){function n(t,i){void 0===i&&(i={blur:!1,threshold:85}),this.contours=[],this.isSimplified=!1,this.visited={},this.data=t.data,this.width=t.width,this.height=t.height,this.channels=this.data.length/(this.width*this.height),this.channels>1&&(i.blur&&this.blur(),i.threshold>0&&this.toBitData(i.threshold)),this.extract()}var s=n.prototype;return s.pointToIndex=function(t){return t.y*this.width+t.x},s.blur=function(){for(var t=this,i=function(i){for(var n=function(n){for(var s=t.pointToIndex({x:i,y:n}),o=function(o){t.data[s+o]=Object.values(r).reduce((function(r,s){return r+(t.data[t.pointToIndex({x:i+s.x,y:n+s.y})]+o)}),t.data[s+o])/9},h=0;h<t.channels;h+=1)o(h)},s=0;s<t.height;s+=1)n(s)},n=0;n<this.width;n+=1)i(n)},s.toBitData=function(t){for(var i=[],r=0;r<this.data.length;r+=this.channels)i.push(.2126*this.data[r]+.7152*this.data[r+1]+.0722*this.data[r+2]>=t?255:0);this.data=i},s.nextClockwise=function(t){var i=t.previous,n=t.boundary,s=t.start,o=void 0===s?i:s,h=r[i.x-n.x+","+(i.y-n.y)],e={x:n.x+h.x,y:n.y+h.y};return e.x<0||e.y<0||e.x>=this.width||e.y>=this.height?this.nextClockwise({previous:e,boundary:n,start:o}):e.x===o.x&&e.y===o.y?{previous:e,boundary:n}:0===this.data[this.pointToIndex(e)]?{previous:i,boundary:e}:this.nextClockwise({previous:e,boundary:n,start:o})},s.traceContour=function(i){var r=[t({},i)],n={x:i.x,y:i.y+1},s=t({},i),o=t({},n);do{var h=this.nextClockwise({previous:o,boundary:s});o=h.previous;var e=this.pointToIndex(s=h.boundary);this.visited[e]||(r.push(s),this.visited[e]=!0)}while(o.x!==n.x||o.y!==n.y||s.x!==i.x||s.y!==i.y);return r},s.extract=function(){for(var t=!1,i=0;i<this.width;i+=1)for(var r=this.height-1;r>=0;r-=1){var n=this.pointToIndex({x:i,y:r});0===this.data[n]?this.visited[n]||t?t=!0:(this.visited[n]=!0,this.contours.push(this.traceContour({x:i,y:r}))):t=!1}},s.simplify=function(t){var r=this;return void 0===t&&(t=1),this.contours.forEach((function(n,s){r.contours[s]=function t(r,n){void 0===n&&(n=1);var s,o,h,e=r.length-1,a=0,u=0;if(r.length<=2)return r;for(var d=1;d<e;d+=1){var x=(s=r[d],(o=r[0]).x===(h=r[e]).x&&o.y===h.y?i(s,o):Math.abs((o.y-h.y)*s.x+(h.x-o.x)*s.y+o.x*h.y-h.x*o.y)/i(o,h));x>a&&(u=d,a=x)}return a>n?[].concat(t(r.slice(0,u),n),t(r.slice(u,e),n)):[r[0],r[e]]}(n,t)})),this.isSimplified=!0,this},s.approximate=function(){return this.isSimplified||this.simplify(),[]},n}();n.THRESHOLD=85,exports.ContourFinder=n;
//# sourceMappingURL=contours.ts.cjs.production.min.js.map

@@ -1,5 +0,1 @@

function approximate() {
console.log('yay');
}
function _extends() {

@@ -23,5 +19,89 @@ _extends = Object.assign || function (target) {

function distance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function perpendicularDistance(point, start, end) {
if (start.x === end.x && start.y === end.y) {
return distance(point, start);
}
return Math.abs((start.y - end.y) * point.x + (end.x - start.x) * point.y + start.x * end.y - end.x * start.y) / distance(start, end);
}
/*
function DouglasPeucker(PointList[], epsilon)
// Find the point with the maximum distance
dmax = 0
index = 0
end = length(PointList)
for i = 2 to (end - 1) {
d = perpendicularDistance(PointList[i], Line(PointList[1], PointList[end]))
if (d > dmax) {
index = i
dmax = d
}
}
ResultList[] = empty;
// If max distance is greater than epsilon, recursively simplify
if (dmax > epsilon) {
// Recursive call
recResults1[] = DouglasPeucker(PointList[1...index], epsilon)
recResults2[] = DouglasPeucker(PointList[index...end], epsilon)
// Build the result list
ResultList[] = {recResults1[1...length(recResults1) - 1], recResults2[1...length(recResults2)]}
} else {
ResultList[] = {PointList[1], PointList[end]}
}
// Return the result
return ResultList[]
end
*/
/**
*
* Ramer–Douglas–Peucker algorithm: https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
*
* @param contour array of polygon/contour points
* @param epsilon
*/
function RDP(contour, epsilon) {
if (epsilon === void 0) {
epsilon = 1;
}
var endIndex = contour.length - 1;
var collection = [];
var maxDistance = 0;
var index = 0; // no need to simplify 2 points... duh!
if (contour.length <= 2) return contour;
for (var i = 1; i < endIndex; i += 1) {
var _distance = perpendicularDistance(contour[i], contour[0], contour[endIndex]);
if (_distance > maxDistance) {
index = i;
maxDistance = _distance;
}
}
if (maxDistance > epsilon) {
collection = [].concat(RDP(contour.slice(0, index), epsilon), RDP(contour.slice(index, endIndex), epsilon));
} else {
collection = [contour[0], contour[endIndex]];
}
return collection;
}
/**
* Moore neighborhood
*/
var clockwiseOffset = {

@@ -66,6 +146,16 @@ '1,0': {

var ContourFinder = /*#__PURE__*/function () {
function ContourFinder(imageData) {
function ContourFinder(imageData, options) {
if (options === void 0) {
options = {
blur: false,
threshold: 85
};
}
this.contours = [];
this.isSimplified = false;
/**
* Caches information about visited points
*/
this.visited = {};

@@ -75,2 +165,13 @@ this.data = imageData.data;

this.height = imageData.height;
this.channels = this.data.length / (this.width * this.height); // preprocess image if multi channel
if (this.channels > 1) {
// blurs the image for better edge detection
if (options.blur) this.blur(); // threshold image to get bit data
if (options.threshold > 0) this.toBitData(options.threshold);
} // perform contour detection
this.extract();
}

@@ -89,2 +190,56 @@ /**

/**
* Blurs the image
*/
;
_proto.blur = function blur() {
var _this = this;
var _loop = function _loop(x) {
var _loop2 = function _loop2(y) {
var i = _this.pointToIndex({
x: x,
y: y
}); // average each channel of the pixel
var _loop3 = function _loop3(c) {
_this.data[i + c] = Object.values(clockwiseOffset).reduce(function (prev, curr) {
prev += _this.data[_this.pointToIndex({
x: x + curr.x,
y: y + curr.y
})] + c;
return prev;
}, _this.data[i + c]) / 9;
};
for (var c = 0; c < _this.channels; c += 1) {
_loop3(c);
}
};
for (var y = 0; y < _this.height; y += 1) {
_loop2(y);
}
};
for (var x = 0; x < this.width; x += 1) {
_loop(x);
}
}
/**
* Threshold image data
*/
;
_proto.toBitData = function toBitData(threshold) {
var bitData = []; // pixel average
for (var i = 0; i < this.data.length; i += this.channels) {
bitData.push(0.2126 * this.data[i] + 0.7152 * this.data[i + 1] + 0.0722 * this.data[i + 2] >= threshold ? 255 : 0);
}
this.data = bitData;
}
/**
* Returns next clockwise pixel based on current position and direction

@@ -138,36 +293,36 @@ * @param previous previous point

}
/*
Input: A square tessellation, T, containing a connected component P of black cells.
Output: A sequence B (b1, b2, ..., bk) of boundary pixels i.e. the contour.
Define M(a) to be the Moore neighborhood of pixel a.
Let p denote the current boundary pixel.
Let c denote the current pixel under consideration i.e. c is in M(p).
Let b denote the backtrack of c (i.e. neighbor pixel of p that was previously tested)
Begin
Set B to be empty.
From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
Insert s in B.
Set the current boundary point p to s i.e. p=s
Let b = the pixel from which s was entered during the image scan.
Set c to be the next clockwise pixel (from b) in M(p).
While c not equal to s do
If c is black
insert c in B
Let b = p
Let p = c
(backtrack: move the current pixel c to the pixel from which p was entered)
Let c = next clockwise pixel (from b) in M(p).
else
(advance the current pixel c to the next clockwise pixel in M(p) and update backtrack)
Let b = c
Let c = next clockwise pixel (from b) in M(p).
end If
end While
End
*/
/**
*
* Moore-Neighbor tracing algorithm:
* Moore-Neighbor tracing algorithm: https://en.wikipedia.org/wiki/Moore_neighborhood
*
* Input: A square tessellation, T, containing a connected component P of black cells.
*
* Output: A sequence B (b1, b2 ,..., bk) of boundary pixels i.e. the contour.
*
* Define M(a) to be the Moore neighborhood of pixel a.
* Let p denote the current boundary pixel.
* Let c denote the current pixel under consideration i.e. c is in M(p).
*
* Begin
*
* Set B to be empty.
* From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
* Insert s in B.
* Set the current boundary point p to s i.e. p=s
* Backtrack i.e. move to the pixel from which s was entered.
* Set c to be the next clockwise pixel in M(p).
*
* While c not equal to s do
*
* If c is black
* insert c in B
* set p=c
* backtrack (move the current pixel c to the pixel from which p was entered)
* else
* advance the current pixel c to the next clockwise pixel in M(p)
*
* end While
*
* End
*
* @param first first black pixel found

@@ -220,3 +375,2 @@ * @param firstPrevious Previous pixel for first

_proto.extract = function extract() {
var contours = [];
var skipping = false; // find first black pixel from top-left to bottom-right

@@ -245,3 +399,3 @@

contours.push(this.traceContour({
this.contours.push(this.traceContour({
x: x,

@@ -252,12 +406,30 @@ y: y

}
}
/**
* Simplifies contours using Ramer–Douglas–Peucker algorithm
*/
;
return contours;
_proto.simplify = function simplify(epsilon) {
var _this2 = this;
if (epsilon === void 0) {
epsilon = 1;
}
this.contours.forEach(function (contour, index) {
_this2.contours[index] = RDP(contour, epsilon);
});
this.isSimplified = true;
return this;
}
/**
* Applies threshold to image data
* Approximate contours to shapes
*/
;
_proto.threshold = function threshold() {
return this;
_proto.approximate = function approximate() {
if (!this.isSimplified) this.simplify(); // TODO: approximate contours
return [];
};

@@ -267,28 +439,5 @@

}();
ContourFinder.THRESHOLD = 85;
// https://rosettacode.org/wiki/Ramer-Douglas-Peucker_line_simplification#JavaScript
function RDP(contour, eps) {
var lastIndex = contour.length - 1;
var firstPoint = contour[0];
var lastPoint = contour[lastIndex];
var x21 = lastPoint.x - firstPoint.x;
var y21 = lastPoint.y - firstPoint.y;
var _contour$slice$map$re = contour.slice(1, lastIndex).map(function (p) {
return Math.abs(y21 * p.x - x21 * p.y + lastPoint.x * firstPoint.y - lastPoint.y * firstPoint.x);
}).reduce(function (p, c, i) {
var v = Math.max(p[0], c);
return [v, v === p[0] ? p[1] : i + 1];
}, [-1, 0]),
dMax = _contour$slice$map$re[0],
x = _contour$slice$map$re[1];
if (dMax > eps) {
return [].concat(RDP(contour.slice(0, x + 1), eps), RDP(contour.slice(x), eps).slice(1));
}
return [contour[0], contour[lastIndex]];
}
export { ContourFinder, RDP, approximate };
export { ContourFinder };
//# sourceMappingURL=contours.ts.esm.js.map

@@ -1,3 +0,1 @@

export * from './approximate';
export * from './contours';
export * from './rdp';
import { Point } from './types/Point';
export declare function RDP(contour: Point[], eps: number): Point[];
export declare function perpendicularDistance(point: Point, start: Point, end: Point): number;
/**
*
* Ramer–Douglas–Peucker algorithm: https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
*
* @param contour array of polygon/contour points
* @param epsilon
*/
export declare function RDP(contour: Point[], epsilon?: number): Point[];

@@ -6,10 +6,11 @@ {

"devDependencies": {
"@size-limit/preset-small-lib": "^4.9.0",
"@types/eslint": "^7.2.5",
"@size-limit/preset-small-lib": "^4.9.1",
"@types/eslint": "^7.2.6",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/jest": "^26.0.15",
"@typescript-eslint/eslint-plugin": "^4.8.2",
"@typescript-eslint/parser": "^4.8.2",
"eslint": "^7.14.0",
"eslint-config-prettier": "^6.15.0",
"@types/jest": "^26.0.19",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"babel-eslint": "^10.1.0",
"eslint": "^7.15.0",
"eslint-config-prettier": "^7.0.0",
"eslint-config-react-app": "^6.0.0",

@@ -23,3 +24,3 @@ "eslint-config-standard": "^16.0.2",

"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-prettier": "^3.2.0",
"eslint-plugin-promise": "^4.2.1",

@@ -29,9 +30,10 @@ "eslint-plugin-react": "^7.21.5",

"eslint-plugin-standard": "^4.1.0",
"husky": "^4.3.0",
"husky": "^4.3.5",
"jest": "^26.6.3",
"size-limit": "^4.9.0",
"prettier": "^2.2.1",
"size-limit": "^4.9.1",
"ts-jest": "^26.4.4",
"tsdx": "^0.14.1",
"tslib": "^2.0.3",
"typescript": "^4.1.2"
"typescript": "^4.1.3"
},

@@ -94,3 +96,3 @@ "engines": {

"typings": "dist/index.d.ts",
"version": "0.1.3"
"version": "0.1.4"
}
# Contours.ts
Extract shapes & contours in an image, for browsers and node.js.
Extract contours & shapes from an image, for browsers and node.js.
[![NPM](https://nodei.co/npm/contours.ts.png?compact=true)](https://nodei.co/npm/contours.ts/)
![CI](https://github.com/mubaidr/contours.ts/workflows/CI/badge.svg)
[![codecov](https://codecov.io/gh/mubaidr/contours.ts/branch/master/graph/badge.svg?token=3SJIBJ1679)](https://codecov.io/gh/mubaidr/contours.ts)
[![codebeat badge](https://codebeat.co/badges/0c5399f3-60d7-466f-b87d-94dcc0b47d9f)](https://codebeat.co/projects/github-com-mubaidr-contours-ts-master)
[![codecov](https://codecov.io/gh/mubaidr/contours.ts/branch/master/graph/badge.svg?token=3SJIBJ1679)](https://codecov.io/gh/mubaidr/contours.ts)
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)
[![NPM](https://nodei.co/npm/contours.ts.png)](https://nodei.co/npm/contours.ts/)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="https://patreon.com/mubaidr">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" height="45">
</a>
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
## Features
### Extract Contours
### Approximate Contours to Shapes
## How to use
### Use
### Install
#### using pckage manager:
```bash
npm i contours.ts
```
Or
#### add to your webpage:
```html
<script src="unpkg.com/contours.ts"></script>
```
Or
#### download manually:
[Contours.ts](https://unpkg.com/contours.ts)
### Examples
#### Input Type
`ContourFinder` expects image data (multi-channel image data is suported) in following format:
```ts
import { ContourFinder } from 'contours.ts'
/*
if `imageData` is like `{
const imageData: {
data: number[] | Uint8ClampedArray
width: number
height: number
}`
} = {
data: [0,0,0],
width: 1,
height: 1
}
*/
```
const contours = new ContourFinder(imageData).threshold().extract()
#### Get Contours
```ts
import { ContourFinder } from 'contours.ts'
const { contours } = new ContourFinder(imageData, {
blur: true // blurs the image data
threshold: 85 // threshold image data
})
console.log(contours)
/*
- `contours` is collection of contours found in image
- each contour is collection of points.
e.g.
logs:
[
[{x: 0, y: 0}, {x: 1, y: 0}], //contour
[{x: 0, y: 1}, {x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}] // another contour
[{x: 0, y: 0}, {x: 1, y: 0}], // contour
[{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}] // another contour
]
*/
```
### Install
#### Simplify Contours
Recommended way to install is by using package manager (npm, yarn etc):
```ts
import { ContourFinder } from 'contours.ts'
// simplify contours using Ramer–Douglas–Peucker algorithm
const { simplifiedContours } = new ContourFinder(imageData).simplify(epsilon)
```bash
npm i contours.ts
console.log(simplifiedContours)
/*
logs:
[
[{x: 0, y: 0}, {x: 1, y: 0}], // contour
[{x: 0, y: 0}, {x: 3, y: 3}] // another contour
]
*/
```
or use cdn:
#### Approximate Shapes
```html
<script src="//unpkg.com/contours.ts"></script>
```
```ts
import { ContourFinder } from 'contours.ts'
// Or approximate contours to shapes
const { shapeCollection } = new ContourFinder(imageData).approximate()
or download manually:
console.log(shapeCollection)
[contours.ts](https://unpkg.com/contours.ts)
/*
logs:
[
{x: 0, y: 0, width: 1, height: 1}, // Rect
{x: 1, y: 1, radius: 1}, // Circle
[{x: 0, y: 0}, {x: 3, y: 3}] // Polygon
]
*/
```
## Contributions
Contirbutions are welcome, entire code base is filled with comments and `tests` are defined using `jest`
Contirbutions are welcome, code is heavily commented and `tests` are defined using `jest`
## License
Distributed under the MIT License. See `LICENSE` for more information.
## Contact
Muhammad Ubaid Raza - [@mubaidr](https://twitter.com/mubaidr) - mubaidr@gmail.com
## Acknowledgements
This project is heavily inspired by the these:
- [benfoxall/contours](https://github.com/benfoxall/contours)
- [dkendal/moore-neighbor_contour_tracer](https://github.com/Dkendal/Moore-Neighbor_Contour_Tracer)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://benjaminbenben.com"><img src="https://avatars3.githubusercontent.com/u/51385?v=4" width="100px;" alt=""/><br /><sub><b>Ben Foxall</b></sub></a><br /><a href="https://github.com/mubaidr/contours.ts/commits?author=benfoxall" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

@@ -0,3 +1,5 @@

import { RDP } from './rdp'
import { ImageDataLike } from './types/ImageDataLike'
import { Point } from './types/Point'
import { Circle, Rectangle } from './types/ShapeType'

@@ -23,2 +25,7 @@ /**

interface ContourFinderOptions {
blur: boolean
threshold: number
}
/**

@@ -28,6 +35,13 @@ * Contour tracing on a black and white image

export class ContourFinder {
private readonly data: Uint8ClampedArray | number[]
private static readonly THRESHOLD = 85
private data: Uint8ClampedArray | number[]
private readonly width: number
private readonly height: number
private readonly channels: number
public readonly contours: Point[][] = []
private isSimplified = false
/**

@@ -38,6 +52,25 @@ * Caches information about visited points

constructor(imageData: ImageDataLike) {
constructor(
imageData: ImageDataLike,
options: ContourFinderOptions = {
blur: false,
threshold: 85,
}
) {
this.data = imageData.data
this.width = imageData.width
this.height = imageData.height
this.channels = this.data.length / (this.width * this.height)
// preprocess image if multi channel
if (this.channels > 1) {
// blurs the image for better edge detection
if (options.blur) this.blur()
// threshold image to get bit data
if (options.threshold > 0) this.toBitData(options.threshold)
}
// perform contour detection
this.extract()
}

@@ -49,3 +82,3 @@

*/
pointToIndex(point: Point): number {
private pointToIndex(point: Point): number {
return point.y * this.width + point.x

@@ -55,2 +88,53 @@ }

/**
* Blurs the image
*/
private blur(): void {
for (let x = 0; x < this.width; x += 1) {
for (let y = 0; y < this.height; y += 1) {
const i = this.pointToIndex({
x,
y,
})
// average each channel of the pixel
for (let c = 0; c < this.channels; c += 1) {
this.data[i + c] =
Object.values(clockwiseOffset).reduce((prev, curr) => {
prev +=
this.data[
this.pointToIndex({
x: x + curr.x,
y: y + curr.y,
})
] + c
return prev
}, this.data[i + c]) / 9
}
}
}
}
/**
* Threshold image data
*/
private toBitData(threshold: number): void {
const bitData: number[] = []
// pixel average
for (let i = 0; i < this.data.length; i += this.channels) {
bitData.push(
0.2126 * this.data[i] +
0.7152 * this.data[i + 1] +
0.0722 * this.data[i + 2] >=
threshold
? 255
: 0
)
}
this.data = bitData
}
/**
* Returns next clockwise pixel based on current position and direction

@@ -110,36 +194,39 @@ * @param previous previous point

/*
Input: A square tessellation, T, containing a connected component P of black cells.
Output: A sequence B (b1, b2, ..., bk) of boundary pixels i.e. the contour.
Define M(a) to be the Moore neighborhood of pixel a.
Let p denote the current boundary pixel.
Let c denote the current pixel under consideration i.e. c is in M(p).
Let b denote the backtrack of c (i.e. neighbor pixel of p that was previously tested)
Begin
Set B to be empty.
From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
Insert s in B.
Set the current boundary point p to s i.e. p=s
Let b = the pixel from which s was entered during the image scan.
Set c to be the next clockwise pixel (from b) in M(p).
While c not equal to s do
If c is black
insert c in B
Let b = p
Let p = c
(backtrack: move the current pixel c to the pixel from which p was entered)
Let c = next clockwise pixel (from b) in M(p).
else
(advance the current pixel c to the next clockwise pixel in M(p) and update backtrack)
Let b = c
Let c = next clockwise pixel (from b) in M(p).
end If
end While
End
*/
/**
*
* Moore-Neighbor tracing algorithm:
* Moore-Neighbor tracing algorithm: https://en.wikipedia.org/wiki/Moore_neighborhood
*
* Input: A square tessellation, T, containing a connected component P of black cells.
*
* Output: A sequence B (b1, b2 ,..., bk) of boundary pixels i.e. the contour.
*
* Define M(a) to be the Moore neighborhood of pixel a.
* Let p denote the current boundary pixel.
* Let c denote the current pixel under consideration i.e. c is in M(p).
*
* Begin
*
* Set B to be empty.
* From bottom to top and left to right scan the cells of T until a black pixel, s, of P is found.
* Insert s in B.
* Set the current boundary point p to s i.e. p=s
* Backtrack i.e. move to the pixel from which s was entered.
* Set c to be the next clockwise pixel in M(p).
*
* While c not equal to s do
*
* If c is black
* insert c in B
* set p=c
* backtrack (move the current pixel c to the pixel from which p was entered)
* else
* advance the current pixel c to the next clockwise pixel in M(p)
*
* end While
*
* End
*
* @param first first black pixel found

@@ -192,4 +279,3 @@ * @param firstPrevious Previous pixel for first

*/
public extract(): Point[][] {
const contours: Point[][] = []
private extract(): void {
let skipping = false

@@ -218,3 +304,3 @@

// we will trace contour starting from this black pixel
contours.push(
this.contours.push(
this.traceContour({

@@ -227,13 +313,26 @@ x,

}
return contours
}
/**
* Applies threshold to image data
* Simplifies contours using Ramer–Douglas–Peucker algorithm
*/
public simplify(epsilon = 1): ContourFinder {
this.contours.forEach((contour, index) => {
this.contours[index] = RDP(contour, epsilon)
})
public threshold(): ContourFinder {
this.isSimplified = true
return this
}
/**
* Approximate contours to shapes
*/
public approximate(): Array<Rectangle | Circle | Point[]> {
if (!this.isSimplified) this.simplify()
// TODO: approximate contours
return []
}
}

@@ -1,3 +0,1 @@

export * from './approximate'
export * from './contours'
export * from './rdp'

@@ -1,38 +0,99 @@

// https://rosettacode.org/wiki/Ramer-Douglas-Peucker_line_simplification#JavaScript
import { Point } from './types/Point'
export function RDP(contour: Point[], eps: number): Point[] {
const lastIndex = contour.length - 1
const firstPoint = contour[0]
const lastPoint = contour[lastIndex]
const x21 = lastPoint.x - firstPoint.x
const y21 = lastPoint.y - firstPoint.y
function distance(p1: Point, p2: Point): number {
return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
}
const [dMax, x] = contour
.slice(1, lastIndex)
.map((p: { x: number; y: number }) =>
Math.abs(
y21 * p.x -
x21 * p.y +
lastPoint.x * firstPoint.y -
lastPoint.y * firstPoint.x
)
export function perpendicularDistance(
point: Point,
start: Point,
end: Point
): number {
if (start.x === end.x && start.y === end.y) {
return distance(point, start)
}
return (
Math.abs(
(start.y - end.y) * point.x +
(end.x - start.x) * point.y +
start.x * end.y -
end.x * start.y
) / distance(start, end)
)
}
/*
function DouglasPeucker(PointList[], epsilon)
// Find the point with the maximum distance
dmax = 0
index = 0
end = length(PointList)
for i = 2 to (end - 1) {
d = perpendicularDistance(PointList[i], Line(PointList[1], PointList[end]))
if (d > dmax) {
index = i
dmax = d
}
}
ResultList[] = empty;
// If max distance is greater than epsilon, recursively simplify
if (dmax > epsilon) {
// Recursive call
recResults1[] = DouglasPeucker(PointList[1...index], epsilon)
recResults2[] = DouglasPeucker(PointList[index...end], epsilon)
// Build the result list
ResultList[] = {recResults1[1...length(recResults1) - 1], recResults2[1...length(recResults2)]}
} else {
ResultList[] = {PointList[1], PointList[end]}
}
// Return the result
return ResultList[]
end
*/
/**
*
* Ramer–Douglas–Peucker algorithm: https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
*
* @param contour array of polygon/contour points
* @param epsilon
*/
export function RDP(contour: Point[], epsilon = 1): Point[] {
const endIndex = contour.length - 1
let collection: Point[] = []
let maxDistance = 0
let index = 0
// no need to simplify 2 points... duh!
if (contour.length <= 2) return contour
for (let i = 1; i < endIndex; i += 1) {
const distance = perpendicularDistance(
contour[i],
contour[0],
contour[endIndex]
)
.reduce(
(p: number[], c: number, i: number) => {
const v = Math.max(p[0], c)
return [v, v === p[0] ? p[1] : i + 1]
},
[-1, 0]
)
if (dMax > eps) {
return [
...RDP(contour.slice(0, x + 1), eps),
...RDP(contour.slice(x), eps).slice(1),
if (distance > maxDistance) {
index = i
maxDistance = distance
}
}
if (maxDistance > epsilon) {
collection = [
...RDP(contour.slice(0, index), epsilon),
...RDP(contour.slice(index, endIndex), epsilon),
]
} else {
collection = [contour[0], contour[endIndex]]
}
return [contour[0], contour[lastIndex]]
return collection
}

@@ -0,0 +0,0 @@ export interface ImageDataLike {

@@ -0,0 +0,0 @@ export interface Point {

@@ -0,0 +0,0 @@ export enum ShapeTypes {

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc