Skip to content

Commit

Permalink
refactor(qrcode): use kmp to speed up rule3 pattern calculating. (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
yeqown authored Dec 21, 2021
1 parent cd37a34 commit 39e6cb7
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 46 deletions.
61 changes: 61 additions & 0 deletions kmp_variant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package qrcode

import "github.com/yeqown/go-qrcode/v2/matrix"

// kmp is variant of kmp algorithm to count the pattern been in
// src slice.
// TODO(@yeqown): implement this in generic way.
func kmp(src, pattern []matrix.State, next []int) (count int) {
if next == nil {
next = kmpGetNext(pattern)
}
slen := len(src)
plen := len(pattern)
i := 0 // cursor of src
j := 0 // cursor of pattern

loop:
for i < slen && j < plen {
if j == -1 || src[i] == pattern[j] {
i++
j++
} else {
j = next[j]
}
}

if j == plen {
if i-j >= 0 {
count++
}

// reset cursor to count duplicate pattern.
// such as: "aaaa" and "aa", we want 3 rather than 2.
i -= plen - 1
j = 0

goto loop
}

return count
}

func kmpGetNext(pattern []matrix.State) []int {
fail := make([]int, len(pattern))
fail[0] = -1

j := 0
k := -1

for j < len(pattern)-1 {
if k == -1 || pattern[j] == pattern[k] {
k++
j++
fail[j] = k
} else {
k = fail[k]
}
}

return fail
}
45 changes: 45 additions & 0 deletions kmp_variant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package qrcode

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/yeqown/go-qrcode/v2/matrix"
)

func Test_kmp(t *testing.T) {
src := binaryToStateSlice("11001010010111001010010101011100")

type args struct {
src []matrix.State
pattern []matrix.State
}
tests := []struct {
name string
args args
wantCount int
}{
{
name: "test1",
args: args{
src: src,
pattern: binaryToStateSlice("0101"),
},
wantCount: 6,
},
{
name: "test1",
args: args{
src: src,
pattern: binaryToStateSlice("1001010"),
},
wantCount: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.wantCount, kmp(tt.args.src, tt.args.pattern, nil), "kmp(%v, %v)", tt.args.src, tt.args.pattern)
})
}
}
114 changes: 74 additions & 40 deletions mask.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,6 @@ const (
modulo7
)

var (
// 1011101 0000
statePattern1 = []matrix.State{matrix.StateTrue, matrix.StateFalse, matrix.StateTrue, matrix.StateTrue, matrix.StateTrue, matrix.StateFalse, matrix.StateTrue,
matrix.StateFalse, matrix.StateFalse, matrix.StateFalse, matrix.StateFalse}
// 0000 1011101
statePattern2 = []matrix.State{matrix.StateFalse, matrix.StateFalse, matrix.StateFalse, matrix.StateFalse,
matrix.StateTrue, matrix.StateFalse, matrix.StateTrue, matrix.StateTrue, matrix.StateTrue, matrix.StateFalse, matrix.StateTrue}
)

// calculateScore calculate the maskScore of masking result ...
func calculateScore(mat *matrix.Matrix) int {
debugLogf("calculate maskScore starting")
Expand Down Expand Up @@ -128,41 +119,69 @@ func rule2(mat *matrix.Matrix) int {
// 如果存在看起来类似于取景器模式的模式,则第三规则给QR码一个大的惩罚
// dark-light-dark-dark-dark-light-dark
// 1011101 0000 or 0000 1011101
func rule3(mat *matrix.Matrix) int {
//func rule3_backup(mat *matrix.Matrix) (score int) {
// for y := 0; y < mat.Height(); y++ {
// for x := 0; x < mat.Width()-11; x++ {
// stateSlice := make([]matrix.State, 0, 11)
// for i := 0; i < 11; i++ {
// s, _ := mat.Get(x+i, y)
// stateSlice = append(stateSlice, s)
// }
// if matrix.StateSliceMatched(statePattern1, stateSlice) {
// score += 40
// }
// if matrix.StateSliceMatched(statePattern2, stateSlice) {
// score += 40
// }
// }
// }
//
// for x := 0; x < mat.Width(); x++ {
// for y := 0; y < mat.Height()-11; y++ {
// stateSlice := make([]matrix.State, 0, 11)
// for i := 0; i < 11; i++ {
// s, _ := mat.Get(x, y+i)
// stateSlice = append(stateSlice, s)
// }
// if matrix.StateSliceMatched(statePattern1, stateSlice) {
// score += 40
// }
// if matrix.StateSliceMatched(statePattern2, stateSlice) {
// score += 40
// }
// }
// }
//
// return score
//}

// rule3 calculate punishment score in rule3, find pattern in QR Code matrix.
func rule3(mat *matrix.Matrix) (score int) {
var (
score int
stateSlice []matrix.State
pattern1 = binaryToStateSlice("1011101 0000")
pattern2 = binaryToStateSlice("0000 1011101")
pattern1Next = kmpGetNext(pattern1)
pattern2Next = kmpGetNext(pattern2)
)

for y := 0; y < mat.Height(); y++ {
for x := 0; x < mat.Width()-11; x++ {
for i := 0; i < 11; i++ {
s, _ := mat.Get(x+i, y)
stateSlice = append(stateSlice, s)
}
if matrix.StateSliceMatched(statePattern1, stateSlice) {
score += 40
}
if matrix.StateSliceMatched(statePattern2, stateSlice) {
score += 40
}
}
// prerequisites:
//
// mat.Width() == mat.Height()
if mat.Width() != mat.Height() {
debugLogf("rule3 got matrix but not matched prerequisites")
}

for x := 0; x < mat.Width(); x++ {
for y := 0; y < mat.Height()-11; y++ {
// stateSlice =
for i := 0; i < 11; i++ {
s, _ := mat.Get(x, y+i)
stateSlice = append(stateSlice, s)
}
if matrix.StateSliceMatched(statePattern1, stateSlice) {
score += 40
}
if matrix.StateSliceMatched(statePattern2, stateSlice) {
score += 40
}
}
dimension := mat.Width()

for i := 0; i < dimension; i++ {
col := mat.Col(i)
row := mat.Row(i)

// DONE(@yeqown): statePattern1 and statePattern2 are fixed, so maybe kmpGetNext
// could cache result to speed up.
score += 40 * kmp(col, pattern1, pattern1Next)
score += 40 * kmp(col, pattern2, pattern2Next)
score += 40 * kmp(row, pattern1, pattern1Next)
score += 40 * kmp(row, pattern2, pattern2Next)
}

return score
Expand Down Expand Up @@ -317,3 +336,18 @@ func modulo6Func(x, y int) bool {
func modulo7Func(x, y int) bool {
return ((x+y)%2+(x*y)%3)%2 == 0
}

func binaryToStateSlice(s string) []matrix.State {
var states = make([]matrix.State, 0, len(s))
for _, c := range s {
switch c {
case '1':
states = append(states, matrix.StateTrue)
case '0':
states = append(states, matrix.StateFalse)
default:
continue
}
}
return states
}
21 changes: 21 additions & 0 deletions mask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package qrcode
import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"

"github.com/yeqown/go-qrcode/v2/matrix"
Expand Down Expand Up @@ -52,3 +54,22 @@ func TestMask(t *testing.T) {
mask7 := newMask(cpyMat, modulo7)
_ = debugDraw("./assets/modulo7.jpeg", *mask7.mat)
}

//func Test_rule3_refactor(t *testing.T) {
// qrc, err := New("baidu.com google.com qq.com sina.com apple.com")
// assert.NoError(t, err)
// _ = qrc
// old := rule3_backup(qrc.mat)
// refactor := rule3(qrc.mat)
// assert.Equal(t, old, refactor)
//}

func Benchmark_rule3(b *testing.B) {
qrc, err := New("baidu.com google.com qq.com sina.com apple.com")
assert.NoError(b, err)

b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = rule3(qrc.mat)
}
}
26 changes: 25 additions & 1 deletion matrix/matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
)

// ScanDirection scan matrix driection
// ScanDirection scan matrix direction
type ScanDirection uint

const (
Expand Down Expand Up @@ -54,6 +54,8 @@ var (
ErrorOutRangeOfH = errors.New("out of range of height")
)

// StateSliceMatched should be
// Deprecated: since rule3_backup removed
func StateSliceMatched(ss1, ss2 []State) bool {
if len(ss1) != len(ss2) {
return false
Expand Down Expand Up @@ -197,3 +199,25 @@ func XOR(s1, s2 State) State {
}
return StateFalse
}

// Row return a row of matrix, cur should be y dimension.
func (m *Matrix) Row(cur int) []State {
if cur >= m.height || cur < 0 {
return nil
}

col := make([]State, m.height)
for w := 0; w < m.width; w++ {
col[w] = m.mat[w][cur]
}
return col
}

// Col return a slice of column, cur should be x dimension.
func (m *Matrix) Col(cur int) []State {
if cur >= m.width || cur < 0 {
return nil
}

return m.mat[cur]
}
Loading

0 comments on commit 39e6cb7

Please sign in to comment.