从0开始的图形学大师
标签(空格分隔):图形学
peter Shirley
<计算机图形学>
这篇blog是看《计算机图形学》(绿皮书)的笔记,博主个人保存。
第1章 引言
1.1 图形学领域
- 建模。对形状和外观性质进行数学定义,而且能够存储在计算机中。例如:一只咖啡杯可以表示为有序的三维点集,这些三维点按一定的插值规则连接起来,还可以建立咖啡杯受光线照射的反射模型。
- 绘制。该术语来自艺术领域,根据三维计算机模型生成带阴影的图像。
- 动画。大家都知道的,其中用到了建模和绘制。
。。。
1.2 主要应用
- 仿真。
- CAD。
- 游戏/动画。
1.3 图像学API
解释API是什么,简单介绍两种两种API模式
1.4 三维几何模型
三维图像采集有时要借用距离扫描仪(淘宝上有卖),这些距离扫描仪得到的就是三维几何模型,最常用的模型由三维三角形组成,这些三角形共享顶点,常被称作三角形网格。美术设计人员用设计软件也可以生成这样的三维几何模型。
Ps:为何常见的是三维三角形呢?
三角形重心好算,建系可以根据三条边来建,以三条边作为基准向量。
1.5 图形流水线(graphic pipline)
图形流水线是现在计算机都有的一个软硬件子系统。包含很多对矩阵和向量进行高效处理和组合运算的机制。
多数图形流水线的速度都与正在画的三角形的数量有关。因为交互能力要比视觉效果更重要,所以在生成模型的过程中需要将三角形数量减到最少。另外,如果模型出现在不远处,所需要的三角形数量就比模型在近处时少。这就意味着,在表示模型时要采用不同的细节等级(LOD)(原书摘抄)
1.6 数值问题
图像学程序实际是三维数值代码,所以表示数值很重要(这让我想起图书馆的《c语言数值算法》这本书,还没看过)。幸运的是,现代计算机都服从IEEE浮点标准。后面举了个例子说明IEEE的高效。
1.7 效率
效率往往是各种利弊的权衡,往往没有超级规则。
1.8 软件工程(重点)
图形学关键部分都有比较好的几何基本代码。哪些声明为内联函数,哪些用单精度哪些用双精度不清楚,哪些用const,需要包含很多assert宏?这一节是重点。
第2章 数学知识
作为大学生,这些高中知识我们应该都知道了。这里就不讲了。
线性插值:告诉你几个点,用直线连起来。
关于三角形重心要求看懂
第3章 光栅算法
3.1 光栅显像
光栅=像素矩阵
3.2 显示器亮度和γ值
我们显示器显示的信号和我们输入的信号往往不同,于是我们要进行伽马矫正,
3.3 RGB
24位系统 = 1个字节(0~255)* 8位 * 3基色
3.4 α通道
如图
3.5 直线绘制(重点)
画一个点集
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 画一个点集
*/
#include<iostream>
#include<cstdio>
using namespace std;
class Vec
{
public:
int x, y, z;
};
int f(int x, int y)
{
return (2*x-3*y);
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
for(int y = 0; y < h; y++)
{
fprintf(stderr, "\rRendering %5.2f%%", 100.0*y/(h-1));
for(int x = 0; x < w; x++)
{
int i = (h - y - 1) * w + x;
if(f(x, y) == 0)
{
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
else
{
c[i].x = 0;
c[i].y = 0;
c[i].z = 0;
}
}
}
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
3.5.1 基于隐式方程绘制直线
==中点算法==(Pitteway,1967)
重要假设:我们能绘出没有间隔的最细的直线。两对角像素之间的连接被认为不产生间隔。
我们先讨论 0
y = y0
d = f(x0+1, y0+0.5)
for x = x0 to x1 do
draw(x,y)
if d < 0 then
y = y+1;
d = d+(x1-x0)+(y0-y1)
else
d = d+(y0-y1)
这里我们用了增量算法,不必在循环内每次都计算d的值,我们用上一次的d的值通过加法计算d的值,以代替原来算d时的乘法运算。
因为我们可以推导出下面两个公式:
完整代码为
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 中点法画直线
*/
#include<iostream>
#include<cstdio>
using namespace std;
class Vec
{
public:
int x, y, z;
};
class Point2
{
public:
int x, y;
Point2(int x_, int y_){x = x_;y = y_;}
};
double f(double x, double y)
{
return (2*x-3*y+100);
}
void drawline(Point2 p1, Point2 p2, Vec * c, int h, int w)
{
int y = p1.y;
double d = f(p1.x+1, p1.y+0.5);
for(int x = p1.x; x <= p2.x; x++)
{
int i = (h - y - 1) * w + x;
if(d < 0)
{
y = y + 1;
d = d + (p2.x - p1.x) + (p1.y - p2.y);
}
else
d = d + (p1.y - p2.y);
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
for(int y = 0; y < h; y++)
{
fprintf(stderr, "\rRendering %5.2f%%", 100.0*y/(h-1));
for(int x = 0; x < w; x++)
{
int i = (h - y - 1) * w + x;
if(f(x, y) == .0f)
{
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
else
{
c[i].x = 0;
c[i].y = 0;
c[i].z = 0;
}
}
}
Point2 p1(0, 0),p2(1000, 200);
drawline(p1, p2, c, h, w);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
我们用两点式,这样不用写f()函数:
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 中点法画直线
*/
#include<iostream>
#include<cstdio>
using namespace std;
class Vec
{
public:
int x, y, z;
};
class Point2
{
public:
int x, y;
Point2(int x_, int y_){x = x_;y = y_;}
};
void drawline(Point2 p1, Point2 p2, Vec * c, int h, int w)
{
int y0 = p1.y, x0 = p1.x;
int y1 = p2.y, x1 = p2.x;
int y = y0;
double d = (y0-y1)*(x0+1)+(x1-x0)*(y0+0.5)+x0*y1-x1*y0;
for(int x = x0; x <= x1; x++)
{
int i = (h - y - 1) * w + x;
if(d < .0f)
{
y = y + 1;
d = d + (x1 - x0) + (y0 - y1);
}
else
d = d + (y0 - y1);
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
for(int y = 0; y < h; y++)
{
fprintf(stderr, "\rRendering %5.2f%%", 100.0*y/(h-1));
for(int x = 0; x < w; x++)
{
int i = (h - y - 1) * w + x;
if(2*x-3*y == 0)
{
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
else
{
c[i].x = 0;
c[i].y = 0;
c[i].z = 0;
}
}
}
Point2 p1(0, 0),p2(1000, 200);
drawline(p1, p2, c, h, w);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
增量算法会使代码变快,但结果会使误差得以积累。
有时,如果只有整数进行运算,算法会执行得更快。但是我们初始化时用到了 d = f(x0+1, y0+0.5)
于是我们用2f(x,y)代替f(x,y)。
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 中点法画直线
*/
#include<iostream>
#include<cstdio>
using namespace std;
class Vec
{
public:
int x, y, z;
};
class Point2
{
public:
int x, y;
Point2(int x_, int y_){x = x_;y = y_;}
};
void drawline(Point2 p1, Point2 p2, Vec * c, int h, int w)
{
int y0 = p1.y, x0 = p1.x;
int y1 = p2.y, x1 = p2.x;
int y = y0;
int d = 2*(y0-y1)*(x0+1)+(x1-x0)*(2*y0+1)+2*x0*y1-2*x1*y0;
for(int x = x0; x <= x1; x++)
{
int i = (h - y - 1) * w + x;
if(d < 0)
{
y = y + 1;
d = d + 2*(x1 - x0) + 2*(y0 - y1);
}
else
d = d + 2*(y0 - y1);
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
for(int y = 0; y < h; y++)
{
fprintf(stderr, "\rRendering %5.2f%%", 100.0*y/(h-1));
for(int x = 0; x < w; x++)
{
int i = (h - y - 1) * w + x;
if(2*x-3*y == 0)
{
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
else
{
c[i].x = 0;
c[i].y = 0;
c[i].z = 0;
}
}
}
Point2 p1(0, 0),p2(1000, 200);
drawline(p1, p2, c, h, w);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
还有就是我们在运行这段代码时,要检查0
3.5.2 基于参数方程绘制直线
我们考虑-1
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 参数方程画直线
*/
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
class Vec
{
public:
int x, y, z;
};
class Point2
{
public:
int x, y;
Vec color;
Point2(int x_, int y_){x = x_;y = y_;}
};
//double round(double r)
//{
// return (r > 0.0)?floor(r + 0.5):ceil(r - 0.5);
//}
void drawline(Point2 p1, Point2 p2, Vec * c, int h, int w)
{
int y0 = p1.y, x0 = p1.x;
int y1 = p2.y, x1 = p2.x;
double y = y0;
double yy = 1.0*(y1-y0)/(x1-x0);
for(int x = x0; x <= x1; x++)
{
int i = (h - round(y) - 1) * w + x;
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
y = y + yy;
}
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
for(int y = 0; y < h; y++)
{
fprintf(stderr, "\rRendering %5.2f%%", 100.0*y/(h-1));
for(int x = 0; x < w; x++)
{
int i = (h - y - 1) * w + x;
if(2*x-3*y == 0)
{
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
else
{
c[i].x = 0;
c[i].y = 0;
c[i].z = 0;
}
}
}
Point2 p1(0, 0),p2(1000, 200);
drawline(p1, p2, c, h, w);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
我们还可以画一条彩色的渐变线
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 参数方程画直线
*/
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
class Vec
{
public:
int x, y, z;
Vec(int x_ = 0, int y_ = 0, int z_= 0):x(x_),y(y_),z(z_){}
};
class Point2
{
public:
int x, y;
Vec color;
Point2(int x_, int y_, int r, int g, int b):x(x_),y(y_),color(r,g,b){}
};
//double round(double r)
//{
// return (r > 0.0)?floor(r + 0.5):ceil(r - 0.5);
//}
void drawline(Point2 p1, Point2 p2, Vec * c, int h, int w)
{
int y0 = p1.y, x0 = p1.x;
int y1 = p2.y, x1 = p2.x;
int r1 = p2.color.x, g1 = p2.color.y, b1 = p2.color.z;
int r0 = p1.color.x, g0 = p1.color.y, b0 = p1.color.z;
double y = y0, r = r0, g = g0, b = b0;
double yy = 1.0*(y1-y0)/(x1-x0);
double rr = 1.0*(r1-r0)/(x1-x0);
double gg = 1.0*(g1-g0)/(x1-x0);
double bb = 1.0*(b1-b0)/(x1-x0);
for(int x = x0; x <= x1; x++)
{
int i = (h - round(y) - 1) * w + x;
r = r + rr;
g = g + gg;
b = b + bb;
c[i].x = round(r);
c[i].y = round(g);
c[i].z = round(b);
y = y + yy;
}
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
for(int y = 0; y < h; y++)
{
fprintf(stderr, "\rRendering %5.2f%%", 100.0*y/(h-1));
for(int x = 0; x < w; x++)
{
int i = (h - y - 1) * w + x;
if(2*x-3*y == 0)
{
c[i].x = 255;
c[i].y = 255;
c[i].z = 255;
}
else
{
c[i].x = 0;
c[i].y = 0;
c[i].z = 0;
}
}
}
Point2 p1(0, 0, 255, 0, 0),p2(1000, 200, 0, 0, 255);
drawline(p1, p2, c, h, w);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
3.6 三角形光栅化(重点)
我们用重心坐标系可以很清楚的画出一个三角形,而且如果我们画一个最简单的彩色三角形,也可以用顶点颜色(假设为c0,c1,c2)表示:c = αc0+βc1+γc2。这种颜色插值方法为Gouraud插值法(作者Gouraud,1971)。
暴力光栅化伪代码
for all x do
for all y do
compute(α,β,γ)for(x,y)
if(0<=α<=1 and 0<=β<=1 and 0<=γ<=1)then
c = αc0+βc1+γc2
drawpixel(x,y) with color c
我们不用遍历所有像素点,只需要遍历三角形最小包围矩形。
x_min = floor(x0,x1,x2)//floor指求最小
x_max = ceiling(x0,x1,x2)//ceiling指求最大
y_min = floor(y0,y1,y2)
y_max = ceiling(y0,y1,y2)
for y=y_min to y_max do//一般都是从y开始遍历,一行一行来
for x=x_min to x_maxi do
α = f12(x,y)/f12(x0,y0)
β = f20(x,y)/f20(x1,y1)
γ = f01(x,y)/f01(x2,y2)
if(α>0 and β>0 and γ>0)then
c = αc0+βc1+γc2
drawpixel(x,y) with color c
其中fij表示编号为i与j的直线,α>0表示点在(x1,y1)与(x2,y2)形成的这条直线靠近x0的五边形区域。
f01(x,y) = (y0-y1)x+(x1-x0)y+x0y1-x1y0xb
f01(x,y) = (y1-y2)x+(x2-x1)y+x1y2-x2y1
f01(x,y) = (y2-y0)x+(x0-x2)y+x2y0-x0y2
完整代码为:
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 三角形光栅化(画一个彩色三角形)
*/
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int max(int a, int b, int c)
{
if(a >= b && a >= c)
return a;
else if(b >= a && b >= c)
return b;
else
return c;
}
int min(int a, int b, int c)
{
if(a <= b && a <= c)
return a;
else if(b <= a && b <= c)
return b;
else
return c;
}
class Vec
{
public:
int x, y, z;
Vec(int x_ = 0, int y_ = 0, int z_= 0):x(x_),y(y_),z(z_){}
Vec operator+(const Vec &b) const
{ return Vec(x + b.x, y + b.y, z + b.z); }
};
class Point2
{
public:
int x, y;
Vec color;
Point2(int x_, int y_, int r, int g, int b):x(x_),y(y_),color(r,g,b){}
};
//double round(double r)
//{
// return (r > 0.0)?floor(r + 0.5):ceil(r - 0.5);
//}
int fij(int x, int y, int x0, int y0, int x1, int y1)
{
return (y0-y1)*x+(x1-x0)*y+x0*y1-x1*y0;
}
void drawtriangle(Point2 p0, Point2 p1, Point2 p2, Vec *c, int w, int h)
{
int y0 = p0.y, x0 = p0.x;
int y1 = p1.y, x1 = p1.x;
int y2 = p2.y, x2 = p2.x;
int x_min = min(x0, x1, x2);
int x_max = max(x0, x1, x2);
int y_min = min(y0, y1, y2);
int y_max = max(y0, y1, y2);
double a,b,r;
for(int y = y_min; y <= y_max; y++)
{
for(int x = x_min; x <= x_max; x++)
{
int i = (h - y - 1) * w + x;
a = 1.0*fij(x,y,x1,y1,x2,y2)/fij(x0,y0,x1,y1,x2,y2);
b = 1.0*fij(x,y,x2,y2,x0,y0)/fij(x1,y1,x2,y2,x0,y0);
r = 1.0*fij(x,y,x0,y0,x1,y1)/fij(x2,y2,x0,y0,x1,y1);
if(a > 0 && b > 0 && r > 0)
{
c[i].x = round(a*p0.color.x + b*p1.color.x + r*p2.color.x);
c[i].y = round(a*p0.color.y + b*p1.color.y + r*p2.color.y);
c[i].z = round(a*p0.color.z + b*p1.color.z + r*p2.color.z);
}
// else
// {
// c[i].x = 0;
// c[i].y = 0;
// c[i].z = 0;
// }
}
}
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
Point2 p0(30, 30, 255, 0, 0),p1(30, 300, 0, 255, 0),p2(300, 30, 0, 0, 255);
drawtriangle(p0, p1, p2, c, w, h);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for(int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
处理三角形边上的像素
我们发现fij(x,y)>0(x,y随便定)和α>0在两个相邻三角形中往往只有一个满足。
于是我们规定在边上的那些像素点必须满足fij(-1,-1)>0才画,伪代码如下:
x_min = floor(x0,x1,x2)//floor指求最小
x_max = ceiling(x0,x1,x2)//ceiling指求最大
y_min = floor(y0,y1,y2)
y_max = ceiling(y0,y1,y2)
for y=y_min to y_max do//一般都是从y开始遍历,一行一行来
for x=x_min to x_maxi do
α = f12(x,y)/f12(x0,y0)
β = f20(x,y)/f20(x1,y1)
γ = f01(x,y)/f01(x2,y2)
if(α>0 and β>0 and γ>0)then
if(α>0 or f12(-1,-1)>0) and (β>0 or f20(-1,-1)>0) and (γ>0 or f01(-1,-1)>0) then
c = αc0+βc1+γc2
drawpixel(x,y) with color c
f01(x,y) = (y0-y1)x+(x1-x0)y+x0y1-x1y0xb
f01(x,y) = (y1-y2)x+(x2-x1)y+x1y2-x2y1
f01(x,y) = (y2-y0)x+(x0-x2)y+x2y0-x0y2
作者指出,这些以上的这些代码还有很多需要改进的,比如如果α是负的,就不用计算β和γ了,对于输入为一条直线的三角形,可能出现除数为0的情况,应采用浮点误差条件。
3.7 简单反走样技术
用盒状滤波即可
/**
* @title 从0开始的图形学大师
* @author hezenggeng
* @code 中点法画直线+不大恰当的均值滤波反走样
*/
#include<iostream>
#include<cstdio>
using namespace std;
class Vec
{
public:
int x, y, z;
Vec(int x_ = 255, int y_ = 255, int z_ = 255):x(x_),y(y_),z(z_){}
};
class Point2
{
public:
int x, y;
Point2(int x_, int y_){x = x_;y = y_;}
};
void drawline(Point2 p1, Point2 p2, Vec * c, int h, int w)
{
int y0 = p1.y, x0 = p1.x;
int y1 = p2.y, x1 = p2.x;
int y = y0;
int d = 2*(y0-y1)*(x0+1)+(x1-x0)*(2*y0+1)+2*x0*y1-2*x1*y0;
Vec *temp = new Vec[w*h];
for(int x = x0; x <= x1; x++)
{
int i = (h - y - 1) * w + x;
if(d < 0)
{
y = y + 1;
d = d + 2*(x1 - x0) + 2*(y0 - y1);
}
else
d = d + 2*(y0 - y1);
temp[i].x = 0;
temp[i].y = 0;
temp[i].z = 0;
}
//反走样的不大恰当的演示,我用九宫格的均值代替中间那个值(均值滤波)
for(y = y0; y <= y1; y++)
{
for(int x = x0; x <= x1; x++)
{
int i00 = (h - y - 2) * w + x-1;
int i01 = (h - y - 2) * w + x;
int i02 = (h - y - 2) * w + x+1;
int i10 = (h - y - 1) * w + x-1;
int i11 = (h - y - 1) * w + x;
int i12 = (h - y - 1) * w + x+1;
int i20 = (h - y) * w + x-1;
int i21 = (h - y) * w + x;
int i22 = (h - y) * w + x+1;
c[i11].x = (temp[i00].x + temp[i01].x + temp[i02].x +
temp[i10].x + temp[i11].x + temp[i12].x +
temp[i20].x + temp[i21].x + temp[i22].x
) / 9;
c[i11].y = c[i11].x;
c[i11].z = c[i11].x;
}
}
free(temp);
}
int main(int argc, char *argv[])
{
int w = 1024, h = 768;
Vec *c = new Vec[w*h];
Point2 p1(10, 10),p2(1000, 200);
drawline(p1, p2, c, h, w);
FILE *f = fopen("hh.ppm", "w");
fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);
for (int i = 0; i < h*w; i++)
{
fprintf(f, "%d %d %d ", c[i].x, c[i].y, c[i].z);
}
free(c);
return 0;
}
3.8 图像捕捉与储存
3.8.1 扫描仪和数码摄像机
看看就好
3.8.2 图像储存
- gif。有损压缩,只能表示256种颜色。适用于自然图像。
- jpeg。有损压缩,以人类视觉系统为阈值。适用于自然图像。
- tiff。无损压缩,每个像素通常占24位(3*8)。
- ppm。无损压缩,每个像素通常占24位(3*8)。
- png。无损格式集合,具有很好的开放源码管理工具。
由于压缩和变形,对于非商业应用,如果没有读/写库可用的话,最好使用原始的ppm。
第4章 信号处理
我们在上一章用相邻的像素点表示一条直线,这叫采样表示法。
这部分需要看看书p49页。讲的不错。
20世纪20年代,通信领域已经用到了采样与重构技术。到20世纪40年代,关于采样与重构理论的论述就和现在所用的形势一样了(摘录)
4.1 数字音频
要接触这类知识,记得日本有一位科普漫画家画一个解释傅里叶变化的漫画,当时女猪脚的设定就是乐队的成员。
4.2 卷积
符号
4.2.1 滑动平均
我们想要知道函数某一段的平均值,需要对该段积分再除以区间长度。
滑动平均可以做滤波,叫 滑动平均滤波
4.2.2 离散卷积
在图形学中,假设a的支撑集有限,(使|j|>r时,都有a[j]=0)此时,上面的求和公式可写为:
理解1:(加权平均)就是b中以i为中心,取b[i+r,i-r]范围(我故意写反的)与a所有值a[-r,r]相乘,得到的变为新的(a*b)[i]。
一般来说,a是滤波器,提供权值,b是信号。
书上伪代码
function convolve(sequence a, sequence b, int r, int i)
s = 0
for j = -r to r
s = s + a[j]b[i-j]
return
卷积滤波器
当非零区间上存在一个常数,我们称之为盒式滤波(不是何氏滤波)。滑动平均公式也可以由盒式滤波得到:
代入上式得滑动平均公式。
卷积性质
卷积的一大优点是:滤波器和信号可以互换。我们用i-k替换j推导得到(推导得到或者通过图形想象):
- (i-k和k和j都是全集R,于是可以替换)
因此,对于任意序列a和b, ,卷积满足交换律
卷积还满足结合律、对加法满足分配率。
一种简单的滤波器:**离散脉冲**d[i] = …,0,0,0,1,0,0,0…
所以d就相当于单位向量或者单位矩阵或者单位元,运算时可以任意加。
4.2.3 把卷积看做移位滤波器之和
标题说明这里又产生了卷积的一种新解释:卷积就是b的位移序列之和。
首先定义位移序列:
,这只是一种表示而已,把原来卷积中的
换成
即可。得到新的公式:
其实按照 理解,我们可以写一些不符合数学规范的公式,来更形象的说明公式:
理解2:之前我们是一段a[i+r,i-r]和一段b[-r,r]的乘积得到一个(a*b)[i]。 现在我们的 解释为,先把每一个a[j]与b[i]相乘(两序列相乘)得到第一个(a*b),然后再平移b,得到b[i-1],再相乘每一个a[j]与b[i-1](b平移得到的序列)得到又一个(a*b),重复j次平移相乘,得到j个(a*b),相加所有(a*b)为最终结果。
(ps:)这样我们就有两种算卷积的算法了。
4.2.4 与连续函数的卷积
连续函数卷积用积分代替求和运算。
理解:和卷积的理解1相同,(f*g)(x)就是移动f使f(0)与g(x)对应后,两函数之积形成的曲线下面的面积。
连续函数的卷积也满足交换律、结合律、对加法满足分配率。同样,连续卷积也可以看做是对移位滤波器求和(理解2)。不同之处在于,在连续的情况下,有无限多的位移滤波器拷贝。
之后书中有一个简单的例题P57可以看一看。
狄拉克
函数(狄拉克脉冲函数)
函数在0处没有确定值,对非零x都有
4.2.5 离散-连续卷积
连续序列变为离散序列:采样即可
离散序列变为连续序列:用连续滤波器对离散序列进行滤波,其中x不是离散的,而i是离散的。
我们用伪代码表示滤波器半径为r的 离散-连续卷积(好久没敲了-。-)
function reconstruct(sequence a, filter f, real x)
s = 0
r = f.radius
for i=|x-r| to |x+r| do
s = s + a[i]f(x-i)
return s
4.2.6 多维卷积
用伪代码表示
function convolve2d(filter2d a, filter2d b, int i, int j)
s = 0;
r = a.radius
for i' = -r to r do
for j' = -r to r do
s = s + a[i'][j']b[i-i'][j-j']
return s
解释:输出样本等于输入面积的加权平均值,其中将二维滤波器作为掩膜,以确定每个样本的权值。
继续推广,可推出二维的连续-连续卷积和离散-连续卷积
4.3 卷积滤波器
下面是图形学常用的特殊滤波器
4.3.1 各种卷积滤波器
- 盒式滤波器 ,(存在跳变,边界要引起重视)
- 帐篷式滤波器 ,(不存在跳变)
- 高斯滤波器 ,(后面还会学,常用来采样)
- 三次B样条滤波器 (导数连续,感觉也很平滑,常用来重构)
- 。。。
为什么高斯滤波很平滑?
4.3.2 滤波器的性质
- 具有负值的滤波器存在振铃
- 重构函数继承滤波器连续性
- 构建可分滤波器最简单就是
比如高斯滤波器(具有可分性和径向对称性)
可分滤波器实现效率高
伪代码:
function filterImage(image I, filter f)
r = f.radius
nx = I.width
ny = I.height
allocate storage array S[0,...,nx-1]
allocate image Iout[r,...,nx-r-1][r,...,ny-r-1]
initialize S and Iout to all zero
for y=r to ny-r-1 do
for x=0 to nx-1 do
S[x]=0
for i=-r to r do
S[x]=S[x]+f[i]I[x][y-i]
for x=r to nx-r-1 do
for i=-r to r do
Iout[x][y]=Iout[x][y]+f[i]S[x-i]
return Iout
4.4 图像信号处理
4.4.1 离散图像滤波
图像模糊化和锐化(给了公式)
产生阴影(给了公式)
opencv3+python3代码实现阴影
import cv2
import numpy as np
img1 = cv2.imread("../images/favorite/luoxiaohei.jpg")
img2 = cv2.imread("../images/favorite/luoxiaohei.jpg")
# kernel = np.array([(0, 0, 0, 0, 0),
# (0, 0, 0, 0, 0),
# (0, 0, 0, 0, 0),
# (0, 0, 0, 1, 0),
# (0, 0, 0, 0, 0)])
kernel = np.zeros((205,205), np.float32)
kernel[100][85] = 1
img1 = cv2.filter2D(img1, -1, kernel)
img1 = cv2.GaussianBlur(img1, (205,205), 0)
# cv2.imshow("blur", img1)
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols ]
img2gray = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(img2gray, 175, 255, cv2.THRESH_BINARY)
# cv2.imshow("mask",mask)
mask_inv = cv2.bitwise_not(mask)
# cv2.imshow("mask_inv", mask_inv)
img1_bg = cv2.bitwise_and(roi,roi,mask = mask)
# cv2.imshow("img1_bg", img1_bg)
img2_fg = cv2.bitwise_and(img2,img2,mask = mask_inv)
# cv2.imshow("img2_fg", img2_fg)
dst = cv2.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst
cv2.imshow("img1", img1)
cv2.waitKey(20000)
cv2.destroyAllWindows()
等以后再写一下c++的
4.4.2 图像采样中的反走样技术
我们在采样中会遇到莫尔图案。因此要用平滑滤波进行处理。
4.4.3 重构与重采样
重采样=重构+采样
我们改变图像大小时,可以用重构来实现重采样,就类似电视或者bilibili改变屏幕大小却不改变内容。
无论放大还是缩小,我们把它看做是像素密度的变化,而非尺寸的变化。(就像化学中相同容积,气体的分子数减小一样)这样我们就可以想出一个方法来求得输出图像的像素值:取图像中与样本点最近的值作为输出值。而这个方法与用一个像素宽的盒式滤波器(假设函数为f)重构图像然后点采样一样。(重构-重采样过程)
我们考虑一维的情况。
function resample(sequence a, float x0, float xx, int n, filter f)
create sequence b of length n
for i=0 to n-1 do
b[i]=reconstruct(a,f,x0+i*xx)
return b
我们上一节讲过,采样前要进行平滑滤波,假设函数为g。
于是我们把结果表示为a*f*g,我们把f和g先运算了,(f*g)叫做重采样滤波器
在图像的边缘处,这种简单的处理会超过序列的范围,有三种方案:
- 在序列末尾停止循环,这等于在图像所有边缘上补0
- 修改序列末尾可以访问,如a[-1] = a[0]
- 当接近边缘时修改滤波器,使之不会超过序列范围
第一种方法会产生模糊边缘,第二种方法容易实现,第三种方法最好。在图像边缘处,修改滤波器最好的方法是对滤波器重新规范化。
4.5 采样理论
4.5.1 傅里叶变换
看不懂,呜呜呜