// ivgrid.cxx - read Open Inventor "iv" file format for 3-D models // and extract from it a regular grid of depth and color samples, // a rectangular array of points with the following attributes: // Z R G B // // Written specifically to parse Inventor files written by the // Minolta Vivid 700 laser scanner, // for 15-869 assignment 2: View Transformation // // some terminology comes from the paper: // Head-Tracked Stereoscopic Display Using Image Warping // Leonard McMillan and Gary Bishop // Stereoscopic Displays and Virtual Reality Systems II // SPIE Proceedings 2409, Feb. 1995. // // IMPORTANT: // for an explanation of coordinate systems, read all comments in // transform_points() and extract_camera() below // This program understands only a small subset of the Open Inventor // file format, namely // Texture2 { image WIDTH HEIGHT 3 COLOR_IN_HEXADECIMAL ... } // TextureCoordinate2 { point[ X Y, ... X Y ] } // Coordinate3 { point [ X Y Z, ... X Y Z ] } // coordIndex [ I11, I12, I13, I14, -1, // ... // In1, In2, In3, In4, -1 ] // textureCoordIndex [ I11, I12, I13, I14, -1, // ... // In1, In2, In3, In4, -1 ] // where all capitals means a numerical or string variable, // and it ignores comments // # EVERYTHING_UP_TO_NEWLINE // and any other tokens it doesn't recognize. // Currently, comments within the Texture2, TextureCoordinate2, Coordinate3, // coordIndex, and textureCoordIndex commands are not tolerated (a bug). // Paul Heckbert // version 1, 1pm 28 Sept 1999 - handles zoom 1 ok, // but doesn't do a great job on zoom>1 // version 2, 1pm 29 Sept 1999 - fixed portability problem that was causing // "Unknown error reading 'Texture2' command" error // version 3, 3pm 30 Sept 1999 - added comments about the sign of w // version 4, 3pm 1 Oct 1999 - fixed extract_camera to make |uvec|=|vvec| // version 5, 4pm 3 Oct 1999 - now smart about zoom>1 // scales the projected and texture coordinates much better now #include #include // for strcmp #include // for file I/O, includes iostream.h #include // for ios:: #include #include #include #include #include // for color image, from libpicio #include "grid.h" // for Grid_data and Gpoint structures typedef struct {float r, g, b;} Pixelf_rgb; // float color struct Spoint { // scanner point // data from IV file Point3 wo; // position in world space float s, t; // texture coordinates unsigned char r, g, b; // color // data we compute Point3 gr; // position in grid space // see comments in transform_points() below for explanation of coord sys }; struct Line { float a, b, c; // line equation ax+by+c=0 void set(Point3 &p, Point3 &q, Point3 &r); }; void Line::set(Point3 &p, Point3 &q, Point3 &r) { // initialize line so ax+by+c has value 0 at points p and q // and value 1 and r (a barycentric coordinate over the triangle) a = p.y-q.y; b = q.x-p.x; c = p.x*q.y-p.y*q.x; float s = a*r.x+b*r.y+c; // = 2*(area of triangle pqr) s = 1./s; a *= s; // scale so that a*rx+b*ry+c=1 b *= s; c *= s; } struct Triangle { Spoint *v[3]; // pointers to three vertices }; //---------------------------------------------------------------------- #define NMAX 40000 // because Vivid scans 200*200 3-D points, max enum Quad_list_type {COORD_INDEX, TEXTURE_COORD_INDEX}; // first or second batch of coordinate indices? class Object { // a 3-D object with shape and texture public: Pic *pic; // texture Spoint pt[NMAX]; // scanner points int npt; // number of scanner points float wxmin, wxmax, wymin, wymax, wzmin, wzmax; // bounding box in world space int zoom; // Vivid lens zoom float pxmin, pxmax, pymin, pymax; // projected coordinates (-x/z,-y/z) lie within the range above Triangle tri[NMAX*2]; // triangles int ntri; // number of triangles Object(); ~Object(); void get_color_bilinear(float s, float t, unsigned char *r, unsigned char *g, unsigned char *b); // do bilinear interpolation from texture // to read Open Inventor file format void inventor_read(istream &s, const char *streamname); int inventor_read(const char *file); void print_points(); void print_transform(); void transform_points(int nx, int ny); void extract_camera(Grid_data &gd); void grid_compute(Grid_data &gd); private: // to read Open Inventor file format, internals void inventor_read_texture2(istream &s, const char *streamname); void inventor_read_texturecoordinate2(istream &s, const char *streamname); void inventor_read_coordinate3(istream &s, const char *streamname); void inventor_read_coordindex(istream &s, const char *streamname, Quad_list_type which); }; Object::Object() { pic = NULL; npt = ntri = 0; zoom = 0; // flag to indicate that pxmin etc uninit } Object::~Object() { if (pic) pic_free(pic); } void Object::get_color_bilinear(float s, float t, unsigned char *r, unsigned char *g, unsigned char *b) { // use bilinear interpolation to extract the color of the // texture pixel at (s,t) from pic float x = s*pic->nx; float y = t*pic->ny; // in case some of the (s,t)'s are outside [0,1] range if (x<0) x = 0; if (x>=pic->nx-1) x = pic->nx-1.001; if (y<0) y = 0; if (y>=pic->ny-1) y = pic->ny-1.001; int xi = (int)floor(x); // integer part int yi = (int)floor(y); float xf = x-xi; // fractional part float yf = y-yi; Pixel1_rgb *p00, *p10, *p01, *p11; Pixelf_rgb px0, px1, pxy; p00 = (Pixel1_rgb *)&PIC_PIXEL(pic, xi, yi, 0); p10 = p00+1; // note that we're reading unsigned chars below, but writing floats // linearly interpolate (lerp) between colors 00 and 10 px0.r = p00->r + xf*(p10->r - p00->r); px0.g = p00->g + xf*(p10->g - p00->g); px0.b = p00->b + xf*(p10->b - p00->b); p01 = (Pixel1_rgb *)&PIC_PIXEL(pic, xi, yi+1, 0); p11 = p01+1; // lerp between colors 01 and 11 px1.r = p01->r + xf*(p11->r - p01->r); px1.g = p01->g + xf*(p11->g - p01->g); px1.b = p01->b + xf*(p11->b - p01->b); // lerp between colors x0 and x1 pxy.r = px0.r + yf*(px1.r - px0.r); pxy.g = px0.g + yf*(px1.g - px0.g); pxy.b = px0.b + yf*(px1.b - px0.b); // no need to check if in range since colors we started with were in range assert(pxy.r>=0 && pxy.r<256); assert(pxy.g>=0 && pxy.g<256); assert(pxy.b>=0 && pxy.b<256); // convert back to unsigned char *r = (unsigned char)pxy.r; *g = (unsigned char)pxy.g; *b = (unsigned char)pxy.b; } //---------------------------------------------------------------------- // Open Inventor parsing #define TOKEN_SIZE 20 static int next_token_is_not(istream &s, const char *str) { // read a token from stream s, return whether token is not str static char tok[TOKEN_SIZE]; s >> setw(sizeof tok) >> tok; return strcmp(tok, str); } static void expect_token(istream &s, const char *command, const char *str) { // read a token from stream s, and print an error and exit if it's not str char tok[TOKEN_SIZE]; s >> setw(sizeof tok) >> tok; if (strcmp(tok, str)) { cerr << "Error reading '" << command << "' command, got '" << tok << "', not '" << str << "', as expected" << endl; // would be neat to print filename and line number of input file here exit(1); } } static void exit_if_trouble(istream &s, const char *command, int line) { if (s.fail()) { cerr << "Unknown error reading '" << command << "' command" << " at line " << line << endl; exit(1); } } // warning: the following routines do a lousy job of checking for errors // (e.g. braces not preceded by space) void Object::inventor_read_texture2(istream &s, const char *streamname) { // Texture2 { image WIDTH HEIGHT 3 COLOR_IN_HEXADECIMAL ... } // read a color image expect_token(s, "Texture2", "{"); expect_token(s, "Texture2", "image"); int nx, ny, x, y, color24; s >> nx; // read width s >> ny; // read height cout << nx << "x" << ny << " texture array" << endl; expect_token(s, "Texture2", "3"); pic = pic_alloc(nx, ny, 3, NULL); // allocate a picture array char tok[TOKEN_SIZE]; for (y=0; y> color24; // s.setf(ios::dec); // back to decimal s >> setw(sizeof tok) >> tok; // read in hexadecimal 24-bit color as a string if (sscanf(tok, "%x", &color24) != 1) { cerr << "ERROR: sscanf expecting a hex color in Texture2" << endl; exit(1); } exit_if_trouble(s, "Texture2", __LINE__); // cout << "(" << x << "," << y << ")"; // get the address of pixel (x,y) in pic Pixel1_rgb *p = (Pixel1_rgb *)&PIC_PIXEL(pic, x, y, 0); p->r = color24>>16; // save the bytes p->g = color24>>8; p->b = color24; // printf("rgb=(%02x,%02x,%02x)=(%3d,%3d,%3d)\n", // p->r, p->g, p->b, // p->r, p->g, p->b); } expect_token(s, "Texture2", "}"); exit_if_trouble(s, "Texture2", __LINE__); // check that no stream error so far } void Object::inventor_read_texturecoordinate2 (istream &s, const char *streamname) { // TextureCoordinate2 { point[ X Y, ... X Y ] } // read a list of texture coordinates expect_token(s, "TextureCoordinate2", "{"); expect_token(s, "TextureCoordinate2", "point["); // note! we're relying on the quirks of Minolta's VIVID software, // expecting no space between "point" and "[" here! int n; char tok[TOKEN_SIZE]; for (n=0;;) { float x, y; s >> x >> y; // read texture x and y (a.k.a. s and t) assert(n> setw(sizeof tok) >> tok; if (strcmp(tok, ",")) // usually, token is a comma if (!strcmp(tok, "]")) break; // "]" means end of list else { cerr << "Didn't find end of TextureCoordinate2 list in " << streamname << endl; exit(1); } } expect_token(s, "TextureCoordinate2", "}"); exit_if_trouble(s, "TextureCoordinate2", __LINE__); // check that no error so far cout << "read " << n << " texture coordinates" << endl; npt = n; } void Object::inventor_read_coordinate3(istream &s, const char *streamname) { // Coordinate3 { point [ X Y Z, ... X Y Z ] } // read a list of xyz coordinates expect_token(s, "Coordinate3", "{"); expect_token(s, "Coordinate3", "point"); expect_token(s, "Coordinate3", "["); if (!pic) { // we should have encountered the Texture2 command by now cerr << "ERROR: Inventor file does not contain a texture image.\n" << "Perhaps data wasn't saved properly." << endl; exit(1); } wxmin = HUGE; // find 3-D bounding box wxmax = -HUGE; wymin = HUGE; wymax = -HUGE; wzmin = HUGE; wzmax = -HUGE; int n; char tok[TOKEN_SIZE]; for (n=0;;) { float x, y, z; s >> x >> y >> z; // read x, y, and z if (xwxmax) wxmax = x; if (ywymax) wymax = y; if (zwzmax) wzmax = z; assert(n> setw(sizeof tok) >> tok; if (strcmp(tok, ",")) // usually, token is a comma if (!strcmp(tok, "]")) break; // "]" means end of list else { cerr << "Didn't find end of Coordinate3 list in " << streamname << endl; exit(1); } } expect_token(s, "Coordinate3", "}"); exit_if_trouble(s, "Coordinate3", __LINE__); // check that no stream error so far cout << "read " << n << " 3-D points" << endl; cout << " bounding box: " << "x [" << wxmin << ":" << wxmax << "] " << "y [" << wymin << ":" << wymax << "] " << "z [" << wzmin << ":" << wzmax << "]" << endl; // check that the number of texture points and 3-D points are equal assert(n==npt); } void set_tri(Triangle &tri, Quad_list_type which, Spoint *p, Spoint *q, Spoint *r) { if (which==COORD_INDEX) { tri.v[0] = p; tri.v[1] = q; tri.v[2] = r; } else { assert(tri.v[0]==p); assert(tri.v[1]==q); assert(tri.v[2]==r); } } void Object::inventor_read_coordindex(istream &s, const char *streamname, Quad_list_type which) { // parse either // coordIndex [ POLYLIST ] // or // textureCoordIndex [ POLYLIST ] // where POLYLIST = POLY, POLY, ... POLY // and POLY = I1, I2, I3, -1 (for a triangle) // or I1, I2, I3, I4, -1 (for a quadrilateral) // etc // (Vivid software creates only quadrilaterals and triangles, apparently) // read a list of polygons and triangulate them // if we're doing coordIndex, then build up the list // if we're doing textureCoordIndex, then check that list is identical char *command = which==COORD_INDEX ? "coordIndex" : "textureCoordIndex"; expect_token(s, command, "["); char tok[TOKEN_SIZE]; #define NSMAX 10 int n, i, ns, ind[NSMAX+1]; for (n=0;;) { for (ns=0; ns> ind[ns]; if (ind[ns]<0) break; assert(ind[ns]> setw(sizeof tok) >> tok; if (strcmp(tok, ",")) // usually, token is a comma if (!strcmp(tok, "]")) break; // "]" means end of list else { cerr << "Didn't find end of " << command << " list in " << streamname << endl; exit(1); } } if (which==COORD_INDEX) { ntri = n; cout << "read " << n << " triangles" << endl; } else assert(n==ntri); } void Object::inventor_read(istream &s, const char *streamname) { // read an Inventor file, returning a texture, a set of points, // and a list of triangles char tok[TOKEN_SIZE]; cout << "reading " << streamname << endl; while (s >> setw(sizeof tok) >> tok) { // cout << "(" << tok << ")" << endl; if (!strcmp(tok, "#")) { // gobble comment s.ignore(1000, '\n'); } else if (!strcmp(tok, "Texture2")) inventor_read_texture2(s, streamname); else if (!strcmp(tok, "TextureCoordinate2")) inventor_read_texturecoordinate2(s, streamname); else if (!strcmp(tok, "Coordinate3")) inventor_read_coordinate3(s, streamname); else if (!strcmp(tok, "coordIndex")) inventor_read_coordindex(s, streamname, COORD_INDEX); else if (!strcmp(tok, "textureCoordIndex")) inventor_read_coordindex(s, streamname, TEXTURE_COORD_INDEX); } cout << "done with " << streamname << endl; } int Object::inventor_read(const char *file) { ifstream s(file, ios::in); if (!s) { cerr << "inventor_read: can't read " << file << endl; return 0; } inventor_read(s, file); s.close(); return 1; } // end Open Inventor-specific code //---------------------------------------------------------------------- #define EPS -1e-3 // tolerance for roundoff error on point-in-triangle testing. // using floats (not doubles), it seems to be necessary to use an // epsilon, instead of testing against 0, // else some grid points fall through the cracks // (are considered to be outside all the triangles) void interpolate(Spoint *v[3], Line edge[2], float x, float y, Gpoint &gp) { // test if point (x,y) is inside triangle with vertices v[0], v[1], v[2] // and edge equations edge[0], edge[1], // and interpolate grid point gp (z,r,g,b) there, if so int i; float bary[3]; // barycentric coordinates for (i=0; i<2; i++) { bary[i] = edge[i].a*x + edge[i].b*y + edge[i].c; if (bary[i] < EPS) return; // if point was outside this edge } // optimization: compute bary[2] using invariant bary0+bary1+bary2=1 // that way we don't need edge[2] bary[2] = 1-bary[0]-bary[1]; if (bary[2] < EPS) return; // if point was outside this edge // if we get to here, point (x,y) is on or in triangle // interpolate z,r,g,b at (x,y) // no need for range checking because colors we started with were within // range and this interpolation obeys the convex hull property gp.gz = bary[0]*v[0]->gr.z + bary[1]*v[1]->gr.z + bary[2]*v[2]->gr.z; gp.r = (unsigned char) (bary[0]*v[0]->r + bary[1]*v[1]->r + bary[2]*v[2]->r); gp.g = (unsigned char) (bary[0]*v[0]->g + bary[1]*v[1]->g + bary[2]*v[2]->g); gp.b = (unsigned char) (bary[0]*v[0]->b + bary[1]*v[1]->b + bary[2]*v[2]->b); // printf(" (%2.0f,%2.0f) bary=(%5.3f,%5.3f,%5.3f) zrgb=%5.2f,%3d,%3d,%3d\n", // x, y, bary[0], bary[1], bary[2], gp.gz, gp.r, gp.g, gp.b); } void grid_resample_triangle(Spoint *p, Spoint *q, Spoint *r, Grid_data &gd) { // resample triangle pqr into grid, // find grid points that lie within it using a ray-tracing-like approach // printf("resample_triangle %d,%d,%d\n", p-pt, q-pt, r-pt); Spoint *v[3] = {p, q, r}; // vertices of triangle // find a bounding box around triangle in grid space int i; float gxmin = HUGE, gxmax = -HUGE, gymin = HUGE, gymax = -HUGE; for (i=0; i<3; i++) { if (v[i]->gr.xgr.x; if (v[i]->gr.x>gxmax) gxmax = v[i]->gr.x; if (v[i]->gr.ygr.y; if (v[i]->gr.y>gymax) gymax = v[i]->gr.y; } int gx0 = (int)ceil (gxmin); int gx1 = (int)floor(gxmax); int gy0 = (int)ceil (gymin); int gy1 = (int)floor(gymax); if (gx0<0) gx0 = 0; if (gx1>=gd.nx) gx1 = gd.nx-1; if (gy0<0) gy0 = 0; if (gy1>=gd.ny) gy1 = gd.ny-1; if (gx0>gx1 || gy0>gy1) return; // bbox was entirely out of bounds // compute line equations for edges of triangle Line edge[2]; edge[0].set(v[1]->gr, v[2]->gr, v[0]->gr); edge[1].set(v[2]->gr, v[0]->gr, v[1]->gr); // edge[2].set(v[0]->gr, v[1]->gr, v[2]->gr); // we don't need it // interpolate at integer grid points inside triangle (if any) int gx, gy; for (gy=gy0; gy<=gy1; gy++) for (gx=gx0; gx<=gx1; gx++) interpolate(v, edge, gx, gy, gd.grid[gy][gx]); } void Object::transform_points(int nx, int ny) { // coordinate systems: // // world space is the coordinates in the Inventor file, // its origin is at the camera, so McMillan's cdot is (0,0,0) // stored in (x,y,z) in Spoint struct // // projected space is (px,py,pz)=(-x/z,-y/z,-1/z) // // grid space is (gx,gy,gz) where 0<=gx0); cout << "zoom was " << zoom << endl; pxmin = -cam[zoom].pmax; pxmax = cam[zoom].pmax; pymin = -cam[zoom].pmax; pymax = cam[zoom].pmax; // compute vectors o, u, and v of McMillan's paper gd.cam = Point3(0., 0., 0.); // world space position of camera (Vivid's convention) gd.ovec = Point3(pxmin, pymin, -1.); // world space direction of grid space point (0,0) from camera gd.uvec = Point3((pxmax-pxmin)/(gd.nx-1.), 0., 0.); // world space direction of grid space vector (1,0) gd.vvec = Point3(0., (pymax-pymin)/(gd.ny-1.), 0.); // world space direction of grid space vector (0,1) // IMPORTANT NOTE: this vvec points up (origin in lower left of image) // whereas McMillan's points down (origin in upper left of image) // so you'll need to make some changes to the algorithm to // compensate. This flips the sign of w, for instance. // The coordinate system used by the Vivid has world z pointing // out of the picture, not in. Consequently, ALL THE GZ'S IN THE // GRID WILL BE NEGATIVE. // By setting ovec.z=-1 and uvec.z=vvec.z=0 above, we give the // projection plane an (arbitrary) depth of -1. // This choice along with the formula gz=pz=-1/z // means that McMillan's generalized disparity // delta = len(ovec+gx*uvec+gy*vvec) / sqrt(x*x+y*y+z*z) // and our // gz = -1/z // are exactly equal, by similar triangles! // In other words, McMillan's "generalized disparity" is just a special // case of "perspective depth", as defined in, say, // Newman & Sproull, "Principles of Interactive Computer Graphics", // 1979, p. 356. } #ifdef OLD // ----------------------------------- for data gathering void Object::old_extract_camera(Grid_data &gd) { // extract camera parameters from point set in Object, // and save them in Grid_data // to guess the Vivid's lens zoom, compute a bounding box of // projected coordinates (-x/z,-y/z) // 'd' for data // also find the linear fit px(s) = a*s+b, py(t) = c*t+d // by least squares float dxmin = HUGE, dxmax = -HUGE, dymin = HUGE, dymax = -HUGE; double S = npt, Ss = 0, St = 0, Sx = 0, Sy = 0, Sss = 0, Stt = 0, Ssx = 0, Sty = 0; int i; for (i=0; idxmax) dxmax = px; if (pydymax) dymax = py; Ss += pt[i].s; Sx += px; Sss += (double)pt[i].s*pt[i].s; Ssx += (double)pt[i].s*px; St += pt[i].t; Sy += py; Stt += (double)pt[i].t*pt[i].t; Sty += (double)pt[i].t*py; } float dmax = -dxmin > dxmax ? -dxmin : dxmax; if (-dymin > dmax) dmax = -dymin; if (dymax > dmax) dmax = dymax; cout << "bbox of projected: " << "x [" << dxmin << ":" << dxmax << "] " << "y [" << dymin << ":" << dymax << "] " << "dmax=" << dmax << endl; double a = (Ssx*S - Ss*Sx)/(Sss*S-Ss*Ss); double b = (Sss*Sx-Ssx*Ss)/(Sss*S-Ss*Ss); double c = (Sty*S - St*Sy)/(Stt*S-St*St); double d = (Stt*Sy-Sty*St)/(Stt*S-St*St); cout << "px = " << a << " * s + " << b << endl; cout << "py = " << c << " * t + " << d << endl; double Sex = 0, Sey = 0; for (i=0; i0 && ny>0); delete grid[0]; // delete the array of Gpoints delete grid; // delete the array of row pointers } void Grid_data::print() { printf("\ncam=(%g,%g,%g) ovec=(%g,%g,%g)\n", cam.x, cam.y, cam.z, ovec.x, ovec.y, ovec.z); printf("uvec=(%g,%g,%g) vvec=(%g,%g,%g)\n", uvec.x, uvec.y, uvec.z, vvec.x, vvec.y, vvec.z); int gx, gy; printf("# x y gz(delta) r g b\n"); for (gy=0; gy