package goqr import ( "errors" "math" ) const ( thresholdSMin = 1 thresholdSDen = 8 thresholdT = 5 ) // Error definition var ( ErrOutOfRange = errors.New("out of range") ) // Recognizer is a Qr Code recognizer interface type Recognizer interface { SetPixel(x, y int, val uint8) Begin() End() Count() int Decode(index int) (*QRData, error) } type recognizer struct { pixels []uint8 w int h int regions []qrRegion capstones []qrCapstone grids []qrGrid } // NewRecognizer news a recognizer func NewRecognizer(w, h int) Recognizer { if w <= 0 || h <= 0 { return nil } return &recognizer{ h: h, w: w, pixels: make([]qrPixelType, w*h), } } func (q *recognizer) SetPixel(x, y int, val uint8) { q.pixels[x+y*q.w] = val } func (q *recognizer) Count() int { return len(q.grids) } func (q *recognizer) Begin() { q.regions = make([]qrRegion, qrPixelRegion) } func (q *recognizer) End() { q.threshold() for i := 0; i < q.h; i++ { q.finderScan(i) } for i := 0; i < len(q.capstones); i++ { q.testGrouping(i) } } func (q *recognizer) extract(index int) (*qrCode, error) { code := &qrCode{} qr := &q.grids[index] if index < 0 || index >= len(q.grids) { return nil, ErrOutOfRange } perspectiveMap(qr.c[:], 0.0, 0.0, &code.corners[0]) perspectiveMap(qr.c[:], float64(qr.gridSize), 0.0, &code.corners[1]) perspectiveMap(qr.c[:], float64(qr.gridSize), float64(qr.gridSize), &code.corners[2]) perspectiveMap(qr.c[:], 0.0, float64(qr.gridSize), &code.corners[3]) code.size = qr.gridSize i := uint(0) for y := 0; y < qr.gridSize; y++ { for x := 0; x < qr.gridSize; x++ { if q.readCell(index, x, y) > 0 { code.cellBitmap[i>>3] |= uint8(1 << (i & 7)) } i++ } } return code, nil } func (q *recognizer) Decode(index int) (*QRData, error) { code, err := q.extract(index) if err != nil { return nil, err } var data QRData data.Payload = make([]uint8, 0) err = decode(code, &data) return &data, err } func (q *recognizer) threshold() { var x, y int avgW := 0 avgU := 0 thresholds := q.w / thresholdSDen // Ensure a sane, non-zero value for threshold_s. // threshold_s can be zero if the image width is small. We need to avoid // SIGFPE as it will be used as divisor. if thresholds < thresholdSMin { thresholds = thresholdSMin } for y = 0; y < q.h; y++ { row := q.pixels[q.w*y : q.w*(y+1)] rowAverage := make([]int, q.w) for x = 0; x < q.w; x++ { var w, u int if y&1 == 1 { w = x u = q.w - 1 - x } else { w = q.w - 1 - x u = x } avgW = (avgW*(thresholds-1))/thresholds + int(row[w]) avgU = (avgU*(thresholds-1))/thresholds + int(row[u]) rowAverage[w] += avgW rowAverage[u] += avgU } for x = 0; x < q.w; x++ { if int(row[x]) < rowAverage[x]*(100-thresholdT)/(200*thresholds) { row[x] = qrPixelBlack } else { row[x] = qrPixelWhite } } } } const floodFileMaxDepth = 4096 type spanFunc func(userData interface{}, y, left, right int) func (q *recognizer) floodFillSeed(x, y, from, to int, span spanFunc, userData interface{}, depth int) { left := x right := x row := q.pixels[y*q.w : (y+1)*q.w] if depth >= floodFileMaxDepth { return } for left > 0 && int(row[left-1]) == from { left-- } for right < q.w-1 && int(row[right+1]) == from { right++ } // Fill the extent for i := left; i <= right; i++ { row[i] = qrPixelType(to) } if span != nil { span(userData, y, left, right) } // Seed new flood-fills if y > 0 { row = q.pixels[(y-1)*q.w : (y)*q.w] for i := left; i <= right; i++ { if int(row[i]) == from { q.floodFillSeed(i, y-1, from, to, span, userData, depth+1) } } } if y < q.h-1 { row = q.pixels[(y+1)*q.w : (y+2)*q.w] for i := left; i <= right; i++ { if int(row[i]) == from { q.floodFillSeed(i, y+1, from, to, span, userData, depth+1) } } } } func areaCount(userData interface{}, y, left, right int) { region := userData.(*qrRegion) region.count += right - left + 1 } func (q *recognizer) regionCode(x, y int) int { if x < 0 || y < 0 || x >= q.w || y >= q.h { return -1 } pixel := int(q.pixels[y*q.w+x]) if pixel >= qrPixelRegion { return pixel } if pixel == qrPixelWhite { return -1 } if len(q.regions) >= qrMaxRegion { return -1 } region := len(q.regions) q.regions = append(q.regions, qrRegion{}) box := &q.regions[region] box.seed.x = x box.seed.y = y box.count = 0 box.capstone = -1 q.floodFillSeed(x, y, pixel, region, areaCount, box, 0) return region } func findOneCorner(userData interface{}, y, left, right int) { psd := userData.(*polygonScoreData) xs := [2]int{left, right} dy := y - psd.ref.y for i := 0; i < 2; i++ { dx := xs[i] - psd.ref.x d := dx*dx + dy*dy if d > psd.scores[0] { psd.scores[0] = d psd.corners[0].x = xs[i] psd.corners[0].y = y } } } func findOtherCorners(userData interface{}, y, left, right int) { psd := userData.(*polygonScoreData) xs := [2]int{left, right} for i := 0; i < 2; i++ { up := xs[i]*psd.ref.x + y*psd.ref.y right := xs[i]*-psd.ref.y + y*psd.ref.x scores := [4]int{up, right, -up, -right} for j := 0; j < 4; j++ { if scores[j] > psd.scores[j] { psd.scores[j] = scores[j] psd.corners[j].x = xs[i] psd.corners[j].y = y } } } } func (q *recognizer) findRegionCorners(rcode int, ref *point, corners []point) { region := &q.regions[rcode] psd := polygonScoreData{} psd.corners = corners[:] psd.ref = *ref psd.scores[0] = -1 q.floodFillSeed(region.seed.x, region.seed.y, rcode, qrPixelBlack, findOneCorner, &psd, 0) psd.ref.x = psd.corners[0].x - psd.ref.x psd.ref.y = psd.corners[0].y - psd.ref.y for i := 0; i < 4; i++ { psd.corners[i] = region.seed } i := region.seed.x*psd.ref.x + region.seed.y*psd.ref.y psd.scores[0] = i psd.scores[2] = -i i = region.seed.x*-psd.ref.y + region.seed.y*psd.ref.x psd.scores[1] = i psd.scores[3] = -i q.floodFillSeed(region.seed.x, region.seed.y, qrPixelBlack, rcode, findOtherCorners, &psd, 0) } func (q *recognizer) recordCapstone(ring, stone int) { stoneReg := &q.regions[stone] ringReg := &q.regions[ring] if len(q.capstones) >= qrMaxCastones { return } csIndex := len(q.capstones) q.capstones = append(q.capstones, qrCapstone{}) capstone := &q.capstones[csIndex] capstone.qrGrid = -1 capstone.ring = ring capstone.stone = stone stoneReg.capstone = csIndex ringReg.capstone = csIndex // Find the corners of the ring q.findRegionCorners(ring, &stoneReg.seed, capstone.corners[:]) // Set up the perspective transform and find the center perspectiveSetup(capstone.c[:], capstone.corners[:], 7.0, 7.0) perspectiveMap(capstone.c[:], 3.5, 3.5, &capstone.center) } func perspectiveSetup(c []float64, rect []point, w, h float64) { x0 := float64(rect[0].x) y0 := float64(rect[0].y) x1 := float64(rect[1].x) y1 := float64(rect[1].y) x2 := float64(rect[2].x) y2 := float64(rect[2].y) x3 := float64(rect[3].x) y3 := float64(rect[3].y) wden := w * (x2*y3 - x3*y2 + (x3-x2)*y1 + x1*(y2-y3)) hden := h * (x2*y3 + x1*(y2-y3) - x3*y2 + (x3-x2)*y1) c[0] = (x1*(x2*y3-x3*y2) + x0*(-x2*y3+x3*y2+(x2-x3)*y1) + x1*(x3-x2)*y0) / wden c[1] = -(x0*(x2*y3+x1*(y2-y3)-x2*y1) - x1*x3*y2 + x2*x3*y1 + (x1*x3-x2*x3)*y0) / hden c[2] = x0 c[3] = (y0*(x1*(y3-y2)-x2*y3+x3*y2) + y1*(x2*y3-x3*y2) + x0*y1*(y2-y3)) / wden c[4] = (x0*(y1*y3-y2*y3) + x1*y2*y3 - x2*y1*y3 + y0*(x3*y2-x1*y2+(x2-x3)*y1)) / hden c[5] = y0 c[6] = (x1*(y3-y2) + x0*(y2-y3) + (x2-x3)*y1 + (x3-x2)*y0) / wden c[7] = (-x2*y3 + x1*y3 + x3*y2 + x0*(y1-y2) - x3*y1 + (x2-x1)*y0) / hden } func perspectiveMap(c []float64, u, v float64, ret *point) { den := c[6]*u + c[7]*v + 1.0 x := (c[0]*u + c[1]*v + c[2]) / den y := (c[3]*u + c[4]*v + c[5]) / den ret.x = int(x + 0.5) ret.y = int(y + 0.5) } func perspectiveUnmap(c []float64, in *point, u, v *float64) { x := float64(in.x) y := float64(in.y) den := -c[0]*c[7]*y + c[1]*c[6]*y + (c[3]*c[7]-c[4]*c[6])*x + c[0]*c[4] - c[1]*c[3] *u = -(c[1]*(y-c[5]) - c[2]*c[7]*y + (c[5]*c[7]-c[4])*x + c[2]*c[4]) / den *v = (c[0]*(y-c[5]) - c[2]*c[6]*y + (c[5]*c[6]-c[3])*x + c[2]*c[3]) / den } func (q *recognizer) testCapstone(x, y int, pb []int) { ringRight := q.regionCode(x-pb[4], y) stone := q.regionCode(x-pb[4]-pb[3]-pb[2], y) ringLeft := q.regionCode(x-pb[4]-pb[3]-pb[2]-pb[1]-pb[0], y) if ringLeft < 0 || ringRight < 0 || stone < 0 { return } // Left and ring of ring should be connected if ringLeft != ringRight { return } // Ring should be disconnected from stone if ringLeft == stone { return } stoneReg := &q.regions[stone] ringReg := &q.regions[ringLeft] /* Already detected */ if stoneReg.capstone >= 0 || ringReg.capstone >= 0 { return } // Ratio should ideally be 37.5 ratio := stoneReg.count * 100 / ringReg.count if ratio < 10 || ratio > 70 { return } q.recordCapstone(ringLeft, stone) } func (q *recognizer) finderScan(y int) { row := q.pixels[y*q.w : (y+1)*q.w] x := 0 lastColor := 0 runLength := 0 runCount := 0 pb := make([]int, 5) check := [5]int{1, 1, 3, 1, 1} for x = 0; x < q.w; x++ { color := 0 if row[x] > 0 { color = 1 } if x > 0 && color != lastColor { for i := 0; i < 4; i++ { pb[i] = pb[i+1] } pb[4] = runLength runLength = 0 runCount++ if color == 0 && runCount >= 5 { var avg, err int ok := true avg = (pb[0] + pb[1] + pb[3] + pb[4]) / 4 err = avg * 3 / 4 for i := 0; i < 5; i++ { if pb[i] < check[i]*avg-err || pb[i] > check[i]*avg+err { ok = false break } } if ok { q.testCapstone(x, y, pb) } } } runLength++ lastColor = color } } func (q *recognizer) findAlignmentPattern(index int) { qr := &q.grids[index] c0 := &q.capstones[qr.caps[0]] c2 := &q.capstones[qr.caps[2]] var a, b, c point stepSize := 1 dir := 0 var u, v float64 // Grab our previous estimate of the alignment pattern corner b = qr.align // Guess another two corners of the alignment pattern so that we // can estimate its size. perspectiveUnmap(c0.c[:], &b, &u, &v) perspectiveMap(c0.c[:], u, v+1.0, &a) perspectiveUnmap(c2.c[:], &b, &u, &v) perspectiveMap(c2.c[:], u+1.0, v, &c) sizeEstimate := int(math.Abs(float64((a.x-b.x)*-(c.y-b.y) + (a.y-b.y)*(c.x-b.x)))) // Spiral outwards from the estimate point until we find something // roughly the right size. Don't look too far from the estimate point for stepSize*stepSize < sizeEstimate*100 { dxMap := []int{1, 0, -1, 0} dyMap := []int{0, -1, 0, 1} for i := 0; i < stepSize; i++ { code := q.regionCode(b.x, b.y) if code >= 0 { reg := &q.regions[code] if reg.count >= sizeEstimate/2 && reg.count <= sizeEstimate*2 { qr.alignRegion = code return } } b.x += dxMap[dir] b.y += dyMap[dir] } dir = (dir + 1) % 4 if (dir & 1) == 1 { stepSize++ } } } // readCell read a cell from a grid using the currently set perspective // transform. Returns +/- 1 for black/white, 0 for cells which are // out of image bounds. func (q *recognizer) readCell(index, x, y int) int { qr := &q.grids[index] var p point perspectiveMap(qr.c[:], float64(x)+0.5, float64(y)+0.5, &p) if p.y < 0 || p.y >= q.h || p.x < 0 || p.x >= q.w { return 0 } if q.pixels[p.y*q.w+p.x] != 0 { return 1 } return -1 } func (q *recognizer) fitnessCell(index, x, y int) int { qr := &q.grids[index] score := 0 offsets := []float64{0.3, 0.5, 0.7} for v := 0; v < 3; v++ { for u := 0; u < 3; u++ { var p point perspectiveMap(qr.c[:], float64(x)+offsets[u], float64(y)+offsets[v], &p) if p.y < 0 || p.y >= q.h || p.x < 0 || p.x >= q.w { continue } if q.pixels[p.y*q.w+p.x] != 0 { score++ } else { score-- } } } return score } func (q *recognizer) fitnessRing(index, cx, cy, radius int) int { score := 0 for i := 0; i < radius*2; i++ { score += q.fitnessCell(index, cx-radius+i, cy-radius) score += q.fitnessCell(index, cx-radius, cy+radius-i) score += q.fitnessCell(index, cx+radius, cy-radius+i) score += q.fitnessCell(index, cx+radius-i, cy+radius) } return score } func (q *recognizer) fitnessApat(index, cx, cy int) int { return q.fitnessCell(index, cx, cy) - q.fitnessRing(index, cx, cy, 1) + q.fitnessRing(index, cx, cy, 2) } func (q *recognizer) fitnessCapstone(index, x, y int) int { x += 3 y += 3 return q.fitnessCell(index, x, y) + q.fitnessRing(index, x, y, 1) - q.fitnessRing(index, x, y, 2) + q.fitnessRing(index, x, y, 3) } // fitnessAll compute a fitness score for the currently configured perspective // transform, using the features we expect to find by scanning the // grid. func (q *recognizer) fitnessAll(index int) int { qr := &q.grids[index] version := (qr.gridSize - 17) / 4 info := &qrVersionDb[version] score := 0 // Check the timing pattern for i := 0; i < qr.gridSize-14; i++ { expect := 1 if i&1 == 0 { expect = -1 } score += q.fitnessCell(index, i+7, 6) * expect score += q.fitnessCell(index, 6, i+7) * expect } // Check capstones score += q.fitnessCapstone(index, 0, 0) score += q.fitnessCapstone(index, qr.gridSize-7, 0) score += q.fitnessCapstone(index, 0, qr.gridSize-7) if version < 0 || version > qrMaxVersion { return score } // Check alignment patterns apCount := 0 for (apCount < qrMaxAliment) && info.apat[apCount] != 0 { apCount++ } for i := 1; i+1 < apCount; i++ { score += q.fitnessApat(index, 6, info.apat[i]) score += q.fitnessApat(index, info.apat[i], 6) } for i := 1; i < apCount; i++ { for j := 1; j < apCount; j++ { score += q.fitnessApat(index, info.apat[i], info.apat[j]) } } return score } func (q *recognizer) jigglePerspective(index int) { qr := &q.grids[index] best := q.fitnessAll(index) adjustments := make([]float64, 8) for i := 0; i < 8; i++ { adjustments[i] = qr.c[i] * 0.02 } for pass := 0; pass < 5; pass++ { for i := 0; i < 16; i++ { j := i >> 1 old := qr.c[j] step := adjustments[j] var new float64 if i&1 == 1 { new = old + step } else { new = old - step } qr.c[j] = new test := q.fitnessAll(index) if test > best { best = test } else { qr.c[j] = old } } for i := 0; i < 8; i++ { adjustments[i] *= 0.5 } } } // Once the capstones are in place and an alignment point has been chosen, // we call this function to set up a grid-reading perspective transform. func (q *recognizer) setupQrPerspective(index int) { qr := &q.grids[index] var rect [4]point /* Set up the perspective map for reading the grid */ rect[0] = q.capstones[qr.caps[1]].corners[0] rect[1] = q.capstones[qr.caps[2]].corners[0] rect[2] = qr.align rect[3] = q.capstones[qr.caps[0]].corners[0] perspectiveSetup(qr.c[:], rect[:], float64(qr.gridSize-7), float64(qr.gridSize-7)) q.jigglePerspective(index) } func rotateCapstone(cap *qrCapstone, h0, hd *point) { copy := [4]point{} var best int var bestScore int for j := 0; j < 4; j++ { p := &cap.corners[j] score := (p.x-h0.x)*-hd.y + (p.y-h0.y)*hd.x if j == 0 || score < bestScore { best = j bestScore = score } } ///* Rotate the capstone */ for j := 0; j < 4; j++ { copy[j] = cap.corners[(j+best)%4] } for j := 0; j < 4; j++ { cap.corners[j] = copy[j] } perspectiveSetup(cap.c[:], cap.corners[:], 7.0, 7.0) } func (q *recognizer) timingScan(p0, p1 *point) int { n := p1.x - p0.x d := p1.y - p0.y x := p0.x y := p0.y a := 0 runlength := 0 count := 0 var dom, nondom *int var domStep int var nondomStep int if p0.x < 0 || p0.y < 0 || p0.x >= q.w || p0.y >= q.h { return -1 } if p1.x < 0 || p1.y < 0 || p1.x >= q.w || p1.y >= q.h { return -1 } if math.Abs(float64(n)) > math.Abs(float64(d)) { n, d = d, n dom = &x nondom = &y } else { dom = &y nondom = &x } if n < 0 { n = -n nondomStep = -1 } else { nondomStep = 1 } if d < 0 { d = -d domStep = -1 } else { domStep = 1 } x = p0.x y = p0.y for i := 0; i <= d; i++ { if y < 0 || y >= q.h || x < 0 || x >= q.w { break } pixel := q.pixels[y*q.w+x] if pixel > 0 { if runlength >= 2 { count++ } runlength = 0 } else { runlength++ } a += n *dom += domStep if a >= d { *nondom += nondomStep a -= d } } return count } func findLeftMostToLine(userData interface{}, y, left, right int) { psd := userData.(*polygonScoreData) xs := []int{left, right} for i := 0; i < 2; i++ { d := -psd.ref.y*xs[i] + psd.ref.x*y if d < psd.scores[0] { psd.scores[0] = d psd.corners[0].x = xs[i] psd.corners[0].y = y } } } // Try the measure the timing pattern for a given QR code. This does // not require the global perspective to have been set up, but it // does require that the capstone corners have been set to their // canonical rotation. // // For each capstone, we find a point in the middle of the ring band // which is nearest the centre of the code. Using these points, we do // a horizontal and a vertical timing scan. func (q *recognizer) measureTimingPattern(index int) int { qr := &q.grids[index] for i := 0; i < 3; i++ { us := []float64{6.5, 6.5, 0.5} vs := []float64{0.5, 6.5, 6.5} cap := &q.capstones[qr.caps[i]] perspectiveMap(cap.c[:], us[i], vs[i], &qr.tpep[i]) } qr.hscan = q.timingScan(&qr.tpep[1], &qr.tpep[2]) qr.vscan = q.timingScan(&qr.tpep[1], &qr.tpep[0]) scan := qr.hscan if qr.vscan > scan { scan = qr.vscan } // If neither scan worked, we can't go any further. if scan < 0 { return -1 } // Choose the nearest allowable grid size size := scan*2 + 13 ver := (size - 15) / 4 qr.gridSize = ver*4 + 17 return 0 } func (q *recognizer) recordQrGrid(a, b, c int) { if len(q.grids) >= qrMaxGrids { return } // Construct the hypotenuse line from A to C. B should be tothe left of this line. h0 := q.capstones[a].center var hd point hd.x = q.capstones[c].center.x - q.capstones[a].center.x hd.y = q.capstones[c].center.y - q.capstones[a].center.y // Make sure A-B-C is clockwise if (q.capstones[b].center.x-h0.x)*-hd.y+(q.capstones[b].center.y-h0.y)*hd.x > 0 { a, c = c, a hd.x = -hd.x hd.y = -hd.y } qrIndex := len(q.grids) q.grids = append(q.grids, qrGrid{}) qr := &q.grids[qrIndex] qr.caps[0] = a qr.caps[1] = b qr.caps[2] = c qr.alignRegion = -1 // Rotate each capstone so that corner 0 is top-left with respect // to the grid. for i := 0; i < 3; i++ { cap := &q.capstones[qr.caps[i]] rotateCapstone(cap, &h0, &hd) cap.qrGrid = qrIndex } // Check the timing pattern. This doesn't require a perspective transform. if q.measureTimingPattern(qrIndex) < 0 { goto fail } // Make an estimate based for the alignment pattern based on extending lines from capstones A and C. if !lineIntersect(&q.capstones[a].corners[0], &q.capstones[a].corners[1], &q.capstones[c].corners[0], &q.capstones[c].corners[3], &qr.align) { goto fail } // On V2+ grids, we should use the alignment pattern. if qr.gridSize > 21 { // Try to find the actual location of the alignment pattern. q.findAlignmentPattern(qrIndex) // Find the point of the alignment pattern closest to the // top-left of the QR grid. if qr.alignRegion >= 0 { var psd polygonScoreData psd.corners = make([]point, 1) reg := &q.regions[qr.alignRegion] // Start from some point inside the alignment pattern qr.align = reg.seed psd.ref = hd psd.corners[0] = qr.align psd.scores[0] = -hd.y*qr.align.x + hd.x*qr.align.y q.floodFillSeed(reg.seed.x, reg.seed.y, qr.alignRegion, qrPixelBlack, nil, nil, 0) q.floodFillSeed(reg.seed.x, reg.seed.y, qrPixelBlack, qr.alignRegion, findLeftMostToLine, &psd, 0) qr.align = psd.corners[0] } } q.setupQrPerspective(qrIndex) return // We've been unable to complete setup for this grid. Undo what we've // recorded and pretend it never happened. fail: for i := 0; i < 3; i++ { q.capstones[qr.caps[i]].qrGrid = -1 } q.grids = q.grids[:len(q.grids)-1] } func (q *recognizer) testNeighbours(i int, hlist []*neighbour, vlist []*neighbour) { bestScore := 0.0 bestH := -1 bestV := -1 // Test each possible grouping for j := 0; j < len(hlist); j++ { hn := hlist[j] for k := 0; k < len(vlist); k++ { vn := vlist[k] score := math.Abs(1.0 - hn.distance/vn.distance) if score > 2.5 { continue } if bestH < 0 || score < bestScore { bestH = hn.index bestV = vn.index bestScore = score } } } if bestH < 0 || bestV < 0 { return } q.recordQrGrid(bestH, i, bestV) } func (q *recognizer) testGrouping(i int) { c1 := &q.capstones[i] hlist := make([]*neighbour, 0) vlist := make([]*neighbour, 0) if c1.qrGrid >= 0 { return } // Look for potential neighbours by examining the relative gradients // from this capstone to others. for j := 0; j < len(q.capstones); j++ { c2 := &q.capstones[j] if i == j || c2.qrGrid >= 0 { continue } var u, v float64 perspectiveUnmap(c1.c[:], &c2.center, &u, &v) u = math.Abs(u - 3.5) v = math.Abs(v - 3.5) if u < 0.2*v { n := &neighbour{} n.index = j n.distance = v hlist = append(hlist, n) } if v < 0.2*u { n := &neighbour{} n.index = j n.distance = u vlist = append(vlist, n) } } if !(len(hlist) > 0 && len(vlist) > 0) { return } q.testNeighbours(i, hlist, vlist) }