这个项目的任务是在Web中将用户上传的图片马赛克化,为了提升速度,采用并发模式进行。

1. 马赛克处理函数

马赛克意思是:指定文件夹内有数百张图片,用户上传一张图片后,根据相应的条件,将这数百张图片以方块形式映射到用于上传的图片上,形成马赛克效果。

马赛克处理函数分为以下几块:

  • 马赛克图片数据库的构建
    1. 图片RGB通道均值计算函数
    2. 缩放函数
    3. 图片扫描建档函数
  • 映射
    1. 计算欧式距离
    2. 寻找最近图片
    3. 克隆映射

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. 提取上传文件内容
  2. 执行马赛克化
  3. 将结果重新编码,送入结果模板

(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份,分别执行。但注意,这里存在竞争,因为大家在访问资料库时,每次都会删除,所以我们需要用锁锁起来。