Giter Club home page Giter Club logo

blog's Introduction

blog's People

Contributors

zuluoaaa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

浅谈图像处理

关于图像处理

前文,前几个月接触了不少图像处理的内容,也做了一些相关东西,最近复习了一下图像处理的一些基础知识,做一些记录并分享。如有错误,望指出。

image

什么是图像?

从计算机的角度看,使用二进制流所表示离散值的集合

在编程语言内的展示形式:base64,binary stream,uint8array,matrix...

先缩小到10x10

image

再放大30倍

image

图像背后的数据

image

一个非常重要的概念:图像即矩阵,图形处理即矩阵运算

output = f(inputImg)

输入一张图像(intput),进行处理,得到output(输出可能是图像,也可能是特征、其他自定义数据)

在这其中,函数f所做的事情便是图像处理,处理包括增强,分类,识别,切割,边缘计算,重建……

关于像素

主流使用的是0~255,即一字节(8比特),双字节在高质量图像保存也经常被使用,一个经典的图片压缩策略便是将双字节图像压缩成单字节,绝大部分显示设备的颜色范围都不会超出单字节所能表示的精度。

关于灰度

再讲灰度

我们目前看到的大部分图像,都是基于三通道(rgb)或四通道(rgba),即彩色图像。

[255,0,0] 三通道的红颜色,我们看到的就是字面意义上的红

image
image

我们把单通道(只有rgb中的一种)(从黑到白的单色)图像称之灰度图,

[255] 单通道红的灰度图,我们能看到的是白色

image
image

RGB转灰度图

  • gray = R | G | B (只取RGB内的任意一个通道)
  • gray = (R+G+B)/3 (平均)
  • gray = R * 0.4+ G * 0.3+B * 0.2 (按比例)

二值化

设定一个临界值,大于这个值设为最大值,小于这个值(255),设为最小值(0)

放大/缩小/旋转

插值算法

  • 最近邻内插法

将放大/缩小的坐标,换算到百分比,映射到原始坐标上,完成插值(这种方法最简单,但是效果极差,不推荐使用)

function f(img,s) {
    let h = img.length;
    let w = img[0].length;
    
    let dh = h * s;
    let dw = w * s;
    
    let dis = [];

    for(let r=0;r<dh;r++){
        let row = [];
        for(let c=0;c<dw;c++){
            let u = c/dw;
            let v = r/dh;

            let u1 = u * w;
            let v1 = v * h;

            let u2 = parseInt(u1);
            let v2 = parseInt(v1);

            let node = img[v2][u2];

            row.push(node)
        }
        dis.push(row);
    }
    console.log(dis)
}
f([[0,50],
    [150,200]],2)
0 50
150 200

放大两倍

0 0 50 50
0 0 50 50
150 150 200 200
150 150 200 200

image

  • 双线性内插法

使用4个最近邻点进行求值拟合

f(i+u,j+v) = (1-u)(1-v)f(i,j) + (1-u)vf(i,j+1) + u(1-v)f(i+1,j) + uvf(i+1,j+1)

function f(img,s) {
    let h = img.length;
    let w = img[0].length;

    let dh = h * s;
    let dw = w * s;


    let dis = [];

    for(let r=0;r<dh;r++){
        let row = [];
        for(let c=0;c<dw;c++){
            let u = c/dw;
            let v = r/dh;

            let u1 = u * w;
            let v1 = v * h;

            let u2 = parseInt(u1);
            let v2 = parseInt(v1);

            v1 = v1 - v2;
            u1 = u1 - u2;


            let bottom = u2+1 < h  ? u2+1 : u2;
            let right = v2+1 < w  ? v2+1 : v2;

            let leftTop = img[v2][u2];
            let leftBottom = img[v2][bottom];
            let rightTop = img[right][u2];
            let rightBottom = img[right][bottom];


            let val = leftTop* (1-v1) * (1-u1) + leftBottom * u1 * (1-v1) + rightTop * v1 * (1-u1) + rightBottom  * v1 * u1;
            row.push(val)
        }
        dis.push(row);
    }
    console.log(dis)
}
f([[0,50],
    [150,200]],2)
0 50
150 200

放大两倍

0 25 50 50
75 100 125 125
150 175 200 200
150 175 200 200

image

  • 双三次内插法

原理同双线性内插是一样的,只不过它包括了16个最近邻点,计算量增加,效果强于双线性内插。

image

平移/旋转

平移

预设偏移量 D ( D是一个矩阵,例如[10,15],就是向下移动10,向右移动15)
output[X,Y] = Input[X,Y] + D

旋转
预设旋转角度 R,再预设两个矩阵,调整原点矩阵O和旋转矩阵R、还原原点矩阵D

output[X,Y] = Input[X,Y] * O * R * D
function rotate90(imgVer) {
    let dist = [];
    let angle = 90;

    let ih = imgVer.length;
    let iw = imgVer[0].length;

    let ow = ih;
    let oh = iw;
    
    let rad = angle * Math.PI/180;
    let rotateVer = [
        Math.cos(rad),-Math.sin(rad),0,
        Math.sin(rad),Math.cos(rad),0,
        0,0,1
    ];
    let ori = [
        1,0,0,
        0,-1,0,
        -0.5*(iw-1),0.5*(ih-1),1
    ];

    let dec = [
        1,0,0,
        0,-1,0,
        0.5*(iw-1),0.5*(ih-1),1
    ];

    for(let r=0;r<oh;r++){
        dist.push([]);
    }

    for(let r=0;r<ih;r++){
        for(let c=0;c<iw;c++){

            let result = verMul([r,c,1],ori);
            result = verMul(result,rotateVer);
            result = verMul(result,dec);
            
            let x = Math.round(result[0]);
            let y = Math.round(result[1]);

            dist[x][y] =  imgVer[r][c]
        }
    }
    return dist;
}

function verMul(a,b){
    let r1= a[0]*b[0] + a[1]*b[3] + a[2]*b[6];
    let r2= a[0]*b[1] + a[1]*b[4] + a[2]*b[7];
    let r3= a[0]*b[2] + a[1]*b[5] + a[2]*b[8];


    return [(r1),(r2),(r3)];
}

console.log(rotate90([
    [1,2,3],
    [4,5,6],
    [7,8,9],
],90))
1 2 3
4 5 6
7 8 9
旋转90度之后
3 6 9
2 5 8
1 4 7

两张图片如何叠加?

假设宽高一致的情况

不透明度图片叠加
targetImg = overlayImg
透明图片叠加
预设透明度T(介于0~1),
targetImg = originImg * (1-T) + T * overlayImg 

如果宽高位置不一致,需先计算宽高和叠加位置

如何提高/降低亮度?

预设亮度值V(通常介于-150~150)
targetImg = originImg + V

对比度是怎么实现的?

当我们在说增强对比度的时候,本质上就是让亮的更亮,暗的更暗,降低对比度则相反。

自动化对比度实现
先采样图像内的最高像素值Max和最低像素值Min(通常是取前5%最高/最低像素值的平均数,防止结果与预期产生偏差)

预设对比系数K, k = 255/(Max-Min)
targetImg = k(inputImg-Min)

滤波(均值/中值/锐化/高斯/双边/...)

绝大部分滤波都是基于滤波器模版(卷积核),这个模版一般是基于3x3 或 5x5 或 7x7... (必须是奇数)

均值/中值滤波
1 1 1
1 2 1
1 1 1

output[x,y] = (
i(x,y)+i(x-1,y-1)+i(x-1,y)+i(x,y-1)+i(x+1,y)+i(x,y+1)+i(x+1,y+1)+i(x-1,y+1)+i(x+1,y-1)
) / 9

锐化滤波
-1 -1 -1
-1 8 -1
-1 1 -1

output[x,y] = 8i(x,y)-(
i(x-1,y-1)+i(x-1,y)+i(x,y-1)+i(x+1,y)+i(x,y+1)+i(x+1,y+1)+i(x-1,y+1)+i(x+1,y-1)
)

高斯滤波
0.094 0.118 0.094
0.118 0.148 0.118
0.094 0.118 0.094
function gaussian(img){
    let ver = [
        -1,-1,-1,0,-1,1,
        0,-1,0,0,0,1,
        1,-1,1,0,1,1
    ];
    let ver2 = [
        0.094,0.118,0.094,
        0.118,0.148,0.118,
        0.094,0.118,0.094,
    ];

    let out = img.copy();

    let rows = img.rows;
    let cols = img.cols;

    for(let r=0;r<rows;r++){
        for(let c=0;c<cols;c++){
            let r1,g,b;
            r1 = g = b = 0;

            let num = 0;
            for(let i=0;i<ver.length;i+=2){
                let x = r+ver[i];
                let y = c+ver[i+1];

                if(x < 0){
                    x = 0
                }else if(x >= rows){
                    x = rows - 1;
                }
                if(y<0){
                    y = 0;
                }else if(y >= cols){
                    y = cols-1;
                }
                let rgb = img.at(x,y);


                r1 += rgb.x * ver2[num];
                g += rgb.y * ver2[num];
                b += rgb.z * ver2[num];

                num++;
            }

            out.set(r,c,[r1,g,b])
        }
    }
    return out;
}
双边滤波

前面介绍的两种滤波在平滑图像的过程中也带来了一个问题,图像平滑的同时,图像边缘也被平滑了;

双边滤波即可平滑图像,也可保留边缘,在像素波动不大的地方进行滤波的时候,效果跟高斯滤波效果是一样的,而到了像素波动起伏大的时候,才是它真正起作用的地方。

空间权重:

0.094 0.118 0.094
0.118 0.148 0.118
0.094 0.118 0.094

像素权重:

像素值越接近,权重越高;颜色差距越大,权重越低,低到忽略不计,

举个例子:假设存在下列这样一张图
1 1 100
1 1 100
1 1 100

我们求 XY=[1,1]位置的输出值,根据高斯模糊计算(只计算空间距离权重

output[1,1] = 1 * 0.094 +  1 * 0.118 + 100 * 0.094 +  1 * 0.118 +  1 * 0.148 + 100 * 0.118 +  1 * 0.094 +  1 * 0.118 + 100 * 0.094 = 31.29

加上一个像素距离权重之后就变成

output[1,1] = 1 * 0.094 * 0.96 +  1 * 0.118  * 0.96 + 100 * 0.094  * 0.02 +  1 * 0.118 * 0.96 +  1 * 0.148 * 0.96 + 100 * 0.118 * 0.02 +  1 * 0.094 * 0.96 +  1 * 0.118 * 0.96 + 100 * 0.094  * 0.02 = 1.27

边缘检测

先进行图像缩放,高斯滤波降噪,图像二值化,然后进行BFC或DFC(广度搜索或深度搜索的图搜索)

image

图像匹配/分类/识别/重建

目前这类工作基本上已经完全被机器学习所占领了,越来越多的图像处理工作迁移到了机器学习上,想接触图像处理的话,这方面是必然会遇到的。

关于机器学习原理和机制我也不是太懂,但是目前市面上存在着非常多的开源轮子,理论上是可以开箱即用,所以不懂原理并不影响我们去使用深度学习框架训练我们自己的模型。

在可以简单谈谈我所理解的,如何基本去使用这些框架轮子:

先了解最基本的概念

绝大部分机器学习都是基于监督式学习,除了监督式学习还有半监督式,无监督学习。

所谓监督式学习即我们需要先标注好数据;

半监督即只给部分数据打标注;

无监督即只输入数据并进行标注,无监督学习一般适用于GAN(生成式对抗网络);

一般上手步骤:

例如做一个目标检测模型:

  • 准备图像数据集(几十张到几万张,越多越好)
  • 标注图像内要识别的事物,保存为XML
  • 寻找合适的机器学习算法框架
  • 下载下来框架,安装相关依赖
  • 开始小范围训练后开始大范围训练
  • 训练完毕,检查模型训练准确度
  • 准确度满足要求,部署模型上线

如果你想亲自上手一把机器学习,搞练丹,网络到处充斥着各类机器学习教程,不过最好去看深度学习框架官网的提供的教程,tensorflow目前是最流行的机器学习框架,想上手可以尝试接触keras(对tensorflow的再封装)

以我短暂的接触经验来看,通常大部分人(比如我)都会在google、github上搜索关键字,查找论文实现,文章教程,开源轮子。然后大概看一下轮子是否能够满足自己的需求,如果是论文再去找找是否有代码复现,

最好再看看是否有人复现了论文/算法,感觉可行,把代码down下来。

尝试代码能否运行,运行了结果是否可复现,

结果复现了训练效果不好又是为什么,是数据集太小,还是初始化参数需要调整,还是数据集存在问题(比如标注偏差),还是训练次数不够,还是存在过拟合,还是作者训练的几十次只把最好的一次贴了出来,或者干脆就是胡编乱造,算法根本没有实际应用的价值……

所以选择已经被人多次复现的框架进行使用是最好不过了

PS:以上仅限于使用,只是作为业余爱好者的尝试,无法让你真正的懂得或理解机器学习,也不能让你有能力去修改调优框架能力。

如果真的是感兴趣,应该去接触一些真正的机器学习课程和书籍,跟着课程书籍上面的学习步骤进行。

基于经过时间的帧渲染

最近在看游戏引擎架构这本书,记录一些书中知识点。

很多人在刚开始学GUI编程的时候,都会在不借助框架或其他外部工具的情况下,实现一些简单或复杂动画效果。这些动画一般都是基于一个whilte(true)、setInterval 或一些专门用来做帧渲染的方法(例如requestanimationframe)。

这是一段简单的动画伪代码,每次渲染的时候去移动对象的位置。

let obj = { x:0, y:0, };//初始化一个对象
`

let fn = ()=>{
    //每次渲染的时候去修改对象的坐标位置
    ++obj.x;
    ++obj.y;

    //渲染该对象
    render();

    requestAnimationFrame(fn);
}
requestAnimationFrame(fn)`

据我观察,有不少人(比如我:D),都写过类似的代码,这段代码里有个很常见的错误。那就是直接在循环方法里,算出一个固定值(如果是加速或缓速,这个固定值每次都会递增或递减),每次都直接加上这个固定值,直接进行位移操作。

这个错误导致的后果便是,对象的动画效果,完全是基于当前CPU的效率,如果CPU足够空闲的话,看上去就会一切正常。如果这时CPU忙碌或者其他导致原因(导致代码执行效率下降的,上面的代码就会开始出错,对象的移动速度开始变慢。最终导致的后果就是,这个动画效果,在不同的平台或机器上显示,物体的移动速度不一致。

解决的方法也很简单,那么就是改成基于经过时间的更新。

举个例子说明-

假设:
有个动画:对象X,从A点移动到B点,耗时N秒,移动M像素。
绘制频率不可控(大于16帧,小于60帧),要保证在N秒内准时移动到B点,保证每帧的绘制准确;

新建一个变量L,保存相对于当前帧的前5帧耗费时间的平均时间;(因为每帧的执行时间都可能是不一样的,每次都通过前几次绘制的平均时间来预测下一次的执行时间会比较准确)

获取每帧的消耗时间:在每帧绘制执行的时候,before:获取当前时间,after:获取当前时间,两者相减为该帧的消耗时间;

绘制帧率K = 1000/L

每帧移动的像素距离J = M/K

一开始X的当前位置是A,
每帧执行的时候,X的当前位置 += J

这里就不贴具体的实现代码了,大家可以自行写一下:D

编译原理:如何从0写一个js解释器

前段时间写了一个js解释器,到目前已经已经把除了对象以外的JS大部分基础功能写出来了,在此分享和回顾一下重点技术细节和实现。
https://github.com/zuluoaaa/makeJs

0 初始化

我们输入一串有意义的js字符串

1 词法分析

遍历循环输入,将字符串逐个解释成有意义的数据结构(这里用token代表这个数据结构

var a = 1

上面这行代码,会被解析成以下4个token

[{token:"var"},{token:"indent",val:"a"},{token:"assign"},{token:"number",val:1}]

对于上述的输入,将字符串转换成token数组很简单,我们只要去逐个读取输入串的值并跳过其中的空格就可以导出这个值。

但是,这只是一个开始,你可能还需要处理一些不一样的输入,必须要求你读完某个部分的值之后才能判断这个值是什么token

例如:区分== 和 = , > 和 >= ……

解决办法也很简单,直接贴实现代码

            case "<":
             next = nextChar();
            if(next === "="){
                token.type = tokenTypes.T_LE;//判断为 <=
            }else {
                token.type = tokenTypes.T_LT;//判断为 <
                putBack(next);
            }

PS:在这一步中,我们不关心语法和语义是否正确,我们只负责解析成token,是关键字就解析成对应的关键字token;如果是数字就解析成数字token,是字母我们就解析成变量token。
然后由后续的程序来处理这些问题。

2 语法分析

将token转换成AST (语法树),也就是将拿到的一组token转换成整体连接的语法结构

这一步就是整个解释器的重点了。

先举个例子

var a = 1 + 3 * 2;

所对应的语法树

       =
      /  \
     +    a
    / \
    *  1
  /  \
 3    2

之所以要把变量A放在右侧,而不是左侧,是因为我们后续会通过前序遍历来解析执行AST,这样便于求值。

上面只是一个简单的例子,这一步骤最复杂的部分就是处理各种关键字 if,else ,function,while……

处理复杂的表达式也是令人头大,各种运算符、例如 &&,>,<=,+,-,*,/,?:……

举个例子

let a = 8+6*3-2*5 > 12*3+(1+5) ? 1 : 2;

将以上的表达式解析到对应的AST,不熟悉解释器和编译器的同学(比如之前的我),往往是卡在这一步,不知所措,陷入自我怀疑...

由于这里面处理的细节处理太多了,这里只将核心架构来告诉大家,具体实现的话,大家有兴趣可以到我项目代码里面进行翻看

负责解析的主方法的核心实现

function statement(){
    let tree = null,left = null;
    while (true){
        let {token}  = gData;
        //不同关键字,跳转到对应的解析函数
        switch (token.type) {
            case tokenTypes.T_VAR:
                left = varDeclaration();
                break;
            case tokenTypes.T_IF:
                left = ifStatement();
                break;
            case tokenTypes.T_WHILE:
                left = whileStatement();
                break;
            case tokenTypes.T_FUN:
                left = funStatement();
                break;
            case tokenTypes.T_RETURN:
                left = returnStatement();
                break;
            case tokenTypes.T_EOF://EOF是整个输入串已经执行完毕了,退出解析
                return tree;
            default:
                 left = normalStatement();
        }
        //基本上每次循环只解析一行语句,这里是将多行语句组合起来,最后将整个输入串组装成一棵语法树
        if(left !== null){
            if(tree === null){
                tree = left;
            }else{
                tree = new ASTNode().initTwoNode(ASTNodeTypes.T_GLUE,tree,left,null);
            }
        }
    }
}

function normalStatement() {
    let tree =  parseExpression(0);//执行表达式解析,得到语法树
    semicolon();//检查逗号
    return tree;
}

...

上述是语法解析,下面是最最核心的表达式解析(例如解析算术表达式 1+3*(6+1)

首先定义一组前缀解析和一组中缀解析的map,根据类型自动到对应的解析方法,这样的话,我们有任何新增要解析的符号,直接往里面新增就可以了,而不需要改动函数内部的实现

const prefixParserMap = {
    [tokenTypes.T_IDENT]:identifier,//变量
    [tokenTypes.T_INT]:int,
    [tokenTypes.T_STRING]:str,
    [tokenTypes.T_LPT]:group,//括号
    [tokenTypes.T_LMBR]:array,//中括号
    [tokenTypes.T_ADD]:prefix.bind(null,tokenTypes.T_ADD),
    [tokenTypes.T_SUB]:prefix.bind(null,tokenTypes.T_SUB),
};

const infixParserMap = {
    [tokenTypes.T_LPT]:{parser:funCall,precedence:precedenceList.call},
    [tokenTypes.T_QST]:{parser:condition,precedence:precedenceList.condition},//三元表达式

    [tokenTypes.T_ASSIGN]:{parser:assign,precedence:precedenceList.assign},//= 赋值符

    [tokenTypes.T_AND]:{parser:infix.bind(null,precedenceList.and),precedence:precedenceList.and},
    [tokenTypes.T_OR]:{parser:infix.bind(null,precedenceList.and),precedence:precedenceList.and},
    [tokenTypes.T_ADD]:{parser:infix.bind(null,precedenceList.sum),precedence:precedenceList.sum},
    [tokenTypes.T_SUB]:{parser:infix.bind(null,precedenceList.sum),precedence:precedenceList.sum},

    [tokenTypes.T_GT]:{parser:infix.bind(null,precedenceList.compare),precedence:precedenceList.compare},
    [tokenTypes.T_GE]:{parser:infix.bind(null,precedenceList.compare),precedence:precedenceList.compare},
    ...
};

表达式解析核心实现,使用普拉特分析法(也是递归下降分析法的一种

function parseExpression(precedenceValue) {
    let {token} = gData;

    //获取当前token对应的前缀解析函数
    let prefixParser = prefixParserMap[token.type];

    if(!prefixParser){
        errPrint(`unknown token : ${token.value}${token.type})`)
    }

    let left = prefixParser();//执行解析函数
    scan();
    if(token.type === tokenTypes.T_SEMI
        || token.type === tokenTypes.T_RPT
        || token.type === tokenTypes.T_EOF
        || token.type === tokenTypes.T_COMMA
        || token.type === tokenTypes.T_COL
        || token.type === tokenTypes.T_RMBR
    ){
        return left;
    }
    let value = getPrecedence();//获取当前运算符的优先级
    while (value>precedenceValue){
// 如果当前运算符的优先大于之前的优先级,就继续向下解析
// 例如1+6*7,很明显 * 的优先级是大于 + 的,所以我们先解析 6 * 7再回去解析前面的
        let type = token.type;
        if(token.type === tokenTypes.T_SEMI
            || token.type === tokenTypes.T_RPT
            || token.type === tokenTypes.T_EOF
            || token.type === tokenTypes.T_COMMA
            || token.type === tokenTypes.T_RMBR
        ){
            return left;
        }
        let infix = infixParserMap[type]; 
        scan();
        left = infix.parser(left,type);

        if(infixParserMap[token.type]){
            value = getPrecedence();
        }
    }

    return left;
}

关于普拉特解析法,特别推荐这位大佬写的关于普拉特介绍
https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/

3 解释执行AST

将前一步得到的AST语法树通过前序遍历,挨个节点执行,求值,一个简单的解释器就这么搞定了。

function interpretAST(astNode,result=null,scope){
    ...

    let leftResult,rightResult;
    if(astNode.left){
        leftResult = interpretAST(astNode.left,null,scope);
    }
    if(astNode.right){
        rightResult = interpretAST(astNode.right,leftResult,scope);
    }

    ...

    switch (astNode.op) {
        case ASTNodeTypes.T_VAR:
            scope.add(astNode.value);
            return;
        case ASTNodeTypes.T_INT:
            return astNode.value;
        case ASTNodeTypes.T_STRING:
            return astNode.value;
        case ASTNodeTypes.T_ADD:
            if(rightResult === null || typeof rightResult === "undefined"){
                return leftResult;
            }
            return leftResult + rightResult;
        case ASTNodeTypes.T_SUB:
            if(rightResult === null || typeof rightResult === "undefined"){
                return -leftResult;
            }
            return leftResult - rightResult;
        case ASTNodeTypes.T_MUL:
            return leftResult * rightResult;
        case ASTNodeTypes.T_DIV:
            return leftResult / rightResult;
        case ASTNodeTypes.T_ASSIGN:
            return rightResult;
        case ASTNodeTypes.T_IDENT:
            return findVar(astNode.value,scope);
        case ASTNodeTypes.T_GE:
            return  leftResult >= rightResult;
        case ASTNodeTypes.T_GT:
            return  leftResult > rightResult;
        ...

最后

完毕,有兴趣的同学可以到我的github 仓库上查看完整的实现
https://github.com/zuluoaaa/makeJs

写的可能不是很好,如有错误,请指出。

遇到一个棘手问题的解决过程

概述

本文主要讲的是笔者近期遇到一个难题,从遇上,到尝试各种方案,踩各种坑,最后解决的心路旅程,供作参考借鉴。

前文

我们的产品是一个工具类软件(使用nwjs的桌面应用),用户可以提交自己感兴趣的链接,我们去帮忙采集里面的视频。

最近一个用户常使用的一个网站上的视频加了非常严格的水印(动态位置,从左到右移动的水印,而且还是很多个水印)

类似这样:
demo.jpg

在检查了视频链接无法逆向到无水印视频的链接之后(视频链接是一个UUID),就有了本文。

笔者技能介绍

一个前端,自认为编程基础还行,写过JAVA(crud boy)。

第一步

一开始,毫无疑问,我当然是跟产品说,不行,这个做不了,去水印不现实。不过,链接里面不是有图片么,我们自己把图片合成视频,你觉得这样的降级方案怎么样?
经过一番激烈讨(si)论(bi),产品勉强同意了。但是,需要我在图片上做一些动画效果,例如图片之间的动画图片什么的,不能单纯一张图片静止不动播放几秒。

为了省钱且开发时间赶(线上急着用),我首先尝试了有没有方案能在前端直接生成视频,结果还真的有~

Whammy 这个库能将你的canvas动画转成webp(视频格式的一种)

他的原理其实就是将canvas逐帧保存到webp,最后将一组webp转成webm格式的视频)

我很快就把canvas动画写好了,然后逐帧保存,嗯,就这样搞定了~写的时候还是很easy的

但是嘛~~~缺点就出来了,速度实在是太特么慢了,视频体积也太特么大了。

在当时至少也勉强解决了一点问题,给部署上去了(又不是不能用.jpg)

第二步

前面的方案部署上线之后,我便立刻开始了优化之路,经过一下午的摸索,我发现………………这玩意没的优化空间,撑死就是把canvas动画改成webgl渲染,渲染速度快一点。

但是上面说到的两个缺点跟动画渲染速度没啥关系,把一帧动画转成webp的速度实在是慢到令人发指,最后的webp转成webm的速度也是无法忍受,最最无法忍受的是还极度吃CPU和内存。

所以,我很快就放弃了优化方案,准备重构!既然你JS速度这么慢,C++速度快啊,我用C++写不就行了嘛~自己写一个c++扩展,再打包成nodejs原生模块。

emmm,听起来似乎可行,但是,我没写过C++啊!(只在菜鸟教程上看过C++入门)

自己从0开始写是不太现实的,所以借助了搜索引擎,

先看了看C++通过图片转成视频的方案(从stackoverflow上扒了不少代码),又看了一些nodejs原生模块的教程,又看了FFMPEG,嗯,这个好强大!

一番血和泪,最后把拷贝出来的代码修修改改,使用了freeImg 打开图片,进行图片操作,逐帧编辑操作,最后调用FFMPEG合成MP4导出,

当然了,把这个C++最终编译到nwjs客户端上去执行,也是踩了不少坑。

到这里,还是存在一个致命问题,懂图像处理的同学到这里是不是发现一个问题了,没错,由于我的弱鸡,我没有用到GPU!!!这样重构出来的性能并没有比原来的方案提升太多,还是没有解决问题。

到这里,我已经快熬不住了,功能性能好歹是优化了一些,先上线吧。

第三步

后面其实一直想的是把C++改成调用GPU去实现,但是一直没时间,而且中途有统计过我们用户电脑的配置,大部分用户的电脑都没什么独立显卡……

而且不少用户都不满意这种降级方案的实现……

早在第一步的时候我就跟产品说过视频去水印的可能解决方案,深度学习标记出视频水印位置,然后使用水印原图的差值叠加的方式去水印,嗯,理论上是可行的。但是,我们团队只有一个前端和后端啊,
上哪去给你生出一个会深度学习的!

迫于产品天天给我压力,我开始去了解一些深度学习的方案了,不看不知道一看吓一跳。

似乎好像没有想象中那么难,深度学习框架都已经封装的那么完全了,似乎不懂深度学习的人也可以调用框架来训练自己的模型?

到这里的时候,其实我已经跟产品夸下海口了233333

根据网上的一大堆教程和介绍,最后决定选用tensorflow object detect api,使用官方提供的预训练模型,花了两三天终于把tensorflow-gpu版本下载了下来,
又花了几天跑了官方demo和别人开源训练好的模型,嗯,可行!

期间最痛苦的事情是自己标了几十张图片,太折磨人了。。

公司电脑显卡不太行,我拿了自己的战神笔记本1060跑了好几天,终于!!!!

用opencv调用训练出来的数据模型,识别水印位置,进行修复,保存到一个新视频里,再使用FFMPEG将音频合过来。

圆满解决了问题了。

后续

由于涉及到产品隐私问题,就不提供代码和示例了~

整个过程中,给我帮助最大的是谷歌,github,阅读英文技术文章的能力。遇到问题就谷歌一下(强烈推荐英文搜索!!!),查找解决方案,

想了解/使用一个技术框架/第三方库的最好的方法是上官网看教程,到github看issue,都找不到就看看下源码吧,说不定就有思路了。

不要把自己局限在某个技术范围上,敢想敢做233333

关于字体加密的机制

什么是字体加密

不使用公开的字体,而是自己创建一份私有的字体

一些基础的知识

一个字符串在计算机内是怎么表示的?

答案是数字,用某个数字去代指某个字符串,这个数字即是

我们日常了解的unicode字符集,又或者utf8,gkb……等各种编码。

举个例子,我们用将中文转成unicode字符(比如
“你”这个字符,转换成 unicode 是 “\u4f60”

这是一个16进制字符串,如果我们把它转成10进制,结果是“\u20320” ,再去掉 unicode的统一前缀

“20320”

“你” 在unicode 字符集里面对应的数字是 “20320”

20320的其实是就是索引,

一个简单的理解,即是字符集是一个大数组,数组内的每一项对应一个字符的矢量图形

当我们拿到数字,又明白他属于哪个字符集的,那么就能渲染出正确的字符(常见的乱码错误就是因为字符集不正确导致的)

加密原理

我们自己在现有的字符集里面新增一些项(即是我们的加密字体),传给前端的时候,将加密字体添加到浏览器的当前字符集,然后在渲染位置使用unicode字符进行渲染,这个unicode字符所对应的就是我们刚刚新增的字符集,

解密原理

拿到unicode的同时,一并拿到字符集的新增项,将unicode和这些新增项,一一对应起来,才能拿到真实的字符数据

如果是浏览器,那么很简单,因为字体内容本身是无法加密的(否则浏览器无法识别),而且要明文传给前端。

拿到字体内容,通过opentype等工具,对字体进行解压,拿到一个大数组及内每一项所对应的矢量数据

将矢量数据绘制出来,数据量比较少的话,可以人工肉眼去标记每个矢量图形所对应的字符串;

数据量多的话,将绘制出来的矢量图形,调用第三方文字识别API或者文字特征识别库等等,拿到对应字符;

keep it

tow year ago , I was a rookie developer, emm maybe not a rookie developer . I know nothing of computer developer

at first , I start learning C language and Excel formulas that learning keep months ,when I start leanrning front end ,it makes me study is easy .

after I learning jquery and css tow month , I found a job of front end , this work is easy to me ,I learning native Javascript in the work , a few months later and leave the company as soon .

new work is backend developer , I learning new language for PHP and JAVA ... more than a year later and company close down

I try learning as many things as possible , jquery 、 css 、 native javascript 、php 、java 、webgl 、nodejs 、data structure 、computer network

from easy to hard , its so coooooool .

keep up !

从前序和中序的结果输出还原一颗二叉树

这个例子是在《数据结构》书本上看到的,大概看了一下思路,遂来实现一下。

前序遍历:先遍历根节点,再遍历左节点,最后遍历右节点
中序遍历:先遍历左节点,再遍历中节点,最后遍历右节点

已知一颗二叉树的
前序输出为:12485367
中序输出为:84251637

1、已知前序遍历顺序,得知1是根节点,我们把1拿到中序输出上,得到根节点的左节点输出和右节点输出:8425(左) 1 637(右);
2、通过中序输出的左子树,推出前序输出的左子树和右子树 1 2485(左) 367(右);
3、对前序和中序输出结果的字符串不断切割,重复第一步和第二步,最后还原整棵树 ;

下面是JavaScript的代码实现:

`

function restore(first,mid){
    let root = first[0];
    let tree = {
        value:root
    }
    for(let i =0;i<mid.length;i++){
        if(mid[i] === root){
            let left = mid.substring(0,i);
            let right = mid.substring(i+1);

            let fl = first.substring(1,left.length+1);
            let fr = first.substring(left.length+1);

            tree.left = restore(fl,left);
            tree.right = restore(fr,right);
        }
    }
    return tree;
}

`

使用归一化处理动画同步

归一化就是一种简化复杂运算的处理方法,通过归一化可以表示一系列的浮点数和整数。

在GUI编程中,就可以用归一化处理去做动画同步处理,代码相对来说较难出错。

归一化时间:
一段动画,不论动画的持续时间有多长,我们都可以用0来表示动画的开始,1表示动画的结束。

可能你会认为这样子有什么意义?
当我们有两个动画或更多动画的时候(每个动画代表一个动画片段),每个动画片段的持续时间皆不相同的情况下。
假设我们想把两个持续时间不同的动画以相同的速率执行的时候,动画A的开始时间Ua和动画B的开始时间Ub,一开始都等于0,以相同的速率进行推进,直到等于1。

关于区间单位变换的一个小小小问题

由于本人数学菜的抠脚,在日常工作中遇到的一个关于百分比换算的问题。

最开始想的时候觉得挺简单的,打算写成函数调用的时候,居然无法立刻写出来,经过一番思考后方才写了出来。

代码是写出来了,感觉理不顺,随尝试用文字写出来,便于自己更好的理解。

假设存在一个区间范围数(例如5-15),我们想把这个区间数转换成设定好的区间百分比(例如0.4~0.8)。

最大区间值15转换后变成0.8,最小区间值5转换后变成0.4.

思考过程:

把5 ~ 15的区间数想象成一个直线,要把它映射到另外一条直线上并且一一对应。

第一步,先化简,先把5 ~ 15和0.4 ~ 0.8归零处理,即最大值减去最小值,变成0 ~ 10和0 ~ 0.4;经过这么一步的处理,突然就变得简单起来了

要做的就是把0 ~ 10映射到0 ~ 0.4,那还不简单,获取输入数的当前百分比再乘以0.4就搞定了。。。。最后再加上最小值,整个换算就完成了。。。。。
PS:输入的值也需要进行归零处理

const MAX_PERCENT_VALUE = 0.8;
const MIN_PERCENT_VALUE = 0.4;

const  IS_TRANSPARENCE_MIN = 5;
const  IS_TRANSPARENCE_MAX = 15;

function transform(diffVal) {
    return (diffVal - IS_TRANSPARENCE_MIN)/(IS_TRANSPARENCE_MAX-IS_TRANSPARENCE_MIN) * (MAX_PERCENT_VALUE-MIN_PERCENT_VALUE) + MIN_PERCENT_VALUE
}

console.log(transform(5))  // 0.4
console.log(transform(6))  // 0.44
console.log(transform(7))  // 0.48
console.log(transform(15)) // 0.8

最后总结,其实跟随机数取区间范围是一个原理,但是换了个方式出现我就突然转不出来了,尴尬(─.─|||)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.