Processing Forum
The theory on 4 point warping is explained quite well on Paul Heckbert's Master's Thesis "Fundamentals of Texture Mapping and Image Warping" (www.cs.cmu.edu/~ph/texfund/texfund.pdf). It's a bit dated (1989!) but still relevant.
To cut a long story short: for any point in a normalized quad (0,0 - 1,1), first interpolate along the bottom edge of the new corners to find the bottom position. Then interpolate along the top edge of the new corners to find the top position. Then interpolate between those two points to find the transformed position of the point. If the quad isn't initally a normalized quad (i.e. like the QC coordinate system), it needs to be normalized (a linear map takes care of that).
I implemented this method using a vertex shader by moving a grids vertices depending on the input 4 coordinates for the new corner positions.
uniform vec2 BL, BR, TL, TR;uniform vec2 renderSize;void main() {// transform from QC object coords to 0...1
vec2 p = (vec2(gl_Vertex.x, gl_Vertex.y) + 1.) * 0.5;// interpolate bottom edge x coordinate
vec2 x1 = mix(BL, BR, p.x);// interpolate top edge x coordinate
vec2 x2 = mix(TL, TR, p.x);// interpolate y position
p = mix(x1, x2, p.y);// transform from 0...1 to QC screen coords
p = (p - 0.5) * renderSize;gl_Position = gl_ModelViewProjectionMatrix * vec4(p, 0, 1);gl_FrontColor = gl_Color;gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;}
vec2 x1 = mix(BL, BR, p.x);
vec2 x1 = mix(BL, BR, p.x);
p = mix(x1, x2, p.y);
import processing.video.*; Capture video; PImage bgimage; FINDER finder; TRACKER tracker; //PFont font; int minBlobSize = 100; int fontSize = 30; float threshold = 160; boolean start = false; boolean startTracking = true; void setup() { size((screen.width)/2, (screen.height)/2); String[] cams = Capture.list(); if(cams.length == 3) { video = new Capture(this, width, height); } else { video = new Capture(this, width, height, "Sony HD Eye for PS3 (SLEH 00201)", 60); } smooth(); // font = loadFont("Helvetica-48.vlw"); finder = new FINDER(); tracker = new TRACKER(); bgimage = createImage(video.width, video.height, RGB); } void draw() { // textFont(font, fontSize); if (video.available()) { video.read(); } finder.findBlobs(bgimage); if(startTracking) { tracker.track(finder.blobs); } println(threshold+" "+minBlobSize+" "+frameRate); } void keyPressed() { if(key == ' ') { if(start == false) { start = true; } bgimage.copy(video, 0, 0, video.width, video.height, 0, 0, video.width, video.height); bgimage.updatePixels(); } else if(key =='+') { threshold += 10; } else if(key == '-') { threshold -= 10; } else if(key == 't') { if(startTracking) { startTracking = false; } else { startTracking = true; } } else if(key == 's') { if(minBlobSize > 10) { minBlobSize -= 10; } } else if(key == 'S') { minBlobSize += 10; } /* else if(key == 'f') { if(fontSize > 1) { fontSize--; } } else if(key == 'F') { fontSize++; }*/ }
class FINDER { int x1, x2, x, y; color fgColor, bgColor; float r1, g1, b1, r2, g2, b2; boolean A = false; ArrayList blobs = new ArrayList(); PImage thisFrame; void findBlobs(PImage temp_ThisFrame) { thisFrame = temp_ThisFrame; loadPixels(); video.loadPixels(); thisFrame.loadPixels(); image(video, 0, 0, width, height); if(start) { for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { if(comparePixels() > threshold) { pixels[x + y*video.width] = fgColor; x1 = x; do { x++; } while(x < width && comparePixels() > threshold); x2 = x - 1; BLOB a = new BLOB(x1, x2, y); a.Size += (x2 - x1); for (int h = 0; h < blobs.size(); h++) { for (int i = x1; i <= x2; i++) { for (int j = ((BLOB) blobs.get(h)).x1; j <= ((BLOB) blobs.get(h)).x2; j++) { if (i == j && (y - (((BLOB) blobs.get(h)).y1 + ((BLOB) blobs.get(h)).y2)) == 1) { A = true; if (a.x1 < ((BLOB) blobs.get(h)).x1) { ((BLOB) blobs.get(h)).x1 = a.x1; } else { a.x1 = ((BLOB) blobs.get(h)).x1; } if (a.x2 > ((BLOB) blobs.get(h)).x2) { ((BLOB) blobs.get(h)).x2 = a.x2; } else { a.x2 = ((BLOB) blobs.get(h)).x2; } if(a.y1 > ((BLOB) blobs.get(h)).y1) { ((BLOB) blobs.get(h)).y2++; } break; } } if (A) { break; } } } if (!A) { if(a.Size > minBlobSize) { blobs.add(a); } } A = false; } else { pixels[x + y*video.width] = color(0); } } } if(blobs.size() != 0) { for(int i = 0; i < blobs.size(); i++) { ((BLOB) blobs.get(i)).calcenter(); } } else { image(video, 0, 0, width, height); tracker.prevFrame.clear(); } updatePixels(); } } float comparePixels() { fgColor = video.pixels[x + y*video.width]; bgColor = thisFrame.pixels[x + y*video.width]; r1 = red(fgColor); g1 = green(fgColor); b1 = blue(fgColor); r2 = red(bgColor); g2 = green(bgColor); b2 = blue(bgColor); return(dist(r1, g1, b1, r2, g2, b2)); } }
class TRACKER { float distance, CD = dist(0, 0, width, height); //CD stands for Closest Distance, which initially will have the biggest one in order to get in the first time ArrayList prevFrame = new ArrayList(); boolean newBlob = true; int[] newBlobs = new int[0]; void track(ArrayList CurrFrame) { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(prevFrame.size() == 0) { for(int i = 0; i < CurrFrame.size(); i++) { BLOB cBlob = (BLOB) CurrFrame.get(i); cBlob.ID = i+1; cBlob.hasID = true; } } else { for(int i = 0; i < CurrFrame.size(); i++) { boolean blobMatched = false; BLOB cBlob = (BLOB) CurrFrame.get(i); for(int j = 0; j < prevFrame.size(); j++) { BLOB pBlob = (BLOB) prevFrame.get(j); if(PVector.dist(cBlob.loc, pBlob.loc) <= (cBlob.radio + pBlob.radio)) { cBlob.ID = pBlob.ID; blobMatched = cBlob.hasID = true; break; } } if(!blobMatched) { newBlobs = append(newBlobs, i); } } //WITH THE CURRENT APPROACH THERE IS NO NEED TO FIND THE CLOSEST BLOB //Find Closest Blob: this time we´ll search in THE CURRENT ARRAY since the blobs that also appear on the prev array have already been matched for(int a = 0; a < newBlobs.length; a++) { BLOB cBlob = (BLOB) CurrFrame.get(newBlobs[a]); boolean ID_REPEATED = true; int CB, i = 1; for(int j = 0; j < CurrFrame.size(); j++) { BLOB blob = (BLOB) CurrFrame.get(j); if(blob.hasID) { float distance; if((distance = PVector.dist(cBlob.loc, blob.loc)) < CD) { CD = distance; CB = blob.ID; } } } while(ID_REPEATED) { ID_REPEATED = false; for(int k = 0; k < CurrFrame.size(); k++) { BLOB blob = (BLOB) CurrFrame.get(k); if(i == blob.ID) { ID_REPEATED = true; break; } } if(!ID_REPEATED) { break; } i++; } cBlob.ID = i; } newBlobs = expand(newBlobs, 0); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// for(int i = 0; i < CurrFrame.size(); i++) { BLOB cBlob = (BLOB) CurrFrame.get(i); fill(255); // text("obj."+cBlob.ID+" size."+cBlob.Size, cBlob.loc.x, cBlob.loc.y); stroke(255); noFill(); ellipse(cBlob.loc.x, cBlob.loc.y, cBlob.radio, cBlob.radio); } ///////////////////////////////////////////////////////////////////////////////////////////////// if(prevFrame.size() != 0) { prevFrame.clear(); for(int i = 0; i < CurrFrame.size(); i++) { prevFrame.add((BLOB) CurrFrame.get(i)); } CurrFrame.clear(); } else if(prevFrame.size() == 0) { for(int i = 0; i < CurrFrame.size(); i++) { prevFrame.add((BLOB) CurrFrame.get(i)); } CurrFrame.clear(); } } }