Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 126 additions & 4 deletions qrcode/detector/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,23 @@ func (this *Detector) Detect(hints map[gozxing.DecodeHintType]interface{}) (*com
return nil, e
}

return this.ProcessFinderPatternInfo(info)
result, err := this.ProcessFinderPatternInfo(info)
if err != nil {
// The initial finder pattern triple failed validation (timing,
// dimension, or version mismatch). This usually means one of the
// three patterns is a false positive from QR data that mimics the
// finder pattern ratio, and the scanner stopped before finding
// the real third pattern. Retry with an exhaustive scan that
// collects ALL candidates, letting SelectBestPatterns pick the
// geometrically best triple.
exhaustiveFinder := NewFinderPatternFinder(this.image, this.resultPointCallback)
info, e = exhaustiveFinder.FindExhaustive()
if e != nil {
return nil, e
}
return this.ProcessFinderPatternInfo(info)
}
return result, nil
}

func (this *Detector) ProcessFinderPatternInfo(info *FinderPatternInfo) (*common.DetectorResult, error) {
Expand All @@ -55,6 +71,17 @@ func (this *Detector) ProcessFinderPatternInfo(info *FinderPatternInfo) (*common
if moduleSize < 1.0 {
return nil, gozxing.NewNotFoundException("moduleSize = %v", moduleSize)
}

// Validate that timing patterns exist between the finder patterns.
// Timing patterns are alternating black-white lines at module-size
// intervals along row 6 (TL -> TR) and column 6 (TL -> BL). A false
// positive finder pattern won't have these connecting it to the
// real patterns.
if !this.checkTimingPattern(topLeft, topRight, bottomLeft, moduleSize) ||
!this.checkTimingPattern(topLeft, bottomLeft, topRight, moduleSize) {
return nil, gozxing.NewNotFoundException("timing pattern validation failed")
}

dimension, e := this.computeDimension(topLeft, topRight, bottomLeft, moduleSize)
if e != nil {
return nil, e
Expand Down Expand Up @@ -155,22 +182,117 @@ func Detector_sampleGrid(image *gozxing.BitMatrix, transform *common.Perspective
func (this *Detector) computeDimension(topLeft, topRight, bottomLeft gozxing.ResultPoint, moduleSize float64) (int, error) {
tltrCentersDimension := util.MathUtils_Round(gozxing.ResultPoint_Distance(topLeft, topRight) / moduleSize)
tlblCentersDimension := util.MathUtils_Round(gozxing.ResultPoint_Distance(topLeft, bottomLeft) / moduleSize)

// The two side measurements should be close for a correctly identified
// finder pattern triple. A large discrepancy likeluy indicates a misidentified
// pattern (e.g. a false positive from QR data mimicking the finder
// pattern ratio). Reject early rather than averaging garbage data.
//
// Each side independently implies a dimension (modules + 7). Round
// each to the nearest valid QR dimension (≡ 1 mod 4) and check they
// agree. This catches cases where the average looks plausible but the
// two sides would produce different QR versions.
dimFromTR := tltrCentersDimension + 7
dimFromBL := tlblCentersDimension + 7
roundQR := func(d int) int {
switch d % 4 {
case 0:
return d + 1
case 2:
return d - 1
case 3:
return d // invalid, let downstream catch it
}
return d
}
dimDiff := roundQR(dimFromTR) - roundQR(dimFromBL)
if dimDiff < 0 {
dimDiff = -dimDiff
}
// Allow sides to differ by at most one QR version (4 modules) to
// accommodate perspective skew from camera captures. A false positive
// finder pattern typically produces a difference of 2+ versions.
if dimDiff > 4 {
return 0, gozxing.NewNotFoundException(
"dimension mismatch: sides imply %v vs %v", roundQR(dimFromTR), roundQR(dimFromBL))
}

dimension := ((tltrCentersDimension + tlblCentersDimension) / 2) + 7
switch dimension % 4 {
default: // 1? do nothing
break
case 0:
dimension++
break
case 2:
dimension--
break
case 3:
return 0, gozxing.NewNotFoundException("dimension = %v", dimension)
}
return dimension, nil
}

// checkTimingPattern verifies that an alternating black-white timing pattern
// exists between two finder pattern centers. The timing pattern runs along
// row 6 (horizontal, TL -> TR) or column 6 (vertical, TL -> BL) of the QR code.
// It samples the line between the two centers and counts black-white
// transitions. A valid timing pattern should have roughly one transition
// per module. A false positive finder pattern won't have this structure.
//
// The QR timing pattern runs along row 6 and column 6 — offset 3 modules
// from the finder center toward the QR interior. We sample along that
// offset line and count transitions.
func (this *Detector) checkTimingPattern(from, to, third gozxing.ResultPoint, moduleSize float64) bool {
dx := to.GetX() - from.GetX()
dy := to.GetY() - from.GetY()
dist := math.Sqrt(dx*dx + dy*dy)
modules := dist / moduleSize

if modules < 30 {
// Too short to validate reliably. False positive finder
// patterns are predominantly a problem with larger QR codes
// (version 7+, 45+ modules) where the data area is large
// enough to accidentally contain finder-like patterns.
return true
}

// The timing pattern is offset 3 modules from the center line
// toward the third finder pattern (the QR interior).
thirdDx := third.GetX() - from.GetX()
thirdDy := third.GetY() - from.GetY()

// Perpendicular component: project (from→third) onto the normal of (from→to)
perpX := thirdDx - (thirdDx*dx+thirdDy*dy)/(dist*dist)*dx
perpY := thirdDy - (thirdDx*dx+thirdDy*dy)/(dist*dist)*dy
perpDist := math.Sqrt(perpX*perpX + perpY*perpY)
if perpDist < moduleSize {
return true // degenerate, skip check
}
offsetX := perpX / perpDist * 3 * moduleSize
offsetY := perpY / perpDist * 3 * moduleSize

// Sample along the offset line, skipping 3.5 modules at each end
// to avoid the finder patterns themselves.
skip := 3.5 * moduleSize / dist
transitions := 0
prevBlack := false
steps := int(modules)
for i := 0; i <= steps; i++ {
t := skip + float64(i)*(1.0-2*skip)/float64(steps)
x := int(from.GetX() + offsetX + t*dx)
y := int(from.GetY() + offsetY + t*dy)
black := this.image.Get(x, y)
if i > 0 && black != prevBlack {
transitions++
}
prevBlack = black
}

// A perfect timing pattern has roughly (modules - 7) transitions.
// If we see random QR data, it should typically produce
// ~50% of expected. Require at least 30% to distinguish timing from noise.
expectedTransitions := modules - 7
return float64(transitions) >= expectedTransitions*0.3
}

func (this *Detector) calculateModuleSize(topLeft, topRight, bottomLeft gozxing.ResultPoint) float64 {
// Take the average
return (this.calculateModuleSizeOneWay(topLeft, topRight) +
Expand Down
45 changes: 42 additions & 3 deletions qrcode/detector/detector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ import (
"github.com/makiuchi-d/gozxing"
)

// makeTimingPattern draws an alternating black-white timing line between two
// finder pattern centers. m is the module size. The timing runs from
// (center + 4*m) to (other center - 4*m) along the horizontal or vertical
// axis, alternating every m pixels.
func makeTimingPattern(image *gozxing.BitMatrix, fromX, fromY, toX, toY, m int) {
dx, dy := 0, 0
dist := 0
if fromX != toX {
if toX > fromX {
dx = 1
} else {
dx = -1
}
dist = (toX - fromX) * dx
} else {
if toY > fromY {
dy = 1
} else {
dy = -1
}
dist = (toY - fromY) * dy
}
// Start 4 modules from the first center, end 4 modules from the second
start := 4 * m
end := dist - 4*m
for i := start; i <= end; i += m {
module := (i - start) / m
if module%2 == 0 {
// Set black pixel(s) for this module
x := fromX + i*dx
y := fromY + i*dy
for p := 0; p < m; p++ {
image.Set(x+p*dx, y+p*dy)
}
}
}
}

func makeAlignPattern(image *gozxing.BitMatrix, x, y int) {
image.SetRegion(x-2, y-2, 5, 5)
unsetRegion(image, x-1, y-1, 3, 3)
Expand All @@ -19,6 +57,7 @@ func makeAlignPattern3(image *gozxing.BitMatrix, x, y int) {
unsetRegion(image, x-4, y-4, 9, 9)
image.SetRegion(x-1, y-1, 3, 3)
}

func makePattern3(image *gozxing.BitMatrix, x, y int) {
image.SetRegion(x-10, y-10, 21, 21)
unsetRegion(image, x-7, y-7, 15, 15)
Expand Down Expand Up @@ -366,9 +405,6 @@ func TestDetector_ProcessFinderPatternInfo(t *testing.T) {
if e == nil {
t.Fatalf("ProcessFinderPatternInfo must be error")
}
if _, ok := e.(gozxing.FormatException); !ok {
t.Fatalf("ProcessFinderPatternInfo must be FormatException, %v", e)
}

// no alignment patterns
image.Clear()
Expand Down Expand Up @@ -401,6 +437,9 @@ func TestDetector_ProcessFinderPatternInfo(t *testing.T) {
makePattern(image, 13, 13, 1)
makePattern(image, 13+38, 13, 1)
makePattern(image, 13, 13+38, 1)
// timing patterns (row 6 and column 6 of the QR, at y=16 and x=16)
makeTimingPattern(image, 13, 16, 13+38, 16, 1) // horizontal TL -> TR
makeTimingPattern(image, 16, 13, 16, 13+38, 1) // vertical TL -> BL
info.topLeft = NewFinderPattern1(13, 13, 1)
info.topRight = NewFinderPattern1(13+38, 13, 1)
info.bottomLeft = NewFinderPattern1(13, 13+38, 1)
Expand Down
24 changes: 21 additions & 3 deletions qrcode/detector/finder_pattern_finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,23 @@ func (f *FinderPatternFinder) GetPossibleCenters() []*FinderPattern {
}

func (f *FinderPatternFinder) Find(hints map[gozxing.DecodeHintType]interface{}) (*FinderPatternInfo, gozxing.NotFoundException) {
_, tryHarder := hints[gozxing.DecodeHintType_TRY_HARDER]
return f.find(hints, true)
}

// FindExhaustive scans the full image without early termination, collecting
// all possible finder pattern candidates. This produces more candidates for
// SelectBestPatterns to choose from, at the cost of scanning the entire image.
func (f *FinderPatternFinder) FindExhaustive() (*FinderPatternInfo, gozxing.NotFoundException) {
f.possibleCenters = make([]*FinderPattern, 0)
f.hasSkipped = false
return f.find(nil, false)
}

func (f *FinderPatternFinder) find(hints map[gozxing.DecodeHintType]interface{}, allowEarlyStop bool) (*FinderPatternInfo, gozxing.NotFoundException) {
tryHarder := false
if hints != nil {
_, tryHarder = hints[gozxing.DecodeHintType_TRY_HARDER]
}
maxI := f.image.GetHeight()
maxJ := f.image.GetWidth()

Expand Down Expand Up @@ -67,7 +83,9 @@ func (f *FinderPatternFinder) Find(hints map[gozxing.DecodeHintType]interface{})
if confirmed {
iSkip = 2
if f.hasSkipped {
done = f.HaveMultiplyConfirmedCenters()
if allowEarlyStop {
done = f.HaveMultiplyConfirmedCenters()
}
} else {
rowSkip := f.FindRowSkip()
if rowSkip > stateCount[2] {
Expand Down Expand Up @@ -99,7 +117,7 @@ func (f *FinderPatternFinder) Find(hints map[gozxing.DecodeHintType]interface{})
confirmed := f.HandlePossibleCenter(stateCount, i, maxJ)
if confirmed {
iSkip = stateCount[0]
if f.hasSkipped {
if f.hasSkipped && allowEarlyStop {
done = f.HaveMultiplyConfirmedCenters()
}
}
Expand Down
Loading