-
Notifications
You must be signed in to change notification settings - Fork 4
/
bokeh-apply.go
151 lines (127 loc) · 2.97 KB
/
bokeh-apply.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/*
https://www.scratchapixel.com/lessons/digital-imaging/simple-image-manipulations/bookeh-effect
https://en.wikipedia.org/wiki/Bokeh
*/
package main
import (
"flag"
"fmt"
"log"
"math"
"os"
"github.com/qeedquan/go-media/image/imageutil"
)
var (
lightthreshold = flag.Float64("t", 0.7, "light threshold")
lightfactor = flag.Float64("l", 3, "lighten factor")
)
func main() {
log.SetFlags(0)
log.SetPrefix("bokeh-apply: ")
flag.Parse()
if flag.NArg() != 3 {
usage()
}
in, err := imageutil.LoadFloatFile(flag.Arg(0))
mask, err := imageutil.LoadFloatFile(flag.Arg(1))
ck(err)
ck(err)
lighten(in, *lightthreshold, *lightfactor)
lighten(mask, *lightthreshold, *lightfactor)
out := imageutil.NewFloat(in.Bounds())
bokeh(in, mask, out)
err = imageutil.WriteRGBAFile(flag.Arg(2), out.ToRGBA())
ck(err)
}
func usage() {
fmt.Fprintln(os.Stderr, "usage: [options] infile maskfile outfile")
flag.PrintDefaults()
os.Exit(2)
}
func ck(err error) {
if err != nil {
log.Fatal(err)
}
}
/*
The idea is for every non-zero value in the mask, circularly mix the image with the mask at the non-zero location.
The circular mixing is achieved by moving the image around and multiplying it with the mask, then adds it to the output.
Since the image values are normalized, this circular mixing has the effect of averaging out the image and make it blurry.
We lighten the parts where we want the focus to be to make it larger than the normalized range [0, 1].
The darken and brighten parts follow the shape of the mask.
*/
func bokeh(in, mask, out *imageutil.Float) {
tmp := imageutil.NewFloat(in.Bounds())
r := in.Bounds()
w := r.Dx()
h := r.Dy()
t := 0.0
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := mask.FloatAt(x, y)
if !eq(c, [4]float64{0, 0, 0, 1}) {
circshift(tmp, in, x, y)
mix(tmp, c)
add(out, tmp)
t += average(c)
}
}
}
mul(out, 1/t)
}
func lighten(m *imageutil.Float, th, ds float64) {
for i := range m.Pix {
for j := 0; j < 3; j++ {
if m.Pix[i][j] > th {
m.Pix[i][j] *= ds
}
}
}
}
func eq(a, b [4]float64) bool {
const eps = 1e-8
return math.Abs(a[0]-b[0]) <= eps &&
math.Abs(a[1]-b[1]) <= eps &&
math.Abs(a[2]-b[2]) <= eps
}
func average(a [4]float64) float64 {
return (a[0] + a[1] + a[2]) / 3
}
func mul(m *imageutil.Float, s float64) {
for i := range m.Pix {
m.Pix[i] = [4]float64{
m.Pix[i][0] * s,
m.Pix[i][1] * s,
m.Pix[i][2] * s,
m.Pix[i][3],
}
}
}
func mix(m *imageutil.Float, c [4]float64) {
for i := range m.Pix {
m.Pix[i][0] *= c[0]
m.Pix[i][1] *= c[1]
m.Pix[i][2] *= c[2]
m.Pix[i][3] = 1
}
}
func add(a, b *imageutil.Float) {
for i := range a.Pix {
a.Pix[i][0] += b.Pix[i][0]
a.Pix[i][1] += b.Pix[i][1]
a.Pix[i][2] += b.Pix[i][2]
a.Pix[i][3] = 1
}
}
func circshift(c, m *imageutil.Float, sx, sy int) {
r := m.Bounds()
w := r.Dx()
h := r.Dy()
for y := 0; y < h; y++ {
j := (y + sy) % h
for x := 0; x < w; x++ {
i := (x + sx) % w
c.SetFloat(i, j, m.FloatAt(x, y))
}
}
}