Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Plunger/barCodeSensor.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 104:6e06e0f4b476
File content as of revision 116:7a67265d7c19:
// Plunger sensor type for bar-code based absolute position encoders. // This type of sensor uses an optical sensor that moves with the plunger // along a guide rail with printed bar codes along its length that encode // the absolute position at each point. We figure the plunger position // by reading the bar code and decoding it into a position figure. // // The bar code has to be encoded in a specific format that we recognize. // We use a reflected Gray code, optically encoded in black/white pixel // patterns. Each bit is represented by a fixed-width area. Half the // pixels in every bit are white, and half are black. A '0' bit is // represented by black pixels in the left half and white pixels in the // right half, and a '1' bit is white on the left and black on the right. // To read a bit, we identify the set of pixels covering the bit's fixed // area in the code, then we see if the left or right half is brighter. // // (This optical encoding scheme is based on Manchester coding, which is // normally used in the context of serial protocols, but translates to // bar codes straightforwardly. Replace the serial protocol's time // dimension with the spatial dimension across the bar, and replace the // high/low wire voltage levels with white/black pixels.) // // Gray codes are ideal for this type of application. Gray codes are // defined such that each code point differs in exactly one bit from each // adjacent code point. This provides natural error correction when used // as a position scale, since any single-bit error will yield a code point // reading that's only one spot off from the true position. So a bit read // error acts like a reduction in precision. Likewise, any time the sensor // is halfway between two code points, only one bit will be ambiguous, so // the reading will come out as one of points on either side of the true // position. Finally, motion blur will have the same effect, of creating // ambiguity in the least significant bits, and thus giving us a reading // that's correct to as many bits as we can make out. // // The half-and-half optical coding also has good properties for our // purposes. The fixed-width bit regions require essentially no CPU work // to find the bits, which is good because we're using a fairly slow CPU. // The half white/half black coding of each pixel makes every pixel // self-relative in terms of brightness, so we don't need to figure the // black and white thresholds globally for the whole image. That makes // the physical device engineering and installation easier because the // software can tolerate a fairly wide range of lighting conditions. // #ifndef _BARCODESENSOR_H_ #define _BARCODESENSOR_H_ #include "plunger.h" // Gray code to binary mapping for our special coding. This is a custom // 7-bit code, minimum run length 6, 110 positions populated. The minimum // run length is the minimum number of consecutive code points where each // bit must remain fixed. For out optical coding, this defines the smallest // "island" size for a black or white bar horizontally. Small features are // prone to light scattering that makes them appear gray on the sensor. // Larger features are less subject to scatter, making them easier to // distinguish by brightness level. static const uint8_t grayToBin[] = { 0, 1, 83, 2, 71, 100, 84, 3, 69, 102, 82, 128, 70, 101, 57, 4, // 0-15 35, 50, 36, 37, 86, 87, 85, 128, 34, 103, 21, 104, 128, 128, 20, 5, // 16-31 11, 128, 24, 25, 98, 99, 97, 40, 68, 67, 81, 80, 55, 54, 56, 41, // 32-47 10, 51, 23, 38, 128, 52, 128, 39, 9, 66, 22, 128, 8, 53, 7, 6, // 48-63 47, 14, 60, 128, 72, 15, 59, 16, 46, 91, 93, 92, 45, 128, 58, 17, // 64-79 48, 49, 61, 62, 73, 88, 74, 75, 33, 90, 106, 105, 32, 89, 19, 18, // 80-95 12, 13, 95, 26, 128, 28, 96, 27, 128, 128, 94, 79, 44, 29, 43, 42, // 96-111 128, 64, 128, 63, 110, 128, 109, 76, 128, 65, 107, 78, 31, 30, 108, 77 // 112-127 }; // Auto-exposure counter class BarCodeExposureCounter { public: BarCodeExposureCounter() { nDark = 0; nBright = 0; nZero = 0; nSat = 0; } inline void count(int pix) { if (pix <= 2) ++nZero; else if (pix < 12) ++nDark; else if (pix >= 253) ++nSat; else if (pix > 200) ++nBright; } int nDark; // dark pixels int nBright; // bright pixels int nZero; // pixels at zero brightness int nSat; // pixels at full saturation }; // Base class for bar-code sensors // // This is a template class with template parameters for the bar // code pixel structure. The bar code layout is fixed for a given // sensor type. We can assume fixed pixel sizes because we don't // have to process arbitrary images. We only have to read scales // specially prepared for this application, so we can count on them // being printed at an exact size relative to the sensor pixels. // // nBits = Number of bits in the code // // leftBarWidth = Width in pixels of delimiting left bar. The code is // delimited by a black bar on the "left" end, nearest pixel 0. This // gives the pixel width of the bar. // // leftBarMaxOfs = Maximum offset of the delimiting bar from the left // edge of the sensor (pixel 0), in pixels // // bitWidth = Width of each bit in pixels. This is the width of the // full bit, including both "half bits" - it's the full white/black or // black/white pattern area. struct BarCodeProcessResult { int pixofs; int raw; int mask; }; template <int nBits, int leftBarWidth, int leftBarMaxOfs, int bitWidth> class PlungerSensorBarCode: public PlungerSensorImage<BarCodeProcessResult> { public: PlungerSensorBarCode(PlungerSensorImageInterface &sensor, int npix) : PlungerSensorImage(sensor, npix, (1 << nBits) - 1) { startOfs = 0; } // process a configuration change virtual void onConfigChange(int varno, Config &cfg) { // check for bar-code variables switch (varno) { case 20: // bar code offset startOfs = cfg.plunger.barCode.startPix; break; } // do the generic work PlungerSensorImage::onConfigChange(varno, cfg); } protected: // process the image virtual bool process(const uint8_t *pix, int npix, int &pos, BarCodeProcessResult &res) { // adjust auto-exposure adjustExposure(pix, npix); // clear the result descriptor res.pixofs = 0; res.raw = 0; res.mask = 0; #if 0 // $$$ // scan from the left edge until we find the fixed '0' start bit for (int i = 0 ; i < leftBarMaxOfs ; ++i, ++pix) { // check for the '0' bit if (readBit8(pix) == 0) { // got it - skip the start bit pix += bitWidth; // read the gray code bits int gray = 0; for (int j = 0 ; j < nBits ; ++j, pix += bitWidth) { // read the bit; return failure if we can't decode a bit int bit = readBit8(pix); if (bit < 0) return false; // shift it into the code gray = (gray << 1) | bit; } } // convert the gray code to binary int bin = grayToBin(gray); // compute the parity of the binary value int parity = 0; for (int j = 0 ; j < nBits ; ++j) parity ^= bin >> j; // figure the bit required for odd parity int odd = (parity & 0x01) ^ 0x01; // read the check bit int bit = readBit8(pix); if (pix < 0) return false; // check that it matches the expected parity if (bit != odd) return false; // success pos = bin; return true; } // no code found return false; #else int barStart = leftBarMaxOfs/2; if (leftBarWidth != 0) // $$$ { // Find the black bar on the left side (nearest pixel 0) that // delimits the start of the bar code. To find it, first figure // the average brightness over the left margin up to the maximum // allowable offset, then look for the bar by finding the first // bar-width run of pixels that are darker than the average. int lsum = 0; for (int x = 1 ; x <= leftBarMaxOfs ; ++x) lsum += pix[x]; int lavg = lsum / leftBarMaxOfs; // now find the first dark edge for (int x = 0 ; x < leftBarMaxOfs ; ++x) { // if this pixel is dark, and one of the next two is dark, // take it as the edge if (pix[x] < lavg && (pix[x+1] < lavg || pix[x+2] < lavg)) { // move past the delimier barStart = x + leftBarWidth; break; } } } else { // start at the fixed pixel offset barStart = startOfs; } // Start with zero in the barcode and success mask. The mask // indicates which bits we were able to read successfully: a // '1' bit in the mask indicates that the corresponding bit // position in 'barcode' was successfully read, a '0' bit means // that the image was too fuzzy to read. int barcode = 0, mask = 0; // Scan the bits for (int bit = 0, x0 = barStart; bit < nBits ; ++bit, x0 += bitWidth) { #if 0 // Figure the extent of this bit. The last bit is double // the width of the other bits, to give us a better chance // of making out the small features of the last bit. int w = bitWidth; if (bit == nBits - 1) w *= 2; #else // width of the bit const int w = bitWidth; #endif // figure the bit's internal pixel layout int halfBitWidth = w / 2; int x1 = x0 + halfBitWidth; // midpoint int x2 = x0 + w; // right edge // make sure we didn't go out of bounds if (x1 > npix) x1 = npix; if (x2 > npix) x2 = npix; #if 0 // get the average of the pixels over the bit int sum = 0; for (int x = x0 ; x < x2 ; ++x) sum += pix[x]; int avg = sum / w; // Scan the left and right sections. Classify each // section according to whether the majority of its // pixels are above or below the local average. int lsum = 0, rsum = 0; for (int x = x0 + 1 ; x < x1 - 1 ; ++x) lsum += (pix[x] < avg ? 0 : 1); for (int x = x1 + 1 ; x < x2 - 1 ; ++x) rsum += (pix[x] < avg ? 0 : 1); #else // Sum the pixel readings in each half-bit. Ignore // the first and last bit of each section, since these // could be contaminated with scattered light from the // adjacent half-bit. On the right half, hew to the // right side if the overall pixel width is odd. int lsum = 0, rsum = 0; for (int x = x0 + 1 ; x < x1 - 1 ; ++x) lsum += pix[x]; for (int x = x2 - halfBitWidth + 1 ; x < x2 - 1 ; ++x) rsum += pix[x]; #endif // shift a zero bit into the code and success mask barcode <<= 1; mask <<= 1; // Brightness difference required per pixel. Higher values // require greater contrast to make a reading, which reduces // spurious readings at the cost of reducing the overall // success rate. The right level depends on the quality of // the optical system. Setting this to zero makes us maximally // tolerant of low-contrast images, allowing for the simplest // optical system. Our simple optical system suffers from // poor focus, which in turn causes poor contrast in small // features. const int minDelta = 2; // see if we could tell the difference in brightness int delta = lsum - rsum; if (delta < 0) delta = -delta; if (delta > minDelta * w/2) { // got it - black/white = 0, white/black = 1 if (lsum > rsum) barcode |= 1; mask |= 1; } } // decode the Gray code value to binary pos = grayToBin[barcode]; // set the results descriptor structure res.pixofs = barStart; res.raw = barcode; res.mask = mask; // return success if we decoded all bits, and the Gray-to-binary // mapping was populated return pos != (1 << nBits) && mask == ((1 << nBits) - 1); #endif } // read a bar starting at the given pixel int readBit8(const uint8_t *pix) { // pull out the pixels for the bar uint8_t s[8]; memcpy(s, pix, 8); // sort them in brightness order (using an 8-element network sort) #define SWAP(a, b) if (s[a] > s[b]) { uint8_t tmp = s[a]; s[a] = s[b]; s[b] = tmp; } SWAP(0, 1); SWAP(2, 3); SWAP(0, 2); SWAP(1, 3); SWAP(1, 2); SWAP(4, 5); SWAP(6, 7); SWAP(4, 6); SWAP(5, 7); SWAP(5, 6); SWAP(0, 4); SWAP(1, 5); SWAP(1, 4); SWAP(2, 6); SWAP(3, 7); SWAP(3, 6); SWAP(2, 4); SWAP(3, 5); SWAP(3, 4); #undef SWAP // figure the median brightness int median = (int(s[3]) + s[4] + 1) / 2; // count pixels below the median on each side int ldark = 0, rdark = 0; for (int i = 0 ; i < 3 ; ++i) { if (pix[i] < median) ldark++; } for (int i = 4 ; i < 8 ; ++i) { if (pix[i] < median) rdark++; } // we need >=3 dark + >=3 light or vice versa if (ldark >= 3 && rdark <= 1) { // dark + light = '0' bit return 0; } if (ldark <= 1 && rdark >= 3) { // light + dark = '1' bit return 1; } else { // ambiguous bit return -1; } } // bar code sensor orientation is fixed virtual int getOrientation() const { return 1; } // send extra status report headers virtual void extraStatusHeaders(USBJoystick &js, BarCodeProcessResult &res) { // Send the bar code status report. We use coding type 1 (Gray code, // Manchester pixel coding). js.sendPlungerStatusBarcode(nBits, 1, res.pixofs, bitWidth, res.raw, res.mask); } // adjust the exposure void adjustExposure(const uint8_t *pix, int npix) { #if 1 // The Manchester code has a nice property for auto exposure // control: each bit area has equal numbers of white and black // pixels. So we know exactly how the overall population of // pixels has to look: the bit area will be 50% black and 50% // white, and the margins will be all white. For maximum // contrast, target an exposure level where the black pixels // are all below a certain brightness level and the white // pixels are all above. Start by figuring the number of // pixels above and below. const int medianTarget = 160; int nBelow = 0; for (int i = 0 ; i < npix ; ++i) { if (pix[i] < medianTarget) ++nBelow; } // Figure the desired number of black pixels: the left bar is // all black pixels, and 50% of each bit is black pixels. int targetBelow = leftBarWidth + (nBits * bitWidth)/2; // Increase exposure time if too many pixels are below the // halfway point; decrease it if too many pixels are above. int d = nBelow - targetBelow; if (d > 5 || d < -5) { axcTime += d; } #elif 0 //$$$ // Count exposure levels of pixels in the left and right margins BarCodeExposureCounter counter; for (int i = 0 ; i < leftBarMaxOfs/2 ; ++i) { // count the pixels at the left and right margins counter.count(pix[i]); counter.count(pix[npix - i - 1]); } // The margin is all white, so try to get all of these pixels // in the bright range, but not saturated. That should give us // the best overall contrast throughout the image. if (counter.nSat > 0) { // overexposed - reduce exposure time if (axcTime > 5) axcTime -= 5; else axcTime = 0; } else if (counter.nBright < leftBarMaxOfs) { // they're not all in the bright range - increase exposure time axcTime += 5; } #else // $$$ // Count the number of pixels near total darkness and // total saturation int nZero = 0, nDark = 0, nBri = 0, nSat = 0; for (int i = 0 ; i < npix ; ++i) { int pi = pix[i]; if (pi <= 2) ++nZero; else if (pi < 12) ++nDark; else if (pi >= 254) ++nSat; else if (pi > 242) ++nBri; } // If more than 30% of pixels are near total darkness, increase // the exposure time. If more than 30% are near total saturation, // decrease the exposure time. int pct5 = uint32_t(npix * 3277) >> 16; int pct30 = uint32_t(npix * 19661) >> 16; int pct50 = uint32_t(npix) >> 1; if (nSat == 0) { // no saturated pixels - increase exposure time axcTime += 5; } else if (nSat > pct5) { if (axcTime > 5) axcTime -= 5; else axcTime = 0; } else if (nZero == 0) { // no totally dark pixels - decrease exposure time if (axcTime > 5) axcTime -= 5; else axcTime = 0; } else if (nZero > pct5) { axcTime += 5; } else if (nZero > pct30 || (nDark > pct50 && nSat < pct30)) { // very dark - increase exposure time a lot if (axcTime < 450) axcTime += 50; } else if (nDark > pct30 && nSat < pct30) { // dark - increase exposure time a bit if (axcTime < 490) axcTime += 10; } else if (nSat > pct30 || (nBri > pct50 && nDark < pct30)) { // very overexposed - decrease exposure time a lot if (axcTime > 50) axcTime -= 50; else axcTime = 0; } else if (nBri > pct30 && nDark < pct30) { // overexposed - decrease exposure time a little if (axcTime > 10) axcTime -= 10; else axcTime = 0; } #endif // don't allow the exposure time to go below 0 or over 2.5ms if (int(axcTime) < 0) axcTime = 0; if (axcTime > 2500) axcTime = 2500; } #if 0 // convert a reflected Gray code value (up to 16 bits) to binary static inline int grayToBin(int grayval) { int temp = grayval ^ (grayval >> 8); temp ^= (temp >> 4); temp ^= (temp >> 2); temp ^= (temp >> 1); return temp; } #endif // bar code starting pixel offset int startOfs; }; #endif