fullscreen // Author: "Golan Levin" <golan@flong.com>, 01 August, 2011
//================================================================
class RowInfo {
int x0;
int x1;
int y;
}
//================================================================
void bridgeCorners (color[] bufferToProcess, int bufW, int bufH) {
// thicken the corners of grid units under certain geometric conditions.
int N = CORNER_THICKENING;
if (N > 0) {
color cornerColor = white;
if (DO_WHITE_PAINT_STENCIL){
cornerColor = black;
}
for (int y=N; y<(bufH-N); y++) {
for (int x=N; x<(bufW-N); x++) {
int indexC = y*bufW + x;
int indexE = indexC + 1;
int indexS = indexC + bufW;
int indexSE = indexS + 1;
color colorC = bufferToProcess [indexC];
color colorE = bufferToProcess [indexE];
color colorS = bufferToProcess [indexS];
color colorSE = bufferToProcess [indexSE];
if (((colorC == black)
&& (colorE == white)
&& (colorS == white)
&& (colorSE == black)) ||
((colorC == white)
&& (colorE == black)
&& (colorS == black)
&& (colorSE == white))) {
//------------------------------------------
for (int i=0; i<=N; i++) {
int Cx = indexC % bufW;
int Cy = indexC / bufW;
int Ex = indexE % bufW;
int Ey = indexE / bufW;
int Sx = indexS % bufW;
int Sy = indexS / bufW;
int SEx = indexSE % bufW;
int SEy = indexSE / bufW;
boolean bUseObsoleteRoundedCorners = false;
if (bUseObsoleteRoundedCorners) {
// legacy code block; remove or don't use.
for (int j=0; j<=N; j++) {
int dx = N - i;
int dy = N - j;
float dh = sqrt(dx*dx + dy*dy);
if (dh >= N) {
int indexCc = ((Cy-j) * bufW) + (Cx-i);
int indexEc = ((Ey-j) * bufW) + (Ex+i);
int indexSc = ((Sy+j) * bufW) + (Sx-i);
int indexSEc = ((SEy+j) * bufW) + (SEx+i);
bufferToProcess [indexCc] = cornerColor;
bufferToProcess [indexEc] = cornerColor;
bufferToProcess [indexSc] = cornerColor;
bufferToProcess [indexSEc] = cornerColor;
}
}
}
else {
for (int j=0; j<i; j++) {
int Cxc = Cx - (N-i);
int Cyc = Cy - j;
int indexCc = (Cyc * bufW) + Cxc;
bufferToProcess [indexCc] = cornerColor;
int Exc = Ex + (N-i);
int Eyc = Ey - j;
int indexEc = (Eyc * bufW) + Exc;
bufferToProcess [indexEc] = cornerColor;
int Sxc = Sx - (N-i);
int Syc = Sy + j;
int indexSc = (Syc * bufW) + Sxc;
bufferToProcess [indexSc] = cornerColor;
int SExc = SEx + (N-i);
int SEyc = SEy + j;
int indexSEc = (SEyc * bufW) + SExc;
bufferToProcess [indexSEc] = cornerColor;
}
}
}
}
}
}
}
}
//===============================================================
void bridgeIslands (color[] inputBuffer, int inputW, int inputH) {
// Dispatches the advanced or the simple bridging routine.
// SIMPLE: just one or two bridges on the top edge of islands. Produces fragile stencils.
// ADVANCED: produces multiple, quasi-optimal bridges for each island, on multiple sides.
if (DO_ADVANCED_BRIDGING) {
bridgeIslandsAdvanced (inputBuffer, inputW, inputH);
}
else {
bridgeIslandsSimple (inputBuffer, inputW, inputH);
}
}
//===============================================================
void bridgeIslandsSimple (color[] inputBuffer, int inputW, int inputH) {
// inputBuffer is an array of colors(i.e. ints), which
// are a 'pure' black-and-white version of the QR code.
int gridSize = computeGridSize (inputBuffer, inputW, inputH); // see Utils.pde
int ccLabelColors[] = CCL.getLabelColors();
int nLabelColors = ccLabelColors.length;
while (nLabelColors > 2) {
//---------------------
// Find out where to build the bridge(s).
// Get the first color other than black or white.
// (Assuming white and black are colors 0 and 1.)
color firstLabelCol = ccLabelColors[2];
RowInfo responseTop = getRowInfoOfTheTopRowOfABlobWithACertainColor (firstLabelCol, coloredLabeledImage, inputW, inputH);
int topLeftXValue = responseTop.x0;
int topRightXValue = responseTop.x1;
int topRowYValue = responseTop.y;
//----------
// If I have found the row of that color, then build the bridges.
if ((topLeftXValue > -1) && (topRightXValue > -1)) {
int bridgeBottomY = topRowYValue;
if (((topRightXValue - topLeftXValue)/gridSize) > 2) {
buildABridge (topLeftXValue, bridgeBottomY, gridSize, DIR_UP, gridSize, inputBuffer, inputW, inputH);
int bridgeWidth = getBridgeWidthFromGridSize (gridSize);
int rightHandBridgeX = topRightXValue - (bridgeWidth-1);
buildABridge (rightHandBridgeX, bridgeBottomY, gridSize, DIR_UP, gridSize, inputBuffer, inputW, inputH);
}
else {
int centerX = (topLeftXValue + topRightXValue)/2;
buildABridge (centerX, bridgeBottomY, gridSize, DIR_UP, gridSize, inputBuffer, inputW, inputH);
}
}
//----------
// re-compute the connected components now that the bridge exists.
coloredLabeledImage = CCL.doLabel( inputBuffer, inputW, inputH);
//----------
// re-extract the number of current labels.
// presumably, because we built a bridge, it's one less than it was before.
ccLabelColors = CCL.getLabelColors();
nLabelColors = ccLabelColors.length;
}
}
//===============================================================
void bridgeIslandsAdvanced (color[] inputBuffer, int inputW, int inputH) {
int gridSize = computeGridSize (inputBuffer, inputW, inputH); // see Utils.pde
int ccLabelColors[] = CCL.getLabelColors();
int nLabelColors = ccLabelColors.length;
//while (nLabelColors > 2) {
for (int LC=2; LC<nLabelColors; LC++) {
//---------------------
// Find out where to build the bridge(s); get the first color other than black or white.
// firstLabelCol = ccLabelColors[2];
color firstLabelCol = ccLabelColors[LC];
//---------------------------------------------------------
// Find the extreme locations of the blob with that color.
// Note that e.g. indexA might be the same as indexE, etc.
final int UNDEFINED = -1;
int indexA = UNDEFINED; // (A) the first pixel of the top row; top's left
int indexB = UNDEFINED; // (B) the last pixel of the top row; top's right
int indexC = UNDEFINED; // (C) the first pixel of the bottom row; bottom's left
int indexD = UNDEFINED; // (D) the last pixel of the bottom row; bottom's right
int indexE = UNDEFINED; // (E) the top pixel of leftmost column; left's top
int indexF = UNDEFINED; // (F) the bot pixel of leftmost column; left's bottom
int indexG = UNDEFINED; // (G) the top pixel of rightmost column; right's top
int indexH = UNDEFINED; // (H) the bot pixel of rightmost column; right's bottom
int topRowY = UNDEFINED;
int bottomRowY = UNDEFINED;
int leftRowX = UNDEFINED;
int rightRowX = UNDEFINED;
// A---B
// | |
// E--- ---G
// | |
// F--- ---H
// | |
// C---D
//
// find indexA and indexB
for (int y = 0; y < inputH; y++) {
for (int x = 0; x < inputW; x++) {
int index = y*inputW + x;
color someCol = coloredLabeledImage[index];
if ((someCol == firstLabelCol) && (indexA == UNDEFINED)) {
indexA = index;
topRowY = y;
}
if ((someCol == firstLabelCol) && (y == topRowY)) {
indexB = index;
}
}
}
// find indexC and indexD
for (int y = (inputH-1); y >= 0; y--) {
for (int x = (inputW-1); x >= 0; x--) {
int index = y*inputW + x;
color someCol = coloredLabeledImage[index];
if ((someCol == firstLabelCol) && (indexD == UNDEFINED)) {
indexD = index;
bottomRowY = y;
}
if ((someCol == firstLabelCol) && (y == bottomRowY)) {
indexC = index;
}
}
}
// find indexE and indexF
for (int x=0; x< inputW; x++) {
for (int y=0; y< inputH; y++) {
int index = y*inputW + x;
color someCol = coloredLabeledImage[index];
if ((someCol == firstLabelCol) && (indexE == UNDEFINED)) {
indexE = index;
leftRowX = x;
}
if ((someCol == firstLabelCol) && (x == leftRowX)) {
indexF = index;
}
}
}
// find indexG and indexH
for (int x=(inputW-1); x>=0; x--) {
for (int y=(inputH-1); y>=0; y--) {
int index = y*inputW + x;
color someCol = coloredLabeledImage[index];
if ((someCol == firstLabelCol) && (indexH == UNDEFINED)) {
indexH = index;
rightRowX = x;
}
if ((someCol == firstLabelCol) && (x == rightRowX)) {
indexG = index;
}
}
}
//---------------------------------------------------------
// for each of the locations (A-H), search in the appropriate direction
// until the next non-black pixel is encountered. Store that distance (dA, dB, dC, ...) in a Pier object
final int idA = 0;
final int idB = 1;
final int idC = 2;
final int idD = 3;
final int idE = 4;
final int idF = 5;
final int idG = 6;
final int idH = 7;
ArrayList<Pier> piers;
piers = new ArrayList<Pier>(); // Constructor requests: int id_, int index_, int bearing_, int distance_
piers.clear();
if (indexA != UNDEFINED) { // check UP and LEFT from A
piers.add (new Pier (idA, indexA, DIR_UP, getVDistanceToNearestNonBlackPixel (indexA, inputW, inputH, DIR_UP) ));
piers.add (new Pier (idA, indexA, DIR_LEFT, getHDistanceToNearestNonBlackPixel (indexA, inputW, inputH, DIR_LEFT ) ));
}
if (indexB != UNDEFINED) { // check UP and RIGHT from B
piers.add (new Pier (idB, indexB, DIR_UP, getVDistanceToNearestNonBlackPixel (indexB, inputW, inputH, DIR_UP) ));
piers.add (new Pier (idB, indexB, DIR_RIGHT, getHDistanceToNearestNonBlackPixel (indexB, inputW, inputH, DIR_RIGHT) ));
}
if (indexC != UNDEFINED) { // check DOWN and LEFT from C
piers.add (new Pier (idC, indexC, DIR_DOWN, getVDistanceToNearestNonBlackPixel (indexC, inputW, inputH, DIR_DOWN ) ));
piers.add (new Pier (idC, indexC, DIR_LEFT, getHDistanceToNearestNonBlackPixel (indexC, inputW, inputH, DIR_LEFT ) ));
}
if (indexD != UNDEFINED) { // check DOWN and RIGHT from D
piers.add (new Pier (idD, indexD, DIR_DOWN, getVDistanceToNearestNonBlackPixel (indexD, inputW, inputH, DIR_DOWN ) ));
piers.add (new Pier (idD, indexD, DIR_RIGHT, getHDistanceToNearestNonBlackPixel (indexD, inputW, inputH, DIR_RIGHT) ));
}
if ((indexE != UNDEFINED) && (indexE != indexA)) { // check UP and LEFT from E
piers.add (new Pier (idE, indexE, DIR_UP, getVDistanceToNearestNonBlackPixel (indexE, inputW, inputH, DIR_UP) ));
piers.add (new Pier (idE, indexE, DIR_LEFT, getHDistanceToNearestNonBlackPixel (indexE, inputW, inputH, DIR_LEFT ) ));
}
if ((indexG != UNDEFINED) && (indexG != indexB)) { // check UP and RIGHT from G
piers.add (new Pier (idG, indexG, DIR_UP, getVDistanceToNearestNonBlackPixel (indexG, inputW, inputH, DIR_UP) ));
piers.add (new Pier (idG, indexG, DIR_RIGHT, getHDistanceToNearestNonBlackPixel (indexG, inputW, inputH, DIR_RIGHT) ));
}
if ((indexF != UNDEFINED) && (indexF != indexC)) { // check DOWN and LEFT from F
piers.add (new Pier (idF, indexF, DIR_DOWN, getVDistanceToNearestNonBlackPixel (indexF, inputW, inputH, DIR_DOWN ) ));
piers.add (new Pier (idF, indexF, DIR_LEFT, getHDistanceToNearestNonBlackPixel (indexF, inputW, inputH, DIR_LEFT ) ));
}
if ((indexH != UNDEFINED) && (indexH != indexD)) { // check DOWN and RIGHT from H
piers.add (new Pier (idH, indexH, DIR_DOWN, getVDistanceToNearestNonBlackPixel (indexH, inputW, inputH, DIR_DOWN ) ));
piers.add (new Pier (idH, indexH, DIR_RIGHT, getHDistanceToNearestNonBlackPixel (indexH, inputW, inputH, DIR_RIGHT) ));
}
// Compute the area of the blob with that color.
// We'll use the area as a direct basis for deciding how many bridges to build.
int nPixelsInThatBlob = getAreaOfBlobWithACertainColor (firstLabelCol, coloredLabeledImage, inputW, inputH);
int nGridCellsInThatBlob = nPixelsInThatBlob/ (gridSize*gridSize);
int N_BRIDGES_TO_MAKE = 4; // default.
if (DO_ALL_COMPUTED_BRIDGES) {
N_BRIDGES_TO_MAKE = piers.size();
}
else {
N_BRIDGES_TO_MAKE = min(piers.size(), max(MIN_BRIDGES_PER_ISLAND, nGridCellsInThatBlob));
}
for (int Br=0; Br < N_BRIDGES_TO_MAKE; Br++) {
// Select (at least 2 of) the shortest distances (that are not on the same side), and build bridges there.
// We can build more bridges (as appropriate) depending on the area of the blob:
// extremely large blobs (whose areas contain many grid units) deserve more bridges.
// Sort the Piers by their length.
Collections.sort (piers, new Comparator<Pier>() {
public int compare(Pier e0, Pier e1) {
return ((Integer)(e0.distance)).compareTo((Integer)(e1.distance));
}
}
);
boolean bPrintPiers = false;
if (bPrintPiers) {
println("------------------");
for (int i=0; i<piers.size(); i++) {
piers.get(i).print();
}
}
//------------------------------------
// First: bridge the shortest pier:
// Get the length of the shortest pier. (Remember, we sorted piers, above.)
if (piers.size() > 0) { // safety check
int lengthOfShortestPier = piers.get(0).distance;
// (Discard degenerate piers (if any!) which are shorter than gridSize.)
if (lengthOfShortestPier < gridSize) {
int len = 9999999;
for (int i=0; i<piers.size(); i++) {
if (piers.get(i).distance >= gridSize) { // kosher
lengthOfShortestPier = min(lengthOfShortestPier, piers.get(i).distance);
}
}
}
Pier shortestPier = null;
int countOfPiersWithTheShortestLength = 0;
for (int i=0; i<piers.size(); i++) {
if (piers.get(i).distance == lengthOfShortestPier) {
countOfPiersWithTheShortestLength++;
shortestPier = piers.get(i);
}
}
if (countOfPiersWithTheShortestLength > 1) {
// This is rather ridiculous, but it's late.
// If there is more than one pier with the shortest length,
// Tally the total number of piers for each bearing (up, down, left, right) that has a pier with that length
// And select the pier (with that length) from the direction with the fewest piers.
// This leaves the maximum number of other possible piers for connecting later.
int nPiersU = 0; // UP
int nPiersD = 0; // DOWN
int nPiersL = 0; // LEFT
int nPiersR = 0; // RIGHT
boolean bHasShortestU = false;
boolean bHasShortestD = false;
boolean bHasShortestL = false;
boolean bHasShortestR = false;
for (int i=0; i<piers.size(); i++) {
int bearing = piers.get(i).bearing;
switch(bearing) {
case DIR_UP:
if (piers.get(i).distance == lengthOfShortestPier) {
bHasShortestU = true;
}
nPiersU++;
break;
case DIR_DOWN:
if (piers.get(i).distance == lengthOfShortestPier) {
bHasShortestD = true;
}
nPiersD++;
break;
case DIR_LEFT:
if (piers.get(i).distance == lengthOfShortestPier) {
bHasShortestL = true;
}
nPiersL++;
break;
case DIR_RIGHT:
if (piers.get(i).distance == lengthOfShortestPier) {
bHasShortestR = true;
}
nPiersR++;
break;
}
}
int nPiersInBearingsWithShortestPiers[] = new int[4];
nPiersInBearingsWithShortestPiers[DIR_UP] = (bHasShortestU) ? nPiersU : 0;
nPiersInBearingsWithShortestPiers[DIR_DOWN] = (bHasShortestD) ? nPiersD : 0;
nPiersInBearingsWithShortestPiers[DIR_LEFT] = (bHasShortestL) ? nPiersL : 0;
nPiersInBearingsWithShortestPiers[DIR_RIGHT] = (bHasShortestR) ? nPiersR : 0;
int bearingWithFewestPiersThatAlsoHasAShortestPier = UNDEFINED;
int minNumPiers = 999;
for (int i=0; i<4; i++) {
if (nPiersInBearingsWithShortestPiers[i] > 0) {
if (nPiersInBearingsWithShortestPiers[i] < minNumPiers) {
minNumPiers = nPiersInBearingsWithShortestPiers[i];
bearingWithFewestPiersThatAlsoHasAShortestPier = i;
}
}
}
// Therefore, search for the pier that (1) has the shortest length and (2) has the bearingWithFewestPiersThatAlsoHasAShortestPier.
for (int i=0; i<piers.size(); i++) {
int bearing = piers.get(i).bearing;
if (bearing == bearingWithFewestPiersThatAlsoHasAShortestPier) {
int len = piers.get(i).distance;
if (len == lengthOfShortestPier) {
shortestPier = piers.get(i);
}
}
}
// Now we have found the shortest pier, and from an underrepresented side, to boot!
}
else if (countOfPiersWithTheShortestLength == 1) {
; // Solo case. Just use shortestPier. We're good!
}
// Actually do the BRIDGING.
if (shortestPier != null) {
int bridgeIndex = shortestPier.index;
int bridgeX = bridgeIndex % inputW;
int bridgeY = bridgeIndex / inputW;
int bearing = shortestPier.bearing;
int distance = shortestPier.distance;
// Hey, why not do some bridge pixel HINTING!
int bridgeWidth = getBridgeWidthFromGridSize (gridSize);
int id = shortestPier.ID;
switch (id) {
case idA:
case idE:
; // should be good to go
break;
case idB:
case idG:
if (bearing == DIR_UP) {
bridgeX -= (bridgeWidth-1);
}
break;
case idC:
case idF:
if (bearing == DIR_LEFT) {
bridgeY -= (bridgeWidth-1);
}
break;
case idD:
case idH:
if (bearing == DIR_DOWN) {
bridgeX -= (bridgeWidth-1);
}
else if (bearing == DIR_RIGHT) {
bridgeY -= (bridgeWidth-1);
}
break;
}
buildABridge (bridgeX, bridgeY, gridSize, bearing, distance, inputBuffer, inputW, inputH);
}
// Remove shortestPier.
// Also, with some probability, remove all piers with the same bearing.
piers.remove(shortestPier);
if (DO_ALL_COMPUTED_BRIDGES == false) {
ArrayList<Pier> PiersToRemove = new ArrayList<Pier>();
for (int i=0; i<(piers.size()); i++) {
if (piers.get(i).bearing == shortestPier.bearing) {
// the higher BRIDGE_CULLING_FACTOR is closer to 1.0,
// the more likely to enforce only having single bridges per side.
if ((random(0, 1) < BRIDGE_CULLING_FACTOR)) { // !!!!!!!!!!!!!!!!! DITHER
PiersToRemove.add (piers.get(i));
}
}
}
for (int i=0; i<PiersToRemove.size(); i++) {
piers.remove (PiersToRemove.get(i));
}
}
}
} // repeat the adding of bridges!
/*
//----------
// re-compute the connected components now that the bridge exists, and re-extract the number of current labels.
coloredLabeledImage = CCL.doLabel( inputBuffer, inputW, inputH);
ccLabelColors = CCL.getLabelColors();
nLabelColors = ccLabelColors.length;
*/
}
}
//===============================================================
class Pier { // A starting point for a possible bridge!
//-------------
Pier (int id_, int index_, int bearing_, int distance_) {
ID = id_;
index = index_;
bearing = bearing_;
distance = distance_;
}
//-------------
void print() {
String dir = "";
switch(bearing) {
case DIR_UP:
dir = "UP";
break;
case DIR_DOWN:
dir = "DOWN";
break;
case DIR_LEFT:
dir = "LEFT";
break;
case DIR_RIGHT:
dir = "RIGHT";
break;
}
println("Pier: From point " + ID + "\t" + distance + "\t" + dir);
}
//-------------
int ID; // the "name" of the start point
int index; // the index (in the QR-image sized buffer) of the start point
int bearing; // which way the bridge would go from here
int distance; // how long the bridge would need to be
}
//===============================================================
int getVDistanceToNearestNonBlackPixel (int startIndex, int inputW, int inputH, int direction) {
int distance = 0;
if (startIndex > -1) {
int x = startIndex % inputW;
int y = startIndex / inputW;
if (direction == DIR_UP) {
y = y-1;
int testIndex = y*inputW + x;
color testColor = coloredLabeledImage[testIndex];
while ( (testColor == black) && (y > 0)) {
y = y-1;
testIndex = y*inputW + x;
testColor = coloredLabeledImage[testIndex];
}
distance = abs(y - (startIndex/inputW)) -1;
}
else if (direction == DIR_DOWN) {
y = y+1;
int testIndex = y*inputW + x;
color testColor = coloredLabeledImage[testIndex];
while ( (testColor == black) && (y < (inputH-1))) {
y = y+1;
testIndex = y*inputW + x;
testColor = coloredLabeledImage[testIndex];
}
distance = abs(y - (startIndex/inputW)) -1;
}
}
return distance;
}
//===============================================================
int getHDistanceToNearestNonBlackPixel (int startIndex, int inputW, int inputH, int direction) {
int distance = 0;
if (startIndex > -1) {
int x = startIndex % inputW;
int y = startIndex / inputW;
if (direction == DIR_LEFT) {
x = x-1; // to get started, on the pixel above
int testIndex = y*inputW + x;
color testColor = coloredLabeledImage[testIndex];
while ( (testColor == black) && (x > 0)) {
x = x-1;
testIndex = y*inputW + x;
testColor = coloredLabeledImage[testIndex];
}
distance = abs(x - (startIndex%inputW)) -1;
}
else if (direction == DIR_RIGHT) {
x = x+1; // to get started, on the pixel above
int testIndex = y*inputW + x;
color testColor = coloredLabeledImage[testIndex];
while ( (testColor == black) && (x < (inputW-1))) {
x = x+1;
testIndex = y*inputW + x;
testColor = coloredLabeledImage[testIndex];
}
distance = abs(x - (startIndex%inputW)) -1;
}
}
return distance;
}
//===============================================================
void buildABridge (int bridgeX, int bridgeY, int gridSize, int direction, int distance, color[] inputBuffer, int inputW, int inputH) {
int bridgeWidth = getBridgeWidthFromGridSize (gridSize);
if (direction == DIR_UP) {
// bridgeY is interpreted as the Y end value.
int yStart = bridgeY - distance - 1;
for (int y=bridgeY; y>=yStart; y--) {
for (int x=bridgeX; x<(bridgeX+bridgeWidth); x++) {
if ((y >= 0) && (x >= 0)) {
int index = y*inputW + x;
inputBuffer[index] = white;
}
}
}
}
else if (direction == DIR_DOWN) {
// bridgeY is interpreted as the Y start value.
int yEnd = bridgeY + distance + 1;
for (int y=bridgeY; y<yEnd; y++) {
for (int x=bridgeX; x<(bridgeX+bridgeWidth); x++) {
if ((y < inputH) && (x < inputW)) {
int index = y*inputW + x;
blackAndWhiteImage[index] = white;
}
}
}
}
else if (direction == DIR_LEFT) {
int xStart = bridgeX;
int xEnd = bridgeX - distance - 1;
for (int x=xStart; x >= xEnd; x--) {
for (int y=bridgeY; y<(bridgeY+bridgeWidth); y++) {
if ((y >= 0) && (x >= 0)) {
int index = y*inputW + x;
blackAndWhiteImage[index] = white;
}
}
}
}
else if (direction == DIR_RIGHT) {
int xStart = bridgeX;
int xEnd = bridgeX + distance + 1;
for (int x=xStart; x<= xEnd; x++) {
for (int y=bridgeY; y<(bridgeY+bridgeWidth); y++) {
if ((y < inputH) && (x < inputW)) {
int index = y*inputW + x;
blackAndWhiteImage[index] = white;
}
}
}
}
}
//===============================================================
int getBridgeWidthFromGridSize (int grs) {
int bridgeWidth = (int)(grs * BRIDGE_THICKNESS); // ratio: bridge is ~1/6 of gridSize
if ((bridgeWidth > 1) && (bridgeWidth %2 == 1)) {
bridgeWidth--;
}
bridgeWidth = max(1, bridgeWidth);
return bridgeWidth;
}
//===============================================================
RowInfo getRowInfoOfTheTopRowOfABlobWithACertainColor (color firstLabelCol, color[] coloredBuffer, int inputW, int inputH) {
RowInfo response = new RowInfo();
response.x0 = -1;
response.x1 = -1;
response.y = -1;
// find the indexes of the first & last pixels in the top row of the blob with that color.
int topLeftIndex = -1; // this will hold the index of the first pixel to contain that color.
int topRightIndex = -1; // this will hold the index of the last pixel to contain that color, from the same row.
int topRowYValue = -1;
int topLeftXValue = -1;
int topRightXValue = -1;
boolean bFoundTopLeft = false;
for (int y = 0; y < inputH; y++) {
for (int x = 0; x < inputW; x++) {
int index = y*inputW + x;
color someCol = coloredBuffer[index];
if ((someCol == firstLabelCol) && (bFoundTopLeft == false)) {
topLeftIndex = index;
bFoundTopLeft = true;
topLeftXValue = x;
topRowYValue = y;
}
if ((y == topRowYValue) && (someCol == firstLabelCol)) {
topRightIndex = index;
topRightXValue = x;
}
}
}
response.x0 = topLeftXValue;
response.x1 = topRightXValue;
response.y = topRowYValue;
return response;
}
//===============================================================
int getAreaOfBlobWithACertainColor (color testColor, color[] colorBuffer, int inputW, int inputH) {
int pixelCount = 0;
for (int y=0; y<inputH; y++) {
for (int x=0; x<inputW; x++) {
int index = y*inputW + x;
if (colorBuffer[index] == testColor) {
pixelCount++;
}
}
}
return pixelCount;
}
// Author: "Golan Levin" <golan@flong.com>, 01 August, 2011
//================================================================
class ConnectedComponentLabeler {
// Adapted from:
// http://courses.csail.mit.edu/6.141/spring2010/pub/labs/VisualServo/src/ConnectedComponents.java
// ConnectedComponentLabeler is an algorithm that applies Connected Component Labeling
// algorithm to an input image. Only mono images are catered for.
// author: Neil Brown, DAI
// author: Judy Robertson, SELLIC OnLine
//----------------------------------------------------------------------
//the width of the input image in pixels
int i1_w;
//the width and height of the output image
int d_w;
int d_h;
int numberOfLabels;
boolean labelsValid = false;
int dest_1d[];
int labels[];
int labelColors[];
//----------------------------------------------------------------------
// Constructs a new Image Operator
// param firstwidth The width of the input image
// param firstheight The height of the input image
ConnectedComponentLabeler( int firstwidth, int firstheight ) {
labels = new int[firstwidth * firstheight];
i1_w = firstwidth;
}
//----------------------------------------------------------------------
// getNeighbors will get the pixel value of i's neighbor that's ox and oy
// away from i, if the point is outside the image, then 0 is returned.
// This version gets from source image.
int getNeighbors( int [] src1d, int i, int ox, int oy ) {
int x, y, result;
x = ( i % d_w ) + ox; // d_w and d_h are assumed to be set to the
y = ( i / d_w ) + oy; // width and height of scr1d
if ( ( x < 0 ) || ( x >= d_w ) || ( y < 0 ) || ( y >= d_h ) ) {
result = 0;
}
else {
result = src1d[ y * d_w + x ] & 0x000000ff;
}
return result;
}
//----------------------------------------------------------------------
// getNeighbord will get the pixel value of i's neighbor that's ox and oy
// away from i, if the point is outside the image, then 0 is returned.
// This version gets from destination image.
int getNeighbord( int [] src1d, int i, int ox, int oy ) {
int x, y, result;
x = ( i % d_w ) + ox; // d_w and d_h are assumed to be set to the
y = ( i / d_w ) + oy; // width and height of scr1d
if ( (x < 0) || (x >= d_w) || (y < 0) || (y >= d_h) ) {
result = 0;
}
else {
result = src1d[ y * d_w + x ];
}
return result;
}
//----------------------------------------------------------------------
// Associate(equivalence) a with b.
// a should be less than b to give some ordering (sorting)
// if b is already associated with some other value, then propagate
// down the list.
void associate( int a, int b ) {
if ( a > b ) {
associate( b, a );
return;
}
if ( ( a == b ) || ( labels[ b ] == a ) ) return;
if ( labels[ b ] == b ) {
labels[ b ] = a;
}
else {
associate ( labels[ b ], a );
if (labels[ b ] > a) {
labels[ b ] = a;
}
}
}
//----------------------------------------------------------------------
// Reduces the number of labels.
int reduce( int a ) {
if ( labels[ a ] == a ) {
return a;
}
else {
return reduce( labels[ a ] );
}
}
//----------------------------------------------------------------------
// doLabel applies the Labeling algorithm plus offset and scaling
// The input image is expected to be 8-bit mono, 0=black everything else=white
// param src1_1d The input pixel array
// param width width of the destination image in pixels
// param height height of the destination image in pixels
// return A pixel array containing the labelled image
// NB For images 0,0 is the top left corner.
int [] doLabel(int [] src1_1d, int wid, int hig) {
int nextlabel = 1;
int nbs[] = new int[ 4 ];
int nbls[] = new int[ 4 ];
//Get size of image and make 1d_arrays
d_w = wid;
d_h = hig;
dest_1d = new int[d_w*d_h];
labels = new int[d_w*d_h / 2]; // the most labels there can be is 1/2 of the points in checkerboard
int src1rgb;
int result = 0;
int px, py, count, found;
labelsValid = false; // only set to true once we've complete the task
//initialise labels
for (int i=0; i<labels.length; i++) labels[ i ] = i;
//now Label the image
for (int i=0; i< src1_1d. length; i++) {
src1rgb = src1_1d[ i ] & 0x000000ff;
if ( src1rgb == 0 ) {
result = 0; //nothing here
}
else {
//The 4 visited neighbors
nbs[ 0 ] = getNeighbors( src1_1d, i, -1, 0 );
nbs[ 1 ] = getNeighbors( src1_1d, i, 0, -1 );
nbs[ 2 ] = getNeighbors( src1_1d, i, -1, -1 );
nbs[ 3 ] = getNeighbors( src1_1d, i, 1, -1 );
//Their corresponding labels
nbls[ 0 ] = getNeighbord( dest_1d, i, -1, 0 );
nbls[ 1 ] = getNeighbord( dest_1d, i, 0, -1 );
nbls[ 2 ] = getNeighbord( dest_1d, i, -1, -1 );
nbls[ 3 ] = getNeighbord( dest_1d, i, 1, -1 );
//label the point
if ( (nbs[0] == nbs[1]) && (nbs[1] == nbs[2]) && (nbs[2] == nbs[3])
&& (nbs[0] == 0 )) {
// all neighbors are 0 so gives this point a new label
result = nextlabel;
nextlabel++;
}
else { //one or more neighbors have already got labels
count = 0;
found = -1;
for ( int j=0; j<4; j++) {
if ( nbs[ j ] != 0 ) {
count +=1;
found = j;
}
}
if ( count == 1 ) {
// only one neighbor has a label, so assign the same label to this.
result = nbls[ found ];
}
else {
// more than 1 neighbor has a label
result = nbls[ found ];
// Equivalence the connected points
for ( int j=0; j<4; j++) {
if ( ( nbls[ j ] != 0 ) && (nbls[ j ] != result ) ) {
associate( nbls[ j ], result );
}
}
}
}
}
dest_1d[i] = result;
}
// reduce labels ie 76=23=22=3 -> 76=3
// done in reverse order to preserve sorting
for ( int i= labels.length -1; i > 0; i-- ) {
labels[ i ] = reduce( i );
}
// now labels will look something like 1=1 2=2 3=2 4=2 5=5.. 76=5 77=5
// this needs to be condensed down again, so that there is no wasted
// space. E.g. in the above, the labels 3 and 4 are not used, instead it jumps to 5
int condensed[] = new int[ nextlabel ]; // cant be more than nextlabel labels
count = 0;
for (int i=0; i< nextlabel; i++) {
if ( i == labels[ i ] ) condensed[ i ] = count++;
}
// Record the number of labels
numberOfLabels = count - 1;
// now run back through our preliminary results, replacing the raw label
// with the reduced and condensed one, and do the scaling and offsets too
// Now generate an array of colours which will be used to label the image
labelColors = new int[numberOfLabels+1];
// The likelihood that two colors would be the same... is very small.
for (int i = 0; i < labelColors.length; i++) {
int rr = (int) random(1, 254);
int rg = (int) random(1, 254);
int rb = (int) random(1, 254);
color randomColor = color(rr, rg, rb, 255);
labelColors[i] = randomColor;
if (i == 0) labelColors[i] = black;
if (i == 1) labelColors[i] = white;
}
for (int i=0; i< src1_1d. length; i++) {
result = condensed[ labels[ dest_1d[ i ] ] ];
dest_1d[i] = labelColors[result];
}
labelsValid = true; // only set to true now we've complete the task
return dest_1d;
}
//----------------------------------------------------------------------
//return the number of unique, non zero colours. -1 if not valid
int getColors() {
if ( labelsValid ) {
return numberOfLabels;
}
else {
return -1;
}
}
//----------------------------------------------------------------------
//Returns the number of labels.
int getNumberOfLabels() {
return numberOfLabels;
}
//----------------------------------------------------------------------
int[] getLabelColors() {
return labelColors;
}
}
// Author: "Golan Levin" <golan@flong.com>, 01 August, 2011
//================================================================
class Point2d {
int x, y;
Point2d (int inx, int iny) {
x = inx;
y = iny;
}
}
//===============================================
class ContourTracer {
Point2d firstPixel;
ArrayList<ArrayList> chains;
ArrayList colorLabels;
ContourTracer () {
chains = new ArrayList<ArrayList>(); // a collection of contours.
colorLabels = new ArrayList();
firstPixel = new Point2d(0, 0);
}
//===============================================================
void traceBlobContours (color[] coloredBlobBuffer, int bufW, int bufH) {
int nLabels = compileLabelColors ( coloredBlobBuffer, bufW, bufH);
chains.clear();
for (int i=0; i<nLabels; i++) {
findBlobStartPixel(i, coloredBlobBuffer, bufW, bufH);
compute8NeighborChainCode (firstPixel.x, firstPixel.y, coloredBlobBuffer, bufW, bufH);
repairLeftTurningInteriorCorners();
}
}
//===============================================
void drawAllChains() {
for (int n=0; n<chains.size(); n++) {
ArrayList aContour = (ArrayList)chains.get(n);
beginShape();
for (int i=0; i<aContour.size(); i++) {
Point2d P = (Point2d) aContour.get(i);
vertex(P.x, P.y);
}
endShape(CLOSE);
}
}
//===============================================
void drawAllChainsWithFilletedCorners() {
for (int n=0; n<chains.size(); n++) {
ArrayList aContour = (ArrayList)chains.get(n);
if (aContour.size() > 0) {
float px0, py0;
float px1, py1;
float px2, py2;
Point2d P0 = (Point2d) aContour.get(0);
px2 = px1 = px0 = P0.x;
py2 = py1 = py0 = P0.y;
int orientationPrev = -1;
int orientationCurr = -1;
int indexOfPrevAppendedPoint = -1;
beginShape();
boolean pPrevWasDiagonal = true;
int nPointsInContour = aContour.size();
for (int i=0; i<(2+nPointsInContour); i++) {
int index = i%nPointsInContour;
Point2d P2 = (Point2d) aContour.get(index);
px2 = P2.x;
py2 = P2.y;
if ((px2 != px1) && (py2 != py1)) {
// if this (P2) point's x and y are BOTH different from the previous points',
// then this point is DIAGONAL from the previous one. Don't include it, but store it as a start point (p0).
if (pPrevWasDiagonal == false) {
pPrevWasDiagonal = true;
// if we have just begun a diagonal span, make a note of the first point, P0.
px0 = px1;
py0 = py1;
}
}
else {
// Since this (P2's) x and y are (horizontally or vertically) aligned with the previous,
// then -- if we had been tracing a diagonal -- the previous point was the last in the diagonal.
// Add it (p1) to the output chain/drawing, and also the new one (p2) as well.
if (pPrevWasDiagonal) {
pPrevWasDiagonal = false;
orientationPrev = -1;
orientationCurr = -1;
// NOTE: we'll need to detect whether it was an interior corner.
// check whether p00 (before 0) is left or down from p0
// Add the bezier corner curve. For more details on the math, see:
// http://www.cgafaq.info/wiki/B%C3%A9zier_circle_approximation
// http://www.tinaja.com/glib/ellipse4.pdf
vertex(px0, py0);
float dx = px1 - px0;
float dy = py1 - py0;
float r = dx; // dx and dy are the same -- but the sign matters.
float d = r * 4.0*(sqrt(2.0) - 1.0)/3.0; //i.e. 0.55228475
//---------------
if (((px1 > px0) && (py1 < py0)) || ((px1 < px0) && (py1 > py0))) {
if (USE_BEZIER_NOT_ARCS) {
float cxa = px0 ;
float cya = py0 - d;
float cxb = px1 - d;
float cyb = py1 ;
bezierVertex(cxa, cya, cxb, cyb, px1, py1);
}
else {
for (int t=1; t<=ARC_RESOLUTION; t++) {
float angle = PI + HALF_PI * (float)t/ (float)ARC_RESOLUTION;
float cx = px1 + r * cos(angle);
float cy = py0 + r * sin(angle);
vertex(cx, cy);
}
}
}
//---------------
else if (((px1 < px0) && (py1 < py0)) || ((px1 > px0) && (py1 > py0))) {
if (USE_BEZIER_NOT_ARCS) {
float cxa = px0 + d;
float cya = py0 ;
float cxb = px1 ;
float cyb = py1 - d;
bezierVertex(cxa, cya, cxb, cyb, px1, py1);
}
else {
for (int t=1; t<=ARC_RESOLUTION; t++) {
float angle = HALF_PI + HALF_PI * (float)t/ (float)ARC_RESOLUTION;
float cx = px0 - r * cos(angle);
float cy = py1 - r * sin(angle);
vertex(cx, cy);
}
}
}
else {
if (i > 0) {
// no good reason why this would be the case, but...
vertex(px1, py1);
}
}
indexOfPrevAppendedPoint = index;
}
else if (!pPrevWasDiagonal) {
// Point p2 is aligned (horizontally or vertically) with the previous point, p1.
// We only append p1 if the orientation has changed (i.e., turned a corner).
if ((px2 > px1) && (py2 == py1)) {
orientationCurr = DIR_RIGHT;
}
else if ((px2 < px1) && (py2 == py1)) {
orientationCurr = DIR_LEFT;
}
else if ((px2 == px1) && (py2 > py1)) {
orientationCurr = DIR_DOWN;
}
else if ((px2 == px1) && (py2 < py1)) {
orientationCurr = DIR_UP;
}
else {
// WTF; not sure how this would happen.
}
if (orientationCurr != orientationPrev) { // if there has been a change in orientation
if (true) { //index != (indexOfPrevAppendedPoint +1)) { // if it's not just the next point
if (i < nPointsInContour) { // prevent extra looparoundie
vertex(px1, py1);
indexOfPrevAppendedPoint = index;
}
}
}
orientationPrev = orientationCurr;
}
}
px1 = px2;
py1 = py2;
}
endShape(CLOSE);
}
}
}
//===============================================
int compileLabelColors(int buffer[], int bufW, int bufH) {
colorLabels.clear();
color black = color(0, 0, 0);
for (int y=0; y<bufH; y++) {
for (int x=0; x<bufW; x++) {
color val = buffer[y*bufW + x];
if (val != black) {
if (colorLabels.size() == 0) {
colorLabels.add(val);
}
else {
boolean bFound = false;
for (int i=0; i<colorLabels.size(); i++) {
color col = (color)(((Integer)(colorLabels.get(i))).intValue()); // yech
if (val == col) {
bFound = true;
}
}
if (bFound == false) {
colorLabels.add(val);
}
}
}
}
}
return colorLabels.size();
}
//===============================================
void findBlobStartPixel (int whichLabel, int[] buffer, int bufW, int bufH) {
color searchColor = (color)(((Integer)(colorLabels.get(whichLabel))).intValue());
boolean foundBlobStartPixel = false;
color black = color(0, 0, 0);
for (int y=0; y<bufH; y++) {
for (int x=0; x<bufW; x++) {
color val = buffer[y*bufW + x];
if (!foundBlobStartPixel && (val == searchColor)) {
firstPixel.x = x;
firstPixel.y = y;
foundBlobStartPixel = true;
}
}
}
}
//===============================================
boolean isPixelLocationLegal (int x, int y, int bufW, int bufH) {
if (x < 0 || x >= bufW) return false;
if (y < 0 || y >= bufH) return false;
return true;
}
//===============================================
/* Compute the chain code of the object beginning at pixel (i,j).
Return the code as NN integers in the array C. */
void compute8NeighborChainCode (int i, int j, int[] buffer, int bufW, int bufH) {
int val, m, q, r, ii, d, dii;
int lastdir, jj;
ArrayList aContour = new ArrayList<Point2d>();
aContour.clear();
// Table given index offset for each of the 8 directions.
int di[] = {
0, -1, -1, -1, 0, 1, 1, 1
};
int dj[] = {
1, 1, 0, -1, -1, -1, 0, 1
};
val = buffer[j*bufW+i];
q = i;
r = j;
lastdir = 4;
do {
m = 0;
dii = -1;
d = 100;
for (ii=lastdir+1; ii<lastdir+8; ii++) { /* Look for next */
jj = ii%8;
if (isPixelLocationLegal (di[jj]+q, dj[jj]+r, bufW, bufH)) {
if ( buffer[(dj[jj]+r)*bufW + (di[jj]+q)] == val) {
dii = jj;
m = 1;
break;
}
}
}
if (m != 0) { /* Found the next pixel ... */
Point2d P = new Point2d(q, r);
aContour.add(P);
q += di[dii];
r += dj[dii];
lastdir = (dii+5)%8;
}
else {
break; /* NO next pixel */
}
}
while ( (q!=i) || (r!=j) ); /* Stop when next to start pixel */
chains.add(aContour);
}
//===============================================
void repairLeftTurningInteriorCorners () {
// left-turning interior corners were clipped by one pixel. Fix 'em.
for (int n=0; n<chains.size(); n++) {
ArrayList aContour = (ArrayList)chains.get(n);
for (int i=1; i<(aContour.size()-1); i++) {
Point2d P0 = (Point2d) aContour.get(i-1);
Point2d P1 = (Point2d) aContour.get(i );
Point2d P2 = (Point2d) aContour.get(i+1);
if ((P1.x == P0.x) && (P1.y < P0.y) && (P2.x < P1.x) && (P2.y < P1.y)) {
Point2d P = new Point2d(P1.x, P2.y);
aContour.add(i+1, P);
}
else if ((P1.x > P0.x) && (P1.y == P0.y) && (P2.x > P1.x) && (P2.y < P1.y)) {
Point2d P = new Point2d(P2.x, P1.y);
aContour.add(i+1, P);
}
else if ((P1.x < P0.x) && (P1.y == P0.y) && (P2.x < P1.x) && (P2.y > P1.y)) {
Point2d P = new Point2d(P2.x, P1.y);
aContour.add(i+1, P);
}
else if ((P1.x == P0.x) && (P1.y > P0.y) && (P2.x > P1.x) && (P2.y > P1.y)) {
Point2d P = new Point2d(P1.x, P2.y);
aContour.add(i+1, P);
}
}
}
}
// end ContourTracer class
}
// Author: "Golan Levin" <golan@flong.com>, 01 August, 2011
//================================================================
String getUserSelectedQRCodeImageFilename () {
// Ask the user to load a QR code image file.
// These commands to open a file chooser are taken from:
// http://processing.org/discourse/yabb2/YaBB.pl?board=Syntax;action=display;num=1210972905
println("Please select a QR code image file.");
QRImageFilename = QRDefaultImageFilename;
JFileChooser chooser = new JFileChooser();
boolean bFailed = false;
try
{
File dataDir = new File(sketchPath, "data/");
if (!dataDir.exists()) {
dataDir.mkdirs();
}
String fileExtensions[] = {"gif", "jpg", "jpeg", "png", "tiff", "tif"};
ImageFileFilter IFF = new ImageFileFilter (fileExtensions, "Images");
chooser.setAcceptAllFileFilterUsed (true);
chooser.addChoosableFileFilter(IFF);
chooser.setFileFilter(IFF);
chooser.setDialogTitle("Please select a QR code image!");
chooser.setApproveButtonText("Load QR Image");
chooser.setApproveButtonToolTipText ("Load the selected QR code. This should be a png, jpg, gif or tif.");
chooser.setCurrentDirectory(dataDir);
int returnVal = chooser.showOpenDialog(null);
if (returnVal == JFileChooser.APPROVE_OPTION) {
String tmpString = chooser.getSelectedFile().getName();
String ext = tmpString.substring(tmpString.lastIndexOf('.') + 1);
ext.toLowerCase();
if (ext.equals("jpg") || ext.equals("jpeg") || ext.equals("gif") || ext.equals("png")) {
//QRImageFilename = chooser.getSelectedFile().getName();
QRImageFilename = chooser.getSelectedFile().getPath();
println("You chose to open this file:\n " + QRImageFilename);
println();
}
else {
bFailed = true;
}
}
else {
bFailed = true;
}
}
catch(Exception e) {
e.printStackTrace();
}
if (bFailed) {
println("Loading default QR code image: " + QRImageFilename);
}
return QRImageFilename;
}
//===============================================================
// adapted from http://www.student.nada.kth.se/~u1eetop7/prost/ExampleFileFilter.java
class ImageFileFilter extends FileFilter {
private String TYPE_UNKNOWN = "Type Unknown";
private String HIDDEN_FILE = "Hidden File";
private Hashtable filters = null;
private String description = null;
private String fullDescription = null;
private boolean useExtensionsInDescription = true;
//----------------------------------------------------------------
// Example: new ImageFileFilter (String {"gif", "jpg"}, "Gif and JPG Images");
// "gif", "jpg", "jpeg", "png", "tiff", "tif"
public ImageFileFilter(String[] filters, String description) {
this.filters = new Hashtable();
for (int i = 0; i < filters.length; i++) {
addExtension(filters[i]);
}
if (description!=null) {
setDescription(description);
}
}
//----------------------------------------------------------------
public void addExtension(String extension) {
if (filters == null) {
filters = new Hashtable(5);
}
filters.put(extension.toLowerCase(), this);
fullDescription = null;
}
//----------------------------------------------------------------
public void setDescription(String description) {
this.description = description;
fullDescription = null;
}
//----------------------------------------------------------------
// Returns the human readable description of this filter. For
// example: "JPEG and GIF Image Files (*.jpg, *.gif)"
public String getDescription() {
if (fullDescription == null) {
if (description == null || isExtensionListInDescription()) {
fullDescription = description==null ? "(" : description + " (";
// build the description from the extension list
Enumeration extensions = filters.keys();
if (extensions != null) {
fullDescription += "." + (String) extensions.nextElement();
while (extensions.hasMoreElements ()) {
fullDescription += ", " + (String) extensions.nextElement();
}
}
fullDescription += ")";
}
else {
fullDescription = description;
}
}
return fullDescription;
}
//----------------------------------------------------------------
public boolean accept(File f) {
if (f != null) {
if (f.isDirectory()) {
return true;
}
String extension = getExtension(f);
if (extension != null && filters.get(getExtension(f)) != null) {
return true;
};
}
return false;
}
//----------------------------------------------------------------
public boolean isExtensionListInDescription() {
return useExtensionsInDescription;
}
//----------------------------------------------------------------
public String getExtension(File f) {
if (f != null) {
String filename = f.getName();
int i = filename.lastIndexOf('.');
if (i>0 && i<filename.length()-1) {
return filename.substring(i+1).toLowerCase();
};
}
return null;
}
//----------------------------------------------------------------
public void setExtensionListInDescription(boolean b) {
useExtensionsInDescription = b;
fullDescription = null;
}
}
//===============================================================
void setupGUI() {
controlP5 = new ControlP5(this);
myTextlabelA = controlP5.addTextlabel("label", "QR_STENCILER BY F.A.T. LAB ", 20, 50);
bang1 = controlP5.addBang("recomputeStencil", 20, 80, 20, 20);
bang1.setTriggerEvent(Bang.RELEASE);
bang1.setLabel("RE-COMPUTE STENCIL");
if (!DISABLE_PDF_EXPORT_FOR_APPLET) {
bang2 = controlP5.addBang("openPDFInAcrobat", 20, 350, 20, 20);
bang2.setTriggerEvent(Bang.RELEASE);
bang2.setLabel("OPEN PDF IN ACROBAT");
}
checkbox = controlP5.addCheckBox("checkBox", 20, 130);
checkbox.setColorForeground(color(120));
checkbox.setColorActive(color(255));
checkbox.setColorLabel(color(255));
checkbox.setItemsPerRow(1);
checkbox.setSpacingColumn(0);
checkbox.setSpacingRow(10);
// add items to a checkbox.
checkbox.addItem("INVERT STENCIL", 1);
checkbox.addItem("DO ALL COMPUTED BRIDGES", 1);
checkbox.addItem("DO ROUNDED CORNERS", 1);
checkbox.addItem("USE BEZIER NOT ARCS", 1);
// float sliders
slider0 = controlP5.addSlider("BRIDGE_CULLING_FACTOR", 0.00, 0.99, 0.50, 20, 230, 90, 10);
slider1 = controlP5.addSlider("BRIDGE_THICKNESS", 0.05, 0.30, 0.17, 20, 250, 90, 10);
slider2 = controlP5.addSlider("PDF_LINE_THICKNESS", 0.0072, 1.00, 0.50, 20, 270, 90, 10);
// int sliders
slider3 = controlP5.addSlider("MIN_BRIDGES_PER_ISLAND", 1, 4, 3, 20, 290, 90, 10);
slider4 = controlP5.addSlider("CORNER_THICKENING", 1, 10, 3, 20, 320, 90, 10);
slider3.setNumberOfTickMarks(4);
slider3.setSliderMode(Slider.FLEXIBLE);
slider4.setNumberOfTickMarks(10);
slider4.setSliderMode(Slider.FLEXIBLE);
int n = 0;
if (DO_WHITE_PAINT_STENCIL) {
checkbox.activate(n);
}
else {
checkbox.deactivate(n);
}
n++;
if (DO_ALL_COMPUTED_BRIDGES) {
checkbox.activate(n);
}
else {
checkbox.deactivate(n);
}
n++;
if (DO_ROUNDED_CORNERS) {
checkbox.activate(n);
}
else {
checkbox.deactivate(n);
}
n++;
if (USE_BEZIER_NOT_ARCS) {
checkbox.activate(n);
}
else {
checkbox.deactivate(n);
}
n++;
}
//===============================================================
void updateParametersFromGUI() {
BRIDGE_CULLING_FACTOR = slider0.value();
BRIDGE_THICKNESS = slider1.value();
PDF_LINE_THICKNESS = slider2.value();
MIN_BRIDGES_PER_ISLAND = (int) slider3.value();
CORNER_THICKENING = (int) slider4.value();
}
//===============================================================
public void recomputeStencil() {
bCompleted = false;
}
//===============================================================
public void openPDFInAcrobat() {
if (bComputedStencilPDF && !DISABLE_PDF_EXPORT_FOR_APPLET) {
open (QRStencilPDFFilename);
}
}
//===============================================================
void controlEvent(ControlEvent theEvent) {
if (!bSetupPhase) {
if (theEvent.isGroup()) {
recomputeStencil();
// println("got an event from "+theEvent.group().name()+"\t");
// checkbox uses arrayValue to store the state of individual checkbox-items:
for (int i=0;i<theEvent.group().arrayValue().length;i++) {
int val = (int)theEvent.group().arrayValue()[i];
int n = 0;
if (i== n++) {
DO_WHITE_PAINT_STENCIL = (val==1);
}
else if (i==n++) {
DO_ALL_COMPUTED_BRIDGES= (val==1);
}
else if (i==n++) {
DO_ROUNDED_CORNERS = (val==1);
}
else if (i==n++) {
USE_BEZIER_NOT_ARCS = (val==1);
}
println (i + " " + val);
}
}
else {
String name = theEvent.name();
if (name.equals("BRIDGE_CULLING_FACTOR") ||
name.equals("BRIDGE_THICKNESS") ||
name.equals("PDF_LINE_THICKNESS") ||
name.equals("MIN_BRIDGES_PER_ISLAND") ||
name.equals("CORNER_THICKENING")) {
if (mouseX != pmouseX) {
recomputeStencil();
}
}
}
}
}
// Author: "Golan Levin" <golan@flong.com>, 01 August, 2011
//================================================================
// Code in this file is NOT USED OR REQUIRED by the QR_STENCILER program.
// It was used to process the 100 QR hobo codes that accompany QR_STENCILER.
// Feel free to delete this file.
// in keyPressed():
// QRImageFilename = getNextHoboFile();
// QR = loadImage (QRImageFilename);
// println("Loading:\t" + QRImageFilename);
ArrayList<String> hoboQRFiles;
int currentHoboFileIndex;
//===============================================================
void loadHoboFileList(){
hoboQRFiles = new ArrayList<String>();
currentHoboFileIndex = 0;
String aFilename;
String myPath = sketchPath + "/data/QR_hobo_codes/";
File aFolder = new File(myPath);
File[] listOfFiles = aFolder.listFiles();
int count = 0;
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) {
aFilename = listOfFiles[i].getName();
if (aFilename.endsWith (".png")) {
aFilename = myPath + aFilename;
hoboQRFiles.add(aFilename);
}
}
}
}
//===============================================================
String getNextHoboFile(){
String hoboFilename = QRDefaultImageFilename;
if (currentHoboFileIndex < hoboQRFiles.size()){
hoboFilename = hoboQRFiles.get(currentHoboFileIndex);
currentHoboFileIndex = currentHoboFileIndex+1;
}
return hoboFilename;
}
//===============================================================
void generateHoboCodeHtml() {
// generates a fragment of an HTML table to present all of the QR hobo codes.
String aFilename;
String myPath = sketchPath + "/data/QR_hobo_codes/";
File aFolder = new File(myPath);
File[] listOfFiles = aFolder.listFiles();
int count = 0;
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) {
aFilename = listOfFiles[i].getName();
if (aFilename.endsWith (".png")) {
String aPDFFile = aFilename.substring(0, aFilename.lastIndexOf('.')) + ".pdf";
String transcription = aFilename.substring(0, aFilename.lastIndexOf('.'));
String newTranscription = "";
for (int j=0; j<transcription.length(); j++) {
char c = transcription.charAt(j);
if (c == '_') {
c = ' ';
}
newTranscription += c;
}
if (count%4 == 0) {
println("<tr>");
}
print("\t<td width=\"125\" align=\"center\" valign=\"top\">");
print("<a href=\"QR_hobo_codes/" + aFilename + "\">");
print("<img \n\t\tsrc=\"QR_hobo_codes/" + aFilename + "\" ");
print("width=\"108\" height=\"108\" border=\"0\" /></a><br />");
print("<em>" + newTranscription + "</em><br />");
println();
print("\t\t<a href=\"QR_hobo_codes/" + aFilename + "\">png</a> | ");
print("<a href=\"QR_hobo_codes/" + aPDFFile + "\">stencil</a></td>");
println();
if ((count%4 == 3) || (i==(listOfFiles.length - 1))) {
println("</tr>");
}
count++;
}
}
}
}
// Author: "Golan Levin" <golan@flong.com>
// Note: this is the applet version. For the executable version, please see:
// https://github.com/golanlevin/QR_STENCILER
// To convert this into the executable version, set
// DISABLE_PDF_EXPORT_FOR_APPLET = false;
//
// For more information about this project, please see:
// http://fffff.at/qr-stenciler-and-qr-hobo-codes/
//=====================================================================
void QRStencilerInfo() {
//
// ___ ___ ___ _____ ___ _ _ ___ ___ _ ___ ___
// / _ \| _ \ / __|_ _| __| \| |/ __|_ _| | | __| _ \
// | (_) | / \__ \ | | | _|| .` | (__ | || |__| _|| /
// \__\_\_|_\ |___/ |_| |___|_|\_|\___|___|____|___|_|_\
// By Golan Levin and Asa Foster III for FFFFF.AT, 2011
//
println ();
println (" ********************************************************");
println (" * *");
println (" * QR_STENCILER *");
println (" * Version: 01 August, 2011 *");
println (" * http://fffff.at/qr-stenciler-and-qr-hobo-codes/ *");
println (" * By Golan Levin and Asa Foster III for FFFFF.AT *");
println (" * Developed with Processing v.0198, a free, cross- *");
println (" * platform Java programming toolkit for the arts. *");
println (" * *");
println (" * ABOUT *");
println (" * This free program loads a user-specified QR code *");
println (" * image, from which it generates a topologically *");
println (" * correct stencil PDF, suitable for laser-cutting. *");
println (" * *");
println (" * INSTRUCTIONS *");
println (" * >> QR_STENCILER has been tested in MacOSX 10.6.8. *");
println (" * 1. Make a QR code image which embeds a short text. *");
println (" * Try GoQR.me, Kaywa, or the Google Chart API. *");
println (" * 2. Download and install 'Processing' from *");
println (" * http://www.processing.org/download *");
println (" * We used v.0198 but v.1.5.1 seems OK too. *");
println (" * 3. Unzip 'QR_STENCILER.zip' to a folder. *");
println (" * 4. Put your QR code image in 'QR_STENCILER/data/' *");
println (" * 5. Launch Processing and open 'QR_STENCILER.pde' *");
println (" * 6. Press 'Run' (Command-R) to start the stenciler. *");
println (" * 7. You will be prompted to Open your QR code image. *");
println (" * A default will be opened if none is provided. *");
println (" * 8. After doing so, the program will generate a *");
println (" * stencil PDF in the 'data' folder. *");
println (" * 9. That PDF can now be opened in your favorite CAD *");
println (" * software, for laser-cutting cardboard, etc. *");
println (" * 10.After marking your stencil, test it with a QR *");
println (" * reader, such as TapMedia's free iPhone app. *");
println (" * *");
println (" * LICENSE *");
println (" * QR_STENCILER shall be used for Good, not Evil. *");
println (" * It is licensed under a Creative Commons *");
println (" * Attribution-NonCommercial-ShareAlike 3.0 Unported *");
println (" * License (by-nc-sa 3.0), as described at *");
println (" * http://creativecommons.org/licenses/by-nc-sa/3.0/ *");
println (" * You are free to distribute, remix, and modify *");
println (" * QR_STENCILER, so long as you share alike and *");
println (" * provide attribution to FFFFF.AT. The repackaging *");
println (" * of QR_STENCILER as or into commercial software, is *");
println (" * expressly prohibited. Please note that QR_STENCILER *");
println (" * also enjoys protections under the GRL Repercussions *");
println (" * 3.0 license, http://bit.ly/cc-repercussions. *");
println (" * For other uses, please contact FFFFF.AT. *");
println (" * The 100 QR_HOBO_CODES and their respective stencils *");
println (" * are hereby dedicated to the public domain. *");
println (" * *");
println (" * WARRANTY *");
println (" * This software is provided AS IS, without warranty *");
println (" * of any kind, expressed or implied, including but *");
println (" * not limited to the warranties of merchantibility, *");
println (" * fitness for a particular purpose, and noninfringe- *");
println (" * ment. In no event shall the authors be liable for *");
println (" * any claim, damages or other liability, whether in *");
println (" * an action of contract, tort or otherwise, arising *");
println (" * from, out of or in connection with this software *");
println (" * or its use. *");
println (" * *");
println (" * ACKNOWLEDGEMENTS *");
println (" * QR_STENCILER was created by Golan Levin and *");
println (" * Asa Foster III with support from the STUDIO for *");
println (" * Creative Inquiry and the School of Art at *");
println (" * Carnegie Mellon University. Thanks to Ben Fry, *");
println (" * Marcus Beausang, Neil Brown & Judy Robertson for *");
println (" * great code. A tip of the hat to Fred Trotter, *");
println (" * Jovino, le Suedois, Patrick Donnelly and others *");
println (" * who have gone down similar paths. Additional *");
println (" * thanks to Andrea Boykowycz for creative input. *");
println (" * Some of the QR Hobo Codes are inspired by: *");
println (" * http://www.worldpath.net/~minstrel/hobosign.htm *");
println (" * http://cockeyed.com/archive/hobo/modern_hobo.html *");
println (" * http://en.wikipedia.org/wiki/Warchalking *");
println (" * 'QR code' is trademarked by Denso Wave, Inc. *");
println (" * *");
println (" * CONTACT *");
println (" * Inquiries about QR_STENCILER may be directed to: *");
println (" * Golan Levin <golan@flong.com> *");
println (" * *");
println (" ********************************************************");
println ();
}
import processing.pdf.*;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import controlP5.*;
//=====================================================================
// ADJUSTABLE PARAMETERS: You can change these, if you so desire
//
float BRIDGE_THICKNESS = 0.17; // Ratio of bridge thickness to QR grid size, i.e. 1:6
float PDF_LINE_THICKNESS = 0.50; // For 0.001" thick lines, use 0.072.
float BRIDGE_CULLING_FACTOR = 0.60; // Set between 0..1; Higher values cull more bridges.
int MIN_BRIDGES_PER_ISLAND = 3; // Just what it says
int CORNER_THICKENING = 3; // Number of pixels of corner thickening.
int ARC_RESOLUTION = 12; // Number of points on a generated circular arc corner.
boolean DO_WHITE_PAINT_STENCIL = false;
boolean DO_ROUNDED_CORNERS = true; // Enables corner rounding and path simplification.
boolean USE_BEZIER_NOT_ARCS = false; // Enables Bezier exporting instead of generating arc points.
boolean DO_ADVANCED_BRIDGING = true; // Chooses between advanced and simple bridging.
boolean DO_ALL_COMPUTED_BRIDGES = false; // Invalidates RANDOM_BRIDGE_CULLING_FACTOR
boolean DO_OPEN_PDF_WHEN_DONE = false; // Open the stencil PDF in Acrobat when done?
// IMPORTANT:
boolean DISABLE_PDF_EXPORT_FOR_APPLET = true; // turns this into an executable version that can export PDFs!
//=====================================================================
// For more information about generating your own QR codes, see:
// http://code.google.com/apis/chart/image/docs/gallery/qr_codes.html
// Note: You may obtain better results by experimenting with the QR
// "error correction level" (L,M,Q,H) in the "chld" field.
//
// Our default QR code image ("hello world") is from:
// https://chart.googleapis.com/chart?chs=540x540&cht=qr&chld=L|1&chl=hello%20world
String QRDefaultImageFilename;
String QRImageFilename;
String QRStencilPDFFilename;
boolean bComputedStencilPDF = false;
PImage QR;
ControlP5 controlP5;
CheckBox checkbox;
Slider slider0;
Slider slider1;
Slider slider2;
Slider slider3;
Slider slider4;
Bang bang1;
Bang bang2;
Textlabel myTextlabelA;
//=====================================================================
// Other internal (global) variables and constants.
//
ConnectedComponentLabeler CCL; // Determines which pixels are holes/islands
ContourTracer CT; // Traces hole contours for PDF output
PFont tinyPdfFont;
boolean bCompleted;
boolean bSetupPhase;
int blackAndWhiteImage[];
int blackAndWhiteImageInverse[];
int coloredLabeledImage[];
int nPixels;
final color white = color(255);
final color black = color(0);
final int inverseMargin = 1;
final int controlPanelWidth = 900/4;
final int DIR_UP = 0;
final int DIR_DOWN = 1;
final int DIR_LEFT = 2;
final int DIR_RIGHT = 3;
//===============================================================
void setup() {
bSetupPhase = true;
QRStencilerInfo();
QRDefaultImageFilename = sketchPath + "/data/" + "QR_hello_world.png";
QRImageFilename = "QR_hello_world.png";
if (!DISABLE_PDF_EXPORT_FOR_APPLET){
QRImageFilename = getUserSelectedQRCodeImageFilename(); // See FileLoading.pde
}
QR = loadImage (QRImageFilename);
size (900, 675, JAVA2D);
nPixels = QR.width * QR.height;
blackAndWhiteImage = new int[nPixels];
blackAndWhiteImageInverse = new int[nPixels];
coloredLabeledImage = new int[nPixels];
CCL = new ConnectedComponentLabeler (QR.width, QR.height);
CT = new ContourTracer();
bCompleted = false;
hint (ENABLE_NATIVE_FONTS);
tinyPdfFont = createFont("Arial", 6);
setupGUI(); // see Gui.pde
bSetupPhase = false;
}
//===============================================================
void draw() {
updateParametersFromGUI();
if (bCompleted == false) {
doMainProcess();
bCompleted = true;
}
}
//===============================================================
void keyPressed() {
// Restart computation of a new stencil. Useful if you have some randomization enabled.
if (key == ' ') {
bCompleted = false;
}
}
//===============================================================
void doMainProcess() {
// This is the main processing routine for the program.
// Assuming PImage QR contains a valid QR code image,
// this routine will export a PDF stencil file for it.
// 1. Extract the pixels (as a color[] array) from the loaded QR image.
// See Utils.pde for implementation.
copyBlackAndWhiteImageFromPImage (QR); // see Utils.pde
println("* Extracted pixels.");
// 2. Some jpgs have compression artifacts that produce "near-black" or "near-white"; enforce pure colors.
// See Utils.pde for implementation.
binarizeTheImage (blackAndWhiteImage, QR.width, QR.height); // see Utils.pde
println("* Input image binarized.");
// 3. Thicken the corners of adjacent grid units, creating a stronger and more effective stencil.
// See Bridger.pde for implementation.
bridgeCorners (blackAndWhiteImage, QR.width, QR.height);
println("* Stencil interior corners bridged.");
// 4. Deal with white-painted QR stenciling, if requested.
// Involves adding a margin and inversion.
// See Utils.pde for implementation.
handleInverseStencil();
// 5. Uniquely label (with individual colors) each of the blobs; this identifies "islands".
// See: http://en.wikipedia.org/wiki/Connected-component_labeling
// See: http://en.wikipedia.org/wiki/Stencil
// See ConnectedComponentLabeler.pde for implementation.
coloredLabeledImage = CCL.doLabel(blackAndWhiteImage, QR.width, QR.height);
println("* Stencil islands identified.");
// 6. Create bridges from the main stencil body to all floaters... so they don't fall out!
// See Bridger.pde for implementation.
// bridgeIslands (blackAndWhiteImage, QR.width, QR.height);
bridgeIslands (blackAndWhiteImage, QR.width, QR.height);
println("* Stencil islands bridged.");
// 7. Invert the QR image, so that we can find its *holes* with the contour tracer.
// See Utils.pde for implementation.
computeBlackAndWhiteInverseBuffer (blackAndWhiteImage, blackAndWhiteImageInverse, nPixels);
println("* Stencil inverted, for hole-finding.");
// 8. Uniquely label all of the holes in the stencil.
// See ConnectedComponentLabeler.pde for implementation.
coloredLabeledImage = CCL.doLabel(blackAndWhiteImageInverse, QR.width, QR.height);
println("* Stencil holes identified.");
// 9. Trace all of the holes, producing vector-based 8-connected chain codes. See:
// http://imageprocessingplace.com/downloads_V3/root_downloads/tutorials/contour_tracing_Abeer_George_Ghuneim/alg.html
// See ContourExtraction.pde for implementation.
CT.traceBlobContours (coloredLabeledImage, QR.width, QR.height);
println("* Stencil holes traced.");
// 10. Draw the contours (chain codes) of the holes, and export a PDF of the result.
// This PDF can now be loaded into Illustrator, etc. and used for laser-cutting.
if (!DISABLE_PDF_EXPORT_FOR_APPLET) {
String QrPdfFileName = drawAndExportPDF(); // See below for implementation.
println("* Stencil PDF exported to the 'data' folder of this sketch:\n " + QrPdfFileName);
}
// 11. Draw the QR to the screen, as it will appear and with the exported stencil contour lines.
drawPrettyResultsToScreen();
println (" Done creating QR stencil at " + hour() + ":" + minute() + ":" + second() + " \n");
println (" ********************************************************");
}
//===============================================================
void drawPrettyResultsToScreen() {
background ((DO_WHITE_PAINT_STENCIL) ? black : white);
fill (100);
rect(0, 0, controlPanelWidth, height);
boolean bCenterVersusScale = true;
pushMatrix();
float drawMargin = 50;
float scaleFactor = (float)(height - drawMargin*2) / (float)(max(QR.height, QR.width));
translate(controlPanelWidth, 0);
translate(drawMargin, drawMargin);
scale (scaleFactor, scaleFactor);
stroke(255, 0, 128);
strokeWeight(1.0/scaleFactor);
fill ((DO_WHITE_PAINT_STENCIL) ? white : black);
if (DO_ROUNDED_CORNERS) {
CT.drawAllChainsWithFilletedCorners();
}
else {
CT.drawAllChains();
}
popMatrix();
}
//===============================================================
String drawAndExportPDF() {
// generate the filename for the stencil PDF.
int QRImageFilenameLen = QRImageFilename.length();
String QR_PDFFilename = QRImageFilename.substring(0, QRImageFilename.lastIndexOf('.'));
String QR_PdfFullFilename = QR_PDFFilename;
if (DO_WHITE_PAINT_STENCIL) {
QR_PdfFullFilename += "_inverse";
}
QR_PdfFullFilename += ".pdf";
QRStencilPDFFilename = QR_PdfFullFilename;
beginRecord(PDF, QR_PdfFullFilename);
// Generate the text written on the QR code stencil
boolean bDrawTitleText = true;
if (bDrawTitleText) {
String QR_StencilText = QRImageFilename.substring(QRImageFilename.lastIndexOf('/')+1, QRImageFilename.lastIndexOf('.'));
if (DO_WHITE_PAINT_STENCIL) {
QR_StencilText += " (for LIGHT/WHITE marking) ";
}
else {
QR_StencilText += " (for DARK/BLACK marking) ";
}
QR_StencilText += " --- Generated by QR_STENCILER, http://fffff.at/qr-stenciler-and-qr-hobo-codes/";
fill(0, 0, 0);
textFont(tinyPdfFont, 6);
text(QR_StencilText, 10, 10);
}
pushMatrix();
translate((width - QR.width)/2.0, (height - QR.height)/2.0);
strokeWeight (PDF_LINE_THICKNESS);
stroke(0, 0, 0);
noFill();
if (DO_ROUNDED_CORNERS) {
CT.drawAllChainsWithFilletedCorners();
}
else {
CT.drawAllChains();
}
popMatrix();
endRecord();
bComputedStencilPDF = true;
if (DO_OPEN_PDF_WHEN_DONE) {
open (QR_PdfFullFilename);
}
return QR_PdfFullFilename;
}
// Author: "Golan Levin" <golan@flong.com>, 01 August, 2011
//================================================================
void copyBlackAndWhiteImageFromPImage (PImage img) {
for (int i=0; i<nPixels; i++) {
blackAndWhiteImage[i] = img.pixels[i];
}
}
//===============================================================
void copyBufferToScreen(color[] imageBuffer, int nPix) {
// Note: no safety checks here.
loadPixels();
for (int i=0; i<nPix; i++) {
pixels[i] = imageBuffer[i];
}
updatePixels();
}
//===============================================================
void computeBlackAndWhiteInverseBuffer (color[] srcBuffer, color[] dstBuffer, int nPix) {
for (int i=0; i<nPix; i++) {
if (srcBuffer[i] == black) {
dstBuffer[i] = white;
}
else {
dstBuffer[i] = black;
}
}
}
//===============================================================
void binarizeTheImage (color[] bufferToProcess, int bufW, int bufH) {
// We clobber the image to ensure that .JPG artifacts don't confuse us.
// This forces pixels to pure black and pure white.
int binarizationThreshold = 127;
for (int y=0; y<bufH; y++) {
for (int x=0; x<bufW; x++) {
int index = y*bufW + x;
color c = bufferToProcess[index];
float bri = brightness(c);
if (bri < binarizationThreshold) {
bufferToProcess[index] = black;
}
else {
bufferToProcess[index] = white;
}
}
}
}
//===============================================================
int computeGridSize (color[] qrImageBuffer, int qrImageW, int qrImageH) {
// calculate the size of the QR code's grid units.
// we know that the first thing is a black square which is 7 units wide.
// use this information to automatically calculate the number of pixels per unit.
int marginX = 0;
int marginY = 0;
color searchColor = black;
if (DO_WHITE_PAINT_STENCIL){
marginX = inverseMargin+1;
marginY = inverseMargin+1;
searchColor = white;
}
boolean bFoundFirstSearchColorPixel = false;
boolean bColorHasntChangedSinceIFoundTheFirstSearchColorPixel = true;
int searchColorLocStartX = -1;
int searchColorLocEndX = -1;
int searchColorLocY = -1;
for (int y = marginY; y < (qrImageH-marginY); y++) {
for (int x = marginX; x < (qrImageW-marginX); x++) {
int index = y*qrImageW + x;
color someCol = qrImageBuffer[index];
if ((someCol == searchColor) && (bFoundFirstSearchColorPixel == false)) {
bFoundFirstSearchColorPixel = true;
searchColorLocStartX = x;
searchColorLocY = y;
}
if (y == searchColorLocY) { // if we are still in the same row
if (bColorHasntChangedSinceIFoundTheFirstSearchColorPixel) {
if (someCol != searchColor) {
bColorHasntChangedSinceIFoundTheFirstSearchColorPixel = false;
}
else {
searchColorLocEndX = x;
}
}
}
}
}
if (bFoundFirstSearchColorPixel && (searchColorLocStartX > -1) && (searchColorLocEndX > -1)) {
int distance = 1 + searchColorLocEndX - searchColorLocStartX; // +1 corrects off-by-one err
int gridSize = distance / 7; // fixed.
return gridSize;
}
println ("Error determining QR code grid size. Is your image a valid QR code?");
return -1;
}
//===============================================================
void handleInverseStencil(){
if (DO_WHITE_PAINT_STENCIL) {
// put a narrow margin around everything
int m = inverseMargin;
for (int y=0; y<QR.height; y++) {
for (int x=0; x<m; x++) {
int index = y*QR.width + x;
blackAndWhiteImage[index] = black;
}
for (int x=(QR.width-m); x<QR.width; x++) {
int index = y*QR.width + x;
blackAndWhiteImage[index] = black;
}
}
for (int y=0; y<m; y++) {
for (int x=0; x<QR.width; x++) {
int index = y*QR.width + x;
blackAndWhiteImage[index] = black;
}
for (int x=0; x<QR.width; x++) {
int index = (QR.height-1-y)*QR.width + x;
blackAndWhiteImage[index] = black;
}
}
// invert the image for further processing
for (int y=0; y<QR.height; y++) {
for (int x=0; x<QR.width; x++) {
int index = y*QR.width + x;
if (blackAndWhiteImage[index] == black){
blackAndWhiteImage[index] = white;
} else {
blackAndWhiteImage[index] = black;
}
}
}
}
}
The QR_STENCILER is a free, fully-automated utility which converts QR codes into vector-based stencil patterns suitable for laser-cutting. For information, see QR_STENCILER_applet.pde.
See also:
http://github.com/golanlevin/QR_STENCILER
http://fffff.at/qr-stenciler-and-qr-hobo-codes