Add watermark to the picture - taught farmers how to code new technology into products

Foreword

Watermarking is a commonly used method for the picture claiming it originated.
Usually are written technical articles, the article focuses on the technology itself, often do not need to add a watermark photos, or need to add is not much, wielding artifact PhotoShop be soon.
Before taking advantage of some very hot summer is not the time to go out wandering, he should come back to write about travel, in fact, is for others to use as the Raiders.
Travel will be different, become the subject of the photo, and a large amount. Just a scenic process, more than a dozen deputy photos always inevitable. This time, also with PhotoShop watermarked, of course, is not no, but that is clearly not my other "siege Lion" is a wish.
So we add a watermark to images "product", this project it.

Emergence of a technology may be due to the accumulation, probably because of an accident, probably because the hobby. But the product, always as a "demand" to start.

Watermark file

Add a picture watermark, first you have to have a watermark. Of course, his words casually add watermark is in the picture, but if you want any decent, there was art to help your surgeon could not be better. Now programmers say, every day to work with the team, who have not a few artists to be good friends.
what? you have not? Then you had better pay attention. Now whether it is to do research and development, or do product, a person to conquer the world's era has passed.

In the team, technology is important, communication is even more important. If you can not in every job has its own die-hard brothers, busy life, you can only be a small yard farmers.

In this regard, do not superstition "right" position brings, can play the role of "rights" and "relationship", which was the big difference.

I do have an existing watermark, with amazing ten years. Although it seems the design is already behind the times, but this purely personal thing, like you Jiabu Zhu.

The user needs is the first one, as a programmer, you can say that the user is an outsider, what do not know. However, the number of users to be considered, and you say, does not count.
Of course, if your superior communication skills, to persuade the user, and then when I did.

As a watermark pictures, we must first have the characteristics of "hollow" and. For example, you look at the lower right corner of the map header, only the main part of the watermark appears on the picture. The rest, still photo itself. It looks watermark image is hollow look.
In fact, many standard image formats natively supports hollow, such as GIF images, such as PNG images. In Web page design, engraving pictures do have a lot of usage.
But this is clearly not in our big projects, using standard graphics formats such as the watermark image and is not worth the one hand, user-generated watermark image often requires additional operations increase the workload. On the other hand hollow structure to resolve these pictures in the automatic watermarking program also requires additional work.

Unless the "standardized" itself is one of the user's needs, although standardized otherwise there are many benefits, but the rapid completion of the project is the first goal.

Make a watermark file is the easiest method in PhotoShop, the subject matter of the independent one, then the part of all black background. This must be a real black black, that is three RGB values ​​are all 0. Virtually any color without causing conflicts are possible, such as we often see special effects used in the shooting of Blue Box, Green Box. However, the use of black background is easiest to deal with.

Operating picture in the program, of course, is the most powerful opencv library. For Engineers, get Python to write a script enough. If you are a regular user, can be compiled into an executable file c / c ++ is certainly more preferable.

version 1

Then Whether you are a master of image processing itself, originally familiar with this area. Or search for the experience of others on the Internet, learning other people's programs. In short, soon you will come up with a version, add watermark to the image.

#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();
}

The problem is not complex, open the picture and logo as a watermark, and then read the picture as a background part of the hollow. Then remove the logo hollow section, and then copied to the target picture to complete the work, the main work is only seven lines of code.
The main function uses copyTo , click on the link is opencv official documentation.
compiled opencv require additional parameter gives the header files and libraries on the command line, it is recommended to write a script to compile, which also posted (using the current opencv4 in this example):

#!/bin/bash

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

Use scripts to compile and execute the following command (assuming that the source name wmv1.cpp):

$ ./mkcv4.sh wmv1
$ ./wmv1

This program runs on a picture of a sample, the results obtained results are as follows:

It seems the perfect solution to the needs of users, complete live call it a day ......

And so on, this is our "virtual" of a project, writing articles Well, no point to write down how excuse. But if this is a real project, this time to see clients. Believe me, if a customer saw this program, and will definitely come back a bunch of comments. such as:

  • This is a watermark on it? Watermark should be translucent, which can only be called stickers.
  • Why can only deal with what a mess IMG_20190521_125150.jpg file, I want to change the name of each file in order to deal with it?
  • Why watermark seem so big, is not coordinated with the screen
  • Why watermark only on the upper left corner, I want to place in the lower right corner can you?
  • ......

From the client side to come back, no matter the product manager or sales manager, I suspect that life is estimated to have been learned from the user. So this time they are not very good temper, and then communicate it with the programmer, it is certainly not enough patience. So programmers, on the verge of collapse. How many users reviews, there is reason programmers crazy how many.

  • Users pay, since want to make money from the user, the user say what you have to learn to listen.
  • In fact, users do not know what they want, Jobs said so. However, users will naturally look for flaws.
  • Remember said earlier, a man can not play in the world, because there are a lot of people look for flaws, your product can accommodate more people.

Version 2

No matter how unhappy life always continue, they have to work to promote it.
In fact, most users never look for flaws horrible, horrible thing is that users do not look for flaws, and not pay.
Since there are so user feedback, we solved one by one just fine.
First look at the issue "watermark effect", opencv have a special function addWeighted handle overlapping interaction issues between the two pictures. Simpler to use, and even masks do not need a mask part:

    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

至此,才可以真的完活,收工!

Guess you like

Origin www.cnblogs.com/andrewwang/p/11041117.html