mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
4ff93bf46e
fixes #2871
port 1.5
(cherry picked from commit f8fe16738e)
408 lines
9.4 KiB
Go
408 lines
9.4 KiB
Go
package screenshot
|
|
|
|
// Frame stitcher after mark-shot's column-sampling design
|
|
// (https://github.com/jswysnemc/mark-shot, src/scroll/stitcher_algorithm.cpp).
|
|
// Only rows overhanging the captured range are committed; frames that match
|
|
// nothing are dropped without touching state.
|
|
|
|
const (
|
|
stitchMaxCanvasBytes = 256 << 20
|
|
stitchMaxRowsCap = 30000
|
|
|
|
// mark-shot: StitchConfig{100, 9.0f, 15, 1.0f}
|
|
stitchAcceptDiff = 9.0
|
|
stitchApproxDiff = 1.0
|
|
stitchMinCompare = 50
|
|
stitchMinCanvas = 100
|
|
stitchMinAppend = 15
|
|
stitchCoarseStep = 8
|
|
stitchPredictWindow = 160
|
|
stitchBandSamples = 17
|
|
|
|
// mark-shot: kDuplicateAvgDiff=1.1f, kDuplicateMaxDiff=4, 18x24 grid
|
|
stitchDupAvgDiff = 1.1
|
|
stitchDupMaxDiff = 4.0
|
|
stitchSigCols = 18
|
|
stitchSigRows = 24
|
|
|
|
// blank rows agree at every offset and must not decide a match
|
|
stitchActivityMin = 2.0
|
|
stitchRowMatchTol = 4.0
|
|
stitchMinActive = 12
|
|
)
|
|
|
|
// mean luminance per band (8-32%, 34-66%, 68-92%); the outer 8% is chrome
|
|
type rowCols [3]float32
|
|
|
|
type stitcher struct {
|
|
stride int
|
|
sampleOffs [3][]int
|
|
|
|
canvas []byte
|
|
cols []rowCols
|
|
|
|
anchor int
|
|
last []rowCols
|
|
lastOffset int
|
|
|
|
maxRows int
|
|
full bool
|
|
}
|
|
|
|
func newStitcher(stride int) *stitcher {
|
|
px := stride / 4
|
|
st := &stitcher{
|
|
stride: stride,
|
|
maxRows: min(stitchMaxCanvasBytes/stride, stitchMaxRowsCap),
|
|
}
|
|
bands := [3][2]float64{{0.08, 0.32}, {0.34, 0.66}, {0.68, 0.92}}
|
|
for b, band := range bands {
|
|
lo := int(float64(px) * band[0])
|
|
hi := max(int(float64(px)*band[1]), lo+1)
|
|
n := min(stitchBandSamples, hi-lo)
|
|
for s := range n {
|
|
st.sampleOffs[b] = append(st.sampleOffs[b], (lo+(hi-lo)*s/n)*4)
|
|
}
|
|
}
|
|
return st
|
|
}
|
|
|
|
func (st *stitcher) rowSamples(data []byte) []rowCols {
|
|
rows := len(data) / st.stride
|
|
cols := make([]rowCols, rows)
|
|
for y := range rows {
|
|
row := data[y*st.stride:]
|
|
for b := range 3 {
|
|
var sum float32
|
|
for _, off := range st.sampleOffs[b] {
|
|
sum += 0.114*float32(row[off]) + 0.587*float32(row[off+1]) + 0.299*float32(row[off+2])
|
|
}
|
|
cols[y][b] = sum / float32(len(st.sampleOffs[b]))
|
|
}
|
|
}
|
|
return cols
|
|
}
|
|
|
|
func (st *stitcher) frameSig(data []byte) []float32 {
|
|
rows := len(data) / st.stride
|
|
px := st.stride / 4
|
|
sig := make([]float32, 0, stitchSigCols*stitchSigRows)
|
|
for gy := range stitchSigRows {
|
|
y := (2*gy + 1) * rows / (2 * stitchSigRows)
|
|
for gx := range stitchSigCols {
|
|
x := (2*gx + 1) * px / (2 * stitchSigCols)
|
|
off := y*st.stride + x*4
|
|
sig = append(sig, 0.114*float32(data[off])+0.587*float32(data[off+1])+0.299*float32(data[off+2]))
|
|
}
|
|
}
|
|
return sig
|
|
}
|
|
|
|
func (st *stitcher) rows() int {
|
|
return len(st.cols)
|
|
}
|
|
|
|
func rowColsDiff(a, b rowCols) float32 {
|
|
return (abs32(a[0]-b[0]) + abs32(a[1]-b[1]) + abs32(a[2]-b[2])) / 3
|
|
}
|
|
|
|
func duplicateFrame(a, b []float32) bool {
|
|
if len(a) != len(b) || len(a) == 0 {
|
|
return false
|
|
}
|
|
var sum, maxDiff float32
|
|
for i := range a {
|
|
d := abs32(a[i] - b[i])
|
|
sum += d
|
|
maxDiff = max(maxDiff, d)
|
|
}
|
|
return sum/float32(len(a)) <= stitchDupAvgDiff && maxDiff <= stitchDupMaxDiff
|
|
}
|
|
|
|
// sticky header/footer zones, per mark-shot: 10% top, 8% bottom, min 16px
|
|
func matchIgnores(h int) (top, bottom int) {
|
|
if h < 80 {
|
|
return 0, 0
|
|
}
|
|
return clamp(h/10, 16, h/4), clamp(h*8/100, 16, h/4)
|
|
}
|
|
|
|
func activity(f []rowCols) []bool {
|
|
active := make([]bool, len(f))
|
|
for i := 1; i < len(f); i++ {
|
|
active[i] = rowColsDiff(f[i], f[i-1]) > stitchActivityMin
|
|
}
|
|
return active
|
|
}
|
|
|
|
func (st *stitcher) pushFrame(frame []byte, f []rowCols) (int, bool) {
|
|
if st.full || len(f) == 0 {
|
|
return 0, true
|
|
}
|
|
|
|
h := len(f)
|
|
if len(st.cols) == 0 {
|
|
n := st.appendRows(frame, f, 0)
|
|
st.anchor = 0
|
|
st.last = f
|
|
st.lastOffset = 0
|
|
return n, true
|
|
}
|
|
|
|
pos, ok := st.locateFrame(f, activity(f))
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
|
|
delta := pos - st.anchor
|
|
added := 0
|
|
if over := pos + h - len(st.cols); over >= stitchMinAppend {
|
|
added += st.appendRows(frame, f, h-over)
|
|
}
|
|
if over := -pos; over >= stitchMinAppend {
|
|
n := st.prependRows(frame, f, over)
|
|
added += n
|
|
pos += n
|
|
}
|
|
|
|
st.anchor = pos
|
|
st.last = f
|
|
st.lastOffset = delta
|
|
return added, true
|
|
}
|
|
|
|
// seamAppend starts a new segment after a jump capture couldn't follow.
|
|
func (st *stitcher) seamAppend(frame []byte, f []rowCols) int {
|
|
if st.full || len(f) == 0 {
|
|
return 0
|
|
}
|
|
pos := len(st.cols)
|
|
n := st.appendRows(frame, f, 0)
|
|
st.anchor = pos
|
|
st.last = f
|
|
st.lastOffset = 0
|
|
return n
|
|
}
|
|
|
|
func (st *stitcher) locateFrame(f []rowCols, active []bool) (int, bool) {
|
|
d, diff := st.adjacentOffset(f, active)
|
|
pred := st.anchor + d
|
|
|
|
if diff <= stitchAcceptDiff {
|
|
if _, ok := st.verifyAt(f, active, pred); ok {
|
|
return pred, true
|
|
}
|
|
}
|
|
if pos, _, ok := st.scanPositions(f, active, pred, true); ok {
|
|
return pos, true
|
|
}
|
|
pos, _, ok := st.scanPositions(f, active, pred, false)
|
|
return pos, ok
|
|
}
|
|
|
|
func (st *stitcher) verifyAt(f []rowCols, active []bool, pos int) (float32, bool) {
|
|
diff, count, activeMatches := st.canvasDiff(f, active, pos)
|
|
ok := count >= stitchMinCanvas && diff <= stitchAcceptDiff && activeMatches >= stitchMinActive
|
|
return diff, ok
|
|
}
|
|
|
|
// signed deltas searched outward from the previous one (mark-shot's
|
|
// predictOffsetIter), early-exiting once a diff beats approxDiff
|
|
func (st *stitcher) adjacentOffset(f []rowCols, active []bool) (int, float32) {
|
|
h := len(f)
|
|
if len(st.last) != h {
|
|
return 0, float32(1e9)
|
|
}
|
|
limit := max(h-stitchMinCompare-1, 0)
|
|
|
|
bestD, bestDiff := 0, float32(1e9)
|
|
countdown := -1
|
|
try := func(d int) bool {
|
|
if d < -limit || d > limit {
|
|
return false
|
|
}
|
|
diff, activeMatches := st.pairDiff(f, active, d)
|
|
if activeMatches >= stitchMinActive && diff < bestDiff {
|
|
bestDiff, bestD = diff, d
|
|
}
|
|
switch {
|
|
case bestDiff < stitchApproxDiff/4:
|
|
return true
|
|
case bestDiff < stitchApproxDiff && countdown < 0:
|
|
countdown = 10
|
|
}
|
|
if countdown > 0 {
|
|
countdown--
|
|
}
|
|
return countdown == 0
|
|
}
|
|
|
|
if try(st.lastOffset) {
|
|
return bestD, bestDiff
|
|
}
|
|
for k := 1; ; k++ {
|
|
lo, hi := st.lastOffset-k, st.lastOffset+k
|
|
if lo < -limit && hi > limit {
|
|
break
|
|
}
|
|
if try(hi) || try(lo) {
|
|
break
|
|
}
|
|
}
|
|
return bestD, bestDiff
|
|
}
|
|
|
|
func (st *stitcher) pairDiff(f []rowCols, active []bool, d int) (float32, int) {
|
|
h := len(f)
|
|
top, bottom := matchIgnores(h)
|
|
lo := max(top, -d)
|
|
hi := min(h-bottom, h-d)
|
|
|
|
count := hi - lo
|
|
if count < stitchMinCompare {
|
|
return float32(1e9), 0
|
|
}
|
|
var sum float32
|
|
activeMatches := 0
|
|
for i := lo; i < hi; i++ {
|
|
rd := rowColsDiff(f[i], st.last[i+d])
|
|
sum += rd
|
|
if active[i] && rd <= stitchRowMatchTol {
|
|
activeMatches++
|
|
}
|
|
}
|
|
return sum / float32(count), activeMatches
|
|
}
|
|
|
|
func (st *stitcher) canvasDiff(f []rowCols, active []bool, pos int) (float32, int, int) {
|
|
h := len(f)
|
|
top, bottom := matchIgnores(h)
|
|
lo := max(top, -pos)
|
|
hi := min(h-bottom, len(st.cols)-pos)
|
|
|
|
count := hi - lo
|
|
if count < 1 {
|
|
return float32(1e9), 0, 0
|
|
}
|
|
var sum float32
|
|
activeMatches := 0
|
|
for i := lo; i < hi; i++ {
|
|
rd := rowColsDiff(f[i], st.cols[pos+i])
|
|
sum += rd
|
|
if active[i] && rd <= stitchRowMatchTol {
|
|
activeMatches++
|
|
}
|
|
}
|
|
return sum / float32(count), count, activeMatches
|
|
}
|
|
|
|
// mark-shot's findEdgePosition (nearOnly: edges + prediction window, 1px) and
|
|
// findKnownPosition (coarse sweep refined around the winner)
|
|
func (st *stitcher) scanPositions(f []rowCols, active []bool, pred int, nearOnly bool) (int, float32, bool) {
|
|
h := len(f)
|
|
C := len(st.cols)
|
|
minPos := stitchMinCanvas - h
|
|
maxPos := C - stitchMinCanvas
|
|
|
|
bestPos, bestDiff := 0, float32(1e9)
|
|
bestDist := 1 << 30
|
|
consider := func(pos int) {
|
|
if pos < minPos || pos > maxPos {
|
|
return
|
|
}
|
|
diff, ok := st.verifyAt(f, active, pos)
|
|
if !ok {
|
|
return
|
|
}
|
|
dist := pos - pred
|
|
if dist < 0 {
|
|
dist = -dist
|
|
}
|
|
better := diff < bestDiff
|
|
if !nearOnly {
|
|
better = dist < bestDist || dist == bestDist && diff < bestDiff
|
|
}
|
|
if better {
|
|
bestPos, bestDiff, bestDist = pos, diff, dist
|
|
}
|
|
}
|
|
|
|
if nearOnly {
|
|
for pos := pred - stitchPredictWindow; pos <= pred+stitchPredictWindow; pos++ {
|
|
consider(pos)
|
|
}
|
|
for pos := C - h; pos <= maxPos; pos++ {
|
|
consider(pos)
|
|
}
|
|
for pos := minPos; pos <= 0; pos++ {
|
|
consider(pos)
|
|
}
|
|
if bestDiff > stitchAcceptDiff {
|
|
return 0, 0, false
|
|
}
|
|
return bestPos, bestDiff, true
|
|
}
|
|
|
|
for pos := minPos; pos <= maxPos; pos += stitchCoarseStep {
|
|
consider(pos)
|
|
}
|
|
if bestDiff > stitchAcceptDiff {
|
|
return 0, 0, false
|
|
}
|
|
|
|
refined, refinedDiff := bestPos, bestDiff
|
|
for pos := bestPos - stitchCoarseStep + 1; pos < bestPos+stitchCoarseStep; pos++ {
|
|
if pos == bestPos {
|
|
continue
|
|
}
|
|
if diff, ok := st.verifyAt(f, active, pos); ok && diff < refinedDiff {
|
|
refined, refinedDiff = pos, diff
|
|
}
|
|
}
|
|
return refined, refinedDiff, true
|
|
}
|
|
|
|
func (st *stitcher) appendRows(frame []byte, f []rowCols, from int) int {
|
|
n := len(f) - from
|
|
if room := st.maxRows - len(st.cols); n > room {
|
|
n = room
|
|
st.full = true
|
|
}
|
|
if n <= 0 {
|
|
st.full = true
|
|
return 0
|
|
}
|
|
|
|
st.canvas = append(st.canvas, frame[from*st.stride:(from+n)*st.stride]...)
|
|
st.cols = append(st.cols, f[from:from+n]...)
|
|
return n
|
|
}
|
|
|
|
func (st *stitcher) prependRows(frame []byte, f []rowCols, n int) int {
|
|
if room := st.maxRows - len(st.cols); n > room {
|
|
n = room
|
|
st.full = true
|
|
}
|
|
if n <= 0 {
|
|
st.full = true
|
|
return 0
|
|
}
|
|
|
|
canvas := make([]byte, n*st.stride+len(st.canvas))
|
|
copy(canvas, frame[:n*st.stride])
|
|
copy(canvas[n*st.stride:], st.canvas)
|
|
st.canvas = canvas
|
|
|
|
cols := make([]rowCols, 0, n+len(st.cols))
|
|
cols = append(cols, f[:n]...)
|
|
st.cols = append(cols, st.cols...)
|
|
return n
|
|
}
|
|
|
|
func abs32(f float32) float32 {
|
|
if f < 0 {
|
|
return -f
|
|
}
|
|
return f
|
|
}
|