给图片加水印--手把手教新码农如何把技术变成产品

前言

加水印是为图片声明版权出处的一种经常使用方法。
日常都是写技术文章,文章的重点在技术自己,照片每每不须要加水印,或者须要加也很少,祭出神器PhotoShop很快就能完成。
前一段趁着夏天还不很热的时候出去游荡,回来应约写了游记,实际上是给别人当作攻略来用。
游记可就不一样了,照片成为了主体,而且量很大。随便一个景区的流程,十几副照片老是免不了的。这个时候,还用PhotoShop来加水印,固然不是不行,但那显然非我等“攻城狮”所愿为的。
因而咱们为图片加水印的“产品”,就此立项啦。html

某个技术的出现多是由于积累,可能由于意外,可能由于爱好。但产品,老是由于一个“需求”而开始。c++

水印文件

为图片加水印,首先你得先有一个水印。固然随随便便在图片上加一行字也是水印,但若是想拿得出手,有位美工帮你操刀再好不过。要说如今的程序员,天天团队一块儿工做,谁还没几位要好的美工朋友。
什么?你没有?那你可要注意了。如今无论是作研发,仍是作产品,一我的打天下的时代已通过了。程序员

在团队中,技术当然重要,沟通能力则更为重要。若是不能在每一个岗位都有本身的铁杆兄弟,忙碌一生,你也只能是个小码农。bash

在这方面,可别迷信职位所带来的“权利”,“权利”和“关系”所能起的做用,那但是天壤之别。服务器

我手头就有一个现成的水印,用了得十多年了。虽然看起来在设计上已经跟不上时代,但这种纯个性化的东西,你架不住喜欢。网络

用户的需求才是第一位的,做为程序员,你能够说用户是外行,啥也不懂。但用户要的才算数,你说的,不算数。
固然若是你的沟通能力超群,把用户给劝服了,那当我没说。函数

用做水印的图片,首先要有“镂空”的特质。好比你看题头图的右下角,水印只有主体的部分出如今图片上。其他的部分,仍然是照片自己。看上去水印图片,就是镂空的样子。
其实不少标准的图片格式自己就支持镂空,好比GIF图片,好比PNG图片。在Web网页的设计中,镂空图片原本就有很大的使用量。
可是在咱们这个显然并不大的项目中,采用这些图形格式做为水印图片的标准并不划算,一方面用户制做水印图片每每须要额外的操做增长工做量。另外一方面在自动添加水印的程序中解析这些图片中的镂空结构也须要额外的工做量。post

除非“标准化”自己也是用户的需求之一,不然虽然标准化有不少好处,但快速完成项目才是第一追求的目标。学习

制做一个水印文件最容易的方法是在PhotoShop中,把主体内容独立一层,随后把背景部分所有涂黑。这个黑必定要是真正的黑,也即RGB三个值所有为0。实际上任何不会引发冲突的颜色都是能够的,好比咱们常见到特技拍摄中用到的蓝箱、绿箱。但使用全黑的背景处理起来仍是最容易的。

测试

在程序中操做图片,最强大的固然是opencv库。给工程师用,拿Python写个脚本就够了。若是是给普通用户,能够编译为可执行文件的c/c++确定是更优选。

版本1

接着无论是你自己就是图像处理的高手,原来就熟悉这方面的工做。仍是在互联网上搜索别人的经验,学习别人的程序。总之,很快你就拿出了一个版本,为图片添加水印。

#include <stdio.h>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace std;
using namespace cv;

const char *picfile="IMG_20190521_125150.jpg";
const char *logofile="logo.png";
const char *outputfile="IMG_20190521_125150-logoed.jpg";
const int mx=10,my=10;

int main(int argc, char **argv){
    Mat image = imread(picfile);
    Mat logo = imread(logofile);
    Mat mask=imread(logofile,0);
    Mat imageROI;

    imageROI = image(Rect(mx,my,logo.cols,logo.rows));
    logo.copyTo(imageROI,mask);
    imshow("result",image);
    waitKey();
}

问题并不复杂,打开图片和做为水印的logo,而后再读取图片中做为镂空的背景部分。接着把logo镂空部分去除,而后复制到目标图片上就完成了工做,主要的工做代码只有7行。
主要函数使用copyTo,点击连接是opencv官方的说明文档。
opencv的编译,须要在命令行给出头文件和连接库的额外参数,建议写一个脚原本编译,这里也贴出来(本例中使用当前的opencv4):

#!/bin/bash

g++ -std=c++11 -o $1 $1.cpp `pkg-config --cflags --libs opencv4`

使用脚原本编译和执行使用以下命令(假设源码名称为wmv1.cpp):

$ ./mkcv4.sh wmv1
$ ./wmv1

在一张样本的图片上运行这个程序,获得的结果效果以下:

看起来,完美的解决了用户的需求,完活收工......

等等,这是咱们“虚拟”的一个项目,写文章嘛,没点借口怎么向下写。不过若是这是一个真实的项目,这就到了见客户的时候。相信我,若是客户见了这个程序,确定会提出一堆的意见回来。好比:

  • 这是水印吗?水印应当是半透明的,这只能叫不干胶。
  • 为何只能处理什么乱七八糟的IMG_20190521_125150.jpg文件,我要把每一个文件都改为这个名字才能处理吗?
  • 为何水印看上去这么大,跟画面一点也不协调
  • 水印为何只能放在左上角,我想放在右下角可不能够?
  • ......

从客户那边回来,甭管是产品经理仍是销售经理,我估计已经被用户教训的怀疑人生了。因此这个时候他们的脾气不会太好,而后跟程序员沟通起来,耐心确定也就不够。因而程序员,就处在了崩溃的边缘。用户有多少条意见,程序员就有多少条抓狂的理由。

  • 用户是掏钱的,既然想从用户那里挣钱,用户说什么你都得学会听着。
  • 用户其实根本不知道本身想要什么,乔布斯都这么说。但用户天生会挑毛病。
  • 记着前面说的,一我的打不了天下,由于有不少人挑毛病,你的产品才能适应更多人。

版本2

无论有多么不高兴,生活总要继续,工做也得推进下去。
其实用户挑毛病永远不是最可怕的,可怕的是用户不挑毛病,而且还不买单。
因此既然用户有反馈,咱们逐条解决就行了。
首先看“水印效果”的问题,opencv中有专门的函数addWeighted处理两幅图片之间的重叠互动问题。用起来更简单,连蒙版mask部分都不须要了:

const float _alpha=0.5;

    Mat image = imread(picfile);
    Mat logo = imread(logofile);
    Mat imageROI;

    imageROI = image(Rect(mx,my,logo.cols,logo.rows));
    addWeighted(imageROI, 1.0, logo, _alpha, 0, imageROI);
    imwrite(outputfile,image);

水印尺寸偏大的问题,水印文件自己确定是固定的。但在大的图片中,水印确定显得小,小的图片中,水印就会显得大。所以须要水印图片的尺寸是能够变化的,是一个合理的需求。
opencv中调整图片的尺寸很容易,咱们能够要求用户输入一个水印logo尺寸的宽度,随后保持logo的比例,计算出来logo的新高度。而后调整logo的尺寸就能够了。

int neww,newh;
    neww = (int)_logowidth;
    newh = (int)(logo.rows * ((float)neww / logo.cols));
    Size dsize=Size(neww,newh);
    resize(logo,logo,dsize);

文件名、logo位置问题,均可以由程序运行时,用户输入的参数来肯定,这个再简单不过。
很快,第二版新鲜出炉:

#include <stdio.h>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace std;
using namespace cv;

#define PATH_MAX 1024

const float _alpha=0.5;

char _picfile[PATH_MAX];
char _outputfile[PATH_MAX];
char _logofile[PATH_MAX];
int _logowidth;
int _mx,_my;

int main(int argc, char **argv){
    if (argc != 7) {
        printf("Wrong parament!\n");
        return 1;
    }

    strcpy(_picfile,argv[1]);
    strcpy(_outputfile,argv[2]);
    strcpy(_logofile,argv[3]);
    _logowidth=atol(argv[4]);
    _mx=atol(argv[5]);
    _my=atol(argv[6]);


    Mat image = imread(_picfile);
    Mat logo = imread(_logofile);
    Mat imageROI;

    int neww,newh;
    neww = (int)_logowidth;
    newh = (int)(logo.rows * ((float)neww / logo.cols));
    Size dsize=Size(neww,newh);
    resize(logo,logo,dsize);

    imageROI = image(Rect(_mx,_my,logo.cols,logo.rows));
    addWeighted(imageROI, 1.0, logo, _alpha, 0, imageROI);
    imwrite(_outputfile,image);
}

咱们再次编译、执行来试一试:

$ ./mkcv4.sh wmv2
$ ./wmv2 IMG_20190521_125150.jpg IMG_20190521_125150-logoed.jpg logo.png 150 100 100

获得的图片以下:

看起来顺眼多了,刚才的问题,也都获得了解决。

咱们就再也不“装做”有用户的样子,相信刚才描述的用户反馈,大多人都有过这种经历,谁也不开心别人在本身的心血上指手画脚。但在真实的工做中,每每如此。
这只是一个虚拟的项目,用户也只是咱们本身。因此仍是让咱们本身来继续为项目挑毛病,指望能进一步完善。

  • 找到问题最好的办法就是大量使用,大范围使用。
  • 要珍视给你反馈意见的人,无论是测试仍是产品经理,他们是在帮你完善产品。

第二版的程序的确有了进步,但问题依然不少。

  • 参数太多,用起来很繁琐而且不友好,参数多了、少了、错了都会致使程序错误。
  • 初版“不干胶”模式添加水印的方式,实际仍是有意义的,值得保留。
  • 虽然水印添加位置能够随意了,但并很差用,咱们并不但愿水印出如今主题的位置。
  • 水印的尺寸虽然能够指定,但用起来并不方便,当目标图片尺寸不肯定的时候,给定水印的尺寸实际上不现实。

版本3

一样是挑毛病,由本身主动挑出来,是否是比别人挑出来在心理上更舒服?
同理,由本身的团队挑出来,固然也比让用户挑出来,更容易让全部人满意。
并且,若是把为图片加水印这一个动做算做“核心技术”的话,这一次挑出的全部毛病,基本都不是技术问题。而都是“好用”问题,或者叫“用户体验”问题。

在正常的工做中,最多不超过10%算的上技术问题,绝大多数开发工做,都是为了把技术,开发成可被用户接受的产品。而这些工做中,仍然有绝大多数不过是把参数换个顺序,按钮换个颜色之类的内容。

对于上面找出来的问题,c/c++中原本就有比较好的解决方案。就是使用getopt_long/switch配合的参数处理系统。在处理过程当中,为没有给出的参数,给出合理的默认值。
命令行程序,通常的窍门都是尽可能支持更多的参数,让动手能力强的用户能够更精细的定制。同时为参数尽量的提供默认值,让极少必要的参数,程序就能正常运行。
随后在这样的命令行程序的支持下,既能够在服务器端定制网页把程序包装成网络云服务。也可以写图形界面的外壳,给用户单机使用。
在这个思想的指导下,咱们梳理一下可能定制的参数:

  • 输入的图片文件名,程序将为这个图片添加水印,这个参数必不可少。
  • 输出的图片文件名,添加水印以后的图片,保存到这个文件。这个参数能够省略,省略的话,程序应当自动在输入文件名的基础上重命名一个文件名输出。此外还有一个潜在需求,输出文件名若是等同于输入文件名的话,至关于添加水印后替换原始文件。这要求程序读取完输入文件后,立刻关闭文件,不然写出到原文件会失败。
  • 水印Logo文件名。若是省略,应当使用当前目录中的一个默认Logo文件。
  • 水印图片缩放尺寸。创意一下,若是这个参数小于1,则表明水印图片缩放到目标图片的比例,好比0.3个目标图片宽度。若是这个参数大于1,则表明水印图片缩放到实际给定的尺寸。潜在需求,在这个应用中,用户天生只对图片宽度敏感,因此这个参数实际表明Logo宽度,Logo的高度应当等比缩放。
  • 水印的位置。刚才一个版本有了高度的自由,实际上并很差用。咱们只要指定水印在目标图片的四角之一就够了。这也能避免用户没法知道目标图片中,水印图片坐标的问题。
  • 水印方式,默认使用水印图片和目标图片混合的方式,也能够指定水印图片覆盖目标图片的方式。

梳理完修改需求,再次印证了上面的话,这些修改内容,跟核心的技术彻底没有关系。如今你知道“码农”这个词所为什么来了吧?

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <string.h>

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace std;
using namespace cv;

#define PATH_MAX 1024
#define LOGOPIC "./logo.png"

char _logoFilename[PATH_MAX];
char _srcFilename[PATH_MAX];
char _dstFilename[PATH_MAX];
const float _margin=0.01;
const float _alpha=0.5;
float _scale=0.3;
int _position=0;
int _copy=0;

struct option longopts[] = {
    { "input",        required_argument, NULL, 'i'},
    { "out",        required_argument, NULL, 'o'},
    { "scale",      required_argument, NULL, 's'},
    { "position",   required_argument, NULL, 'p'},
    { "logo",   required_argument, NULL, 'l'},
    { "copy",   required_argument, NULL, 'c'},
    { 0, 0, 0, 0},
};

void usage(){
    printf("Options:\n");
    printf("\t -i,--input\tPicture file to add water mark.\n");
    printf("\t -o,--out\tOutput picture name, add postfix '_logoed' on src filename if omit.\n");
    printf("\t -l,--logo\tlogo picture name, set to ./logo.png if omited.\n");
    printf("\t -s,--scale\tZooming logo picture to a new size, if this value below 1, \n");
    printf("\t\t\tmeans width of logo set to width of src picture * scale value,\n");
    printf("\t\t\totherwise, means width of logo scale to this pixel.\n");
    printf("\t -p,--position\tLogo position on src picture. can be 0/1/2/3, four corner.\n");    
    printf("\t -c,--copy\tCopy is keep logo's color, or shadow as default.\n");    
}

void dumpDefault(){
    printf("input:%s\n",_srcFilename);
    printf("out:%s\n",_dstFilename);
    printf("logo:%s\n",_logoFilename);
    printf("scale:%f\n",_scale);
    printf("postion:%d\n",_position);
    printf("copy:%d\n",_copy);
}

void addPostfix(char *srcfile,char *dstfile){
    const char *postfix="_logoed";
    char fname[PATH_MAX];
    strcpy(fname,srcfile);
    // char extName[PATH_MAX];
    char *p=strrchr(fname,'.');
    if (p == NULL) {
        strcpy(dstfile,fname);
        strcat(dstfile,postfix);
        return;
    }
    *p = '\0';
    strcpy(dstfile,fname);
    strcat(dstfile,postfix);
    strcat(dstfile,".");
    strcat(dstfile,p+1);
    return;
}

int getOptions(int argc,char **argv){
    int optIndex = 0;
    int c;

    strcpy(_logoFilename,LOGOPIC);
    strcpy(_srcFilename,"");
    strcpy(_dstFilename,"");

    while(1){
        c = getopt_long(argc, argv, "i:o:s:p:l:c", longopts, &optIndex);
        if(c == -1) {
            break;
        }
        switch(c) {
            case 'i':
                strncpy(_srcFilename,optarg,PATH_MAX);
                break;
            case 'o':
                strncpy(_dstFilename,optarg,PATH_MAX);
                break;
            case 'l':
                strncpy(_logoFilename,optarg,PATH_MAX);
                break;
            case 's':
                _scale = atof(optarg);
                break;
            case 'p':
                _position = atol(optarg);
                if ((_position>3) || (_position<0))
                    _position=0;
                break;
            case 'c':
                _copy = 1; //meas true
                break;
            default:
                usage();
        }
    }
    if (strlen(_srcFilename) == 0) {
        usage();
        exit(1);
    };
    if (strlen(_dstFilename) == 0) {
        addPostfix(_srcFilename,_dstFilename);
    };
    return 0;
}

/*
    position = 0, logo on right,bottom
    position = 1, logo on left,bottom
    position = 2, logo on left,top
    position = 3, logo on right,top
*/
void getPosition(int position,Mat image,Mat logo,int *X,int *Y){
    // x/y _margin using image.cols,not rows

    switch(position){
        case 0:
            *X=(image.cols-logo.cols) - (image.cols * _margin);
            *Y=(image.rows-logo.rows) - (image.cols * _margin);
            break;
        case 1:
            *X=image.cols * _margin;
            *Y=(image.rows-logo.rows) - image.cols * _margin;
            break;
        case 2:
            *X=image.cols * _margin;
            *Y=image.cols * _margin;
            break;
        case 3:
            *X=(image.cols-logo.cols) - (image.cols * _margin);
            *Y=image.cols * _margin;
            break;
        default:
            *X=(image.cols-logo.cols) - (image.cols * _margin);
            *Y=(image.rows-logo.rows) - (image.cols * _margin);
            break;
    };
    return;
}

void markIt(const char *srcpic, const char *logopic, const char *dstpic, int position=0){
    Mat image = imread(srcpic);
    Mat logo = imread(logopic);
    Mat imageROI;
    int markx,marky;

    Mat mask=imread(logopic,0);

    if (_scale < 1){
        float scale=(image.cols * _scale) / logo.cols;
        Size dsize=Size(logo.cols*scale,logo.rows*scale);
        resize(logo,logo,dsize);
        resize(mask,mask,dsize);
    } else if(_scale > 1) {
        int neww,newh;
        neww = (int)_scale;
        newh = (int)(logo.rows * ((float)neww / logo.cols));
        Size dsize=Size(neww,newh);
        resize(logo,logo,dsize);
        resize(mask,mask,dsize);
    };
logo.rows);

    getPosition(position,image,logo,&markx,&marky);
    imageROI = image(Rect(markx,marky,logo.cols,logo.rows));
    if (_copy){
        logo.copyTo(imageROI,mask);
    } else {
        addWeighted(imageROI, 1.0, logo, _alpha, 0, imageROI);
    }
    imwrite(dstpic,image);
}

int main(int argc, char **argv){
    getOptions(argc,argv);
    dumpDefault();

    markIt(_srcFilename,_logoFilename,_dstFilename,_position);
    return 0;
}

从完成的程序代码上看一样也是如此,大量的代码都是用于处理参数和默认值逻辑,实际加水印的代码,几乎没有什么变化。

技术人员不能只沉迷于技术,技术人员的升职加薪,每每得益于其它经验的积累,好比行业经验,好比沟通协调经验。

假设咱们当前目录准备了一张图片叫DSCF2183.jpg:

而且准备两个logo水印文件,一张logo.png是刚才的黑白图片,另一张logo1.png是红字黑底的图片:

咱们把第三版的程序编译一下,而后作几个测试,

$ ./mkcv4.sh wmv3
$ ./wmv3 -i DSCF2183.jpg
input:DSCF2183.jpg
out:DSCF2183_logoed.jpg
logo:./logo.png
scale:0.300000
postion:0
copy:0
$

这是最简的运行模式,只须要一个输入文件。水印文件自动缩放到目标图片宽度的30%,而后透明叠加在右下角:

简单使用-c参数,能够用覆盖的方式叠加水印:

$ ./wmv3 -i DSCF2183.jpg -c
input:DSCF2183.jpg
out:DSCF2183_logoed.jpg
logo:./logo.png
scale:0.300000
postion:0
copy:1


更换第二幅水印logo来试试:

$ ./wmv3 -i DSCF2183.jpg --logo logo1.png -o DSCF2183_red.jpg
input:DSCF2183.jpg
out:DSCF2183_red.jpg
logo:logo1.png
scale:0.300000
postion:0
copy:0
$ ./wmv3 -i DSCF2183.jpg --logo logo1.png -o DSCF2183_red_copy.jpg -c
input:DSCF2183.jpg
out:DSCF2183_red_copy.jpg
logo:logo1.png
scale:0.300000
postion:0
copy:1


补充

做为一个命令行程序,第三版已经基本能够知足应用见用户了。
回到最初的话题,若是是本身做为这个用户,那还有一个小需求没有被知足。那就是,个人图片量很大,而且分布在多篇游记的复杂目录结构中。如何同时为多幅图片添加水印?
这算的上很是个性化的需求,固然能够实如今程序中。但在没有大量用户支持的状况下,这种需求可能只是增长了程序的复杂度,但并无多少人用。
对于这种需求,彻底可使用外围脚本的形式来解决。使用bash写这样的脚本,也不过几行代码而已:

#!/bin/bash

files=$(find $1 -name "*jpg" -o -name "*png" -o -name "*jpeg")

for file in $files
do
    wmv3 -i $file -o $file
done

把脚本设置为可执行,而后把脚本和主程序都拷贝到系统的可执行文件夹:

$ chmod +x markall.sh
$ sudo cp markall.sh /usr/bin
$ sudo cp wmv3 /usr/bin

此次为再多的图片加水印也不怕了,好比咱们有一个测试文件夹,是这样的结构:

只要如此执行就能够为文件夹下面,及其子文件夹中全部的jpg/jpeg/png文件添加水印:

$ markall.sh test

至此,才能够真的完活,收工!

相关文章
相关标签/搜索