- Article -

使用GOLANG查找图片内容并替换

分类于 后端开发 标签 GOLAN图片搜索 发表于2020-08-16 08:45

如果你是一名设计师,有可能需要将几万张图片里面的文字或者一些关键内容替换成其他文字或者图片内容,或者找出这些图片中存在某些内容的特殊图片,如何快速的用程序来帮我完成这些事情呢?还记得你写 JAVASCRIPT 的时候使用的字符串方法 replace 和 indexOf 吗?so 相应的实现了这两个方法我们就能处理这些图片。

设计接口

在开发之前我们先想清楚对外提供的接口

结构体和初始化


const defaultCompareAccuracy = 10 //查找图片的精确值,默认查找图片平均有10分之一的像素对应即认为两部分图片一样。

type Picture struct {
	Img             image.Image
	Width           int
	Height          int
	Path            string
	CompareAccuracy int
}

func NewJpeg(path string) (*Picture, error) {

	read, err := os.Open(path)
	if err != nil {
		return &Picture{}, err
	}
	defer read.Close()

	img, err := jpeg.Decode(read)
	if err != nil {
		return &Picture{}, err
	}

	return newPic(img, path), nil
}

func NewPng(path string) (*Picture, error) {

	read, err := os.Open(path)
	if err != nil {
		return &Picture{}, err
	}
	defer read.Close()

	img, err := png.Decode(read)
	if err != nil {
		return &Picture{}, err
	}

	return newPic(img, path), nil
}
func newPic(img image.Image, path string) *Picture {

	rectangle := img.Bounds()
	w := rectangle.Max.X
	h := rectangle.Max.Y
	return &Picture{
		Img:             img,
		Width:           w,
		Height:          h,
		Path:            path,
		CompareAccuracy: defaultCompareAccuracy,
	}
}

方法

func (p *Picture) SetCompareAccuracy(compareAccuracy int) {
	p.CompareAccuracy = compareAccuracy
}

func (p *Picture) SearchPic(searchPic *Picture) (bool, image.Rectangle) {
	rectangles := seekPos(p, searchPic, true)
	if len(rectangles) == 0 {
		return false, image.Rectangle{}
	}
	return true, rectangles[0]
}

func (p *Picture) SearchAllPic(searchPic *Picture) (bool, []image.Rectangle) {
	rectangles := seekPos(p, searchPic, false)
	if len(rectangles) == 0 {
		return false, rectangles
	}
	return true, rectangles
}

func (p *Picture) Replace(searchPic *Picture, replacer *Picture) (image.Image, error) {

	if searchPic.Width != replacer.Width || searchPic.Height != replacer.Height {
		return p.Img, errors.New("查找和替换的图片大小不一致")
	}

	isExist, rectangle := p.SearchPic(searchPic)
	if !isExist {
		return p.Img, errors.New("在" + p.Path + "并未发现" + searchPic.Path)
	}

	dst := p.Img
	if dst, ok := dst.(draw.Image); ok {
		draw.Draw(dst, rectangle, replacer.Img, image.Point{}, draw.Src)
	}
	return dst, nil
}

func (p *Picture) ReplaceAll(searchPic *Picture, replacer *Picture) (image.Image, error) {

	if searchPic.Width != replacer.Width || searchPic.Height != replacer.Height {
		return p.Img, errors.New("查找和替换的图片大小不一致")
	}

	isExist, rectangles := p.SearchAllPic(searchPic)
	if !isExist {
		return p.Img, errors.New("在" + p.Path + "并未发现" + searchPic.Path)
	}

	dst := p.Img
	if dst, ok := dst.(draw.Image); ok {
		for _,rectangle := range rectangles{
			draw.Draw(dst, rectangle, replacer.Img, image.Point{}, draw.Src)
		}
	}
	return dst, nil
}

func scanAreaOk(intX, intY int, p, searchPic *Picture) bool {
	h :=  searchPic.Height - 1
	w := searchPic.Width - 1

	if p.CompareAccuracy < 1 || h < p.CompareAccuracy || w < p.CompareAccuracy{
		p.SetCompareAccuracy(1)
	}

	for y := 0; y <= searchPic.Height-1; y += p.CompareAccuracy {
		for x := 0; x <= searchPic.Width-1; x += p.CompareAccuracy {
			if searchPic.Img.At(x, y) != p.Img.At(intX+x, intY+y) {
				return false
			}
		}
	}
	return true
}

func seekPos(p *Picture, searchPic *Picture, searchOnce bool) []image.Rectangle {
	var rectangles []image.Rectangle
	if searchPic.Width > p.Width || searchPic.Height > p.Height {
		return rectangles
	}
	for y := 0; y <= (p.Height - searchPic.Height); y++ {
		for x := 0; x <= (p.Width - searchPic.Width); x++ {
			if searchPic.Img.At(0, 0) != p.Img.At(x, y) ||
				searchPic.Img.At(searchPic.Width - 1, 0) != p.Img.At(x + searchPic.Width - 1, y) ||
				searchPic.Img.At(searchPic.Width - 1, searchPic.Height - 1) != p.Img.At(x + searchPic.Width - 1, y + searchPic.Height - 1) ||
				searchPic.Img.At(0, searchPic.Height - 1) != p.Img.At(x, y + searchPic.Height - 1) { //四个角只要有一个颜色对应不上直接跳到下一次
				continue
			}

			if !scanAreaOk(x, y, p, searchPic) { //四个角对上了在扫描区域,不成功直接下一次,
				continue
			}

			min := image.Point{X: x, Y: y}
			max := image.Point{X: x + searchPic.Width, Y: y + searchPic.Height}
			rectangles = append(rectangles, image.Rectangle{Min: min, Max: max})
			if searchOnce {
				return rectangles
			}
		}
	}
	return rectangles
}

总结

关键的就是 seekPos 和 scanAreaOk 两个内部方法了,一个负责查找位置并给出矩形区域,另外一个是扫描矩形是否和查找的图片一样,有了这几个关键的函数,那我们就可以基于此遍历所有目录,将100张图片为一组的进行并行处理,根据上面的方法,我们也很清楚的知道一些规则。

让我浪费时间的地方是对于 CompareAccuracy 的值处理,我一度怀疑我是不是做错了,CompareAccuracy 是对比的准确度。这个值是用来提高函数处理效率的, 被我描述为"查找图片的精确值,默认查找图片平均有10分之一的像素对应即认为两部分图片一样",他被使用在 scanAreaOk 函数,如果一个图片有1000个像素,如果 CompareAccuracy 值是 10 的话,那么我希望他在这 1000 个像素里面 平均的取 10/1 个像素来比较,总共取 1000/10 个点, y += p.CompareAccuracy 最后肯定是被执行 1000/10 次就超出 y <= searchPic.Height-1 了,因此,这一描述是对的。

这个逻辑不难,却花费了我一些时间,看来我需要补充算法的一些知识点了 😥