加水印应该是个很常见的需求,但是网上找的代码,都感觉不太完善。记录下自己搞出来的一个方案
水印有几个需求:
- 中文文字水印
- 文字倾斜
- 满图都是,而不是只有一个地方
- 水印文字所在之处完全展示水印
实现思路
准备水印图
我是这么做的,先手工生成一张水印图,尺寸比较小,约100*100。然后也把文字旋转一定角度(当然如果想做随机角度,也是可以的,代码再复杂点处理就好,不影响这个思路),生成这个一张图片,并且底色选择纯黑。
然后用opencv将水印图处理出一张二值图
这个也不难,底色纯黑,这个很好做,用Threshold
很容易实现。
然后用水印图和原图叠加
核心就是要用copyTo()
,然后要输入mask,二值图的作用就是做掩码,这样就可以实现完美的水印添加效果。
其他细节
因为水印图通常是远小于原图的(其实也可以采用其他方案。总之核心就是要有水印图和对应的二值图),如何实现满图添加水印呢?
思路
- 先设置一个水印的间距,比如两百个像素点
- 然后通过水印图的大小和间距的值,计算生成一个尺寸大于等于原图的纯水印图(同时也要有二值图)
- 然后裁剪成和原图一样大
- 然后用
copyTo()
叠加即可。
这样性能很高,go语言实现处理一张图片不超过10ms。
go语言实现
思路都是通用的,基于opencv都能实现。
var (
Watermark3 *gocv.Mat
Watermark4 *gocv.Mat
WatermarkBW3 *gocv.Mat
WatermarkBW4 *gocv.Mat
Watermark1 *gocv.Mat
WatermarkBW1 *gocv.Mat
)
func ImgWatermark(img gocv.Mat) {
var Watermark gocv.Mat
var WatermarkBW gocv.Mat
// 不同通道的图片使用不同的水印
if img.Channels() == 3 {
// 判断是否需要初始化
if Watermark3 == nil {
mat := gocv.IMRead(`watermark.jpg`, gocv.IMReadColor)
Watermark3 = &mat
// 取得水印图片的二值图
watermarkGray3 := gocv.NewMat()
gocv.CvtColor(*Watermark3, &watermarkGray3, gocv.ColorBGRToGray)
// 取得二值图的黑白图
BW3 := gocv.NewMat()
WatermarkBW3 = &BW3
gocv.Threshold(watermarkGray3, WatermarkBW3, 30, 255, gocv.ThresholdBinary)
watermarkGray3.Close()
}
Watermark = *Watermark3
WatermarkBW = *WatermarkBW3
} else if img.Channels() == 4 {
// 判断是否需要初始化
if Watermark4 == nil {
mat := gocv.IMRead(`watermark.jpg`, gocv.IMReadColor)
Watermark4 = &mat
findOrCreateWatermark1()
// 创建一个纯白背景图
singleWhiteMat := gocv.NewMatWithSizeFromScalar(gocv.NewScalar(255, 255, 255, 255), mat.Rows(), mat.Cols(), gocv.MatTypeCV8UC1)
ha := gocv.NewMat()
gocv.Multiply(singleWhiteMat, *WatermarkBW1, &ha)
// 纯白图片与二值图进行矩阵乘法,得到单通道的
gocv.Merge([]gocv.Mat{mat, ha}, Watermark4)
gocv.IMWrite("Watermark4.png", *Watermark4)
// 取得黑白图的二值图
BW4 := gocv.NewMat()
WatermarkBW4 = &BW4
gocv.Merge([]gocv.Mat{*WatermarkBW1, *WatermarkBW1, *WatermarkBW1, *WatermarkBW1}, WatermarkBW4)
}
Watermark = *Watermark4
WatermarkBW = *WatermarkBW4
} else if img.Channels() == 1 {
// 判断是否需要初始化
if Watermark1 == nil {
mat := gocv.IMRead(`watermark.jpg`, gocv.IMReadGrayScale) // 直接读取黑白
Watermark1 = &mat
// 取得水印图片的二值图
BW1 := gocv.NewMat()
WatermarkBW1 = &BW1
gocv.Threshold(mat, WatermarkBW1, 30, 255, gocv.ThresholdBinary)
}
Watermark = *Watermark1
WatermarkBW = *WatermarkBW1
} else {
fmt.Println("图片通道数不支持", img.Channels())
return
}
waterCol := Watermark.Cols()
waterRow := Watermark.Rows()
// 为了水印不要那么密,这里设置一个间距
spacing := 200 // 水印的间距
waterColAndSpacing := waterCol + spacing
watreRowAndSpacing := waterRow + spacing
// 计算需要添加水印的次数
watermarkHugeRowTimes := int(math.Ceil(float64(img.Rows()) / float64(watreRowAndSpacing))) //向上取整
watermarkHugeColTimes := int(math.Ceil(float64(img.Cols()) / float64(waterColAndSpacing)))
imgRect := image.Rectangle{} //每次取图像中哪一块区域的数据结构
var croppedMat gocv.Mat // 每次从原图像上裁剪和水印图像一样大小的一块
// 循环一块块的给图像添加水印
for i := 0; i < watermarkHugeColTimes; i++ {
newWater := Watermark // 因为有可能要裁剪后才可以进行复制,所以不能直接在原图上裁剪,得复制一个新的,下同
newWaterBW := WatermarkBW
imgRect.Min.X = i * waterColAndSpacing
imgRect.Max.X = imgRect.Min.X + waterCol
if imgRect.Max.X >= img.Cols() { // 判断是否超出原图像的列数
imgRect.Max.X = img.Cols()
}
for j := 0; j < watermarkHugeRowTimes; j++ {
imgRect.Min.Y = j * watreRowAndSpacing
imgRect.Max.Y = imgRect.Min.Y + waterRow
if imgRect.Max.Y >= img.Rows() { // 判断是否超出原图像的行数
imgRect.Max.Y = img.Rows()
}
croppedMat = img.Region(imgRect) // 原图像中一块区域的浅拷贝,修改它会连带修改原图像
// 判断裁剪的图像大小是否与水印图像大小一致,不一致则需要重新裁剪
if croppedMat.Rows() != Watermark.Rows() || croppedMat.Cols() != Watermark.Cols() {
newRect := image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: croppedMat.Cols(), Y: croppedMat.Rows()}}
newWater = Watermark.Region(newRect)
newWaterBW = WatermarkBW.Region(newRect)
}
newWater.CopyToWithMask(&croppedMat, newWaterBW) // 用水印图覆盖原图像
}
gocv.IMWrite(fmt.Sprintf("result%d.png", i), img)
}
}
func findOrCreateWatermark1() {
// 判断是否需要初始化
if Watermark1 == nil {
mat := gocv.IMRead(`watermark.jpg`, gocv.IMReadGrayScale) // 直接读取黑白
Watermark1 = &mat
// 取得水印图片的二值图
BW1 := gocv.NewMat()
WatermarkBW1 = &BW1
gocv.Threshold(mat, WatermarkBW1, 30, 255, gocv.ThresholdBinary)
}
}