pXY.js provides a pleasant interface for analyzing <canvas> pixels in an effort to speed up implementation, readability and debugging of custom analysis algorithms such as document feature extraction, OCR segmentation, etc. I suppose this tutorial also demonstrates the lib's usefulness as an instructional tool for algo visualization in general :P
Let's start by loading a 400x200 playground image onto a canvas. You have two methods of doing this.
Provide an image path and callback to the .load() method.
pXY.load("/img/playgrnd.png", function(can, ctx, img, imgd) {
var pxy = this;
document.getElementById("ctnr").appendChild(can);
// your code here
});
Provide an existing <canvas>, <img> or ImageData object.
var can = document.getElementById("can"),
pxy = new pXY(can);
// your code here
.moveTo(x, y) - absolute.moveBy(stepX, stepY) - relative.ok - success indicator.moveTl() - top left.moveTr() - top right.moveBl() - bottom left.moveBr() - bottom rightAll move methods are chainable, returning the same pXY instance. To determine whether a move failed due to coordinates falling outside the extents, check the boolean .ok property. Relative stepX and stepY parameters may be negative or positive integers depending on the desired move direction. Additionally, 4 shorthand methods move to the corners.
var i;
pxy.moveTo(50,100);
i = 30;
while (i--)
pxy.moveBy(2,-1); // red
i = 25;
while (i--)
pxy.moveBy(0,1); // green
i = 40;
while (i--)
pxy.moveBy(1,0); // blue
// move down until limit
while (pxy.moveBy(0,1).ok) {} // magenta
* For this tutorial, the shown code is throttled & traced with the indicated colors for visualization. These features are covered in their own sections later.
.x - x-coord.y - y-coord.px(x, y, abs) - returns a px object which wraps all direct properties and implements all computed methods..r - red.g - green.b - blue.a - alphaAfter each move you can read the current position and pixel. The direct sub-pixel components will be exposed automatically, though they aren't too useful in practice. Computed properties are slower, so they're available through explicit getter methods and cached on initial access. As a shorthand, the pXY instance provides proxies to the direct and computed properties of the current pixel. For example, all of the following are equivalent and will return the current pixel's luminance: pxy.px(0,0).lum()pxy.px().lum()pxy.lum()
pxy.moveTo(75,80);
var lum = pxy.lum();
while (pxy.lum() >= lum) // yellow
pxy.moveBy(0,1);
pxy.moveTo(65,80);
var alph = pxy.a;
while (pxy.a >= alph) // red
pxy.moveBy(0,1);
Using .px(x,y), you can read pixels around the current position as well. Here's the same code using a lookahead read that now halts prior to running 1px into the edges.
pxy.moveTo(75,80);
var lum = pxy.lum();
while (pxy.px(0,1).lum() >= lum) // yellow
pxy.moveBy(0,1);
pxy.moveTo(65,80);
var alph = pxy.a;
while (pxy.px(0,1).a >= alph) // red
pxy.moveBy(0,1);
.scanLf(fn, step).scanRt().scanUp().scanDn().scanXY(fn, stepX, stepY).scanYX().scanAR(fn, stepA, stepR, bbox, aType).scanRA().scanRand(fn).scan(nextXY, fn)Scanning elminitates the need to write move loops. Signatures are uniform within each set above. fn is a callback to be executed after each move; it can accept a single steps argument indicating the number of steps taken so far; returning false from fn will terminate the scan. step, stepX, stepY indicate the distance and direction to move at each step on respective axes, defaulting to 1.
Polar scanning functions are desgined to scan in a circular fashion around the current location. Imagine the wheel of a bicycle, you can scan around the circumference or along the spokes. This is useful for locating pixels by proximity. These scanning methods are also bidirectional with stepA dictating step and direction along the A (angle) axis and stepR controlling the step and direction of the on the R (radius) axis. bbox is an optional bounding box along both scan axes. Its form is {rmin: minRadius, rmax: maxRadius, amin: startAngle, amax: endAngle}.
For the custom scanner, nextXY should be either a step array [stepX, stepY] or a calc function which returns an absolute array [x, y] with the next coordinates at each step; this within the function is the pXY instance.
Here are the 8 possible bidirectional scan patterns (1px steps) laid over a 3x3 pixel grid if the starting position is at the green arrow. They're attained by choosing a scanning method and a positive or negative stepX and stepY.
| +x +y | +x -y | -x +y | -x -y | |
|---|---|---|---|---|
| .scanXY() | ![]() |
![]() |
![]() |
![]() |
| .scanYX() | ![]() |
![]() |
![]() |
![]() |
Example: An XY and YX scan locate the top and left non-transparent extents of the image.
var top, lft;
// top extent
pxy.moveTl().scanXY(function(steps) { // red
if (this.a != 0) {
top = this.y;
return false;
}
});
// left extent
pxy.moveBl().scanYX(function(steps) { // blue
if (this.a != 0) {
lft = this.x;
return false;
}
}, 1, -1);
.push() - save current position.pop() - move to last saved positionWhen scanning (especially during nested scans) it's useful to save and restore previous positions. Two self-explanatory, chainable methods manage a position save stack.
Example: A bit which might be used to build a vertical projection profile as part of an OCR algorithm, eg: here and here
pxy.moveTo(143,95).push().scanRt(function(steps) {
this.push().scanUp(function(steps) {
return this.px(0,-1).lum() < 250;
}).pop();
return steps < 35;
}).pop();
.xywh(x,y,w,h) - new instance by a corner, width, height.xyxy(x0,y0,x1,y1) - new instance by opposite cornersSections of the image can be isolated using additional pXY instances. This creates re-zero'd coordinate spaces and limits the extents for moves and scans. Each method takes various parameters of a bounding box.
Example: Isolate a bounding box around "test" and run the same extent detection sequence as before, using luminance this time.
var pxy2 = pxy.xywh(314,15,45,25);
var top, rgt;
// top extent
pxy2.moveTl().scanXY(function(steps) { // red
if (this.lum() < 50) {
top = this.y;
return false;
}
});
// right extent
pxy2.moveBr().scanYX(function(steps) { // blue
if (this.lum() < 50) {
rgt = this.x;
return false;
}
}, -1, -1);
.w - width.h - height.top.rgt.btm.lft.relXy(lvl) - x/y coords.relIdx(lvl) - pixel index.relOff(lvl) - bounding box offset.absXy().absIdx().absOff()After sectioning, the resulting instances hold references to their parent, forming a quasi-nested heirarchy. The above methods can be used to obtain accumulated coordinates, pixel indices and bounding box offsets relative to ancestor pXY instances. This information is useful when building an HTML or SVG structure to overlay on top of the canvas. The lvl param, indicates how many ancestors to traverse upward. The absolute methods are just shorthands with an implicit lvl of 10000.
.sub(fn, ctx) - subscribe.unsub(fn, ctx) - unsubscribe.pub(evt) - publish custom eventEach pXY instance emits 3 types of events - move (0), scan-start (1) and scan-end (2). These events can be subscribed to by external functions with all subscribers getting notified of all event types. If provided, ctx becomes this within the fn callback body (otherwise the pXY instance) while evt is passed to it as the sole argument. The structure of each built-in event is simple:
{
type: <type id>,
id: <event id>,
pxy: <pXY instance>,
}
Example: General overview
function notify(evt) {
console.log(this.foo);
switch (evt.type) {
case 0: console.log("moved!"); break;
case 1: console.log("scan-start!"); break;
case 2: console.log("scan-end!"); break;
}
console.log("@ " + evt.pxy.x + "," + evt.pxy.y);
}
var myCtx = {
foo: "bar";
};
pxy.sub(notify, myCtx);
pxy.moveTo(100,30);
/*
"bar"
"moved!"
"@ 100,30"
*/
* Even though .pop() perfoms a move, it does not emit a move event by design - doing otherwise proved to be a nuisance.
The decoupled tracer library used for this tutorial works like this, so more-concrete examples appear in the Tracing section ahead.
Tracing enables visualization and debugging of algorithms. It can be done in different colors on a single or multiple layers. Each layer becomes a separate canvas element that overlays on top of the primary image canvas. The functionality is provided by a pxTrcr class which subscribes to move and scan Events emitted by one or multiple pXY instances.
new pxTrcr(width, height, ctnr) - construct.sub(pxy) & .unsub(pxy) - subscribe to events.set(px) - set trace pixel.draw() - renderSingle-layer, direct tracing is fairly straightforward and will likely suffice for most needs. The only thing to make sure is that the target container element ctnr contains the main image canvas and has its CSS "position" explicitly set (eg: "relative", "absolute", "fixed").
// new tracer using pxy's width and height and container
var ctnr = document.getElementById("ctnr"),
trc = new pxTrcr(pxy.w, pxy.h, ctnr);
// subscribe to pxy events
trc.sub(pxy);
// trace in red
trc.set([255,0,0]);
// move around
pxy.moveTo(0,100).scanRt();
// trace in blue (half opacity)
trc.set([0,0,255,128]);
// move around
pxy.moveTo(200,0).scanDn();
// show trace
trc.draw();
.set(px, lyrId) - set trace pixel and/or layer.clr(lyrId) - clear layer(s)
You can trace onto different layers if needed. .set() actually wraps quite a bit of functionality. It can select and/or create pixels and/or layers, accepting either one or 2 arguments interchangeably. If a layer with the specified lyrId exists, it will be selected, otherwise it will first be created and then selected. If only one parameter is specified, the other will remain unaltered. The default layer has lyrId of 0. To clear one or all layers, call .clr() with or without a lyrId, respectively.
Here's an example to make sense of all this.
// create and select lyrA, set red trace pixel
trc.set([255,0,0], "lyrA")
pxy.moveTo(150,0).scanDn();
// set green trace pixel, use same layer
trc.set([0,255,0]);
pxy.moveBy(10,0).scanUp();
// create new layer lyrB, use same pixel
trc.set("lyrB");
pxy.moveBy(10,0).scanDn();
// set magenta pixel, use same layer
trc.set([255,0,255]);
pxy.moveBy(10,0).scanUp();
// switch to lyrA, set blue pixel
trc.set([0,0,255], "lyrA");
pxy.moveBy(10,0).scanDn();
trc.draw();
then toggle
.set(px, lyrId) - manage last item, without growing the stack.push(px, lyrId) - add a px/lyrId combo to stack.pop() - remove last item, activating previous.one(px, lyrId) - same as push, except auto-pops after a single scan or moveDiving a bit deeper, .set(px, lyrId) actually manipulates the last item of a stack which can contain other px/lyrId combinations. The last item added to the stack always represents the currently active trace layer and pixel. Up to this point there has only been a single item there, so .set() has been sufficient. As with pXY's position stack, the trace config stack lets you easily save and recall trace settings with nested operations, but its utility is a bit more valuable. If you have a sequence wrapped in a function which uses multiple moves and scans, you can use the stack to unobtrusively decorate sub-routines with different trace colors and/or layers.
// lets add an expander algo to pXY
pXY.prototype.bloom = function bloom() {
var lum = this.lum(),
chk = function(steps) {
return this.lum() >= lum;
};
this.push().scanUp(chk).pop()
.push().scanRt(chk).pop()
.push().scanDn(chk).pop()
.push().scanLf(chk).pop();
};
// now wrap it with some tracing code
function bloomTraced(pxy) {
pxy.moveTo(300,110);
trc.one([255,0,0]);
trc.one([0,255,0]);
trc.one([0,0,255]);
trc.one([255,0,255]);
pxy.bloom();
}
bloomTraced(pxy);
trc.draw();
Granted, things can't always be decorated this elegantly...for example if your algorithm contains nested scans (in some cases) or lots of individual move calls. However, this is as complicated as I care to make it; if you have ideas for improvement, let me know.
.rec() - record events and actions.draw(rate) - playback with steps/sec rateTo animate the traces, recording must be enabled and a step rate may be passed into the .draw() method. Once recording is activated, all pxTrcr stack calls and emitted pXY events are placed into a queue that is then processed at a chosen steps/sec rate. Because everything is enqueued in a huge array, be mindful of mem usage - recording a million events will take some space. Also, the actual speed you can expect largely depends on how fast you machine and browser are able update the canvas(es), since they're updated up to 60 times per sec. So for large canvases, multiple layers and/or slow machines, the achievable rate may be performance-limited.
Example: Here's the same function from the previous section, but animated at 30 steps/sec.
trc.rec();
bloomTraced(pxy);
trc.draw(100);
new pxChkr(cfg, pxy) - construct.chk() - check all pixels/tolerancesA pxChkr class takes some pain out of coding repetitive px() retrieval and checking logic by wrapping property tolerances, offsets and base values into a config array of objects cfg. Each element in the array represents a config for pixel to be checked. Here is the structure for it.
var cfg = [
{ // pixel 1
tol: {a: 5, lum: [-30,20]},
},
{ // pixel 2
off: [1,0], // offset of pixel to grab
tol: {r: [-5,0]}, // tolerances to check
ref: {r: 80}, // reference props
},
];
var chkr = new pxChkr(cfg, pxy);
pxy.scanDn(function(steps) {
return chkr.chk();
});
Let's dissect what's happening here. We're asking the pxChkr instance to check 2 pixels every time .chk() is called. The first pixel defaults to offset [0,0] (the current pixel) and because ref is unspecified, reference properties are taken from that offset at the time of instantiation. tol provides the properties to be checked and the necessary upper and lower bounds relative to the reference which will be deemed acceptable; they can be provided as an array for non-uniform or a value for uniform bounds. The second pixel is a bit more explicit.
Here is what the code would look like for an equivalent check sequence.
// get ref pixels
var refA = pxy.px(0,0),
refB = {r: 80};
pxy.scanDn(function(steps) {
// get current pixels
var pxA = this.px(0,0),
pxB = this.px(1,0);
if (
(pxA.a < refA.a - 5 || pxA.a > refA + 5) ||
(pxA.lum() < refA.lum() - 30 || pxA.lum() > refA.lum() + 20) ||
(pxB.r < refB.r - 5 || pxB.r > refB.r + 0)
) return false;
});
So about the same number of lines but severely reduced readability. Actually also reduced performance, since the tolerance ranges are not pre-computed before the loop and the OR logic is unoptimized.
.cfg - computed and referenced, initialized config.pxs - retrieved pixels.res - detailed results of .chk()While .chk() returns true/false, .res gives you detailed insight into the results of the checks for each pixel and property so you can determine the cause of a false return. .pxs is the retrieved pixel array which is being compared. Feel free to inspect the structure of these in the console with this example.
pxy.moveTo(104,60);
var cfg = [
{tol: {lum: 0}},
{tol: {a: 0}, off: [-40,0]},
];
var chkr = new pxChkr(cfg,pxy);
pxy.scanDn(function(steps) {
var chk = chkr.chk();
if (!chk) {
console.log(chkr.res);
console.log(chkr.pxs);
}
return chk;
});
Here we're terminating the scan if the luminance of the current pixel changes or if the transparency of the pixel 40px to the left changes. So we end up hitting the semi-transparent area before reaching the end of the vertical line segment. Something similar but using adjacent pixels can be employed to create conditional edge followers.
* The x/y pairs, axis labels and checkerboard background are not part of the image, they're shown here to denote the image extents and areas of full and partial transparency.