給圖片加水印--手把手教新碼農如何把技術變成產品

前言

加水印是爲圖片聲明版權出處的一種經常使用方法。
日常都是寫技術文章,文章的重點在技術自己,照片每每不須要加水印,或者須要加也很少,祭出神器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

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

相關文章
相關標籤/搜索