这个项目的任务是在Web中将用户上传的图片马赛克化,为了提升速度,采用并发模式进行。
1. 马赛克处理函数
马赛克意思是:指定文件夹内有数百张图片,用户上传一张图片后,根据相应的条件,将这数百张图片以方块形式映射到用于上传的图片上,形成马赛克效果。
马赛克处理函数分为以下几块:
- 马赛克图片数据库的构建
- 图片RGB通道均值计算函数
- 缩放函数
- 图片扫描建档函数
- 映射
- 计算欧式距离
- 寻找最近图片
- 克隆映射
1.1 马赛克图片数据库的构建
RGB均值计算函数如下,返回一个三元数组:
func averageColor(img image.Image) [3]float64{
bounds:=img.Bounds()
r,g,b :=0.0,0.0,0.0
for y:=bounds.Min.Y;y<bounds.Max.Y;y++{
for x:=bounds.Min.X; x<bounds.Max.X;x++{
r1,g1,b1,_:=img.At(x,y).RGBA()
r,g,b = r+float64(r1),g+float64(g1),b+float64(b1)
}
}
totalPixels:=float64(bounds.Max.X * bounds.Max.Y)
return [3]float64{r/totalPixels,g/totalPixels,b/totalPixels}
}
接下来是图片缩放函数,将指定文件夹内的图片资料缩放:
func resize(in image.Image,newWidth int) image.NRGBA{
bounds:=in.Bounds()
ratio := bounds.Dx()/newWidth
out := image.NewNRGBA(image.Rect(bounds.Min.X/ratio, bounds.Min.X/ratio,
bounds.Max.X/ratio, bounds.Max.Y/ratio))
for y,j := bounds.Min.Y,bounds.Min.Y;y<bounds.Max.Y;y,j = y+ratio,j+1{
for x, i := bounds.Min.X, bounds.Min.X; x < bounds.Max.X; x, i =x+ratio, i+1 {
r,g,b,a := in.At(x,y).RGBA()
out.SetNRGBA(i,j,color.NRGBA{uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8)})
}
}
return *out
}
然后是图片扫描建档函数,注意包必须引入import _ "image/jpeg"
否则无法解析:
func tilesDB() map[string][3]float64{
fmt.Println("Start populating tiles db ...")
db := make(map[string][3]float64)
files,_ := ioutil.ReadDir("tiles")
for _,f:=range files{
name := "tiles/" + f.Name()
file,_:=os.Open(name)
img,_,_:=image.Decode(file)
db[name]=averageColor(img)
file.Close()
}
fmt.Println("Finished populating tiles db.")
return db
}
1.2 映射
首先是计算欧式距离,计算两张图片平均RGB之间的距离:
func distance(p1 [3]float64,p2 [3]float64) float64{
r2:=(p2[0]-p1[0])*(p2[0]-p1[0])
g2:=(p2[1]-p1[1])*(p2[1]-p1[1])
b2:=(p2[2]-p1[2])*(p2[2]-p1[2])
return math.Sqrt(r2+g2+b2)
}
然后需要在资料库中找到最相似的一张图片,找到以后需要在资料库中删除,以防止重复:
func nearest(target [3]float64, db *map[string][3]float64) string{
var filename string
smallest := 1000000.0
for k,v := range *db{
dist:=distance(target,v)
if dist<smallest{
filename,smallest = k,dist
}
}
delete(*db,filename)
return filename
}
因为需要删除,所以每次生成图片时我们还需要克隆一份资料库,不然下一次资料库就没了。
var TILESDB map[string][3]float64
func cloneTilesDB() map[string][3]float64{
db := make(map[string][3]float64)
for k,v:=range TILESDB{
db[k]=v
}
return db
}
2. Web应用
Web应用可以分为两部分:上传和结果显示。
2.1 上传
上传部分比较简单,将模板解析后,执行相应的请求即可。
func upload(w http.ResponseWriter, r *http.Request){
t,_:=template.ParseFiles("upload.html")
t.Execute(w,nil)
}
2.2 显示
显示部分很复杂,代码如下:
func mosaic(w http.ResponseWriter, r *http.Request){
t0:=time.Now()
r.ParseMultipartForm(10*1024*1024)
file,_,_:=r.FormFile("image")
//FormFile returns the first file for the provided form key.
defer file.Close()
tileSize, _:=strconv.Atoi(r.FormValue("tile_size"))
original,_,_:=image.Decode(file)//文件解码
bounds:=original.Bounds()
newimage:=image.NewNRGBA(image.Rect(bounds.Min.X, bounds.Min.X,
bounds.Max.X, bounds.Max.Y))
db:=cloneTilesDB()
sp:=image.Point{0,0}
for y:=bounds.Min.Y;y<bounds.Max.Y;y=y+tileSize{
for x:=bounds.Min.X;x<bounds.Max.X;x=x+tileSize{
r,g,b,_:=original.At(x,y).RGBA()
color:=[3]float64{float64(r),float64(g),float64(b)}
nearest:=nearest(color,&db)
file,err := os.Open(nearest)
if err!=nil{
fmt.Println("error:",err,nearest)
}
img,_,err:=image.Decode(file)
t:=resize(img,tileSize)
tile:=t.SubImage(t.Bounds())
tileBounds := image.Rect(x, y, x+tileSize, y+tileSize)
draw.Draw(newimage,tileBounds,tile,sp,draw.Src)
file.Close()
}
}
buf1 := new(bytes.Buffer)
jpeg.Encode(buf1,original,nil)
originalStr:=base64.StdEncoding.EncodeToString(buf1.Bytes())
buf2:=new(bytes.Buffer)
jpeg.Encode(buf2,newimage,nil)
mosaic:=base64.StdEncoding.EncodeToString(buf2.Bytes())
t1 := time.Now()
images:=map[string]string{
"original":originalStr,
"mosaic":mosaic,
"duration":fmt.Sprintf("%v",t1.Sub(t0)),
}
t,_:=template.ParseFiles("results.html")
t.Execute(w,images)
}
大体上分为三部分:
- 提取上传文件内容
- 执行马赛克化
- 将结果重新编码,送入结果模板
(1)提取上传内容
r.ParseMultipartForm(10*1024*1024)
file,_,_:=r.FormFile("image")
//FormFile returns the first file for the provided form key.
defer file.Close()
tileSize, _:=strconv.Atoi(r.FormValue("tile_size"))
original,_,_:=image.Decode(file)//文件解码
上传内容以请求的形式放在了http.request
中,前面提到过ParseMultipartForm
专门用于解析文件,参数是文件的大小限制(这里限制到10MB)。接着从Form中提取图片文件和马赛克大小。然后将文件解码形成图片。
image.Decode
函数第二个返回对象是图片的后缀格式,一个字符串。
(2)执行马赛克化
bounds:=original.Bounds()
newimage:=image.NewNRGBA(image.Rect(bounds.Min.X, bounds.Min.X,
bounds.Max.X, bounds.Max.Y))
db:=cloneTilesDB()
sp:=image.Point{0,0}
for y:=bounds.Min.Y;y<bounds.Max.Y;y=y+tileSize{
for x:=bounds.Min.X;x<bounds.Max.X;x=x+tileSize{
r,g,b,_:=original.At(x,y).RGBA()
color:=[3]float64{float64(r),float64(g),float64(b)}
nearest:=nearest(color,&db)
file,err := os.Open(nearest)
if err!=nil{
fmt.Println("error:",err,nearest)
}
img,_,err:=image.Decode(file)
t:=resize(img,tileSize)
tile:=t.SubImage(t.Bounds())
tileBounds := image.Rect(x, y, x+tileSize, y+tileSize)
draw.Draw(newimage,tileBounds,tile,sp,draw.Src)
file.Close()
}
}
首先要做准备工作:以原图片为基准,新建一个图片容器,同时克隆一份资料库。然后在循环中找到最佳图片,经过大小缩放,裁剪以后,放到新图片中去。
(3)重新编码
buf1 := new(bytes.Buffer)
jpeg.Encode(buf1,original,nil)
originalStr:=base64.StdEncoding.EncodeToString(buf1.Bytes())
buf2:=new(bytes.Buffer)
jpeg.Encode(buf2,newimage,nil)
mosaic:=base64.StdEncoding.EncodeToString(buf2.Bytes())
t1 := time.Now()
images:=map[string]string{
"original":originalStr,
"mosaic":mosaic,
"duration":fmt.Sprintf("%v",t1.Sub(t0)),
}
t,_:=template.ParseFiles("results.html")
t.Execute(w,images)
这一步就是将图片重新编码为二进制字符串,然后送入模板中解析。
3. 并发化
并发的思路很简单:将图片分割为4份,分别执行。但注意,这里存在竞争,因为大家在访问资料库时,每次都会删除,所以我们需要用锁锁起来。