/* Simple OV511 video capture program, version 1.0.1 * * Copyright 2000 Peter S. Housel. * * Portions of this program were modeled after or adapted from the * OV511 dirver for Linux by Mark W. McClelland; see * http://alpha.dyndns.org/ov511/ for more information. * * This program is free software; you can redistribute it and/or modify * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include #include #include #include #include #include #include #include #include #include #include /*#include */ #include #include #include "vid.h" #define VERSION "1.0.1 (2000-05-07)" static int ov511_reg_read(int fd, int reg) { struct usb_ctl_request ur; unsigned char data[1024]; ur.ucr_request.bmRequestType = UT_READ_VENDOR_INTERFACE; ur.ucr_request.bRequest = 2; USETW(ur.ucr_request.wValue, 0); /* unused */ USETW(ur.ucr_request.wIndex, reg); /* index */ USETW(ur.ucr_request.wLength, 1); /* payload len in bytes */ ur.ucr_data = data; ur.ucr_flags = 0; ur.ucr_actlen = 0; if(ioctl(fd, USB_DO_REQUEST, &ur) < 0) { return -1; } return data[0] & 0xFF; } static int ov511_reg_write(int fd, int reg, int val) { struct usb_ctl_request ur; unsigned char data[1024]; data[0] = val; ur.ucr_request.bmRequestType = UT_WRITE_VENDOR_INTERFACE; ur.ucr_request.bRequest = 2; USETW(ur.ucr_request.wValue, 0); /* unused */ USETW(ur.ucr_request.wIndex, reg); /* index */ USETW(ur.ucr_request.wLength, 1); /* payload len in bytes */ ur.ucr_data = data; ur.ucr_flags = 0; ur.ucr_actlen = 0; if(ioctl(fd, USB_DO_REQUEST, &ur) < 0) { return -1; } return 0; } static int ov511_i2c_read(int fd, int reg) { int status = 0; int val = 0; int retries = OV7610_I2C_RETRIES; while(--retries >= 0) { /* wait until bus idle */ do { if((status = ov511_reg_read(fd, OV511_REG_I2C_CONTROL)) < 0) return -1; } while((status & 0x01) == 0); /* perform a dummy write cycle to set the register */ if(ov511_reg_write(fd, OV511_REG_SMA, reg) < 0) return -1; /* initiate the dummy write */ if(ov511_reg_write(fd, OV511_REG_I2C_CONTROL, 0x03) < 0) return -1; /* wait until bus idle */ do { if((status = ov511_reg_read(fd, OV511_REG_I2C_CONTROL)) < 0) return -1; } while((status & 0x01) == 0); if((status & 0x2) == 0) break; } if(retries < 0) return -1; retries = OV7610_I2C_RETRIES; while(--retries >= 0) { /* initiate read */ if(ov511_reg_write(fd, OV511_REG_I2C_CONTROL, 0x05) < 0) return -1; /* wait until bus idle */ do { if((status = ov511_reg_read(fd, OV511_REG_I2C_CONTROL)) < 0) return -1; } while ((status & 0x01) == 0); if((status & 0x2) == 0) break; /* abort I2C bus before retrying */ if(ov511_reg_write(fd, OV511_REG_I2C_CONTROL, 0x10) < 0) return -1; } if(retries < 0) return -1; /* retrieve data */ val = ov511_reg_read(fd, OV511_REG_SDA); /* issue another read for some weird reason */ if(ov511_reg_write(fd, OV511_REG_I2C_CONTROL, 0x05) < 0) return -1; return val; } static int ov511_i2c_write(int fd, int reg, int val) { int status = 0; int retries = OV7610_I2C_RETRIES; while(--retries >= 0) { if(ov511_reg_write(fd, OV511_REG_SWA, reg) < 0) return -1; if(ov511_reg_write(fd, OV511_REG_SDA, val) < 0) return -1; if(ov511_reg_write(fd, OV511_REG_I2C_CONTROL, 0x1) < 0) return -1; /* wait until bus idle */ do { if((status = ov511_reg_read(fd, OV511_REG_I2C_CONTROL)) < 0) return -1; } while((status & 0x01) == 0); /* OK if ACK */ if((status & 0x02) == 0) return 0; } return -1; } struct vidstate { enum {SKIPPING, READING, DONE} state; int width, height; /* image dimensions */ xel **xels; /* output image data */ int segsize; /* size of each data segment */ int iY, jY; /* current horizontal/vertical Y coords */ int iUV, jUV; /* current horizontal/vertical UV coords */ u_char buf[2048]; /* segment data being processed */ int residue; /* left over from last processing run */ }; static void procdata(struct vidstate *vsp, u_char *data, size_t len); static void progsegment(struct vidstate *vsp, u_char *data); static void postproc(struct vidstate *vsp); int main(int argc, char *argv[]) { int fd = -1, isoc = -1; /* control and isochronous input fds */ struct usb_device_info udi; /* device info */ struct usb_alt_interface alt; /* interface/alternate selection */ int cid = -1; /* OV511 camera ID */ u_char buf[1024]; /* isochronous input read buffer */ char dev[FILENAME_MAX]; /* for constructing device names */ char isocdev[FILENAME_MAX]; /* for constructing endpoint 1 device names */ char *devname = NULL; /* device name */ int len = -1; /* isochronous input read length */ struct vidstate vs; /* current read state */ int small = 0; /* use 320x240 */ int frmnm = 0; /* cyclic frame number key */ int isplus; /* bridge is OV511+ if true, else OV511 */ int is20; /* sensor is OV7620 if true, else OV7610 */ int bufsize; /* size of packet buffer */ /* pnm_init(&argc, argv); */ /* required for PNM programs? */ while(++argv, --argc) { if(strcmp(*argv, "--version") == 0) { fprintf(stderr, "OV511/OV511+ capture program version " VERSION "\nCopyright 2000 Peter S. Housel" "\nThis program is free software; " "you may redistribute it under the terms of" "\nthe GNU General Public License. " "This program has absolutely no warranty.\n"); exit(0); } else if(strcmp(*argv, "--usage") == 0) { fprintf(stderr, "usage: vid [--version] [--usage] [--help] [--small]\n" " [-d device] [--device-name=device]\n"); exit(0); } else if(strcmp(*argv, "--help") == 0) { fprintf(stderr, "usage: vid [options]\n" "Capture an image frame from an OV511/OV511+ based USB video camera\n" "and write image data to standard output in PNM format\n\n" "--version print program version information\n" "--usage summarize command line options\n" "--help print this description\n" "--small capture as 320x240 image (default is 640x480)\n" "-d device, --device-name=device\n" " specify OV511 ugen device\n"); exit(0); } else if(strcmp(*argv, "--small") == 0) { small = 1; } else if(strncmp(*argv, "--device-name", strlen("--device-name")) == 0) { if((*argv)[strlen("--device-name")] == '=') { devname = *argv + strlen("--device-name") + 1; } else if((*argv)[strlen("--device-name")] == '\0' && argc > 0) { devname = *++argv; --argc; } else { fprintf(stderr, "usage: vid [--version] [--usage] [--help] [--small]\n" " [-d device] [--device-name=device]\n"); exit(1); } } else if((*argv)[0] == '-' && (*argv)[1] == 'd') { if((*argv)[2] != '\0') { devname = *argv + 2; fprintf(stderr, "info: %s\n", devname); } else if(argc > 0) { devname = *++argv; fprintf(stderr, "info: %s\n", devname); --argc; } else { fprintf(stderr, "usage: vid [--version] [--usage] [--help] [--small]\n" " [-d device] [--device-name=device]\n"); exit(1); } } else { fprintf(stderr, "usage: vid [--version] [--usage] [--help] [--small]\n" " [-d device] [--device-name=device]\n"); exit(1); } } if(devname) { if((fd = open(devname, O_RDWR)) < 0) { perror(devname); exit(1); } /* make sure it's an OV511 */ if(ioctl(fd, USB_GET_DEVICEINFO, &udi) < 0) { perror("USB_GET_DEVICEINFO"); exit(1); } if(udi.udi_vendorNo != 0x05A9 || (udi.udi_productNo != 0x0511 && udi.udi_productNo != 0xa511)) { fprintf(stderr, "device %s is not an OmniVision OV511 or OV511+\n", devname); exit(1); } } else { int i = 0; for(i = 0; i < 15; ++i) { sprintf(dev, "/dev/ugen%d", i); if((fd = open(dev, O_RDWR)) < 0) continue; if(ioctl(fd, USB_GET_DEVICEINFO, &udi) < 0 || udi.udi_vendorNo != 0x05A9 || (udi.udi_productNo != 0x0511 && udi.udi_productNo != 0xa511)) { close(fd); fd = -1; continue; } else { break; } } if(fd < 0) { fprintf(stderr, "vid: couldn't locate an OV511 or OV511+ device\n"); exit(1); } devname = dev; } isplus = udi.udi_productNo == 0xa511; bufsize = (isplus ? 961 : 993); fprintf(stderr, "bufsize: %d\n", bufsize); /* reset the OV511 */ if(ov511_reg_write(fd, OV511_REG_RST, 0x7f) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_RST, 0) < 0) exit(1); /* initialize system */ if(ov511_reg_write(fd, OV511_REG_EN_SYS, 0x1) < 0) exit(1); /* determine the camera model */ if((cid = ov511_reg_read(fd, OV511_REG_CID)) < 0) exit(1); fprintf(stderr, "vid: %d\n", cid); switch(cid) { case 0: /* This also means that no custom ID was set */ fprintf(stderr, "vid: MediaForte MV300\n"); break; case 3: fprintf(stderr, "vid: D-Link DSB-C300\n"); break; case 4: fprintf(stderr, "vid: Generic OV511/OV7610\n"); break; case 5: fprintf(stderr, "vid: Puretek PT-6007\n"); break; case 21: fprintf(stderr, "vid: Creative Labs WebCam 3\n"); break; case 36: fprintf(stderr, "vid: Koala-Cam\n"); break; case 100: fprintf(stderr, "vid: Lifeview RoboCam\n"); break; case 102: fprintf(stderr, "vid: AverMedia InterCam Elite\n"); break; case 112: /* The OmniVision OV7110 evaluation kit uses this too */ fprintf(stderr, "vid: MediaForte MV300\n"); break; default: fprintf(stderr, "vid: Camera custom ID %d not recognized\n", cid); exit(1); } /* set I2C write slave ID for OV7601 */ if(ov511_reg_write(fd, OV511_REG_SID, OV7610_I2C_WRITE_ID) < 0) exit(1); /* set I2C read slave ID for OV7601 */ if(ov511_reg_write(fd, OV511_REG_SRA, OV7610_I2C_READ_ID) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_PKSZ, 0x1) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_PKFMT, 0x0) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_RST, 0x3d) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_RST, 0x0) < 0) exit(1); if(ov511_i2c_read(fd, 0x00) < 0) exit(1); /* set YUV 4:2:0 format, Y channel LPF */ if(ov511_reg_write(fd, OV511_REG_M400, 0x01) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_M420_YFIR, 0x03) < 0) exit(1); /* disable snapshot */ if(ov511_reg_write(fd, OV511_REG_SNAP, 0x0) < 0) exit(1); /* disable compression */ if(ov511_reg_write(fd, OV511_REG_CE_EN, 0x0) < 0) exit(1); /* This returns 0 if we have an OV7620 sensor */ if((is20 = ov511_i2c_read(fd, OV7610_REG_COMI)) < 0) exit(1); is20 = !is20; fprintf(stderr, "is20: %d\n",is20); /* set up the OV7610/OV7620 */ if(is20) { ov511_i2c_write(fd, OV7610_REG_EC, 0xff); ov511_i2c_write(fd, OV7610_REG_FD, 0x06); ov511_i2c_write(fd, OV7610_REG_COMH, 0x24); ov511_i2c_write(fd, OV7610_REG_EHSL, 0xac); ov511_i2c_write(fd, OV7610_REG_COMA, 0x00); ov511_i2c_write(fd, OV7610_REG_COMH, 0x24); ov511_i2c_write(fd, OV7610_REG_RWB, 0x85); ov511_i2c_write(fd, OV7610_REG_COMD, 0x01); ov511_i2c_write(fd, 0x23, 0x00); ov511_i2c_write(fd, OV7610_REG_ECW, 0x10); ov511_i2c_write(fd, OV7610_REG_ECB, 0x8a); ov511_i2c_write(fd, OV7610_REG_COMG, 0xe2); ov511_i2c_write(fd, OV7610_REG_EHSH, 0x00); ov511_i2c_write(fd, OV7610_REG_EXBK, 0xfe); ov511_i2c_write(fd, 0x30, 0x71); ov511_i2c_write(fd, 0x31, 0x60); ov511_i2c_write(fd, 0x32, 0x26); ov511_i2c_write(fd, OV7610_REG_YGAM, 0x20); ov511_i2c_write(fd, OV7610_REG_BADJ, 0x48); ov511_i2c_write(fd, OV7610_REG_COMA, 0x24); ov511_i2c_write(fd, OV7610_REG_SYN_CLK, 0x01); ov511_i2c_write(fd, OV7610_REG_BBS, 0x24); ov511_i2c_write(fd, OV7610_REG_RBS, 0x24); } else { ov511_i2c_write(fd, OV7610_REG_RWB, 0x5); ov511_i2c_write(fd, OV7610_REG_EC, 0xFF); /* ov511_i2c_write(fd, OV7610_REG_COMB, 0x01); */ /* manual adjust mode */ ov511_i2c_write(fd, OV7610_REG_COMB, 0x00); ov511_i2c_write(fd, OV7610_REG_FD, 0x06); ov511_i2c_write(fd, OV7610_REG_COME, 0x1c); ov511_i2c_write(fd, OV7610_REG_COMF, 0x90); ov511_i2c_write(fd, OV7610_REG_ECW, 0x2e); ov511_i2c_write(fd, OV7610_REG_ECB, 0x7C); ov511_i2c_write(fd, OV7610_REG_COMH, 0x24); ov511_i2c_write(fd, OV7610_REG_EHSH, 0x04); ov511_i2c_write(fd, OV7610_REG_EHSL, 0xAC); ov511_i2c_write(fd, OV7610_REG_EXBK, 0xFE); /* ov511_i2c_write(fd, OV7610_REG_COMJ, 0x93);*/ /* manual control of brightness */ ov511_i2c_write(fd, OV7610_REG_COMJ, 0x83); ov511_i2c_write(fd, OV7610_REG_BADJ, 0x48); ov511_i2c_write(fd, OV7610_REG_COMK, 0x81); ov511_i2c_write(fd, OV7610_REG_GAM, 0x04); } /* Brightness control */ ov511_i2c_write(fd, OV7610_REG_BRT, 0x55); /* Saturation control */ ov511_i2c_write(fd, OV7610_REG_SAT, 0x65); /* Blue gain control */ ov511_i2c_write(fd, OV7610_REG_BLU, 0x01); /* Red gain control */ ov511_i2c_write(fd, OV7610_REG_RED, 0x51); /* RGB Gamma control */ ov511_i2c_write(fd, 0x62, 0x52); if(small) { vs.width = 320; vs.height = 240; ov511_reg_write(fd, OV511_REG_PXCNT, 0x27); ov511_reg_write(fd, OV511_REG_LNCNT, 0x1D); ov511_i2c_write(fd, OV7610_REG_SYN_CLK, 0x01); ov511_i2c_write(fd, OV7610_REG_COMA, 0x04); ov511_i2c_write(fd, OV7610_REG_COMC, 0x24); ov511_i2c_write(fd, OV7610_REG_COML, 0x9e); } else { vs.width = 640; vs.height = 480; ov511_reg_write(fd, OV511_REG_PXCNT, 0x4F); ov511_reg_write(fd, OV511_REG_LNCNT, 0x3D); ov511_i2c_write(fd, OV7610_REG_SYN_CLK, 0x06); ov511_i2c_write(fd, OV7610_REG_HE, 0x3a + (640>>2)); ov511_i2c_write(fd, OV7610_REG_VE, 5 + (480>>1)); ov511_i2c_write(fd, OV7610_REG_COMA, 0x24); ov511_i2c_write(fd, OV7610_REG_COMC, 0x04); ov511_i2c_write(fd, OV7610_REG_COML, 0x1e); } ov511_reg_write(fd, OV511_REG_PXDV, 0x00); ov511_reg_write(fd, OV511_REG_LNDV, 0x00); /* set FIFO format (993-byte packets) */ if(ov511_reg_write(fd, OV511_REG_PKSZ, bufsize/32) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_PKFMT, 0x03) < 0) exit(1); /* select the 993-byte alternative */ alt.uai_interface_index = 0; alt.uai_alt_index = (isplus ? 7 : 1); if(ioctl(fd, USB_SET_ALTINTERFACE, &alt) < 0) { perror("USB_SET_ALTINTERFACE"); exit(1); } /* reset the device again */ if(ov511_reg_write(fd, OV511_REG_RST, 0x3F) < 0) exit(1); if(ov511_reg_write(fd, OV511_REG_RST, 0x00) < 0) exit(1); vs.state = SKIPPING; vs.segsize = 384; vs.xels = pnm_allocarray(vs.width, vs.height); /* open the isochronous endpoint (endpoint 1) */ /*sprintf(isocdev, "%s.1", devname);*/ sprintf(isocdev, "%s", "/dev/usb/0.3.1"); /* awful hack :-) */ if((isoc = open(isocdev, O_RDONLY)) < 0) { perror(isocdev); fprintf(stderr, "info: it fails!\n"); exit(1); } fprintf(stderr, "info: /dev/usb/0.3.1 opened\n"); /* read, looking for start and end frames */ while(vs.state != DONE && (len = read(isoc, &buf, bufsize)) >= 0) { if(buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 0 && buf[4] == 0 && buf[5] == 0 && buf[6] == 0 && buf[7] == 0 && (buf[8] & 0x80) == 0 && buf[bufsize-1] == 0 && vs.state == SKIPPING) { vs.state = READING; vs.iY = vs.jY = vs.iUV = vs.jUV = 0; vs.residue = 0; procdata(&vs, buf + 9, bufsize - 10); } else if(buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 0 && buf[4] == 0 && buf[5] == 0 && buf[6] == 0 && buf[7] == 0 && (buf[8] & 0x80) == 0x80 && buf[bufsize-1] == 0 && vs.state == READING) { vs.state = DONE; } else if(vs.state == READING) { procdata(&vs, buf, bufsize - 1); /* abort the capture and start over if packets come in out-of-order */ if(buf[bufsize-1] != frmnm && buf[bufsize-1] != 1) { vs.state = SKIPPING; } frmnm = buf[bufsize-1] + 1; if(frmnm == 256) frmnm = 1; } else if(buf[bufsize-1] != 0) { frmnm = buf[bufsize-1] + 1; if(frmnm == 256) frmnm = 1; } } /* reset and close the OV511 */ ov511_reg_write(fd, OV511_REG_RST, 0x7f); close(isoc); close(fd); /* convert from CIE YCbCr color to RGB */ postproc(&vs); /* write out as PPM */ pnm_writepnm(stdout, vs.xels, vs.width, vs.height, 255, PPM_TYPE, 0); exit(0); } static void procdata(struct vidstate *vsp, u_char *data, size_t datalen) { size_t len = vsp->residue + datalen; int offset = 0; memcpy(vsp->buf + vsp->residue, data, datalen); while(len - offset >= vsp->segsize) { if(vsp->jY >= vsp->height && vsp->jUV >= vsp->height) { vsp->state = DONE; return; } else if(vsp->jY >= vsp->height || vsp->jUV >= vsp->height) { vsp->state = SKIPPING; return; } progsegment(vsp, vsp->buf + offset); vsp->iY += 32; /* each segment contains 32x8 Y pixels */ if(vsp->iY >= vsp->width) { vsp->iY = 0; vsp->jY += 8; } vsp->iUV += 16; /* and 16x16 downsampled U and V pixels */ if(vsp->iUV >= vsp->width) { vsp->iUV = 0; vsp->jUV += 16; } offset += vsp->segsize; } vsp->residue = len - offset; memmove(vsp->buf, vsp->buf + offset, len - offset); } /* The first 64 bytes of each segment are U, the next 64 are V. The U and V are arranged as follows: 0 1 ... 7 8 9 ... 15 ... 56 57 ... 63 U and V are shipped at half resolution (1 U,V sample -> one 2x2 block). The next 256 bytes are full resolution Y data and represent 4 squares of 8x8 pixels as follows: 0 1 ... 7 64 65 ... 71 ... 192 193 ... 199 8 9 ... 15 72 73 ... 79 200 201 ... 207 ... ... ... 56 57 ... 63 120 121 127 248 249 ... 255 Note that the U and V data in one segment represents a 16 x 16 pixel area, but the Y data represents a 32 x 8 pixel area. */ static void progsegment(struct vidstate *vsp, u_char *data) { int i, j; u_char *Up = data, *Vp = data + 64, *Yp = data + 128; xel *xp; for(j = 0; j < 8; ++j) { xp = vsp->xels[vsp->jUV + j * 2] + vsp->iUV; for(i = 0; i < 8; ++i) { PPM_ASSIGN(*xp, *Up, PPM_GETG(*xp), *Vp); Up++; Vp++; xp += 2; } } for(j = 0; j < 8; ++j) { xp = vsp->xels[vsp->jY + j] + vsp->iY; for(i = 0; i < 8; ++i) { PPM_ASSIGN(xp[0], PPM_GETR(xp[0]), Yp[0], PPM_GETB(xp[0])); PPM_ASSIGN(xp[8], PPM_GETR(xp[8]), Yp[64], PPM_GETB(xp[8])); PPM_ASSIGN(xp[16], PPM_GETR(xp[16]), Yp[128], PPM_GETB(xp[16])); PPM_ASSIGN(xp[24], PPM_GETR(xp[24]), Yp[192], PPM_GETB(xp[24])); Yp++; xp++; } } } #define LIMIT(x) ((x)>0xffffff?0xff: ((x)<=0xffff?0:((x)>>16))) static void postproc(struct vidstate *vsp) { int i, j; xel *xp, *xq; const double brightness = 1.0; /* 0->black; 1->full scale */ const double saturation = 1.0; /* 0->greyscale; 1->full color */ const double fixScale = brightness * 256 * 256; const int rvScale = (int)(1.402 * saturation * fixScale); const int guScale = (int)(-0.344136 * saturation * fixScale); const int gvScale = (int)(-0.714136 * saturation * fixScale); const int buScale = (int)(1.772 * saturation * fixScale); const int yScale = (int)(fixScale); for(j = 0; j < vsp->height; j += 2) { xp = vsp->xels[j]; xq = vsp->xels[j+1]; for(i = 0; i < vsp->width; i += 2) { int u = PPM_GETR(xp[0]) - 128; int v = PPM_GETB(xp[0]) - 128; int yTL = PPM_GETG(xp[0]) * yScale; int yTR = PPM_GETG(xp[1]) * yScale; int yBL = PPM_GETG(xq[0]) * yScale; int yBR = PPM_GETG(xq[1]) * yScale; int r = rvScale * v; int g = guScale * u + gvScale * v; int b = buScale * u; PPM_ASSIGN(xp[0], LIMIT(r+yTL), LIMIT(g+yTL), LIMIT(b+yTL)); PPM_ASSIGN(xp[1], LIMIT(r+yTR), LIMIT(g+yTR), LIMIT(b+yTR)); PPM_ASSIGN(xq[0], LIMIT(r+yBL), LIMIT(g+yBL), LIMIT(b+yBL)); PPM_ASSIGN(xq[1], LIMIT(r+yBR), LIMIT(g+yBR), LIMIT(b+yBR)); xp += 2; xq += 2; } } if(vsp->width > 400) { for(j = 0; j < vsp->height - 1; ++j) { xp = vsp->xels[j]; xq = vsp->xels[j+1]; for(i = 0; i < vsp->width; ++i) { PPM_PUTR(*xp, PPM_GETR(*xq)); ++xp; ++xq; } } for(j = vsp->height - 1; j >= 1; --j) { xp = vsp->xels[j-1]; xq = vsp->xels[j]; for(i = 0; i < vsp->width; ++i) { PPM_PUTB(*xq, PPM_GETB(*xp)); ++xp; ++xq; } } } }