🎨 CV | 过渡效果和中位切割算法

这里使用 Golang 中的GoCV来实现图片的过渡效果以及生成 8 位颜色图片的中位切割算法

🚀 代码: Github

瞳孔过渡效果

Now suppose we wish to create a video transition such that the second video appears under the first video through a moving radius (like a clock hand), as in Fig. 2.23. Write a formula to use the correct pixels from the two videos to achieve this special effect for the red channel.

1538973246254

原图片

lena

Nobel

算法描述

这道题需要我们实现一个从切换的效果,从一张图片切换到另一张图片,并且从中间不断变大。

要实现这个效果,需要多张图片合成起来,而每一张图片中心的圆圈的半径应该是逐张变大。

对于过程中的每一张图片,生成的步骤如下:

  • 找出图片的中点(midX, midY)
  • 找出所要生成的圆圈的半径的大小radius
  • 生成一张新图片,对于其每一个像素点(x, y)
    • 如果(x, y)到(midX, midY)的距离小于 radius,那么填充第一张图片对应像素的颜色
    • 如果(x, y)到(midX, midY)的距离大于 radius,那么填充第二张图片对应像素的颜色

最后把过程中所有的图片合并起来,生成gif或者avi

由于题目只是要求红色通道的颜色,因此生成的图片可以使用灰度图来表示。

因此,新图片中的像素可以用以下公式来表示

$$
\begin{cases}
color1(x, y)& \sqrt{(x-r)^2+(y-r)^2} > r\
color2(x, y)& \sqrt{(x-r)^2+(y-r)^2} < r
\end{cases}$$

程序实现

这里使用了GoCV ,使用go语言调用了 OpenCV库来实现程序功能。

如果你需要在你的电脑使用GoCV,需要minGWCMake环境编译安装OpenCV3.4.2并设置好环境变量

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
// ./main.go
package main

import (
"fmt"
"github.com/ZhenlyChen/ImageProcessing/IrisImage"
"github.com/andybons/gogif"
"gocv.io/x/gocv"
"image"
"image/gif"
"image/jpeg"
"os"
"strconv"
)

func main() {
processIris()
}

func processIris() {
// 读取源图像
var err error
var file1, file2 *os.File
var img1, img2 image.Image
file1, err = os.OpenFile("./img/Nobel.jpg", os.O_RDONLY, 0)
check(err)
defer file1.Close()
file2, err = os.OpenFile("./img/lena.jpg", os.O_RDONLY, 0)
check(err)
defer file2.Close()
img1, err = jpeg.Decode(file1)
check(err)
img2, err = jpeg.Decode(file2)
check(err)

// 生成不同半径的图片
var names []string
var subimages []image.Image
for i := 0.01; i <= 1; i += 0.01 {
resImg, err := IrisImage.GetIrisImage(img1, img2, i)
subimages = append(subimages, resImg)
check(err)
name := "./dist/iris" + strconv.Itoa(int(i*100)) + ".jpg"
names = append(names, name)
distFile, err := os.Create(name)
check(err)
err = jpeg.Encode(distFile, resImg, &jpeg.Options{Quality: 100})
check(err)
}

// 生成gif
distGif, err := os.Create("./dist/iris.gif")
check(err)
outGif := &gif.GIF{}
for _, simage := range subimages {
bounds := simage.Bounds()
palettedImage := image.NewPaletted(bounds, nil)
quantizer := gogif.MedianCutQuantizer{NumColor: 64}
quantizer.Quantize(palettedImage, bounds, simage, image.ZP)
// Add new frame to animated GIF
outGif.Image = append(outGif.Image, palettedImage)
outGif.Delay = append(outGif.Delay, 0)
}
gif.EncodeAll(distGif, outGif)

// 生成video
video, err := gocv.VideoWriterFile("./dist/iris.avi", "MJPG", 60, img1.Bounds().Dy(), img1.Bounds().Dx(), true)
check(err)
for _, n := range names {
img := gocv.IMRead(n, gocv.IMReadColor)
video.Write(img)
}
video.Close()
}

func check(err error) {
if err != nil {
panic(err)
}
}
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
// ./IrisImage/IrisImage.go
func GetIrisImage(img1, img2 image.Image, per float64) (image.Image, error) {
// 图片色彩模型为rCbCr,先统一转换为RGBA
b1 := img1.Bounds()
m1 := image.NewRGBA(image.Rect(0, 0, b1.Dx(), b1.Dy()))
draw.Draw(m1, m1.Bounds(), img1, b1.Min, draw.Src)

b2 := img2.Bounds()
img2.At(1, 2)
m2 := image.NewRGBA(image.Rect(0, 0, b2.Dx(), b2.Dy()))
draw.Draw(m2, m2.Bounds(), img2, b2.Min, draw.Src)

res := image.NewGray(img1.Bounds())
// 计算圆心
midX := img1.Bounds().Dx() / 2
midY := img1.Bounds().Dy() / 2
// 计算半径
radius := per * getDis(0, 0, midX, midY)
for x := 0; x < img1.Bounds().Dx(); x++ {
for y := 0; y < img1.Bounds().Dy(); y++ {
var thisColor color.Color
// 判断距离
if getDis(x, y, midX, midY) < radius {
thisColor = m2.At(x, y)
} else {
thisColor = m1.At(x, y)
}
// 填充颜色
red, _, _, _ := thisColor.RGBA()
res.Set(x, y, color.Gray{Y: uint8(red)})
}
}
return res, nil
}

func getDis(x1, y1, x2, y2 int) float64 {
lenX := math.Abs(float64(x1 - x2))
lenY := math.Abs(float64(y1 - y2))
return math.Sqrt(float64(lenX*lenX + lenY*lenY))
}

实现效果

生成过程中的图片:

半径:25%

iris25

半径60%:

iris60

GIF效果图:

iris_c

中位切分算法

For the color LUT problem, try out the median-cut algorithm on a sample image. Explain briefly why it is that this algorithm, carried out on an image of red apples, puts more color gradation in the resulting 24-bit color image where it is needed, among the reds.

redapple

算法描述

这道题需要使用median-cut算法将原图简化为256种颜色的图片。

首先,我们要根据中值区分算法找出256种颜色:

  • 将图片中所有的颜色放入一个数组里面
  • 根据红色将所有的颜色划分为两个数量相等区域,前一个区域标记为0,后一个区域标记为1
  • 对于两个区域,再次根据绿色将其划分为数量相等的两个区域
  • 按照红、绿、蓝的顺序,重复上面两个步骤,直到所有颜色被划分为256个区域
  • 对于每一个区域,取其R、G、B平均值作为其中心颜色

到此,我们就得到了一个Look-up table,里面存放这这张图片最有代表性的256种颜色。

然后生成新的图片:

  • 遍历原图片所有点,对于其每一个点:
    • 在LUT中寻找与其RGB值的欧氏距离最小的值
    • 填充到新图片对应的像素点中

程序实验

这里还是使用GoCV来进行实验

对于Look-up table的生成可以使用递归来实现,每次将颜色数组分割成两个部分,一直分割到第8次将结果的平均值放入table中,那样就可以生成256种颜色的LUT

然后再对于图片每个像素计算到LUT各个颜色的欧氏距离,取其最小距离的颜色,放入新图片中,那样就可以得到结果了。

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
// ./main.go
package main

import (
"github.com/ZhenlyChen/ImageProcessing/EightBit"
"gocv.io/x/gocv"
"image"
"image/jpeg"
"os"
)

func main() {
process8Bit()
}

func process8Bit() {
img := gocv.IMRead("./img/redapple.jpg", gocv.IMReadColor)
resImg := EightBit.To8Bit(img)
distFile, err := os.Create("./dist/goodapple.jpg")
check(err)
dist, err := resImg.ToImage()
check(err)
err = jpeg.Encode(distFile, dist, &jpeg.Options{Quality: 100})
check(err)
}
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
// ./EightBit/EightBig.go
package EightBit

import (
"fmt"
"gocv.io/x/gocv"
"sort"
)

type RGBColor struct {
R uint8
G uint8
B uint8
}

type ColorSlice []RGBColor

func (c ColorSlice) Len() int {
return len(c)
}

func (c ColorSlice) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}

type SortByR struct{ ColorSlice }
type SortByG struct{ ColorSlice }
type SortByB struct{ ColorSlice }

func (c SortByR) Less(i, j int) bool {
return c.ColorSlice[i].R < c.ColorSlice[j].R
}
func (c SortByG) Less(i, j int) bool {
return c.ColorSlice[i].G < c.ColorSlice[j].G
}
func (c SortByB) Less(i, j int) bool {
return c.ColorSlice[i].B < c.ColorSlice[j].B
}

// colorTable LUT
var colorTable ColorSlice

// DivRGB 划分颜色,生成LUT
func DivRGB(data ColorSlice, deep int) {
colorType := deep % 3
half := len(data) / 2
if colorType == 0 { // R
sort.Sort(SortByR{data})
} else if colorType == 1 { // G
sort.Sort(SortByG{data})
} else { // B
sort.Sort(SortByB{data})
}
if deep >= 7 { // 已经划分了7次,再划分一次就可以生成256个区域
var sumR, sumG, sumB int
for _, c := range data[:half] {
sumR += int(c.R)
sumG += int(c.G)
sumB += int(c.B)
}
colorTable = append(colorTable, RGBColor{
R: uint8(sumR / half),
G: uint8(sumG / half),
B: uint8(sumB / half),
})
sumR, sumG, sumB = 0, 0, 0
for _, c := range data[half:] {
sumR += int(c.R)
sumG += int(c.G)
sumB += int(c.B)
}
colorTable = append(colorTable, RGBColor{
R: uint8(sumR / half),
G: uint8(sumG / half),
B: uint8(sumB / half),
})
} else { // 继续划分
DivRGB(data[:half], deep+1)
DivRGB(data[half:], deep+1)
}
}

func ToRGBColor(src gocv.Mat) (res ColorSlice) {
size := src.Size()
for i := 0; i < size[0]; i++ {
for j := 0; j < size[1]; j++ {
res = append(res, RGBColor{
R: src.GetUCharAt(i, j*3),
G: src.GetUCharAt(i, j*3+1),
B: src.GetUCharAt(i, j*3+2),
})
}
}
return
}

func To8Bit(src gocv.Mat) (res gocv.Mat) {
res = src.Clone()
DivRGB(ToRGBColor(src), 0)
fmt.Println(colorTable)
size := src.Size()
for i := 0; i < size[0]; i++ {
for j := 0; j < size[1]; j++ {
oldColor := RGBColor{
R: src.GetUCharAt(i, j*3),
G: src.GetUCharAt(i, j*3+1),
B: src.GetUCharAt(i, j*3+2),
}
newColor := getColor(oldColor)
res.SetUCharAt(i, j*3, newColor.R)
res.SetUCharAt(i, j*3+1, newColor.G)
res.SetUCharAt(i, j*3+2, newColor.B)
}
}
return
}

// 寻找欧氏距离最短的颜色
func getColor(src RGBColor) RGBColor {
index := 0
dis := getDis(src, colorTable[0])
for i, c := range colorTable {
nDis := getDis(src, c)
if nDis < dis {
index = i
dis = nDis
}
}
return colorTable[index]
}

func getDis(a, c RGBColor) int {
var r, g, b int
r = int(a.R) - int(c.R)
g = int(a.G) - int(c.G)
b = int(a.B) - int(c.B)
return r*r + g*g + b*b
}

效果分析

原图:

redapple

效果图

goodapple

可以看出,一些细节部分的颜色缺失掉了。

我们可以输出他的LUT看一看

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
[{0 4 121} {0 16 103} {5 11 122} {8 21 104} {1 8 136} {1 16 139} {7 13 139} {9 21 139}
{1 32 98} {1 53 93} {8 32 95} {8 49 98} {3 32 132} {3 46 134} {10 32 135} {10 46 134}
{18 26 124} {19 35 117} {28 33 130} {32 39 129} {20 30 141} {22 38 142} {31 35 141}
{34 40 141} {25 44 121} {23 54 113} {40 44 136} {43 52 127} {32 43 143} {32 48 143}
{42 46 143} {46 51 143} {7 18 148} {11 33 149} {28 36 148} {31 42 148} {9 25 156}
{13 37 161} {28 36 159} {31 42 161} {21 46 151} {18 53 152} {35 46 151} {35 51 153}
{23 48 167} {22 57 169} {35 49 167} {35 57 171} {41 45 149} {41 50 150} {45 49 151}
{47 53 151} {42 46 163} {42 53 163} {48 50 166} {49 53 166} {44 55 159} {45 59 162}
{53 56 156} {55 60 158} {47 57 177} {46 60 176} {56 57 176} {58 60 179} {3 68 89}
{2 88 86} {22 69 86} {27 84 90} {17 65 158} {12 81 150} {40 63 166} {39 73 163}
{4 101 96} {5 113 107} {27 106 104} {30 117 111} {14 118 127} {17 137 136} {38 117 126}
{39 137 136} {53 63 158} {54 74 135} {65 67 158} {73 82 139} {52 64 172} {54 69 174}
{66 66 173} {74 78 174} {55 109 118} {55 131 130} {85 107 127} {84 128 135} {71 123 154}
{70 153 158} {100 110 158} {99 148 162} {50 65 181} {49 71 181} {65 65 181} {68 70 182}
{51 64 193} {51 70 194} {66 67 192} {69 72 192} {51 76 185} {41 90 187} {70 76 186}
{69 86 188} {55 81 200} {54 102 206} {71 80 199} {70 104 209} {78 75 187} {80 83 187}
{86 84 188} {92 92 188} {81 80 198} {82 89 202} {91 88 200} {94 94 202} {95 98 196}
{92 123 196} {105 102 196} {107 122 195} {89 105 213} {90 130 220} {105 106 212}
{106 136 220} {116 117 179} {119 138 166} {134 136 179} {140 148 178} {115 113 203}
{121 122 204} {130 128 204} {142 142 204} {139 155 176} {137 164 179} {153 158 181}
{154 163 182} {135 160 194} {136 175 200} {154 159 193} {153 174 197} {159 163 186}
{160 168 187} {163 168 188} {166 172 191} {161 160 200} {163 170 200} {169 167 203}
{170 173 201} {168 175 196} {168 181 199} {175 178 200} {180 184 202} {172 177 207}
{173 185 207} {182 184 207} {187 191 208} {118 117 213} {126 130 214} {137 135 214}
{149 148 214} {125 128 223} {127 145 226} {144 142 222} {152 152 224} {156 168 213}
{156 184 214} {174 173 213} {175 185 213} {151 162 228} {145 185 226} {168 169 226}
{170 189 226} {180 178 214} {181 186 214} {187 185 215} {189 190 214} {183 179 223}
{184 188 222} {192 185 226} {193 190 224} {187 193 213} {187 196 215} {195 194 215}
{195 198 216} {190 193 224} {190 197 224} {200 194 226} {201 198 226} {191 200 218}
{191 203 219} {201 201 219} {201 204 221} {190 201 228} {194 204 227} {204 202 229}
{204 205 228} {198 206 224} {197 210 225} {206 207 225} {206 210 226} {195 208 233}
{197 216 236} {207 209 231} {208 215 233} {210 209 228} {211 214 230} {214 211 230}
{215 216 230} {212 210 236} {213 217 236} {217 213 236} {219 218 237} {217 220 236}
{218 224 238} {224 223 239} {230 231 241} {227 229 246} {236 239 248} {243 240 249}
{246 245 248} {237 237 251} {244 242 251} {246 242 251} {247 243 252} {229 235 254}
{243 243 253} {247 243 253} {248 244 253} {247 245 253} {247 247 252} {251 247 253}
{251 251 252} {247 245 254} {246 247 255} {251 246 254} {251 249 254} {253 248 254}
{253 253 253} {254 249 254} {254 253 254} {255 255 255} {255 255 255} {255 255 255}
{255 255 255} {254 255 254} {255 255 255} {255 255 255} {255 255 255} {254 255 255}
{255 255 255} {255 255 255} {255 255 255}]

从上面可能看不出什么东西,因此我把LUT中三个通道的颜色提取出来并做了一个排序:

1
2
3
4
5
6
R:
0 0 1 1 1 1 2 3 3 3 4 5 5 7 7 8 8 8 9 9 10 10 11 12 13 14 17 17 18 18 19 20 21 22 22 22 23 23 25 27 27 28 28 28 30 31 31 31 32 32 32 34 35 35 35 35 38 39 39 40 40 41 41 41 42 42 42 43 44 45 45 46 46 47 47 48 49 49 50 51 51 51 52 53 53 54 54 54 55 55 55 55 56 58 65 65 66 66 68 69 69 70 70 70 71 71 73 74 78 79 81 82 84 85 86 89 90 91 92 92 94 95 99 100 105 105 106 107 115 116 118 118 121 125 126 126 130 134 135 136 137 137 139 140 142 144 145 149 151 152 153 153 154 154 156 156 159 160 161 162 163 166 168 168 168 169 170 170 172 173 174 175 175 180 180 181 182 183 184 187 187 187 187 189 190 190 190 191 191 192 193 193 195 195 195 197 197 198 200 201 201 201 204 204 206 206 207 208 210 211 212 212 214 215 217 217 218 219 224 227 229 230 235 237 243 243 244 246 246 246 247 247 247 247 247 248 251 251 251 251 253 253 254 254 254 254 255 255 255 255 255 255 255 255 255 255
G:
4 8 11 13 16 16 18 21 21 25 26 30 32 32 32 32 33 33 35 35 36 36 37 38 39 40 42 42 43 44 44 45 45 46 46 46 46 46 48 48 49 49 49 50 50 51 51 52 52 53 53 53 53 53 55 56 57 57 57 57 58 60 60 60 63 63 64 64 65 65 65 66 67 67 68 69 69 70 70 71 72 73 74 75 76 76 78 80 80 81 81 82 82 84 84 86 88 88 89 90 92 94 98 101 102 102 104 105 106 106 107 109 110 113 113 117 117 117 117 118 122 122 123 123 128 128 128 130 130 131 135 136 136 137 137 138 142 142 145 148 148 148 152 153 155 158 159 160 160 162 163 163 164 167 167 168 168 169 170 172 173 173 174 175 175 177 178 178 179 181 184 184 184 185 185 185 185 185 186 188 189 190 190 191 193 193 194 194 196 197 198 198 200 201 201 202 203 204 204 205 206 207 208 209 209 210 210 210 211 213 214 215 216 216 216 218 220 223 224 229 231 235 237 238 240 242 242 243 243 243 244 245 245 245 246 247 247 247 248 249 249 251 253 253 255 255 255 255 255 255 255 255 255 255 255 255
B:
86 86 89 90 93 95 96 98 98 103 104 104 107 111 113 117 118 121 121 122 124 126 127 127 127 129 129 130 132 134 134 135 135 135 136 136 136 136 139 139 139 139 141 141 141 142 143 143 143 143 148 148 148 149 149 150 150 151 151 151 151 152 153 154 156 156 158 158 158 158 158 158 159 159 161 161 162 162 163 163 163 166 166 166 166 167 167 169 171 172 173 174 174 176 176 176 177 178 179 179 179 179 181 181 181 181 182 182 185 186 186 187 187 187 187 188 188 188 188 191 192 192 193 193 194 194 195 195 196 196 196 197 198 199 199 199 200 200 200 200 200 201 202 202 202 203 203 204 204 204 206 207 207 207 208 209 212 213 213 213 213 213 213 214 214 214 214 214 214 214 215 215 215 216 218 219 219 219 220 221 222 222 223 223 224 224 224 224 224 225 225 226 226 226 226 226 226 226 226 227 228 228 228 228 229 230 230 230 231 233 233 236 236 236 236 236 237 238 239 241 246 247 248 249 251 251 251 252 252 252 253 253 253 253 253 253 254 254 254 254 254 254 254 254 254 255 255 255 255 255 255 255 255 255 255 255

然后我们得以下的图表

1539525642385

1539525961074

由于我们对R和G划分了3次,而对B只划分了两次,而且图中大多数的颜色都是红色和绿色,因此R和G的颜色的变化和范围也就更加地广。再加上图中红色是主体颜色,因此可以看出红色颜色的变化比起绿色和蓝色会更加地平滑,换句话来说,就是需要更多的红色来表示图片。
$$

土豪通道
0%