-
Notifications
You must be signed in to change notification settings - Fork 0
/
stereogram.go
229 lines (210 loc) · 6.3 KB
/
stereogram.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
package deepx
import (
"fmt"
"image"
"image/color"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"math"
"math/rand"
"sync"
)
var (
defaultStereogramCfg = StereogramConfig{
Palette: make([]Color, 0),
Mu: 1 / 3.,
DPI: 72,
ERatio: 2.5,
}
)
// StereogramConfig represents a stereogram image processing configuration model.
type StereogramConfig struct {
// Contains the color of the mask image pixels that should be considered transparent.
//
// If not specifed, every pixel in a mask image that has a zero alpha channel
// will be considered transparent.
MaskTransparentColor *Color
// Represents a list of colors used to create pixels in a stereogram image.
//
// It's recommended to specify at least 2 different colors so that
// the final stereogram image is colored (if you specify one color, the final image
// will always be monotonously filled with this color, which doesn't make sense 0_o).
// Each color presented will be randomly selected to form a unique pixel
// in the stereogram image.
//
// If the list of colors is not specified (by defaul), then a randomization algorithm
// will be applied when forming each individual pixel color.
Palette []Color
// Depth of field (fraction of viewing distance).
//
// Equal to 1/3 by default.
Mu float64
// Output stereogram image DPI.
//
// By defualt has 72 pixels per inch.
DPI int
// Eye separation ratio.
//
// Eye separation is assumed to be 2.5 * DPI in by default.
ERatio float64
}
// StereogramOption represents type for stereogram image processing option.
type StereogramOption func(*StereogramConfig)
// NewStereogramFromMask creates a new "Random-Dot Stereogram" image from the provided
// mask source using the algorithm of Harold W. Thimbleby, Stuart Inglis and Ian H:
// https://www2.cs.sfu.ca/CourseCentral/414/li/material/refs/SIRDS-Computer-94.pdf
//
// The mask source must contain an encoded valid png, jpeg or gif image data.
// The mask image will be interpreted as monochrome, regardless of the actual number of colors
// encoded in that image.
// All pixels in the mask image that have a zero alpha channel (transparent)
// will be ignored (by default), and the remaining pixels will be included in the final mask image.
// To explicitly specify the color that should be perceived as transparent in the mask image,
// specify a `WithMaskTransparentColor(...)` in the list of options.
//
// A list of options can be provided to specify additional stereogram processing settings.
func NewStereogramFromMask(maskSrc io.Reader, opts ...StereogramOption) (*image.RGBA, error) {
maskImg, _, err := image.Decode(maskSrc)
if err != nil {
return nil, fmt.Errorf("could not decode mask image data: %v", err)
}
cfg := defaultStereogramCfg
for _, opt := range opts {
opt(&cfg)
}
e := math.Ceil(cfg.ERatio * float64(cfg.DPI))
maskImgBounds := maskImg.Bounds()
imgWidth, imgHeight := maskImgBounds.Dx(), maskImgBounds.Dy()
stereogramImg := drawAutoStereogram(
newDepthBufferFromImage(maskImg, cfg.MaskTransparentColor),
imgWidth, imgHeight, cfg.Mu, e, cfg.Palette,
)
return stereogramImg, nil
}
// WithMaskTransparentColor sets the color that must be transparent for the mask source image.
//
// By default, every pixel in a mask image that has a zero alpha channel
// will be considered transparent.
func WithMaskTransparentColor(color Color) StereogramOption {
return func(cfg *StereogramConfig) {
cfg.MaskTransparentColor = &color
}
}
// WithColorPalette sets the list of colors used to create pixels in a stereogram image.
//
// By default, this list is empty and therefore the colors of each pixel will be selected randomly.
func WithColorPalette(palette ...Color) StereogramOption {
return func(cfg *StereogramConfig) {
cfg.Palette = palette
}
}
// WithOutputDPI sets the DPI of the stegeogram image.
//
// 72 by default.
func WithOutputDPI(dpi int) StereogramOption {
return func(cfg *StereogramConfig) {
cfg.DPI = dpi
}
}
// WithEyeSepartionRatio sets the eye separtion ratio.
//
// 2.5 by default.
func WithEyeSepartionRatio(ratio float64) StereogramOption {
return func(cfg *StereogramConfig) {
cfg.ERatio = ratio
}
}
func projSeparation(z, mu, e float64) int {
return int(math.Ceil((1 - mu*z) * e / (2 - mu*z)))
}
func getRandomPaletteColor(palette []Color) Color {
if len(palette) == 0 {
return Color{
R: uint8(rand.Intn(256)),
G: uint8(rand.Intn(256)),
B: uint8(rand.Intn(256)),
A: 255,
}
}
return palette[rand.Intn(len(palette))]
}
func isTransparentMaskPixel(pxColor color.Color, maskTransparentColor *Color) bool {
if maskTransparentColor == nil {
_, _, _, a := pxColor.RGBA()
return a == 0
}
return ColorRGBA(pxColor).Equal(*maskTransparentColor)
}
func drawAutoStereogram(
zBuf [][]float64,
imgWidth, imgHeight int,
mu, e float64,
palette []Color,
) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
var wg sync.WaitGroup
wg.Add(imgHeight)
for y := 0; y < imgHeight; y++ {
go func(y int) {
defer wg.Done()
same := make([]int, imgWidth)
for x := 0; x < imgWidth; x++ {
same[x] = x
}
for x := 0; x < imgWidth; x++ {
s := projSeparation(zBuf[x][y], mu, e)
left := x - (s+(s&y&1))/2
right := left + s
if left < 0 || right >= imgWidth {
continue
}
var isVisible bool
for t := 1; ; t++ {
zt := zBuf[x][y] + 2*(2-mu*zBuf[x][y])*float64(t)/(mu*e)
isVisible = zBuf[x-t][y] < zt && zBuf[x+t][y] < zt
if !(isVisible && zt < 1) {
break
}
}
if !isVisible {
continue
}
for k := same[left]; k != left && k != right; k = same[left] {
if k < right {
left = k
continue
}
left, right = right, k
}
same[left] = right
}
pixels := make([]Color, imgWidth)
for x := imgWidth - 1; x >= 0; x-- {
pixels[x] = pixels[same[x]]
if same[x] == x {
pixels[x] = getRandomPaletteColor(palette)
}
img.Set(x, y, pixels[x].RGBA())
}
}(y)
}
wg.Wait()
return img
}
func newDepthBufferFromImage(img image.Image, transparentColor *Color) [][]float64 {
imgBounds := img.Bounds()
sizeX, sizeY := imgBounds.Dx(), imgBounds.Dy()
z := make([][]float64, sizeX)
for x := 0; x < sizeX; x++ {
z[x] = make([]float64, sizeY)
for y := 0; y < sizeY; y++ {
if isTransparentMaskPixel(img.At(x, y), transparentColor) {
continue
}
z[x][y] = 1
}
}
return z
}