zuluoaaa / blog

blog
6 stars 0 forks source link

浅谈图像处理 #8

Open zuluoaaa opened 4 years ago

zuluoaaa commented 4 years ago

关于图像处理

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

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(生成式对抗网络);

一般上手步骤:

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

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

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

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

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

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

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

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

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